nuxtseo-layer-devtools 0.1.2 → 0.2.0
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
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
--color-surface: var(--color-neutral-950);
|
|
43
43
|
--color-surface-elevated: var(--color-neutral-900);
|
|
44
44
|
--color-surface-sunken: oklch(7% 0.004 260);
|
|
45
|
-
--color-border:
|
|
46
|
-
--color-border-subtle: oklch(
|
|
45
|
+
--color-border: oklch(25% 0.012 260);
|
|
46
|
+
--color-border-subtle: oklch(20% 0.01 260);
|
|
47
47
|
--color-text: var(--color-neutral-100);
|
|
48
48
|
--color-text-muted: var(--color-neutral-400);
|
|
49
49
|
--color-text-subtle: var(--color-neutral-500);
|
|
@@ -507,42 +507,26 @@ html.dark {
|
|
|
507
507
|
}
|
|
508
508
|
|
|
509
509
|
/* Preview source toggle */
|
|
510
|
-
.devtools-
|
|
511
|
-
display: flex;
|
|
512
|
-
gap: 1px;
|
|
513
|
-
background: var(--color-border);
|
|
514
|
-
border-radius: 6px;
|
|
515
|
-
overflow: hidden;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
.devtools-preview-btn {
|
|
519
|
-
display: flex;
|
|
510
|
+
.devtools-mode-btn {
|
|
511
|
+
display: inline-flex;
|
|
520
512
|
align-items: center;
|
|
521
513
|
gap: 0.25rem;
|
|
522
|
-
padding: 0.
|
|
514
|
+
padding: 0.2rem 0.5rem;
|
|
523
515
|
font-size: 0.6875rem;
|
|
524
516
|
font-weight: 500;
|
|
525
517
|
color: var(--color-text-muted);
|
|
526
518
|
background: var(--color-surface-sunken);
|
|
527
|
-
border:
|
|
519
|
+
border: 1px solid var(--color-border);
|
|
520
|
+
border-radius: 6px;
|
|
528
521
|
cursor: pointer;
|
|
529
|
-
transition: color 150ms, background 150ms;
|
|
522
|
+
transition: color 150ms, background 150ms, border-color 150ms;
|
|
530
523
|
white-space: nowrap;
|
|
531
524
|
}
|
|
532
525
|
|
|
533
|
-
.devtools-
|
|
526
|
+
.devtools-mode-btn:hover {
|
|
534
527
|
color: var(--color-text);
|
|
535
528
|
background: var(--color-surface-elevated);
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
.devtools-preview-btn.active {
|
|
539
|
-
color: var(--color-text);
|
|
540
|
-
background: var(--color-surface-elevated);
|
|
541
|
-
box-shadow: 0 1px 2px oklch(0% 0 0 / 0.06);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
.dark .devtools-preview-btn.active {
|
|
545
|
-
box-shadow: 0 1px 2px oklch(0% 0 0 / 0.2);
|
|
529
|
+
border-color: var(--color-border-hover, var(--color-border));
|
|
546
530
|
}
|
|
547
531
|
|
|
548
532
|
/* Production URL badge */
|
|
@@ -5,6 +5,8 @@ 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<{
|
|
@@ -19,10 +21,13 @@ const { items, striped = false } = defineProps<{
|
|
|
19
21
|
v-for="item in items"
|
|
20
22
|
:key="item.key"
|
|
21
23
|
class="devtools-kv-row group"
|
|
22
|
-
:class="{
|
|
24
|
+
:class="{
|
|
25
|
+
'devtools-kv-striped': striped,
|
|
26
|
+
'devtools-kv-stacked': !!item.code,
|
|
27
|
+
}"
|
|
23
28
|
>
|
|
24
29
|
<span class="devtools-kv-key">{{ item.key }}</span>
|
|
25
|
-
<div class="devtools-kv-value-wrap">
|
|
30
|
+
<div class="devtools-kv-value-wrap" :class="{ 'devtools-kv-value-wrap-full': !!item.code }">
|
|
26
31
|
<a
|
|
27
32
|
v-if="item.link"
|
|
28
33
|
:href="item.link"
|
|
@@ -32,6 +37,12 @@ const { items, striped = false } = defineProps<{
|
|
|
32
37
|
>
|
|
33
38
|
{{ item.value }}
|
|
34
39
|
</a>
|
|
40
|
+
<DevtoolsSnippet
|
|
41
|
+
v-else-if="item.code && item.value !== undefined && item.value !== ''"
|
|
42
|
+
:code="String(item.value)"
|
|
43
|
+
:lang="item.code"
|
|
44
|
+
class="devtools-kv-snippet"
|
|
45
|
+
/>
|
|
35
46
|
<span
|
|
36
47
|
v-else
|
|
37
48
|
class="devtools-kv-value"
|
|
@@ -45,7 +56,7 @@ const { items, striped = false } = defineProps<{
|
|
|
45
56
|
{{ item.value === undefined || item.value === '' ? '(empty)' : item.value }}
|
|
46
57
|
</span>
|
|
47
58
|
<DevtoolsCopyButton
|
|
48
|
-
v-if="item.copyable && item.value !== undefined && item.value !== ''"
|
|
59
|
+
v-if="!item.code && item.copyable && item.value !== undefined && item.value !== ''"
|
|
49
60
|
:text="String(item.value)"
|
|
50
61
|
class="opacity-0 group-hover:opacity-100 transition-opacity"
|
|
51
62
|
/>
|
|
@@ -64,6 +75,12 @@ const { items, striped = false } = defineProps<{
|
|
|
64
75
|
transition: background-color 150ms ease;
|
|
65
76
|
}
|
|
66
77
|
|
|
78
|
+
.devtools-kv-row.devtools-kv-stacked {
|
|
79
|
+
flex-direction: column;
|
|
80
|
+
align-items: flex-start;
|
|
81
|
+
gap: 0.375rem;
|
|
82
|
+
}
|
|
83
|
+
|
|
67
84
|
.devtools-kv-row:hover {
|
|
68
85
|
background: var(--color-surface-sunken);
|
|
69
86
|
}
|
|
@@ -86,12 +103,20 @@ const { items, striped = false } = defineProps<{
|
|
|
86
103
|
min-width: 0;
|
|
87
104
|
}
|
|
88
105
|
|
|
106
|
+
.devtools-kv-value-wrap-full {
|
|
107
|
+
width: 100%;
|
|
108
|
+
}
|
|
109
|
+
|
|
89
110
|
.devtools-kv-value {
|
|
90
111
|
font-size: 0.8125rem;
|
|
91
112
|
text-align: right;
|
|
92
|
-
overflow:
|
|
93
|
-
|
|
94
|
-
|
|
113
|
+
overflow-wrap: break-word;
|
|
114
|
+
word-break: break-all;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.devtools-kv-snippet {
|
|
118
|
+
width: 100%;
|
|
119
|
+
margin: 0 !important;
|
|
95
120
|
}
|
|
96
121
|
|
|
97
122
|
.devtools-kv-true {
|
|
@@ -92,8 +92,34 @@ const isRouteNav = computed(() => navItems.some(item => item.to))
|
|
|
92
92
|
v-if="version"
|
|
93
93
|
class="font-mono text-[10px] sm:text-xs hidden sm:inline-flex"
|
|
94
94
|
>
|
|
95
|
-
{{ version }}
|
|
95
|
+
v{{ version }}
|
|
96
96
|
</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
|
+
>
|
|
104
|
+
<button type="button" class="devtools-mode-btn">
|
|
105
|
+
<UIcon :name="isProductionMode ? 'carbon:cloud' : 'carbon:laptop'" class="w-3.5 h-3.5" />
|
|
106
|
+
<span class="hidden sm:inline">{{ isProductionMode ? 'Production' : 'Local' }}</span>
|
|
107
|
+
<template v-if="isProductionMode">
|
|
108
|
+
<span class="devtools-production-badge">
|
|
109
|
+
<span class="devtools-production-dot" />
|
|
110
|
+
{{ productionHostname }}
|
|
111
|
+
</span>
|
|
112
|
+
</template>
|
|
113
|
+
<UIcon name="carbon:chevron-down" class="w-3 h-3 opacity-50" />
|
|
114
|
+
</button>
|
|
115
|
+
<template #item-label="{ item }">
|
|
116
|
+
<span>{{ item.label }}</span>
|
|
117
|
+
<span v-if="(item as any).hostname" class="devtools-production-badge text-[10px]">
|
|
118
|
+
<span class="devtools-production-dot" />
|
|
119
|
+
{{ (item as any).hostname }}
|
|
120
|
+
</span>
|
|
121
|
+
</template>
|
|
122
|
+
</UDropdownMenu>
|
|
97
123
|
</div>
|
|
98
124
|
</div>
|
|
99
125
|
|
|
@@ -150,36 +176,6 @@ const isRouteNav = computed(() => navItems.some(item => item.to))
|
|
|
150
176
|
</template>
|
|
151
177
|
</div>
|
|
152
178
|
|
|
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
179
|
<!-- Actions -->
|
|
184
180
|
<div class="flex items-center gap-1">
|
|
185
181
|
<slot name="actions" />
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxtseo-layer-devtools",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
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.
|
|
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
|
+
```
|