Skip to content

Commit b3c8efc

Browse files
antfubotantfu
andauthored
docs: add an interactive Getting Started wizard to the guide (#303)
Co-authored-by: Anthony Fu <github@antfu.me>
1 parent 8731a47 commit b3c8efc

4 files changed

Lines changed: 358 additions & 0 deletions

File tree

docs/app/app.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export default defineAppConfig({
7474
title: 'Introduction',
7575
items: [
7676
'/guide',
77+
'/guide/getting-started',
7778
'/guide/tutorial-server-data-inspector',
7879
],
7980
},
@@ -97,6 +98,7 @@ export default defineAppConfig({
9798
'/adapters',
9899
'/guide/standalone-cli',
99100
'/guide/client',
101+
'/guide/in-page-channel',
100102
'/guide/transports',
101103
'/guide/security',
102104
],
Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
<script setup lang="ts">
2+
/**
3+
* Interactive "what should I read" wizard for the Getting Started guide.
4+
*
5+
* Every question is a grid of selectable cards (multiple answers allowed per
6+
* question, since a real devtool usually spans more than one answer — e.g.
7+
* it reads from both the node side and the user's web app). Selections
8+
* persist to `localStorage` so a reader can leave the page and pick up where
9+
* they left off; the recommended reading list at the bottom recomputes from
10+
* whatever is currently checked.
11+
*/
12+
13+
interface WizardItem {
14+
value: string
15+
label: string
16+
icon: string
17+
/** Accent color key from `ITEM_COLORS`, applied once the item is selected. */
18+
color: keyof typeof ITEM_COLORS
19+
description?: string
20+
}
21+
22+
/**
23+
* Per-item accent colors, applied only to a *selected* card (its badge, plus
24+
* the card's border/background/ring); unselected cards stay neutral gray.
25+
* Spelled out as full class strings (not interpolated) so Tailwind's scanner
26+
* keeps them in the build.
27+
*/
28+
const ITEM_COLORS = {
29+
sky: { badge: 'bg-sky-500/10 text-sky-500', card: 'border-sky-500/60 bg-sky-500/5 ring-1 ring-sky-500/20' },
30+
indigo: { badge: 'bg-indigo-500/10 text-indigo-500', card: 'border-indigo-500/60 bg-indigo-500/5 ring-1 ring-indigo-500/20' },
31+
violet: { badge: 'bg-violet-500/10 text-violet-500', card: 'border-violet-500/60 bg-violet-500/5 ring-1 ring-violet-500/20' },
32+
purple: { badge: 'bg-purple-500/10 text-purple-500', card: 'border-purple-500/60 bg-purple-500/5 ring-1 ring-purple-500/20' },
33+
fuchsia: { badge: 'bg-fuchsia-500/10 text-fuchsia-500', card: 'border-fuchsia-500/60 bg-fuchsia-500/5 ring-1 ring-fuchsia-500/20' },
34+
pink: { badge: 'bg-pink-500/10 text-pink-500', card: 'border-pink-500/60 bg-pink-500/5 ring-1 ring-pink-500/20' },
35+
rose: { badge: 'bg-rose-500/10 text-rose-500', card: 'border-rose-500/60 bg-rose-500/5 ring-1 ring-rose-500/20' },
36+
amber: { badge: 'bg-amber-500/10 text-amber-500', card: 'border-amber-500/60 bg-amber-500/5 ring-1 ring-amber-500/20' },
37+
orange: { badge: 'bg-orange-500/10 text-orange-500', card: 'border-orange-500/60 bg-orange-500/5 ring-1 ring-orange-500/20' },
38+
emerald: { badge: 'bg-emerald-500/10 text-emerald-500', card: 'border-emerald-500/60 bg-emerald-500/5 ring-1 ring-emerald-500/20' },
39+
teal: { badge: 'bg-teal-500/10 text-teal-500', card: 'border-teal-500/60 bg-teal-500/5 ring-1 ring-teal-500/20' },
40+
cyan: { badge: 'bg-cyan-500/10 text-cyan-500', card: 'border-cyan-500/60 bg-cyan-500/5 ring-1 ring-cyan-500/20' },
41+
blue: { badge: 'bg-blue-500/10 text-blue-500', card: 'border-blue-500/60 bg-blue-500/5 ring-1 ring-blue-500/20' },
42+
green: { badge: 'bg-green-500/10 text-green-500', card: 'border-green-500/60 bg-green-500/5 ring-1 ring-green-500/20' },
43+
} as const
44+
45+
interface WizardSection {
46+
key: string
47+
title: string
48+
hint: string
49+
icon: string
50+
items: WizardItem[]
51+
}
52+
53+
interface DocEntry {
54+
title: string
55+
description: string
56+
icon: string
57+
}
58+
59+
const STORAGE_KEY = 'devframe-docs:getting-started'
60+
61+
const sections: WizardSection[] = [
62+
{
63+
key: 'environments',
64+
title: 'Target environments',
65+
hint: 'What do you expect your tool to work with?',
66+
icon: 'i-lucide-target',
67+
items: [
68+
{ value: 'standalone', label: 'Standalone', icon: 'i-lucide-terminal', color: 'sky', description: 'A CLI or dev server with no host framework' },
69+
{ value: 'framework', label: 'Specific framework', icon: 'i-lucide-shapes', color: 'violet', description: 'Specifically for frameworks like Vite, Next.js, Nuxt, etc.' },
70+
{ value: 'all', label: 'All frameworks', icon: 'i-lucide-infinity', color: 'emerald', description: 'I want to support as many frameworks as possible' },
71+
],
72+
},
73+
{
74+
key: 'dataSource',
75+
title: 'Data source',
76+
hint: 'Where do you want to visualize data from?',
77+
icon: 'i-lucide-database',
78+
items: [
79+
{ value: 'node', label: 'The node side', icon: 'i-lucide-server', color: 'indigo', description: 'Server state, build output, the filesystem, child processes' },
80+
{ value: 'browser', label: 'The user\'s web app', icon: 'i-lucide-app-window', color: 'cyan', description: 'State living in the page you\'re developing' },
81+
],
82+
},
83+
{
84+
key: 'availability',
85+
title: 'Data availability',
86+
hint: 'When is the data available?',
87+
icon: 'i-lucide-clock',
88+
items: [
89+
{ value: 'dev', label: 'Development time', icon: 'i-lucide-code', color: 'blue', description: 'Live, over a running dev server' },
90+
{ value: 'build', label: 'Production build time', icon: 'i-lucide-hammer', color: 'amber', description: 'Data from the production build' },
91+
{ value: 'static', label: 'Statically available', icon: 'i-lucide-hard-drive', color: 'teal', description: 'Local filesystem, uploaded files, etc.' },
92+
{ value: 'remote', label: 'Remotely', icon: 'i-lucide-cloud', color: 'sky', description: 'Over the web' },
93+
],
94+
},
95+
{
96+
key: 'frontend',
97+
title: 'Frontend approach',
98+
hint: 'How do you want to build the frontend view?',
99+
icon: 'i-lucide-palette',
100+
items: [
101+
{ value: 'framework', label: 'A preferred framework', icon: 'i-lucide-component', color: 'green', description: 'Vue, React, Svelte, Solid...' },
102+
{ value: 'webcomponents', label: 'Web Components', icon: 'i-lucide-box', color: 'orange', description: 'Use Web Components for renderering' },
103+
{ value: 'nodeside', label: 'Build it on the node side', icon: 'i-lucide-braces', color: 'purple', description: 'Describe the UI as data instead of shipping a bundle' },
104+
],
105+
},
106+
{
107+
key: 'requirements',
108+
title: 'Other requirements',
109+
hint: 'Any specific requirements?',
110+
icon: 'i-lucide-list-checks',
111+
items: [
112+
{ value: 'agent', label: 'Exposed to coding agents', icon: 'i-lucide-bot', color: 'pink', description: 'Some functionality should be available to coding agents.' },
113+
{ value: 'terminal', label: 'Sub process access', icon: 'i-lucide-square-terminal', color: 'amber', description: 'Need to spawn other child process from the node side' },
114+
{ value: 'streaming', label: 'Streaming data', icon: 'i-lucide-radio', color: 'rose', description: 'Push chunk-style data from the node side to the browser side' },
115+
{ value: 'deep-linking', label: 'Deep linking', icon: 'i-lucide-link', color: 'cyan', description: 'Shareable URLs into a specific view' },
116+
{ value: 'overlay', label: 'User app overlay', icon: 'i-lucide-layers', color: 'fuchsia', description: 'I want to overlay a UI on top of the user\'s web app' },
117+
],
118+
},
119+
]
120+
121+
/** Every doc a recommendation can point at, keyed by its route. */
122+
const DOC_CATALOG: Record<string, DocEntry> = {
123+
'/guide': { title: 'Introduction', description: 'What devframe is and who it\'s for.', icon: 'i-lucide-book-open' },
124+
'/guide/devframe-definition': { title: 'Devframe Definition', description: 'One defineDevframe() call returns a portable definition every adapter consumes.', icon: 'i-lucide-package' },
125+
'/guide/tutorial-server-data-inspector': { title: 'Tutorial: Build a Server Data Inspector', description: 'Build a devtool end to end, then ship it as a hub dock, a static build, a dev server, and a CLI.', icon: 'i-lucide-graduation-cap' },
126+
'/guide/rpc': { title: 'RPC', description: 'Type-safe, bidirectional calls between the node side and the browser side.', icon: 'i-lucide-cable' },
127+
'/guide/shared-state': { title: 'Shared State', description: 'Observable state synced between the node side and every RPC client.', icon: 'i-lucide-refresh-cw' },
128+
'/guide/streaming': { title: 'Streaming', description: 'Push chunk-style data from the node side to the browser side.', icon: 'i-lucide-radio' },
129+
'/guide/client-assets': { title: 'Client Assets', description: 'Where a devframe\'s built SPA lives — a local directory or an npm package.', icon: 'i-lucide-folder-tree' },
130+
'/guide/client': { title: 'Client', description: 'Connects any surface to a devframe\'s node side with RPC and shared state.', icon: 'i-lucide-plug' },
131+
'/guide/transports': { title: 'Transports', description: 'Live RPC over WebSocket or SSE, transparent to your RPC code.', icon: 'i-lucide-waypoints' },
132+
'/guide/security': { title: 'Security', description: 'Localhost binding and a trust handshake before a browser can call RPC.', icon: 'i-lucide-shield-check' },
133+
'/guide/agent-native': { title: 'Agent-Native Devframe', description: 'Expose RPC functions, resources, and shared state to coding agents over MCP.', icon: 'i-lucide-bot' },
134+
'/guide/hub': { title: 'Hub', description: 'Orchestrate many devtools sharing one UI — docks, terminals, messages, commands.', icon: 'i-lucide-layout-dashboard' },
135+
'/guide/client-context': { title: 'Client Scripts & Client Context', description: 'How a dock client script runs a devframe\'s code inside the host page.', icon: 'i-lucide-code' },
136+
'/guide/hub-initiate': { title: 'Serve a Hub Anywhere', description: 'initHub() serves a whole multi-devframe install from one handler.', icon: 'i-lucide-server-cog' },
137+
'/guide/services': { title: 'Cross-Devframe Services', description: 'Expose a typed, namespaced capability to every devframe in a hub.', icon: 'i-lucide-share-2' },
138+
'/guide/deep-linking': { title: 'Deep Linking', description: 'Send a user to a specific view inside a devframe from a URL or an agent.', icon: 'i-lucide-link' },
139+
'/guide/json-render': { title: 'JSON-Render', description: 'Describe a UI as data — a serializable component spec any frontend renders.', icon: 'i-lucide-braces' },
140+
'/guide/build-your-own-json-render-frontend': { title: 'Build Your Own JSON-Render Frontend', description: 'Implement the renderer contract in your own framework instead of the reference one.', icon: 'i-lucide-component' },
141+
'/guide/build-your-own-hub-ui': { title: 'Build Your Own Hub UI', description: 'The two contracts a hub UI provider implements — node side and browser side.', icon: 'i-lucide-layout-panel-left' },
142+
'/guide/standalone-cli': { title: 'Standalone CLI with Devframe', description: 'npx my-tool starts a dev server serving your SPA over type-safe RPC.', icon: 'i-lucide-terminal' },
143+
'/helpers/interactive-auth': { title: 'Interactive Auth', description: 'An OTP auth layer over devframe\'s node-side primitives.', icon: 'i-lucide-key-round' },
144+
'/helpers/utilities': { title: 'Utilities', description: 'Small, stable helpers bundled into devframe — no npm install.', icon: 'i-lucide-wrench' },
145+
'/adapters': { title: 'Adapters', description: 'Every path from a DevframeDefinition to a running devframe.', icon: 'i-lucide-shuffle' },
146+
'/adapters/initiate': { title: 'The Standard Handler', description: 'initDevframe() turns a definition into a Web Standard Request → Response handler.', icon: 'i-lucide-server' },
147+
'/adapters/cac': { title: 'CLI (cac)', description: 'A cac CLI around a DevframeDefinition with dev, build, and mcp commands.', icon: 'i-lucide-square-terminal' },
148+
'/adapters/build': { title: 'Build', description: 'Produces a static deploy of a devframe.', icon: 'i-lucide-hammer' },
149+
'/adapters/vite': { title: 'Vite (adapter)', description: 'Wraps a definition so Vite DevTools\' plugin-scan picks it up.', icon: 'i-lucide-zap' },
150+
'/adapters/embedded': { title: 'Embedded', description: 'Register a devframe into an already-running context at runtime.', icon: 'i-lucide-plug-zap' },
151+
'/adapters/mcp': { title: 'MCP', description: 'Exposes a devframe\'s agent-facing API as a Model Context Protocol server.', icon: 'i-lucide-bot' },
152+
'/frameworks': { title: 'Frameworks', description: 'Framework kits that integrate devframe with a meta-framework\'s dev server.', icon: 'i-lucide-blocks' },
153+
'/frameworks/vite': { title: 'Vite', description: 'Author one devframe\'s SPA, or mount a whole hub, from a Vite plugin.', icon: 'i-simple-icons-vite' },
154+
'/frameworks/next': { title: 'Next', description: 'Host devframes from a Next.js App Router app via a route handler.', icon: 'i-simple-icons-nextdotjs' },
155+
'/frameworks/nuxt': { title: 'Nuxt', description: 'A Nuxt module split into authoring one devframe or mounting a hub.', icon: 'i-simple-icons-nuxtdotjs' },
156+
'/plugins/a11y': { title: 'Accessibility Inspector', description: 'Runs axe-core against the user app and highlights violations in the page.', icon: 'i-lucide-accessibility' },
157+
'/plugins/terminals': { title: 'Terminals', description: 'A terminal panel built on xterm.js.', icon: 'i-lucide-square-terminal' },
158+
}
159+
160+
/** Always worth reading, regardless of what's checked above. */
161+
const BASE_DOCS = ['/guide', '/guide/devframe-definition', '/guide/tutorial-server-data-inspector']
162+
163+
/** `${section.key}:${item.value}` -> doc routes that answer is worth reading. */
164+
const RECOMMENDATIONS: Record<string, string[]> = {
165+
'dataSource:node': ['/guide/rpc', '/guide/shared-state', '/helpers/utilities'],
166+
'dataSource:browser': ['/guide/client-context', '/guide/deep-linking', '/plugins/a11y'],
167+
168+
'environments:standalone': ['/guide/standalone-cli', '/adapters/cac', '/adapters/build'],
169+
'environments:framework': ['/adapters'],
170+
'environments:all': ['/adapters/initiate', '/adapters', '/guide/devframe-definition'],
171+
172+
'availability:dev': ['/guide/rpc', '/guide/transports'],
173+
'availability:build': ['/adapters/build', '/guide/client-assets'],
174+
'availability:static': ['/adapters/build', '/helpers/utilities'],
175+
'availability:remote': ['/guide/transports', '/guide/security'],
176+
177+
'frontend:framework': ['/guide/client-assets', '/guide/client'],
178+
'frontend:webcomponents': ['/guide/hub', '/guide/build-your-own-hub-ui'],
179+
'frontend:nodeside': ['/guide/json-render', '/guide/build-your-own-json-render-frontend'],
180+
181+
'requirements:agent': ['/guide/agent-native', '/adapters/mcp'],
182+
'requirements:terminal': ['/plugins/terminals'],
183+
'requirements:streaming': ['/guide/streaming'],
184+
'requirements:deep-linking': ['/guide/deep-linking'],
185+
'requirements:overlay': ['/guide/client-context', '/plugins/a11y'],
186+
}
187+
188+
const selections = reactive<Record<string, string[]>>(
189+
Object.fromEntries(sections.map(section => [section.key, [] as string[]])),
190+
)
191+
192+
onMounted(() => {
193+
if (!import.meta.client)
194+
return
195+
try {
196+
const raw = localStorage.getItem(STORAGE_KEY)
197+
if (!raw)
198+
return
199+
const saved = JSON.parse(raw) as Record<string, unknown>
200+
for (const section of sections) {
201+
const values = saved[section.key]
202+
if (!Array.isArray(values))
203+
continue
204+
const known = new Set(section.items.map(item => item.value))
205+
selections[section.key] = values.filter((value): value is string => typeof value === 'string' && known.has(value))
206+
}
207+
}
208+
catch {
209+
// Corrupt or inaccessible storage - fall back to a clean slate.
210+
}
211+
})
212+
213+
watch(selections, (value) => {
214+
if (!import.meta.client)
215+
return
216+
localStorage.setItem(STORAGE_KEY, JSON.stringify(value))
217+
}, { deep: true })
218+
219+
function isChecked(sectionKey: string, value: string): boolean {
220+
return selections[sectionKey]!.includes(value)
221+
}
222+
223+
function toggle(sectionKey: string, value: string): void {
224+
const current = selections[sectionKey]!
225+
const index = current.indexOf(value)
226+
if (index === -1)
227+
current.push(value)
228+
else
229+
current.splice(index, 1)
230+
}
231+
232+
const hasSelections = computed(() => sections.some(section => selections[section.key]!.length > 0))
233+
234+
const recommendedDocs = computed(() => {
235+
const paths = new Set(BASE_DOCS)
236+
for (const section of sections) {
237+
for (const value of selections[section.key]!) {
238+
for (const path of RECOMMENDATIONS[`${section.key}:${value}`] ?? [])
239+
paths.add(path)
240+
}
241+
}
242+
return [...paths]
243+
.filter(path => path in DOC_CATALOG)
244+
.map(path => ({ path, ...DOC_CATALOG[path]! }))
245+
})
246+
247+
function reset(): void {
248+
for (const section of sections) selections[section.key] = []
249+
}
250+
</script>
251+
252+
<template>
253+
<div class="not-prose rounded-xl border border-default divide-y divide-default overflow-hidden">
254+
<div class="flex items-center justify-between gap-4 px-5 py-3 sm:px-6 bg-muted">
255+
<div>
256+
<p class="font-medium text-highlighted">
257+
What kind of devtool do you want to build?
258+
</p>
259+
<p class="text-sm text-muted mt-0.5">
260+
Multi-select, check whatever applies (selection persists in localStorage)
261+
</p>
262+
</div>
263+
<UButton
264+
label="Reset"
265+
icon="i-lucide-rotate-ccw"
266+
color="neutral"
267+
variant="ghost"
268+
size="xs"
269+
:disabled="!hasSelections"
270+
class="cursor-pointer shrink-0"
271+
@click="reset"
272+
/>
273+
</div>
274+
275+
<div
276+
v-for="section in sections"
277+
:key="section.key"
278+
class="px-5 py-4 sm:px-6"
279+
>
280+
<div class="flex flex-wrap items-baseline gap-x-2 mb-3">
281+
<p class="flex items-center gap-2 font-medium text-highlighted">
282+
<UIcon :name="section.icon" class="size-4 text-muted" />
283+
{{ section.hint }}
284+
</p>
285+
</div>
286+
287+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2.5">
288+
<button
289+
v-for="item in section.items"
290+
:key="item.value"
291+
type="button"
292+
role="checkbox"
293+
:aria-checked="isChecked(section.key, item.value)"
294+
class="relative flex items-center gap-3 rounded-lg border p-3 text-left transition-colors cursor-pointer"
295+
:class="isChecked(section.key, item.value)
296+
? ITEM_COLORS[item.color].card
297+
: 'border-default hover:border-accented hover:bg-elevated/50'"
298+
@click="toggle(section.key, item.value)"
299+
>
300+
<span
301+
class="inline-flex items-center justify-center size-8 shrink-0 rounded-full transition-colors"
302+
:class="isChecked(section.key, item.value) ? ITEM_COLORS[item.color].badge : 'bg-elevated text-muted'"
303+
>
304+
<UIcon :name="item.icon" class="size-4" />
305+
</span>
306+
<span class="min-w-0 flex-1">
307+
<span class="block text-sm font-medium text-highlighted">{{ item.label }}</span>
308+
<span
309+
v-if="item.description"
310+
class="block text-xs text-muted mt-0.5"
311+
>{{ item.description }}</span>
312+
</span>
313+
</button>
314+
</div>
315+
</div>
316+
317+
<div class="px-5 py-4 sm:px-6 bg-muted">
318+
<p class="font-medium text-highlighted mb-3">
319+
{{ hasSelections ? 'Recommended docs, based on your answers' : 'Start here' }}
320+
</p>
321+
<div class="flex flex-col divide-y divide-default rounded-lg border border-default overflow-hidden bg-default">
322+
<NuxtLink
323+
v-for="doc in recommendedDocs"
324+
:key="doc.path"
325+
:to="doc.path"
326+
class="group flex items-center gap-3 p-3 transition-colors hover:bg-elevated/50"
327+
>
328+
<span class="inline-flex items-center justify-center size-8 shrink-0 rounded-full bg-elevated text-muted">
329+
<UIcon :name="doc.icon" class="size-4" />
330+
</span>
331+
<span class="min-w-0 flex-1">
332+
<span class="block text-sm font-medium text-highlighted">{{ doc.title }}</span>
333+
<span class="block text-xs text-muted mt-0.5">{{ doc.description }}</span>
334+
</span>
335+
<UIcon
336+
name="i-lucide-arrow-right"
337+
class="size-4 shrink-0 text-dimmed transition-transform group-hover:translate-x-0.5 group-hover:text-muted"
338+
/>
339+
</NuxtLink>
340+
</div>
341+
</div>
342+
</div>
343+
</template>

0 commit comments

Comments
 (0)