nuxtseo-layer-devtools 0.4.5 → 0.5.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/DevtoolsChecklistBadge.vue +65 -0
- package/components/DevtoolsChecklistItem.vue +171 -0
- package/components/DevtoolsLayout.vue +91 -7
- package/components/DevtoolsModuleSplash.vue +270 -51
- package/components/DevtoolsPlaygrounds.vue +99 -0
- package/components/DevtoolsSetupChecklist.vue +60 -0
- package/components/DevtoolsTroubleshooting.vue +332 -0
- package/composables/checklist.ts +486 -0
- package/composables/modules.ts +48 -13
- package/composables/package-manager.ts +41 -0
- package/composables/update-check.ts +39 -0
- package/error.vue +1 -0
- package/package.json +3 -2
- package/skills/devtools-layer-skilld/SKILL.md +17 -8
- package/skills/devtools-layer-skilld/reference.md +70 -7
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import type { TabsItem } from '@nuxt/ui'
|
|
3
|
+
import { onClickOutside, useClipboard } from '@vueuse/core'
|
|
4
|
+
import { computed, ref, watch } from 'vue'
|
|
5
|
+
import { useSetupChecklist } from '../composables/checklist'
|
|
4
6
|
import { moduleCatalog, showModuleSplash, switchToModule } from '../composables/modules'
|
|
5
7
|
import { isConnected } from '../composables/state'
|
|
6
8
|
|
|
@@ -16,8 +18,69 @@ onClickOutside(panelRef, () => {
|
|
|
16
18
|
const coreModules = computed(() => moduleCatalog.value.filter(m => !m.pro))
|
|
17
19
|
const proModules = computed(() => moduleCatalog.value.filter(m => m.pro))
|
|
18
20
|
|
|
21
|
+
const selectedForInstall = ref(new Set<string>())
|
|
22
|
+
|
|
23
|
+
const installCommand = computed(() => {
|
|
24
|
+
if (!selectedForInstall.value.size)
|
|
25
|
+
return ''
|
|
26
|
+
const names = Array.from(selectedForInstall.value, name => moduleCatalog.value.find(m => m.name === name)?.npm).filter(Boolean)
|
|
27
|
+
return `npx nuxt module add ${names.join(' ')}`
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const { copy, copied } = useClipboard({ source: installCommand })
|
|
31
|
+
|
|
32
|
+
const { summary, evaluated, evaluate, getModuleResultByName } = useSetupChecklist()
|
|
33
|
+
|
|
34
|
+
// Evaluate checklist on first splash open
|
|
35
|
+
const hasEvaluated = ref(false)
|
|
36
|
+
watch(showModuleSplash, (open) => {
|
|
37
|
+
if (open && !hasEvaluated.value) {
|
|
38
|
+
hasEvaluated.value = true
|
|
39
|
+
evaluate()
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const activeTab = ref('modules')
|
|
44
|
+
|
|
45
|
+
const healthBadge = computed(() => {
|
|
46
|
+
if (!evaluated.value || summary.value.total === 0)
|
|
47
|
+
return undefined
|
|
48
|
+
if (summary.value.requiredPending > 0)
|
|
49
|
+
return { label: `${summary.value.requiredPending}`, color: 'error' as const, variant: 'subtle' as const }
|
|
50
|
+
if (summary.value.recommendedPending > 0)
|
|
51
|
+
return { label: `${summary.value.recommendedPending}`, color: 'warning' as const, variant: 'subtle' as const }
|
|
52
|
+
return { label: '', icon: 'i-carbon-checkmark', color: 'success' as const, variant: 'subtle' as const }
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const tabs = computed<TabsItem[]>(() => {
|
|
56
|
+
const items: TabsItem[] = [
|
|
57
|
+
{ label: 'Modules', value: 'modules', icon: 'i-carbon-grid' },
|
|
58
|
+
]
|
|
59
|
+
if (proModules.value.length) {
|
|
60
|
+
items.push({ label: 'Pro', value: 'pro', icon: 'i-carbon-locked' })
|
|
61
|
+
}
|
|
62
|
+
if (evaluated.value && summary.value.total > 0) {
|
|
63
|
+
items.push({
|
|
64
|
+
label: 'Setup',
|
|
65
|
+
value: 'setup',
|
|
66
|
+
icon: 'i-carbon-task-complete',
|
|
67
|
+
badge: healthBadge.value,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
return items
|
|
71
|
+
})
|
|
72
|
+
|
|
19
73
|
function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
20
|
-
if (!mod.installed
|
|
74
|
+
if (!mod.installed) {
|
|
75
|
+
const set = new Set(selectedForInstall.value)
|
|
76
|
+
if (set.has(mod.name))
|
|
77
|
+
set.delete(mod.name)
|
|
78
|
+
else
|
|
79
|
+
set.add(mod.name)
|
|
80
|
+
selectedForInstall.value = set
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
if (mod.name === props.currentModule)
|
|
21
84
|
return
|
|
22
85
|
switchToModule(mod.name)
|
|
23
86
|
}
|
|
@@ -36,12 +99,21 @@ function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
|
36
99
|
</button>
|
|
37
100
|
</div>
|
|
38
101
|
|
|
39
|
-
<!--
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
102
|
+
<!-- Tabs -->
|
|
103
|
+
<UTabs
|
|
104
|
+
v-model="activeTab"
|
|
105
|
+
variant="link"
|
|
106
|
+
size="sm"
|
|
107
|
+
:content="false"
|
|
108
|
+
:items="tabs"
|
|
109
|
+
:ui="{
|
|
110
|
+
root: 'splash-tabs',
|
|
111
|
+
trigger: 'splash-tab-trigger',
|
|
112
|
+
}"
|
|
113
|
+
/>
|
|
114
|
+
|
|
115
|
+
<!-- Modules tab -->
|
|
116
|
+
<div v-show="activeTab === 'modules'" class="splash-tab-content">
|
|
45
117
|
<div class="splash-grid">
|
|
46
118
|
<button
|
|
47
119
|
v-for="mod of coreModules"
|
|
@@ -50,28 +122,43 @@ function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
|
50
122
|
class="splash-module"
|
|
51
123
|
:class="{
|
|
52
124
|
'is-current': mod.name === currentModule,
|
|
53
|
-
'is-unavailable': !mod.installed,
|
|
125
|
+
'is-unavailable': !mod.installed && !selectedForInstall.has(mod.name) && mod.name !== currentModule,
|
|
126
|
+
'is-selected-install': selectedForInstall.has(mod.name),
|
|
54
127
|
'is-switchable': mod.installed && mod.name !== currentModule && isConnected,
|
|
55
128
|
}"
|
|
56
|
-
:disabled="
|
|
129
|
+
:disabled="mod.name === currentModule"
|
|
57
130
|
@click="handleModuleClick(mod)"
|
|
58
131
|
>
|
|
59
132
|
<div class="splash-module-icon">
|
|
60
133
|
<UIcon :name="mod.icon" class="text-base" />
|
|
61
134
|
</div>
|
|
62
135
|
<span class="splash-module-title">{{ mod.title }}</span>
|
|
63
|
-
<
|
|
64
|
-
|
|
136
|
+
<DevtoolsChecklistBadge
|
|
137
|
+
v-if="mod.installed && evaluated && getModuleResultByName(mod.name)"
|
|
138
|
+
:required-pending="getModuleResultByName(mod.name)!.requiredPending"
|
|
139
|
+
:recommended-pending="getModuleResultByName(mod.name)!.recommendedPending"
|
|
140
|
+
/>
|
|
141
|
+
<span v-else-if="mod.name === currentModule" class="splash-current-badge">Current</span>
|
|
142
|
+
<span v-else-if="!mod.installed" class="splash-not-installed">{{ selectedForInstall.has(mod.name) ? 'Selected' : 'Not installed' }}</span>
|
|
65
143
|
</button>
|
|
66
144
|
</div>
|
|
145
|
+
|
|
146
|
+
<!-- Install command -->
|
|
147
|
+
<Transition name="install-bar">
|
|
148
|
+
<div v-if="installCommand" class="splash-install">
|
|
149
|
+
<div class="splash-install-code">
|
|
150
|
+
<code>{{ installCommand }}</code>
|
|
151
|
+
</div>
|
|
152
|
+
<button type="button" class="splash-install-copy" @click="copy()">
|
|
153
|
+
<UIcon :name="copied ? 'carbon:checkmark' : 'carbon:copy'" class="w-3.5 h-3.5" />
|
|
154
|
+
{{ copied ? 'Copied' : 'Copy' }}
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
</Transition>
|
|
67
158
|
</div>
|
|
68
159
|
|
|
69
|
-
<!-- Pro
|
|
70
|
-
<div v-
|
|
71
|
-
<div class="splash-section-label">
|
|
72
|
-
<span class="splash-section-dot splash-section-dot--pro" />
|
|
73
|
-
Pro
|
|
74
|
-
</div>
|
|
160
|
+
<!-- Pro tab -->
|
|
161
|
+
<div v-show="activeTab === 'pro'" class="splash-tab-content">
|
|
75
162
|
<div class="splash-grid">
|
|
76
163
|
<button
|
|
77
164
|
v-for="mod of proModules"
|
|
@@ -95,7 +182,6 @@ function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
|
95
182
|
</UBadge>
|
|
96
183
|
</button>
|
|
97
184
|
</div>
|
|
98
|
-
<!-- Pro status / CTA -->
|
|
99
185
|
<div class="splash-pro-status">
|
|
100
186
|
<div class="splash-pro-status-inner">
|
|
101
187
|
<UIcon name="carbon:locked" class="w-3.5 h-3.5 opacity-50" />
|
|
@@ -108,6 +194,21 @@ function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
|
108
194
|
</div>
|
|
109
195
|
</div>
|
|
110
196
|
|
|
197
|
+
<!-- Setup tab -->
|
|
198
|
+
<div v-show="activeTab === 'setup'" class="splash-tab-content">
|
|
199
|
+
<div v-if="evaluated && summary.total > 0" class="splash-health-header">
|
|
200
|
+
<span class="splash-health-bar">
|
|
201
|
+
<span
|
|
202
|
+
class="splash-health-bar-fill"
|
|
203
|
+
:class="summary.requiredPending > 0 ? 'is-danger' : summary.recommendedPending > 0 ? 'is-warning' : 'is-complete'"
|
|
204
|
+
:style="{ width: `${summary.total > 0 ? (summary.passed / summary.total) * 100 : 0}%` }"
|
|
205
|
+
/>
|
|
206
|
+
</span>
|
|
207
|
+
<span class="splash-health-count">{{ summary.passed }}/{{ summary.total }} complete</span>
|
|
208
|
+
</div>
|
|
209
|
+
<DevtoolsSetupChecklist :current-module="currentModule" />
|
|
210
|
+
</div>
|
|
211
|
+
|
|
111
212
|
<!-- Footer -->
|
|
112
213
|
<div class="splash-footer">
|
|
113
214
|
<a href="https://nuxtseo.com" target="_blank" rel="noopener" class="splash-link">
|
|
@@ -135,11 +236,12 @@ function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
|
135
236
|
|
|
136
237
|
.splash-panel {
|
|
137
238
|
width: min(480px, calc(100vw - 2rem));
|
|
239
|
+
max-height: calc(100vh - 4rem);
|
|
240
|
+
overflow-y: auto;
|
|
138
241
|
border-radius: var(--radius-lg);
|
|
139
242
|
border: 1px solid var(--color-border);
|
|
140
243
|
background: var(--color-surface);
|
|
141
244
|
box-shadow: 0 24px 48px oklch(0% 0 0 / 0.2);
|
|
142
|
-
overflow: hidden;
|
|
143
245
|
}
|
|
144
246
|
|
|
145
247
|
.dark .splash-panel {
|
|
@@ -172,36 +274,19 @@ function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
|
172
274
|
color: var(--color-text);
|
|
173
275
|
}
|
|
174
276
|
|
|
175
|
-
/*
|
|
176
|
-
.splash-
|
|
177
|
-
padding: 0
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
.splash-section + .splash-section {
|
|
181
|
-
border-top: 1px solid var(--color-border);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
.splash-section-label {
|
|
185
|
-
display: flex;
|
|
186
|
-
align-items: center;
|
|
187
|
-
gap: 0.375rem;
|
|
188
|
-
font-size: 0.625rem;
|
|
189
|
-
font-weight: 600;
|
|
190
|
-
text-transform: uppercase;
|
|
191
|
-
letter-spacing: 0.05em;
|
|
192
|
-
color: var(--color-text-muted);
|
|
193
|
-
padding: 0.25rem 0.375rem 0.375rem;
|
|
277
|
+
/* Tabs */
|
|
278
|
+
.splash-tabs {
|
|
279
|
+
padding: 0 0.75rem;
|
|
280
|
+
border-bottom: 1px solid var(--color-border);
|
|
194
281
|
}
|
|
195
282
|
|
|
196
|
-
.splash-
|
|
197
|
-
|
|
198
|
-
height: 0.3125rem;
|
|
199
|
-
border-radius: 50%;
|
|
200
|
-
background: var(--color-text-subtle);
|
|
283
|
+
.splash-tab-trigger {
|
|
284
|
+
font-size: 0.6875rem !important;
|
|
201
285
|
}
|
|
202
286
|
|
|
203
|
-
|
|
204
|
-
|
|
287
|
+
/* Tab content */
|
|
288
|
+
.splash-tab-content {
|
|
289
|
+
padding: 0.5rem 0.75rem 0.625rem;
|
|
205
290
|
}
|
|
206
291
|
|
|
207
292
|
/* Grid */
|
|
@@ -241,7 +326,29 @@ function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
|
241
326
|
}
|
|
242
327
|
|
|
243
328
|
.splash-module.is-unavailable {
|
|
244
|
-
opacity: 0.
|
|
329
|
+
opacity: 0.4;
|
|
330
|
+
cursor: pointer;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.splash-module.is-unavailable:hover {
|
|
334
|
+
opacity: 0.7;
|
|
335
|
+
background: var(--color-surface-elevated);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.splash-module.is-selected-install {
|
|
339
|
+
opacity: 1;
|
|
340
|
+
background: oklch(from var(--seo-green) l c h / 0.08);
|
|
341
|
+
cursor: pointer;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.splash-module.is-selected-install .splash-module-icon {
|
|
345
|
+
background: oklch(from var(--seo-green) l c h / 0.15);
|
|
346
|
+
color: var(--seo-green);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.splash-module.is-selected-install .splash-not-installed {
|
|
350
|
+
color: var(--seo-green);
|
|
351
|
+
font-weight: 600;
|
|
245
352
|
}
|
|
246
353
|
|
|
247
354
|
.splash-module-icon {
|
|
@@ -288,6 +395,8 @@ function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
|
288
395
|
.splash-pro-badge {
|
|
289
396
|
font-size: 9px !important;
|
|
290
397
|
flex-shrink: 0;
|
|
398
|
+
color: oklch(65% 0.25 290) !important;
|
|
399
|
+
border-color: oklch(65% 0.25 290 / 0.3) !important;
|
|
291
400
|
}
|
|
292
401
|
|
|
293
402
|
/* Pro status / CTA */
|
|
@@ -295,10 +404,11 @@ function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
|
295
404
|
display: flex;
|
|
296
405
|
align-items: center;
|
|
297
406
|
justify-content: space-between;
|
|
298
|
-
margin: 0.375rem 0
|
|
407
|
+
margin: 0.375rem 0 0;
|
|
299
408
|
padding: 0.4rem 0.625rem;
|
|
300
409
|
border-radius: var(--radius-md);
|
|
301
|
-
background:
|
|
410
|
+
background: oklch(65% 0.25 290 / 0.06);
|
|
411
|
+
border: 1px solid oklch(65% 0.25 290 / 0.12);
|
|
302
412
|
font-size: 0.6875rem;
|
|
303
413
|
color: var(--color-text-muted);
|
|
304
414
|
}
|
|
@@ -315,7 +425,7 @@ function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
|
315
425
|
gap: 0.25rem;
|
|
316
426
|
font-size: 0.6875rem;
|
|
317
427
|
font-weight: 500;
|
|
318
|
-
color:
|
|
428
|
+
color: oklch(65% 0.25 290);
|
|
319
429
|
text-decoration: none;
|
|
320
430
|
transition: opacity 100ms;
|
|
321
431
|
}
|
|
@@ -324,6 +434,115 @@ function handleModuleClick(mod: typeof moduleCatalog.value[0]) {
|
|
|
324
434
|
opacity: 0.8;
|
|
325
435
|
}
|
|
326
436
|
|
|
437
|
+
/* Setup Health header */
|
|
438
|
+
.splash-health-header {
|
|
439
|
+
display: flex;
|
|
440
|
+
align-items: center;
|
|
441
|
+
gap: 0.5rem;
|
|
442
|
+
margin-bottom: 0.5rem;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.splash-health-bar {
|
|
446
|
+
flex: 1;
|
|
447
|
+
height: 0.25rem;
|
|
448
|
+
border-radius: 2px;
|
|
449
|
+
background: var(--color-surface-sunken);
|
|
450
|
+
overflow: hidden;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.splash-health-bar-fill {
|
|
454
|
+
display: block;
|
|
455
|
+
height: 100%;
|
|
456
|
+
border-radius: 2px;
|
|
457
|
+
transition: width 400ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.splash-health-bar-fill.is-complete {
|
|
461
|
+
background: oklch(55% 0.15 145);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.dark .splash-health-bar-fill.is-complete {
|
|
465
|
+
background: oklch(65% 0.18 145);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.splash-health-bar-fill.is-warning {
|
|
469
|
+
background: oklch(65% 0.18 85);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.splash-health-bar-fill.is-danger {
|
|
473
|
+
background: oklch(60% 0.2 25);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.splash-health-count {
|
|
477
|
+
font-size: 0.5625rem;
|
|
478
|
+
font-family: var(--font-mono, monospace);
|
|
479
|
+
font-weight: 600;
|
|
480
|
+
color: var(--color-text-subtle);
|
|
481
|
+
font-variant-numeric: tabular-nums;
|
|
482
|
+
white-space: nowrap;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/* Install bar */
|
|
486
|
+
.splash-install {
|
|
487
|
+
display: flex;
|
|
488
|
+
align-items: center;
|
|
489
|
+
gap: 0.5rem;
|
|
490
|
+
margin-top: 0.5rem;
|
|
491
|
+
padding: 0.5rem 0.625rem;
|
|
492
|
+
border-radius: var(--radius-md);
|
|
493
|
+
background: var(--color-surface-sunken);
|
|
494
|
+
border: 1px solid oklch(from var(--seo-green) l c h / 0.2);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.splash-install-code {
|
|
498
|
+
flex: 1;
|
|
499
|
+
min-width: 0;
|
|
500
|
+
overflow-x: auto;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.splash-install-code code {
|
|
504
|
+
font-size: 0.6875rem;
|
|
505
|
+
font-family: var(--font-mono, monospace);
|
|
506
|
+
color: var(--color-text);
|
|
507
|
+
white-space: nowrap;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.splash-install-copy {
|
|
511
|
+
display: flex;
|
|
512
|
+
align-items: center;
|
|
513
|
+
gap: 0.25rem;
|
|
514
|
+
flex-shrink: 0;
|
|
515
|
+
padding: 0.25rem 0.5rem;
|
|
516
|
+
border-radius: var(--radius-sm);
|
|
517
|
+
font-size: 0.625rem;
|
|
518
|
+
font-weight: 600;
|
|
519
|
+
color: var(--seo-green);
|
|
520
|
+
background: oklch(from var(--seo-green) l c h / 0.1);
|
|
521
|
+
cursor: pointer;
|
|
522
|
+
transition: background 100ms;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.splash-install-copy:hover {
|
|
526
|
+
background: oklch(from var(--seo-green) l c h / 0.18);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.install-bar-enter-active,
|
|
530
|
+
.install-bar-leave-active {
|
|
531
|
+
transition: opacity 150ms ease, max-height 150ms ease;
|
|
532
|
+
overflow: hidden;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.install-bar-enter-from,
|
|
536
|
+
.install-bar-leave-to {
|
|
537
|
+
opacity: 0;
|
|
538
|
+
max-height: 0;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.install-bar-enter-to,
|
|
542
|
+
.install-bar-leave-from {
|
|
543
|
+
max-height: 4rem;
|
|
544
|
+
}
|
|
545
|
+
|
|
327
546
|
/* Footer */
|
|
328
547
|
.splash-footer {
|
|
329
548
|
display: flex;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { moduleCatalog } from '../composables/modules'
|
|
4
|
+
|
|
5
|
+
const { moduleName } = defineProps<{
|
|
6
|
+
moduleName: string
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
const keyLabels: Record<string, string> = {
|
|
10
|
+
'basic': 'Basic',
|
|
11
|
+
'i18n': 'Nuxt I18n',
|
|
12
|
+
'content': 'Nuxt Content',
|
|
13
|
+
'basic-satori': 'Basic (Satori)',
|
|
14
|
+
'basic-takumi': 'Basic (Takumi)',
|
|
15
|
+
'dynamic-urls': 'Dynamic URLs',
|
|
16
|
+
'custom-rules': 'Custom Rules',
|
|
17
|
+
'broken-links': 'Broken Links',
|
|
18
|
+
'skip-inspection': 'Skip Inspection',
|
|
19
|
+
'breadcrumbs': 'Breadcrumbs',
|
|
20
|
+
'meta-tags': 'Meta Tags',
|
|
21
|
+
'blog': 'Blog',
|
|
22
|
+
'e-commerce': 'E-Commerce',
|
|
23
|
+
'env-driven': 'Env Driven',
|
|
24
|
+
'multi-site': 'Multi Site',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const CAMEL_RE = /([a-z])([A-Z])/g
|
|
28
|
+
const SEP_RE = /[-_]/g
|
|
29
|
+
const WORD_RE = /\b\w/g
|
|
30
|
+
|
|
31
|
+
function humanizeKey(key: string) {
|
|
32
|
+
if (keyLabels[key])
|
|
33
|
+
return keyLabels[key]
|
|
34
|
+
return key
|
|
35
|
+
.replace(CAMEL_RE, '$1 $2')
|
|
36
|
+
.replace(SEP_RE, ' ')
|
|
37
|
+
.replace(WORD_RE, c => c.toUpperCase())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const playgrounds = computed(() => {
|
|
41
|
+
const mod = moduleCatalog.value.find(m => m.name === moduleName)
|
|
42
|
+
if (!mod?.playgrounds)
|
|
43
|
+
return []
|
|
44
|
+
return Object.entries(mod.playgrounds).map(([key, url]) => ({
|
|
45
|
+
key,
|
|
46
|
+
label: humanizeKey(key),
|
|
47
|
+
url,
|
|
48
|
+
}))
|
|
49
|
+
})
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<div v-if="playgrounds.length" class="playgrounds-list">
|
|
54
|
+
<a
|
|
55
|
+
v-for="pg in playgrounds"
|
|
56
|
+
:key="pg.key"
|
|
57
|
+
:href="pg.url"
|
|
58
|
+
target="_blank"
|
|
59
|
+
rel="noopener"
|
|
60
|
+
class="playground-btn"
|
|
61
|
+
>
|
|
62
|
+
<UIcon name="carbon:launch" class="w-3 h-3 shrink-0 opacity-50" />
|
|
63
|
+
<span>{{ pg.label }}</span>
|
|
64
|
+
</a>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<style scoped>
|
|
69
|
+
.playgrounds-list {
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-wrap: wrap;
|
|
72
|
+
gap: 0.375rem;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.playground-btn {
|
|
76
|
+
display: inline-flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
gap: 0.375rem;
|
|
79
|
+
padding: 0.25rem 0.625rem;
|
|
80
|
+
border-radius: var(--radius-sm);
|
|
81
|
+
font-size: 0.75rem;
|
|
82
|
+
font-weight: 500;
|
|
83
|
+
color: var(--color-text-muted);
|
|
84
|
+
background: var(--color-surface-elevated);
|
|
85
|
+
border: 1px solid var(--color-border);
|
|
86
|
+
text-decoration: none;
|
|
87
|
+
transition: background 100ms, color 100ms, border-color 100ms;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.playground-btn:hover {
|
|
91
|
+
background: var(--color-surface-sunken);
|
|
92
|
+
color: var(--color-text);
|
|
93
|
+
border-color: var(--color-neutral-400);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.dark .playground-btn:hover {
|
|
97
|
+
border-color: var(--color-neutral-600);
|
|
98
|
+
}
|
|
99
|
+
</style>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useSetupChecklist } from '../composables/checklist'
|
|
3
|
+
|
|
4
|
+
const { currentModule } = defineProps<{
|
|
5
|
+
currentModule?: string
|
|
6
|
+
}>()
|
|
7
|
+
|
|
8
|
+
const { results, loading, evaluated, evaluate } = useSetupChecklist()
|
|
9
|
+
|
|
10
|
+
// Evaluate on mount if not already done
|
|
11
|
+
if (!evaluated.value)
|
|
12
|
+
evaluate()
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<div class="setup-checklist">
|
|
17
|
+
<!-- Loading state -->
|
|
18
|
+
<DevtoolsLoading v-if="loading" />
|
|
19
|
+
|
|
20
|
+
<!-- Per-module sections -->
|
|
21
|
+
<template v-else-if="evaluated">
|
|
22
|
+
<DevtoolsSection
|
|
23
|
+
v-for="result of results"
|
|
24
|
+
:key="result.moduleSlug"
|
|
25
|
+
:icon="result.moduleIcon"
|
|
26
|
+
:text="result.moduleLabel"
|
|
27
|
+
:open="result.requiredPending > 0 || result.moduleSlug === currentModule"
|
|
28
|
+
:padding="false"
|
|
29
|
+
>
|
|
30
|
+
<template #actions>
|
|
31
|
+
<DevtoolsChecklistBadge
|
|
32
|
+
:required-pending="result.requiredPending"
|
|
33
|
+
:recommended-pending="result.recommendedPending"
|
|
34
|
+
/>
|
|
35
|
+
</template>
|
|
36
|
+
<div class="setup-checklist-items">
|
|
37
|
+
<DevtoolsChecklistItem
|
|
38
|
+
v-for="item of result.items"
|
|
39
|
+
:key="item.id"
|
|
40
|
+
:item="item"
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
</DevtoolsSection>
|
|
44
|
+
</template>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<style scoped>
|
|
49
|
+
.setup-checklist {
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-direction: column;
|
|
52
|
+
gap: 0.375rem;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.setup-checklist-items {
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-direction: column;
|
|
58
|
+
padding: 0.125rem 0.25rem;
|
|
59
|
+
}
|
|
60
|
+
</style>
|