nuxtseo-layer-devtools 0.1.3 → 0.2.1

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.
@@ -4,19 +4,20 @@ const {
4
4
  variant = 'info',
5
5
  } = defineProps<{
6
6
  icon?: string
7
- variant?: 'info' | 'warning' | 'success' | 'production'
7
+ variant?: 'info' | 'warning' | 'error' | 'success' | 'production'
8
8
  }>()
9
9
 
10
10
  const defaultIcons: Record<string, string> = {
11
11
  info: 'carbon:information',
12
12
  warning: 'carbon:warning',
13
+ error: 'carbon:close-outline',
13
14
  success: 'carbon:checkmark-outline',
14
15
  production: 'carbon:cloud',
15
16
  }
16
17
  </script>
17
18
 
18
19
  <template>
19
- <div class="devtools-alert" :class="`devtools-alert-${variant}`" role="status">
20
+ <div class="devtools-alert" :class="`devtools-alert-${variant}`" :role="variant === 'error' ? 'alert' : 'status'">
20
21
  <UIcon :name="icon || defaultIcons[variant]" class="devtools-alert-icon" aria-hidden="true" />
21
22
  <div class="devtools-alert-content">
22
23
  <slot />
@@ -63,6 +64,18 @@ const defaultIcons: Record<string, string> = {
63
64
  color: oklch(80% 0.1 230);
64
65
  }
65
66
 
67
+ /* Error */
68
+ .devtools-alert-error {
69
+ background: oklch(65% 0.18 25 / 0.1);
70
+ color: oklch(52% 0.18 25);
71
+ border-bottom-color: oklch(55% 0.15 25 / 0.25);
72
+ }
73
+
74
+ .dark .devtools-alert-error {
75
+ background: oklch(40% 0.14 25 / 0.18);
76
+ color: oklch(75% 0.14 25);
77
+ }
78
+
66
79
  /* Warning */
67
80
  .devtools-alert-warning {
68
81
  background: oklch(85% 0.12 85 / 0.1);
@@ -5,12 +5,20 @@ export interface KeyValueItem {
5
5
  copyable?: boolean
6
6
  mono?: boolean
7
7
  link?: string
8
+ /** Render value using DevtoolsSnippet. Pass the language for syntax highlighting. */
9
+ code?: 'js' | 'json' | 'xml'
8
10
  }
9
11
 
10
12
  const { items, striped = false } = defineProps<{
11
13
  items: KeyValueItem[]
12
14
  striped?: boolean
13
15
  }>()
16
+
17
+ const urlRe = /^https?:\/\/\S+$/
18
+
19
+ function isAutoLink(item: KeyValueItem): boolean {
20
+ return !item.link && !item.code && typeof item.value === 'string' && urlRe.test(item.value)
21
+ }
14
22
  </script>
15
23
 
16
24
  <template>
@@ -19,10 +27,13 @@ const { items, striped = false } = defineProps<{
19
27
  v-for="item in items"
20
28
  :key="item.key"
21
29
  class="devtools-kv-row group"
22
- :class="{ 'devtools-kv-striped': striped }"
30
+ :class="{
31
+ 'devtools-kv-striped': striped,
32
+ 'devtools-kv-stacked': !!item.code,
33
+ }"
23
34
  >
24
35
  <span class="devtools-kv-key">{{ item.key }}</span>
25
- <div class="devtools-kv-value-wrap">
36
+ <div class="devtools-kv-value-wrap" :class="{ 'devtools-kv-value-wrap-full': !!item.code }">
26
37
  <a
27
38
  v-if="item.link"
28
39
  :href="item.link"
@@ -32,6 +43,21 @@ const { items, striped = false } = defineProps<{
32
43
  >
33
44
  {{ item.value }}
34
45
  </a>
46
+ <DevtoolsSnippet
47
+ v-else-if="item.code && item.value !== undefined && item.value !== ''"
48
+ :code="String(item.value)"
49
+ :lang="item.code"
50
+ class="devtools-kv-snippet"
51
+ />
52
+ <a
53
+ v-else-if="isAutoLink(item)"
54
+ :href="String(item.value)"
55
+ target="_blank"
56
+ rel="noopener"
57
+ class="link-external text-sm"
58
+ >
59
+ {{ item.value }}
60
+ </a>
35
61
  <span
36
62
  v-else
37
63
  class="devtools-kv-value"
@@ -45,7 +71,7 @@ const { items, striped = false } = defineProps<{
45
71
  {{ item.value === undefined || item.value === '' ? '(empty)' : item.value }}
46
72
  </span>
47
73
  <DevtoolsCopyButton
48
- v-if="item.copyable && item.value !== undefined && item.value !== ''"
74
+ v-if="!item.code && item.copyable && item.value !== undefined && item.value !== ''"
49
75
  :text="String(item.value)"
50
76
  class="opacity-0 group-hover:opacity-100 transition-opacity"
51
77
  />
@@ -64,6 +90,12 @@ const { items, striped = false } = defineProps<{
64
90
  transition: background-color 150ms ease;
65
91
  }
66
92
 
93
+ .devtools-kv-row.devtools-kv-stacked {
94
+ flex-direction: column;
95
+ align-items: flex-start;
96
+ gap: 0.375rem;
97
+ }
98
+
67
99
  .devtools-kv-row:hover {
68
100
  background: var(--color-surface-sunken);
69
101
  }
@@ -86,12 +118,20 @@ const { items, striped = false } = defineProps<{
86
118
  min-width: 0;
87
119
  }
88
120
 
121
+ .devtools-kv-value-wrap-full {
122
+ width: 100%;
123
+ }
124
+
89
125
  .devtools-kv-value {
90
126
  font-size: 0.8125rem;
91
127
  text-align: right;
92
- overflow: hidden;
93
- text-overflow: ellipsis;
94
- white-space: nowrap;
128
+ overflow-wrap: break-word;
129
+ word-break: break-all;
130
+ }
131
+
132
+ .devtools-kv-snippet {
133
+ width: 100%;
134
+ margin: 0 !important;
95
135
  }
96
136
 
97
137
  .devtools-kv-true {
@@ -98,14 +98,29 @@ const isRouteNav = computed(() => navItems.some(item => item.to))
98
98
  v-if="hasProductionUrl"
99
99
  :items="[
100
100
  { label: 'Local', icon: 'carbon:laptop', onSelect: () => previewSource = 'local' },
101
- { label: `Production (${productionHostname})`, icon: 'carbon:cloud', onSelect: () => previewSource = 'production' },
101
+ { label: 'Production', icon: 'carbon:cloud', hostname: productionHostname, onSelect: () => previewSource = 'production' },
102
102
  ]"
103
+ :content="{ side: 'bottom', align: 'start', sideOffset: 8 }"
104
+ :ui="{ content: 'z-[200]' }"
103
105
  >
104
106
  <button type="button" class="devtools-mode-btn">
105
107
  <UIcon :name="isProductionMode ? 'carbon:cloud' : 'carbon:laptop'" class="w-3.5 h-3.5" />
106
108
  <span class="hidden sm:inline">{{ isProductionMode ? 'Production' : 'Local' }}</span>
109
+ <template v-if="isProductionMode">
110
+ <span class="devtools-production-badge">
111
+ <span class="devtools-production-dot" />
112
+ {{ productionHostname }}
113
+ </span>
114
+ </template>
107
115
  <UIcon name="carbon:chevron-down" class="w-3 h-3 opacity-50" />
108
116
  </button>
117
+ <template #item-label="{ item }">
118
+ <span>{{ item.label }}</span>
119
+ <span v-if="(item as any).hostname" class="devtools-production-badge text-[10px]">
120
+ <span class="devtools-production-dot" />
121
+ {{ (item as any).hostname }}
122
+ </span>
123
+ </template>
109
124
  </UDropdownMenu>
110
125
  </div>
111
126
  </div>
@@ -47,5 +47,7 @@ const {
47
47
  font-size: 0.6875rem;
48
48
  line-height: 1.6;
49
49
  padding: 0.5rem 0.625rem !important;
50
+ max-height: 300px;
51
+ overflow-y: auto;
50
52
  }
51
53
  </style>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxtseo-layer-devtools",
3
3
  "type": "module",
4
- "version": "0.1.3",
4
+ "version": "0.2.1",
5
5
  "description": "Shared Nuxt layer for Nuxt SEO devtools clients.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -15,13 +15,14 @@
15
15
  "assets",
16
16
  "components",
17
17
  "composables",
18
- "nuxt.config.ts"
18
+ "nuxt.config.ts",
19
+ "skills"
19
20
  ],
20
21
  "dependencies": {
21
22
  "@nuxt/devtools-kit": "^3.2.4",
22
23
  "@nuxt/fonts": "^0.14.0",
23
24
  "@nuxt/kit": "^4.4.2",
24
- "@nuxt/ui": "^4.5.1",
25
+ "@nuxt/ui": "^4.6.0",
25
26
  "@shikijs/langs": "^4.0.2",
26
27
  "@shikijs/themes": "^4.0.2",
27
28
  "@vueuse/core": "^14.2.1",
@@ -0,0 +1,107 @@
1
+ ---
2
+ name: devtools-layer-skilld
3
+ description: "nuxtseo-layer-devtools shared devtools layer for Nuxt SEO modules. ALWAYS use when building, modifying, or reviewing devtools client code in any Nuxt SEO module. Consult for component API, composables, implementation patterns, or debugging devtools clients."
4
+ ---
5
+
6
+ # nuxtseo-layer-devtools
7
+
8
+ Shared Nuxt layer providing components, composables, and a design system for all Nuxt SEO module devtools clients.
9
+
10
+ **Source:** `packages/devtools-layer/` (published as `nuxtseo-layer-devtools`)
11
+
12
+ ## Architecture
13
+
14
+ 1. **nuxtseo-shared/devtools** (`packages/shared/src/devtools.ts`): `setupDevToolsUI()` registers iframe tab, handles dev proxy + production sirv
15
+ 2. **nuxtseo-layer-devtools** (`packages/devtools-layer/`): Nuxt layer with shared UI, composables, CSS
16
+ 3. **Module client** (`<module>/client/`): Extends the layer, adds module specific UI and debug routes
17
+
18
+ ## Rules
19
+
20
+ 1. ALWAYS extend `nuxtseo-layer-devtools` in client/nuxt.config.ts
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
23
+ 4. NEVER add custom CSS that duplicates what the layer provides
24
+ 5. NEVER enable SSR in the client (it runs in an iframe)
25
+ 6. ALWAYS disable the module itself in the client nuxt config (e.g. `robots: false`)
26
+ 7. ALWAYS guard devtools setup with `if (nuxt.options.dev)` in module.ts
27
+ 8. ALWAYS use `useAsyncData` with `watch: [refreshTime]` for reactive data fetching
28
+ 9. Use Carbon icons consistently (prefix: `carbon:`)
29
+ 10. Debug server routes ONLY registered in dev mode
30
+
31
+ ## Required File Structure
32
+
33
+ ```
34
+ client/
35
+ ├── nuxt.config.ts # extends nuxtseo-layer-devtools
36
+ ├── app.vue # OR pages/ directory
37
+ ├── composables/
38
+ │ └── rpc.ts # connection setup (REQUIRED)
39
+ src/
40
+ ├── devtools.ts # calls setupDevToolsUI from nuxtseo-shared/devtools
41
+ ├── module.ts # calls setupDevToolsUI in dev, registers debug routes
42
+ └── runtime/server/routes/__<module>__/
43
+ └── debug.ts # JSON debug endpoint
44
+ ```
45
+
46
+ ## Implementation Templates
47
+
48
+ For full component/composable API reference, read [reference.md](./reference.md).
49
+
50
+ ### client/nuxt.config.ts
51
+
52
+ ```ts
53
+ export default defineNuxtConfig({
54
+ extends: ['nuxtseo-layer-devtools'],
55
+ <moduleName>: false,
56
+ app: { baseURL: '/__<module-route>' },
57
+ nitro: { output: { publicDir: resolve('./dist/client') } },
58
+ })
59
+ ```
60
+
61
+ ### client/composables/rpc.ts
62
+
63
+ ```ts
64
+ useDevtoolsConnection({
65
+ onConnected() { refreshSources() },
66
+ onRouteChange() { refreshSources() },
67
+ })
68
+ ```
69
+
70
+ ### src/devtools.ts
71
+
72
+ ```ts
73
+ import { setupDevToolsUI as _setup } from 'nuxtseo-shared/devtools'
74
+
75
+ export function setupDevToolsUI(config: any, resolve: Resolver['resolve'], nuxt?: Nuxt) {
76
+ return _setup({ route: '/__<route>', name: '<name>', title: '<Title>', icon: 'carbon:<icon>' }, resolve, nuxt)
77
+ }
78
+ ```
79
+
80
+ ### src/module.ts
81
+
82
+ ```ts
83
+ if (nuxt.options.dev) {
84
+ addServerHandler({ route: '/__<module>__/debug', handler: resolve('./runtime/server/routes/__<module>__/debug') })
85
+ setupDevToolsUI(config, resolve)
86
+ }
87
+ ```
88
+
89
+ ### app.vue pattern
90
+
91
+ ```vue
92
+ <script setup lang="ts">
93
+ const activeTab = ref('overview')
94
+ const loading = ref(true)
95
+ const { data } = await useAsyncData('debug', () => $fetch('/__<module>__/debug.json'), { watch: [refreshTime] })
96
+ watch(data, () => { loading.value = false })
97
+ </script>
98
+
99
+ <template>
100
+ <DevtoolsLayout v-model:active-tab="activeTab" title="Name" icon="carbon:icon" :nav-items="navItems" github-url="..." :loading @refresh="refreshSources">
101
+ <DevtoolsLoading v-if="loading" />
102
+ <template v-else-if="activeTab === 'overview'">
103
+ <!-- content -->
104
+ </template>
105
+ </DevtoolsLayout>
106
+ </template>
107
+ ```
@@ -0,0 +1,148 @@
1
+ # Devtools Layer API Reference
2
+
3
+ ## Components (auto imported)
4
+
5
+ ### Layout & Structure
6
+
7
+ | Component | Props | Key Slots | Purpose |
8
+ |---|---|---|---|
9
+ | `DevtoolsLayout` | `title`, `icon`, `version?`, `navItems`, `githubUrl`, `loading?` | `actions`, default | Main shell with header, tabs, refresh |
10
+ | `DevtoolsPanel` | `title?` | `header`, `actions`, default | Card container with close button |
11
+ | `DevtoolsToolbar` | `variant` ('default'\|'minimal') | default | Horizontal toolbar strip |
12
+ | `DevtoolsSection` | `icon?`, `text?`, `description?`, `collapse?`, `open?`, `padding?` | `text`, `description`, `actions`, `details`, default, `footer` | Collapsible details/summary block |
13
+
14
+ ### Feedback & States
15
+
16
+ | Component | Props | Key Slots | Purpose |
17
+ |---|---|---|---|
18
+ | `DevtoolsAlert` | `icon?`, `variant` ('info'\|'warning'\|'success'\|'production') | default, `action` | Color coded status bar |
19
+ | `DevtoolsError` | `icon?`, `title?`, `error?` | default | Centered error display |
20
+ | `DevtoolsEmptyState` | `icon?`, `title`, `description?`, `variant` ('default'\|'error') | default, `description` | No results placeholder |
21
+ | `DevtoolsProductionError` | `error?` | none | Production URL unreachable |
22
+ | `DevtoolsLoading` | none | none | Centered spinner |
23
+
24
+ ### Data Display
25
+
26
+ | Component | Props | Key Slots | Purpose |
27
+ |---|---|---|---|
28
+ | `DevtoolsKeyValue` | `items: KeyValueItem[]`, `striped?` | none | Key value table with copy, links, booleans |
29
+ | `DevtoolsMetric` | `label?`, `value`, `icon?`, `variant` | none | Small inline metric badge |
30
+ | `DevtoolsCopyButton` | `text` | none | Copy to clipboard button |
31
+ | `DevtoolsSnippet` | `label?`, `code`, `lang` ('js'\|'json'\|'xml') | `header` | Code block with header and copy |
32
+ | `OCodeBlock` | `code`, `lang`, `lines?`, `transformRendered?` | none | Shiki syntax highlighted pre |
33
+ | `DevtoolsDocs` | `url` | none | Full height iframe |
34
+
35
+ ### KeyValueItem Interface
36
+
37
+ ```ts
38
+ interface KeyValueItem {
39
+ key: string
40
+ value: any
41
+ copyable?: boolean
42
+ mono?: boolean
43
+ link?: string
44
+ }
45
+ ```
46
+
47
+ ## Composables (auto imported)
48
+
49
+ ### Connection (`composables/rpc.ts`)
50
+
51
+ ```ts
52
+ const appFetch: Ref<$Fetch | undefined> // Host app's $fetch
53
+ const devtools: Ref<NuxtDevtoolsClient> // Devtools client instance
54
+ const colorMode: Ref<'dark' | 'light'> // Synced from host
55
+
56
+ function useDevtoolsConnection(options?: {
57
+ onConnected?: (client: any) => void
58
+ onRouteChange?: (route: any) => void
59
+ }): void
60
+ ```
61
+
62
+ ### State (`composables/state.ts`)
63
+
64
+ ```ts
65
+ const refreshTime: Ref<number> // Watch to trigger re-fetch
66
+ const hostname: string
67
+ const path: Ref<string>
68
+ const query: Ref<any>
69
+ const base: Ref<string>
70
+ const host: ComputedRef<string>
71
+ const refreshSources: Function // Debounced 200ms
72
+ const slowRefreshSources: Function // Debounced 1000ms
73
+
74
+ // Production preview mode
75
+ const previewSource: Ref<'local' | 'production'> // localStorage persisted
76
+ const productionUrl: Ref<string>
77
+ const hasProductionUrl: ComputedRef<boolean>
78
+ const isProductionMode: ComputedRef<boolean>
79
+ ```
80
+
81
+ ### Shiki (`composables/shiki.ts`)
82
+
83
+ ```ts
84
+ async function loadShiki(options?: { extraLangs?: LanguageRegistration[] }): Promise<HighlighterCore>
85
+ function useRenderCodeHighlight(code: MaybeRef<string>, lang: string): ComputedRef<string>
86
+ ```
87
+
88
+ ### Clipboard (`composables/clipboard.ts`)
89
+
90
+ ```ts
91
+ function useCopy(timeout?: number): { copy: (text: string) => Promise<void>, copied: Ref<boolean> }
92
+ ```
93
+
94
+ ## CSS Design System
95
+
96
+ Do NOT add custom CSS unless layer components are insufficient.
97
+
98
+ ### Semantic Variables
99
+
100
+ `--color-surface`, `--color-surface-elevated`, `--color-surface-sunken`, `--color-border`, `--color-border-subtle`, `--color-text`, `--color-text-muted`, `--color-text-subtle`, `--seo-green`, `--radius-sm/md/lg`
101
+
102
+ ### Utility Classes
103
+
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`
105
+
106
+ ### Nuxt UI Defaults (app.config.ts)
107
+
108
+ Primary: green, Neutral: neutral. Buttons: ghost/neutral/sm. Badges: subtle/neutral/xs. Tooltips: zero delay.
109
+
110
+ ## Common Patterns
111
+
112
+ ### Production Mode Data Fetching
113
+
114
+ ```ts
115
+ async function refreshSources() {
116
+ if (isProductionMode.value)
117
+ data.value = await $fetch(`/__<module>__/debug-production.json`, { query: { url: productionUrl.value } })
118
+ else
119
+ data.value = await $fetch('/__<module>__/debug.json', { query: { path: path.value } })
120
+ }
121
+ watch(isProductionMode, refreshSources)
122
+ ```
123
+
124
+ ### Custom Shiki Language
125
+
126
+ ```ts
127
+ import { loadShiki as _loadShiki } from '#imports'
128
+
129
+ const customLang: LanguageRegistration = { name: 'my-lang', scopeName: 'source.my-lang', patterns: [] }
130
+ export function loadShiki() { return _loadShiki({ extraLangs: [customLang] }) }
131
+ ```
132
+
133
+ ### Fallback Connection
134
+
135
+ ```ts
136
+ const connectionState = ref<'connecting' | 'connected' | 'fallback' | 'failed'>('connecting')
137
+ 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' })
145
+ refreshSources()
146
+ }
147
+ }, 2000)
148
+ ```