nuxt-devtools-observatory 0.1.11 → 0.1.13
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.
- package/LICENSE +9 -0
- package/README.md +114 -31
- package/client/dist/assets/index-BFrWlkvI.js +17 -0
- package/client/dist/assets/index-BUQNNbrq.css +1 -0
- package/client/dist/index.html +2 -2
- package/client/src/stores/observatory.ts +59 -15
- package/client/src/views/ComposableTracker.vue +685 -101
- package/client/src/views/ProvideInjectGraph.vue +232 -61
- package/client/src/views/RenderHeatmap.vue +138 -37
- package/client/src/views/TransitionTimeline.vue +2 -2
- package/client/src/views/ValueInspector.vue +124 -0
- package/dist/module.json +1 -1
- package/dist/runtime/composables/composable-registry.d.ts +21 -0
- package/dist/runtime/composables/composable-registry.js +208 -53
- package/dist/runtime/composables/provide-inject-registry.d.ts +13 -1
- package/dist/runtime/composables/provide-inject-registry.js +34 -6
- package/dist/runtime/composables/render-registry.d.ts +18 -8
- package/dist/runtime/composables/render-registry.js +29 -35
- package/dist/runtime/composables/transition-registry.js +13 -7
- package/dist/runtime/plugin.js +49 -17
- package/package.json +7 -3
- package/client/dist/assets/index--Igqz_EM.js +0 -17
- package/client/dist/assets/index-BoC4M4Nb.css +0 -1
|
@@ -1,74 +1,64 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed } from 'vue'
|
|
3
|
-
import { useObservatoryData, type ComposableEntry as RuntimeComposableEntry } from '../stores/observatory'
|
|
3
|
+
import { useObservatoryData, getObservatoryOrigin, type ComposableEntry as RuntimeComposableEntry } from '../stores/observatory'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
leakReason?: string
|
|
13
|
-
refs: Array<{ key: string; type: string; val: string }>
|
|
14
|
-
watchers: number
|
|
15
|
-
intervals: number
|
|
16
|
-
lifecycle: {
|
|
17
|
-
onMounted: boolean
|
|
18
|
-
onUnmounted: boolean
|
|
19
|
-
watchersCleaned: boolean
|
|
20
|
-
intervalsCleaned: boolean
|
|
5
|
+
const { composables: rawEntries, connected, clearComposables } = useObservatoryData()
|
|
6
|
+
|
|
7
|
+
function clearSession() {
|
|
8
|
+
const origin = getObservatoryOrigin()
|
|
9
|
+
|
|
10
|
+
if (!origin) {
|
|
11
|
+
return
|
|
21
12
|
}
|
|
13
|
+
|
|
14
|
+
clearComposables()
|
|
15
|
+
window.top?.postMessage({ type: 'observatory:clear-composables' }, origin)
|
|
22
16
|
}
|
|
23
17
|
|
|
24
|
-
|
|
18
|
+
// ── Flat per-instance display ─────────────────────────────────────────────
|
|
19
|
+
// Each entry is shown individually so you can see exactly which component
|
|
20
|
+
// instance uses which state. No grouping — instanceA and instanceB of the
|
|
21
|
+
// same composable in different components are two separate rows.
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
function formatVal(v: unknown): string {
|
|
24
|
+
if (v === null) {
|
|
25
|
+
return 'null'
|
|
26
|
+
}
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const list = groups.get(key) ?? []
|
|
32
|
-
list.push(entry)
|
|
33
|
-
groups.set(key, list)
|
|
28
|
+
if (v === undefined) {
|
|
29
|
+
return 'undefined'
|
|
34
30
|
}
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
leakReason: leakReasons.join(' · ') || undefined,
|
|
48
|
-
refs: Object.entries(latest.refs).map(([refKey, refValue]) => ({
|
|
49
|
-
key: refKey,
|
|
50
|
-
type: refValue.type,
|
|
51
|
-
val: typeof refValue.value === 'string' ? refValue.value : JSON.stringify(refValue.value),
|
|
52
|
-
})),
|
|
53
|
-
watchers: group.reduce((sum, entry) => sum + entry.watcherCount, 0),
|
|
54
|
-
intervals: group.reduce((sum, entry) => sum + entry.intervalCount, 0),
|
|
55
|
-
lifecycle: {
|
|
56
|
-
onMounted: group.some((entry) => entry.lifecycle.hasOnMounted),
|
|
57
|
-
onUnmounted: group.some((entry) => entry.lifecycle.hasOnUnmounted),
|
|
58
|
-
watchersCleaned: group.every((entry) => entry.lifecycle.watchersCleaned),
|
|
59
|
-
intervalsCleaned: group.every((entry) => entry.lifecycle.intervalsCleaned),
|
|
60
|
-
},
|
|
32
|
+
if (typeof v === 'string') {
|
|
33
|
+
return `"${v}"`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof v === 'object') {
|
|
37
|
+
try {
|
|
38
|
+
const s = JSON.stringify(v)
|
|
39
|
+
|
|
40
|
+
return s.length > 80 ? s.slice(0, 80) + '…' : s
|
|
41
|
+
} catch {
|
|
42
|
+
return String(v)
|
|
61
43
|
}
|
|
62
|
-
}
|
|
63
|
-
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return String(v)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function basename(file: string) {
|
|
50
|
+
return file.split('/').pop() ?? file
|
|
51
|
+
}
|
|
64
52
|
|
|
65
53
|
const filter = ref('all')
|
|
66
54
|
const search = ref('')
|
|
67
55
|
const expanded = ref<string | null>(null)
|
|
68
56
|
|
|
57
|
+
const entries = computed<RuntimeComposableEntry[]>(() => rawEntries.value)
|
|
58
|
+
|
|
69
59
|
const counts = computed(() => ({
|
|
70
|
-
mounted: entries.value.filter((
|
|
71
|
-
leaks: entries.value.filter((
|
|
60
|
+
mounted: entries.value.filter((e) => e.status === 'mounted').length,
|
|
61
|
+
leaks: entries.value.filter((e) => e.leak).length,
|
|
72
62
|
}))
|
|
73
63
|
|
|
74
64
|
const filtered = computed(() => {
|
|
@@ -77,24 +67,49 @@ const filtered = computed(() => {
|
|
|
77
67
|
return false
|
|
78
68
|
}
|
|
79
69
|
|
|
70
|
+
if (filter.value === 'mounted' && entry.status !== 'mounted') {
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
|
|
80
74
|
if (filter.value === 'unmounted' && entry.status !== 'unmounted') {
|
|
81
75
|
return false
|
|
82
76
|
}
|
|
83
77
|
|
|
84
78
|
const q = search.value.toLowerCase()
|
|
85
79
|
|
|
86
|
-
if (q
|
|
87
|
-
|
|
80
|
+
if (q) {
|
|
81
|
+
const matchesName = entry.name.toLowerCase().includes(q)
|
|
82
|
+
const matchesFile = entry.componentFile.toLowerCase().includes(q)
|
|
83
|
+
const matchesRef = Object.keys(entry.refs).some((k) => k.toLowerCase().includes(q))
|
|
84
|
+
const matchesVal = Object.values(entry.refs).some((v) => {
|
|
85
|
+
try {
|
|
86
|
+
return String(JSON.stringify(v.value)).toLowerCase().includes(q)
|
|
87
|
+
} catch {
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
if (!matchesName && !matchesFile && !matchesRef && !matchesVal) {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
return true
|
|
91
98
|
})
|
|
92
99
|
})
|
|
93
100
|
|
|
94
|
-
function lifecycleRows(entry:
|
|
101
|
+
function lifecycleRows(entry: RuntimeComposableEntry) {
|
|
95
102
|
return [
|
|
96
|
-
{
|
|
97
|
-
|
|
103
|
+
{
|
|
104
|
+
label: 'onMounted',
|
|
105
|
+
ok: entry.lifecycle.hasOnMounted,
|
|
106
|
+
status: entry.lifecycle.hasOnMounted ? 'registered' : 'not used',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
label: 'onUnmounted',
|
|
110
|
+
ok: entry.lifecycle.hasOnUnmounted,
|
|
111
|
+
status: entry.lifecycle.hasOnUnmounted ? 'registered' : 'missing',
|
|
112
|
+
},
|
|
98
113
|
{
|
|
99
114
|
label: 'watchers cleaned',
|
|
100
115
|
ok: entry.lifecycle.watchersCleaned,
|
|
@@ -107,6 +122,93 @@ function lifecycleRows(entry: ComposableViewEntry) {
|
|
|
107
122
|
},
|
|
108
123
|
]
|
|
109
124
|
}
|
|
125
|
+
|
|
126
|
+
function typeBadgeClass(type: string) {
|
|
127
|
+
if (type === 'computed') {
|
|
128
|
+
return 'badge-info'
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (type === 'reactive') {
|
|
132
|
+
return 'badge-purple'
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return 'badge-gray'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Reverse lookup ────────────────────────────────────────────────────────
|
|
139
|
+
// Clicking a ref key shows every mounted instance that exposes the same key.
|
|
140
|
+
|
|
141
|
+
const lookupKey = ref<string | null>(null)
|
|
142
|
+
|
|
143
|
+
const lookupResults = computed(() => {
|
|
144
|
+
if (!lookupKey.value) {
|
|
145
|
+
return []
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const key = lookupKey.value
|
|
149
|
+
|
|
150
|
+
return entries.value.filter((e) => key in e.refs)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
function openLookup(key: string) {
|
|
154
|
+
lookupKey.value = lookupKey.value === key ? null : key
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Inline editing ────────────────────────────────────────────────────────
|
|
158
|
+
// Only writable refs (type === 'ref') can be edited. Computed are read-only.
|
|
159
|
+
|
|
160
|
+
interface EditTarget {
|
|
161
|
+
id: string
|
|
162
|
+
key: string
|
|
163
|
+
rawValue: string
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const editTarget = ref<EditTarget | null>(null)
|
|
167
|
+
const editError = ref('')
|
|
168
|
+
|
|
169
|
+
function openEdit(id: string, key: string, currentValue: unknown) {
|
|
170
|
+
editError.value = ''
|
|
171
|
+
editTarget.value = {
|
|
172
|
+
id,
|
|
173
|
+
key,
|
|
174
|
+
rawValue: JSON.stringify(currentValue, null, 2),
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function applyEdit() {
|
|
179
|
+
if (!editTarget.value) {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let parsed: unknown
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
parsed = JSON.parse(editTarget.value.rawValue)
|
|
187
|
+
editError.value = ''
|
|
188
|
+
} catch (err) {
|
|
189
|
+
editError.value = `Invalid JSON: ${(err as Error).message}`
|
|
190
|
+
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const origin = getObservatoryOrigin()
|
|
195
|
+
|
|
196
|
+
if (!origin) {
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
window.top?.postMessage(
|
|
201
|
+
{
|
|
202
|
+
type: 'observatory:edit-composable',
|
|
203
|
+
id: editTarget.value.id,
|
|
204
|
+
key: editTarget.value.key,
|
|
205
|
+
value: parsed,
|
|
206
|
+
},
|
|
207
|
+
origin
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
editTarget.value = null
|
|
211
|
+
}
|
|
110
212
|
</script>
|
|
111
213
|
|
|
112
214
|
<template>
|
|
@@ -126,20 +228,17 @@ function lifecycleRows(entry: ComposableViewEntry) {
|
|
|
126
228
|
</div>
|
|
127
229
|
<div class="stat-card">
|
|
128
230
|
<div class="stat-label">instances</div>
|
|
129
|
-
<div class="stat-val">{{ entries.
|
|
231
|
+
<div class="stat-val">{{ entries.length }}</div>
|
|
130
232
|
</div>
|
|
131
233
|
</div>
|
|
132
234
|
|
|
133
235
|
<div class="toolbar">
|
|
134
236
|
<button :class="{ active: filter === 'all' }" @click="filter = 'all'">all</button>
|
|
237
|
+
<button :class="{ active: filter === 'mounted' }" @click="filter = 'mounted'">mounted</button>
|
|
135
238
|
<button :class="{ 'danger-active': filter === 'leak' }" @click="filter = 'leak'">leaks only</button>
|
|
136
239
|
<button :class="{ active: filter === 'unmounted' }" @click="filter = 'unmounted'">unmounted</button>
|
|
137
|
-
<input
|
|
138
|
-
|
|
139
|
-
type="search"
|
|
140
|
-
placeholder="search composable or component…"
|
|
141
|
-
style="max-width: 220px; margin-left: auto"
|
|
142
|
-
/>
|
|
240
|
+
<input v-model="search" type="search" placeholder="search name, file, or ref…" style="max-width: 220px; margin-left: auto" />
|
|
241
|
+
<button class="clear-btn" title="Clear session history" @click="clearSession">clear</button>
|
|
143
242
|
</div>
|
|
144
243
|
|
|
145
244
|
<div class="list">
|
|
@@ -151,40 +250,126 @@ function lifecycleRows(entry: ComposableViewEntry) {
|
|
|
151
250
|
@click="expanded = expanded === entry.id ? null : entry.id"
|
|
152
251
|
>
|
|
153
252
|
<div class="comp-header">
|
|
154
|
-
<
|
|
155
|
-
|
|
253
|
+
<div class="comp-identity">
|
|
254
|
+
<span class="comp-name mono">{{ entry.name }}</span>
|
|
255
|
+
<span class="comp-file muted mono">{{ basename(entry.componentFile) }}</span>
|
|
256
|
+
</div>
|
|
156
257
|
<div class="comp-meta">
|
|
157
|
-
<span v-if="entry.
|
|
158
|
-
<span v-if="entry.
|
|
159
|
-
|
|
160
|
-
</span>
|
|
161
|
-
<span v-if="entry.intervals > 0 && !entry.leak" class="badge badge-warn">
|
|
162
|
-
{{ entry.intervals }} interval{{ entry.intervals > 1 ? 's' : '' }}
|
|
163
|
-
</span>
|
|
164
|
-
<span v-if="entry.leak" class="badge badge-err">leak detected</span>
|
|
258
|
+
<span v-if="entry.watcherCount > 0 && !entry.leak" class="badge badge-warn">{{ entry.watcherCount }}w</span>
|
|
259
|
+
<span v-if="entry.intervalCount > 0 && !entry.leak" class="badge badge-warn">{{ entry.intervalCount }}t</span>
|
|
260
|
+
<span v-if="entry.leak" class="badge badge-err">leak</span>
|
|
165
261
|
<span v-else-if="entry.status === 'mounted'" class="badge badge-ok">mounted</span>
|
|
166
262
|
<span v-else class="badge badge-gray">unmounted</span>
|
|
167
263
|
</div>
|
|
168
264
|
</div>
|
|
169
265
|
|
|
266
|
+
<!-- Inline ref preview — shows up to 3 refs without expanding -->
|
|
267
|
+
<div v-if="Object.keys(entry.refs).length" class="refs-preview">
|
|
268
|
+
<span
|
|
269
|
+
v-for="[k, v] in Object.entries(entry.refs).slice(0, 3)"
|
|
270
|
+
:key="k"
|
|
271
|
+
class="ref-chip"
|
|
272
|
+
:class="{
|
|
273
|
+
'ref-chip--reactive': v.type === 'reactive',
|
|
274
|
+
'ref-chip--computed': v.type === 'computed',
|
|
275
|
+
'ref-chip--shared': entry.sharedKeys?.includes(k),
|
|
276
|
+
}"
|
|
277
|
+
:title="entry.sharedKeys?.includes(k) ? 'shared global state' : ''"
|
|
278
|
+
>
|
|
279
|
+
<span class="ref-chip-key">{{ k }}</span>
|
|
280
|
+
<span class="ref-chip-val">{{ formatVal(v.value) }}</span>
|
|
281
|
+
<span v-if="entry.sharedKeys?.includes(k)" class="ref-chip-shared-dot" title="global"></span>
|
|
282
|
+
</span>
|
|
283
|
+
<span v-if="Object.keys(entry.refs).length > 3" class="muted text-sm">
|
|
284
|
+
+{{ Object.keys(entry.refs).length - 3 }} more
|
|
285
|
+
</span>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
170
288
|
<div v-if="expanded === entry.id" class="comp-detail" @click.stop>
|
|
171
289
|
<div v-if="entry.leak" class="leak-banner">{{ entry.leakReason }}</div>
|
|
172
290
|
|
|
291
|
+
<!-- Global state warning -->
|
|
292
|
+
<div v-if="entry.sharedKeys?.length" class="global-banner">
|
|
293
|
+
<span class="global-dot"></span>
|
|
294
|
+
<span>
|
|
295
|
+
<strong>global state</strong>
|
|
296
|
+
— {{ entry.sharedKeys.join(', ') }}
|
|
297
|
+
{{ entry.sharedKeys.length === 1 ? 'is' : 'are' }}
|
|
298
|
+
shared across all instances of {{ entry.name }}
|
|
299
|
+
</span>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
173
302
|
<div class="section-label">reactive state</div>
|
|
174
|
-
<div v-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
303
|
+
<div v-if="!Object.keys(entry.refs).length" class="muted text-sm" style="padding: 2px 0 6px">
|
|
304
|
+
no tracked state returned
|
|
305
|
+
</div>
|
|
306
|
+
<div v-for="[k, v] in Object.entries(entry.refs)" :key="k" class="ref-row">
|
|
307
|
+
<span
|
|
308
|
+
class="mono text-sm ref-key ref-key--clickable"
|
|
309
|
+
:title="`click to see all instances exposing '${k}'`"
|
|
310
|
+
@click.stop="openLookup(k)"
|
|
311
|
+
>
|
|
312
|
+
{{ k }}
|
|
178
313
|
</span>
|
|
179
|
-
<span class="
|
|
314
|
+
<span class="mono text-sm ref-val">{{ formatVal(v.value) }}</span>
|
|
315
|
+
<span class="badge text-xs" :class="typeBadgeClass(v.type)">{{ v.type }}</span>
|
|
316
|
+
<span v-if="entry.sharedKeys?.includes(k)" class="badge badge-amber text-xs">global</span>
|
|
317
|
+
<button v-if="v.type === 'ref'" class="edit-btn" title="Edit value" @click.stop="openEdit(entry.id, k, v.value)">
|
|
318
|
+
edit
|
|
319
|
+
</button>
|
|
180
320
|
</div>
|
|
181
321
|
|
|
182
|
-
<
|
|
322
|
+
<template v-if="entry.history && entry.history.length">
|
|
323
|
+
<div class="section-label" style="margin-top: 10px">
|
|
324
|
+
change history
|
|
325
|
+
<span class="muted" style="font-weight: 400; text-transform: none; letter-spacing: 0">
|
|
326
|
+
({{ entry.history.length }} events)
|
|
327
|
+
</span>
|
|
328
|
+
</div>
|
|
329
|
+
<div class="history-list">
|
|
330
|
+
<div v-for="(evt, idx) in [...entry.history].reverse().slice(0, 20)" :key="idx" class="history-row">
|
|
331
|
+
<span class="history-time mono muted">+{{ (evt.t / 1000).toFixed(2) }}s</span>
|
|
332
|
+
<span class="history-key mono">{{ evt.key }}</span>
|
|
333
|
+
<span class="history-val mono">{{ formatVal(evt.value) }}</span>
|
|
334
|
+
</div>
|
|
335
|
+
<div v-if="entry.history.length > 20" class="muted text-sm" style="padding: 2px 0">
|
|
336
|
+
… {{ entry.history.length - 20 }} earlier events
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
</template>
|
|
340
|
+
|
|
341
|
+
<div class="section-label" style="margin-top: 10px">lifecycle</div>
|
|
183
342
|
<div v-for="row in lifecycleRows(entry)" :key="row.label" class="lc-row">
|
|
184
343
|
<span class="lc-dot" :style="{ background: row.ok ? 'var(--teal)' : 'var(--red)' }"></span>
|
|
185
|
-
<span class="muted text-sm" style="min-width:
|
|
344
|
+
<span class="muted text-sm" style="min-width: 120px">{{ row.label }}</span>
|
|
186
345
|
<span class="text-sm" :style="{ color: row.ok ? 'var(--teal)' : 'var(--red)' }">{{ row.status }}</span>
|
|
187
346
|
</div>
|
|
347
|
+
|
|
348
|
+
<div class="section-label" style="margin-top: 10px">context</div>
|
|
349
|
+
<div class="lc-row">
|
|
350
|
+
<span class="muted text-sm" style="min-width: 120px">component</span>
|
|
351
|
+
<span class="mono text-sm">{{ basename(entry.componentFile) }}</span>
|
|
352
|
+
</div>
|
|
353
|
+
<div class="lc-row">
|
|
354
|
+
<span class="muted text-sm" style="min-width: 120px">uid</span>
|
|
355
|
+
<span class="mono text-sm muted">{{ entry.componentUid }}</span>
|
|
356
|
+
</div>
|
|
357
|
+
<div class="lc-row">
|
|
358
|
+
<span class="muted text-sm" style="min-width: 120px">defined in</span>
|
|
359
|
+
<span class="mono text-sm muted">{{ entry.file }}:{{ entry.line }}</span>
|
|
360
|
+
</div>
|
|
361
|
+
<div class="lc-row">
|
|
362
|
+
<span class="muted text-sm" style="min-width: 120px">route</span>
|
|
363
|
+
<span class="mono text-sm muted">{{ entry.route }}</span>
|
|
364
|
+
</div>
|
|
365
|
+
<div class="lc-row">
|
|
366
|
+
<span class="muted text-sm" style="min-width: 120px">watchers</span>
|
|
367
|
+
<span class="mono text-sm">{{ entry.watcherCount }}</span>
|
|
368
|
+
</div>
|
|
369
|
+
<div class="lc-row">
|
|
370
|
+
<span class="muted text-sm" style="min-width: 120px">intervals</span>
|
|
371
|
+
<span class="mono text-sm">{{ entry.intervalCount }}</span>
|
|
372
|
+
</div>
|
|
188
373
|
</div>
|
|
189
374
|
</div>
|
|
190
375
|
|
|
@@ -192,6 +377,47 @@ function lifecycleRows(entry: ComposableViewEntry) {
|
|
|
192
377
|
{{ connected ? 'No composables recorded yet.' : 'Waiting for connection to the Nuxt app…' }}
|
|
193
378
|
</div>
|
|
194
379
|
</div>
|
|
380
|
+
|
|
381
|
+
<!-- ── Reverse lookup panel ──────────────────────────────────────── -->
|
|
382
|
+
<Transition name="slide">
|
|
383
|
+
<div v-if="lookupKey" class="lookup-panel">
|
|
384
|
+
<div class="lookup-header">
|
|
385
|
+
<span class="mono text-sm">{{ lookupKey }}</span>
|
|
386
|
+
<span class="muted text-sm">— {{ lookupResults.length }} instance{{ lookupResults.length !== 1 ? 's' : '' }}</span>
|
|
387
|
+
<button class="clear-btn" style="margin-left: auto" @click="lookupKey = null">✕</button>
|
|
388
|
+
</div>
|
|
389
|
+
<div v-if="!lookupResults.length" class="muted text-sm" style="padding: 6px 0">No mounted instances expose this key.</div>
|
|
390
|
+
<div v-for="r in lookupResults" :key="r.id" class="lookup-row">
|
|
391
|
+
<span class="mono text-sm">{{ r.name }}</span>
|
|
392
|
+
<span class="muted text-sm">{{ basename(r.componentFile) }}</span>
|
|
393
|
+
<span class="muted text-sm" style="margin-left: auto">{{ r.route }}</span>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
</Transition>
|
|
397
|
+
|
|
398
|
+
<!-- ── Edit value dialog ─────────────────────────────────────────── -->
|
|
399
|
+
<Transition name="fade">
|
|
400
|
+
<div v-if="editTarget" class="edit-overlay" @click.self="editTarget = null">
|
|
401
|
+
<div class="edit-dialog">
|
|
402
|
+
<div class="edit-dialog-header">
|
|
403
|
+
edit
|
|
404
|
+
<span class="mono">{{ editTarget.key }}</span>
|
|
405
|
+
<button class="clear-btn" style="margin-left: auto" @click="editTarget = null">✕</button>
|
|
406
|
+
</div>
|
|
407
|
+
<p class="muted text-sm" style="padding: 4px 0 8px">
|
|
408
|
+
Value applied immediately to the live ref. Only
|
|
409
|
+
<span class="mono">ref</span>
|
|
410
|
+
values are writable.
|
|
411
|
+
</p>
|
|
412
|
+
<textarea v-model="editTarget.rawValue" class="edit-textarea" rows="6" spellcheck="false" />
|
|
413
|
+
<div v-if="editError" class="edit-error text-sm">{{ editError }}</div>
|
|
414
|
+
<div class="edit-actions">
|
|
415
|
+
<button @click="applyEdit">apply</button>
|
|
416
|
+
<button class="clear-btn" @click="editTarget = null">cancel</button>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</Transition>
|
|
195
421
|
</div>
|
|
196
422
|
</template>
|
|
197
423
|
|
|
@@ -220,12 +446,24 @@ function lifecycleRows(entry: ComposableViewEntry) {
|
|
|
220
446
|
flex-wrap: wrap;
|
|
221
447
|
}
|
|
222
448
|
|
|
449
|
+
.clear-btn {
|
|
450
|
+
color: var(--text3);
|
|
451
|
+
border-color: var(--border);
|
|
452
|
+
flex-shrink: 0;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.clear-btn:hover {
|
|
456
|
+
color: var(--red);
|
|
457
|
+
border-color: var(--red);
|
|
458
|
+
background: transparent;
|
|
459
|
+
}
|
|
460
|
+
|
|
223
461
|
.list {
|
|
224
462
|
flex: 1;
|
|
225
463
|
overflow: auto;
|
|
226
464
|
display: flex;
|
|
227
465
|
flex-direction: column;
|
|
228
|
-
gap:
|
|
466
|
+
gap: 5px;
|
|
229
467
|
min-height: 0;
|
|
230
468
|
}
|
|
231
469
|
|
|
@@ -254,30 +492,104 @@ function lifecycleRows(entry: ComposableViewEntry) {
|
|
|
254
492
|
.comp-header {
|
|
255
493
|
display: flex;
|
|
256
494
|
align-items: center;
|
|
495
|
+
justify-content: space-between;
|
|
496
|
+
padding: 8px 12px;
|
|
257
497
|
gap: 8px;
|
|
258
|
-
|
|
259
|
-
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.comp-identity {
|
|
501
|
+
display: flex;
|
|
502
|
+
align-items: baseline;
|
|
503
|
+
gap: 6px;
|
|
504
|
+
min-width: 0;
|
|
505
|
+
flex: 1;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.comp-name {
|
|
509
|
+
font-size: 12px;
|
|
510
|
+
font-weight: 500;
|
|
511
|
+
color: var(--text);
|
|
512
|
+
flex-shrink: 0;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.comp-file {
|
|
516
|
+
font-size: 11px;
|
|
517
|
+
color: var(--text3);
|
|
518
|
+
overflow: hidden;
|
|
519
|
+
text-overflow: ellipsis;
|
|
520
|
+
white-space: nowrap;
|
|
260
521
|
}
|
|
261
522
|
|
|
262
523
|
.comp-meta {
|
|
263
524
|
display: flex;
|
|
264
525
|
align-items: center;
|
|
265
|
-
gap:
|
|
266
|
-
|
|
526
|
+
gap: 5px;
|
|
527
|
+
flex-shrink: 0;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/* Inline ref preview chips */
|
|
531
|
+
.refs-preview {
|
|
532
|
+
display: flex;
|
|
533
|
+
flex-wrap: wrap;
|
|
534
|
+
gap: 4px;
|
|
535
|
+
padding: 0 12px 8px;
|
|
536
|
+
align-items: center;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.ref-chip {
|
|
540
|
+
display: inline-flex;
|
|
541
|
+
align-items: center;
|
|
542
|
+
gap: 4px;
|
|
543
|
+
padding: 2px 7px;
|
|
544
|
+
border-radius: 4px;
|
|
545
|
+
background: var(--bg2);
|
|
546
|
+
border: 0.5px solid var(--border);
|
|
547
|
+
font-size: 11px;
|
|
548
|
+
font-family: var(--mono);
|
|
549
|
+
max-width: 220px;
|
|
550
|
+
overflow: hidden;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
.ref-chip--reactive {
|
|
554
|
+
border-color: color-mix(in srgb, var(--purple) 40%, var(--border));
|
|
555
|
+
background: color-mix(in srgb, var(--purple) 8%, var(--bg2));
|
|
267
556
|
}
|
|
268
557
|
|
|
558
|
+
.ref-chip--computed {
|
|
559
|
+
border-color: color-mix(in srgb, var(--blue) 40%, var(--border));
|
|
560
|
+
background: color-mix(in srgb, var(--blue) 8%, var(--bg2));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.ref-chip-key {
|
|
564
|
+
color: var(--text2);
|
|
565
|
+
flex-shrink: 0;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.ref-chip-val {
|
|
569
|
+
color: var(--teal);
|
|
570
|
+
overflow: hidden;
|
|
571
|
+
text-overflow: ellipsis;
|
|
572
|
+
white-space: nowrap;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/* Expanded detail */
|
|
269
576
|
.comp-detail {
|
|
577
|
+
padding: 4px 12px 12px;
|
|
270
578
|
border-top: 0.5px solid var(--border);
|
|
271
|
-
|
|
579
|
+
display: flex;
|
|
580
|
+
flex-direction: column;
|
|
581
|
+
gap: 3px;
|
|
272
582
|
}
|
|
273
583
|
|
|
274
584
|
.leak-banner {
|
|
275
|
-
background:
|
|
585
|
+
background: color-mix(in srgb, var(--red) 12%, transparent);
|
|
586
|
+
border: 0.5px solid color-mix(in srgb, var(--red) 40%, var(--border));
|
|
276
587
|
border-radius: var(--radius);
|
|
277
|
-
padding:
|
|
278
|
-
font-size:
|
|
588
|
+
padding: 6px 10px;
|
|
589
|
+
font-size: 11px;
|
|
279
590
|
color: var(--red);
|
|
280
|
-
margin-bottom:
|
|
591
|
+
margin-bottom: 6px;
|
|
592
|
+
font-family: var(--mono);
|
|
281
593
|
}
|
|
282
594
|
|
|
283
595
|
.section-label {
|
|
@@ -286,30 +598,302 @@ function lifecycleRows(entry: ComposableViewEntry) {
|
|
|
286
598
|
text-transform: uppercase;
|
|
287
599
|
letter-spacing: 0.4px;
|
|
288
600
|
color: var(--text3);
|
|
289
|
-
margin-
|
|
601
|
+
margin-top: 6px;
|
|
602
|
+
margin-bottom: 3px;
|
|
290
603
|
}
|
|
291
604
|
|
|
292
605
|
.ref-row {
|
|
293
606
|
display: flex;
|
|
294
607
|
align-items: center;
|
|
295
608
|
gap: 8px;
|
|
296
|
-
padding:
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
609
|
+
padding: 3px 0;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.ref-key {
|
|
613
|
+
min-width: 90px;
|
|
614
|
+
color: var(--text2);
|
|
615
|
+
flex-shrink: 0;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.ref-val {
|
|
619
|
+
flex: 1;
|
|
620
|
+
overflow: hidden;
|
|
621
|
+
text-overflow: ellipsis;
|
|
622
|
+
white-space: nowrap;
|
|
623
|
+
color: var(--teal);
|
|
300
624
|
}
|
|
301
625
|
|
|
302
626
|
.lc-row {
|
|
303
627
|
display: flex;
|
|
304
628
|
align-items: center;
|
|
305
629
|
gap: 8px;
|
|
306
|
-
padding:
|
|
630
|
+
padding: 2px 0;
|
|
307
631
|
}
|
|
308
632
|
|
|
309
633
|
.lc-dot {
|
|
310
|
-
width:
|
|
311
|
-
height:
|
|
634
|
+
width: 6px;
|
|
635
|
+
height: 6px;
|
|
636
|
+
border-radius: 50%;
|
|
637
|
+
flex-shrink: 0;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.ref-chip--shared {
|
|
641
|
+
border-color: color-mix(in srgb, var(--amber) 50%, var(--border));
|
|
642
|
+
background: color-mix(in srgb, var(--amber) 10%, var(--bg2));
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.ref-chip-shared-dot {
|
|
646
|
+
display: inline-block;
|
|
647
|
+
width: 5px;
|
|
648
|
+
height: 5px;
|
|
649
|
+
border-radius: 50%;
|
|
650
|
+
background: var(--amber);
|
|
651
|
+
flex-shrink: 0;
|
|
652
|
+
margin-left: 1px;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.global-banner {
|
|
656
|
+
display: flex;
|
|
657
|
+
align-items: flex-start;
|
|
658
|
+
gap: 8px;
|
|
659
|
+
background: color-mix(in srgb, var(--amber) 10%, transparent);
|
|
660
|
+
border: 0.5px solid color-mix(in srgb, var(--amber) 40%, var(--border));
|
|
661
|
+
border-radius: var(--radius);
|
|
662
|
+
padding: 7px 10px;
|
|
663
|
+
font-size: 11px;
|
|
664
|
+
color: var(--text2);
|
|
665
|
+
margin-bottom: 6px;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.global-dot {
|
|
669
|
+
display: inline-block;
|
|
670
|
+
width: 6px;
|
|
671
|
+
height: 6px;
|
|
312
672
|
border-radius: 50%;
|
|
673
|
+
background: var(--amber);
|
|
313
674
|
flex-shrink: 0;
|
|
675
|
+
margin-top: 3px;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.badge-amber {
|
|
679
|
+
background: color-mix(in srgb, var(--amber) 15%, transparent);
|
|
680
|
+
color: color-mix(in srgb, var(--amber) 80%, var(--text));
|
|
681
|
+
border: 0.5px solid color-mix(in srgb, var(--amber) 40%, var(--border));
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.history-list {
|
|
685
|
+
display: flex;
|
|
686
|
+
flex-direction: column;
|
|
687
|
+
gap: 1px;
|
|
688
|
+
background: var(--bg2);
|
|
689
|
+
border-radius: var(--radius);
|
|
690
|
+
padding: 4px 8px;
|
|
691
|
+
max-height: 180px;
|
|
692
|
+
overflow-y: auto;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.history-row {
|
|
696
|
+
display: flex;
|
|
697
|
+
align-items: center;
|
|
698
|
+
gap: 8px;
|
|
699
|
+
padding: 2px 0;
|
|
700
|
+
font-size: 11px;
|
|
701
|
+
font-family: var(--mono);
|
|
702
|
+
border-bottom: 0.5px solid var(--border);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
.history-row:last-child {
|
|
706
|
+
border-bottom: none;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
.history-time {
|
|
710
|
+
min-width: 52px;
|
|
711
|
+
color: var(--text3);
|
|
712
|
+
flex-shrink: 0;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.history-key {
|
|
716
|
+
min-width: 80px;
|
|
717
|
+
color: var(--text2);
|
|
718
|
+
flex-shrink: 0;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.history-val {
|
|
722
|
+
color: var(--amber);
|
|
723
|
+
overflow: hidden;
|
|
724
|
+
text-overflow: ellipsis;
|
|
725
|
+
white-space: nowrap;
|
|
726
|
+
flex: 1;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/* Stat cards */
|
|
730
|
+
.stat-card {
|
|
731
|
+
background: var(--bg3);
|
|
732
|
+
border: 0.5px solid var(--border);
|
|
733
|
+
border-radius: var(--radius-lg);
|
|
734
|
+
padding: 10px 14px;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.stat-label {
|
|
738
|
+
font-size: 10px;
|
|
739
|
+
font-weight: 500;
|
|
740
|
+
text-transform: uppercase;
|
|
741
|
+
letter-spacing: 0.4px;
|
|
742
|
+
color: var(--text3);
|
|
743
|
+
margin-bottom: 4px;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.stat-val {
|
|
747
|
+
font-size: 22px;
|
|
748
|
+
font-weight: 500;
|
|
749
|
+
line-height: 1;
|
|
750
|
+
color: var(--text);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/* Clickable ref key */
|
|
754
|
+
.ref-key--clickable {
|
|
755
|
+
cursor: pointer;
|
|
756
|
+
text-decoration: underline dotted var(--text3);
|
|
757
|
+
text-underline-offset: 2px;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
.ref-key--clickable:hover {
|
|
761
|
+
color: var(--purple);
|
|
762
|
+
text-decoration-color: var(--purple);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/* Edit button inline with ref row */
|
|
766
|
+
.edit-btn {
|
|
767
|
+
font-size: 10px;
|
|
768
|
+
padding: 1px 6px;
|
|
769
|
+
border-radius: var(--radius);
|
|
770
|
+
border: 0.5px solid var(--border);
|
|
771
|
+
background: transparent;
|
|
772
|
+
color: var(--text3);
|
|
773
|
+
cursor: pointer;
|
|
774
|
+
margin-left: auto;
|
|
775
|
+
flex-shrink: 0;
|
|
776
|
+
font-family: var(--mono);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.edit-btn:hover {
|
|
780
|
+
border-color: var(--purple);
|
|
781
|
+
color: var(--purple);
|
|
782
|
+
background: color-mix(in srgb, var(--purple) 8%, transparent);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/* Reverse lookup panel — appears below the list */
|
|
786
|
+
.lookup-panel {
|
|
787
|
+
flex-shrink: 0;
|
|
788
|
+
border: 0.5px solid var(--border);
|
|
789
|
+
border-radius: var(--radius-lg);
|
|
790
|
+
background: var(--bg3);
|
|
791
|
+
overflow: hidden;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
.lookup-header {
|
|
795
|
+
display: flex;
|
|
796
|
+
align-items: center;
|
|
797
|
+
gap: 6px;
|
|
798
|
+
padding: 7px 12px;
|
|
799
|
+
border-bottom: 0.5px solid var(--border);
|
|
800
|
+
background: var(--bg2);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
.lookup-row {
|
|
804
|
+
display: flex;
|
|
805
|
+
align-items: center;
|
|
806
|
+
gap: 8px;
|
|
807
|
+
padding: 5px 12px;
|
|
808
|
+
border-bottom: 0.5px solid var(--border);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.lookup-row:last-child {
|
|
812
|
+
border-bottom: none;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/* Edit overlay + dialog */
|
|
816
|
+
.edit-overlay {
|
|
817
|
+
position: fixed;
|
|
818
|
+
inset: 0;
|
|
819
|
+
background: rgba(0, 0, 0, 0.4);
|
|
820
|
+
z-index: 100;
|
|
821
|
+
display: flex;
|
|
822
|
+
align-items: center;
|
|
823
|
+
justify-content: center;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
.edit-dialog {
|
|
827
|
+
background: var(--bg1, var(--bg2));
|
|
828
|
+
border: 0.5px solid var(--border);
|
|
829
|
+
border-radius: var(--radius-lg);
|
|
830
|
+
padding: 14px 16px;
|
|
831
|
+
width: 380px;
|
|
832
|
+
max-width: 92vw;
|
|
833
|
+
display: flex;
|
|
834
|
+
flex-direction: column;
|
|
835
|
+
gap: 6px;
|
|
836
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
.edit-dialog-header {
|
|
840
|
+
display: flex;
|
|
841
|
+
align-items: center;
|
|
842
|
+
gap: 6px;
|
|
843
|
+
font-size: 12px;
|
|
844
|
+
color: var(--text2);
|
|
845
|
+
margin-bottom: 2px;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
.edit-textarea {
|
|
849
|
+
width: 100%;
|
|
850
|
+
font-family: var(--mono);
|
|
851
|
+
font-size: 12px;
|
|
852
|
+
padding: 8px 10px;
|
|
853
|
+
background: var(--bg2);
|
|
854
|
+
border: 0.5px solid var(--border);
|
|
855
|
+
border-radius: var(--radius);
|
|
856
|
+
color: var(--text);
|
|
857
|
+
resize: vertical;
|
|
858
|
+
outline: none;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
.edit-textarea:focus {
|
|
862
|
+
border-color: var(--purple);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.edit-error {
|
|
866
|
+
color: var(--red);
|
|
867
|
+
font-family: var(--mono);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
.edit-actions {
|
|
871
|
+
display: flex;
|
|
872
|
+
gap: 6px;
|
|
873
|
+
padding-top: 4px;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/* Slide / fade transitions for the panels */
|
|
877
|
+
.slide-enter-active,
|
|
878
|
+
.slide-leave-active {
|
|
879
|
+
transition:
|
|
880
|
+
opacity 0.15s,
|
|
881
|
+
transform 0.15s;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
.slide-enter-from,
|
|
885
|
+
.slide-leave-to {
|
|
886
|
+
opacity: 0;
|
|
887
|
+
transform: translateY(6px);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
.fade-enter-active,
|
|
891
|
+
.fade-leave-active {
|
|
892
|
+
transition: opacity 0.15s;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.fade-enter-from,
|
|
896
|
+
.fade-leave-to {
|
|
897
|
+
opacity: 0;
|
|
314
898
|
}
|
|
315
899
|
</style>
|