nuxtseo-shared 0.1.5 → 0.1.6

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,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" class="w-full h-full border-none" style="min-height: calc(100vh - 100px);" />
10
+ </div>
11
+ </template>
@@ -0,0 +1,213 @@
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
+ class="flex items-center opacity-90 hover:opacity-100 transition-opacity"
76
+ >
77
+ <NuxtSeoLogo class="h-6 sm:h-7" />
78
+ </a>
79
+
80
+ <div class="devtools-divider" />
81
+
82
+ <div class="flex items-center gap-2">
83
+ <div class="devtools-brand-icon">
84
+ <UIcon :name="icon" class="text-base sm:text-lg" />
85
+ </div>
86
+ <h1 class="text-sm sm:text-base font-semibold tracking-tight text-[var(--color-text)]">
87
+ {{ title }}
88
+ </h1>
89
+ <UBadge
90
+ v-if="version"
91
+ class="font-mono text-[10px] sm:text-xs hidden sm:inline-flex"
92
+ >
93
+ {{ version }}
94
+ </UBadge>
95
+ </div>
96
+ </div>
97
+
98
+ <!-- Navigation -->
99
+ <nav class="flex items-center gap-1 sm:gap-2">
100
+ <!-- Nav Tabs -->
101
+ <div class="devtools-nav-tabs">
102
+ <template v-if="isRouteNav">
103
+ <NuxtLink
104
+ v-for="item of filteredNavItems"
105
+ :key="item.value"
106
+ :to="item.to"
107
+ class="devtools-nav-tab"
108
+ :class="[
109
+ activeTab === item.value ? 'active' : '',
110
+ loading ? 'opacity-50 pointer-events-none' : '',
111
+ ]"
112
+ >
113
+ <UTooltip :text="item.label">
114
+ <div class="devtools-nav-tab-inner">
115
+ <UIcon
116
+ :name="item.icon"
117
+ class="text-base sm:text-lg"
118
+ :class="activeTab === item.value ? 'text-[var(--seo-green)]' : ''"
119
+ />
120
+ <span class="devtools-nav-label">{{ item.label }}</span>
121
+ </div>
122
+ </UTooltip>
123
+ </NuxtLink>
124
+ </template>
125
+ <template v-else>
126
+ <button
127
+ v-for="item of filteredNavItems"
128
+ :key="item.value"
129
+ type="button"
130
+ class="devtools-nav-tab"
131
+ :class="[
132
+ activeTab === item.value ? 'active' : '',
133
+ loading ? 'opacity-50 pointer-events-none' : '',
134
+ ]"
135
+ @click="activeTab = item.value"
136
+ >
137
+ <UTooltip :text="item.label">
138
+ <div class="devtools-nav-tab-inner">
139
+ <UIcon
140
+ :name="item.icon"
141
+ class="text-base sm:text-lg"
142
+ :class="activeTab === item.value ? 'text-[var(--seo-green)]' : ''"
143
+ />
144
+ <span class="devtools-nav-label">{{ item.label }}</span>
145
+ </div>
146
+ </UTooltip>
147
+ </button>
148
+ </template>
149
+ </div>
150
+
151
+ <!-- Preview source toggle -->
152
+ <div v-if="hasProductionUrl" class="devtools-preview-toggle">
153
+ <button
154
+ class="devtools-preview-btn"
155
+ :class="{ active: previewSource === 'local' }"
156
+ @click="previewSource = 'local'"
157
+ >
158
+ <UIcon name="carbon:laptop" class="w-3.5 h-3.5" />
159
+ <span class="hidden sm:inline">Local</span>
160
+ </button>
161
+ <button
162
+ class="devtools-preview-btn"
163
+ :class="{ active: previewSource === 'production' }"
164
+ @click="previewSource = 'production'"
165
+ >
166
+ <UIcon name="carbon:cloud" class="w-3.5 h-3.5" />
167
+ <span class="hidden sm:inline">Production</span>
168
+ </button>
169
+ </div>
170
+
171
+ <!-- Production URL indicator -->
172
+ <UTooltip v-if="isProductionMode" :text="productionUrl">
173
+ <span class="devtools-production-badge">
174
+ <span class="devtools-production-dot" />
175
+ <span class="hidden sm:inline text-xs">{{ productionHostname }}</span>
176
+ </span>
177
+ </UTooltip>
178
+
179
+ <!-- Actions -->
180
+ <div class="flex items-center gap-1">
181
+ <slot name="actions" />
182
+
183
+ <UTooltip text="Refresh">
184
+ <UButton
185
+ icon="carbon:reset"
186
+ class="devtools-nav-action"
187
+ @click="emit('refresh')"
188
+ />
189
+ </UTooltip>
190
+
191
+ <UTooltip text="GitHub">
192
+ <UButton
193
+ icon="simple-icons:github"
194
+ :to="githubUrl"
195
+ target="_blank"
196
+ class="devtools-nav-action hidden sm:flex"
197
+ />
198
+ </UTooltip>
199
+ </div>
200
+ </nav>
201
+ </div>
202
+ </header>
203
+
204
+ <!-- Main Content -->
205
+ <div class="devtools-main">
206
+ <main class="mx-auto flex flex-col w-full max-w-7xl">
207
+ <DevtoolsLoading v-if="loading" />
208
+ <slot v-else />
209
+ </main>
210
+ </div>
211
+ </div>
212
+ </UApp>
213
+ </template>
@@ -0,0 +1,15 @@
1
+ import { computed } from 'vue'
2
+ import { colorMode } from './rpc'
3
+
4
+ export function useDevtoolsInit(title: string) {
5
+ const isDark = computed(() => colorMode.value === 'dark')
6
+
7
+ useHead({
8
+ title: `Nuxt ${title}`,
9
+ htmlAttrs: {
10
+ class: () => isDark.value ? 'dark' : '',
11
+ },
12
+ })
13
+
14
+ return { isDark }
15
+ }
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.6",
5
5
  "description": "Shared utilities for Nuxt SEO modules.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",