nuxt-devtools-observatory 0.1.26 → 0.1.30

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.
@@ -1,27 +1,24 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed } from 'vue'
3
- import { useObservatoryData, getObservatoryOrigin, type ComposableEntry as RuntimeComposableEntry } from '../stores/observatory'
3
+ import {
4
+ useObservatoryData,
5
+ setComposableMode,
6
+ editComposableValue,
7
+ openInEditor as openInEditorFromStore,
8
+ } from '@observatory-client/stores/observatory'
9
+ import type { ComposableEntry as RuntimeComposableEntry } from '@observatory/types/snapshot'
4
10
 
5
11
  const { composables: rawEntries, connected, features, clearComposables } = useObservatoryData()
6
12
 
7
13
  const composableMode = computed<'route' | 'session'>(() => (features.value?.composableNavigationMode === 'session' ? 'session' : 'route'))
8
14
 
9
15
  function toggleComposableMode() {
10
- const origin = getObservatoryOrigin()
11
-
12
- if (!origin) {
13
- return
14
- }
15
-
16
16
  const nextMode = composableMode.value === 'route' ? 'session' : 'route'
17
- window.top?.postMessage({ type: 'observatory:set-composable-mode', mode: nextMode }, origin)
17
+ setComposableMode(nextMode)
18
18
  }
19
19
 
20
20
  function clearSession() {
21
- const origin = getObservatoryOrigin()
22
- if (!origin) return
23
21
  clearComposables()
24
- window.top?.postMessage({ type: 'observatory:clear-composables' }, origin)
25
22
  }
26
23
 
27
24
  // ── Flat per-instance display ─────────────────────────────────────────────
@@ -89,17 +86,7 @@ function basename(file: string) {
89
86
  }
90
87
 
91
88
  function openInEditor(file: string) {
92
- if (!file || file === 'unknown') {
93
- return
94
- }
95
-
96
- const origin = getObservatoryOrigin()
97
-
98
- if (!origin) {
99
- return
100
- }
101
-
102
- window.top?.postMessage({ type: 'observatory:open-in-editor', file }, origin)
89
+ openInEditorFromStore(file)
103
90
  }
104
91
 
105
92
  const filter = ref('all')
@@ -302,40 +289,26 @@ function applyEdit() {
302
289
  return
303
290
  }
304
291
 
305
- const origin = getObservatoryOrigin()
306
-
307
- if (!origin) {
308
- return
309
- }
310
-
311
- window.top?.postMessage(
312
- {
313
- type: 'observatory:edit-composable',
314
- id: editTarget.value.id,
315
- key: editTarget.value.key,
316
- value: parsed,
317
- },
318
- origin
319
- )
292
+ editComposableValue(editTarget.value.id, editTarget.value.key, parsed)
320
293
 
321
294
  editTarget.value = null
322
295
  }
323
296
  </script>
324
297
 
325
298
  <template>
326
- <div class="view">
327
- <div class="stats-row">
299
+ <div class="composable-tracker tracker-view">
300
+ <div class="composable-tracker__stats tracker-stats-row">
328
301
  <div class="stat-card">
329
302
  <div class="stat-label">total</div>
330
303
  <div class="stat-val">{{ entries.length }}</div>
331
304
  </div>
332
305
  <div class="stat-card">
333
306
  <div class="stat-label">mounted</div>
334
- <div class="stat-val" style="color: var(--teal)">{{ counts.mounted }}</div>
307
+ <div class="stat-val stat-val--active">{{ counts.mounted }}</div>
335
308
  </div>
336
309
  <div class="stat-card">
337
310
  <div class="stat-label">leaks</div>
338
- <div class="stat-val" style="color: var(--red)">{{ counts.leaks }}</div>
311
+ <div class="stat-val stat-val--error">{{ counts.leaks }}</div>
339
312
  </div>
340
313
  <div class="stat-card">
341
314
  <div class="stat-label">instances</div>
@@ -343,38 +316,47 @@ function applyEdit() {
343
316
  </div>
344
317
  </div>
345
318
 
346
- <div class="toolbar">
319
+ <div class="composable-tracker__toolbar tracker-toolbar">
347
320
  <button :class="{ active: filter === 'all' }" @click="filter = 'all'">all</button>
348
321
  <button :class="{ active: filter === 'mounted' }" @click="filter = 'mounted'">mounted</button>
349
322
  <button :class="{ 'danger-active': filter === 'leak' }" @click="filter = 'leak'">leaks only</button>
350
323
  <button :class="{ active: filter === 'unmounted' }" @click="filter = 'unmounted'">unmounted</button>
351
324
  <button
352
- class="mode-btn"
325
+ class="composable-tracker__mode-btn"
353
326
  :title="`switch to ${composableMode === 'route' ? 'session' : 'route'} mode`"
354
327
  @click="toggleComposableMode"
355
328
  >
356
329
  mode: {{ composableMode }}
357
330
  </button>
358
- <input v-model="search" type="search" placeholder="search name, file, or ref…" style="max-width: 220px; margin-left: auto" />
359
- <button v-if="composableMode === 'session'" class="clear-btn" title="Clear session history" @click="clearSession">
331
+ <span class="tracker-toolbar__spacer"></span>
332
+ <input v-model="search" class="composable-tracker__search" type="search" placeholder="search name, file, or ref…" />
333
+ <button
334
+ v-if="composableMode === 'session'"
335
+ class="composable-tracker__clear-btn"
336
+ title="Clear session history"
337
+ @click="clearSession"
338
+ >
360
339
  clear session
361
340
  </button>
362
341
  </div>
363
342
 
364
- <div class="list">
343
+ <div class="composable-tracker__list">
365
344
  <div
366
345
  v-for="entry in filtered"
367
346
  :key="entry.id"
368
- class="comp-card"
369
- :class="{ leak: entry.leak, expanded: expanded === entry.id }"
347
+ class="composable-tracker__card"
348
+ :class="{
349
+ 'composable-tracker__card--leak': entry.leak,
350
+ 'composable-tracker__card--expanded': expanded === entry.id,
351
+ }"
370
352
  @click="expanded = expanded === entry.id ? null : entry.id"
371
353
  >
372
- <div class="comp-header">
373
- <div class="comp-identity">
374
- <span class="comp-name mono">{{ entry.name }}</span>
375
- <span class="comp-file muted mono">{{ basename(entry.componentFile) }}</span>
354
+ <div class="composable-tracker__card-header">
355
+ <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>
376
358
  </div>
377
- <div class="comp-meta">
359
+ <div class="composable-tracker__meta">
378
360
  <span v-if="entry.watcherCount > 0 && !entry.leak" class="badge badge-warn">{{ entry.watcherCount }}w</span>
379
361
  <span v-if="entry.intervalCount > 0 && !entry.leak" class="badge badge-warn">{{ entry.intervalCount }}t</span>
380
362
  <span v-if="entry.leak" class="badge badge-err">leak</span>
@@ -384,33 +366,33 @@ function applyEdit() {
384
366
  </div>
385
367
 
386
368
  <!-- Inline ref preview — shows up to 3 refs without expanding -->
387
- <div v-if="Object.keys(entry.refs).length" class="refs-preview">
369
+ <div v-if="Object.keys(entry.refs).length" class="composable-tracker__refs-preview">
388
370
  <span
389
371
  v-for="[k, v] in Object.entries(entry.refs).slice(0, 3)"
390
372
  :key="k"
391
- class="ref-chip"
373
+ class="composable-tracker__ref-chip"
392
374
  :class="{
393
- 'ref-chip--reactive': v.type === 'reactive',
394
- 'ref-chip--computed': v.type === 'computed',
395
- 'ref-chip--shared': entry.sharedKeys?.includes(k),
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),
396
378
  }"
397
379
  :title="entry.sharedKeys?.includes(k) ? 'shared global state' : ''"
398
380
  >
399
- <span class="ref-chip-key">{{ k }}</span>
400
- <span class="ref-chip-val">{{ formatVal(v.value) }}</span>
401
- <span v-if="entry.sharedKeys?.includes(k)" class="ref-chip-shared-dot" title="global"></span>
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>
402
384
  </span>
403
385
  <span v-if="Object.keys(entry.refs).length > 3" class="muted text-sm">
404
386
  +{{ Object.keys(entry.refs).length - 3 }} more
405
387
  </span>
406
388
  </div>
407
389
 
408
- <div v-if="expanded === entry.id" class="comp-detail" @click.stop>
409
- <div v-if="entry.leak" class="leak-banner">{{ entry.leakReason }}</div>
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>
410
392
 
411
393
  <!-- Global state warning -->
412
- <div v-if="entry.sharedKeys?.length" class="global-banner">
413
- <span class="global-dot"></span>
394
+ <div v-if="entry.sharedKeys?.length" class="composable-tracker__global-banner">
395
+ <span class="composable-tracker__global-dot"></span>
414
396
  <span>
415
397
  <strong>global state</strong>
416
398
  — {{ entry.sharedKeys.join(', ') }}
@@ -419,31 +401,31 @@ function applyEdit() {
419
401
  </span>
420
402
  </div>
421
403
 
422
- <div class="section-label">reactive state</div>
423
- <div v-if="!Object.keys(entry.refs).length" class="muted text-sm" style="padding: 2px 0 6px">
404
+ <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">
424
406
  no tracked state returned
425
407
  </div>
426
- <div v-for="[k, v] in Object.entries(entry.refs)" :key="k" class="ref-row">
408
+ <div v-for="[k, v] in Object.entries(entry.refs)" :key="k" class="composable-tracker__ref-row">
427
409
  <span
428
- class="mono text-sm ref-key ref-key--clickable"
410
+ class="composable-tracker__ref-key composable-tracker__ref-key--clickable mono text-sm"
429
411
  :title="`click to see all instances exposing '${k}'`"
430
412
  @click.stop="openLookup(k)"
431
413
  >
432
414
  {{ k }}
433
415
  </span>
434
416
  <span
435
- class="mono text-sm ref-val"
417
+ class="composable-tracker__ref-val mono text-sm"
436
418
  :class="{
437
- 'ref-val--full': isLongValue(v.value) && isRefExpanded(entry.id, k),
438
- 'ref-val--collapsed': isLongValue(v.value) && !isRefExpanded(entry.id, k),
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),
439
421
  }"
440
422
  >
441
423
  {{ isLongValue(v.value) && !isRefExpanded(entry.id, k) ? formatVal(v.value) : formatValFull(v.value) }}
442
424
  </span>
443
- <div class="ref-row-actions">
425
+ <div class="composable-tracker__ref-row-actions">
444
426
  <button
445
427
  v-if="isLongValue(v.value)"
446
- class="expand-btn"
428
+ class="composable-tracker__expand-btn"
447
429
  :title="isRefExpanded(entry.id, k) ? 'Collapse' : 'Expand'"
448
430
  @click.stop="toggleRefExpand(entry.id, k)"
449
431
  >
@@ -453,7 +435,7 @@ function applyEdit() {
453
435
  <span v-if="entry.sharedKeys?.includes(k)" class="badge badge-amber text-xs">global</span>
454
436
  <button
455
437
  v-if="v.type === 'ref'"
456
- class="edit-btn"
438
+ class="composable-tracker__edit-btn"
457
439
  title="Edit value"
458
440
  @click.stop="openEdit(entry.id, k, v.value)"
459
441
  >
@@ -463,103 +445,127 @@ function applyEdit() {
463
445
  </div>
464
446
 
465
447
  <template v-if="entry.history && entry.history.length">
466
- <div class="section-label" style="margin-top: 10px">
448
+ <div class="composable-tracker__section-label composable-tracker__section-label--spaced tracker-section-label">
467
449
  change history
468
- <span class="muted" style="font-weight: 400; text-transform: none; letter-spacing: 0">
469
- ({{ entry.history.length }} events)
470
- </span>
450
+ <span class="composable-tracker__section-label-meta muted">({{ entry.history.length }} events)</span>
471
451
  </div>
472
- <div class="history-list">
473
- <div v-for="(evt, idx) in [...entry.history].reverse().slice(0, 20)" :key="idx" class="history-row">
474
- <span class="history-time mono muted">+{{ (evt.t / 1000).toFixed(2) }}s</span>
475
- <span class="history-key mono">{{ evt.key }}</span>
476
- <span class="history-val mono">{{ formatVal(evt.value) }}</span>
452
+ <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>
477
461
  </div>
478
- <div v-if="entry.history.length > 20" class="muted text-sm" style="padding: 2px 0">
462
+ <div v-if="entry.history.length > 20" class="composable-tracker__compact-muted muted text-sm">
479
463
  … {{ entry.history.length - 20 }} earlier events
480
464
  </div>
481
465
  </div>
482
466
  </template>
483
467
 
484
- <div class="section-label" style="margin-top: 10px">lifecycle</div>
485
- <div v-for="row in lifecycleRows(entry)" :key="row.label" class="lc-row">
486
- <span class="lc-dot" :style="{ background: row.ok ? 'var(--teal)' : 'var(--red)' }"></span>
487
- <span class="muted text-sm" style="min-width: 120px">{{ row.label }}</span>
488
- <span class="text-sm" :style="{ color: row.ok ? 'var(--teal)' : 'var(--red)' }">{{ row.status }}</span>
468
+ <div class="composable-tracker__section-label composable-tracker__section-label--spaced tracker-section-label">
469
+ lifecycle
470
+ </div>
471
+ <div v-for="row in lifecycleRows(entry)" :key="row.label" class="composable-tracker__lifecycle-row">
472
+ <span
473
+ class="composable-tracker__lifecycle-dot"
474
+ :class="row.ok ? 'composable-tracker__lifecycle-dot--ok' : 'composable-tracker__lifecycle-dot--error'"
475
+ ></span>
476
+ <span class="composable-tracker__context-label muted text-sm">{{ row.label }}</span>
477
+ <span
478
+ class="composable-tracker__lifecycle-status text-sm"
479
+ :class="row.ok ? 'composable-tracker__lifecycle-status--ok' : 'composable-tracker__lifecycle-status--error'"
480
+ >
481
+ {{ row.status }}
482
+ </span>
489
483
  </div>
490
484
 
491
- <div class="section-label" style="margin-top: 10px">context</div>
492
- <div class="lc-row">
493
- <span class="muted text-sm" style="min-width: 120px">component</span>
494
- <span class="mono text-sm">{{ basename(entry.componentFile) }}</span>
485
+ <div class="composable-tracker__section-label composable-tracker__section-label--spaced tracker-section-label">
486
+ context
495
487
  </div>
496
- <div class="lc-row">
497
- <span class="muted text-sm" style="min-width: 120px">uid</span>
498
- <span class="mono text-sm muted">{{ entry.componentUid }}</span>
488
+ <div class="composable-tracker__lifecycle-row">
489
+ <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>
499
491
  </div>
500
- <div class="lc-row">
501
- <span class="muted text-sm" style="min-width: 120px">defined in</span>
502
- <span class="mono text-sm muted" style="display: flex; align-items: center; gap: 6px">
492
+ <div class="composable-tracker__lifecycle-row">
493
+ <span class="composable-tracker__context-label muted text-sm">uid</span>
494
+ <span class="composable-tracker__context-value composable-tracker__context-value--muted mono text-sm muted">
495
+ {{ entry.componentUid }}
496
+ </span>
497
+ </div>
498
+ <div class="composable-tracker__lifecycle-row">
499
+ <span class="composable-tracker__context-label muted text-sm">defined in</span>
500
+ <span
501
+ class="composable-tracker__context-value composable-tracker__context-value--row composable-tracker__context-value--muted mono text-sm muted"
502
+ >
503
503
  {{ entry.file }}:{{ entry.line }}
504
- <button class="jump-btn" title="Open in editor" @click.stop="openInEditor(entry.file)">open ↗</button>
504
+ <button class="composable-tracker__jump-btn" title="Open in editor" @click.stop="openInEditor(entry.file)">
505
+ open ↗
506
+ </button>
505
507
  </span>
506
508
  </div>
507
- <div class="lc-row">
508
- <span class="muted text-sm" style="min-width: 120px">route</span>
509
- <span class="mono text-sm muted">{{ entry.route }}</span>
509
+ <div class="composable-tracker__lifecycle-row">
510
+ <span class="composable-tracker__context-label muted text-sm">route</span>
511
+ <span class="composable-tracker__context-value composable-tracker__context-value--muted mono text-sm muted">
512
+ {{ entry.route }}
513
+ </span>
510
514
  </div>
511
- <div class="lc-row">
512
- <span class="muted text-sm" style="min-width: 120px">watchers</span>
513
- <span class="mono text-sm">{{ entry.watcherCount }}</span>
515
+ <div class="composable-tracker__lifecycle-row">
516
+ <span class="composable-tracker__context-label muted text-sm">watchers</span>
517
+ <span class="composable-tracker__context-value mono text-sm">{{ entry.watcherCount }}</span>
514
518
  </div>
515
- <div class="lc-row">
516
- <span class="muted text-sm" style="min-width: 120px">intervals</span>
517
- <span class="mono text-sm">{{ entry.intervalCount }}</span>
519
+ <div class="composable-tracker__lifecycle-row">
520
+ <span class="composable-tracker__context-label muted text-sm">intervals</span>
521
+ <span class="composable-tracker__context-value mono text-sm">{{ entry.intervalCount }}</span>
518
522
  </div>
519
523
  </div>
520
524
  </div>
521
525
 
522
- <div v-if="!filtered.length" class="muted text-sm" style="padding: 16px 0">
526
+ <div v-if="!filtered.length" class="composable-tracker__empty muted text-sm">
523
527
  {{ connected ? 'No composables recorded yet.' : 'Waiting for connection to the Nuxt app…' }}
524
528
  </div>
525
529
  </div>
526
530
 
527
531
  <!-- ── Reverse lookup panel ──────────────────────────────────────── -->
528
532
  <Transition name="slide">
529
- <div v-if="lookupKey" class="lookup-panel">
530
- <div class="lookup-header">
533
+ <div v-if="lookupKey" class="composable-tracker__lookup-panel">
534
+ <div class="composable-tracker__lookup-header">
531
535
  <span class="mono text-sm">{{ lookupKey }}</span>
532
536
  <span class="muted text-sm">— {{ lookupResults.length }} instance{{ lookupResults.length !== 1 ? 's' : '' }}</span>
533
- <button class="clear-btn" style="margin-left: auto" @click="lookupKey = null">✕</button>
537
+ <button class="composable-tracker__clear-btn composable-tracker__lookup-close" @click="lookupKey = null">✕</button>
538
+ </div>
539
+ <div v-if="!lookupResults.length" class="composable-tracker__lookup-empty muted text-sm">
540
+ No mounted instances expose this key.
534
541
  </div>
535
- <div v-if="!lookupResults.length" class="muted text-sm" style="padding: 6px 0">No mounted instances expose this key.</div>
536
- <div v-for="r in lookupResults" :key="r.id" class="lookup-row">
542
+ <div v-for="r in lookupResults" :key="r.id" class="composable-tracker__lookup-row">
537
543
  <span class="mono text-sm">{{ r.name }}</span>
538
544
  <span class="muted text-sm">{{ basename(r.componentFile) }}</span>
539
- <span class="muted text-sm" style="margin-left: auto">{{ r.route }}</span>
545
+ <span class="composable-tracker__lookup-route muted text-sm">{{ r.route }}</span>
540
546
  </div>
541
547
  </div>
542
548
  </Transition>
543
549
 
544
550
  <!-- ── Edit value dialog ─────────────────────────────────────────── -->
545
551
  <Transition name="fade">
546
- <div v-if="editTarget" class="edit-overlay" @click.self="editTarget = null">
547
- <div class="edit-dialog">
548
- <div class="edit-dialog-header">
552
+ <div v-if="editTarget" class="composable-tracker__edit-overlay" @click.self="editTarget = null">
553
+ <div class="composable-tracker__edit-dialog">
554
+ <div class="composable-tracker__edit-dialog-header">
549
555
  edit
550
556
  <span class="mono">{{ editTarget.key }}</span>
551
- <button class="clear-btn" style="margin-left: auto" @click="editTarget = null">✕</button>
557
+ <button class="composable-tracker__clear-btn composable-tracker__dialog-close" @click="editTarget = null">✕</button>
552
558
  </div>
553
- <p class="muted text-sm" style="padding: 4px 0 8px">
559
+ <p class="composable-tracker__edit-help muted text-sm">
554
560
  Value applied immediately to the live ref. Only
555
561
  <span class="mono">ref</span>
556
562
  values are writable.
557
563
  </p>
558
- <textarea v-model="editTarget.rawValue" class="edit-textarea" rows="6" spellcheck="false" />
559
- <div v-if="editError" class="edit-error text-sm">{{ editError }}</div>
560
- <div class="edit-actions">
564
+ <textarea v-model="editTarget.rawValue" class="composable-tracker__edit-textarea" rows="6" spellcheck="false" />
565
+ <div v-if="editError" class="composable-tracker__edit-error text-sm">{{ editError }}</div>
566
+ <div class="composable-tracker__edit-actions">
561
567
  <button @click="applyEdit">apply</button>
562
- <button class="clear-btn" @click="editTarget = null">cancel</button>
568
+ <button class="composable-tracker__clear-btn" @click="editTarget = null">cancel</button>
563
569
  </div>
564
570
  </div>
565
571
  </div>
@@ -568,239 +574,222 @@ function applyEdit() {
568
574
  </template>
569
575
 
570
576
  <style scoped>
571
- .view {
572
- display: flex;
573
- flex-direction: column;
574
- height: 100%;
575
- overflow: hidden;
576
- padding: 12px;
577
- gap: 10px;
578
- }
579
-
580
- .stats-row {
581
- display: grid;
582
- grid-template-columns: repeat(4, minmax(0, 1fr));
583
- gap: 8px;
584
- flex-shrink: 0;
585
- }
586
-
587
- .toolbar {
588
- display: flex;
589
- align-items: center;
590
- gap: 6px;
591
- flex-shrink: 0;
592
- flex-wrap: wrap;
593
- }
594
-
595
- .clear-btn {
577
+ .composable-tracker__clear-btn {
596
578
  color: var(--text3);
597
579
  border-color: var(--border);
598
580
  flex-shrink: 0;
599
581
  }
600
582
 
601
- .clear-btn:hover {
583
+ .composable-tracker__clear-btn:hover {
602
584
  color: var(--red);
603
585
  border-color: var(--red);
604
586
  background: transparent;
605
587
  }
606
588
 
607
- .mode-btn {
589
+ .composable-tracker__mode-btn {
608
590
  border-color: color-mix(in srgb, var(--blue) 40%, var(--border));
609
591
  color: var(--blue);
610
592
  }
611
593
 
612
- .mode-btn:hover {
594
+ .composable-tracker__mode-btn:hover {
613
595
  border-color: var(--blue);
614
596
  background: color-mix(in srgb, var(--blue) 12%, transparent);
615
597
  }
616
598
 
617
- .list {
599
+ .composable-tracker__search {
600
+ max-width: 220px;
601
+ }
602
+
603
+ .composable-tracker__list {
618
604
  flex: 1;
619
605
  overflow: auto;
620
606
  display: flex;
621
607
  flex-direction: column;
622
- gap: 5px;
608
+ gap: var(--tracker-space-2);
623
609
  min-height: 0;
624
610
  }
625
611
 
626
- .comp-card {
612
+ .composable-tracker__card {
627
613
  background: var(--bg3);
628
- border: 0.5px solid var(--border);
614
+ border: var(--tracker-border-width) solid var(--border);
629
615
  border-radius: var(--radius-lg);
630
616
  overflow: hidden;
631
617
  cursor: pointer;
632
618
  flex-shrink: 0;
633
619
  }
634
620
 
635
- .comp-card:hover {
621
+ .composable-tracker__card:hover {
636
622
  border-color: var(--text3);
637
623
  }
638
624
 
639
- .comp-card.leak {
625
+ .composable-tracker__card--leak {
640
626
  border-left: 2px solid var(--red);
641
627
  border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
642
628
  }
643
629
 
644
- .comp-card.expanded {
630
+ .composable-tracker__card--expanded {
645
631
  border-color: var(--purple);
646
632
  }
647
633
 
648
- .comp-header {
634
+ .composable-tracker__card-header {
649
635
  display: flex;
650
636
  align-items: center;
651
637
  justify-content: space-between;
652
- padding: 8px 12px;
653
- gap: 8px;
638
+ padding: var(--tracker-space-3) var(--tracker-space-4);
639
+ gap: var(--tracker-space-3);
654
640
  }
655
641
 
656
- .comp-identity {
642
+ .composable-tracker__identity {
657
643
  display: flex;
658
644
  align-items: baseline;
659
- gap: 6px;
645
+ gap: var(--tracker-space-2);
660
646
  min-width: 0;
661
647
  flex: 1;
662
648
  }
663
649
 
664
- .comp-name {
665
- font-size: 12px;
650
+ .composable-tracker__name {
651
+ font-size: var(--tracker-font-size-md);
666
652
  font-weight: 500;
667
653
  color: var(--text);
668
654
  flex-shrink: 0;
669
655
  }
670
656
 
671
- .comp-file {
672
- font-size: 11px;
657
+ .composable-tracker__file {
658
+ font-size: var(--tracker-font-size-sm);
673
659
  color: var(--text3);
674
660
  overflow: hidden;
675
661
  text-overflow: ellipsis;
676
662
  white-space: nowrap;
677
663
  }
678
664
 
679
- .comp-meta {
665
+ .composable-tracker__meta {
680
666
  display: flex;
681
667
  align-items: center;
682
- gap: 5px;
668
+ gap: var(--tracker-space-2);
683
669
  flex-shrink: 0;
684
670
  }
685
671
 
686
- /* Inline ref preview chips */
687
- .refs-preview {
672
+ .composable-tracker__refs-preview {
688
673
  display: flex;
689
674
  flex-wrap: wrap;
690
- gap: 4px;
691
- padding: 0 12px 8px;
675
+ gap: var(--tracker-space-1);
676
+ padding: 0 var(--tracker-space-4) var(--tracker-space-3);
692
677
  align-items: center;
693
678
  }
694
679
 
695
- .ref-chip {
680
+ .composable-tracker__ref-chip {
696
681
  display: inline-flex;
697
682
  align-items: center;
698
- gap: 4px;
683
+ gap: var(--tracker-space-1);
699
684
  padding: 2px 7px;
700
685
  border-radius: 4px;
701
686
  background: var(--bg2);
702
- border: 0.5px solid var(--border);
703
- font-size: 11px;
687
+ border: var(--tracker-border-width) solid var(--border);
688
+ font-size: var(--tracker-font-size-sm);
704
689
  font-family: var(--mono);
705
690
  max-width: 220px;
706
691
  overflow: hidden;
707
692
  }
708
693
 
709
- .ref-chip--reactive {
694
+ .composable-tracker__ref-chip--reactive {
710
695
  border-color: color-mix(in srgb, var(--purple) 40%, var(--border));
711
696
  background: color-mix(in srgb, var(--purple) 8%, var(--bg2));
712
697
  }
713
698
 
714
- .ref-chip--computed {
699
+ .composable-tracker__ref-chip--computed {
715
700
  border-color: color-mix(in srgb, var(--blue) 40%, var(--border));
716
701
  background: color-mix(in srgb, var(--blue) 8%, var(--bg2));
717
702
  }
718
703
 
719
- .ref-chip-key {
704
+ .composable-tracker__ref-chip-key {
720
705
  color: var(--text2);
721
706
  flex-shrink: 0;
722
707
  }
723
708
 
724
- .ref-chip-val {
709
+ .composable-tracker__ref-chip-val {
725
710
  color: var(--teal);
726
711
  overflow: hidden;
727
712
  text-overflow: ellipsis;
728
713
  white-space: nowrap;
729
714
  }
730
715
 
731
- /* Expanded detail */
732
- .comp-detail {
733
- padding: 4px 12px 12px;
734
- border-top: 0.5px solid var(--border);
716
+ .composable-tracker__detail {
717
+ padding: var(--tracker-space-1) var(--tracker-space-4) var(--tracker-space-4);
718
+ border-top: var(--tracker-border-width) solid var(--border);
735
719
  display: flex;
736
720
  flex-direction: column;
737
- gap: 3px;
721
+ gap: var(--tracker-space-1);
738
722
  }
739
723
 
740
- .leak-banner {
724
+ .composable-tracker__leak-banner {
741
725
  background: color-mix(in srgb, var(--red) 12%, transparent);
742
- border: 0.5px solid color-mix(in srgb, var(--red) 40%, var(--border));
726
+ border: var(--tracker-border-width) solid color-mix(in srgb, var(--red) 40%, var(--border));
743
727
  border-radius: var(--radius);
744
728
  padding: 6px 10px;
745
- font-size: 11px;
729
+ font-size: var(--tracker-font-size-sm);
746
730
  color: var(--red);
747
- margin-bottom: 6px;
731
+ margin-bottom: var(--tracker-space-2);
748
732
  font-family: var(--mono);
749
733
  }
750
734
 
751
- .section-label {
752
- font-size: 10px;
753
- font-weight: 500;
754
- text-transform: uppercase;
755
- letter-spacing: 0.4px;
756
- color: var(--text3);
757
- margin-top: 6px;
758
- margin-bottom: 3px;
735
+ .composable-tracker__section-label {
736
+ margin-top: var(--tracker-space-2);
737
+ margin-bottom: var(--tracker-space-1);
738
+ }
739
+
740
+ .composable-tracker__section-label--spaced {
741
+ margin-top: var(--tracker-space-4);
742
+ }
743
+
744
+ .composable-tracker__section-label-meta {
745
+ font-weight: 400;
746
+ text-transform: none;
747
+ letter-spacing: 0;
759
748
  }
760
749
 
761
- .ref-row {
750
+ .composable-tracker__ref-row {
762
751
  display: flex;
763
752
  align-items: flex-start;
764
- gap: 8px;
753
+ gap: var(--tracker-space-3);
765
754
  padding: 3px 0;
766
755
  }
767
756
 
768
- .ref-key {
757
+ .composable-tracker__ref-key {
769
758
  min-width: 90px;
770
759
  color: var(--text2);
771
760
  flex-shrink: 0;
772
761
  }
773
762
 
774
- .ref-val {
763
+ .composable-tracker__ref-val {
775
764
  flex: 1;
776
765
  color: var(--teal);
777
766
  min-width: 0;
778
767
  }
779
768
 
780
- .ref-val--collapsed {
769
+ .composable-tracker__ref-val--collapsed {
781
770
  overflow: hidden;
782
771
  text-overflow: ellipsis;
783
772
  white-space: nowrap;
784
773
  }
785
774
 
786
- .ref-val--full {
775
+ .composable-tracker__ref-val--full {
787
776
  white-space: pre-wrap;
788
777
  word-break: break-all;
789
778
  line-height: 1.5;
790
779
  }
791
780
 
792
- .ref-row-actions {
781
+ .composable-tracker__ref-row-actions {
793
782
  display: flex;
794
783
  align-items: center;
795
- gap: 4px;
784
+ gap: var(--tracker-space-1);
796
785
  flex-shrink: 0;
797
786
  }
798
787
 
799
- .expand-btn {
800
- font-size: 9px;
788
+ .composable-tracker__expand-btn {
789
+ font-size: var(--tracker-font-size-xs);
801
790
  padding: 1px 5px;
802
791
  border-radius: 4px;
803
- border: 0.5px solid var(--border);
792
+ border: var(--tracker-border-width) solid var(--border);
804
793
  background: var(--bg2);
805
794
  color: var(--text3);
806
795
  cursor: pointer;
@@ -808,31 +797,66 @@ function applyEdit() {
808
797
  flex-shrink: 0;
809
798
  }
810
799
 
811
- .expand-btn:hover {
800
+ .composable-tracker__expand-btn:hover {
812
801
  border-color: var(--text3);
813
802
  color: var(--text);
814
803
  }
815
804
 
816
- .lc-row {
805
+ .composable-tracker__lifecycle-row {
817
806
  display: flex;
818
807
  align-items: center;
819
- gap: 8px;
808
+ gap: var(--tracker-space-3);
820
809
  padding: 2px 0;
821
810
  }
822
811
 
823
- .lc-dot {
812
+ .composable-tracker__lifecycle-dot {
824
813
  width: 6px;
825
814
  height: 6px;
826
815
  border-radius: 50%;
827
816
  flex-shrink: 0;
828
817
  }
829
818
 
830
- .ref-chip--shared {
819
+ .composable-tracker__lifecycle-dot--ok {
820
+ background: var(--teal);
821
+ }
822
+
823
+ .composable-tracker__lifecycle-dot--error {
824
+ background: var(--red);
825
+ }
826
+
827
+ .composable-tracker__lifecycle-status--ok {
828
+ color: var(--teal);
829
+ }
830
+
831
+ .composable-tracker__lifecycle-status--error {
832
+ color: var(--red);
833
+ }
834
+
835
+ .composable-tracker__context-label {
836
+ min-width: 120px;
837
+ }
838
+
839
+ .composable-tracker__context-value {
840
+ min-width: 0;
841
+ }
842
+
843
+ .composable-tracker__context-value--row {
844
+ display: flex;
845
+ align-items: center;
846
+ gap: var(--tracker-space-2);
847
+ flex-wrap: wrap;
848
+ }
849
+
850
+ .composable-tracker__context-value--muted {
851
+ color: var(--text3);
852
+ }
853
+
854
+ .composable-tracker__ref-chip--shared {
831
855
  border-color: color-mix(in srgb, var(--amber) 50%, var(--border));
832
856
  background: color-mix(in srgb, var(--amber) 10%, var(--bg2));
833
857
  }
834
858
 
835
- .ref-chip-shared-dot {
859
+ .composable-tracker__ref-chip-shared-dot {
836
860
  display: inline-block;
837
861
  width: 5px;
838
862
  height: 5px;
@@ -842,20 +866,20 @@ function applyEdit() {
842
866
  margin-left: 1px;
843
867
  }
844
868
 
845
- .global-banner {
869
+ .composable-tracker__global-banner {
846
870
  display: flex;
847
871
  align-items: flex-start;
848
- gap: 8px;
872
+ gap: var(--tracker-space-3);
849
873
  background: color-mix(in srgb, var(--amber) 10%, transparent);
850
- border: 0.5px solid color-mix(in srgb, var(--amber) 40%, var(--border));
874
+ border: var(--tracker-border-width) solid color-mix(in srgb, var(--amber) 40%, var(--border));
851
875
  border-radius: var(--radius);
852
876
  padding: 7px 10px;
853
- font-size: 11px;
877
+ font-size: var(--tracker-font-size-sm);
854
878
  color: var(--text2);
855
- margin-bottom: 6px;
879
+ margin-bottom: var(--tracker-space-2);
856
880
  }
857
881
 
858
- .global-dot {
882
+ .composable-tracker__global-dot {
859
883
  display: inline-block;
860
884
  width: 6px;
861
885
  height: 6px;
@@ -871,7 +895,7 @@ function applyEdit() {
871
895
  border: 0.5px solid color-mix(in srgb, var(--amber) 40%, var(--border));
872
896
  }
873
897
 
874
- .history-list {
898
+ .composable-tracker__history-list {
875
899
  display: flex;
876
900
  flex-direction: column;
877
901
  gap: 1px;
@@ -882,33 +906,33 @@ function applyEdit() {
882
906
  overflow-y: auto;
883
907
  }
884
908
 
885
- .history-row {
909
+ .composable-tracker__history-row {
886
910
  display: flex;
887
911
  align-items: center;
888
- gap: 8px;
912
+ gap: var(--tracker-space-3);
889
913
  padding: 2px 0;
890
- font-size: 11px;
914
+ font-size: var(--tracker-font-size-sm);
891
915
  font-family: var(--mono);
892
- border-bottom: 0.5px solid var(--border);
916
+ border-bottom: var(--tracker-border-width) solid var(--border);
893
917
  }
894
918
 
895
- .history-row:last-child {
919
+ .composable-tracker__history-row:last-child {
896
920
  border-bottom: none;
897
921
  }
898
922
 
899
- .history-time {
923
+ .composable-tracker__history-time {
900
924
  min-width: 52px;
901
925
  color: var(--text3);
902
926
  flex-shrink: 0;
903
927
  }
904
928
 
905
- .history-key {
929
+ .composable-tracker__history-key {
906
930
  min-width: 80px;
907
931
  color: var(--text2);
908
932
  flex-shrink: 0;
909
933
  }
910
934
 
911
- .history-val {
935
+ .composable-tracker__history-val {
912
936
  color: var(--amber);
913
937
  overflow: hidden;
914
938
  text-overflow: ellipsis;
@@ -916,48 +940,22 @@ function applyEdit() {
916
940
  flex: 1;
917
941
  }
918
942
 
919
- /* Stat cards */
920
- .stat-card {
921
- background: var(--bg3);
922
- border: 0.5px solid var(--border);
923
- border-radius: var(--radius-lg);
924
- padding: 10px 14px;
925
- }
926
-
927
- .stat-label {
928
- font-size: 10px;
929
- font-weight: 500;
930
- text-transform: uppercase;
931
- letter-spacing: 0.4px;
932
- color: var(--text3);
933
- margin-bottom: 4px;
934
- }
935
-
936
- .stat-val {
937
- font-size: 22px;
938
- font-weight: 500;
939
- line-height: 1;
940
- color: var(--text);
941
- }
942
-
943
- /* Clickable ref key */
944
- .ref-key--clickable {
943
+ .composable-tracker__ref-key--clickable {
945
944
  cursor: pointer;
946
945
  text-decoration: underline dotted var(--text3);
947
946
  text-underline-offset: 2px;
948
947
  }
949
948
 
950
- .ref-key--clickable:hover {
949
+ .composable-tracker__ref-key--clickable:hover {
951
950
  color: var(--purple);
952
951
  text-decoration-color: var(--purple);
953
952
  }
954
953
 
955
- /* Edit button inline with ref row */
956
- .edit-btn {
957
- font-size: 10px;
954
+ .composable-tracker__edit-btn {
955
+ font-size: var(--tracker-font-size-xs);
958
956
  padding: 1px 6px;
959
957
  border-radius: var(--radius);
960
- border: 0.5px solid var(--border);
958
+ border: var(--tracker-border-width) solid var(--border);
961
959
  background: transparent;
962
960
  color: var(--text3);
963
961
  cursor: pointer;
@@ -966,44 +964,60 @@ function applyEdit() {
966
964
  font-family: var(--mono);
967
965
  }
968
966
 
969
- .edit-btn:hover {
967
+ .composable-tracker__edit-btn:hover {
970
968
  border-color: var(--purple);
971
969
  color: var(--purple);
972
970
  background: color-mix(in srgb, var(--purple) 8%, transparent);
973
971
  }
974
972
 
975
- /* Reverse lookup panel — appears below the list */
976
- .lookup-panel {
973
+ .composable-tracker__empty {
974
+ padding: 16px 0;
975
+ }
976
+
977
+ .composable-tracker__compact-muted {
978
+ padding: 2px 0 6px;
979
+ }
980
+
981
+ .composable-tracker__lookup-panel {
977
982
  flex-shrink: 0;
978
- border: 0.5px solid var(--border);
983
+ border: var(--tracker-border-width) solid var(--border);
979
984
  border-radius: var(--radius-lg);
980
985
  background: var(--bg3);
981
986
  overflow: hidden;
982
987
  }
983
988
 
984
- .lookup-header {
989
+ .composable-tracker__lookup-header {
985
990
  display: flex;
986
991
  align-items: center;
987
- gap: 6px;
992
+ gap: var(--tracker-space-2);
988
993
  padding: 7px 12px;
989
- border-bottom: 0.5px solid var(--border);
994
+ border-bottom: var(--tracker-border-width) solid var(--border);
990
995
  background: var(--bg2);
991
996
  }
992
997
 
993
- .lookup-row {
998
+ .composable-tracker__lookup-close,
999
+ .composable-tracker__dialog-close,
1000
+ .composable-tracker__lookup-route {
1001
+ margin-left: auto;
1002
+ }
1003
+
1004
+ .composable-tracker__lookup-empty {
1005
+ padding: 6px 12px;
1006
+ }
1007
+
1008
+ .composable-tracker__lookup-row {
994
1009
  display: flex;
995
1010
  align-items: center;
996
- gap: 8px;
1011
+ gap: var(--tracker-space-3);
997
1012
  padding: 5px 12px;
998
- border-bottom: 0.5px solid var(--border);
1013
+ border-bottom: var(--tracker-border-width) solid var(--border);
999
1014
  }
1000
1015
 
1001
- .lookup-row:last-child {
1016
+ .composable-tracker__lookup-row:last-child {
1002
1017
  border-bottom: none;
1003
1018
  }
1004
1019
 
1005
- /* Edit overlay + dialog */
1006
- .edit-overlay {
1020
+ .composable-tracker__edit-overlay {
1007
1021
  position: fixed;
1008
1022
  inset: 0;
1009
1023
  background: rgb(0 0 0 / 40%);
@@ -1013,9 +1027,9 @@ function applyEdit() {
1013
1027
  justify-content: center;
1014
1028
  }
1015
1029
 
1016
- .edit-dialog {
1030
+ .composable-tracker__edit-dialog {
1017
1031
  background: var(--bg1, var(--bg2));
1018
- border: 0.5px solid var(--border);
1032
+ border: var(--tracker-border-width) solid var(--border);
1019
1033
  border-radius: var(--radius-lg);
1020
1034
  padding: 14px 16px;
1021
1035
  width: 380px;
@@ -1026,44 +1040,47 @@ function applyEdit() {
1026
1040
  box-shadow: 0 8px 32px rgb(0 0 0 / 30%);
1027
1041
  }
1028
1042
 
1029
- .edit-dialog-header {
1043
+ .composable-tracker__edit-dialog-header {
1030
1044
  display: flex;
1031
1045
  align-items: center;
1032
- gap: 6px;
1033
- font-size: 12px;
1046
+ gap: var(--tracker-space-2);
1047
+ font-size: var(--tracker-font-size-md);
1034
1048
  color: var(--text2);
1035
1049
  margin-bottom: 2px;
1036
1050
  }
1037
1051
 
1038
- .edit-textarea {
1052
+ .composable-tracker__edit-help {
1053
+ padding: 4px 0 8px;
1054
+ }
1055
+
1056
+ .composable-tracker__edit-textarea {
1039
1057
  width: 100%;
1040
1058
  font-family: var(--mono);
1041
- font-size: 12px;
1059
+ font-size: var(--tracker-font-size-md);
1042
1060
  padding: 8px 10px;
1043
1061
  background: var(--bg2);
1044
- border: 0.5px solid var(--border);
1062
+ border: var(--tracker-border-width) solid var(--border);
1045
1063
  border-radius: var(--radius);
1046
1064
  color: var(--text);
1047
1065
  resize: vertical;
1048
1066
  outline: none;
1049
1067
  }
1050
1068
 
1051
- .edit-textarea:focus {
1069
+ .composable-tracker__edit-textarea:focus {
1052
1070
  border-color: var(--purple);
1053
1071
  }
1054
1072
 
1055
- .edit-error {
1073
+ .composable-tracker__edit-error {
1056
1074
  color: var(--red);
1057
1075
  font-family: var(--mono);
1058
1076
  }
1059
1077
 
1060
- .edit-actions {
1078
+ .composable-tracker__edit-actions {
1061
1079
  display: flex;
1062
- gap: 6px;
1080
+ gap: var(--tracker-space-2);
1063
1081
  padding-top: 4px;
1064
1082
  }
1065
1083
 
1066
- /* Slide / fade transitions for the panels */
1067
1084
  .slide-enter-active,
1068
1085
  .slide-leave-active {
1069
1086
  transition:
@@ -1087,10 +1104,10 @@ function applyEdit() {
1087
1104
  opacity: 0;
1088
1105
  }
1089
1106
 
1090
- .jump-btn {
1091
- font-size: 10px;
1107
+ .composable-tracker__jump-btn {
1108
+ font-size: var(--tracker-font-size-xs);
1092
1109
  padding: 1px 6px;
1093
- border: 0.5px solid var(--border);
1110
+ border: var(--tracker-border-width) solid var(--border);
1094
1111
  border-radius: var(--radius);
1095
1112
  background: transparent;
1096
1113
  color: var(--text3);
@@ -1099,9 +1116,25 @@ function applyEdit() {
1099
1116
  font-family: var(--mono);
1100
1117
  }
1101
1118
 
1102
- .jump-btn:hover {
1119
+ .composable-tracker__jump-btn:hover {
1103
1120
  border-color: var(--teal);
1104
1121
  color: var(--teal);
1105
1122
  background: color-mix(in srgb, var(--teal) 8%, transparent);
1106
1123
  }
1124
+
1125
+ @media (width <= 900px) {
1126
+ .composable-tracker__card-header,
1127
+ .composable-tracker__ref-row,
1128
+ .composable-tracker__lifecycle-row,
1129
+ .composable-tracker__lookup-row {
1130
+ flex-wrap: wrap;
1131
+ }
1132
+
1133
+ .composable-tracker__identity,
1134
+ .composable-tracker__context-label,
1135
+ .composable-tracker__lookup-route {
1136
+ min-width: 100%;
1137
+ margin-left: 0;
1138
+ }
1139
+ }
1107
1140
  </style>