|
| 1 | +<script setup lang="ts"> |
| 2 | +/** |
| 3 | + * Interactive "what should I read" wizard for the Getting Started guide. |
| 4 | + * |
| 5 | + * Every question is a checkbox group (multiple answers allowed per question, |
| 6 | + * since a real devtool usually spans more than one answer — e.g. it reads |
| 7 | + * from both the node side and the user's web app). Selections persist to |
| 8 | + * `localStorage` so a reader can leave the page and pick up where they left |
| 9 | + * off; the recommended reading list at the bottom recomputes from whatever |
| 10 | + * is currently checked. |
| 11 | + */ |
| 12 | +
|
| 13 | +interface WizardItem { |
| 14 | + value: string |
| 15 | + label: string |
| 16 | + description?: string |
| 17 | +} |
| 18 | +
|
| 19 | +interface WizardSection { |
| 20 | + key: string |
| 21 | + legend: string |
| 22 | + items: WizardItem[] |
| 23 | +} |
| 24 | +
|
| 25 | +interface DocEntry { |
| 26 | + title: string |
| 27 | + description: string |
| 28 | + icon: string |
| 29 | +} |
| 30 | +
|
| 31 | +const STORAGE_KEY = 'devframe-docs:getting-started' |
| 32 | +
|
| 33 | +const sections: WizardSection[] = [ |
| 34 | + { |
| 35 | + key: 'dataSource', |
| 36 | + legend: 'Where do you want to visualize data from?', |
| 37 | + items: [ |
| 38 | + { value: 'node', label: 'The node side', description: 'Server state, build output, the filesystem, child processes' }, |
| 39 | + { value: 'browser', label: 'The user\'s web app', description: 'State living in the page you\'re developing' }, |
| 40 | + ], |
| 41 | + }, |
| 42 | + { |
| 43 | + key: 'environments', |
| 44 | + legend: 'What do you expect your tool to work with?', |
| 45 | + items: [ |
| 46 | + { value: 'standalone', label: 'Standalone', description: 'A CLI or dev server with no host framework' }, |
| 47 | + { value: 'vite', label: 'Vite' }, |
| 48 | + { value: 'next', label: 'Next.js' }, |
| 49 | + { value: 'framework', label: 'A specific framework', description: 'Nuxt, or a host framework the kits don\'t cover yet' }, |
| 50 | + { value: 'all', label: 'All frameworks', description: 'Anything that speaks a Web Standard Request/Response' }, |
| 51 | + ], |
| 52 | + }, |
| 53 | + { |
| 54 | + key: 'availability', |
| 55 | + legend: 'When is the data available?', |
| 56 | + items: [ |
| 57 | + { value: 'dev', label: 'Development time', description: 'Live, over a running dev server' }, |
| 58 | + { value: 'build', label: 'Production build time' }, |
| 59 | + { value: 'static', label: 'Statically available', description: 'Local filesystem, a static dump, etc.' }, |
| 60 | + { value: 'remote', label: 'Remotely', description: 'Over the web, not on localhost' }, |
| 61 | + ], |
| 62 | + }, |
| 63 | + { |
| 64 | + key: 'frontend', |
| 65 | + legend: 'How do you want to build the frontend view?', |
| 66 | + items: [ |
| 67 | + { value: 'framework', label: 'A preferred framework', description: 'Vue, React, Svelte, Solid...' }, |
| 68 | + { value: 'webcomponents', label: 'Web Components' }, |
| 69 | + { value: 'nodeside', label: 'Build the frontend on the node side', description: 'Describe the UI as data instead of shipping a bundle' }, |
| 70 | + ], |
| 71 | + }, |
| 72 | + { |
| 73 | + key: 'agent', |
| 74 | + legend: 'Should it also work with a coding agent?', |
| 75 | + items: [ |
| 76 | + { value: 'agent', label: 'Yes, expose it to a coding agent', description: 'Same RPC functions, resources, and state, over MCP' }, |
| 77 | + ], |
| 78 | + }, |
| 79 | + { |
| 80 | + key: 'requirements', |
| 81 | + legend: 'Any specific requirements?', |
| 82 | + items: [ |
| 83 | + { value: 'streaming', label: 'Streaming data' }, |
| 84 | + { value: 'overlay', label: 'An overlay on the user\'s web app' }, |
| 85 | + { value: 'hub', label: 'Composing with other devtools', description: 'One UI, many devframes' }, |
| 86 | + { value: 'security', label: 'Authentication' }, |
| 87 | + { value: 'deep-linking', label: 'Deep linking', description: 'Shareable URLs into a specific view' }, |
| 88 | + { value: 'terminal', label: 'Terminal / process access' }, |
| 89 | + ], |
| 90 | + }, |
| 91 | +] |
| 92 | +
|
| 93 | +/** Every doc a recommendation can point at, keyed by its route. */ |
| 94 | +const DOC_CATALOG: Record<string, DocEntry> = { |
| 95 | + '/guide': { title: 'Introduction', description: 'What devframe is and who it\'s for.', icon: 'i-lucide-book-open' }, |
| 96 | + '/guide/devframe-definition': { title: 'Devframe Definition', description: 'One defineDevframe() call returns a portable definition every adapter consumes.', icon: 'i-lucide-package' }, |
| 97 | + '/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' }, |
| 98 | + '/guide/rpc': { title: 'RPC', description: 'Type-safe, bidirectional calls between the node side and the browser side.', icon: 'i-lucide-cable' }, |
| 99 | + '/guide/shared-state': { title: 'Shared State', description: 'Observable state synced between the node side and every RPC client.', icon: 'i-lucide-refresh-cw' }, |
| 100 | + '/guide/streaming': { title: 'Streaming', description: 'Push chunk-style data from the node side to the browser side.', icon: 'i-lucide-radio' }, |
| 101 | + '/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' }, |
| 102 | + '/guide/client': { title: 'Client', description: 'Connects any surface to a devframe\'s node side with RPC and shared state.', icon: 'i-lucide-plug' }, |
| 103 | + '/guide/transports': { title: 'Transports', description: 'Live RPC over WebSocket or SSE, transparent to your RPC code.', icon: 'i-lucide-waypoints' }, |
| 104 | + '/guide/security': { title: 'Security', description: 'Localhost binding and a trust handshake before a browser can call RPC.', icon: 'i-lucide-shield-check' }, |
| 105 | + '/guide/agent-native': { title: 'Agent-Native Devframe', description: 'Expose RPC functions, resources, and shared state to coding agents over MCP.', icon: 'i-lucide-bot' }, |
| 106 | + '/guide/hub': { title: 'Hub', description: 'Orchestrate many devtools sharing one UI — docks, terminals, messages, commands.', icon: 'i-lucide-layout-dashboard' }, |
| 107 | + '/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-2' }, |
| 108 | + '/guide/hub-initiate': { title: 'Serve a Hub Anywhere', description: 'initHub() serves a whole multi-devframe install from one handler.', icon: 'i-lucide-server-cog' }, |
| 109 | + '/guide/services': { title: 'Cross-Devframe Services', description: 'Expose a typed, namespaced capability to every devframe in a hub.', icon: 'i-lucide-share-2' }, |
| 110 | + '/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' }, |
| 111 | + '/guide/json-render': { title: 'JSON-Render', description: 'Describe a UI as data — a serializable component spec any frontend renders.', icon: 'i-lucide-braces' }, |
| 112 | + '/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' }, |
| 113 | + '/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' }, |
| 114 | + '/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' }, |
| 115 | + '/helpers/interactive-auth': { title: 'Interactive Auth', description: 'An OTP auth layer over devframe\'s node-side primitives.', icon: 'i-lucide-key-round' }, |
| 116 | + '/helpers/utilities': { title: 'Utilities', description: 'Small, stable helpers bundled into devframe — no npm install.', icon: 'i-lucide-wrench' }, |
| 117 | + '/adapters': { title: 'Adapters', description: 'Every path from a DevframeDefinition to a running devframe.', icon: 'i-lucide-shuffle' }, |
| 118 | + '/adapters/initiate': { title: 'The Standard Handler', description: 'initDevframe() turns a definition into a Web Standard Request → Response handler.', icon: 'i-lucide-server' }, |
| 119 | + '/adapters/cac': { title: 'CLI (cac)', description: 'A cac CLI around a DevframeDefinition with dev, build, and mcp commands.', icon: 'i-lucide-square-terminal' }, |
| 120 | + '/adapters/build': { title: 'Build', description: 'Produces a static deploy of a devframe.', icon: 'i-lucide-hammer' }, |
| 121 | + '/adapters/vite': { title: 'Vite (adapter)', description: 'Wraps a definition so Vite DevTools\' plugin-scan picks it up.', icon: 'i-lucide-zap' }, |
| 122 | + '/adapters/embedded': { title: 'Embedded', description: 'Register a devframe into an already-running context at runtime.', icon: 'i-lucide-plug-zap' }, |
| 123 | + '/adapters/mcp': { title: 'MCP', description: 'Exposes a devframe\'s agent-facing API as a Model Context Protocol server.', icon: 'i-lucide-bot' }, |
| 124 | + '/frameworks': { title: 'Frameworks', description: 'Framework kits that integrate devframe with a meta-framework\'s dev server.', icon: 'i-lucide-blocks' }, |
| 125 | + '/frameworks/vite': { title: 'Vite', description: 'Author one devframe\'s SPA, or mount a whole hub, from a Vite plugin.', icon: 'i-simple-icons-vite' }, |
| 126 | + '/frameworks/next': { title: 'Next', description: 'Host devframes from a Next.js App Router app via a route handler.', icon: 'i-simple-icons-nextdotjs' }, |
| 127 | + '/frameworks/nuxt': { title: 'Nuxt', description: 'A Nuxt module split into authoring one devframe or mounting a hub.', icon: 'i-simple-icons-nuxtdotjs' }, |
| 128 | + '/plugins/a11y': { title: 'Accessibility Inspector', description: 'Runs axe-core against the user app and highlights violations in the page.', icon: 'i-lucide-accessibility' }, |
| 129 | + '/plugins/terminals': { title: 'Terminals', description: 'A terminal panel built on xterm.js.', icon: 'i-lucide-square-terminal' }, |
| 130 | +} |
| 131 | +
|
| 132 | +/** Always worth reading, regardless of what's checked above. */ |
| 133 | +const BASE_DOCS = ['/guide', '/guide/devframe-definition', '/guide/tutorial-server-data-inspector'] |
| 134 | +
|
| 135 | +/** `${section.key}:${item.value}` -> doc routes that answer is worth reading. */ |
| 136 | +const RECOMMENDATIONS: Record<string, string[]> = { |
| 137 | + 'dataSource:node': ['/guide/rpc', '/guide/shared-state', '/helpers/utilities'], |
| 138 | + 'dataSource:browser': ['/guide/client-context', '/guide/deep-linking', '/plugins/a11y'], |
| 139 | +
|
| 140 | + 'environments:standalone': ['/guide/standalone-cli', '/adapters/cac', '/adapters/build'], |
| 141 | + 'environments:vite': ['/frameworks/vite', '/adapters/vite'], |
| 142 | + 'environments:next': ['/frameworks/next'], |
| 143 | + 'environments:framework': ['/frameworks/nuxt', '/adapters/embedded'], |
| 144 | + 'environments:all': ['/adapters/initiate', '/adapters', '/guide/devframe-definition'], |
| 145 | +
|
| 146 | + 'availability:dev': ['/guide/rpc', '/guide/transports'], |
| 147 | + 'availability:build': ['/adapters/build', '/guide/client-assets'], |
| 148 | + 'availability:static': ['/adapters/build', '/helpers/utilities'], |
| 149 | + 'availability:remote': ['/guide/transports', '/guide/security'], |
| 150 | +
|
| 151 | + 'frontend:framework': ['/guide/client-assets', '/guide/client'], |
| 152 | + 'frontend:webcomponents': ['/guide/hub', '/guide/build-your-own-hub-ui'], |
| 153 | + 'frontend:nodeside': ['/guide/json-render', '/guide/build-your-own-json-render-frontend'], |
| 154 | +
|
| 155 | + 'agent:agent': ['/guide/agent-native', '/adapters/mcp'], |
| 156 | +
|
| 157 | + 'requirements:streaming': ['/guide/streaming'], |
| 158 | + 'requirements:overlay': ['/guide/client-context', '/plugins/a11y'], |
| 159 | + 'requirements:hub': ['/guide/hub', '/guide/hub-initiate', '/guide/services'], |
| 160 | + 'requirements:security': ['/guide/security', '/helpers/interactive-auth'], |
| 161 | + 'requirements:deep-linking': ['/guide/deep-linking'], |
| 162 | + 'requirements:terminal': ['/plugins/terminals'], |
| 163 | +} |
| 164 | +
|
| 165 | +const selections = reactive<Record<string, string[]>>( |
| 166 | + Object.fromEntries(sections.map(section => [section.key, [] as string[]])), |
| 167 | +) |
| 168 | +
|
| 169 | +onMounted(() => { |
| 170 | + if (!import.meta.client) |
| 171 | + return |
| 172 | + try { |
| 173 | + const raw = localStorage.getItem(STORAGE_KEY) |
| 174 | + if (!raw) |
| 175 | + return |
| 176 | + const saved = JSON.parse(raw) as Record<string, unknown> |
| 177 | + for (const section of sections) { |
| 178 | + const values = saved[section.key] |
| 179 | + if (!Array.isArray(values)) |
| 180 | + continue |
| 181 | + const known = new Set(section.items.map(item => item.value)) |
| 182 | + selections[section.key] = values.filter((value): value is string => typeof value === 'string' && known.has(value)) |
| 183 | + } |
| 184 | + } |
| 185 | + catch { |
| 186 | + // Corrupt or inaccessible storage - fall back to a clean slate. |
| 187 | + } |
| 188 | +}) |
| 189 | +
|
| 190 | +watch(selections, (value) => { |
| 191 | + if (!import.meta.client) |
| 192 | + return |
| 193 | + localStorage.setItem(STORAGE_KEY, JSON.stringify(value)) |
| 194 | +}, { deep: true }) |
| 195 | +
|
| 196 | +const hasSelections = computed(() => sections.some(section => selections[section.key].length > 0)) |
| 197 | +
|
| 198 | +const recommendedDocs = computed(() => { |
| 199 | + const paths = new Set(BASE_DOCS) |
| 200 | + for (const section of sections) { |
| 201 | + for (const value of selections[section.key]) { |
| 202 | + for (const path of RECOMMENDATIONS[`${section.key}:${value}`] ?? []) |
| 203 | + paths.add(path) |
| 204 | + } |
| 205 | + } |
| 206 | + return [...paths] |
| 207 | + .filter(path => path in DOC_CATALOG) |
| 208 | + .map(path => ({ path, ...DOC_CATALOG[path]! })) |
| 209 | +}) |
| 210 | +
|
| 211 | +function reset() { |
| 212 | + for (const section of sections) selections[section.key] = [] |
| 213 | +} |
| 214 | +</script> |
| 215 | + |
| 216 | +<template> |
| 217 | + <div class="not-prose rounded-xl border border-default divide-y divide-default overflow-hidden"> |
| 218 | + <div class="flex items-center justify-between gap-4 px-5 py-4 bg-muted"> |
| 219 | + <div> |
| 220 | + <p class="font-medium text-highlighted"> |
| 221 | + What kind of devtool do you want to build? |
| 222 | + </p> |
| 223 | + <p class="text-sm text-muted mt-0.5"> |
| 224 | + Check whatever applies — answers save in your browser. |
| 225 | + </p> |
| 226 | + </div> |
| 227 | + <UButton |
| 228 | + label="Reset" |
| 229 | + icon="i-lucide-rotate-ccw" |
| 230 | + color="neutral" |
| 231 | + variant="ghost" |
| 232 | + size="xs" |
| 233 | + :disabled="!hasSelections" |
| 234 | + class="cursor-pointer shrink-0" |
| 235 | + @click="reset" |
| 236 | + /> |
| 237 | + </div> |
| 238 | + |
| 239 | + <div class="grid grid-cols-1 md:grid-cols-2 gap-px bg-default"> |
| 240 | + <div |
| 241 | + v-for="section in sections" |
| 242 | + :key="section.key" |
| 243 | + class="bg-default p-5" |
| 244 | + > |
| 245 | + <UCheckboxGroup |
| 246 | + v-model="selections[section.key]" |
| 247 | + :legend="section.legend" |
| 248 | + :items="section.items" |
| 249 | + :ui="{ legend: 'text-base font-medium text-highlighted mb-3' }" |
| 250 | + /> |
| 251 | + </div> |
| 252 | + </div> |
| 253 | + |
| 254 | + <div class="p-5 bg-muted"> |
| 255 | + <p class="font-medium text-highlighted mb-3"> |
| 256 | + {{ hasSelections ? 'Recommended docs, based on your answers' : 'Start here' }} |
| 257 | + </p> |
| 258 | + <UPageList divide> |
| 259 | + <UPageCard |
| 260 | + v-for="doc in recommendedDocs" |
| 261 | + :key="doc.path" |
| 262 | + :to="doc.path" |
| 263 | + :icon="doc.icon" |
| 264 | + :title="doc.title" |
| 265 | + :description="doc.description" |
| 266 | + orientation="horizontal" |
| 267 | + variant="ghost" |
| 268 | + :ui="{ container: 'p-3 sm:p-3', leadingIcon: 'size-5' }" |
| 269 | + /> |
| 270 | + </UPageList> |
| 271 | + </div> |
| 272 | + </div> |
| 273 | +</template> |
0 commit comments