nuxtseo-layer-devtools 0.2.2 → 0.2.4

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.
@@ -216,7 +216,7 @@ h1, h2, h3, h4, h5, h6 {
216
216
  }
217
217
 
218
218
  .dark .glass {
219
- background: oklch(9% 0.005 260 / 0.85);
219
+ background: oklch(9% 0.005 260 / 0.7);
220
220
  }
221
221
 
222
222
  /* Code block styling */
@@ -565,19 +565,29 @@ html.dark {
565
565
  flex: 1;
566
566
  display: flex;
567
567
  flex-direction: column;
568
- padding: 0.75rem;
569
568
  min-height: calc(100vh - 60px);
570
569
  }
571
570
 
571
+ .devtools-main-content {
572
+ display: flex;
573
+ flex-direction: column;
574
+ width: 100%;
575
+ max-width: 80rem;
576
+ margin: 0 auto;
577
+ padding: 0.75rem 1rem;
578
+ }
579
+
572
580
  @media (min-width: 640px) {
573
- .devtools-main {
574
- padding: 1rem;
581
+ .devtools-main-content {
582
+ padding: 1rem 1.25rem;
575
583
  }
576
584
  }
577
585
 
578
586
  @media (max-height: 600px) {
579
587
  .devtools-main {
580
- padding: 0;
581
588
  min-height: 0;
582
589
  }
590
+ .devtools-main-content {
591
+ padding: 0;
592
+ }
583
593
  }
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
- import { computed } from 'vue'
2
+ import { onClickOutside } from '@vueuse/core'
3
+ import { computed, ref } from 'vue'
3
4
  import { colorMode } from '../composables/rpc'
4
5
  import { hasProductionUrl, isProductionMode, previewSource, productionUrl } from '../composables/state'
5
6
 
@@ -58,6 +59,17 @@ const productionHostname = computed(() => {
58
59
  })
59
60
 
60
61
  const isRouteNav = computed(() => navItems.some(item => item.to))
62
+
63
+ const modeDropdownOpen = ref(false)
64
+ const modeDropdownRef = ref<HTMLElement>()
65
+ onClickOutside(modeDropdownRef, () => {
66
+ modeDropdownOpen.value = false
67
+ })
68
+
69
+ function selectMode(mode: 'local' | 'production') {
70
+ previewSource.value = mode
71
+ modeDropdownOpen.value = false
72
+ }
61
73
  </script>
62
74
 
63
75
  <template>
@@ -94,15 +106,8 @@ const isRouteNav = computed(() => navItems.some(item => item.to))
94
106
  >
95
107
  v{{ version }}
96
108
  </UBadge>
97
- <UDropdownMenu
98
- v-if="hasProductionUrl"
99
- :items="[
100
- { label: 'Local', icon: 'carbon:laptop', onSelect: () => previewSource = 'local' },
101
- { label: 'Production', icon: 'carbon:cloud', hostname: productionHostname, onSelect: () => previewSource = 'production' },
102
- ]"
103
- :portal="false"
104
- >
105
- <button type="button" class="devtools-mode-btn">
109
+ <div v-if="hasProductionUrl" ref="modeDropdownRef" class="mode-dropdown-wrapper">
110
+ <button type="button" class="devtools-mode-btn" @click="modeDropdownOpen = !modeDropdownOpen">
106
111
  <UIcon :name="isProductionMode ? 'carbon:cloud' : 'carbon:laptop'" class="w-3.5 h-3.5" />
107
112
  <span class="hidden sm:inline">{{ isProductionMode ? 'Production' : 'Local' }}</span>
108
113
  <template v-if="isProductionMode">
@@ -111,16 +116,25 @@ const isRouteNav = computed(() => navItems.some(item => item.to))
111
116
  {{ productionHostname }}
112
117
  </span>
113
118
  </template>
114
- <UIcon name="carbon:chevron-down" class="w-3 h-3 opacity-50" />
119
+ <UIcon name="carbon:chevron-down" class="w-3 h-3 opacity-50 transition-transform" :class="modeDropdownOpen ? 'rotate-180' : ''" />
115
120
  </button>
116
- <template #item-label="{ item }">
117
- <span>{{ item.label }}</span>
118
- <span v-if="(item as any).hostname" class="devtools-production-badge text-[10px]">
119
- <span class="devtools-production-dot" />
120
- {{ (item as any).hostname }}
121
- </span>
122
- </template>
123
- </UDropdownMenu>
121
+ <Transition name="dropdown">
122
+ <div v-if="modeDropdownOpen" class="mode-dropdown-menu">
123
+ <button type="button" class="mode-dropdown-item" @click="selectMode('local')">
124
+ <UIcon name="carbon:laptop" class="w-4 h-4" />
125
+ <span>Local</span>
126
+ </button>
127
+ <button type="button" class="mode-dropdown-item" @click="selectMode('production')">
128
+ <UIcon name="carbon:cloud" class="w-4 h-4" />
129
+ <span>Production</span>
130
+ <span class="devtools-production-badge text-[10px]">
131
+ <span class="devtools-production-dot" />
132
+ {{ productionHostname }}
133
+ </span>
134
+ </button>
135
+ </div>
136
+ </Transition>
137
+ </div>
124
138
  </div>
125
139
  </div>
126
140
 
@@ -206,7 +220,7 @@ const isRouteNav = computed(() => navItems.some(item => item.to))
206
220
 
207
221
  <!-- Main Content -->
208
222
  <div class="devtools-main">
209
- <main class="mx-auto flex flex-col w-full max-w-7xl">
223
+ <main class="devtools-main-content">
210
224
  <DevtoolsLoading v-if="loading" />
211
225
  <div v-show="!loading">
212
226
  <slot />
@@ -216,3 +230,59 @@ const isRouteNav = computed(() => navItems.some(item => item.to))
216
230
  </div>
217
231
  </UApp>
218
232
  </template>
233
+
234
+ <style scoped>
235
+ .mode-dropdown-wrapper {
236
+ position: relative;
237
+ }
238
+
239
+ .mode-dropdown-menu {
240
+ position: absolute;
241
+ top: calc(100% + 6px);
242
+ left: 0;
243
+ min-width: 180px;
244
+ padding: 4px;
245
+ border-radius: var(--radius-md);
246
+ border: 1px solid var(--color-border);
247
+ background: var(--color-surface-elevated);
248
+ box-shadow: 0 8px 24px oklch(0% 0 0 / 0.12);
249
+ z-index: 100;
250
+ }
251
+
252
+ .dark .mode-dropdown-menu {
253
+ box-shadow: 0 8px 24px oklch(0% 0 0 / 0.4);
254
+ }
255
+
256
+ .mode-dropdown-item {
257
+ display: flex;
258
+ align-items: center;
259
+ gap: 0.5rem;
260
+ width: 100%;
261
+ padding: 0.4rem 0.625rem;
262
+ font-size: 0.75rem;
263
+ font-weight: 500;
264
+ color: var(--color-text-muted);
265
+ border-radius: var(--radius-sm);
266
+ cursor: pointer;
267
+ transition: background 100ms, color 100ms;
268
+ }
269
+
270
+ .mode-dropdown-item:hover {
271
+ background: var(--color-surface-sunken);
272
+ color: var(--color-text);
273
+ }
274
+
275
+ .dropdown-enter-active {
276
+ transition: opacity 150ms ease, transform 150ms ease;
277
+ }
278
+
279
+ .dropdown-leave-active {
280
+ transition: opacity 100ms ease, transform 100ms ease;
281
+ }
282
+
283
+ .dropdown-enter-from,
284
+ .dropdown-leave-to {
285
+ opacity: 0;
286
+ transform: translateY(-4px);
287
+ }
288
+ </style>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxtseo-layer-devtools",
3
3
  "type": "module",
4
- "version": "0.2.2",
4
+ "version": "0.2.4",
5
5
  "description": "Shared Nuxt layer for Nuxt SEO devtools clients.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -9,6 +9,12 @@
9
9
  "url": "https://harlanzw.com/"
10
10
  },
11
11
  "license": "MIT",
12
+ "exports": {
13
+ ".": "./nuxt.config.ts",
14
+ "./composables/shiki": "./composables/shiki.ts",
15
+ "./composables/rpc": "./composables/rpc.ts",
16
+ "./composables/state": "./composables/state.ts"
17
+ },
12
18
  "main": "./nuxt.config.ts",
13
19
  "files": [
14
20
  "app.config.ts",
@@ -19,7 +19,7 @@ Shared Nuxt layer providing components, composables, and a design system for all
19
19
 
20
20
  1. ALWAYS extend `nuxtseo-layer-devtools` in client/nuxt.config.ts
21
21
  2. ALWAYS create `client/composables/rpc.ts` that calls `useDevtoolsConnection()`
22
- 3. ALWAYS use layer components over custom HTML: `DevtoolsSection` not custom details, `DevtoolsKeyValue` not custom tables, `DevtoolsSnippet` not custom code blocks
22
+ 3. ALWAYS use layer components over custom HTML: `DevtoolsSection` not custom details, `DevtoolsKeyValue` not custom tables, `DevtoolsSnippet` not custom code blocks. Use `KeyValueItem.code` for inline code rendering instead of separate snippets
23
23
  4. NEVER add custom CSS that duplicates what the layer provides
24
24
  5. NEVER enable SSR in the client (it runs in an iframe)
25
25
  6. ALWAYS disable the module itself in the client nuxt config (e.g. `robots: false`)
@@ -52,7 +52,7 @@ For full component/composable API reference, read [reference.md](./reference.md)
52
52
  ```ts
53
53
  export default defineNuxtConfig({
54
54
  extends: ['nuxtseo-layer-devtools'],
55
- <moduleName>: false,
55
+ // <moduleName>: false,
56
56
  app: { baseURL: '/__<module-route>' },
57
57
  nitro: { output: { publicDir: resolve('./dist/client') } },
58
58
  })
@@ -93,7 +93,9 @@ if (nuxt.options.dev) {
93
93
  const activeTab = ref('overview')
94
94
  const loading = ref(true)
95
95
  const { data } = await useAsyncData('debug', () => $fetch('/__<module>__/debug.json'), { watch: [refreshTime] })
96
- watch(data, () => { loading.value = false })
96
+ watch(data, () => {
97
+ loading.value = false
98
+ })
97
99
  </script>
98
100
 
99
101
  <template>
@@ -15,7 +15,7 @@
15
15
 
16
16
  | Component | Props | Key Slots | Purpose |
17
17
  |---|---|---|---|
18
- | `DevtoolsAlert` | `icon?`, `variant` ('info'\|'warning'\|'success'\|'production') | default, `action` | Color coded status bar |
18
+ | `DevtoolsAlert` | `icon?`, `variant` ('info'\|'warning'\|'error'\|'success'\|'production') | default, `action` | Color coded status bar |
19
19
  | `DevtoolsError` | `icon?`, `title?`, `error?` | default | Centered error display |
20
20
  | `DevtoolsEmptyState` | `icon?`, `title`, `description?`, `variant` ('default'\|'error') | default, `description` | No results placeholder |
21
21
  | `DevtoolsProductionError` | `error?` | none | Production URL unreachable |
@@ -25,10 +25,10 @@
25
25
 
26
26
  | Component | Props | Key Slots | Purpose |
27
27
  |---|---|---|---|
28
- | `DevtoolsKeyValue` | `items: KeyValueItem[]`, `striped?` | none | Key value table with copy, links, booleans |
28
+ | `DevtoolsKeyValue` | `items: KeyValueItem[]`, `striped?` | none | Key value table with copy, links, booleans, inline code |
29
29
  | `DevtoolsMetric` | `label?`, `value`, `icon?`, `variant` | none | Small inline metric badge |
30
30
  | `DevtoolsCopyButton` | `text` | none | Copy to clipboard button |
31
- | `DevtoolsSnippet` | `label?`, `code`, `lang` ('js'\|'json'\|'xml') | `header` | Code block with header and copy |
31
+ | `DevtoolsSnippet` | `label?`, `code`, `lang` ('js'\|'json'\|'xml') | `header` | Code block with header, copy, max 300px scroll |
32
32
  | `OCodeBlock` | `code`, `lang`, `lines?`, `transformRendered?` | none | Shiki syntax highlighted pre |
33
33
  | `DevtoolsDocs` | `url` | none | Full height iframe |
34
34
 
@@ -41,6 +41,8 @@ interface KeyValueItem {
41
41
  copyable?: boolean
42
42
  mono?: boolean
43
43
  link?: string
44
+ /** Render value using DevtoolsSnippet. Pass the language for syntax highlighting. */
45
+ code?: 'js' | 'json' | 'xml'
44
46
  }
45
47
  ```
46
48
 
@@ -68,8 +70,8 @@ const path: Ref<string>
68
70
  const query: Ref<any>
69
71
  const base: Ref<string>
70
72
  const host: ComputedRef<string>
71
- const refreshSources: Function // Debounced 200ms
72
- const slowRefreshSources: Function // Debounced 1000ms
73
+ const refreshSources: () => void // Debounced 200ms
74
+ const slowRefreshSources: () => void // Debounced 1000ms
73
75
 
74
76
  // Production preview mode
75
77
  const previewSource: Ref<'local' | 'production'> // localStorage persisted
@@ -101,7 +103,7 @@ Do NOT add custom CSS unless layer components are insufficient.
101
103
 
102
104
  ### Utility Classes
103
105
 
104
- `.glass` (backdrop blur), `.gradient-bg` (green/blue radials), `.card` (elevated hover), `.code-block`, `.status-enabled/.status-disabled`, `.link-external` (with arrow), `.hint-callout`, `.panel-grids`, `.animate-fade-up/.scale-in/.spin`, `.stagger-children`
106
+ `.glass` (backdrop blur), `.gradient-bg` (green/blue radials), `.card` (elevated hover), `.code-block`, `.status-enabled/.status-disabled`, `.link-external` (with arrow), `.hint-callout`, `.panel-grids`, `.animate-fade-up/.scale-in/.spin`, `.stagger-children`, `.devtools-main-content` (max-width 80rem centered container)
105
107
 
106
108
  ### Nuxt UI Defaults (app.config.ts)
107
109
 
@@ -127,7 +129,9 @@ watch(isProductionMode, refreshSources)
127
129
  import { loadShiki as _loadShiki } from '#imports'
128
130
 
129
131
  const customLang: LanguageRegistration = { name: 'my-lang', scopeName: 'source.my-lang', patterns: [] }
130
- export function loadShiki() { return _loadShiki({ extraLangs: [customLang] }) }
132
+ export function loadShiki() {
133
+ return _loadShiki({ extraLangs: [customLang] })
134
+ }
131
135
  ```
132
136
 
133
137
  ### Fallback Connection
@@ -135,14 +139,26 @@ export function loadShiki() { return _loadShiki({ extraLangs: [customLang] }) }
135
139
  ```ts
136
140
  const connectionState = ref<'connecting' | 'connected' | 'fallback' | 'failed'>('connecting')
137
141
  useDevtoolsConnection({
138
- onConnected() { connectionState.value = 'connected'; refreshSources() },
139
- onRouteChange(route) { path.value = route.path; refreshSources() },
140
- })
141
- setTimeout(() => {
142
- if (connectionState.value === 'connecting') {
143
- connectionState.value = 'fallback'
144
- appFetch.value = $fetch.create({ baseURL: 'http://localhost:3000' })
142
+ onConnected() {
143
+ connectionState.value = 'connected'
144
+ refreshSources()
145
+ },
146
+ onRouteChange(route) {
147
+ path.value = route.path
145
148
  refreshSources()
146
- }
147
- }, 2000)
149
+ },
150
+ })
151
+ onMounted(() => {
152
+ const timer = setTimeout(() => {
153
+ if (connectionState.value === 'connecting') {
154
+ connectionState.value = 'fallback'
155
+ appFetch.value = $fetch.create({ baseURL: 'http://localhost:3000' })
156
+ refreshSources()
157
+ }
158
+ }, 2000)
159
+
160
+ onUnmounted(() => {
161
+ clearTimeout(timer)
162
+ })
163
+ })
148
164
  ```