nuxt-devtools-observatory 0.1.31 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +79 -46
  2. package/client/.env.example +1 -0
  3. package/client/dist/assets/index-BqKYgjVB.js +20 -0
  4. package/client/dist/assets/index-bs1JBJ2u.css +1 -0
  5. package/client/dist/index.html +2 -2
  6. package/client/src/App.vue +4 -0
  7. package/client/src/components/Flamegraph.vue +7 -8
  8. package/client/src/components/SpanInspector.vue +1 -1
  9. package/client/src/components/TraceFilter.vue +0 -2
  10. package/client/src/components/WaterfallView.vue +1 -1
  11. package/client/src/composables/composable-search.ts +127 -0
  12. package/client/src/composables/trace-render-aggregation.ts +263 -0
  13. package/client/src/composables/useExportImport.ts +63 -0
  14. package/client/src/composables/useTraceFilter.ts +1 -5
  15. package/client/src/composables/useVirtualizationConfig.ts +40 -0
  16. package/client/src/composables/useVirtualizationFlags.ts +129 -0
  17. package/client/src/stores/observatory.ts +9 -1
  18. package/client/src/views/ComposableTracker.vue +273 -97
  19. package/client/src/views/FetchDashboard.vue +181 -16
  20. package/client/src/views/ProvideInjectGraph.vue +41 -18
  21. package/client/src/views/RenderHeatmap.vue +392 -76
  22. package/client/src/views/TraceViewer.vue +797 -14
  23. package/client/src/views/TransitionTimeline.vue +112 -19
  24. package/dist/module.d.mts +5 -0
  25. package/dist/module.json +1 -1
  26. package/dist/module.mjs +12 -23
  27. package/dist/runtime/composables/composable-registry.d.ts +19 -0
  28. package/dist/runtime/composables/composable-registry.js +63 -5
  29. package/dist/runtime/composables/render-registry.js +23 -13
  30. package/dist/runtime/instrumentation/asyncData.d.ts +1 -1
  31. package/dist/runtime/instrumentation/fetch.d.ts +7 -1
  32. package/dist/runtime/instrumentation/fetch.js +22 -1
  33. package/dist/runtime/nitro/fetch-capture.d.ts +1 -2
  34. package/dist/runtime/nitro/fetch-capture.js +85 -7
  35. package/dist/runtime/nitro/ssr-trace-store.d.ts +85 -0
  36. package/dist/runtime/nitro/ssr-trace-store.js +84 -0
  37. package/dist/runtime/plugin.js +48 -1
  38. package/dist/runtime/test-bridge.d.ts +18 -0
  39. package/dist/runtime/test-bridge.js +86 -0
  40. package/dist/runtime/tracing/trace.d.ts +1 -1
  41. package/package.json +18 -3
  42. package/client/.env +0 -17
  43. package/client/dist/assets/index-BuMXDBO9.js +0 -17
  44. package/client/dist/assets/index-CwcspZ6w.css +0 -1
@@ -1,11 +1,15 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed } from 'vue'
3
+ import { useVirtualizer } from '@tanstack/vue-virtual'
4
+ import { useVirtualizationConfig } from '@observatory-client/composables/useVirtualizationConfig'
5
+ import { useVirtualizationFlags } from '@observatory-client/composables/useVirtualizationFlags'
3
6
  import {
4
7
  useObservatoryData,
5
8
  setComposableMode,
6
9
  editComposableValue,
7
10
  openInEditor as openInEditorFromStore,
8
11
  } from '@observatory-client/stores/observatory'
12
+ import { matchesComposableEntryQuery } from '@observatory-client/composables/composable-search'
9
13
  import type { ComposableEntry as RuntimeComposableEntry } from '@observatory/types/snapshot'
10
14
 
11
15
  const { composables: rawEntries, connected, features, clearComposables } = useObservatoryData()
@@ -92,6 +96,10 @@ function openInEditor(file: string) {
92
96
  const filter = ref('all')
93
97
  const search = ref('')
94
98
  const expanded = ref<string | null>(null)
99
+ const listScrollRef = ref<HTMLElement | null>(null)
100
+
101
+ const { effective: virtualizationFlags } = useVirtualizationFlags()
102
+ const { preset: virtualizationPreset } = useVirtualizationConfig({ rowHeight: 88, overscan: 6 })
95
103
 
96
104
  const entries = computed<RuntimeComposableEntry[]>(() => rawEntries.value)
97
105
 
@@ -118,23 +126,8 @@ const filtered = computed(() => {
118
126
  return false
119
127
  }
120
128
 
121
- const q = search.value.toLowerCase()
122
-
123
- if (q) {
124
- const matchesName = entry.name.toLowerCase().includes(q)
125
- const matchesFile = entry.componentFile.toLowerCase().includes(q)
126
- const matchesRef = Object.keys(entry.refs).some((k) => k.toLowerCase().includes(q))
127
- const matchesVal = Object.values(entry.refs).some((v) => {
128
- try {
129
- return String(JSON.stringify(v.value)).toLowerCase().includes(q)
130
- } catch {
131
- return false
132
- }
133
- })
134
-
135
- if (!matchesName && !matchesFile && !matchesRef && !matchesVal) {
136
- return false
137
- }
129
+ if (search.value.trim() && !matchesComposableEntryQuery(entry, search.value)) {
130
+ return false
138
131
  }
139
132
 
140
133
  return true
@@ -156,6 +149,99 @@ const filtered = computed(() => {
156
149
  return [...layoutEntries, ...regularEntries]
157
150
  })
158
151
 
152
+ const virtualizedCardsEnabled = computed(() => virtualizationFlags.value.composables && expanded.value === null)
153
+
154
+ const listVirtualizerOptions = computed(() => ({
155
+ count: filtered.value.length,
156
+ getScrollElement: () => listScrollRef.value,
157
+ estimateSize: () => virtualizationPreset.value.rowHeight,
158
+ overscan: virtualizationPreset.value.overscan,
159
+ }))
160
+
161
+ const listVirtualizer = useVirtualizer(listVirtualizerOptions)
162
+
163
+ const listVirtualItems = computed(() => {
164
+ if (!virtualizedCardsEnabled.value) {
165
+ return []
166
+ }
167
+
168
+ return listVirtualizer.value.getVirtualItems()
169
+ })
170
+
171
+ const topListPadding = computed(() => {
172
+ if (!virtualizedCardsEnabled.value || listVirtualItems.value.length === 0) {
173
+ return 0
174
+ }
175
+
176
+ return listVirtualItems.value[0].start
177
+ })
178
+
179
+ const bottomListPadding = computed(() => {
180
+ if (!virtualizedCardsEnabled.value || listVirtualItems.value.length === 0) {
181
+ return 0
182
+ }
183
+
184
+ const total = listVirtualizer.value.getTotalSize()
185
+ const last = listVirtualItems.value[listVirtualItems.value.length - 1]
186
+
187
+ return Math.max(0, total - last.end)
188
+ })
189
+
190
+ const visibleEntries = computed(() => {
191
+ if (!virtualizedCardsEnabled.value) {
192
+ return filtered.value
193
+ }
194
+
195
+ return listVirtualItems.value
196
+ .map((item) => filtered.value[item.index])
197
+ .filter((entry): entry is RuntimeComposableEntry => Boolean(entry))
198
+ })
199
+
200
+ const visibleEntryCards = computed<VisibleEntryCard[]>(() => {
201
+ return visibleEntries.value.map((entry) => {
202
+ const refEntries = Object.entries(entry.refs) as Array<[string, RuntimeComposableEntry['refs'][string]]>
203
+ const detailRefs: RefRowView[] = refEntries.map(([key, ref]) => {
204
+ const isLong = isLongValue(ref.value)
205
+ const expanded = isLong && isRefExpanded(entry.id, key)
206
+ const shared = Boolean(entry.sharedKeys?.includes(key))
207
+
208
+ return {
209
+ key,
210
+ ref,
211
+ isLong,
212
+ expanded,
213
+ displayValue: isLong && !expanded ? formatVal(ref.value) : formatValFull(ref.value),
214
+ typeClass: typeBadgeClass(ref.type),
215
+ shared,
216
+ }
217
+ })
218
+
219
+ const history = entry.history ?? []
220
+ const historyRows: HistoryRowView[] = []
221
+ const end = Math.max(history.length - 20, 0)
222
+
223
+ for (let i = history.length - 1; i >= end; i--) {
224
+ const evt = history[i]
225
+ historyRows.push({
226
+ id: `${entry.id}-${evt.key}-${evt.t}-${i}`,
227
+ timeLabel: `+${(evt.t / 1000).toFixed(2)}s`,
228
+ key: evt.key,
229
+ value: formatVal(evt.value),
230
+ })
231
+ }
232
+
233
+ return {
234
+ entry,
235
+ refCount: refEntries.length,
236
+ previewRefs: detailRefs.slice(0, 3),
237
+ detailRefs,
238
+ historyRows,
239
+ historyHiddenCount: Math.max(0, history.length - 20),
240
+ lifecycle: lifecycleRows(entry),
241
+ }
242
+ })
243
+ })
244
+
159
245
  function lifecycleRows(entry: RuntimeComposableEntry) {
160
246
  return [
161
247
  {
@@ -234,22 +320,94 @@ function toggleRefExpand(entryId: string, refKey: string) {
234
320
  }
235
321
 
236
322
  // ── Reverse lookup ────────────────────────────────────────────────────────
237
- // Clicking a ref key shows every mounted instance that exposes the same key.
323
+ // Clicking a ref key prefers identity-based lookup for shared/global refs.
324
+ // For non-shared keys, fallback to legacy key-name lookup.
325
+
326
+ interface LookupTarget {
327
+ key: string
328
+ composableName: string
329
+ identityGroup?: string
330
+ }
331
+
332
+ interface RefRowView {
333
+ key: string
334
+ ref: RuntimeComposableEntry['refs'][string]
335
+ isLong: boolean
336
+ expanded: boolean
337
+ displayValue: string
338
+ typeClass: string
339
+ shared: boolean
340
+ }
341
+
342
+ interface HistoryRowView {
343
+ id: string
344
+ timeLabel: string
345
+ key: string
346
+ value: string
347
+ }
348
+
349
+ interface VisibleEntryCard {
350
+ entry: RuntimeComposableEntry
351
+ refCount: number
352
+ previewRefs: RefRowView[]
353
+ detailRefs: RefRowView[]
354
+ historyRows: HistoryRowView[]
355
+ historyHiddenCount: number
356
+ lifecycle: ReturnType<typeof lifecycleRows>
357
+ }
238
358
 
239
- const lookupKey = ref<string | null>(null)
359
+ const lookupTarget = ref<LookupTarget | null>(null)
240
360
 
241
361
  const lookupResults = computed(() => {
242
- if (!lookupKey.value) {
362
+ if (!lookupTarget.value) {
243
363
  return []
244
364
  }
245
365
 
246
- const key = lookupKey.value
366
+ const target = lookupTarget.value
247
367
 
248
- return entries.value.filter((e) => key in e.refs)
368
+ if (target.identityGroup) {
369
+ return entries.value.filter(
370
+ (entry) =>
371
+ entry.name === target.composableName &&
372
+ entry.sharedKeyGroups?.[target.key] === target.identityGroup &&
373
+ target.key in entry.refs
374
+ )
375
+ }
376
+
377
+ return entries.value.filter((entry) => target.key in entry.refs)
249
378
  })
250
379
 
251
- function openLookup(key: string) {
252
- lookupKey.value = lookupKey.value === key ? null : key
380
+ const lookupTitle = computed(() => {
381
+ if (!lookupTarget.value) {
382
+ return ''
383
+ }
384
+
385
+ if (lookupTarget.value.identityGroup) {
386
+ return `${lookupTarget.value.key} (shared identity)`
387
+ }
388
+
389
+ return lookupTarget.value.key
390
+ })
391
+
392
+ function openLookup(entry: RuntimeComposableEntry, key: string) {
393
+ const identityGroup = entry.sharedKeyGroups?.[key]
394
+ const next: LookupTarget = {
395
+ key,
396
+ composableName: entry.name,
397
+ identityGroup,
398
+ }
399
+
400
+ if (
401
+ lookupTarget.value?.key === next.key &&
402
+ lookupTarget.value?.composableName === next.composableName &&
403
+ lookupTarget.value?.identityGroup === next.identityGroup
404
+ ) {
405
+ lookupTarget.value = null
406
+
407
+ return
408
+ }
409
+
410
+ lookupTarget.value = next
253
411
  }
254
412
 
255
413
  // ── Inline editing ────────────────────────────────────────────────────────
@@ -340,127 +498,133 @@ function applyEdit() {
340
498
  </button>
341
499
  </div>
342
500
 
343
- <div class="composable-tracker__list">
501
+ <div ref="listScrollRef" class="composable-tracker__list">
502
+ <div
503
+ v-if="virtualizedCardsEnabled && topListPadding > 0"
504
+ class="composable-tracker__virtual-spacer"
505
+ :style="{ height: `${topListPadding}px` }"
506
+ aria-hidden="true"
507
+ />
344
508
  <div
345
- v-for="entry in filtered"
346
- :key="entry.id"
509
+ v-for="card in visibleEntryCards"
510
+ :key="card.entry.id"
347
511
  class="composable-tracker__card"
348
512
  :class="{
349
- 'composable-tracker__card--leak': entry.leak,
350
- 'composable-tracker__card--expanded': expanded === entry.id,
513
+ 'composable-tracker__card--leak': card.entry.leak,
514
+ 'composable-tracker__card--expanded': expanded === card.entry.id,
351
515
  }"
352
- @click="expanded = expanded === entry.id ? null : entry.id"
516
+ @click="expanded = expanded === card.entry.id ? null : card.entry.id"
353
517
  >
354
518
  <div class="composable-tracker__card-header">
355
519
  <div class="composable-tracker__identity">
356
- <span class="composable-tracker__name mono">{{ entry.name }}</span>
357
- <span class="composable-tracker__file muted mono">{{ basename(entry.componentFile) }}</span>
520
+ <span class="composable-tracker__name mono">{{ card.entry.name }}</span>
521
+ <span class="composable-tracker__file muted mono">{{ basename(card.entry.componentFile) }}</span>
358
522
  </div>
359
523
  <div class="composable-tracker__meta">
360
- <span v-if="entry.watcherCount > 0 && !entry.leak" class="badge badge-warn">{{ entry.watcherCount }}w</span>
361
- <span v-if="entry.intervalCount > 0 && !entry.leak" class="badge badge-warn">{{ entry.intervalCount }}t</span>
362
- <span v-if="entry.leak" class="badge badge-err">leak</span>
363
- <span v-else-if="entry.status === 'mounted'" class="badge badge-ok">mounted</span>
524
+ <span v-if="card.entry.watcherCount > 0 && !card.entry.leak" class="badge badge-warn">
525
+ {{ card.entry.watcherCount }}w
526
+ </span>
527
+ <span v-if="card.entry.intervalCount > 0 && !card.entry.leak" class="badge badge-warn">
528
+ {{ card.entry.intervalCount }}t
529
+ </span>
530
+ <span v-if="card.entry.leak" class="badge badge-err">leak</span>
531
+ <span v-else-if="card.entry.status === 'mounted'" class="badge badge-ok">mounted</span>
364
532
  <span v-else class="badge badge-gray">unmounted</span>
365
533
  </div>
366
534
  </div>
367
535
 
368
536
  <!-- Inline ref preview — shows up to 3 refs without expanding -->
369
- <div v-if="Object.keys(entry.refs).length" class="composable-tracker__refs-preview">
537
+ <div v-if="card.refCount" class="composable-tracker__refs-preview">
370
538
  <span
371
- v-for="[k, v] in Object.entries(entry.refs).slice(0, 3)"
372
- :key="k"
539
+ v-for="row in card.previewRefs"
540
+ :key="row.key"
373
541
  class="composable-tracker__ref-chip"
374
542
  :class="{
375
- 'composable-tracker__ref-chip--reactive': v.type === 'reactive',
376
- 'composable-tracker__ref-chip--computed': v.type === 'computed',
377
- 'composable-tracker__ref-chip--shared': entry.sharedKeys?.includes(k),
543
+ 'composable-tracker__ref-chip--reactive': row.ref.type === 'reactive',
544
+ 'composable-tracker__ref-chip--computed': row.ref.type === 'computed',
545
+ 'composable-tracker__ref-chip--shared': row.shared,
378
546
  }"
379
- :title="entry.sharedKeys?.includes(k) ? 'shared global state' : ''"
547
+ :title="row.shared ? 'shared global state' : ''"
380
548
  >
381
- <span class="composable-tracker__ref-chip-key">{{ k }}</span>
382
- <span class="composable-tracker__ref-chip-val">{{ formatVal(v.value) }}</span>
383
- <span v-if="entry.sharedKeys?.includes(k)" class="composable-tracker__ref-chip-shared-dot" title="global"></span>
384
- </span>
385
- <span v-if="Object.keys(entry.refs).length > 3" class="muted text-sm">
386
- +{{ Object.keys(entry.refs).length - 3 }} more
549
+ <span class="composable-tracker__ref-chip-key">{{ row.key }}</span>
550
+ <span class="composable-tracker__ref-chip-val">{{ formatVal(row.ref.value) }}</span>
551
+ <span v-if="row.shared" class="composable-tracker__ref-chip-shared-dot" title="global"></span>
387
552
  </span>
553
+ <span v-if="card.refCount > 3" class="muted text-sm">+{{ card.refCount - 3 }} more</span>
388
554
  </div>
389
555
 
390
- <div v-if="expanded === entry.id" class="composable-tracker__detail" @click.stop>
391
- <div v-if="entry.leak" class="composable-tracker__leak-banner">{{ entry.leakReason }}</div>
556
+ <div v-if="expanded === card.entry.id" class="composable-tracker__detail" @click.stop>
557
+ <div v-if="card.entry.leak" class="composable-tracker__leak-banner">{{ card.entry.leakReason }}</div>
392
558
 
393
559
  <!-- Global state warning -->
394
- <div v-if="entry.sharedKeys?.length" class="composable-tracker__global-banner">
560
+ <div v-if="card.entry.sharedKeys?.length" class="composable-tracker__global-banner">
395
561
  <span class="composable-tracker__global-dot"></span>
396
562
  <span>
397
563
  <strong>global state</strong>
398
- — {{ entry.sharedKeys.join(', ') }}
399
- {{ entry.sharedKeys.length === 1 ? 'is' : 'are' }}
400
- shared across all instances of {{ entry.name }}
564
+ — {{ card.entry.sharedKeys.join(', ') }}
565
+ {{ card.entry.sharedKeys.length === 1 ? 'is' : 'are' }}
566
+ shared across all instances of {{ card.entry.name }}
401
567
  </span>
402
568
  </div>
403
569
 
404
570
  <div class="composable-tracker__section-label tracker-section-label">reactive state</div>
405
- <div v-if="!Object.keys(entry.refs).length" class="composable-tracker__compact-muted muted text-sm">
406
- no tracked state returned
407
- </div>
408
- <div v-for="[k, v] in Object.entries(entry.refs)" :key="k" class="composable-tracker__ref-row">
571
+ <div v-if="!card.refCount" class="composable-tracker__compact-muted muted text-sm">no tracked state returned</div>
572
+ <div v-for="row in card.detailRefs" :key="row.key" class="composable-tracker__ref-row">
409
573
  <span
410
574
  class="composable-tracker__ref-key composable-tracker__ref-key--clickable mono text-sm"
411
- :title="`click to see all instances exposing '${k}'`"
412
- @click.stop="openLookup(k)"
575
+ :title="
576
+ card.entry.sharedKeyGroups?.[row.key]
577
+ ? `click to see instances sharing this exact '${row.key}' state`
578
+ : `click to see all instances exposing '${row.key}'`
579
+ "
580
+ @click.stop="openLookup(card.entry, row.key)"
413
581
  >
414
- {{ k }}
582
+ {{ row.key }}
415
583
  </span>
416
584
  <span
417
585
  class="composable-tracker__ref-val mono text-sm"
418
586
  :class="{
419
- 'composable-tracker__ref-val--full': isLongValue(v.value) && isRefExpanded(entry.id, k),
420
- 'composable-tracker__ref-val--collapsed': isLongValue(v.value) && !isRefExpanded(entry.id, k),
587
+ 'composable-tracker__ref-val--full': row.isLong && row.expanded,
588
+ 'composable-tracker__ref-val--collapsed': row.isLong && !row.expanded,
421
589
  }"
422
590
  >
423
- {{ isLongValue(v.value) && !isRefExpanded(entry.id, k) ? formatVal(v.value) : formatValFull(v.value) }}
591
+ {{ row.displayValue }}
424
592
  </span>
425
593
  <div class="composable-tracker__ref-row-actions">
426
594
  <button
427
- v-if="isLongValue(v.value)"
595
+ v-if="row.isLong"
428
596
  class="composable-tracker__expand-btn"
429
- :title="isRefExpanded(entry.id, k) ? 'Collapse' : 'Expand'"
430
- @click.stop="toggleRefExpand(entry.id, k)"
597
+ :title="row.expanded ? 'Collapse' : 'Expand'"
598
+ @click.stop="toggleRefExpand(card.entry.id, row.key)"
431
599
  >
432
- {{ isRefExpanded(entry.id, k) ? '▲' : '▼' }}
600
+ {{ row.expanded ? '▲' : '▼' }}
433
601
  </button>
434
- <span class="badge text-xs" :class="typeBadgeClass(v.type)">{{ v.type }}</span>
435
- <span v-if="entry.sharedKeys?.includes(k)" class="badge badge-amber text-xs">global</span>
602
+ <span class="badge text-xs" :class="row.typeClass">{{ row.ref.type }}</span>
603
+ <span v-if="row.shared" class="badge badge-amber text-xs">global</span>
436
604
  <button
437
- v-if="v.type === 'ref'"
605
+ v-if="row.ref.type === 'ref'"
438
606
  class="composable-tracker__edit-btn"
439
607
  title="Edit value"
440
- @click.stop="openEdit(entry.id, k, v.value)"
608
+ @click.stop="openEdit(card.entry.id, row.key, row.ref.value)"
441
609
  >
442
610
  edit
443
611
  </button>
444
612
  </div>
445
613
  </div>
446
614
 
447
- <template v-if="entry.history && entry.history.length">
615
+ <template v-if="card.historyRows.length">
448
616
  <div class="composable-tracker__section-label composable-tracker__section-label--spaced tracker-section-label">
449
617
  change history
450
- <span class="composable-tracker__section-label-meta muted">({{ entry.history.length }} events)</span>
618
+ <span class="composable-tracker__section-label-meta muted">({{ card.entry.history.length }} events)</span>
451
619
  </div>
452
620
  <div class="composable-tracker__history-list">
453
- <div
454
- v-for="(evt, idx) in [...entry.history].reverse().slice(0, 20)"
455
- :key="idx"
456
- class="composable-tracker__history-row"
457
- >
458
- <span class="composable-tracker__history-time mono muted">+{{ (evt.t / 1000).toFixed(2) }}s</span>
459
- <span class="composable-tracker__history-key mono">{{ evt.key }}</span>
460
- <span class="composable-tracker__history-val mono">{{ formatVal(evt.value) }}</span>
621
+ <div v-for="historyRow in card.historyRows" :key="historyRow.id" class="composable-tracker__history-row">
622
+ <span class="composable-tracker__history-time mono muted">{{ historyRow.timeLabel }}</span>
623
+ <span class="composable-tracker__history-key mono">{{ historyRow.key }}</span>
624
+ <span class="composable-tracker__history-val mono">{{ historyRow.value }}</span>
461
625
  </div>
462
- <div v-if="entry.history.length > 20" class="composable-tracker__compact-muted muted text-sm">
463
- … {{ entry.history.length - 20 }} earlier events
626
+ <div v-if="card.historyHiddenCount > 0" class="composable-tracker__compact-muted muted text-sm">
627
+ … {{ card.historyHiddenCount }} earlier events
464
628
  </div>
465
629
  </div>
466
630
  </template>
@@ -468,7 +632,7 @@ function applyEdit() {
468
632
  <div class="composable-tracker__section-label composable-tracker__section-label--spaced tracker-section-label">
469
633
  lifecycle
470
634
  </div>
471
- <div v-for="row in lifecycleRows(entry)" :key="row.label" class="composable-tracker__lifecycle-row">
635
+ <div v-for="row in card.lifecycle" :key="row.label" class="composable-tracker__lifecycle-row">
472
636
  <span
473
637
  class="composable-tracker__lifecycle-dot"
474
638
  :class="row.ok ? 'composable-tracker__lifecycle-dot--ok' : 'composable-tracker__lifecycle-dot--error'"
@@ -487,12 +651,12 @@ function applyEdit() {
487
651
  </div>
488
652
  <div class="composable-tracker__lifecycle-row">
489
653
  <span class="composable-tracker__context-label muted text-sm">component</span>
490
- <span class="composable-tracker__context-value mono text-sm">{{ basename(entry.componentFile) }}</span>
654
+ <span class="composable-tracker__context-value mono text-sm">{{ basename(card.entry.componentFile) }}</span>
491
655
  </div>
492
656
  <div class="composable-tracker__lifecycle-row">
493
657
  <span class="composable-tracker__context-label muted text-sm">uid</span>
494
658
  <span class="composable-tracker__context-value composable-tracker__context-value--muted mono text-sm muted">
495
- {{ entry.componentUid }}
659
+ {{ card.entry.componentUid }}
496
660
  </span>
497
661
  </div>
498
662
  <div class="composable-tracker__lifecycle-row">
@@ -500,8 +664,8 @@ function applyEdit() {
500
664
  <span
501
665
  class="composable-tracker__context-value composable-tracker__context-value--row composable-tracker__context-value--muted mono text-sm muted"
502
666
  >
503
- {{ entry.file }}:{{ entry.line }}
504
- <button class="composable-tracker__jump-btn" title="Open in editor" @click.stop="openInEditor(entry.file)">
667
+ {{ card.entry.file }}:{{ card.entry.line }}
668
+ <button class="composable-tracker__jump-btn" title="Open in editor" @click.stop="openInEditor(card.entry.file)">
505
669
  open ↗
506
670
  </button>
507
671
  </span>
@@ -509,20 +673,27 @@ function applyEdit() {
509
673
  <div class="composable-tracker__lifecycle-row">
510
674
  <span class="composable-tracker__context-label muted text-sm">route</span>
511
675
  <span class="composable-tracker__context-value composable-tracker__context-value--muted mono text-sm muted">
512
- {{ entry.route }}
676
+ {{ card.entry.route }}
513
677
  </span>
514
678
  </div>
515
679
  <div class="composable-tracker__lifecycle-row">
516
680
  <span class="composable-tracker__context-label muted text-sm">watchers</span>
517
- <span class="composable-tracker__context-value mono text-sm">{{ entry.watcherCount }}</span>
681
+ <span class="composable-tracker__context-value mono text-sm">{{ card.entry.watcherCount }}</span>
518
682
  </div>
519
683
  <div class="composable-tracker__lifecycle-row">
520
684
  <span class="composable-tracker__context-label muted text-sm">intervals</span>
521
- <span class="composable-tracker__context-value mono text-sm">{{ entry.intervalCount }}</span>
685
+ <span class="composable-tracker__context-value mono text-sm">{{ card.entry.intervalCount }}</span>
522
686
  </div>
523
687
  </div>
524
688
  </div>
525
689
 
690
+ <div
691
+ v-if="virtualizedCardsEnabled && bottomListPadding > 0"
692
+ class="composable-tracker__virtual-spacer"
693
+ :style="{ height: `${bottomListPadding}px` }"
694
+ aria-hidden="true"
695
+ />
696
+
526
697
  <div v-if="!filtered.length" class="composable-tracker__empty muted text-sm">
527
698
  {{ connected ? 'No composables recorded yet.' : 'Waiting for connection to the Nuxt app…' }}
528
699
  </div>
@@ -530,14 +701,14 @@ function applyEdit() {
530
701
 
531
702
  <!-- ── Reverse lookup panel ──────────────────────────────────────── -->
532
703
  <Transition name="slide">
533
- <div v-if="lookupKey" class="composable-tracker__lookup-panel">
704
+ <div v-if="lookupTarget" class="composable-tracker__lookup-panel">
534
705
  <div class="composable-tracker__lookup-header">
535
- <span class="mono text-sm">{{ lookupKey }}</span>
706
+ <span class="mono text-sm">{{ lookupTitle }}</span>
536
707
  <span class="muted text-sm">— {{ lookupResults.length }} instance{{ lookupResults.length !== 1 ? 's' : '' }}</span>
537
- <button class="composable-tracker__clear-btn composable-tracker__lookup-close" @click="lookupKey = null">✕</button>
708
+ <button class="composable-tracker__clear-btn composable-tracker__lookup-close" @click="lookupTarget = null">✕</button>
538
709
  </div>
539
710
  <div v-if="!lookupResults.length" class="composable-tracker__lookup-empty muted text-sm">
540
- No mounted instances expose this key.
711
+ No instances matched this lookup.
541
712
  </div>
542
713
  <div v-for="r in lookupResults" :key="r.id" class="composable-tracker__lookup-row">
543
714
  <span class="mono text-sm">{{ r.name }}</span>
@@ -609,6 +780,11 @@ function applyEdit() {
609
780
  min-height: 0;
610
781
  }
611
782
 
783
+ .composable-tracker__virtual-spacer {
784
+ width: 100%;
785
+ flex-shrink: 0;
786
+ }
787
+
612
788
  .composable-tracker__card {
613
789
  background: var(--bg3);
614
790
  border: var(--tracker-border-width) solid var(--border);