nuxt-devtools-observatory 0.1.32 → 0.1.34

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 (36) hide show
  1. package/README.md +37 -1
  2. package/client/.env.example +2 -0
  3. package/client/dist/assets/index-BO7neKEi.css +1 -0
  4. package/client/dist/assets/index-fFBuk6M6.js +20 -0
  5. package/client/dist/index.html +2 -2
  6. package/client/src/App.vue +8 -0
  7. package/client/src/components/Flamegraph.vue +4 -4
  8. package/client/src/components/SpanInspector.vue +1 -1
  9. package/client/src/composables/composable-search.ts +3 -0
  10. package/client/src/composables/trace-render-aggregation.ts +11 -2
  11. package/client/src/composables/useVirtualizationConfig.ts +40 -0
  12. package/client/src/composables/useVirtualizationFlags.ts +129 -0
  13. package/client/src/stores/observatory.ts +20 -0
  14. package/client/src/views/ComposableTracker.vue +212 -71
  15. package/client/src/views/FetchDashboard.vue +181 -16
  16. package/client/src/views/PiniaStoreTracker.vue +343 -0
  17. package/client/src/views/ProvideInjectGraph.vue +66 -18
  18. package/client/src/views/RenderHeatmap.vue +329 -75
  19. package/client/src/views/TraceViewer.vue +190 -20
  20. package/client/src/views/TransitionTimeline.vue +112 -19
  21. package/dist/module.d.mts +15 -0
  22. package/dist/module.json +1 -1
  23. package/dist/module.mjs +28 -24
  24. package/dist/runtime/composables/pinia-store-registry.d.ts +44 -0
  25. package/dist/runtime/composables/pinia-store-registry.js +447 -0
  26. package/dist/runtime/composables/provide-inject-registry.js +13 -8
  27. package/dist/runtime/composables/render-registry.js +6 -4
  28. package/dist/runtime/instrumentation/asyncData.d.ts +1 -1
  29. package/dist/runtime/instrumentation/fetch.d.ts +7 -1
  30. package/dist/runtime/instrumentation/fetch.js +22 -1
  31. package/dist/runtime/plugin.js +39 -2
  32. package/dist/runtime/test-bridge.d.ts +18 -0
  33. package/dist/runtime/test-bridge.js +100 -0
  34. package/package.json +14 -3
  35. package/client/dist/assets/index-5Wl1XYRH.js +0 -17
  36. package/client/dist/assets/index-DT_QUiIh.css +0 -1
@@ -1,5 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref } 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 { useObservatoryData } from '@observatory-client/stores/observatory'
4
7
  import Flamegraph from '@observatory-client/components/Flamegraph.vue'
5
8
  import WaterfallView from '@observatory-client/components/WaterfallView.vue'
@@ -10,11 +13,18 @@ import { exportJson, importJson } from '@observatory-client/composables/useExpor
10
13
  import {
11
14
  buildRenderSummaryForTrace,
12
15
  buildCrossTraceRenderSummary,
16
+ type CrossTraceRenderSummaryRow,
13
17
  type TraceRenderStatsRow,
14
18
  } from '@observatory-client/composables/trace-render-aggregation'
15
19
  import type { ObservatoryExportFile } from '@observatory-client/composables/useExportImport'
16
20
  import type { TraceEntry, TraceSpan } from '@observatory/types/snapshot'
17
21
 
22
+ type TraceSpanRow = {
23
+ span: TraceSpan
24
+ displayName: string
25
+ uid: string | number | undefined
26
+ }
27
+
18
28
  const { traces, connected } = useObservatoryData()
19
29
 
20
30
  const importedTraces = ref<TraceEntry[]>([])
@@ -33,6 +43,11 @@ const crossTraceOnlyComparable = ref(false)
33
43
  const crossTraceSearch = ref('')
34
44
  const highlightedUid = ref<string | number | undefined>(undefined)
35
45
  const highlightedComponentKey = ref<string | undefined>(undefined)
46
+ const traceListScrollRef = ref<HTMLElement | null>(null)
47
+ const crossTraceScrollRef = ref<HTMLElement | null>(null)
48
+
49
+ const { effective: virtualizationFlags } = useVirtualizationFlags()
50
+ const { preset: virtualizationPreset } = useVirtualizationConfig({ rowHeight: 34, overscan: 6 })
36
51
 
37
52
  const {
38
53
  searchQuery,
@@ -54,6 +69,10 @@ const filteredTraces = computed(() => {
54
69
  return applyFilters(sortedTraces.value)
55
70
  })
56
71
 
72
+ const filteredTraceById = computed(() => {
73
+ return new Map(filteredTraces.value.map((trace) => [trace.id, trace] as const))
74
+ })
75
+
57
76
  const traceCountLabel = computed(() => {
58
77
  const suffix = isImportMode.value ? ' (imported)' : ''
59
78
 
@@ -69,7 +88,7 @@ const selectedTrace = computed(() => {
69
88
  return filteredTraces.value[0]
70
89
  }
71
90
 
72
- return filteredTraces.value.find((t) => t.id === selectedTraceId.value)
91
+ return filteredTraceById.value.get(selectedTraceId.value)
73
92
  })
74
93
 
75
94
  const renderSummary = computed<TraceRenderStatsRow[]>(() => buildRenderSummaryForTrace(selectedTrace.value))
@@ -125,6 +144,100 @@ const crossTraceRows = computed(() => {
125
144
 
126
145
  const crossTraceRegressionsCount = computed(() => crossTraceRenderSummary.value.filter((row) => (row.deltaVsBaseline ?? 0) > 0).length)
127
146
 
147
+ const virtualizedTraceRowsEnabled = computed(() => virtualizationFlags.value.traces)
148
+
149
+ const traceListVirtualizerOptions = computed(() => ({
150
+ count: filteredTraces.value.length,
151
+ getScrollElement: () => traceListScrollRef.value,
152
+ estimateSize: () => virtualizationPreset.value.rowHeight,
153
+ overscan: virtualizationPreset.value.overscan,
154
+ }))
155
+
156
+ const traceListVirtualizer = useVirtualizer(traceListVirtualizerOptions)
157
+
158
+ const traceListVirtualItems = computed(() => {
159
+ if (!virtualizedTraceRowsEnabled.value) {
160
+ return []
161
+ }
162
+
163
+ return traceListVirtualizer.value.getVirtualItems()
164
+ })
165
+
166
+ const traceListTopPadding = computed(() => {
167
+ if (!virtualizedTraceRowsEnabled.value || traceListVirtualItems.value.length === 0) {
168
+ return 0
169
+ }
170
+
171
+ return traceListVirtualItems.value[0].start
172
+ })
173
+
174
+ const traceListBottomPadding = computed(() => {
175
+ if (!virtualizedTraceRowsEnabled.value || traceListVirtualItems.value.length === 0) {
176
+ return 0
177
+ }
178
+
179
+ const total = traceListVirtualizer.value.getTotalSize()
180
+ const last = traceListVirtualItems.value[traceListVirtualItems.value.length - 1]
181
+
182
+ return Math.max(0, total - last.end)
183
+ })
184
+
185
+ const visibleTraceRows = computed(() => {
186
+ if (!virtualizedTraceRowsEnabled.value) {
187
+ return filteredTraces.value
188
+ }
189
+
190
+ return traceListVirtualItems.value
191
+ .map((item) => filteredTraces.value[item.index])
192
+ .filter((trace): trace is TraceEntry => Boolean(trace))
193
+ })
194
+
195
+ const crossTraceVirtualizerOptions = computed(() => ({
196
+ count: crossTraceRows.value.length,
197
+ getScrollElement: () => crossTraceScrollRef.value,
198
+ estimateSize: () => virtualizationPreset.value.rowHeight,
199
+ overscan: virtualizationPreset.value.overscan,
200
+ }))
201
+
202
+ const crossTraceVirtualizer = useVirtualizer(crossTraceVirtualizerOptions)
203
+
204
+ const crossTraceVirtualItems = computed(() => {
205
+ if (!virtualizedTraceRowsEnabled.value) {
206
+ return []
207
+ }
208
+
209
+ return crossTraceVirtualizer.value.getVirtualItems()
210
+ })
211
+
212
+ const crossTraceTopPadding = computed(() => {
213
+ if (!virtualizedTraceRowsEnabled.value || crossTraceVirtualItems.value.length === 0) {
214
+ return 0
215
+ }
216
+
217
+ return crossTraceVirtualItems.value[0].start
218
+ })
219
+
220
+ const crossTraceBottomPadding = computed(() => {
221
+ if (!virtualizedTraceRowsEnabled.value || crossTraceVirtualItems.value.length === 0) {
222
+ return 0
223
+ }
224
+
225
+ const total = crossTraceVirtualizer.value.getTotalSize()
226
+ const last = crossTraceVirtualItems.value[crossTraceVirtualItems.value.length - 1]
227
+
228
+ return Math.max(0, total - last.end)
229
+ })
230
+
231
+ const visibleCrossTraceRows = computed(() => {
232
+ if (!virtualizedTraceRowsEnabled.value) {
233
+ return crossTraceRows.value
234
+ }
235
+
236
+ return crossTraceVirtualItems.value
237
+ .map((item) => crossTraceRows.value[item.index])
238
+ .filter((row): row is CrossTraceRenderSummaryRow => Boolean(row))
239
+ })
240
+
128
241
  function elapsedFromSpans(trace: TraceEntry): number | undefined {
129
242
  if (!trace.spans.length) {
130
243
  return undefined
@@ -201,6 +314,26 @@ function getSpanDisplayName(span: TraceSpan): string {
201
314
  return span.name
202
315
  }
203
316
 
317
+ function getSpanUid(span: TraceSpan): string | number | undefined {
318
+ const metadata = span.metadata as Record<string, unknown> | undefined
319
+
320
+ return metadata?.uid as string | number | undefined
321
+ }
322
+
323
+ const selectedTraceSpanRows = computed<TraceSpanRow[]>(() => {
324
+ const trace = selectedTrace.value
325
+
326
+ if (!trace) {
327
+ return []
328
+ }
329
+
330
+ return trace.spans.map((span) => ({
331
+ span,
332
+ displayName: getSpanDisplayName(span),
333
+ uid: getSpanUid(span),
334
+ }))
335
+ })
336
+
204
337
  function selectTrace(trace: TraceEntry) {
205
338
  selectedTraceId.value = trace.id
206
339
  selectedSpan.value = undefined
@@ -322,13 +455,17 @@ function sortIndicator(key: 'avgRerendersPerTrace' | 'deltaVsBaseline' | 'totalM
322
455
  return crossTraceSortDir.value === 'desc' ? ' ↓' : ' ↑'
323
456
  }
324
457
 
458
+ const crossTraceRowByKey = computed(() => {
459
+ return new Map(crossTraceRows.value.map((row) => [row.componentKey, row] as const))
460
+ })
461
+
325
462
  function clearCrossTraceHighlight() {
326
463
  highlightedComponentKey.value = undefined
327
464
  highlightedUid.value = undefined
328
465
  }
329
466
 
330
467
  function handleCrossTraceRowClick(componentKey: string) {
331
- const row = crossTraceRows.value.find((item) => item.componentKey === componentKey)
468
+ const row = crossTraceRowByKey.value.get(componentKey)
332
469
 
333
470
  if (!row || row.selectedUid === undefined) {
334
471
  highlightedUid.value = undefined
@@ -407,7 +544,7 @@ function handleCrossTraceRowClick(componentKey: string) {
407
544
  <div class="trace-viewer__list-header">
408
545
  <span class="trace-viewer__list-title">Traces</span>
409
546
  </div>
410
- <div class="trace-viewer__table-wrap tracker-table-wrap">
547
+ <div ref="traceListScrollRef" class="trace-viewer__table-wrap tracker-table-wrap">
411
548
  <table class="data-table">
412
549
  <thead>
413
550
  <tr>
@@ -418,7 +555,14 @@ function handleCrossTraceRowClick(componentKey: string) {
418
555
  </thead>
419
556
  <tbody>
420
557
  <tr
421
- v-for="trace in filteredTraces"
558
+ v-if="virtualizedTraceRowsEnabled && traceListTopPadding > 0"
559
+ class="trace-viewer__virtual-spacer-row"
560
+ aria-hidden="true"
561
+ >
562
+ <td colspan="3" :style="{ height: `${traceListTopPadding}px` }"></td>
563
+ </tr>
564
+ <tr
565
+ v-for="trace in visibleTraceRows"
422
566
  :key="trace.id"
423
567
  :class="{ 'trace-viewer__trace-row--selected': selectedTrace?.id === trace.id }"
424
568
  class="trace-viewer__trace-row"
@@ -428,6 +572,13 @@ function handleCrossTraceRowClick(componentKey: string) {
428
572
  <td class="mono">{{ formatDuration(trace.durationMs, trace) }}</td>
429
573
  <td class="mono">{{ trace.spans.length }}</td>
430
574
  </tr>
575
+ <tr
576
+ v-if="virtualizedTraceRowsEnabled && traceListBottomPadding > 0"
577
+ class="trace-viewer__virtual-spacer-row"
578
+ aria-hidden="true"
579
+ >
580
+ <td colspan="3" :style="{ height: `${traceListBottomPadding}px` }"></td>
581
+ </tr>
431
582
  <tr v-if="!filteredTraces.length">
432
583
  <td colspan="3" class="tracker-empty-cell">
433
584
  {{
@@ -499,21 +650,20 @@ function handleCrossTraceRowClick(componentKey: string) {
499
650
 
500
651
  <div class="trace-viewer__span-list">
501
652
  <div
502
- v-for="span in selectedTrace.spans"
503
- :key="span.id"
653
+ v-for="spanRow in selectedTraceSpanRows"
654
+ :key="spanRow.span.id"
504
655
  :class="{
505
- 'trace-viewer__span-item--selected': selectedSpan?.id === span.id,
656
+ 'trace-viewer__span-item--selected': selectedSpan?.id === spanRow.span.id,
506
657
  'trace-viewer__span-item--highlighted':
507
- highlightedUid !== undefined &&
508
- (span.metadata as Record<string, unknown> | undefined)?.uid === highlightedUid,
658
+ highlightedUid !== undefined && spanRow.uid === highlightedUid,
509
659
  }"
510
660
  class="trace-viewer__span-item"
511
- @click="selectedSpan = span"
661
+ @click="selectedSpan = spanRow.span"
512
662
  >
513
- <div class="trace-viewer__span-name">{{ getSpanDisplayName(span) }}</div>
663
+ <div class="trace-viewer__span-name">{{ spanRow.displayName }}</div>
514
664
  <div class="trace-viewer__span-meta">
515
- <span class="trace-viewer__span-type">{{ span.type }}</span>
516
- <span class="trace-viewer__span-duration">{{ formatDuration(span.durationMs) }}</span>
665
+ <span class="trace-viewer__span-type">{{ spanRow.span.type }}</span>
666
+ <span class="trace-viewer__span-duration">{{ formatDuration(spanRow.span.durationMs) }}</span>
517
667
  </div>
518
668
  </div>
519
669
  </div>
@@ -585,7 +735,7 @@ function handleCrossTraceRowClick(componentKey: string) {
585
735
  ✕ clear
586
736
  </span>
587
737
  </div>
588
- <div v-if="crossTraceSummaryOpen" class="trace-viewer__render-summary-table-wrap">
738
+ <div v-if="crossTraceSummaryOpen" ref="crossTraceScrollRef" class="trace-viewer__render-summary-table-wrap">
589
739
  <div class="trace-viewer__comparison-toolbar">
590
740
  <button
591
741
  :class="{ 'trace-viewer__comparison-chip--active': crossTraceOnlyRegressions }"
@@ -647,7 +797,14 @@ function handleCrossTraceRowClick(componentKey: string) {
647
797
  </thead>
648
798
  <tbody>
649
799
  <tr
650
- v-for="row in crossTraceRows"
800
+ v-if="virtualizedTraceRowsEnabled && crossTraceTopPadding > 0"
801
+ class="trace-viewer__virtual-spacer-row"
802
+ aria-hidden="true"
803
+ >
804
+ <td colspan="7" :style="{ height: `${crossTraceTopPadding}px` }"></td>
805
+ </tr>
806
+ <tr
807
+ v-for="row in visibleCrossTraceRows"
651
808
  :key="row.componentKey"
652
809
  :class="{
653
810
  'trace-viewer__render-summary-row--active':
@@ -669,6 +826,13 @@ function handleCrossTraceRowClick(componentKey: string) {
669
826
  <td class="mono trace-viewer__col-desktop">{{ formatDuration(row.avgMsPerRender) }}</td>
670
827
  <td class="mono">{{ formatDuration(row.totalMs) }}</td>
671
828
  </tr>
829
+ <tr
830
+ v-if="virtualizedTraceRowsEnabled && crossTraceBottomPadding > 0"
831
+ class="trace-viewer__virtual-spacer-row"
832
+ aria-hidden="true"
833
+ >
834
+ <td colspan="7" :style="{ height: `${crossTraceBottomPadding}px` }"></td>
835
+ </tr>
672
836
  <tr v-if="!crossTraceRows.length">
673
837
  <td colspan="7" class="tracker-empty-cell">No components match the comparison filters.</td>
674
838
  </tr>
@@ -764,16 +928,16 @@ function handleCrossTraceRowClick(componentKey: string) {
764
928
  font-family: var(--mono);
765
929
  }
766
930
 
767
- .trace-viewer__action-btn:hover:not(:disabled) {
768
- background: var(--bg-secondary);
769
- color: var(--text);
770
- }
771
-
772
931
  .trace-viewer__action-btn:disabled {
773
932
  opacity: 0.4;
774
933
  cursor: not-allowed;
775
934
  }
776
935
 
936
+ .trace-viewer__action-btn:hover:not(:disabled) {
937
+ background: var(--bg-secondary);
938
+ color: var(--text);
939
+ }
940
+
777
941
  .trace-viewer__import-mode-btn {
778
942
  padding: 3px 8px;
779
943
  background: var(--accent-bg);
@@ -838,6 +1002,12 @@ function handleCrossTraceRowClick(componentKey: string) {
838
1002
  border-left: 2px solid var(--accent);
839
1003
  }
840
1004
 
1005
+ .trace-viewer__virtual-spacer-row td {
1006
+ padding: 0;
1007
+ border-bottom: 0;
1008
+ background: transparent;
1009
+ }
1010
+
841
1011
  .trace-viewer__detail {
842
1012
  flex: 1;
843
1013
  display: flex;
@@ -1,5 +1,8 @@
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 { useResizablePane } from '@observatory-client/composables/useResizablePane'
4
7
  import { useObservatoryData } from '@observatory-client/stores/observatory'
5
8
  import type { TransitionEntry } from '@observatory/types/snapshot'
@@ -10,10 +13,25 @@ const { paneWidth: detailWidth, onHandleMouseDown } = useResizablePane(260, 'obs
10
13
  type FilterMode = 'all' | 'cancelled' | 'active' | 'completed'
11
14
  const filter = ref<FilterMode>('all')
12
15
  const search = ref('')
13
- const selected = ref<TransitionEntry | null>(null)
16
+ const selectedId = ref<string | null>(null)
17
+ const tableScrollRef = ref<HTMLElement | null>(null)
18
+
19
+ const { effective: virtualizationFlags } = useVirtualizationFlags()
20
+ const { preset: virtualizationPreset } = useVirtualizationConfig({ rowHeight: 42, overscan: 6 })
21
+
22
+ const entriesSorted = computed(() => [...entries.value].sort((a, b) => a.startTime - b.startTime))
23
+ const entriesById = computed(() => new Map(entries.value.map((entry) => [entry.id, entry] as const)))
24
+
25
+ const selected = computed(() => {
26
+ if (!selectedId.value) {
27
+ return null
28
+ }
29
+
30
+ return entriesById.value.get(selectedId.value) ?? null
31
+ })
14
32
 
15
33
  const filtered = computed(() => {
16
- let list = [...entries.value]
34
+ let list = entriesSorted.value
17
35
 
18
36
  if (search.value) {
19
37
  const q = search.value.toLowerCase()
@@ -28,7 +46,7 @@ const filtered = computed(() => {
28
46
  list = list.filter((e) => e.phase === 'entered' || e.phase === 'left')
29
47
  }
30
48
 
31
- return list.sort((a, b) => a.startTime - b.startTime)
49
+ return list
32
50
  })
33
51
 
34
52
  const stats = computed(() => ({
@@ -64,6 +82,60 @@ const timelineGeometry = computed(() => {
64
82
  }))
65
83
  })
66
84
 
85
+ const virtualizedRowsEnabled = computed(() => virtualizationFlags.value.transitions)
86
+
87
+ const tableVirtualizerOptions = computed(() => ({
88
+ count: filtered.value.length,
89
+ getScrollElement: () => tableScrollRef.value,
90
+ estimateSize: () => virtualizationPreset.value.rowHeight,
91
+ overscan: virtualizationPreset.value.overscan,
92
+ }))
93
+
94
+ const tableVirtualizer = useVirtualizer(tableVirtualizerOptions)
95
+
96
+ const tableVirtualItems = computed(() => {
97
+ if (!virtualizedRowsEnabled.value) {
98
+ return []
99
+ }
100
+
101
+ return tableVirtualizer.value.getVirtualItems()
102
+ })
103
+
104
+ const topTablePadding = computed(() => {
105
+ if (!virtualizedRowsEnabled.value || tableVirtualItems.value.length === 0) {
106
+ return 0
107
+ }
108
+
109
+ return tableVirtualItems.value[0].start
110
+ })
111
+
112
+ const bottomTablePadding = computed(() => {
113
+ if (!virtualizedRowsEnabled.value || tableVirtualItems.value.length === 0) {
114
+ return 0
115
+ }
116
+
117
+ const total = tableVirtualizer.value.getTotalSize()
118
+ const last = tableVirtualItems.value[tableVirtualItems.value.length - 1]
119
+
120
+ return Math.max(0, total - last.end)
121
+ })
122
+
123
+ const visibleRows = computed(() => {
124
+ if (!virtualizedRowsEnabled.value) {
125
+ return filtered.value.map((entry, index) => ({
126
+ entry,
127
+ geometry: timelineGeometry.value[index],
128
+ }))
129
+ }
130
+
131
+ return tableVirtualItems.value
132
+ .map((item) => ({
133
+ entry: filtered.value[item.index],
134
+ geometry: timelineGeometry.value[item.index],
135
+ }))
136
+ .filter((row): row is { entry: TransitionEntry; geometry: { left: number; width: number } } => Boolean(row.entry && row.geometry))
137
+ })
138
+
67
139
  function phaseColor(phase: TransitionEntry['phase']): string {
68
140
  if (phase === 'entering' || phase === 'leaving') {
69
141
  return '#7f77dd'
@@ -166,7 +238,7 @@ function directionColor(e: TransitionEntry): string {
166
238
  <!-- Main content -->
167
239
  <div class="transition-timeline__content tracker-split">
168
240
  <!-- Timeline table -->
169
- <div class="transition-timeline__table tracker-table-wrap">
241
+ <div ref="tableScrollRef" class="transition-timeline__table tracker-table-wrap">
170
242
  <table class="data-table">
171
243
  <thead>
172
244
  <tr>
@@ -180,41 +252,56 @@ function directionColor(e: TransitionEntry): string {
180
252
  </thead>
181
253
  <tbody>
182
254
  <tr
183
- v-for="(entry, i) in filtered"
184
- :key="entry.id"
185
- :class="{ selected: selected?.id === entry.id }"
186
- @click="selected = selected?.id === entry.id ? null : entry"
255
+ v-if="virtualizedRowsEnabled && topTablePadding > 0"
256
+ class="transition-timeline__virtual-spacer-row"
257
+ aria-hidden="true"
258
+ >
259
+ <td colspan="6" :style="{ height: `${topTablePadding}px` }"></td>
260
+ </tr>
261
+ <tr
262
+ v-for="row in visibleRows"
263
+ :key="row.entry.id"
264
+ :class="{ selected: selected?.id === row.entry.id }"
265
+ @click="selectedId = selected?.id === row.entry.id ? null : row.entry.id"
187
266
  >
188
267
  <td>
189
- <span class="transition-timeline__name mono">{{ entry.transitionName }}</span>
268
+ <span class="transition-timeline__name mono">{{ row.entry.transitionName }}</span>
190
269
  </td>
191
270
  <td>
192
- <span class="transition-timeline__direction mono" :style="{ color: directionColor(entry) }">
193
- {{ directionLabel(entry) }}
271
+ <span class="transition-timeline__direction mono" :style="{ color: directionColor(row.entry) }">
272
+ {{ directionLabel(row.entry) }}
194
273
  </span>
195
274
  </td>
196
275
  <td>
197
- <span class="badge" :class="phaseBadgeClass(entry.phase)">{{ entry.phase }}</span>
276
+ <span class="badge" :class="phaseBadgeClass(row.entry.phase)">{{ row.entry.phase }}</span>
198
277
  </td>
199
278
  <td class="transition-timeline__duration mono">
200
- {{ entry.durationMs !== undefined ? entry.durationMs + 'ms' : '—' }}
279
+ {{ row.entry.durationMs !== undefined ? row.entry.durationMs + 'ms' : '—' }}
201
280
  </td>
202
- <td class="transition-timeline__component muted">{{ entry.parentComponent }}</td>
281
+ <td class="transition-timeline__component muted">{{ row.entry.parentComponent }}</td>
203
282
  <td class="transition-timeline__bar-cell">
204
283
  <div class="transition-timeline__bar-track">
205
284
  <div
206
285
  class="transition-timeline__bar-fill"
207
286
  :style="{
208
- left: timelineGeometry[i]?.left + '%',
209
- width: Math.max(timelineGeometry[i]?.width ?? 1, 1) + '%',
210
- background: phaseColor(entry.phase),
211
- opacity: entry.phase === 'entering' || entry.phase === 'leaving' ? '0.55' : '1',
287
+ left: row.geometry.left + '%',
288
+ width: Math.max(row.geometry.width, 1) + '%',
289
+ background: phaseColor(row.entry.phase),
290
+ opacity: row.entry.phase === 'entering' || row.entry.phase === 'leaving' ? '0.55' : '1',
212
291
  }"
213
292
  />
214
293
  </div>
215
294
  </td>
216
295
  </tr>
217
296
 
297
+ <tr
298
+ v-if="virtualizedRowsEnabled && bottomTablePadding > 0"
299
+ class="transition-timeline__virtual-spacer-row"
300
+ aria-hidden="true"
301
+ >
302
+ <td colspan="6" :style="{ height: `${bottomTablePadding}px` }"></td>
303
+ </tr>
304
+
218
305
  <tr v-if="!filtered.length">
219
306
  <td colspan="6" class="tracker-empty-cell">
220
307
  {{
@@ -236,7 +323,7 @@ function directionColor(e: TransitionEntry): string {
236
323
  <aside v-if="selected" class="transition-timeline__detail" :style="{ width: detailWidth + 'px' }">
237
324
  <div class="transition-timeline__detail-header">
238
325
  <span class="transition-timeline__detail-title">{{ selected.transitionName }}</span>
239
- <button class="transition-timeline__close-btn" @click="selected = null">✕</button>
326
+ <button class="transition-timeline__close-btn" @click="selectedId = null">✕</button>
240
327
  </div>
241
328
 
242
329
  <div class="transition-timeline__detail-section">
@@ -392,6 +479,12 @@ function directionColor(e: TransitionEntry): string {
392
479
  border-radius: 0;
393
480
  }
394
481
 
482
+ .transition-timeline__virtual-spacer-row td {
483
+ padding: 0;
484
+ border-bottom: 0;
485
+ background: transparent;
486
+ }
487
+
395
488
  /* ── Timeline bar ────────────────────────────────────────────────────────── */
396
489
  .transition-timeline__bar-cell {
397
490
  width: 200px;
package/dist/module.d.mts CHANGED
@@ -19,6 +19,11 @@ interface ModuleOptions {
19
19
  * @default 10000
20
20
  */
21
21
  maxPayloadBytes?: number;
22
+ /**
23
+ * Number of fetch rows to load per infinite-scroll step in the Fetch Dashboard.
24
+ * @default 20
25
+ */
26
+ fetchPageSize?: number;
22
27
  /**
23
28
  * Maximum number of transition entries to keep in memory
24
29
  * @default 500
@@ -34,6 +39,11 @@ interface ModuleOptions {
34
39
  * @default 300
35
40
  */
36
41
  maxComposableEntries?: number;
42
+ /**
43
+ * Maximum number of Pinia timeline events to keep per store
44
+ * @default 100
45
+ */
46
+ maxPiniaTimeline?: number;
37
47
  /**
38
48
  * Maximum number of render timeline events per entry
39
49
  * @default 100
@@ -61,6 +71,11 @@ interface ModuleOptions {
61
71
  * @default true
62
72
  */
63
73
  composableTracker?: boolean;
74
+ /**
75
+ * Enable the Pinia state tracker tab
76
+ * @default true
77
+ */
78
+ piniaTracker?: boolean;
64
79
  /**
65
80
  * Enable the render heatmap tab
66
81
  * @default true
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0 || ^4.0.0"
6
6
  },
7
- "version": "0.1.32",
7
+ "version": "0.1.34",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"