nuxt-devtools-observatory 0.1.9 → 0.1.11
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/client/dist/assets/index--Igqz_EM.js +17 -0
- package/client/dist/assets/index-BoC4M4Nb.css +1 -0
- package/client/dist/index.html +2 -2
- package/client/src/stores/observatory.ts +12 -1
- package/client/src/views/ComposableTracker.vue +3 -0
- package/client/src/views/FetchDashboard.vue +40 -20
- package/client/src/views/ProvideInjectGraph.vue +252 -58
- package/client/src/views/RenderHeatmap.vue +782 -165
- package/client/src/views/TransitionTimeline.vue +7 -2
- package/dist/module.json +1 -1
- package/dist/runtime/composables/provide-inject-registry.js +4 -0
- package/dist/runtime/composables/render-registry.d.ts +4 -0
- package/dist/runtime/composables/render-registry.js +113 -26
- package/dist/runtime/plugin.js +5 -0
- package/package.json +1 -1
- package/client/dist/assets/index-CVI-arV5.js +0 -17
- package/client/dist/assets/index-RdqCF5ft.css +0 -1
|
@@ -1,135 +1,132 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { computed, defineComponent, h, ref, watch, type VNode } from 'vue'
|
|
3
3
|
import { useObservatoryData, type RenderEntry } from '../stores/observatory'
|
|
4
4
|
|
|
5
5
|
interface ComponentNode {
|
|
6
6
|
id: string
|
|
7
7
|
label: string
|
|
8
8
|
file: string
|
|
9
|
+
element?: string
|
|
10
|
+
depth: number
|
|
11
|
+
path: string[]
|
|
9
12
|
renders: number
|
|
13
|
+
navigationRenders: number
|
|
10
14
|
avgMs: number
|
|
11
15
|
triggers: string[]
|
|
12
16
|
children: ComponentNode[]
|
|
17
|
+
parentId?: string
|
|
18
|
+
parentLabel?: string
|
|
13
19
|
}
|
|
14
20
|
|
|
15
|
-
const
|
|
16
|
-
name: '
|
|
21
|
+
const TreeNode = defineComponent({
|
|
22
|
+
name: 'TreeNode',
|
|
17
23
|
props: {
|
|
18
24
|
node: Object as () => ComponentNode,
|
|
19
25
|
mode: String,
|
|
20
26
|
threshold: Number,
|
|
21
|
-
hotOnly: Boolean,
|
|
22
27
|
selected: String,
|
|
28
|
+
expandedIds: Object as () => Set<string>,
|
|
23
29
|
},
|
|
24
|
-
emits: ['select'],
|
|
30
|
+
emits: ['select', 'toggle'],
|
|
25
31
|
setup(props, { emit }): () => VNode | null {
|
|
26
|
-
function
|
|
32
|
+
function nodeValue(node: ComponentNode) {
|
|
27
33
|
return props.mode === 'count' ? node.renders : node.avgMs
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
function walk(nodes: ComponentNode[]) {
|
|
34
|
-
nodes.forEach((node) => {
|
|
35
|
-
const value = getVal(node)
|
|
36
|
-
|
|
37
|
-
if (value > max) {
|
|
38
|
-
max = value
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
walk(node.children)
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
walk([props.node!])
|
|
46
|
-
|
|
47
|
-
return Math.max(max, props.mode === 'count' ? 40 : 20)
|
|
36
|
+
function isHot(node: ComponentNode) {
|
|
37
|
+
return nodeValue(node) >= props.threshold!
|
|
48
38
|
}
|
|
49
39
|
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return { bg: '#EAF3DE', text: '#27500A', border: '#97C459' }
|
|
40
|
+
function rowClass(node: ComponentNode) {
|
|
41
|
+
return {
|
|
42
|
+
selected: props.selected === node.id,
|
|
43
|
+
hot: isHot(node),
|
|
55
44
|
}
|
|
56
|
-
|
|
57
|
-
if (ratio < 0.55) {
|
|
58
|
-
return { bg: '#FAEEDA', text: '#633806', border: '#EF9F27' }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (ratio < 0.8) {
|
|
62
|
-
return { bg: '#FAECE7', text: '#712B13', border: '#D85A30' }
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return { bg: '#FCEBEB', text: '#791F1F', border: '#E24B4A' }
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function isHot(node: ComponentNode) {
|
|
69
|
-
return (props.mode === 'count' ? node.renders : node.avgMs) >= props.threshold!
|
|
70
45
|
}
|
|
71
46
|
|
|
72
47
|
return () => {
|
|
73
48
|
const node = props.node!
|
|
49
|
+
const expanded = props.expandedIds?.has(node.id) ?? false
|
|
50
|
+
const canExpand = node.children.length > 0
|
|
51
|
+
const metric = props.mode === 'count' ? `${node.renders}` : `${node.avgMs.toFixed(1)}ms`
|
|
52
|
+
const metricLabel = props.mode === 'count' ? 'renders' : 'avg'
|
|
53
|
+
const badges = []
|
|
54
|
+
const normalizedElement = node.element?.toLowerCase()
|
|
74
55
|
|
|
75
|
-
if (
|
|
76
|
-
|
|
56
|
+
if (node.element && node.element !== node.label && !['div', 'span', 'p'].includes(normalizedElement ?? '')) {
|
|
57
|
+
badges.push(node.element)
|
|
77
58
|
}
|
|
78
59
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
60
|
+
if (node.file !== 'unknown') {
|
|
61
|
+
const fileBadge = node.file.split('/').pop()?.replace(/\.vue$/i, '') ?? node.file
|
|
62
|
+
|
|
63
|
+
if (fileBadge !== node.label && !badges.includes(fileBadge)) {
|
|
64
|
+
badges.push(fileBadge)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return h('div', { class: 'tree-node' }, [
|
|
69
|
+
h(
|
|
70
|
+
'div',
|
|
71
|
+
{
|
|
72
|
+
class: ['tree-row', rowClass(node)],
|
|
73
|
+
style: { '--tree-depth': String(node.depth) },
|
|
74
|
+
onClick: (event: MouseEvent) => {
|
|
75
|
+
event.stopPropagation()
|
|
76
|
+
emit('select', node)
|
|
77
|
+
},
|
|
96
78
|
},
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
[
|
|
100
|
-
h('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, [
|
|
101
|
-
h('span', { style: { fontFamily: 'var(--mono)', fontSize: '11px', fontWeight: '500', color: colors.text } }, node.label),
|
|
79
|
+
[
|
|
80
|
+
h('span', { class: 'tree-rail', 'aria-hidden': 'true' }),
|
|
102
81
|
h(
|
|
103
|
-
'
|
|
104
|
-
{
|
|
105
|
-
|
|
82
|
+
'button',
|
|
83
|
+
{
|
|
84
|
+
class: ['tree-toggle', { empty: !canExpand }],
|
|
85
|
+
disabled: !canExpand,
|
|
86
|
+
onClick: (event: MouseEvent) => {
|
|
87
|
+
event.stopPropagation()
|
|
88
|
+
|
|
89
|
+
if (canExpand) {
|
|
90
|
+
emit('toggle', node.id)
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
canExpand ? (expanded ? '⌄' : '›') : ''
|
|
106
95
|
),
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
96
|
+
h('div', { class: 'tree-copy' }, [
|
|
97
|
+
h('span', { class: 'tree-name mono', title: node.label }, node.label),
|
|
98
|
+
badges.length
|
|
99
|
+
? h(
|
|
100
|
+
'div',
|
|
101
|
+
{ class: 'tree-badges' },
|
|
102
|
+
badges.slice(0, 1).map((badge) => h('span', { class: 'tree-badge mono', title: badge }, badge))
|
|
103
|
+
)
|
|
104
|
+
: null,
|
|
105
|
+
]),
|
|
106
|
+
h('div', { class: 'tree-metrics mono' }, [
|
|
107
|
+
node.navigationRenders ? h('span', { class: 'tree-nav-pill' }, `${node.navigationRenders} nav`) : null,
|
|
108
|
+
h('span', { class: 'tree-metric-pill' }, `${metric} ${metricLabel}`),
|
|
109
|
+
]),
|
|
110
|
+
]
|
|
111
|
+
),
|
|
112
|
+
expanded && canExpand
|
|
113
|
+
? h(
|
|
114
|
+
'div',
|
|
115
|
+
{ class: 'tree-children' },
|
|
116
|
+
node.children.map((child) =>
|
|
117
|
+
h(TreeNode, {
|
|
118
|
+
node: child,
|
|
119
|
+
mode: props.mode,
|
|
120
|
+
threshold: props.threshold,
|
|
121
|
+
selected: props.selected,
|
|
122
|
+
expandedIds: props.expandedIds,
|
|
123
|
+
onSelect: (value: ComponentNode) => emit('select', value),
|
|
124
|
+
onToggle: (value: string) => emit('toggle', value),
|
|
125
|
+
})
|
|
129
126
|
)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
)
|
|
127
|
+
)
|
|
128
|
+
: null,
|
|
129
|
+
])
|
|
133
130
|
}
|
|
134
131
|
},
|
|
135
132
|
})
|
|
@@ -140,8 +137,34 @@ const activeMode = ref<'count' | 'time'>('count')
|
|
|
140
137
|
const activeThreshold = ref(5)
|
|
141
138
|
const activeHotOnly = ref(false)
|
|
142
139
|
const frozen = ref(false)
|
|
140
|
+
const search = ref('')
|
|
143
141
|
const activeSelectedId = ref<string | null>(null)
|
|
144
|
-
const
|
|
142
|
+
const activeRootId = ref<string | null>(null)
|
|
143
|
+
const expandedIds = ref<Set<string>>(new Set())
|
|
144
|
+
const frozenSnapshot = ref<RenderEntry[]>([])
|
|
145
|
+
const expansionReady = ref(false)
|
|
146
|
+
|
|
147
|
+
function displayLabel(entry: RenderEntry) {
|
|
148
|
+
if (entry.name && entry.name !== 'unknown' && !/^Component#\d+$/.test(entry.name)) {
|
|
149
|
+
return entry.name
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (entry.element) {
|
|
153
|
+
return entry.element
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const basename = entry.file.split('/').pop()?.replace(/\.vue$/i, '')
|
|
157
|
+
|
|
158
|
+
if (basename && basename !== 'unknown') {
|
|
159
|
+
return basename
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (entry.name && entry.name !== 'unknown') {
|
|
163
|
+
return entry.name
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return `Component#${entry.uid}`
|
|
167
|
+
}
|
|
145
168
|
|
|
146
169
|
function formatTrigger(trigger: RenderEntry['triggers'][number]) {
|
|
147
170
|
return `${trigger.type}: ${trigger.key}`
|
|
@@ -153,12 +176,17 @@ function buildNodes(entries: RenderEntry[]) {
|
|
|
153
176
|
for (const entry of entries) {
|
|
154
177
|
byId.set(String(entry.uid), {
|
|
155
178
|
id: String(entry.uid),
|
|
156
|
-
label: entry
|
|
179
|
+
label: displayLabel(entry),
|
|
157
180
|
file: entry.file,
|
|
181
|
+
element: entry.element,
|
|
182
|
+
depth: 0,
|
|
183
|
+
path: [],
|
|
158
184
|
renders: entry.renders,
|
|
185
|
+
navigationRenders: Number.isFinite(entry.navigationRenders) ? entry.navigationRenders : 0,
|
|
159
186
|
avgMs: entry.avgMs,
|
|
160
187
|
triggers: entry.triggers.map(formatTrigger),
|
|
161
188
|
children: [],
|
|
189
|
+
parentId: entry.parentUid !== undefined ? String(entry.parentUid) : undefined,
|
|
162
190
|
})
|
|
163
191
|
}
|
|
164
192
|
|
|
@@ -174,35 +202,217 @@ function buildNodes(entries: RenderEntry[]) {
|
|
|
174
202
|
const parent = entry.parentUid !== undefined ? byId.get(String(entry.parentUid)) : undefined
|
|
175
203
|
|
|
176
204
|
if (parent) {
|
|
205
|
+
node.parentLabel = parent.label
|
|
177
206
|
parent.children.push(node)
|
|
178
207
|
} else {
|
|
179
208
|
roots.push(node)
|
|
180
209
|
}
|
|
181
210
|
}
|
|
182
211
|
|
|
212
|
+
function finalize(node: ComponentNode, path: string[] = [], depth = 0) {
|
|
213
|
+
node.depth = depth
|
|
214
|
+
node.path = [...path, node.label]
|
|
215
|
+
node.children.forEach((child) => finalize(child, node.path, depth + 1))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
roots.forEach((root) => finalize(root))
|
|
219
|
+
|
|
183
220
|
return roots
|
|
184
221
|
}
|
|
185
222
|
|
|
186
|
-
|
|
187
|
-
const
|
|
223
|
+
function flatten(nodes: ComponentNode[]) {
|
|
224
|
+
const flat: ComponentNode[] = []
|
|
188
225
|
|
|
189
|
-
|
|
190
|
-
|
|
226
|
+
function walk(node: ComponentNode) {
|
|
227
|
+
flat.push(node)
|
|
228
|
+
node.children.forEach(walk)
|
|
229
|
+
}
|
|
191
230
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
231
|
+
nodes.forEach(walk)
|
|
232
|
+
|
|
233
|
+
return flat
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function countSubtree(node: ComponentNode): number {
|
|
237
|
+
return 1 + node.children.reduce((sum, child) => sum + countSubtree(child), 0)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function collectIds(node: ComponentNode, target = new Set<string>()) {
|
|
241
|
+
target.add(node.id)
|
|
242
|
+
node.children.forEach((child) => collectIds(child, target))
|
|
243
|
+
return target
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function pathToNode(node: ComponentNode, targetId: string, trail: string[] = []): string[] | null {
|
|
247
|
+
const nextTrail = [...trail, node.id]
|
|
248
|
+
|
|
249
|
+
if (node.id === targetId) {
|
|
250
|
+
return nextTrail
|
|
197
251
|
}
|
|
198
252
|
|
|
199
|
-
|
|
253
|
+
for (const child of node.children) {
|
|
254
|
+
const childTrail = pathToNode(child, targetId, nextTrail)
|
|
255
|
+
|
|
256
|
+
if (childTrail) {
|
|
257
|
+
return childTrail
|
|
258
|
+
}
|
|
259
|
+
}
|
|
200
260
|
|
|
201
|
-
return
|
|
261
|
+
return null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function findFirstHotNode(node: ComponentNode): ComponentNode | null {
|
|
265
|
+
if (isHot(node)) {
|
|
266
|
+
return node
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const child of node.children) {
|
|
270
|
+
const match = findFirstHotNode(child)
|
|
271
|
+
|
|
272
|
+
if (match) {
|
|
273
|
+
return match
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return null
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function defaultExpandedIds(root: ComponentNode | null) {
|
|
281
|
+
if (!root) {
|
|
282
|
+
return new Set<string>()
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return new Set([root.id])
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function searchExpandedIds(root: ComponentNode | null, term: string) {
|
|
289
|
+
const expanded = defaultExpandedIds(root)
|
|
290
|
+
|
|
291
|
+
if (!root || !term) {
|
|
292
|
+
return expanded
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function visit(node: ComponentNode): boolean {
|
|
296
|
+
const childMatched = node.children.some((child) => visit(child))
|
|
297
|
+
const selfMatched = matchesSearch(node, term)
|
|
298
|
+
|
|
299
|
+
if (childMatched) {
|
|
300
|
+
expanded.add(node.id)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return selfMatched || childMatched
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
visit(root)
|
|
307
|
+
|
|
308
|
+
return expanded
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function nodeValue(node: ComponentNode) {
|
|
312
|
+
return activeMode.value === 'count' ? node.renders : node.avgMs
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function isHot(node: ComponentNode) {
|
|
316
|
+
return nodeValue(node) >= activeThreshold.value
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function matchesSearch(node: ComponentNode, searchTerm: string): boolean {
|
|
320
|
+
if (!searchTerm) {
|
|
321
|
+
return true
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const query = searchTerm.toLowerCase()
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
node.label.toLowerCase().includes(query) ||
|
|
328
|
+
node.file.toLowerCase().includes(query) ||
|
|
329
|
+
node.path.some((segment) => segment.toLowerCase().includes(query)) ||
|
|
330
|
+
node.triggers.some((trigger) => trigger.toLowerCase().includes(query))
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function treeMatches(node: ComponentNode, searchTerm: string): boolean {
|
|
335
|
+
if (!searchTerm) {
|
|
336
|
+
return true
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return matchesSearch(node, searchTerm) || node.children.some((child) => treeMatches(child, searchTerm))
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function subtreeHasHotNode(node: ComponentNode): boolean {
|
|
343
|
+
return isHot(node) || node.children.some((child) => subtreeHasHotNode(child))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function isVisibleRoot(node: ComponentNode, searchTerm: string): boolean {
|
|
347
|
+
const matchesCurrentSearch = treeMatches(node, searchTerm)
|
|
348
|
+
const matchesCurrentHeat = !activeHotOnly.value || subtreeHasHotNode(node)
|
|
349
|
+
|
|
350
|
+
return matchesCurrentSearch && matchesCurrentHeat
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function pruneVisibleTree(node: ComponentNode, searchTerm: string): ComponentNode | null {
|
|
354
|
+
const visibleChildren = node.children
|
|
355
|
+
.map((child) => pruneVisibleTree(child, searchTerm))
|
|
356
|
+
.filter((child): child is ComponentNode => child !== null)
|
|
357
|
+
|
|
358
|
+
const matchesCurrentSearch = !searchTerm || matchesSearch(node, searchTerm) || visibleChildren.length > 0
|
|
359
|
+
const matchesCurrentHeat = !activeHotOnly.value || isHot(node) || visibleChildren.length > 0
|
|
360
|
+
|
|
361
|
+
if (!matchesCurrentSearch || !matchesCurrentHeat) {
|
|
362
|
+
return null
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
...node,
|
|
367
|
+
children: visibleChildren,
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const displayEntries = computed(() => (frozen.value ? frozenSnapshot.value : renders.value))
|
|
372
|
+
const rootNodes = computed(() => buildNodes(displayEntries.value))
|
|
373
|
+
const rootMap = computed(() => new Map(rootNodes.value.map((node) => [node.id, node])))
|
|
374
|
+
const allComponents = computed(() => flatten(rootNodes.value))
|
|
375
|
+
|
|
376
|
+
const filteredRoots = computed(() => {
|
|
377
|
+
const term = search.value.trim()
|
|
378
|
+
|
|
379
|
+
return rootNodes.value.filter((root) => isVisibleRoot(root, term))
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
const activeRoot = computed(() => {
|
|
383
|
+
if (activeRootId.value) {
|
|
384
|
+
return filteredRoots.value.find((node) => node.id === activeRootId.value) ?? rootMap.value.get(activeRootId.value) ?? null
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return filteredRoots.value[0] ?? rootNodes.value[0] ?? null
|
|
202
388
|
})
|
|
203
389
|
|
|
390
|
+
const visibleActiveRoot = computed(() => {
|
|
391
|
+
const term = search.value.trim()
|
|
392
|
+
|
|
393
|
+
return activeRoot.value ? pruneVisibleTree(activeRoot.value, term) : null
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
const visibleTreeRoots = computed(() => {
|
|
397
|
+
if (!visibleActiveRoot.value) {
|
|
398
|
+
return []
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return [visibleActiveRoot.value]
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
const appEntries = computed(() =>
|
|
405
|
+
filteredRoots.value.map((root, index) => ({
|
|
406
|
+
id: root.id,
|
|
407
|
+
label: `App ${index + 1}`,
|
|
408
|
+
meta: `${countSubtree(root)} nodes`,
|
|
409
|
+
root,
|
|
410
|
+
}))
|
|
411
|
+
)
|
|
412
|
+
|
|
204
413
|
const activeSelected = computed(() => allComponents.value.find((node) => node.id === activeSelectedId.value) ?? null)
|
|
205
414
|
const totalRenders = computed(() => allComponents.value.reduce((sum, node) => sum + node.renders, 0))
|
|
415
|
+
const totalNavigationRenders = computed(() => allComponents.value.reduce((sum, node) => sum + (Number.isFinite(node.navigationRenders) ? node.navigationRenders : 0), 0))
|
|
206
416
|
const hotCount = computed(() => allComponents.value.filter((node) => isHot(node)).length)
|
|
207
417
|
const avgTime = computed(() => {
|
|
208
418
|
const components = allComponents.value.filter((node) => node.avgMs > 0)
|
|
@@ -214,8 +424,129 @@ const avgTime = computed(() => {
|
|
|
214
424
|
return (components.reduce((sum, node) => sum + node.avgMs, 0) / components.length).toFixed(1)
|
|
215
425
|
})
|
|
216
426
|
|
|
217
|
-
|
|
218
|
-
|
|
427
|
+
watch(
|
|
428
|
+
rootNodes,
|
|
429
|
+
(roots) => {
|
|
430
|
+
if (!roots.length) {
|
|
431
|
+
activeRootId.value = null
|
|
432
|
+
activeSelectedId.value = null
|
|
433
|
+
expandedIds.value = new Set()
|
|
434
|
+
expansionReady.value = false
|
|
435
|
+
return
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const rootIds = new Set(roots.map((root) => root.id))
|
|
439
|
+
|
|
440
|
+
if (!activeRootId.value || !rootIds.has(activeRootId.value)) {
|
|
441
|
+
activeRootId.value = roots[0].id
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (activeSelectedId.value && !allComponents.value.some((node) => node.id === activeSelectedId.value)) {
|
|
445
|
+
activeSelectedId.value = null
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const validIds = new Set(allComponents.value.map((node) => node.id))
|
|
449
|
+
const preserved = new Set([...expandedIds.value].filter((id) => validIds.has(id)))
|
|
450
|
+
|
|
451
|
+
if (!expansionReady.value) {
|
|
452
|
+
expandedIds.value = defaultExpandedIds(activeRoot.value)
|
|
453
|
+
expansionReady.value = true
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (!search.value.trim() && activeSelectedId.value && activeRoot.value) {
|
|
458
|
+
const selectedPath = pathToNode(activeRoot.value, activeSelectedId.value) ?? []
|
|
459
|
+
|
|
460
|
+
selectedPath.forEach((id) => preserved.add(id))
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
expandedIds.value = preserved
|
|
464
|
+
},
|
|
465
|
+
{ immediate: true }
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
watch(search, (term) => {
|
|
469
|
+
if (!activeRoot.value) {
|
|
470
|
+
return
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const normalized = term.trim()
|
|
474
|
+
|
|
475
|
+
if (normalized) {
|
|
476
|
+
expandedIds.value = searchExpandedIds(activeRoot.value, normalized)
|
|
477
|
+
return
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (activeSelectedId.value) {
|
|
481
|
+
const selectedPath = pathToNode(activeRoot.value, activeSelectedId.value)
|
|
482
|
+
|
|
483
|
+
expandedIds.value = selectedPath ? new Set(selectedPath) : defaultExpandedIds(activeRoot.value)
|
|
484
|
+
return
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
expandedIds.value = defaultExpandedIds(activeRoot.value)
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
watch([activeHotOnly, activeThreshold, activeMode, filteredRoots], () => {
|
|
491
|
+
if (!activeHotOnly.value) {
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const topLevelRoot = filteredRoots.value[0] ?? null
|
|
496
|
+
|
|
497
|
+
if (!topLevelRoot) {
|
|
498
|
+
activeSelectedId.value = null
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const firstHot = findFirstHotNode(topLevelRoot)
|
|
503
|
+
|
|
504
|
+
if (!firstHot) {
|
|
505
|
+
activeSelectedId.value = null
|
|
506
|
+
return
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
activeRootId.value = topLevelRoot.id
|
|
510
|
+
|
|
511
|
+
if (activeSelectedId.value !== firstHot.id) {
|
|
512
|
+
activeSelectedId.value = firstHot.id
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
expandedIds.value = new Set(pathToNode(topLevelRoot, firstHot.id) ?? [topLevelRoot.id])
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
function selectNode(node: ComponentNode) {
|
|
519
|
+
activeSelectedId.value = node.id
|
|
520
|
+
|
|
521
|
+
const topLevelRoot = rootNodes.value.find((root) => collectIds(root).has(node.id))
|
|
522
|
+
|
|
523
|
+
if (topLevelRoot) {
|
|
524
|
+
activeRootId.value = topLevelRoot.id
|
|
525
|
+
expandedIds.value = new Set(pathToNode(topLevelRoot, node.id) ?? [topLevelRoot.id])
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function toggleNode(id: string) {
|
|
530
|
+
const next = new Set(expandedIds.value)
|
|
531
|
+
|
|
532
|
+
if (next.has(id)) {
|
|
533
|
+
next.delete(id)
|
|
534
|
+
} else {
|
|
535
|
+
next.add(id)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
expandedIds.value = next
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function selectRoot(root: ComponentNode) {
|
|
542
|
+
activeRootId.value = root.id
|
|
543
|
+
expandedIds.value = defaultExpandedIds(root)
|
|
544
|
+
expansionReady.value = true
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function updateSearch(event: Event) {
|
|
548
|
+
const target = event.target as HTMLInputElement | null
|
|
549
|
+
search.value = target?.value ?? ''
|
|
219
550
|
}
|
|
220
551
|
|
|
221
552
|
function toggleFreeze() {
|
|
@@ -225,9 +556,17 @@ function toggleFreeze() {
|
|
|
225
556
|
return
|
|
226
557
|
}
|
|
227
558
|
|
|
228
|
-
frozenSnapshot.value = JSON.parse(JSON.stringify(
|
|
559
|
+
frozenSnapshot.value = JSON.parse(JSON.stringify(renders.value)) as RenderEntry[]
|
|
229
560
|
frozen.value = true
|
|
230
561
|
}
|
|
562
|
+
|
|
563
|
+
function basename(file: string) {
|
|
564
|
+
return file.split('/').pop()?.replace(/\.vue$/i, '') ?? file
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function pathLabel(node: ComponentNode) {
|
|
568
|
+
return node.path.join(' / ')
|
|
569
|
+
}
|
|
231
570
|
</script>
|
|
232
571
|
|
|
233
572
|
<template>
|
|
@@ -256,6 +595,7 @@ function toggleFreeze() {
|
|
|
256
595
|
<div class="stat-card">
|
|
257
596
|
<div class="stat-label">total renders</div>
|
|
258
597
|
<div class="stat-val">{{ totalRenders }}</div>
|
|
598
|
+
<div class="stat-sub mono">{{ totalNavigationRenders }} nav</div>
|
|
259
599
|
</div>
|
|
260
600
|
<div class="stat-card">
|
|
261
601
|
<div class="stat-label">hot</div>
|
|
@@ -267,50 +607,90 @@ function toggleFreeze() {
|
|
|
267
607
|
</div>
|
|
268
608
|
</div>
|
|
269
609
|
|
|
270
|
-
<div class="
|
|
271
|
-
<
|
|
272
|
-
<div class="
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
610
|
+
<div class="inspector">
|
|
611
|
+
<aside class="roots-panel">
|
|
612
|
+
<div class="panel-title">apps</div>
|
|
613
|
+
<button
|
|
614
|
+
v-for="entry in appEntries"
|
|
615
|
+
:key="entry.id"
|
|
616
|
+
class="root-item"
|
|
617
|
+
:class="{ active: activeRootId === entry.id }"
|
|
618
|
+
@click="selectRoot(entry.root)"
|
|
619
|
+
>
|
|
620
|
+
<div class="root-copy">
|
|
621
|
+
<span class="root-label mono">{{ entry.label }}</span>
|
|
622
|
+
<span class="root-sub muted mono">{{ entry.root.label }}</span>
|
|
278
623
|
</div>
|
|
279
|
-
<span class="
|
|
624
|
+
<span class="root-meta mono">{{ entry.meta }}</span>
|
|
625
|
+
</button>
|
|
626
|
+
<div v-if="!appEntries.length" class="detail-empty">no apps match</div>
|
|
627
|
+
</aside>
|
|
628
|
+
|
|
629
|
+
<section class="tree-panel">
|
|
630
|
+
<div class="tree-toolbar">
|
|
631
|
+
<input :value="search" class="search-input mono" placeholder="Find components..." @input="updateSearch" />
|
|
280
632
|
</div>
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
633
|
+
|
|
634
|
+
<div class="tree-frame">
|
|
635
|
+
<div class="tree-canvas">
|
|
636
|
+
<TreeNode
|
|
637
|
+
v-for="root in visibleTreeRoots"
|
|
638
|
+
:key="root.id"
|
|
639
|
+
:node="root"
|
|
640
|
+
:mode="activeMode"
|
|
641
|
+
:threshold="activeThreshold"
|
|
642
|
+
:selected="activeSelected?.id"
|
|
643
|
+
:expanded-ids="expandedIds"
|
|
644
|
+
@select="selectNode"
|
|
645
|
+
@toggle="toggleNode"
|
|
646
|
+
/>
|
|
647
|
+
</div>
|
|
648
|
+
<div v-if="!visibleTreeRoots.length" class="detail-empty">
|
|
649
|
+
{{ connected ? 'No render activity recorded yet.' : 'Waiting for connection to the Nuxt app…' }}
|
|
650
|
+
</div>
|
|
293
651
|
</div>
|
|
294
|
-
</
|
|
652
|
+
</section>
|
|
295
653
|
|
|
296
|
-
<
|
|
654
|
+
<aside class="detail-panel">
|
|
297
655
|
<template v-if="activeSelected">
|
|
298
656
|
<div class="detail-header">
|
|
299
657
|
<span class="mono bold" style="font-size: 12px">{{ activeSelected.label }}</span>
|
|
300
658
|
<button @click="activeSelectedId = null">×</button>
|
|
301
659
|
</div>
|
|
302
660
|
|
|
661
|
+
<div class="detail-pill-row">
|
|
662
|
+
<span class="detail-pill mono">{{ activeSelected.renders }} renders</span>
|
|
663
|
+
<span v-if="activeSelected.navigationRenders" class="detail-pill mono nav">{{ activeSelected.navigationRenders }} nav</span>
|
|
664
|
+
<span class="detail-pill mono">{{ activeSelected.avgMs.toFixed(1) }}ms avg</span>
|
|
665
|
+
<span class="detail-pill mono" :class="{ hot: isHot(activeSelected) }">{{ isHot(activeSelected) ? 'hot' : 'cool' }}</span>
|
|
666
|
+
</div>
|
|
667
|
+
|
|
668
|
+
<div class="section-label">identity</div>
|
|
303
669
|
<div class="meta-grid">
|
|
304
|
-
<span class="muted text-sm">
|
|
305
|
-
<span class="mono text-sm">{{ activeSelected.
|
|
306
|
-
<span class="muted text-sm">
|
|
307
|
-
<span class="mono text-sm">{{ activeSelected
|
|
308
|
-
<span class="muted text-sm">hot?</span>
|
|
309
|
-
<span class="text-sm" :style="{ color: isHot(activeSelected) ? 'var(--red)' : 'var(--teal)' }">
|
|
310
|
-
{{ isHot(activeSelected) ? 'yes' : 'no' }}
|
|
311
|
-
</span>
|
|
670
|
+
<span class="muted text-sm">label</span>
|
|
671
|
+
<span class="mono text-sm">{{ activeSelected.label }}</span>
|
|
672
|
+
<span class="muted text-sm">path</span>
|
|
673
|
+
<span class="mono text-sm">{{ pathLabel(activeSelected) }}</span>
|
|
312
674
|
<span class="muted text-sm">file</span>
|
|
313
675
|
<span class="mono text-sm muted">{{ activeSelected.file }}</span>
|
|
676
|
+
<span class="muted text-sm">file name</span>
|
|
677
|
+
<span class="mono text-sm">{{ basename(activeSelected.file) }}</span>
|
|
678
|
+
<span class="muted text-sm">parent</span>
|
|
679
|
+
<span class="mono text-sm">{{ activeSelected.parentLabel ?? 'none' }}</span>
|
|
680
|
+
<span class="muted text-sm">children</span>
|
|
681
|
+
<span class="mono text-sm">{{ activeSelected.children.length }}</span>
|
|
682
|
+
</div>
|
|
683
|
+
|
|
684
|
+
<div class="section-label">rendering</div>
|
|
685
|
+
<div class="meta-grid">
|
|
686
|
+
<span class="muted text-sm">mode value</span>
|
|
687
|
+
<span class="mono text-sm">{{ activeMode === 'count' ? activeSelected.renders : `${activeSelected.avgMs.toFixed(1)}ms` }}</span>
|
|
688
|
+
<span class="muted text-sm">navigation renders</span>
|
|
689
|
+
<span class="mono text-sm">{{ activeSelected.navigationRenders }}</span>
|
|
690
|
+
<span class="muted text-sm">threshold</span>
|
|
691
|
+
<span class="mono text-sm">{{ activeThreshold }}</span>
|
|
692
|
+
<span class="muted text-sm">selected mode</span>
|
|
693
|
+
<span class="mono text-sm">{{ activeMode === 'count' ? 'render count' : 'render time' }}</span>
|
|
314
694
|
</div>
|
|
315
695
|
|
|
316
696
|
<div class="section-label">triggers</div>
|
|
@@ -318,7 +698,7 @@ function toggleFreeze() {
|
|
|
318
698
|
<div v-if="!activeSelected.triggers.length" class="muted text-sm">no triggers recorded</div>
|
|
319
699
|
</template>
|
|
320
700
|
<div v-else class="detail-empty">click a component to inspect</div>
|
|
321
|
-
</
|
|
701
|
+
</aside>
|
|
322
702
|
</div>
|
|
323
703
|
</div>
|
|
324
704
|
</template>
|
|
@@ -359,54 +739,256 @@ function toggleFreeze() {
|
|
|
359
739
|
flex-shrink: 0;
|
|
360
740
|
}
|
|
361
741
|
|
|
362
|
-
.
|
|
363
|
-
|
|
742
|
+
.stat-sub {
|
|
743
|
+
margin-top: 4px;
|
|
744
|
+
font-size: 11px;
|
|
745
|
+
color: var(--text3);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
.inspector {
|
|
749
|
+
display: grid;
|
|
750
|
+
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr) minmax(260px, 320px);
|
|
364
751
|
gap: 12px;
|
|
365
752
|
flex: 1;
|
|
366
|
-
overflow: hidden;
|
|
367
753
|
min-height: 0;
|
|
368
754
|
}
|
|
369
755
|
|
|
370
|
-
.
|
|
371
|
-
|
|
372
|
-
|
|
756
|
+
.roots-panel,
|
|
757
|
+
.tree-panel,
|
|
758
|
+
.detail-panel {
|
|
373
759
|
border: 0.5px solid var(--border);
|
|
374
760
|
border-radius: var(--radius-lg);
|
|
375
|
-
padding: 12px;
|
|
376
761
|
background: var(--bg3);
|
|
762
|
+
min-height: 0;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
.roots-panel,
|
|
766
|
+
.detail-panel {
|
|
767
|
+
display: flex;
|
|
768
|
+
flex-direction: column;
|
|
769
|
+
overflow: auto;
|
|
770
|
+
padding: 12px;
|
|
771
|
+
gap: 8px;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.panel-title {
|
|
775
|
+
font-size: 10px;
|
|
776
|
+
font-weight: 500;
|
|
777
|
+
text-transform: uppercase;
|
|
778
|
+
letter-spacing: 0.4px;
|
|
779
|
+
color: var(--text3);
|
|
377
780
|
}
|
|
378
781
|
|
|
379
|
-
.
|
|
782
|
+
.root-item {
|
|
380
783
|
display: flex;
|
|
381
784
|
align-items: center;
|
|
785
|
+
justify-content: space-between;
|
|
382
786
|
gap: 8px;
|
|
383
|
-
|
|
787
|
+
width: 100%;
|
|
788
|
+
padding: 10px 12px;
|
|
789
|
+
border: 1px solid var(--border);
|
|
790
|
+
border-radius: var(--radius);
|
|
791
|
+
background: var(--bg2);
|
|
792
|
+
color: var(--text);
|
|
793
|
+
text-align: left;
|
|
384
794
|
}
|
|
385
795
|
|
|
386
|
-
.
|
|
796
|
+
.root-item.active {
|
|
797
|
+
border-color: var(--teal);
|
|
798
|
+
background: color-mix(in srgb, var(--teal) 16%, var(--bg2));
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
.root-label {
|
|
802
|
+
overflow: hidden;
|
|
803
|
+
text-overflow: ellipsis;
|
|
804
|
+
white-space: nowrap;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
.root-copy {
|
|
387
808
|
display: flex;
|
|
388
|
-
|
|
809
|
+
flex-direction: column;
|
|
810
|
+
min-width: 0;
|
|
389
811
|
}
|
|
390
812
|
|
|
391
|
-
.
|
|
392
|
-
|
|
393
|
-
height: 8px;
|
|
394
|
-
border-radius: 2px;
|
|
813
|
+
.root-sub {
|
|
814
|
+
font-size: 11px;
|
|
395
815
|
}
|
|
396
816
|
|
|
397
|
-
.
|
|
398
|
-
|
|
399
|
-
|
|
817
|
+
.root-meta {
|
|
818
|
+
color: var(--text3);
|
|
819
|
+
font-size: 11px;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.tree-panel {
|
|
823
|
+
display: flex;
|
|
824
|
+
flex-direction: column;
|
|
825
|
+
overflow: hidden;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
.tree-toolbar {
|
|
829
|
+
padding: 12px;
|
|
830
|
+
border-bottom: 0.5px solid var(--border);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
.search-input {
|
|
834
|
+
width: 100%;
|
|
835
|
+
padding: 10px 12px;
|
|
836
|
+
border: 1px solid var(--border);
|
|
837
|
+
border-radius: var(--radius);
|
|
838
|
+
background: var(--bg2);
|
|
839
|
+
color: var(--text);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.tree-frame {
|
|
843
|
+
flex: 1;
|
|
844
|
+
min-height: 0;
|
|
400
845
|
overflow: auto;
|
|
401
|
-
border: 0.5px solid var(--border);
|
|
402
|
-
border-radius: var(--radius-lg);
|
|
403
846
|
padding: 12px;
|
|
404
|
-
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
:deep(.tree-canvas) {
|
|
850
|
+
display: inline-block;
|
|
851
|
+
min-width: 100%;
|
|
852
|
+
width: max-content;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
:deep(.tree-node) {
|
|
856
|
+
margin-bottom: 4px;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
:deep(.tree-row) {
|
|
860
|
+
display: grid;
|
|
861
|
+
grid-template-columns: 8px 18px minmax(0, 1fr) auto;
|
|
862
|
+
align-items: center;
|
|
863
|
+
gap: 6px;
|
|
864
|
+
min-width: 0;
|
|
865
|
+
width: 100%;
|
|
866
|
+
padding: 4px 8px;
|
|
867
|
+
padding-left: calc(8px + (var(--tree-depth, 0) * 16px));
|
|
868
|
+
border: 1px solid transparent;
|
|
869
|
+
border-radius: var(--radius);
|
|
870
|
+
cursor: pointer;
|
|
871
|
+
white-space: nowrap;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
:deep(.tree-row:hover) {
|
|
875
|
+
background: var(--bg2);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
:deep(.tree-row.selected) {
|
|
879
|
+
background: color-mix(in srgb, var(--teal) 12%, var(--bg2));
|
|
880
|
+
border-color: var(--teal);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
:deep(.tree-row.hot) {
|
|
884
|
+
box-shadow: inset 2px 0 0 var(--red);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
:deep(.tree-toggle) {
|
|
888
|
+
width: 16px;
|
|
889
|
+
height: 16px;
|
|
890
|
+
border: none;
|
|
891
|
+
background: transparent;
|
|
892
|
+
color: var(--text3);
|
|
893
|
+
padding: 0;
|
|
894
|
+
font-size: 14px;
|
|
895
|
+
display: inline-flex;
|
|
896
|
+
align-items: center;
|
|
897
|
+
justify-content: center;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
:deep(.tree-toggle:disabled) {
|
|
901
|
+
cursor: default;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
:deep(.tree-toggle.empty) {
|
|
905
|
+
opacity: 0;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
:deep(.tree-rail) {
|
|
909
|
+
display: block;
|
|
910
|
+
width: 2px;
|
|
911
|
+
height: 14px;
|
|
912
|
+
border-radius: 999px;
|
|
913
|
+
background: color-mix(in srgb, var(--border) 75%, transparent);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
:deep(.tree-copy) {
|
|
405
917
|
display: flex;
|
|
406
|
-
|
|
918
|
+
align-items: center;
|
|
919
|
+
min-width: 0;
|
|
920
|
+
gap: 6px;
|
|
921
|
+
overflow: hidden;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
:deep(.tree-name) {
|
|
925
|
+
font-size: 12px;
|
|
926
|
+
color: var(--text);
|
|
927
|
+
min-width: 0;
|
|
928
|
+
overflow: hidden;
|
|
929
|
+
text-overflow: ellipsis;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
:deep(.tree-badges) {
|
|
933
|
+
display: flex;
|
|
934
|
+
gap: 6px;
|
|
935
|
+
flex-shrink: 0;
|
|
936
|
+
overflow: hidden;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
:deep(.tree-badge) {
|
|
940
|
+
border: 1px solid var(--border);
|
|
941
|
+
border-radius: 999px;
|
|
942
|
+
padding: 2px 7px;
|
|
943
|
+
font-size: 10px;
|
|
944
|
+
color: var(--text3);
|
|
945
|
+
white-space: nowrap;
|
|
946
|
+
overflow: hidden;
|
|
947
|
+
text-overflow: ellipsis;
|
|
948
|
+
max-width: 160px;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
:deep(.tree-metrics) {
|
|
952
|
+
display: flex;
|
|
953
|
+
align-items: center;
|
|
954
|
+
min-width: 92px;
|
|
955
|
+
justify-content: flex-end;
|
|
956
|
+
flex-shrink: 0;
|
|
407
957
|
gap: 6px;
|
|
408
958
|
}
|
|
409
959
|
|
|
960
|
+
:deep(.tree-metric-pill) {
|
|
961
|
+
display: inline-flex;
|
|
962
|
+
align-items: center;
|
|
963
|
+
justify-content: center;
|
|
964
|
+
min-width: 78px;
|
|
965
|
+
padding: 2px 8px;
|
|
966
|
+
border: 1px solid var(--border);
|
|
967
|
+
border-radius: 999px;
|
|
968
|
+
background: var(--bg2);
|
|
969
|
+
font-size: 10px;
|
|
970
|
+
color: var(--text3);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
:deep(.tree-nav-pill) {
|
|
974
|
+
display: inline-flex;
|
|
975
|
+
align-items: center;
|
|
976
|
+
justify-content: center;
|
|
977
|
+
min-width: 54px;
|
|
978
|
+
padding: 2px 8px;
|
|
979
|
+
border: 1px solid color-mix(in srgb, var(--purple) 55%, var(--border));
|
|
980
|
+
border-radius: 999px;
|
|
981
|
+
background: color-mix(in srgb, var(--purple) 10%, var(--bg2));
|
|
982
|
+
font-size: 10px;
|
|
983
|
+
color: color-mix(in srgb, var(--purple) 70%, white);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
:deep(.tree-children) {
|
|
987
|
+
margin-left: 7px;
|
|
988
|
+
padding-left: 11px;
|
|
989
|
+
border-left: 1px solid color-mix(in srgb, var(--border) 72%, transparent);
|
|
990
|
+
}
|
|
991
|
+
|
|
410
992
|
.detail-empty {
|
|
411
993
|
display: flex;
|
|
412
994
|
align-items: center;
|
|
@@ -428,6 +1010,30 @@ function toggleFreeze() {
|
|
|
428
1010
|
gap: 4px 12px;
|
|
429
1011
|
}
|
|
430
1012
|
|
|
1013
|
+
.detail-pill-row {
|
|
1014
|
+
display: flex;
|
|
1015
|
+
flex-wrap: wrap;
|
|
1016
|
+
gap: 6px;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
.detail-pill {
|
|
1020
|
+
border: 1px solid var(--border);
|
|
1021
|
+
border-radius: 999px;
|
|
1022
|
+
padding: 4px 8px;
|
|
1023
|
+
background: var(--bg2);
|
|
1024
|
+
font-size: 11px;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
.detail-pill.hot {
|
|
1028
|
+
border-color: color-mix(in srgb, var(--red) 50%, var(--border));
|
|
1029
|
+
color: var(--red);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
.detail-pill.nav {
|
|
1033
|
+
border-color: color-mix(in srgb, var(--purple) 55%, var(--border));
|
|
1034
|
+
color: color-mix(in srgb, var(--purple) 70%, white);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
431
1037
|
.section-label {
|
|
432
1038
|
font-size: 10px;
|
|
433
1039
|
font-weight: 500;
|
|
@@ -445,4 +1051,15 @@ function toggleFreeze() {
|
|
|
445
1051
|
margin-bottom: 3px;
|
|
446
1052
|
color: var(--text2);
|
|
447
1053
|
}
|
|
1054
|
+
|
|
1055
|
+
@media (max-width: 1180px) {
|
|
1056
|
+
.inspector {
|
|
1057
|
+
grid-template-columns: minmax(200px, 240px) minmax(0, 1fr);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
.detail-panel {
|
|
1061
|
+
grid-column: 1 / -1;
|
|
1062
|
+
max-height: 220px;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
448
1065
|
</style>
|