This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch feat/explore
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git

commit c0a71fa7f2db2bfe5c78506fff4290d481f6beee
Author: Wu Sheng <[email protected]>
AuthorDate: Tue Jun 23 17:21:18 2026 +0800

    fix(explore): form-left/distribution-right square layout, Esc close, Traces 
header
    
    - Form (Target + conditions) on the left, the distribution scatter a
      square on the right; Run query + resolved toggle move to the top-right
      of the conditions, under a "Traces" header.
    - Fix Esc on Trace inspect: it had no key handler, so the span-detail
      modal couldn't be dismissed. Mirror the per-layer cascade (Esc closes
      the span modal, else the inline detail) via the card's modal-open
      report + a capture-phase keydown listener.
    - Strip the explanatory comments the recent layout passes added.
---
 .../src/features/operate/explore/ExploreView.vue   | 286 +++++++++++----------
 apps/ui/src/layer/traces/LayerTracesView.vue       |  17 +-
 2 files changed, 146 insertions(+), 157 deletions(-)

diff --git a/apps/ui/src/features/operate/explore/ExploreView.vue 
b/apps/ui/src/features/operate/explore/ExploreView.vue
index 15e987b..4421254 100644
--- a/apps/ui/src/features/operate/explore/ExploreView.vue
+++ b/apps/ui/src/features/operate/explore/ExploreView.vue
@@ -29,7 +29,7 @@
   (raw / browser-error) land next on the same spine.
 -->
 <script setup lang="ts">
-import { computed, reactive, ref, watch } from 'vue';
+import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 
'vue';
 import { useI18n } from 'vue-i18n';
 import { bffClient } from '@/api/client';
 import type {
@@ -333,6 +333,23 @@ function changeSelectedTraceId(id: string): void {
   selectedTraceId.value = id;
   embeddedSpans.value = null;
 }
+
+const detailCard = ref<InstanceType<typeof TraceDetailCard> | null>(null);
+const spanModalOpen = ref<boolean>(false);
+function onPageKeyDown(e: KeyboardEvent): void {
+  if (e.key !== 'Escape') return;
+  if (spanModalOpen.value) {
+    detailCard.value?.closeSpanModal();
+    e.preventDefault();
+    e.stopPropagation();
+  } else if (selectedTraceId.value) {
+    closeDetail();
+    e.preventDefault();
+    e.stopPropagation();
+  }
+}
+onMounted(() => window.addEventListener('keydown', onPageKeyDown, true));
+onBeforeUnmount(() => window.removeEventListener('keydown', onPageKeyDown, 
true));
 </script>
 
 <template>
@@ -353,134 +370,131 @@ function changeSelectedTraceId(id: string): void {
         </div>
       </div>
 
-      <div class="iq-target">
-        <div class="iq-target-h">
-          <span>{{ t('Target') }} <small class="dim">{{ t('optional — blank 
queries all services') }}</small></span>
-          <div class="seg sm">
-            <button :class="{ on: entityMode === 'pick' }" @click="entityMode 
= 'pick'">{{ t('Pick') }}</button>
-            <button :class="{ on: entityMode === 'type' }" @click="entityMode 
= 'type'">{{ t('Type') }}</button>
+      <div class="iq-top-strip">
+        <div class="iq-form">
+          <div class="iq-target">
+            <div class="iq-target-h">
+              <span>{{ t('Target') }} <small class="dim">{{ t('optional — 
blank queries all services') }}</small></span>
+              <div class="seg sm">
+                <button :class="{ on: entityMode === 'pick' }" 
@click="entityMode = 'pick'">{{ t('Pick') }}</button>
+                <button :class="{ on: entityMode === 'type' }" 
@click="entityMode = 'type'">{{ t('Type') }}</button>
+              </div>
+              <button v-if="entityMode === 'pick'" class="iq-link" 
:disabled="!pickServiceId" @click="seedTypeFromPick">
+                {{ t('→ edit as text') }}
+              </button>
+            </div>
+
+            <div class="iq-grid" v-if="entityMode === 'pick'">
+              <label class="cf">
+                <span>{{ t('Layer') }}</span>
+                <TypeaheadSelect v-model="pickLayer" :aria-label="t('Layer')" 
:options="layerOptions" :placeholder="t('Any layer')" class="cf-tas" />
+              </label>
+              <label class="cf">
+                <span>{{ t('Service') }}</span>
+                <TypeaheadSelect
+                  v-model="pickServiceId" :aria-label="t('Service')" 
:options="serviceOptions" :disabled="!pickLayer || servicesLoading"
+                  :placeholder="servicesLoading ? t('Reading…') : t('Any 
service')" class="cf-tas"
+                />
+              </label>
+              <label class="cf">
+                <span>{{ t('Instance') }}</span>
+                <TypeaheadSelect v-model="instanceSel" 
:aria-label="t('Instance')" :options="instanceOptions" 
:disabled="!pickServiceId" :placeholder="t('All instances')" class="cf-tas" />
+              </label>
+              <label class="cf">
+                <span>{{ t('Endpoint') }}</span>
+                <TypeaheadSelect v-model="endpointSel" 
:aria-label="t('Endpoint')" :options="endpointOptions" 
:disabled="!pickServiceId" :placeholder="t('All endpoints')" class="cf-tas" />
+              </label>
+            </div>
+
+            <div class="iq-grid" v-else>
+              <label class="cf">
+                <span>{{ t('Service name') }}</span>
+                <input v-model="typeService" class="cf-input mono" type="text" 
:placeholder="t('e.g. agent::checkout')" />
+              </label>
+              <label class="cf cf-chk">
+                <span>{{ t('Real') }}</span>
+                <span class="iq-chk"><input v-model="typeReal" type="checkbox" 
/> <small class="dim">{{ t('off = virtual / peer') }}</small></span>
+              </label>
+              <label class="cf">
+                <span>{{ t('Instance') }}</span>
+                <input v-model="typeInstance" class="cf-input" type="text" 
:placeholder="t('optional')" />
+              </label>
+              <label class="cf">
+                <span>{{ t('Endpoint') }}</span>
+                <input v-model="typeEndpoint" class="cf-input" type="text" 
:placeholder="t('optional')" />
+              </label>
+            </div>
           </div>
-          <button v-if="entityMode === 'pick'" class="iq-link" 
:disabled="!pickServiceId" @click="seedTypeFromPick">
-            {{ t('→ edit as text') }}
-          </button>
-        </div>
-
-        <div class="iq-grid" v-if="entityMode === 'pick'">
-          <label class="cf">
-            <span>{{ t('Layer') }}</span>
-            <TypeaheadSelect v-model="pickLayer" :aria-label="t('Layer')" 
:options="layerOptions" :placeholder="t('Any layer')" class="cf-tas" />
-          </label>
-          <label class="cf">
-            <span>{{ t('Service') }}</span>
-            <TypeaheadSelect
-              v-model="pickServiceId" :aria-label="t('Service')" 
:options="serviceOptions" :disabled="!pickLayer || servicesLoading"
-              :placeholder="servicesLoading ? t('Reading…') : t('Any 
service')" class="cf-tas"
-            />
-          </label>
-          <label class="cf">
-            <span>{{ t('Instance') }}</span>
-            <TypeaheadSelect v-model="instanceSel" :aria-label="t('Instance')" 
:options="instanceOptions" :disabled="!pickServiceId" :placeholder="t('All 
instances')" class="cf-tas" />
-          </label>
-          <label class="cf">
-            <span>{{ t('Endpoint') }}</span>
-            <TypeaheadSelect v-model="endpointSel" :aria-label="t('Endpoint')" 
:options="endpointOptions" :disabled="!pickServiceId" :placeholder="t('All 
endpoints')" class="cf-tas" />
-          </label>
-        </div>
-
-        <div class="iq-grid" v-else>
-          <label class="cf">
-            <span>{{ t('Service name') }}</span>
-            <input v-model="typeService" class="cf-input mono" type="text" 
:placeholder="t('e.g. agent::checkout')" />
-          </label>
-          <label class="cf cf-chk">
-            <span>{{ t('Real') }}</span>
-            <span class="iq-chk"><input v-model="typeReal" type="checkbox" /> 
<small class="dim">{{ t('off = virtual / peer') }}</small></span>
-          </label>
-          <label class="cf">
-            <span>{{ t('Instance') }}</span>
-            <input v-model="typeInstance" class="cf-input" type="text" 
:placeholder="t('optional')" />
-          </label>
-          <label class="cf">
-            <span>{{ t('Endpoint') }}</span>
-            <input v-model="typeEndpoint" class="cf-input" type="text" 
:placeholder="t('optional')" />
-          </label>
-        </div>
-      </div>
 
-      <!-- Top strip: trace conditions (left) + duration distribution
-           (right), mirroring the per-layer Traces tab's `.tr-top-strip`
-           4fr/1fr split. -->
-      <div class="iq-top-strip">
-        <div class="iq-conditions">
-          <div class="iq-grid">
-            <label class="cf">
-              <span>{{ t('Trace ID') }}</span>
-              <input v-model="cond.traceId" class="cf-input mono" type="text" 
:placeholder="t('paste a trace id')" />
-            </label>
-            <label class="cf">
-              <span>{{ t('Status') }}</span>
-              <select v-model="cond.traceState" class="cf-input">
-                <option value="ALL">{{ t('All') }}</option>
-                <option value="SUCCESS">{{ t('Success') }}</option>
-                <option value="ERROR">{{ t('Error') }}</option>
-              </select>
-            </label>
-            <label class="cf">
-              <span>{{ t('Order') }}</span>
-              <select v-model="cond.queryOrder" class="cf-input">
-                <option value="BY_START_TIME">{{ t('Newest') }}</option>
-                <option value="BY_DURATION">{{ t('Slowest') }}</option>
-              </select>
-            </label>
-            <label class="cf">
-              <span>{{ t('Duration (ms)') }}</span>
-              <span class="cf-range">
-                <input v-model="cond.minDuration" class="cf-input 
cf-range-num" type="number" min="0" :placeholder="t('min')" />
-                <span class="cf-range-sep">–</span>
-                <input v-model="cond.maxDuration" class="cf-input 
cf-range-num" type="number" min="0" :placeholder="t('max')" />
-              </span>
-            </label>
-            <label class="cf cf-wide">
-              <span>{{ t('Tags') }}</span>
-              <input v-model="cond.tags" class="cf-input mono" type="text" 
:placeholder="t('http.status_code=500, …')" />
-            </label>
-            <label class="cf" :class="{ 'cf-wide': isCustomRange }">
-              <span>{{ t('Time') }}</span>
-              <template v-if="isCustomRange">
+          <div class="iq-conditions">
+            <div class="iq-conditions-h">
+              <span class="kicker iq-cond-kicker">{{ t('Traces') }}</span>
+              <button v-if="resolved" class="iq-resolved-tog" 
@click="showResolved = !showResolved">
+                {{ showResolved ? '▾' : '▸' }} {{ t('Resolved query') }}
+                <span class="dim">{{ resolved.source }}{{ resolved.backend ? ` 
· ${resolved.backend}` : '' }}</span>
+              </button>
+              <button class="iq-run" :disabled="!canRun || running" 
@click="runQuery">
+                {{ running ? t('Running…') : t('Run query') }}
+              </button>
+            </div>
+            <div class="iq-grid">
+              <label class="cf">
+                <span>{{ t('Trace ID') }}</span>
+                <input v-model="cond.traceId" class="cf-input mono" 
type="text" :placeholder="t('paste a trace id')" />
+              </label>
+              <label class="cf">
+                <span>{{ t('Status') }}</span>
+                <select v-model="cond.traceState" class="cf-input">
+                  <option value="ALL">{{ t('All') }}</option>
+                  <option value="SUCCESS">{{ t('Success') }}</option>
+                  <option value="ERROR">{{ t('Error') }}</option>
+                </select>
+              </label>
+              <label class="cf">
+                <span>{{ t('Order') }}</span>
+                <select v-model="cond.queryOrder" class="cf-input">
+                  <option value="BY_START_TIME">{{ t('Newest') }}</option>
+                  <option value="BY_DURATION">{{ t('Slowest') }}</option>
+                </select>
+              </label>
+              <label class="cf">
+                <span>{{ t('Duration (ms)') }}</span>
                 <span class="cf-range">
-                  <input v-model="customStart" type="datetime-local" 
class="cf-input cf-range-num" />
+                  <input v-model="cond.minDuration" class="cf-input 
cf-range-num" type="number" min="0" :placeholder="t('min')" />
                   <span class="cf-range-sep">–</span>
-                  <input v-model="customEnd" type="datetime-local" 
class="cf-input cf-range-num" />
-                  <button class="iq-range-reset" type="button" :title="t('Back 
to presets')" @click="cond.windowMinutes = 30">×</button>
+                  <input v-model="cond.maxDuration" class="cf-input 
cf-range-num" type="number" min="0" :placeholder="t('max')" />
                 </span>
-              </template>
-              <select v-else v-model.number="cond.windowMinutes" 
class="cf-input">
-                <option v-for="w in WINDOWS" :key="w" :value="w">{{ w < 60 ? 
`${w}m` : `${w / 60}h` }}</option>
-                <option :value="CUSTOM_RANGE_SENTINEL">{{ t('Custom…') 
}}</option>
-              </select>
-            </label>
-            <label class="cf">
-              <span>{{ t('Limit') }}</span>
-              <select v-model.number="cond.limit" class="cf-input">
-                <option v-for="l in LIMITS" :key="l" :value="l">{{ l 
}}</option>
-              </select>
-            </label>
-          </div>
-
-          <div class="iq-run-row">
-            <button class="iq-run" :disabled="!canRun || running" 
@click="runQuery">
-              {{ running ? t('Running…') : t('Run query') }}
-            </button>
-            <button v-if="resolved" class="iq-resolved-tog" 
@click="showResolved = !showResolved">
-              {{ showResolved ? '▾' : '▸' }} {{ t('Resolved query') }}
-              <span class="dim">{{ resolved.source }}{{ resolved.backend ? ` · 
${resolved.backend}` : '' }}</span>
-            </button>
+              </label>
+              <label class="cf cf-wide">
+                <span>{{ t('Tags') }}</span>
+                <input v-model="cond.tags" class="cf-input mono" type="text" 
:placeholder="t('http.status_code=500, …')" />
+              </label>
+              <label class="cf" :class="{ 'cf-wide': isCustomRange }">
+                <span>{{ t('Time') }}</span>
+                <template v-if="isCustomRange">
+                  <span class="cf-range">
+                    <input v-model="customStart" type="datetime-local" 
class="cf-input cf-range-num" />
+                    <span class="cf-range-sep">–</span>
+                    <input v-model="customEnd" type="datetime-local" 
class="cf-input cf-range-num" />
+                    <button class="iq-range-reset" type="button" 
:title="t('Back to presets')" @click="cond.windowMinutes = 30">×</button>
+                  </span>
+                </template>
+                <select v-else v-model.number="cond.windowMinutes" 
class="cf-input">
+                  <option v-for="w in WINDOWS" :key="w" :value="w">{{ w < 60 ? 
`${w}m` : `${w / 60}h` }}</option>
+                  <option :value="CUSTOM_RANGE_SENTINEL">{{ t('Custom…') 
}}</option>
+                </select>
+              </label>
+              <label class="cf">
+                <span>{{ t('Limit') }}</span>
+                <select v-model.number="cond.limit" class="cf-input">
+                  <option v-for="l in LIMITS" :key="l" :value="l">{{ l 
}}</option>
+                </select>
+              </label>
+            </div>
+            <pre v-if="resolved && showResolved" class="iq-resolved-body">{{ 
JSON.stringify(resolved.condition, null, 2) }}</pre>
           </div>
-          <pre v-if="resolved && showResolved" class="iq-resolved-body">{{ 
JSON.stringify(resolved.condition, null, 2) }}</pre>
         </div>
 
-        <!-- Duration distribution — same dot-plot as the per-layer Traces
-             tab. Click a dot to open that trace. -->
         <section class="iq-scatter sw-card">
           <header class="iq-scatter-head">
             <span class="kicker">{{ t('Distribution') }}</span>
@@ -498,8 +512,6 @@ function changeSelectedTraceId(id: string): void {
         </section>
       </div>
 
-      <!-- Persists across browse + detail so the active trace-query API
-           (and what a row represents) stays visible after a click. -->
       <div v-if="showApiBanner" class="iq-api-banner">
         {{ t('This OAP serves traces via') }} <b>{{ t('Trace Query {label} 
API', { label: traceApiLabel }) }}</b>
         (<code>{{ native?.api }}</code>).
@@ -516,7 +528,6 @@ function changeSelectedTraceId(id: string): void {
         <div v-else-if="errorMsg" class="iq-err">{{ errorMsg }}</div>
         <div v-else-if="rows.length === 0" class="iq-empty">{{ t('No traces in 
this window.') }}</div>
 
-        <!-- Browsing mode: full-width list when no trace is selected. -->
         <article v-else-if="!selectedTraceId" class="iq-list-card sw-card">
           <header class="iq-list-head">
             <h4>{{ isSegmentList ? t('Segments') : t('Traces') }}</h4>
@@ -530,7 +541,6 @@ function changeSelectedTraceId(id: string): void {
           />
         </article>
 
-        <!-- Inline detail: folded rail (left) + detail (right). -->
         <section v-else class="iq-detail-split" :class="{ 'rail-collapsed': 
!railOpen }">
           <TraceListPanel
             foldable
@@ -548,12 +558,14 @@ function changeSelectedTraceId(id: string): void {
             <div v-else-if="waterfallSpans.length === 0" class="iq-empty 
sm">{{ t('No spans (older than the detail window).') }}</div>
             <TraceDetailCard
               v-else
+              ref="detailCard"
               :spans="waterfallSpans"
               :trace-id="selectedTraceId"
               :trace-ids="selectedTraceIds"
               :loading="detailFetching"
               @close="closeDetail"
               @change-trace-id="changeSelectedTraceId"
+              @update:modal-open="spanModalOpen = $event"
             />
           </div>
         </section>
@@ -606,23 +618,21 @@ function changeSelectedTraceId(id: string): void {
 .cf-range { display: flex; align-items: center; gap: 4px; }
 .cf-range-num { flex: 1; min-width: 0; }
 .cf-range-sep { color: var(--sw-fg-3); font-size: 12px; flex: 0 0 auto; }
-.iq-run-row { display: flex; align-items: center; gap: 12px; }
-.iq-run { background: var(--sw-accent); color: #fff; border: none; 
border-radius: 4px; padding: 5px 18px; font: inherit; font-size: 12px; cursor: 
pointer; height: 28px; }
+.iq-conditions-h { display: flex; align-items: center; gap: 12px; }
+.iq-cond-kicker { margin-right: auto; }
+.iq-run { background: var(--sw-accent); color: #fff; border: none; 
border-radius: 4px; padding: 5px 18px; font: inherit; font-size: 12px; cursor: 
pointer; height: 28px; order: 2; }
 .iq-run:disabled { opacity: 0.5; cursor: not-allowed; }
 .iq-resolved-tog { background: none; border: none; color: var(--sw-fg-3); 
font-size: 11px; cursor: pointer; }
 .iq-resolved-tog .dim { color: var(--sw-fg-4); margin-left: 6px; }
 .iq-resolved-body { margin: 0; padding: 8px 12px; font-family: var(--sw-mono); 
font-size: 11px; color: var(--sw-fg-2); background: var(--sw-bg-0); overflow: 
auto; max-height: 160px; border: 1px solid var(--sw-line); border-radius: 5px; }
 
-/* Top strip — conditions (left) + duration distribution (right),
-   mirroring the per-layer Traces tab's `.tr-top-strip` 4fr/1fr split. */
-.iq-top-strip { display: grid; grid-template-columns: 4fr 1fr; gap: 12px; 
align-items: stretch; }
+.iq-top-strip { display: grid; grid-template-columns: minmax(0, 1fr) auto; 
gap: 12px; align-items: stretch; }
 @media (max-width: 1100px) { .iq-top-strip { grid-template-columns: 1fr; } }
+.iq-form { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
 .iq-conditions { display: flex; flex-direction: column; gap: 10px; min-width: 
0; }
 
-/* Distribution card — mirrors the per-layer Traces tab's scatter card:
-   a tight kicker + ok/err legend head, the shared dot-plot filling the
-   rest of the card height. */
-.iq-scatter { padding: 6px 10px 8px; display: flex; flex-direction: column; 
min-height: 0; margin: 0; }
+.iq-scatter { padding: 6px 10px 8px; display: flex; flex-direction: column; 
min-height: 0; margin: 0; height: 100%; aspect-ratio: 1; }
+@media (max-width: 1100px) { .iq-scatter { aspect-ratio: auto; height: 320px; 
} }
 .iq-scatter-head { display: flex; align-items: baseline; gap: 10px; 
margin-bottom: 2px; flex: 0 0 auto; }
 .iq-scatter-head .legend { margin-left: auto; font-size: 10.5px; color: 
var(--sw-fg-3); display: inline-flex; gap: 10px; align-items: center; }
 .iq-scatter-head .lg { display: inline-block; width: 7px; height: 7px; 
border-radius: 50%; margin-right: 3px; vertical-align: middle; }
@@ -650,16 +660,10 @@ function changeSelectedTraceId(id: string): void {
 .iq-empty, .iq-err { padding: 24px; text-align: center; color: var(--sw-fg-3); 
font-size: 12px; }
 .iq-empty.sm { padding: 14px; }
 .iq-err { color: var(--sw-err); }
-/* List pane — the same sw-card + header the per-layer Traces tab uses,
-   with the row list scrolling inside. Full width when no trace is
-   selected. */
-.iq-list-card { padding: 0; display: flex; flex-direction: column; min-height: 
0; }
+.iq-list-card { padding: 0; display: flex; flex-direction: column; min-height: 
0; max-height: calc(100vh - 80px); overflow: hidden; }
 .iq-list-head { display: flex; align-items: baseline; gap: 8px; padding: 10px 
14px; border-bottom: 1px solid var(--sw-line); flex: 0 0 auto; }
 .iq-list-head h4 { margin: 0; font-size: 12px; font-weight: 600; color: 
var(--sw-fg-0); }
 .iq-list-head .hint { margin-left: auto; font-size: 10.5px; color: 
var(--sw-fg-3); }
-/* Inline detail split — folded rail (left) + detail (right). Height is
-   driven by the detail card's content; the rail sticks to match. Mirrors
-   the per-layer Traces tab's `.tr-detail-split`. */
 .iq-detail-split { display: grid; grid-template-columns: 320px 1fr; gap: 12px; 
align-items: start; }
 .iq-detail-split.rail-collapsed { grid-template-columns: 64px 1fr; }
 .iq-detail { overflow: auto; min-width: 0; }
diff --git a/apps/ui/src/layer/traces/LayerTracesView.vue 
b/apps/ui/src/layer/traces/LayerTracesView.vue
index 7b7a5c8..69cce84 100644
--- a/apps/ui/src/layer/traces/LayerTracesView.vue
+++ b/apps/ui/src/layer/traces/LayerTracesView.vue
@@ -417,7 +417,6 @@ onBeforeUnmount(() => window.removeEventListener('keydown', 
onPageKeyDown, true)
 
 <template>
   <div class="tr-tab">
-    <!-- Top strip: filter (80%) + duration distribution (20%). -->
     <div class="tr-top-strip">
       <header class="tr-toolbar sw-card">
         <div class="tr-toolbar-head">
@@ -532,9 +531,6 @@ onBeforeUnmount(() => window.removeEventListener('keydown', 
onPageKeyDown, true)
         </div>
       </header>
 
-      <!-- Distribution: dots only. Y axis dropped — the y value is
-           surfaced on hover (tooltip) and re-surfaced in the list-row
-           bar below. Click a dot to select the matching trace. -->
       <section class="tr-scatter sw-card">
         <header class="tr-scatter-head">
           <!-- Header text swaps when the operator is picking dots:
@@ -566,8 +562,6 @@ onBeforeUnmount(() => window.removeEventListener('keydown', 
onPageKeyDown, true)
       </section>
     </div>
 
-    <!-- Persists across browse + detail so the active trace-query API
-         (and what a row represents) stays visible after a click. -->
     <div v-if="showApiBanner" class="tr-api-banner">
       {{ t('This OAP serves traces via') }} <b>{{ t('Trace Query {label} API', 
{ label: traceApiLabel }) }}</b>
       (<code>{{ native?.api }}</code>).
@@ -579,7 +573,6 @@ onBeforeUnmount(() => window.removeEventListener('keydown', 
onPageKeyDown, true)
       </template>
     </div>
 
-    <!-- Browsing mode: full-width list when no trace is selected -->
     <template v-if="!selectedTraceId">
       <article class="tr-list-card sw-card">
         <header class="tr-list-head">
@@ -607,7 +600,6 @@ onBeforeUnmount(() => window.removeEventListener('keydown', 
onPageKeyDown, true)
 
     </template>
 
-    <!-- Inline detail: folded rail (left) + detail (right) -->
     <section v-else class="tr-detail-split" :class="{ 'rail-collapsed': 
!railOpen }">
       <TraceListPanel
         foldable
@@ -746,8 +738,6 @@ onBeforeUnmount(() => window.removeEventListener('keydown', 
onPageKeyDown, true)
 .sw-btn.small { height: 24px; padding: 0 10px; font-size: 11px; }
 .sw-btn.ghost { background: transparent; border: 1px solid var(--sw-line-2); 
color: var(--sw-fg-2); }
 
-/* Scatter — fills as much of the card as possible. Kicker + legend
-   sit on one tight strip; the chart claims the rest. */
 .tr-scatter {
   padding: 6px 10px 8px;
   display: flex;
@@ -776,8 +766,7 @@ onBeforeUnmount(() => window.removeEventListener('keydown', 
onPageKeyDown, true)
    shared component fills the remaining card height via its own flex. */
 .tr-scatter :deep(.scatter-wrap) { flex: 1; min-height: 0; }
 
-/* Browsing list */
-.tr-list-card { padding: 0; display: flex; flex-direction: column; }
+.tr-list-card { padding: 0; display: flex; flex-direction: column; min-height: 
0; max-height: calc(100vh - 80px); overflow: hidden; }
 .tr-list-head {
   display: flex;
   align-items: baseline;
@@ -815,10 +804,6 @@ onBeforeUnmount(() => 
window.removeEventListener('keydown', onPageKeyDown, true)
 }
 .tr-api-banner b { color: var(--sw-fg-0); }
 .tr-empty { padding: 24px; text-align: center; color: var(--sw-fg-3); 
font-size: 11.5px; }
-/* Inline detail split — height is driven by the *content* of the
-   detail card on the right. The rail on the left stretches to match
-   (sticky to its scroll context). No fixed min/max-height: short
-   traces stay compact, long ones make the page scroll. */
 .tr-detail-split {
   display: grid;
   grid-template-columns: 320px 1fr;

Reply via email to