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.
- package/components/DevtoolsAlert.vue +15 -2
- package/components/DevtoolsKeyValue.vue +46 -6
- package/components/DevtoolsLayout.vue +16 -1
- package/components/DevtoolsSnippet.vue +2 -0
- package/package.json +4 -3
- package/skills/devtools-layer-skilld/SKILL.md +107 -0
- package/skills/devtools-layer-skilld/reference.md +148 -0
|
@@ -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="{
|
|
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:
|
|
93
|
-
|
|
94
|
-
|
|
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:
|
|
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>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxtseo-layer-devtools",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1
|
|
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.
|
|
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
|
+
```
|