nuxtseo-shared 0.1.5 → 0.1.7

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.
@@ -375,3 +375,219 @@ h1, h2, h3, h4, h5, h6 {
375
375
  .dark ::selection {
376
376
  background: oklch(65% 0.2 145 / 0.35);
377
377
  }
378
+
379
+ /* ---- DevtoolsLayout ---- */
380
+
381
+ /* Base HTML for devtools */
382
+ html {
383
+ font-family: var(--font-sans);
384
+ overflow-y: scroll;
385
+ overscroll-behavior: none;
386
+ }
387
+
388
+ body {
389
+ min-height: 100vh;
390
+ }
391
+
392
+ html.dark {
393
+ color-scheme: dark;
394
+ }
395
+
396
+ /* Header */
397
+ .devtools-header {
398
+ border-bottom: 1px solid var(--color-border);
399
+ }
400
+
401
+ .devtools-header-content {
402
+ display: flex;
403
+ justify-content: space-between;
404
+ align-items: center;
405
+ padding: 0.625rem 1rem;
406
+ max-width: 80rem;
407
+ margin: 0 auto;
408
+ width: 100%;
409
+ }
410
+
411
+ @media (min-width: 640px) {
412
+ .devtools-header-content {
413
+ padding: 0.75rem 1.25rem;
414
+ }
415
+ }
416
+
417
+ .devtools-divider {
418
+ width: 1px;
419
+ height: 1.25rem;
420
+ background: var(--color-border);
421
+ }
422
+
423
+ .devtools-brand-icon {
424
+ display: flex;
425
+ align-items: center;
426
+ justify-content: center;
427
+ width: 1.75rem;
428
+ height: 1.75rem;
429
+ border-radius: var(--radius-sm);
430
+ background: oklch(65% 0.2 145 / 0.12);
431
+ color: var(--seo-green);
432
+ }
433
+
434
+ /* Navigation tabs */
435
+ .devtools-nav-tabs {
436
+ display: flex;
437
+ align-items: center;
438
+ gap: 0.125rem;
439
+ padding: 0.25rem;
440
+ border-radius: var(--radius-md);
441
+ background: var(--color-surface-sunken);
442
+ border: 1px solid var(--color-border-subtle);
443
+ }
444
+
445
+ .devtools-nav-tab {
446
+ position: relative;
447
+ border-radius: var(--radius-sm);
448
+ transition: background 150ms cubic-bezier(0.22, 1, 0.36, 1), box-shadow 150ms cubic-bezier(0.22, 1, 0.36, 1);
449
+ }
450
+
451
+ .devtools-nav-tab-inner {
452
+ display: flex;
453
+ align-items: center;
454
+ gap: 0.375rem;
455
+ padding: 0.375rem 0.5rem;
456
+ color: var(--color-text-muted);
457
+ font-size: 0.8125rem;
458
+ font-weight: 500;
459
+ }
460
+
461
+ @media (min-width: 640px) {
462
+ .devtools-nav-tab-inner {
463
+ padding: 0.375rem 0.75rem;
464
+ }
465
+ }
466
+
467
+ .devtools-nav-tab:hover .devtools-nav-tab-inner {
468
+ color: var(--color-text);
469
+ }
470
+
471
+ .devtools-nav-tab.active {
472
+ background: var(--color-surface-elevated);
473
+ box-shadow: 0 1px 3px oklch(0% 0 0 / 0.08);
474
+ }
475
+
476
+ .dark .devtools-nav-tab.active {
477
+ box-shadow: 0 1px 3px oklch(0% 0 0 / 0.3);
478
+ }
479
+
480
+ .devtools-nav-tab.active .devtools-nav-tab-inner {
481
+ color: var(--color-text);
482
+ }
483
+
484
+ .devtools-nav-label {
485
+ display: none;
486
+ }
487
+
488
+ @media (min-width: 640px) {
489
+ .devtools-nav-label {
490
+ display: inline;
491
+ }
492
+ }
493
+
494
+ .devtools-nav-action {
495
+ color: var(--color-text-muted) !important;
496
+ }
497
+
498
+ .devtools-nav-action:hover {
499
+ color: var(--color-text) !important;
500
+ background: var(--color-surface-sunken) !important;
501
+ }
502
+
503
+ /* Preview source toggle */
504
+ .devtools-preview-toggle {
505
+ display: flex;
506
+ gap: 1px;
507
+ background: var(--color-border);
508
+ border-radius: 6px;
509
+ overflow: hidden;
510
+ }
511
+
512
+ .devtools-preview-btn {
513
+ display: flex;
514
+ align-items: center;
515
+ gap: 0.25rem;
516
+ padding: 0.25rem 0.5rem;
517
+ font-size: 0.6875rem;
518
+ font-weight: 500;
519
+ color: var(--color-text-muted);
520
+ background: var(--color-surface-sunken);
521
+ border: none;
522
+ cursor: pointer;
523
+ transition: color 150ms, background 150ms;
524
+ white-space: nowrap;
525
+ }
526
+
527
+ .devtools-preview-btn:hover {
528
+ color: var(--color-text);
529
+ background: var(--color-surface-elevated);
530
+ }
531
+
532
+ .devtools-preview-btn.active {
533
+ color: var(--color-text);
534
+ background: var(--color-surface-elevated);
535
+ box-shadow: 0 1px 2px oklch(0% 0 0 / 0.06);
536
+ }
537
+
538
+ .dark .devtools-preview-btn.active {
539
+ box-shadow: 0 1px 2px oklch(0% 0 0 / 0.2);
540
+ }
541
+
542
+ /* Production URL badge */
543
+ .devtools-production-badge {
544
+ display: inline-flex;
545
+ align-items: center;
546
+ gap: 0.375rem;
547
+ padding: 0.125rem 0.5rem;
548
+ border-radius: 9999px;
549
+ background: oklch(85% 0.12 145 / 0.12);
550
+ color: oklch(45% 0.15 145);
551
+ font-weight: 500;
552
+ font-family: var(--font-mono, ui-monospace, monospace);
553
+ }
554
+
555
+ .dark .devtools-production-badge {
556
+ background: oklch(35% 0.08 145 / 0.2);
557
+ color: oklch(75% 0.12 145);
558
+ }
559
+
560
+ .devtools-production-dot {
561
+ width: 6px;
562
+ height: 6px;
563
+ border-radius: 50%;
564
+ background: oklch(65% 0.2 145);
565
+ animation: pulse-dot 2s ease-in-out infinite;
566
+ }
567
+
568
+ @keyframes pulse-dot {
569
+ 0%, 100% { opacity: 1; }
570
+ 50% { opacity: 0.4; }
571
+ }
572
+
573
+ /* Main content wrapper */
574
+ .devtools-main {
575
+ flex: 1;
576
+ display: flex;
577
+ flex-direction: column;
578
+ padding: 0.75rem;
579
+ min-height: calc(100vh - 60px);
580
+ }
581
+
582
+ @media (min-width: 640px) {
583
+ .devtools-main {
584
+ padding: 1rem;
585
+ }
586
+ }
587
+
588
+ @media (max-height: 600px) {
589
+ .devtools-main {
590
+ padding: 0;
591
+ min-height: 0;
592
+ }
593
+ }
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ import { useCopy } from '../composables/clipboard'
3
+
4
+ const { text } = defineProps<{
5
+ text: string
6
+ }>()
7
+
8
+ const { copy, copied } = useCopy()
9
+ </script>
10
+
11
+ <template>
12
+ <UTooltip :text="copied ? 'Copied!' : 'Copy'">
13
+ <UButton
14
+ :icon="copied ? 'carbon:checkmark' : 'carbon:copy'"
15
+ :aria-label="copied ? 'Copied' : 'Copy to clipboard'"
16
+ :class="copied ? 'text-[var(--seo-green)]' : ''"
17
+ @click="copy(text)"
18
+ />
19
+ </UTooltip>
20
+ </template>
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ const { url } = defineProps<{
3
+ url: string
4
+ }>()
5
+ </script>
6
+
7
+ <template>
8
+ <div class="h-full max-h-full overflow-hidden">
9
+ <iframe :src="url" :title="`Documentation - ${url}`" class="w-full h-full border-none" style="min-height: calc(100vh - 100px);" />
10
+ </div>
11
+ </template>
@@ -0,0 +1,109 @@
1
+ <script setup lang="ts">
2
+ export interface KeyValueItem {
3
+ key: string
4
+ value: string | number | boolean | undefined
5
+ copyable?: boolean
6
+ mono?: boolean
7
+ link?: string
8
+ }
9
+
10
+ const { items, striped = false } = defineProps<{
11
+ items: KeyValueItem[]
12
+ striped?: boolean
13
+ }>()
14
+ </script>
15
+
16
+ <template>
17
+ <div class="divide-y divide-[var(--color-border-subtle)]">
18
+ <div
19
+ v-for="item in items"
20
+ :key="item.key"
21
+ class="devtools-kv-row group"
22
+ :class="{ 'devtools-kv-striped': striped }"
23
+ >
24
+ <span class="devtools-kv-key">{{ item.key }}</span>
25
+ <div class="devtools-kv-value-wrap">
26
+ <a
27
+ v-if="item.link"
28
+ :href="item.link"
29
+ target="_blank"
30
+ rel="noopener"
31
+ class="link-external text-sm"
32
+ >
33
+ {{ item.value }}
34
+ </a>
35
+ <span
36
+ v-else
37
+ class="devtools-kv-value"
38
+ :class="{
39
+ 'font-mono': item.mono !== false,
40
+ 'devtools-kv-true': item.value === true,
41
+ 'devtools-kv-false': item.value === false,
42
+ 'devtools-kv-empty': item.value === undefined || item.value === '',
43
+ }"
44
+ >
45
+ {{ item.value === undefined || item.value === '' ? '(empty)' : item.value }}
46
+ </span>
47
+ <DevtoolsCopyButton
48
+ v-if="item.copyable && item.value !== undefined && item.value !== ''"
49
+ :text="String(item.value)"
50
+ class="opacity-0 group-hover:opacity-100 transition-opacity"
51
+ />
52
+ </div>
53
+ </div>
54
+ </div>
55
+ </template>
56
+
57
+ <style scoped>
58
+ .devtools-kv-row {
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: space-between;
62
+ gap: 1rem;
63
+ padding: 0.625rem 1.25rem;
64
+ transition: background-color 150ms ease;
65
+ }
66
+
67
+ .devtools-kv-row:hover {
68
+ background: var(--color-surface-sunken);
69
+ }
70
+
71
+ .devtools-kv-striped:nth-child(even) {
72
+ background: oklch(from var(--color-surface-sunken) l c h / 0.5);
73
+ }
74
+
75
+ .devtools-kv-key {
76
+ font-size: 0.8125rem;
77
+ font-family: var(--font-mono);
78
+ color: var(--color-text-muted);
79
+ flex-shrink: 0;
80
+ }
81
+
82
+ .devtools-kv-value-wrap {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 0.5rem;
86
+ min-width: 0;
87
+ }
88
+
89
+ .devtools-kv-value {
90
+ font-size: 0.8125rem;
91
+ text-align: right;
92
+ overflow: hidden;
93
+ text-overflow: ellipsis;
94
+ white-space: nowrap;
95
+ }
96
+
97
+ .devtools-kv-true {
98
+ color: var(--seo-green);
99
+ }
100
+
101
+ .devtools-kv-false {
102
+ color: oklch(65% 0.15 25);
103
+ }
104
+
105
+ .devtools-kv-empty {
106
+ color: var(--color-text-subtle);
107
+ font-style: italic;
108
+ }
109
+ </style>
@@ -0,0 +1,219 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { colorMode } from '../composables/rpc'
4
+ import { hasProductionUrl, isProductionMode, previewSource, productionUrl } from '../composables/state'
5
+
6
+ export interface DevtoolsNavItem {
7
+ value: string
8
+ to?: string
9
+ icon: string
10
+ label: string
11
+ devOnly?: boolean
12
+ }
13
+
14
+ const {
15
+ title,
16
+ icon,
17
+ version,
18
+ navItems,
19
+ githubUrl,
20
+ loading = false,
21
+ } = defineProps<{
22
+ title: string
23
+ icon: string
24
+ version?: string
25
+ navItems: DevtoolsNavItem[]
26
+ githubUrl: string
27
+ loading?: boolean
28
+ }>()
29
+
30
+ const emit = defineEmits<{
31
+ refresh: []
32
+ }>()
33
+
34
+ const activeTab = defineModel<string>('activeTab')
35
+
36
+ const isDark = computed(() => colorMode.value === 'dark')
37
+
38
+ useHead({
39
+ title: `Nuxt ${title}`,
40
+ htmlAttrs: {
41
+ class: () => isDark.value ? 'dark' : '',
42
+ },
43
+ })
44
+
45
+ const filteredNavItems = computed(() =>
46
+ isProductionMode.value
47
+ ? navItems.filter(item => !item.devOnly)
48
+ : navItems,
49
+ )
50
+
51
+ const productionHostname = computed(() => {
52
+ try {
53
+ return new URL(productionUrl.value).hostname
54
+ }
55
+ catch {
56
+ return productionUrl.value
57
+ }
58
+ })
59
+
60
+ const isRouteNav = computed(() => navItems.some(item => item.to))
61
+ </script>
62
+
63
+ <template>
64
+ <UApp>
65
+ <div class="relative bg-base flex flex-col min-h-screen">
66
+ <div class="gradient-bg" />
67
+
68
+ <header class="devtools-header glass sticky top-0 z-50">
69
+ <div class="devtools-header-content">
70
+ <!-- Logo & Brand -->
71
+ <div class="flex items-center gap-3 sm:gap-4">
72
+ <a
73
+ href="https://nuxtseo.com"
74
+ target="_blank"
75
+ rel="noopener"
76
+ aria-label="Nuxt SEO"
77
+ class="flex items-center opacity-90 hover:opacity-100 transition-opacity"
78
+ >
79
+ <NuxtSeoLogo class="h-6 sm:h-7" />
80
+ </a>
81
+
82
+ <div class="devtools-divider" />
83
+
84
+ <div class="flex items-center gap-2">
85
+ <div class="devtools-brand-icon" aria-hidden="true">
86
+ <UIcon :name="icon" class="text-base sm:text-lg" />
87
+ </div>
88
+ <h1 class="text-sm sm:text-base font-semibold tracking-tight text-[var(--color-text)]">
89
+ {{ title }}
90
+ </h1>
91
+ <UBadge
92
+ v-if="version"
93
+ class="font-mono text-[10px] sm:text-xs hidden sm:inline-flex"
94
+ >
95
+ {{ version }}
96
+ </UBadge>
97
+ </div>
98
+ </div>
99
+
100
+ <!-- Navigation -->
101
+ <nav class="flex items-center gap-1 sm:gap-2">
102
+ <!-- Nav Tabs -->
103
+ <div class="devtools-nav-tabs">
104
+ <template v-if="isRouteNav">
105
+ <NuxtLink
106
+ v-for="item of filteredNavItems"
107
+ :key="item.value"
108
+ :to="item.to"
109
+ class="devtools-nav-tab"
110
+ :class="[
111
+ activeTab === item.value ? 'active' : '',
112
+ loading ? 'opacity-50 pointer-events-none' : '',
113
+ ]"
114
+ >
115
+ <UTooltip :text="item.label">
116
+ <div class="devtools-nav-tab-inner">
117
+ <UIcon
118
+ :name="item.icon"
119
+ class="text-base sm:text-lg"
120
+ :class="activeTab === item.value ? 'text-[var(--seo-green)]' : ''"
121
+ />
122
+ <span class="devtools-nav-label">{{ item.label }}</span>
123
+ </div>
124
+ </UTooltip>
125
+ </NuxtLink>
126
+ </template>
127
+ <template v-else>
128
+ <button
129
+ v-for="item of filteredNavItems"
130
+ :key="item.value"
131
+ type="button"
132
+ class="devtools-nav-tab"
133
+ :class="[
134
+ activeTab === item.value ? 'active' : '',
135
+ loading ? 'opacity-50 pointer-events-none' : '',
136
+ ]"
137
+ @click="activeTab = item.value"
138
+ >
139
+ <UTooltip :text="item.label">
140
+ <div class="devtools-nav-tab-inner">
141
+ <UIcon
142
+ :name="item.icon"
143
+ class="text-base sm:text-lg"
144
+ :class="activeTab === item.value ? 'text-[var(--seo-green)]' : ''"
145
+ />
146
+ <span class="devtools-nav-label">{{ item.label }}</span>
147
+ </div>
148
+ </UTooltip>
149
+ </button>
150
+ </template>
151
+ </div>
152
+
153
+ <!-- Preview source toggle -->
154
+ <div v-if="hasProductionUrl" class="devtools-preview-toggle">
155
+ <button
156
+ type="button"
157
+ class="devtools-preview-btn"
158
+ :class="{ active: previewSource === 'local' }"
159
+ @click="previewSource = 'local'"
160
+ >
161
+ <UIcon name="carbon:laptop" class="w-3.5 h-3.5" aria-hidden="true" />
162
+ <span class="hidden sm:inline">Local</span>
163
+ </button>
164
+ <button
165
+ type="button"
166
+ class="devtools-preview-btn"
167
+ :class="{ active: previewSource === 'production' }"
168
+ @click="previewSource = 'production'"
169
+ >
170
+ <UIcon name="carbon:cloud" class="w-3.5 h-3.5" aria-hidden="true" />
171
+ <span class="hidden sm:inline">Production</span>
172
+ </button>
173
+ </div>
174
+
175
+ <!-- Production URL indicator -->
176
+ <UTooltip v-if="isProductionMode" :text="productionUrl">
177
+ <span class="devtools-production-badge">
178
+ <span class="devtools-production-dot" />
179
+ <span class="hidden sm:inline text-xs">{{ productionHostname }}</span>
180
+ </span>
181
+ </UTooltip>
182
+
183
+ <!-- Actions -->
184
+ <div class="flex items-center gap-1">
185
+ <slot name="actions" />
186
+
187
+ <UTooltip text="Refresh">
188
+ <UButton
189
+ icon="carbon:reset"
190
+ aria-label="Refresh"
191
+ class="devtools-nav-action"
192
+ @click="emit('refresh')"
193
+ />
194
+ </UTooltip>
195
+
196
+ <UTooltip text="GitHub">
197
+ <UButton
198
+ icon="simple-icons:github"
199
+ aria-label="GitHub"
200
+ :to="githubUrl"
201
+ target="_blank"
202
+ class="devtools-nav-action hidden sm:flex"
203
+ />
204
+ </UTooltip>
205
+ </div>
206
+ </nav>
207
+ </div>
208
+ </header>
209
+
210
+ <!-- Main Content -->
211
+ <div class="devtools-main">
212
+ <main class="mx-auto flex flex-col w-full max-w-7xl">
213
+ <DevtoolsLoading v-if="loading" />
214
+ <slot v-else />
215
+ </main>
216
+ </div>
217
+ </div>
218
+ </UApp>
219
+ </template>
@@ -0,0 +1,96 @@
1
+ <script setup lang="ts">
2
+ const {
3
+ label,
4
+ value,
5
+ icon,
6
+ variant = 'default',
7
+ } = defineProps<{
8
+ label?: string
9
+ value: string | number
10
+ icon?: string
11
+ variant?: 'default' | 'success' | 'warning' | 'danger' | 'info'
12
+ }>()
13
+ </script>
14
+
15
+ <template>
16
+ <div class="devtools-metric" :class="`devtools-metric-${variant}`">
17
+ <UIcon v-if="icon" :name="icon" class="devtools-metric-icon" aria-hidden="true" />
18
+ <span class="devtools-metric-value">{{ value }}</span>
19
+ <span v-if="label" class="devtools-metric-label">{{ label }}</span>
20
+ </div>
21
+ </template>
22
+
23
+ <style scoped>
24
+ .devtools-metric {
25
+ display: inline-flex;
26
+ align-items: center;
27
+ gap: 0.375rem;
28
+ padding: 0.25rem 0.625rem;
29
+ font-size: 0.75rem;
30
+ font-weight: 500;
31
+ border-radius: var(--radius-sm);
32
+ font-variant-numeric: tabular-nums;
33
+ }
34
+
35
+ .devtools-metric-icon {
36
+ font-size: 0.875rem;
37
+ flex-shrink: 0;
38
+ }
39
+
40
+ .devtools-metric-value {
41
+ font-family: var(--font-mono);
42
+ font-weight: 600;
43
+ }
44
+
45
+ .devtools-metric-label {
46
+ color: inherit;
47
+ opacity: 0.7;
48
+ }
49
+
50
+ /* Variants */
51
+ .devtools-metric-default {
52
+ background: var(--color-surface-sunken);
53
+ border: 1px solid var(--color-border-subtle);
54
+ color: var(--color-text);
55
+ }
56
+
57
+ .devtools-metric-success {
58
+ background: oklch(75% 0.15 145 / 0.12);
59
+ color: oklch(50% 0.15 145);
60
+ }
61
+
62
+ .dark .devtools-metric-success {
63
+ background: oklch(50% 0.15 145 / 0.15);
64
+ color: oklch(75% 0.18 145);
65
+ }
66
+
67
+ .devtools-metric-warning {
68
+ background: oklch(80% 0.12 85 / 0.12);
69
+ color: oklch(55% 0.15 85);
70
+ }
71
+
72
+ .dark .devtools-metric-warning {
73
+ background: oklch(55% 0.12 85 / 0.15);
74
+ color: oklch(75% 0.15 85);
75
+ }
76
+
77
+ .devtools-metric-danger {
78
+ background: oklch(65% 0.12 25 / 0.1);
79
+ color: oklch(55% 0.15 25);
80
+ }
81
+
82
+ .dark .devtools-metric-danger {
83
+ background: oklch(45% 0.1 25 / 0.15);
84
+ color: oklch(70% 0.12 25);
85
+ }
86
+
87
+ .devtools-metric-info {
88
+ background: oklch(85% 0.08 200 / 0.1);
89
+ color: oklch(50% 0.12 200);
90
+ }
91
+
92
+ .dark .devtools-metric-info {
93
+ background: oklch(35% 0.08 200 / 0.15);
94
+ color: oklch(70% 0.1 200);
95
+ }
96
+ </style>
@@ -0,0 +1,21 @@
1
+ import { useClipboard } from '@vueuse/core'
2
+ import { ref, watch } from 'vue'
3
+
4
+ export function useCopy(timeout = 2000) {
5
+ const { copy, copied } = useClipboard({ legacy: true })
6
+ const justCopied = ref(false)
7
+
8
+ watch(copied, (val) => {
9
+ if (val) {
10
+ justCopied.value = true
11
+ setTimeout(() => {
12
+ justCopied.value = false
13
+ }, timeout)
14
+ }
15
+ })
16
+
17
+ return {
18
+ copy,
19
+ copied: justCopied,
20
+ }
21
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxtseo-shared",
3
3
  "type": "module",
4
- "version": "0.1.5",
4
+ "version": "0.1.7",
5
5
  "description": "Shared utilities for Nuxt SEO modules.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -38,7 +38,9 @@
38
38
  "types": "./dist/pro.d.mts",
39
39
  "import": "./dist/pro.mjs"
40
40
  },
41
- "./layer-devtools": "./dist/layer-devtools"
41
+ "./package.json": "./package.json",
42
+ "./layer-devtools": "./dist/layer-devtools",
43
+ "./layer-devtools/*": "./dist/layer-devtools/*"
42
44
  },
43
45
  "main": "./dist/index.mjs",
44
46
  "types": "./dist/index.d.mts",
@@ -78,7 +80,7 @@
78
80
  "@nuxtjs/i18n": "^10.2.3",
79
81
  "@shikijs/langs": "^4.0.2",
80
82
  "@shikijs/themes": "^4.0.2",
81
- "@vueuse/core": "^14.2.1",
83
+ "@vueuse/core": "^13.9.0",
82
84
  "nuxt-site-config": "^4.0.0",
83
85
  "obuild": "^0.4.32",
84
86
  "shiki": "^4.0.2",