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.
package/assets/css/global.css
CHANGED
|
@@ -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.
|
|
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 {
|
|
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
|
-
<
|
|
98
|
-
|
|
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
|
-
<
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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="
|
|
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.
|
|
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, () => {
|
|
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
|
|
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:
|
|
72
|
-
const slowRefreshSources:
|
|
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() {
|
|
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() {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
}
|
|
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
|
```
|