nuxt-seo-utils 8.2.0 → 8.3.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.
Files changed (51) hide show
  1. package/dist/cli.mjs +1 -1
  2. package/dist/devtools/lib/seo-utils/rpc.ts +3 -0
  3. package/dist/devtools/lib/seo-utils/state.ts +35 -0
  4. package/dist/devtools/lib/seo-utils/tools.ts +98 -0
  5. package/dist/devtools/nuxt.config.ts +3 -0
  6. package/dist/devtools/pages/seo-utils/debug.vue +50 -0
  7. package/dist/devtools/pages/seo-utils/docs.vue +3 -0
  8. package/dist/devtools/pages/seo-utils/identity.vue +1168 -0
  9. package/dist/devtools/pages/seo-utils/index.vue +609 -0
  10. package/dist/devtools/pages/seo-utils/social.vue +515 -0
  11. package/dist/devtools/pages/seo-utils.vue +64 -0
  12. package/dist/module.json +1 -1
  13. package/dist/module.mjs +2 -2
  14. package/dist/runtime/app/composables/polyfills.js +1 -1
  15. package/dist/runtime/app/composables/useBreadcrumbItems.js +3 -3
  16. package/dist/runtime/app/composables/useFallbackTitle.js +1 -1
  17. package/dist/runtime/app/composables/useShareLinks.js +2 -2
  18. package/dist/runtime/app/logic/applyDefaults.js +2 -2
  19. package/dist/runtime/app/plugins/1.absoluteImageUrls.server.js +1 -1
  20. package/dist/runtime/app/plugins/siteConfig.js +1 -1
  21. package/dist/runtime/server/middleware/redirectCanonical.js +1 -1
  22. package/dist/runtime/server/middleware/resolveImagesInPagesDir.js +1 -1
  23. package/dist/runtime/server/routes/__nuxt-seo-utils/debug.d.ts +1 -1
  24. package/dist/runtime/server/routes/__nuxt-seo-utils/debug.js +1 -1
  25. package/package.json +32 -31
  26. package/dist/devtools/200.html +0 -1
  27. package/dist/devtools/404.html +0 -1
  28. package/dist/devtools/_nuxt/BEw_EXq1.js +0 -14
  29. package/dist/devtools/_nuxt/BgCgKrEE.js +0 -1
  30. package/dist/devtools/_nuxt/C4gkMyix.js +0 -1
  31. package/dist/devtools/_nuxt/CQ4Jy7Nm.js +0 -1
  32. package/dist/devtools/_nuxt/DcgcSEa-.js +0 -33
  33. package/dist/devtools/_nuxt/DeIiaCYF.js +0 -1
  34. package/dist/devtools/_nuxt/DevtoolsMetric.DabK8i8g.css +0 -1
  35. package/dist/devtools/_nuxt/DfZjB7w2.js +0 -1
  36. package/dist/devtools/_nuxt/DjEWf-n1.js +0 -1
  37. package/dist/devtools/_nuxt/builds/latest.json +0 -1
  38. package/dist/devtools/_nuxt/builds/meta/21c3fe25-8926-44ca-9d37-f478243d148a.json +0 -1
  39. package/dist/devtools/_nuxt/debug.B9T5jmI-.css +0 -1
  40. package/dist/devtools/_nuxt/entry.aDw8hoj_.css +0 -1
  41. package/dist/devtools/_nuxt/fira-code.Bc8wnsZt.woff2 +0 -0
  42. package/dist/devtools/_nuxt/hubot-sans.DLGyhQVu.woff2 +0 -0
  43. package/dist/devtools/_nuxt/identity.Dl98KbLG.css +0 -1
  44. package/dist/devtools/_nuxt/index.BrQ8jYVK.css +0 -1
  45. package/dist/devtools/_nuxt/social.DrJTEVxX.css +0 -1
  46. package/dist/devtools/_nuxt/tools.DM91U8p4.css +0 -1
  47. package/dist/devtools/debug/index.html +0 -1
  48. package/dist/devtools/docs/index.html +0 -1
  49. package/dist/devtools/identity/index.html +0 -1
  50. package/dist/devtools/index.html +0 -1
  51. package/dist/devtools/social/index.html +0 -1
@@ -0,0 +1,1168 @@
1
+ <script setup lang="ts">
2
+ import { useClipboard } from '@vueuse/core'
3
+ import { computed, ref, watch } from 'vue'
4
+ import { data } from '../../lib/seo-utils/state'
5
+ import { parseMetaTags } from '../../lib/seo-utils/tools'
6
+
7
+ const { copy } = useClipboard()
8
+
9
+ const loading = ref(false)
10
+ const error = ref<string | null>(null)
11
+
12
+ interface IconLink {
13
+ rel: string
14
+ href: string
15
+ type?: string
16
+ sizes?: string
17
+ media?: string
18
+ }
19
+
20
+ interface ParsedIdentity {
21
+ icons: IconLink[]
22
+ ogImages: Array<{ url: string, width?: string, height?: string, alt?: string }>
23
+ twitterImages: Array<{ url: string, width?: string, height?: string }>
24
+ themeColors: Array<{ color: string, media?: string, context: 'light' | 'dark' | 'default' }>
25
+ colorScheme: string | null
26
+ }
27
+
28
+ const parsedPage = ref<ParsedIdentity | null>(null)
29
+
30
+ const siteConfig = computed(() => data.value?.siteConfig || null)
31
+ const pageIcons = computed(() => parsedPage.value?.icons || [])
32
+ const hasIcons = computed(() => pageIcons.value.length > 0)
33
+
34
+ const iconsByRel = computed(() => {
35
+ const groups: Record<string, IconLink[]> = {}
36
+ pageIcons.value.forEach((link) => {
37
+ const rel = link.rel || 'icon'
38
+ if (!groups[rel])
39
+ groups[rel] = []
40
+ groups[rel].push(link)
41
+ })
42
+ return groups
43
+ })
44
+
45
+ const iconRelLabels: Record<string, { label: string, description: string }> = {
46
+ 'icon': {
47
+ label: 'Favicon',
48
+ description: 'Browser tab icons used across the site',
49
+ },
50
+ 'apple-touch-icon': {
51
+ label: 'Apple Touch Icon',
52
+ description: 'High resolution icon for iOS home screen',
53
+ },
54
+ }
55
+
56
+ // Identity completeness score
57
+ const identityScore = computed(() => {
58
+ if (!siteConfig.value || !parsedPage.value)
59
+ return { present: 0, total: 0 }
60
+ const checks = [
61
+ !!siteConfig.value.name,
62
+ !!siteConfig.value.url && siteConfig.value.url !== 'http://localhost:3000',
63
+ !!siteConfig.value.description,
64
+ hasIcons.value,
65
+ parsedPage.value.ogImages.length > 0,
66
+ parsedPage.value.themeColors.length > 0,
67
+ ]
68
+ return { present: checks.filter(Boolean).length, total: checks.length }
69
+ })
70
+
71
+ const scorePercent = computed(() => {
72
+ const { present, total } = identityScore.value
73
+ return total > 0 ? Math.round((present / total) * 100) : 0
74
+ })
75
+
76
+ const scoreVariant = computed(() => {
77
+ const p = scorePercent.value
78
+ if (p >= 80)
79
+ return 'success'
80
+ if (p >= 50)
81
+ return 'warning'
82
+ return 'warning'
83
+ })
84
+
85
+ // Favicon URL for browser tab preview
86
+ const faviconUrl = computed(() => {
87
+ const ico = pageIcons.value.find(l => l.href?.includes('favicon.ico'))
88
+ if (ico)
89
+ return ico.href
90
+ const smallest = pageIcons.value.find(l => l.rel === 'icon')
91
+ return smallest?.href || null
92
+ })
93
+
94
+ const displayUrl = computed(() => {
95
+ if (!siteConfig.value?.url)
96
+ return 'localhost:3000'
97
+ try {
98
+ const url = new URL(siteConfig.value.url)
99
+ return url.hostname
100
+ }
101
+ catch {
102
+ return siteConfig.value.url
103
+ }
104
+ })
105
+
106
+ async function checkIdentity() {
107
+ loading.value = true
108
+ error.value = null
109
+ parsedPage.value = null
110
+
111
+ try {
112
+ const baseUrl = window.parent?.location?.origin || window.location.origin
113
+ const response = await fetch(`${baseUrl}${path.value}`, {
114
+ headers: { Accept: 'text/html' },
115
+ })
116
+ if (!response.ok)
117
+ throw new Error(`HTTP ${response.status}`)
118
+ const html = await response.text()
119
+ const parsed = parseMetaTags(html)
120
+
121
+ const themeColors: ParsedIdentity['themeColors'] = []
122
+ parsed.allMeta
123
+ .filter(m => m.name === 'theme-color')
124
+ .forEach((m) => {
125
+ let context: 'light' | 'dark' | 'default' = 'default'
126
+ const media = m.media || null
127
+ if (media?.includes('prefers-color-scheme: light'))
128
+ context = 'light'
129
+ else if (media?.includes('prefers-color-scheme: dark'))
130
+ context = 'dark'
131
+ themeColors.push({ color: m.content, media: media || undefined, context })
132
+ })
133
+
134
+ const ogImages: ParsedIdentity['ogImages'] = []
135
+ if (parsed.ogTags['og:image']) {
136
+ ogImages.push({
137
+ url: parsed.ogTags['og:image'],
138
+ width: parsed.ogTags['og:image:width'],
139
+ height: parsed.ogTags['og:image:height'],
140
+ alt: parsed.ogTags['og:image:alt'],
141
+ })
142
+ }
143
+
144
+ const twitterImages: ParsedIdentity['twitterImages'] = []
145
+ if (parsed.twitterTags['twitter:image']) {
146
+ twitterImages.push({
147
+ url: parsed.twitterTags['twitter:image'],
148
+ width: parsed.twitterTags['twitter:image:width'],
149
+ height: parsed.twitterTags['twitter:image:height'],
150
+ })
151
+ }
152
+
153
+ const colorSchemeTag = parsed.allMeta.find(m => m.name === 'color-scheme')
154
+
155
+ // Resolve relative icon URLs to absolute
156
+ const icons = parsed.iconLinks.map(icon => ({
157
+ ...icon,
158
+ href: icon.href.startsWith('http') ? icon.href : `${baseUrl}${icon.href.startsWith('/') ? '' : '/'}${icon.href}`,
159
+ }))
160
+
161
+ parsedPage.value = {
162
+ icons,
163
+ ogImages,
164
+ twitterImages,
165
+ themeColors,
166
+ colorScheme: colorSchemeTag?.content || null,
167
+ }
168
+ }
169
+ catch (e: any) {
170
+ error.value = e.message || 'Failed to fetch page'
171
+ }
172
+ finally {
173
+ loading.value = false
174
+ }
175
+ }
176
+
177
+ watch([() => data.value, path, refreshTime], () => {
178
+ if (data.value)
179
+ checkIdentity()
180
+ }, { immediate: true })
181
+
182
+ const cliCommand = 'npx nuxt-seo-utils icons --source <your-logo.svg>'
183
+
184
+ const iconSizesGenerated = [
185
+ { name: 'favicon.ico', size: '32x32', desc: 'Browser tab' },
186
+ { name: 'icon-16x16.png', size: '16x16', desc: 'Small favicon' },
187
+ { name: 'icon-32x32.png', size: '32x32', desc: 'Standard favicon' },
188
+ { name: 'apple-touch-icon.png', size: '180x180', desc: 'iOS home screen' },
189
+ { name: 'icon-192x192.png', size: '192x192', desc: 'Android/PWA' },
190
+ { name: 'icon-512x512.png', size: '512x512', desc: 'PWA splash' },
191
+ ]
192
+ </script>
193
+
194
+ <template>
195
+ <div class="space-y-5 stagger-children">
196
+ <!-- Loading -->
197
+ <DevtoolsLoading v-if="loading" />
198
+
199
+ <!-- Error -->
200
+ <DevtoolsError v-else-if="error" :error="error" title="Failed to check page">
201
+ <UButton size="sm" variant="ghost" icon="carbon:reset" label="Retry" @click="checkIdentity" />
202
+ </DevtoolsError>
203
+
204
+ <!-- Results -->
205
+ <template v-else-if="parsedPage">
206
+ <!-- Hero: Identity Summary with browser tab preview -->
207
+ <div class="id-hero" :class="`id-hero--${scoreVariant}`">
208
+ <div class="id-hero__left">
209
+ <!-- Score ring -->
210
+ <div class="id-score-ring">
211
+ <svg viewBox="0 0 36 36" class="id-score-ring__svg">
212
+ <circle cx="18" cy="18" r="15.5" fill="none" stroke="var(--color-border-subtle)" stroke-width="2.5" />
213
+ <circle
214
+ cx="18" cy="18" r="15.5" fill="none"
215
+ class="id-score-ring__progress"
216
+ :class="`id-score-ring__progress--${scoreVariant}`"
217
+ stroke-width="2.5"
218
+ stroke-linecap="round"
219
+ :stroke-dasharray="`${scorePercent * 0.974}, 100`"
220
+ transform="rotate(-90 18 18)"
221
+ />
222
+ </svg>
223
+ <span class="id-score-ring__text">{{ identityScore.present }}<span class="id-score-ring__sep">/</span>{{ identityScore.total }}</span>
224
+ </div>
225
+ <div>
226
+ <p class="font-medium text-sm">
227
+ {{ scorePercent >= 80 ? 'Identity configured' : scorePercent >= 50 ? 'Partially configured' : 'Needs setup' }}
228
+ </p>
229
+ <p class="text-xs text-[var(--color-text-muted)]">
230
+ {{ identityScore.present }} of {{ identityScore.total }} identity checks passing
231
+ </p>
232
+ </div>
233
+ </div>
234
+
235
+ <!-- Mini browser tab preview -->
236
+ <div class="id-browser-tab">
237
+ <div class="id-browser-tab__chrome">
238
+ <div class="id-browser-tab__dots">
239
+ <span /><span /><span />
240
+ </div>
241
+ <div class="id-browser-tab__tab">
242
+ <img v-if="faviconUrl" :src="faviconUrl" class="id-browser-tab__favicon" alt="">
243
+ <UIcon v-else name="carbon:earth" class="id-browser-tab__favicon-placeholder" />
244
+ <span class="id-browser-tab__title">{{ siteConfig?.name || 'Untitled' }}</span>
245
+ </div>
246
+ </div>
247
+ <div class="id-browser-tab__bar">
248
+ <UIcon name="carbon:locked" class="id-browser-tab__lock" />
249
+ <span class="id-browser-tab__url">{{ displayUrl }}</span>
250
+ </div>
251
+ </div>
252
+ </div>
253
+
254
+ <!-- Icons -->
255
+ <DevtoolsSection icon="carbon:image" text="Icons">
256
+ <template #actions>
257
+ <DevtoolsMetric :value="pageIcons.length" label="icons" />
258
+ </template>
259
+
260
+ <template v-if="hasIcons">
261
+ <div v-for="(icons, rel) in iconsByRel" :key="rel" class="mb-5 last:mb-0">
262
+ <div class="id-group-header">
263
+ <span>{{ iconRelLabels[rel]?.label || rel }}</span>
264
+ <span class="id-group-count">{{ icons.length }}</span>
265
+ </div>
266
+ <p class="text-xs text-[var(--color-text-muted)] mb-3">
267
+ {{ iconRelLabels[rel]?.description || '' }}
268
+ </p>
269
+ <div class="id-icon-grid">
270
+ <div v-for="icon in icons" :key="icon.href" class="id-icon-card">
271
+ <div class="id-icon-preview">
272
+ <img
273
+ :src="icon.href"
274
+ :alt="icon.rel"
275
+ class="id-icon-img"
276
+ >
277
+ </div>
278
+ <div class="id-icon-meta">
279
+ <a :href="icon.href" target="_blank" class="id-icon-href">
280
+ {{ icon.href }}
281
+ </a>
282
+ <div class="id-icon-badges">
283
+ <span v-if="icon.sizes" class="id-badge">{{ icon.sizes }}</span>
284
+ <span v-if="icon.type" class="id-badge">{{ icon.type }}</span>
285
+ <span v-if="icon.media" class="id-badge id-badge--accent">
286
+ {{ icon.media.includes('dark') ? 'Dark' : icon.media.includes('light') ? 'Light' : icon.media }}
287
+ </span>
288
+ </div>
289
+ </div>
290
+ </div>
291
+ </div>
292
+ </div>
293
+ </template>
294
+
295
+ <!-- No icons: generation CTA -->
296
+ <div v-else class="id-empty panel-grids">
297
+ <div class="id-empty__inner">
298
+ <div class="id-empty__icon-ring">
299
+ <UIcon name="carbon:image" class="text-xl" />
300
+ </div>
301
+ <p class="font-medium text-sm mb-0.5">
302
+ No favicons detected
303
+ </p>
304
+ <p class="text-xs text-[var(--color-text-muted)] mb-5 max-w-xs text-center">
305
+ Generate all required icon variants from a single source image.
306
+ </p>
307
+
308
+ <!-- Terminal card -->
309
+ <div class="id-terminal">
310
+ <div class="id-terminal__chrome">
311
+ <span class="id-terminal__dot id-terminal__dot--red" />
312
+ <span class="id-terminal__dot id-terminal__dot--yellow" />
313
+ <span class="id-terminal__dot id-terminal__dot--green" />
314
+ <span class="id-terminal__label">Terminal</span>
315
+ </div>
316
+ <div class="id-terminal__body">
317
+ <span class="id-terminal__prompt">$</span>
318
+ <code>{{ cliCommand }}</code>
319
+ <UButton
320
+ size="xs"
321
+ variant="ghost"
322
+ color="neutral"
323
+ icon="carbon:copy"
324
+ class="id-terminal__copy"
325
+ @click="copy(cliCommand)"
326
+ />
327
+ </div>
328
+ </div>
329
+
330
+ <div class="id-gen-files">
331
+ <div v-for="icon in iconSizesGenerated" :key="icon.name" class="id-gen-row">
332
+ <UIcon name="carbon:arrow-right" class="id-gen-arrow" />
333
+ <span class="id-gen-name">{{ icon.name }}</span>
334
+ <span class="id-gen-size">{{ icon.size }}</span>
335
+ <span class="id-gen-desc">{{ icon.desc }}</span>
336
+ </div>
337
+ </div>
338
+
339
+ <div class="hint-callout mt-4 w-full">
340
+ <UIcon name="carbon:information" class="hint-callout-icon text-sm mt-0.5 shrink-0" />
341
+ <div class="text-xs text-[var(--color-text-muted)]">
342
+ Place a source image (SVG, PNG, or JPG) in your <code class="id-code">public/</code> directory, then run the command above. Requires <code class="id-code">sharp</code> as a dev dependency:
343
+ <pre class="id-snippet !mt-2 !mb-0">pnpm add -D sharp</pre>
344
+ </div>
345
+ </div>
346
+ </div>
347
+ </div>
348
+ </DevtoolsSection>
349
+
350
+ <!-- Theme Colors -->
351
+ <DevtoolsSection icon="carbon:color-palette" text="Theme Colors">
352
+ <template #actions>
353
+ <DevtoolsMetric :value="parsedPage.themeColors.length" label="colors" />
354
+ </template>
355
+
356
+ <template v-if="parsedPage.themeColors.length || parsedPage.colorScheme">
357
+ <!-- Color Scheme -->
358
+ <div v-if="parsedPage.colorScheme" class="mb-5">
359
+ <div class="id-group-header">
360
+ <span>Color Scheme</span>
361
+ </div>
362
+ <div class="id-color-scheme-row">
363
+ <div class="id-color-scheme-swatch" />
364
+ <div>
365
+ <span class="text-sm font-mono font-medium">{{ parsedPage.colorScheme }}</span>
366
+ <p class="text-xs text-[var(--color-text-muted)]">
367
+ Supported color schemes for system preferences
368
+ </p>
369
+ </div>
370
+ </div>
371
+ </div>
372
+
373
+ <!-- Theme Color Swatches -->
374
+ <div v-if="parsedPage.themeColors.length">
375
+ <div class="id-group-header">
376
+ <span>Theme Colors</span>
377
+ </div>
378
+ <div class="id-color-grid">
379
+ <div v-for="(tc, i) of parsedPage.themeColors" :key="i" class="id-color-card">
380
+ <div
381
+ class="id-color-card__swatch"
382
+ :style="{ backgroundColor: tc.color }"
383
+ />
384
+ <div class="id-color-card__meta">
385
+ <span class="id-color-card__value">{{ tc.color }}</span>
386
+ <span v-if="tc.context !== 'default'" class="id-badge" :class="tc.context === 'dark' ? 'id-badge--dark' : 'id-badge--light'">
387
+ {{ tc.context }}
388
+ </span>
389
+ <span v-if="tc.media" class="text-[0.625rem] text-[var(--color-text-subtle)] block mt-0.5 font-mono">{{ tc.media }}</span>
390
+ </div>
391
+ </div>
392
+ </div>
393
+ </div>
394
+ </template>
395
+
396
+ <div v-else class="hint-callout">
397
+ <UIcon name="carbon:information" class="hint-callout-icon text-sm mt-0.5 shrink-0" />
398
+ <div class="text-xs text-[var(--color-text-muted)]">
399
+ No theme colors found. Add theme color meta tags in your <code class="id-code">nuxt.config.ts</code>:
400
+ <pre class="id-snippet !mt-2">app: {
401
+ head: {
402
+ meta: [
403
+ { name: 'theme-color', content: '#00dc82' },
404
+ { name: 'theme-color', content: '#18181b',
405
+ media: '(prefers-color-scheme: dark)' }
406
+ ]
407
+ }
408
+ }</pre>
409
+ </div>
410
+ </div>
411
+ </DevtoolsSection>
412
+
413
+ <!-- Configuration Reference -->
414
+ <DevtoolsSection icon="carbon:book" text="Configuration">
415
+ <div class="id-guide-grid">
416
+ <div class="id-guide-card">
417
+ <div class="id-guide-card__icon">
418
+ <UIcon name="carbon:settings" />
419
+ </div>
420
+ <h4 class="id-guide-card__title">
421
+ Site Config
422
+ </h4>
423
+ <p class="id-guide-card__desc">
424
+ Core identity settings
425
+ </p>
426
+ <pre class="id-snippet !mt-auto">site: {
427
+ url: 'https://example.com',
428
+ name: 'My Site',
429
+ description: 'A description',
430
+ defaultLocale: 'en',
431
+ }</pre>
432
+ </div>
433
+
434
+ <div class="id-guide-card">
435
+ <div class="id-guide-card__icon">
436
+ <UIcon name="carbon:folder" />
437
+ </div>
438
+ <h4 class="id-guide-card__title">
439
+ Metadata Files
440
+ </h4>
441
+ <p class="id-guide-card__desc">
442
+ Auto-detected from <code class="id-code">public/</code>
443
+ </p>
444
+ <div class="id-file-list">
445
+ <div
446
+ v-for="f in [
447
+ { name: 'favicon.ico', desc: 'Tab icon' },
448
+ { name: 'icon-*.png', desc: 'PNG variants' },
449
+ { name: 'apple-touch-icon.png', desc: 'iOS icon' },
450
+ { name: 'og-image.png', desc: 'Social share' },
451
+ { name: 'twitter-image.png', desc: 'Twitter card' },
452
+ ]" :key="f.name" class="id-file-row"
453
+ >
454
+ <UIcon name="carbon:document" class="id-file-icon" />
455
+ <span class="id-file-name">{{ f.name }}</span>
456
+ <span class="id-file-desc">{{ f.desc }}</span>
457
+ </div>
458
+ </div>
459
+ </div>
460
+
461
+ <div class="id-guide-card">
462
+ <div class="id-guide-card__icon">
463
+ <UIcon name="carbon:terminal" />
464
+ </div>
465
+ <h4 class="id-guide-card__title">
466
+ Icon Generation
467
+ </h4>
468
+ <p class="id-guide-card__desc">
469
+ All variants from one source
470
+ </p>
471
+ <div class="id-terminal__body !mt-auto !rounded-[var(--radius-sm)]">
472
+ <span class="id-terminal__prompt">$</span>
473
+ <code class="text-xs">npx nuxt-seo-utils icons --source logo.svg</code>
474
+ <UButton
475
+ size="xs"
476
+ variant="ghost"
477
+ color="neutral"
478
+ icon="carbon:copy"
479
+ class="id-terminal__copy"
480
+ @click="copy('npx nuxt-seo-utils icons --source logo.svg')"
481
+ />
482
+ </div>
483
+ <p class="text-[0.6875rem] text-[var(--color-text-muted)] mt-2">
484
+ Requires <code class="id-code">sharp</code> dev dependency. Accepts SVG, PNG, JPG, or WebP.
485
+ </p>
486
+ </div>
487
+ </div>
488
+ </DevtoolsSection>
489
+ </template>
490
+
491
+ <!-- Not connected -->
492
+ <DevtoolsEmptyState
493
+ v-else-if="!loading && !error"
494
+ icon="carbon:identification"
495
+ title="Waiting for connection"
496
+ description="Waiting for connection to host app..."
497
+ />
498
+ </div>
499
+ </template>
500
+
501
+ <style scoped>
502
+ /* ═══════════════════════════════════════
503
+ Hero Identity Card
504
+ ═══════════════════════════════════════ */
505
+ .id-hero {
506
+ display: flex;
507
+ align-items: center;
508
+ justify-content: space-between;
509
+ gap: 1.25rem;
510
+ padding: 1rem 1.25rem;
511
+ border-radius: var(--radius-lg);
512
+ background: var(--color-surface-elevated);
513
+ border: 1px solid var(--color-border);
514
+ transition: border-color 200ms;
515
+ }
516
+
517
+ .id-hero--success { border-color: oklch(70% 0.15 145 / 0.4); }
518
+ .id-hero--warning { border-color: oklch(70% 0.15 80 / 0.4); }
519
+
520
+ .id-hero__left {
521
+ display: flex;
522
+ align-items: center;
523
+ gap: 1rem;
524
+ }
525
+
526
+ /* Score Ring (matching index.vue pattern) */
527
+ .id-score-ring {
528
+ position: relative;
529
+ width: 48px;
530
+ height: 48px;
531
+ flex-shrink: 0;
532
+ }
533
+
534
+ .id-score-ring__svg { width: 100%; height: 100%; }
535
+
536
+ .id-score-ring__progress {
537
+ transition: stroke-dasharray 600ms cubic-bezier(0.22, 1, 0.36, 1);
538
+ }
539
+
540
+ .id-score-ring__progress--success { stroke: oklch(65% 0.2 145); }
541
+ .id-score-ring__progress--warning { stroke: oklch(70% 0.18 80); }
542
+
543
+ .id-score-ring__text {
544
+ position: absolute;
545
+ inset: 0;
546
+ display: flex;
547
+ align-items: center;
548
+ justify-content: center;
549
+ font-size: 0.6875rem;
550
+ font-weight: 700;
551
+ font-family: var(--font-mono);
552
+ letter-spacing: -0.02em;
553
+ }
554
+
555
+ .id-score-ring__sep { opacity: 0.35; margin: 0 0.5px; }
556
+
557
+ /* Mini browser tab mockup */
558
+ .id-browser-tab {
559
+ flex-shrink: 0;
560
+ width: 220px;
561
+ border-radius: var(--radius-md);
562
+ overflow: hidden;
563
+ border: 1px solid var(--color-border);
564
+ background: var(--color-surface-sunken);
565
+ font-size: 0;
566
+ }
567
+
568
+ .id-browser-tab__chrome {
569
+ display: flex;
570
+ align-items: center;
571
+ gap: 0.5rem;
572
+ padding: 0.375rem 0.625rem;
573
+ background: var(--color-surface-elevated);
574
+ border-bottom: 1px solid var(--color-border-subtle);
575
+ }
576
+
577
+ .id-browser-tab__dots {
578
+ display: flex;
579
+ gap: 4px;
580
+ }
581
+
582
+ .id-browser-tab__dots span {
583
+ width: 6px;
584
+ height: 6px;
585
+ border-radius: 50%;
586
+ background: var(--color-border);
587
+ }
588
+
589
+ .id-browser-tab__tab {
590
+ display: flex;
591
+ align-items: center;
592
+ gap: 0.25rem;
593
+ padding: 0.125rem 0.375rem;
594
+ background: var(--color-surface-sunken);
595
+ border-radius: 4px 4px 0 0;
596
+ max-width: 140px;
597
+ }
598
+
599
+ .id-browser-tab__favicon {
600
+ width: 12px;
601
+ height: 12px;
602
+ flex-shrink: 0;
603
+ object-fit: contain;
604
+ }
605
+
606
+ .id-browser-tab__favicon-placeholder {
607
+ width: 12px;
608
+ height: 12px;
609
+ color: var(--color-text-subtle);
610
+ }
611
+
612
+ .id-browser-tab__title {
613
+ font-size: 0.5625rem;
614
+ font-weight: 500;
615
+ color: var(--color-text-muted);
616
+ overflow: hidden;
617
+ text-overflow: ellipsis;
618
+ white-space: nowrap;
619
+ }
620
+
621
+ .id-browser-tab__bar {
622
+ display: flex;
623
+ align-items: center;
624
+ gap: 0.25rem;
625
+ padding: 0.25rem 0.5rem;
626
+ }
627
+
628
+ .id-browser-tab__lock {
629
+ width: 10px;
630
+ height: 10px;
631
+ color: var(--color-text-subtle);
632
+ }
633
+
634
+ .id-browser-tab__url {
635
+ font-size: 0.5625rem;
636
+ font-family: var(--font-mono);
637
+ color: var(--color-text-muted);
638
+ overflow: hidden;
639
+ text-overflow: ellipsis;
640
+ white-space: nowrap;
641
+ }
642
+
643
+ @media (max-width: 560px) {
644
+ .id-hero { flex-direction: column; align-items: flex-start; }
645
+ .id-browser-tab { width: 100%; }
646
+ }
647
+
648
+ /* ═══════════════════════════════════════
649
+ Key-Value List
650
+ ═══════════════════════════════════════ */
651
+ .id-kv-list {
652
+ display: flex;
653
+ flex-direction: column;
654
+ }
655
+
656
+ .id-kv-row {
657
+ display: flex;
658
+ align-items: center;
659
+ justify-content: space-between;
660
+ gap: 1rem;
661
+ padding: 0.5rem 0.625rem;
662
+ border-radius: var(--radius-sm);
663
+ transition: background 150ms;
664
+ }
665
+
666
+ .id-kv-row:hover { background: var(--color-surface-sunken); }
667
+
668
+ .id-kv-row:nth-child(even) { background: oklch(0% 0 0 / 0.015); }
669
+ .dark .id-kv-row:nth-child(even) { background: oklch(100% 0 0 / 0.015); }
670
+ .id-kv-row:nth-child(even):hover { background: var(--color-surface-sunken); }
671
+
672
+ .id-kv-label {
673
+ font-size: 0.75rem;
674
+ color: var(--color-text-muted);
675
+ white-space: nowrap;
676
+ }
677
+
678
+ .id-kv-value {
679
+ font-size: 0.8125rem;
680
+ text-align: right;
681
+ overflow: hidden;
682
+ text-overflow: ellipsis;
683
+ white-space: nowrap;
684
+ min-width: 0;
685
+ }
686
+
687
+ .id-link {
688
+ color: var(--seo-green);
689
+ text-decoration: none;
690
+ transition: text-decoration-color 150ms;
691
+ }
692
+
693
+ .id-link:hover { text-decoration: underline; text-underline-offset: 2px; }
694
+
695
+ /* ═══════════════════════════════════════
696
+ Inline Code & Snippets
697
+ ═══════════════════════════════════════ */
698
+ .id-code {
699
+ font-family: var(--font-mono);
700
+ font-size: 0.6875rem;
701
+ padding: 0.1rem 0.35rem;
702
+ border-radius: 4px;
703
+ background: var(--color-surface-sunken);
704
+ color: var(--color-text);
705
+ border: 1px solid var(--color-border-subtle);
706
+ }
707
+
708
+ .id-snippet {
709
+ font-family: var(--font-mono);
710
+ font-size: 0.6875rem;
711
+ line-height: 1.65;
712
+ padding: 0.625rem 0.75rem;
713
+ margin-top: 0.5rem;
714
+ border-radius: var(--radius-sm);
715
+ background: var(--color-surface-sunken);
716
+ color: var(--color-text);
717
+ border: 1px solid var(--color-border-subtle);
718
+ overflow-x: auto;
719
+ white-space: pre;
720
+ }
721
+
722
+ /* ═══════════════════════════════════════
723
+ Group Headers
724
+ ═══════════════════════════════════════ */
725
+ .id-group-header {
726
+ display: flex;
727
+ align-items: center;
728
+ gap: 0.5rem;
729
+ margin-bottom: 0.625rem;
730
+ padding-bottom: 0.375rem;
731
+ border-bottom: 1px solid var(--color-border-subtle);
732
+ }
733
+
734
+ .id-group-header span {
735
+ font-size: 0.6875rem;
736
+ font-weight: 600;
737
+ text-transform: uppercase;
738
+ letter-spacing: 0.06em;
739
+ color: var(--color-text-muted);
740
+ }
741
+
742
+ .id-group-count {
743
+ font-family: var(--font-mono) !important;
744
+ font-size: 0.625rem !important;
745
+ padding: 0 0.3rem;
746
+ border-radius: 4px;
747
+ background: var(--color-surface-sunken);
748
+ color: var(--color-text-subtle) !important;
749
+ }
750
+
751
+ /* ═══════════════════════════════════════
752
+ Badges
753
+ ═══════════════════════════════════════ */
754
+ .id-badge {
755
+ display: inline-flex;
756
+ align-items: center;
757
+ padding: 0.05rem 0.4rem;
758
+ font-size: 0.625rem;
759
+ font-family: var(--font-mono);
760
+ font-weight: 500;
761
+ border-radius: 4px;
762
+ background: var(--color-surface-sunken);
763
+ color: var(--color-text-muted);
764
+ border: 1px solid var(--color-border-subtle);
765
+ }
766
+
767
+ .id-badge--accent {
768
+ background: oklch(65% 0.2 145 / 0.1);
769
+ color: oklch(55% 0.15 145);
770
+ border-color: oklch(65% 0.2 145 / 0.2);
771
+ }
772
+
773
+ .dark .id-badge--accent {
774
+ background: oklch(45% 0.12 145 / 0.15);
775
+ color: oklch(75% 0.15 145);
776
+ border-color: oklch(55% 0.12 145 / 0.2);
777
+ }
778
+
779
+ .id-badge--dark {
780
+ background: oklch(20% 0.01 260 / 0.8);
781
+ color: oklch(80% 0.01 260);
782
+ border-color: oklch(30% 0.01 260);
783
+ }
784
+
785
+ .id-badge--light {
786
+ background: oklch(95% 0.01 80 / 0.8);
787
+ color: oklch(45% 0.05 80);
788
+ border-color: oklch(85% 0.02 80);
789
+ }
790
+
791
+ /* ═══════════════════════════════════════
792
+ Icon Grid
793
+ ═══════════════════════════════════════ */
794
+ .id-icon-grid {
795
+ display: grid;
796
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
797
+ gap: 0.625rem;
798
+ }
799
+
800
+ .id-icon-card {
801
+ display: flex;
802
+ align-items: center;
803
+ gap: 0.75rem;
804
+ padding: 0.625rem 0.75rem;
805
+ border-radius: var(--radius-md);
806
+ background: var(--color-surface-elevated);
807
+ border: 1px solid var(--color-border);
808
+ transition: border-color 200ms, box-shadow 200ms;
809
+ }
810
+
811
+ .id-icon-card:hover {
812
+ border-color: var(--color-neutral-300);
813
+ box-shadow: 0 2px 12px oklch(0% 0 0 / 0.05);
814
+ }
815
+
816
+ .dark .id-icon-card:hover {
817
+ border-color: var(--color-neutral-700);
818
+ box-shadow: 0 2px 12px oklch(0% 0 0 / 0.25);
819
+ }
820
+
821
+ .id-icon-preview {
822
+ width: 48px;
823
+ height: 48px;
824
+ border-radius: var(--radius-sm);
825
+ display: flex;
826
+ align-items: center;
827
+ justify-content: center;
828
+ flex-shrink: 0;
829
+ overflow: hidden;
830
+ /* Checkerboard for transparency */
831
+ background-image:
832
+ linear-gradient(45deg, var(--color-surface-sunken) 25%, transparent 25%),
833
+ linear-gradient(-45deg, var(--color-surface-sunken) 25%, transparent 25%),
834
+ linear-gradient(45deg, transparent 75%, var(--color-surface-sunken) 75%),
835
+ linear-gradient(-45deg, transparent 75%, var(--color-surface-sunken) 75%);
836
+ background-size: 8px 8px;
837
+ background-position: 0 0, 0 4px, 4px -4px, -4px 0;
838
+ background-color: var(--color-surface-elevated);
839
+ }
840
+
841
+ .id-icon-img {
842
+ max-width: 36px;
843
+ max-height: 36px;
844
+ object-fit: contain;
845
+ image-rendering: auto;
846
+ }
847
+
848
+ .id-icon-meta {
849
+ flex: 1;
850
+ min-width: 0;
851
+ }
852
+
853
+ .id-icon-href {
854
+ display: block;
855
+ font-size: 0.75rem;
856
+ font-family: var(--font-mono);
857
+ color: var(--seo-green);
858
+ overflow: hidden;
859
+ text-overflow: ellipsis;
860
+ white-space: nowrap;
861
+ text-decoration: none;
862
+ margin-bottom: 0.25rem;
863
+ }
864
+
865
+ .id-icon-href:hover { text-decoration: underline; text-underline-offset: 2px; }
866
+
867
+ .id-icon-badges {
868
+ display: flex;
869
+ flex-wrap: wrap;
870
+ gap: 0.25rem;
871
+ }
872
+
873
+ /* ═══════════════════════════════════════
874
+ Empty State (Icons)
875
+ ═══════════════════════════════════════ */
876
+ .id-empty {
877
+ border-radius: var(--radius-lg);
878
+ overflow: hidden;
879
+ border: 1px solid var(--color-border);
880
+ }
881
+
882
+ .id-empty__inner {
883
+ display: flex;
884
+ flex-direction: column;
885
+ align-items: center;
886
+ padding: 2rem 1.5rem;
887
+ }
888
+
889
+ .id-empty__icon-ring {
890
+ width: 48px;
891
+ height: 48px;
892
+ border-radius: 50%;
893
+ display: flex;
894
+ align-items: center;
895
+ justify-content: center;
896
+ background: var(--color-surface-elevated);
897
+ border: 2px solid var(--color-border);
898
+ color: var(--color-text-subtle);
899
+ margin-bottom: 1rem;
900
+ }
901
+
902
+ /* ═══════════════════════════════════════
903
+ Terminal Card
904
+ ═══════════════════════════════════════ */
905
+ .id-terminal {
906
+ width: 100%;
907
+ border-radius: var(--radius-md);
908
+ overflow: hidden;
909
+ border: 1px solid var(--color-border);
910
+ background: var(--color-surface-elevated);
911
+ }
912
+
913
+ .id-terminal__chrome {
914
+ display: flex;
915
+ align-items: center;
916
+ gap: 6px;
917
+ padding: 0.5rem 0.75rem;
918
+ background: var(--color-surface-elevated);
919
+ border-bottom: 1px solid var(--color-border-subtle);
920
+ }
921
+
922
+ .id-terminal__dot {
923
+ width: 8px;
924
+ height: 8px;
925
+ border-radius: 50%;
926
+ }
927
+
928
+ .id-terminal__dot--red { background: oklch(65% 0.2 25); }
929
+ .id-terminal__dot--yellow { background: oklch(80% 0.15 85); }
930
+ .id-terminal__dot--green { background: oklch(70% 0.18 145); }
931
+
932
+ .id-terminal__label {
933
+ font-size: 0.625rem;
934
+ font-weight: 600;
935
+ color: var(--color-text-subtle);
936
+ margin-left: 0.25rem;
937
+ text-transform: uppercase;
938
+ letter-spacing: 0.04em;
939
+ }
940
+
941
+ .id-terminal__body {
942
+ display: flex;
943
+ align-items: center;
944
+ gap: 0.5rem;
945
+ padding: 0.75rem;
946
+ background: var(--color-surface-sunken);
947
+ font-family: var(--font-mono);
948
+ font-size: 0.75rem;
949
+ }
950
+
951
+ .id-terminal__prompt {
952
+ color: var(--seo-green);
953
+ font-weight: 700;
954
+ user-select: none;
955
+ }
956
+
957
+ .id-terminal__copy {
958
+ margin-left: auto;
959
+ flex-shrink: 0;
960
+ }
961
+
962
+ /* Generated files list */
963
+ .id-gen-files {
964
+ width: 100%;
965
+ margin-top: 1rem;
966
+ border-radius: var(--radius-sm);
967
+ overflow: hidden;
968
+ border: 1px solid var(--color-border-subtle);
969
+ }
970
+
971
+ .id-gen-row {
972
+ display: flex;
973
+ align-items: center;
974
+ gap: 0.5rem;
975
+ padding: 0.375rem 0.625rem;
976
+ font-size: 0.6875rem;
977
+ border-bottom: 1px solid var(--color-border-subtle);
978
+ }
979
+
980
+ .id-gen-row:last-child { border-bottom: none; }
981
+
982
+ .id-gen-row:nth-child(even) { background: oklch(0% 0 0 / 0.012); }
983
+ .dark .id-gen-row:nth-child(even) { background: oklch(100% 0 0 / 0.012); }
984
+
985
+ .id-gen-arrow {
986
+ width: 10px;
987
+ height: 10px;
988
+ color: var(--seo-green);
989
+ flex-shrink: 0;
990
+ }
991
+
992
+ .id-gen-name {
993
+ font-family: var(--font-mono);
994
+ font-weight: 500;
995
+ color: var(--color-text);
996
+ }
997
+
998
+ .id-gen-size {
999
+ font-family: var(--font-mono);
1000
+ color: var(--color-text-subtle);
1001
+ margin-left: auto;
1002
+ }
1003
+
1004
+ .id-gen-desc {
1005
+ color: var(--color-text-muted);
1006
+ width: 7rem;
1007
+ text-align: right;
1008
+ }
1009
+
1010
+ /* ═══════════════════════════════════════
1011
+ Social Images
1012
+ ═══════════════════════════════════════ */
1013
+ /* ═══════════════════════════════════════
1014
+ Theme Colors
1015
+ ═══════════════════════════════════════ */
1016
+ .id-color-scheme-row {
1017
+ display: flex;
1018
+ align-items: center;
1019
+ gap: 0.75rem;
1020
+ margin-top: 0.5rem;
1021
+ }
1022
+
1023
+ .id-color-scheme-swatch {
1024
+ width: 2.5rem;
1025
+ height: 2.5rem;
1026
+ border-radius: var(--radius-sm);
1027
+ border: 1px solid var(--color-border);
1028
+ background: linear-gradient(135deg, white 50%, #18181b 50%);
1029
+ flex-shrink: 0;
1030
+ }
1031
+
1032
+ .id-color-grid {
1033
+ display: flex;
1034
+ flex-wrap: wrap;
1035
+ gap: 0.625rem;
1036
+ margin-top: 0.625rem;
1037
+ }
1038
+
1039
+ .id-color-card {
1040
+ display: flex;
1041
+ align-items: center;
1042
+ gap: 0.75rem;
1043
+ padding: 0.625rem 0.75rem;
1044
+ border-radius: var(--radius-md);
1045
+ background: var(--color-surface-elevated);
1046
+ border: 1px solid var(--color-border);
1047
+ min-width: 200px;
1048
+ flex: 1;
1049
+ transition: border-color 200ms;
1050
+ }
1051
+
1052
+ .id-color-card:hover { border-color: var(--color-neutral-300); }
1053
+ .dark .id-color-card:hover { border-color: var(--color-neutral-700); }
1054
+
1055
+ .id-color-card__swatch {
1056
+ width: 2.5rem;
1057
+ height: 2.5rem;
1058
+ border-radius: var(--radius-sm);
1059
+ border: 1px solid oklch(0% 0 0 / 0.1);
1060
+ flex-shrink: 0;
1061
+ box-shadow: 0 1px 4px oklch(0% 0 0 / 0.08), inset 0 1px 0 oklch(100% 0 0 / 0.15);
1062
+ }
1063
+
1064
+ .id-color-card__meta {
1065
+ min-width: 0;
1066
+ }
1067
+
1068
+ .id-color-card__value {
1069
+ font-size: 0.8125rem;
1070
+ font-family: var(--font-mono);
1071
+ font-weight: 500;
1072
+ display: block;
1073
+ }
1074
+
1075
+ /* ═══════════════════════════════════════
1076
+ Configuration Guide
1077
+ ═══════════════════════════════════════ */
1078
+ .id-guide-grid {
1079
+ display: grid;
1080
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
1081
+ gap: 0.75rem;
1082
+ }
1083
+
1084
+ .id-guide-card {
1085
+ display: flex;
1086
+ flex-direction: column;
1087
+ padding: 1rem;
1088
+ border-radius: var(--radius-md);
1089
+ background: var(--color-surface-elevated);
1090
+ border: 1px solid var(--color-border);
1091
+ transition: border-color 200ms, box-shadow 200ms;
1092
+ }
1093
+
1094
+ .id-guide-card:hover {
1095
+ border-color: var(--color-neutral-300);
1096
+ box-shadow: 0 2px 12px oklch(0% 0 0 / 0.04);
1097
+ }
1098
+
1099
+ .dark .id-guide-card:hover {
1100
+ border-color: var(--color-neutral-700);
1101
+ box-shadow: 0 2px 12px oklch(0% 0 0 / 0.2);
1102
+ }
1103
+
1104
+ .id-guide-card__icon {
1105
+ width: 28px;
1106
+ height: 28px;
1107
+ border-radius: var(--radius-sm);
1108
+ display: flex;
1109
+ align-items: center;
1110
+ justify-content: center;
1111
+ background: oklch(65% 0.2 145 / 0.1);
1112
+ color: var(--seo-green);
1113
+ margin-bottom: 0.625rem;
1114
+ font-size: 0.875rem;
1115
+ }
1116
+
1117
+ .id-guide-card__title {
1118
+ font-size: 0.8125rem;
1119
+ font-weight: 600;
1120
+ margin-bottom: 0.125rem;
1121
+ }
1122
+
1123
+ .id-guide-card__desc {
1124
+ font-size: 0.6875rem;
1125
+ color: var(--color-text-muted);
1126
+ margin-bottom: 0.75rem;
1127
+ }
1128
+
1129
+ .id-file-list {
1130
+ display: flex;
1131
+ flex-direction: column;
1132
+ margin-top: auto;
1133
+ border-radius: var(--radius-sm);
1134
+ overflow: hidden;
1135
+ border: 1px solid var(--color-border-subtle);
1136
+ }
1137
+
1138
+ .id-file-row {
1139
+ display: flex;
1140
+ align-items: center;
1141
+ gap: 0.375rem;
1142
+ padding: 0.3rem 0.5rem;
1143
+ font-size: 0.625rem;
1144
+ border-bottom: 1px solid var(--color-border-subtle);
1145
+ }
1146
+
1147
+ .id-file-row:last-child { border-bottom: none; }
1148
+ .id-file-row:nth-child(even) { background: oklch(0% 0 0 / 0.012); }
1149
+ .dark .id-file-row:nth-child(even) { background: oklch(100% 0 0 / 0.012); }
1150
+
1151
+ .id-file-icon {
1152
+ width: 10px;
1153
+ height: 10px;
1154
+ color: var(--color-text-subtle);
1155
+ flex-shrink: 0;
1156
+ }
1157
+
1158
+ .id-file-name {
1159
+ font-family: var(--font-mono);
1160
+ font-weight: 500;
1161
+ color: var(--color-text);
1162
+ }
1163
+
1164
+ .id-file-desc {
1165
+ color: var(--color-text-muted);
1166
+ margin-left: auto;
1167
+ }
1168
+ </style>