nuxt-devtools-observatory 0.1.0
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/README.md +209 -0
- package/client/dist/assets/index-C76d764s.js +17 -0
- package/client/dist/assets/index-yIuOV1_N.css +1 -0
- package/client/dist/index.html +47 -0
- package/client/index.html +46 -0
- package/client/src/App.vue +114 -0
- package/client/src/main.ts +5 -0
- package/client/src/stores/observatory.ts +65 -0
- package/client/src/style.css +261 -0
- package/client/src/views/ComposableTracker.vue +347 -0
- package/client/src/views/FetchDashboard.vue +492 -0
- package/client/src/views/ProvideInjectGraph.vue +481 -0
- package/client/src/views/RenderHeatmap.vue +492 -0
- package/client/src/views/TransitionTimeline.vue +527 -0
- package/client/tsconfig.json +16 -0
- package/client/vite.config.ts +12 -0
- package/dist/module.d.mts +38 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +562 -0
- package/dist/runtime/composables/composable-registry.d.ts +40 -0
- package/dist/runtime/composables/composable-registry.js +135 -0
- package/dist/runtime/composables/fetch-registry.d.ts +63 -0
- package/dist/runtime/composables/fetch-registry.js +83 -0
- package/dist/runtime/composables/provide-inject-registry.d.ts +57 -0
- package/dist/runtime/composables/provide-inject-registry.js +96 -0
- package/dist/runtime/composables/render-registry.d.ts +36 -0
- package/dist/runtime/composables/render-registry.js +85 -0
- package/dist/runtime/composables/transition-registry.d.ts +21 -0
- package/dist/runtime/composables/transition-registry.js +125 -0
- package/dist/runtime/plugin.d.ts +2 -0
- package/dist/runtime/plugin.js +66 -0
- package/dist/types.d.mts +3 -0
- package/package.json +89 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, defineComponent, h, onUnmounted, type VNode } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface ComponentNode {
|
|
5
|
+
id: string
|
|
6
|
+
label: string
|
|
7
|
+
file: string
|
|
8
|
+
renders: number
|
|
9
|
+
avgMs: number
|
|
10
|
+
triggers: string[]
|
|
11
|
+
children: ComponentNode[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ComponentBlock — recursive inline component
|
|
15
|
+
const ComponentBlock = defineComponent({
|
|
16
|
+
name: 'ComponentBlock',
|
|
17
|
+
props: {
|
|
18
|
+
node: Object as () => ComponentNode,
|
|
19
|
+
mode: String,
|
|
20
|
+
threshold: Number,
|
|
21
|
+
hotOnly: Boolean,
|
|
22
|
+
selected: String,
|
|
23
|
+
},
|
|
24
|
+
emits: ['select'],
|
|
25
|
+
setup(props, { emit }): () => VNode | null {
|
|
26
|
+
function getVal(n: ComponentNode) {
|
|
27
|
+
return props.mode === 'count' ? n.renders : n.avgMs
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getMax(): number {
|
|
31
|
+
let max = 1
|
|
32
|
+
|
|
33
|
+
function walk(ns: ComponentNode[]) {
|
|
34
|
+
ns.forEach((n) => {
|
|
35
|
+
const v = getVal(n)
|
|
36
|
+
|
|
37
|
+
if (v > max) {
|
|
38
|
+
max = v
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
walk(n.children)
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
walk([props.node!])
|
|
46
|
+
|
|
47
|
+
return Math.max(max, props.mode === 'count' ? 40 : 20)
|
|
48
|
+
}
|
|
49
|
+
function heatColor(val: number, max: number) {
|
|
50
|
+
const r = Math.min(val / max, 1)
|
|
51
|
+
|
|
52
|
+
if (r < 0.25) {
|
|
53
|
+
return { bg: '#EAF3DE', text: '#27500A', border: '#97C459' }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (r < 0.55) {
|
|
57
|
+
return { bg: '#FAEEDA', text: '#633806', border: '#EF9F27' }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (r < 0.8) {
|
|
61
|
+
return { bg: '#FAECE7', text: '#712B13', border: '#D85A30' }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { bg: '#FCEBEB', text: '#791F1F', border: '#E24B4A' }
|
|
65
|
+
}
|
|
66
|
+
function isHot(n: ComponentNode) {
|
|
67
|
+
return (props.mode === 'count' ? n.renders : n.avgMs) >= props.threshold!
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return () => {
|
|
71
|
+
const n = props.node!
|
|
72
|
+
|
|
73
|
+
if (props.hotOnly && !isHot(n) && !n.children.some((c) => (props.mode === 'count' ? c.renders : c.avgMs) >= props.threshold!)) {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const max = getMax()
|
|
78
|
+
const val = getVal(n)
|
|
79
|
+
const col = heatColor(val, max)
|
|
80
|
+
const isSel = props.selected === n.id
|
|
81
|
+
const unit = props.mode === 'count' ? 'renders' : 'ms avg'
|
|
82
|
+
const valStr = props.mode === 'count' ? String(val) : val.toFixed(1) + 'ms'
|
|
83
|
+
|
|
84
|
+
return h(
|
|
85
|
+
'div',
|
|
86
|
+
{
|
|
87
|
+
style: {
|
|
88
|
+
background: col.bg,
|
|
89
|
+
border: isSel ? `2px solid ${col.border}` : `1px solid ${col.border}`,
|
|
90
|
+
borderRadius: '6px',
|
|
91
|
+
padding: '6px 9px',
|
|
92
|
+
marginBottom: '5px',
|
|
93
|
+
cursor: 'pointer',
|
|
94
|
+
},
|
|
95
|
+
onClick: () => emit('select', n),
|
|
96
|
+
},
|
|
97
|
+
[
|
|
98
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, [
|
|
99
|
+
h('span', { style: { fontFamily: 'var(--mono)', fontSize: '11px', fontWeight: '500', color: col.text } }, n.label),
|
|
100
|
+
h(
|
|
101
|
+
'span',
|
|
102
|
+
{ style: { fontFamily: 'var(--mono)', fontSize: '10px', color: col.text, opacity: '0.7', marginLeft: 'auto' } },
|
|
103
|
+
`${valStr} ${unit}`
|
|
104
|
+
),
|
|
105
|
+
]),
|
|
106
|
+
n.children.length
|
|
107
|
+
? h(
|
|
108
|
+
'div',
|
|
109
|
+
{
|
|
110
|
+
style: {
|
|
111
|
+
marginLeft: '10px',
|
|
112
|
+
borderLeft: `1.5px solid ${col.border}40`,
|
|
113
|
+
paddingLeft: '8px',
|
|
114
|
+
marginTop: '5px',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
n.children.map((child) =>
|
|
118
|
+
h(ComponentBlock, {
|
|
119
|
+
node: child,
|
|
120
|
+
mode: props.mode,
|
|
121
|
+
threshold: props.threshold,
|
|
122
|
+
hotOnly: props.hotOnly,
|
|
123
|
+
selected: props.selected,
|
|
124
|
+
onSelect: (v: ComponentNode) => emit('select', v),
|
|
125
|
+
})
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
: null,
|
|
129
|
+
]
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// Mock data
|
|
136
|
+
const baseNodes = ref<ComponentNode[]>([
|
|
137
|
+
{
|
|
138
|
+
id: 'NavBar',
|
|
139
|
+
label: 'NavBar.vue',
|
|
140
|
+
file: 'components/NavBar.vue',
|
|
141
|
+
renders: 3,
|
|
142
|
+
avgMs: 1.2,
|
|
143
|
+
triggers: ['props.user changed'],
|
|
144
|
+
children: [],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'Sidebar',
|
|
148
|
+
label: 'Sidebar.vue',
|
|
149
|
+
file: 'components/Sidebar.vue',
|
|
150
|
+
renders: 2,
|
|
151
|
+
avgMs: 0.8,
|
|
152
|
+
triggers: ['parent re-render'],
|
|
153
|
+
children: [],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: 'ProductGrid',
|
|
157
|
+
label: 'ProductGrid.vue',
|
|
158
|
+
file: 'components/ProductGrid.vue',
|
|
159
|
+
renders: 18,
|
|
160
|
+
avgMs: 14.3,
|
|
161
|
+
triggers: ['store: products updated', 'props.filter changed', 'parent re-render (×16)'],
|
|
162
|
+
children: [
|
|
163
|
+
{
|
|
164
|
+
id: 'ProductCard',
|
|
165
|
+
label: 'ProductCard.vue ×12',
|
|
166
|
+
file: 'components/ProductCard.vue',
|
|
167
|
+
renders: 24,
|
|
168
|
+
avgMs: 3.1,
|
|
169
|
+
triggers: ['parent re-render (×24)'],
|
|
170
|
+
children: [],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: 'PriceTag',
|
|
174
|
+
label: 'PriceTag.vue ×12',
|
|
175
|
+
file: 'components/PriceTag.vue',
|
|
176
|
+
renders: 36,
|
|
177
|
+
avgMs: 0.4,
|
|
178
|
+
triggers: ['props.price changed (×36)'],
|
|
179
|
+
children: [],
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: 'CartSummary',
|
|
185
|
+
label: 'CartSummary.vue',
|
|
186
|
+
file: 'components/CartSummary.vue',
|
|
187
|
+
renders: 9,
|
|
188
|
+
avgMs: 5.7,
|
|
189
|
+
triggers: ['store: cart updated (×9)'],
|
|
190
|
+
children: [
|
|
191
|
+
{
|
|
192
|
+
id: 'CartItem',
|
|
193
|
+
label: 'CartItem.vue ×3',
|
|
194
|
+
file: 'components/CartItem.vue',
|
|
195
|
+
renders: 12,
|
|
196
|
+
avgMs: 1.8,
|
|
197
|
+
triggers: ['parent re-render (×12)'],
|
|
198
|
+
children: [],
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: 'FilterBar',
|
|
204
|
+
label: 'FilterBar.vue',
|
|
205
|
+
file: 'components/FilterBar.vue',
|
|
206
|
+
renders: 7,
|
|
207
|
+
avgMs: 2.1,
|
|
208
|
+
triggers: ['store: filters changed (×7)'],
|
|
209
|
+
children: [],
|
|
210
|
+
},
|
|
211
|
+
{ id: 'Footer', label: 'Footer.vue', file: 'components/Footer.vue', renders: 1, avgMs: 0.3, triggers: ['initial mount'], children: [] },
|
|
212
|
+
])
|
|
213
|
+
|
|
214
|
+
const activeMode = ref<'count' | 'time'>('count')
|
|
215
|
+
const activeThreshold = ref(5)
|
|
216
|
+
const activeHotOnly = ref(false)
|
|
217
|
+
const frozen = ref(false)
|
|
218
|
+
const activeSelected = ref<ComponentNode | null>(null)
|
|
219
|
+
let liveInterval: ReturnType<typeof setInterval> | null = null
|
|
220
|
+
|
|
221
|
+
const rootNodes = computed(() => baseNodes.value)
|
|
222
|
+
|
|
223
|
+
const allComponents = computed(() => {
|
|
224
|
+
const all: ComponentNode[] = []
|
|
225
|
+
|
|
226
|
+
function collect(ns: ComponentNode[]) {
|
|
227
|
+
ns.forEach((n) => {
|
|
228
|
+
all.push(n)
|
|
229
|
+
collect(n.children)
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
collect(baseNodes.value)
|
|
234
|
+
|
|
235
|
+
return all
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
const totalRenders = computed(() => allComponents.value.reduce((a, n) => a + n.renders, 0))
|
|
239
|
+
const hotCount = computed(() => allComponents.value.filter((n) => isHot(n)).length)
|
|
240
|
+
const avgTime = computed(() => {
|
|
241
|
+
const comps = allComponents.value.filter((n) => n.avgMs > 0)
|
|
242
|
+
|
|
243
|
+
if (!comps.length) {
|
|
244
|
+
return '0.0'
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return (comps.reduce((a, n) => a + n.avgMs, 0) / comps.length).toFixed(1)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
function isHot(n: ComponentNode) {
|
|
251
|
+
return (activeMode.value === 'count' ? n.renders : n.avgMs) >= activeThreshold.value
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function startLive() {
|
|
255
|
+
liveInterval = setInterval(() => {
|
|
256
|
+
if (frozen.value) {
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
allComponents.value.forEach((n) => {
|
|
261
|
+
if (Math.random() < 0.3) n.renders += Math.floor(Math.random() * 3) + 1
|
|
262
|
+
})
|
|
263
|
+
}, 1800)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function toggleFreeze() {
|
|
267
|
+
frozen.value = !frozen.value
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
startLive()
|
|
271
|
+
onUnmounted(() => {
|
|
272
|
+
if (liveInterval) clearInterval(liveInterval)
|
|
273
|
+
})
|
|
274
|
+
</script>
|
|
275
|
+
|
|
276
|
+
<template>
|
|
277
|
+
<div class="view">
|
|
278
|
+
<!-- Controls -->
|
|
279
|
+
<div class="controls">
|
|
280
|
+
<div class="mode-group">
|
|
281
|
+
<button :class="{ active: activeMode === 'count' }" @click="activeMode = 'count'">render count</button>
|
|
282
|
+
<button :class="{ active: activeMode === 'time' }" @click="activeMode = 'time'">render time</button>
|
|
283
|
+
</div>
|
|
284
|
+
<div class="threshold-group">
|
|
285
|
+
<span class="muted text-sm">threshold</span>
|
|
286
|
+
<input v-model.number="activeThreshold" type="range" min="1" max="30" step="1" style="width: 90px" />
|
|
287
|
+
<span class="mono text-sm">{{ activeThreshold }}+</span>
|
|
288
|
+
</div>
|
|
289
|
+
<button :class="{ active: activeHotOnly }" @click="activeHotOnly = !activeHotOnly">hot only</button>
|
|
290
|
+
<button :class="{ active: frozen }" style="margin-left: auto" @click="toggleFreeze">
|
|
291
|
+
{{ frozen ? 'unfreeze' : 'freeze snapshot' }}
|
|
292
|
+
</button>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<!-- Stats -->
|
|
296
|
+
<div class="stats-row">
|
|
297
|
+
<div class="stat-card">
|
|
298
|
+
<div class="stat-label">components</div>
|
|
299
|
+
<div class="stat-val">{{ allComponents.length }}</div>
|
|
300
|
+
</div>
|
|
301
|
+
<div class="stat-card">
|
|
302
|
+
<div class="stat-label">total renders</div>
|
|
303
|
+
<div class="stat-val">{{ totalRenders }}</div>
|
|
304
|
+
</div>
|
|
305
|
+
<div class="stat-card">
|
|
306
|
+
<div class="stat-label">hot</div>
|
|
307
|
+
<div class="stat-val" style="color: var(--red)">{{ hotCount }}</div>
|
|
308
|
+
</div>
|
|
309
|
+
<div class="stat-card">
|
|
310
|
+
<div class="stat-label">avg time</div>
|
|
311
|
+
<div class="stat-val">{{ avgTime }}ms</div>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<div class="split">
|
|
316
|
+
<!-- Page mockup -->
|
|
317
|
+
<div class="page-frame">
|
|
318
|
+
<div class="legend">
|
|
319
|
+
<div class="swatch-row">
|
|
320
|
+
<span class="swatch" style="background: #eaf3de"></span>
|
|
321
|
+
<span class="swatch" style="background: #97c459"></span>
|
|
322
|
+
<span class="swatch" style="background: #ef9f27"></span>
|
|
323
|
+
<span class="swatch" style="background: #e24b4a"></span>
|
|
324
|
+
</div>
|
|
325
|
+
<span class="muted text-sm">cool → hot</span>
|
|
326
|
+
</div>
|
|
327
|
+
<ComponentBlock
|
|
328
|
+
v-for="rootNode in rootNodes"
|
|
329
|
+
:key="rootNode.id"
|
|
330
|
+
:node="rootNode"
|
|
331
|
+
:mode="activeMode"
|
|
332
|
+
:threshold="activeThreshold"
|
|
333
|
+
:hot-only="activeHotOnly"
|
|
334
|
+
:selected="activeSelected?.id"
|
|
335
|
+
@select="activeSelected = $event"
|
|
336
|
+
/>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<!-- Detail panel -->
|
|
340
|
+
<div class="sidebar">
|
|
341
|
+
<template v-if="activeSelected">
|
|
342
|
+
<div class="detail-header">
|
|
343
|
+
<span class="mono bold" style="font-size: 12px">{{ activeSelected.label }}</span>
|
|
344
|
+
<button @click="activeSelected = null">×</button>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<div class="meta-grid">
|
|
348
|
+
<span class="muted text-sm">renders</span>
|
|
349
|
+
<span class="mono text-sm">{{ activeSelected.renders }}</span>
|
|
350
|
+
<span class="muted text-sm">avg time</span>
|
|
351
|
+
<span class="mono text-sm">{{ activeSelected.avgMs.toFixed(1) }}ms</span>
|
|
352
|
+
<span class="muted text-sm">hot?</span>
|
|
353
|
+
<span class="text-sm" :style="{ color: isHot(activeSelected) ? 'var(--red)' : 'var(--teal)' }">
|
|
354
|
+
{{ isHot(activeSelected) ? 'yes' : 'no' }}
|
|
355
|
+
</span>
|
|
356
|
+
<span class="muted text-sm">file</span>
|
|
357
|
+
<span class="mono text-sm muted">{{ activeSelected.file }}</span>
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
<div class="section-label">triggers</div>
|
|
361
|
+
<div v-for="t in activeSelected.triggers" :key="t" class="trigger-item mono text-sm">{{ t }}</div>
|
|
362
|
+
<div v-if="!activeSelected.triggers.length" class="muted text-sm">no triggers recorded</div>
|
|
363
|
+
</template>
|
|
364
|
+
<div v-else class="detail-empty">click a component to inspect</div>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
</template>
|
|
369
|
+
|
|
370
|
+
<style scoped>
|
|
371
|
+
.view {
|
|
372
|
+
display: flex;
|
|
373
|
+
flex-direction: column;
|
|
374
|
+
height: 100%;
|
|
375
|
+
overflow: hidden;
|
|
376
|
+
padding: 12px;
|
|
377
|
+
gap: 10px;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.controls {
|
|
381
|
+
display: flex;
|
|
382
|
+
align-items: center;
|
|
383
|
+
gap: 8px;
|
|
384
|
+
flex-shrink: 0;
|
|
385
|
+
flex-wrap: wrap;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.mode-group {
|
|
389
|
+
display: flex;
|
|
390
|
+
gap: 2px;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.threshold-group {
|
|
394
|
+
display: flex;
|
|
395
|
+
align-items: center;
|
|
396
|
+
gap: 6px;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.stats-row {
|
|
400
|
+
display: grid;
|
|
401
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
402
|
+
gap: 8px;
|
|
403
|
+
flex-shrink: 0;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.split {
|
|
407
|
+
display: flex;
|
|
408
|
+
gap: 12px;
|
|
409
|
+
flex: 1;
|
|
410
|
+
overflow: hidden;
|
|
411
|
+
min-height: 0;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.page-frame {
|
|
415
|
+
flex: 1;
|
|
416
|
+
overflow: auto;
|
|
417
|
+
border: 0.5px solid var(--border);
|
|
418
|
+
border-radius: var(--radius-lg);
|
|
419
|
+
padding: 12px;
|
|
420
|
+
background: var(--bg3);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.legend {
|
|
424
|
+
display: flex;
|
|
425
|
+
align-items: center;
|
|
426
|
+
gap: 8px;
|
|
427
|
+
margin-bottom: 10px;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.swatch-row {
|
|
431
|
+
display: flex;
|
|
432
|
+
gap: 2px;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.swatch {
|
|
436
|
+
width: 16px;
|
|
437
|
+
height: 8px;
|
|
438
|
+
border-radius: 2px;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.sidebar {
|
|
442
|
+
width: 260px;
|
|
443
|
+
flex-shrink: 0;
|
|
444
|
+
overflow: auto;
|
|
445
|
+
border: 0.5px solid var(--border);
|
|
446
|
+
border-radius: var(--radius-lg);
|
|
447
|
+
padding: 12px;
|
|
448
|
+
background: var(--bg3);
|
|
449
|
+
display: flex;
|
|
450
|
+
flex-direction: column;
|
|
451
|
+
gap: 6px;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.detail-empty {
|
|
455
|
+
display: flex;
|
|
456
|
+
align-items: center;
|
|
457
|
+
justify-content: center;
|
|
458
|
+
height: 100%;
|
|
459
|
+
color: var(--text3);
|
|
460
|
+
font-size: 12px;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.detail-header {
|
|
464
|
+
display: flex;
|
|
465
|
+
align-items: center;
|
|
466
|
+
justify-content: space-between;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.meta-grid {
|
|
470
|
+
display: grid;
|
|
471
|
+
grid-template-columns: auto 1fr;
|
|
472
|
+
gap: 4px 12px;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.section-label {
|
|
476
|
+
font-size: 10px;
|
|
477
|
+
font-weight: 500;
|
|
478
|
+
text-transform: uppercase;
|
|
479
|
+
letter-spacing: 0.4px;
|
|
480
|
+
color: var(--text3);
|
|
481
|
+
margin-top: 8px;
|
|
482
|
+
margin-bottom: 4px;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.trigger-item {
|
|
486
|
+
background: var(--bg2);
|
|
487
|
+
border-radius: var(--radius);
|
|
488
|
+
padding: 4px 8px;
|
|
489
|
+
margin-bottom: 3px;
|
|
490
|
+
color: var(--text2);
|
|
491
|
+
}
|
|
492
|
+
</style>
|