From 8c171c1b67875f1ea994ffb546a63d10f1f9fdc6 Mon Sep 17 00:00:00 2001 From: John Leider Date: Sat, 4 Apr 2026 13:28:36 -0500 Subject: [PATCH 01/56] chore: scaffold builder app Standalone Vite SPA at apps/builder/ for the v0 Framework Builder. Follows the playground app pattern with file-based routing, UnoCSS, auto-imported components, and the standard v0 plugin stack. --- apps/builder/index.html | 13 ++++ apps/builder/package.json | 27 ++++++++ apps/builder/src/App.vue | 6 ++ apps/builder/src/components.d.ts | 17 +++++ apps/builder/src/main.ts | 102 +++++++++++++++++++++++++++++ apps/builder/src/pages/index.vue | 10 +++ apps/builder/src/plugins/icons.ts | 31 +++++++++ apps/builder/src/plugins/pinia.ts | 4 ++ apps/builder/src/typed-router.d.ts | 70 ++++++++++++++++++++ apps/builder/src/vite-env.d.ts | 1 + apps/builder/uno.config.ts | 64 ++++++++++++++++++ apps/builder/vite.config.ts | 44 +++++++++++++ package.json | 2 +- pnpm-lock.yaml | 40 +++++++++++ 14 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 apps/builder/index.html create mode 100644 apps/builder/package.json create mode 100644 apps/builder/src/App.vue create mode 100644 apps/builder/src/components.d.ts create mode 100644 apps/builder/src/main.ts create mode 100644 apps/builder/src/pages/index.vue create mode 100644 apps/builder/src/plugins/icons.ts create mode 100644 apps/builder/src/plugins/pinia.ts create mode 100644 apps/builder/src/typed-router.d.ts create mode 100644 apps/builder/src/vite-env.d.ts create mode 100644 apps/builder/uno.config.ts create mode 100644 apps/builder/vite.config.ts diff --git a/apps/builder/index.html b/apps/builder/index.html new file mode 100644 index 000000000..0020d62f7 --- /dev/null +++ b/apps/builder/index.html @@ -0,0 +1,13 @@ + + + + + + + v0 Framework Builder + + +
+ + + diff --git a/apps/builder/package.json b/apps/builder/package.json new file mode 100644 index 000000000..f374df16a --- /dev/null +++ b/apps/builder/package.json @@ -0,0 +1,27 @@ +{ + "name": "@vuetify-private/builder", + "version": "0.2.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@mdi/js": "catalog:", + "@vuetify/auth": "catalog:", + "@vuetify/v0": "workspace:*", + "fflate": "catalog:", + "pinia": "catalog:", + "vue": "catalog:" + }, + "devDependencies": { + "unocss": "catalog:", + "unplugin-vue": "catalog:", + "unplugin-vue-components": "catalog:", + "vite": "catalog:", + "vite-plugin-vue-layouts-next": "catalog:", + "vue-router": "catalog:" + } +} diff --git a/apps/builder/src/App.vue b/apps/builder/src/App.vue new file mode 100644 index 000000000..c0482986b --- /dev/null +++ b/apps/builder/src/App.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/builder/src/components.d.ts b/apps/builder/src/components.d.ts new file mode 100644 index 000000000..227ab480a --- /dev/null +++ b/apps/builder/src/components.d.ts @@ -0,0 +1,17 @@ +/* eslint-disable */ +// @ts-nocheck +// biome-ignore lint: disable +// oxlint-disable +// ------ +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 + +export {} + +/* prettier-ignore */ +declare module 'vue' { + export interface GlobalComponents { + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + } +} diff --git a/apps/builder/src/main.ts b/apps/builder/src/main.ts new file mode 100644 index 000000000..3a370a2dd --- /dev/null +++ b/apps/builder/src/main.ts @@ -0,0 +1,102 @@ +import { setupLayouts } from 'virtual:generated-layouts' +import { routes } from 'vue-router/auto-routes' + +// Framework +import { createBreakpointsPlugin, createHydrationPlugin, createLoggerPlugin, createStackPlugin, createStoragePlugin, createThemePlugin, IN_BROWSER } from '@vuetify/v0' + +import App from './App.vue' + +// Utilities +import { createApp } from 'vue' +import { createRouter, createWebHistory } from 'vue-router' + +import { createIconPlugin } from './plugins/icons' +import pinia from './plugins/pinia' + +import 'virtual:uno.css' + +const app = createApp(App) + +const router = createRouter({ + history: createWebHistory(), + routes: setupLayouts(routes), +}) + +app.use(pinia) +app.use(router) +app.use(createIconPlugin()) +app.use(createLoggerPlugin()) +app.use(createHydrationPlugin()) +app.use(createBreakpointsPlugin({ mobileBreakpoint: 768 })) +app.use(createStoragePlugin()) +app.use(createStackPlugin()) + +function getSystemTheme (): 'light' | 'dark' { + if (!IN_BROWSER) return 'light' + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' +} + +app.use( + createThemePlugin({ + default: getSystemTheme(), + target: 'html', + themes: { + light: { + dark: false, + colors: { + 'primary': '#3b82f6', + 'secondary': '#64748b', + 'accent': '#6366f1', + 'error': '#ef4444', + 'info': '#1867c0', + 'success': '#22c55e', + 'warning': '#f59e0b', + 'background': '#f5f5f5', + 'surface': '#ffffff', + 'surface-tint': '#eeeef0', + 'surface-variant': '#f5f5f5', + 'divider': '#e0e0e0', + 'on-primary': '#ffffff', + 'on-secondary': '#ffffff', + 'on-accent': '#1a1a1a', + 'on-error': '#ffffff', + 'on-info': '#ffffff', + 'on-success': '#1a1a1a', + 'on-warning': '#1a1a1a', + 'on-background': '#212121', + 'on-surface': '#212121', + 'on-surface-variant': '#666666', + }, + }, + dark: { + dark: true, + colors: { + 'primary': '#c4b5fd', + 'secondary': '#94a3b8', + 'accent': '#c084fc', + 'error': '#f87171', + 'info': '#38bdf8', + 'success': '#4ade80', + 'warning': '#fb923c', + 'background': '#121212', + 'surface': '#1a1a1a', + 'surface-tint': '#2a2a2a', + 'surface-variant': '#1e1e1e', + 'divider': '#404040', + 'on-primary': '#1a1a1a', + 'on-secondary': '#ffffff', + 'on-accent': '#ffffff', + 'on-error': '#1a1a1a', + 'on-info': '#1a1a1a', + 'on-success': '#1a1a1a', + 'on-warning': '#1a1a1a', + 'on-background': '#e0e0e0', + 'on-surface': '#e0e0e0', + 'on-surface-variant': '#a0a0a0', + }, + }, + }, + }), +) + +app.mount('#app') diff --git a/apps/builder/src/pages/index.vue b/apps/builder/src/pages/index.vue new file mode 100644 index 000000000..8aa63de90 --- /dev/null +++ b/apps/builder/src/pages/index.vue @@ -0,0 +1,10 @@ + + + diff --git a/apps/builder/src/plugins/icons.ts b/apps/builder/src/plugins/icons.ts new file mode 100644 index 000000000..34505be6f --- /dev/null +++ b/apps/builder/src/plugins/icons.ts @@ -0,0 +1,31 @@ +import { mdiArrowLeft, mdiArrowRight, mdiCheck, mdiChevronDown, mdiClose, mdiCog, mdiLock, mdiMagnify, mdiRobot } from '@mdi/js' + +// Framework +import { createPlugin, createTokensContext } from '@vuetify/v0' + +// Types +import type { App } from 'vue' + +export const [useIconContext, provideIconContext, context] = createTokensContext({ + namespace: 'v0:icons', + tokens: { + back: mdiArrowLeft, + forward: mdiArrowRight, + check: mdiCheck, + close: mdiClose, + down: mdiChevronDown, + search: mdiMagnify, + settings: mdiCog, + lock: mdiLock, + ai: mdiRobot, + }, +}) + +export function createIconPlugin () { + return createPlugin({ + namespace: 'v0:icons', + provide: (app: App) => { + provideIconContext(context, app) + }, + }) +} diff --git a/apps/builder/src/plugins/pinia.ts b/apps/builder/src/plugins/pinia.ts new file mode 100644 index 000000000..153625250 --- /dev/null +++ b/apps/builder/src/plugins/pinia.ts @@ -0,0 +1,4 @@ +// Utilities +import { createPinia } from 'pinia' + +export default createPinia() diff --git a/apps/builder/src/typed-router.d.ts b/apps/builder/src/typed-router.d.ts new file mode 100644 index 000000000..ed57a533a --- /dev/null +++ b/apps/builder/src/typed-router.d.ts @@ -0,0 +1,70 @@ +/* eslint-disable */ +/* prettier-ignore */ +// oxfmt-ignore +// @ts-nocheck +// noinspection ES6UnusedImports +// Generated by vue-router. !! DO NOT MODIFY THIS FILE !! +// It's recommended to commit this file. +// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry. + +import type { + RouteRecordInfo, + ParamValue, + ParamValueOneOrMore, + ParamValueZeroOrMore, + ParamValueZeroOrOne, +} from 'vue-router' + +declare module 'vue-router' { + interface TypesConfig { + ParamParsers: + | never + } +} + +declare module 'vue-router/auto-routes' { + /** + * Route name map generated by vue-router + */ + export interface RouteNamedMap { + '/': RouteRecordInfo< + '/', + '/', + Record, + Record, + | never + >, + } + + /** + * Route file to route info map by vue-router. + * Used by the \`sfc-typed-router\` Volar plugin to automatically type \`useRoute()\`. + * + * Each key is a file path relative to the project root with 2 properties: + * - routes: union of route names of the possible routes when in this page (passed to useRoute<...>()) + * - views: names of nested views (can be passed to ) + * + * @internal + */ + export interface _RouteFileInfoMap { + 'src/pages/index.vue': { + routes: + | '/' + views: + | never + } + } + + /** + * Get a union of possible route names in a certain route component file. + * Used by the \`sfc-typed-router\` Volar plugin to automatically type \`useRoute()\`. + * + * @internal + */ + export type _RouteNamesForFilePath = + _RouteFileInfoMap extends Record + ? Info['routes'] + : keyof RouteNamedMap +} + +export {} diff --git a/apps/builder/src/vite-env.d.ts b/apps/builder/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/apps/builder/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/builder/uno.config.ts b/apps/builder/uno.config.ts new file mode 100644 index 000000000..66fbd8616 --- /dev/null +++ b/apps/builder/uno.config.ts @@ -0,0 +1,64 @@ +import { defineConfig, presetWind4 } from 'unocss' + +export default defineConfig({ + presets: [presetWind4()], + shortcuts: { + 'fade-interactive': 'opacity-50 hover:opacity-80 focus-visible:opacity-80 transition-opacity', + 'bg-glass-surface': '[background:var(--v0-glass-surface)] backdrop-blur-12', + }, + preflights: [ + { + getCSS: () => ` + button:not(:disabled), + [role="button"]:not(:disabled) { + cursor: pointer; + } + + *:focus-visible { + outline: 2px solid var(--v0-primary); + outline-offset: 2px; + } + + @media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } + } + `, + }, + ], + theme: { + colors: { + 'primary': 'var(--v0-primary)', + 'secondary': 'var(--v0-secondary)', + 'accent': 'var(--v0-accent)', + 'error': 'var(--v0-error)', + 'info': 'var(--v0-info)', + 'success': 'var(--v0-success)', + 'warning': 'var(--v0-warning)', + 'background': 'var(--v0-background)', + 'surface': 'var(--v0-surface)', + 'surface-tint': 'var(--v0-surface-tint)', + 'surface-variant': 'var(--v0-surface-variant)', + 'divider': 'var(--v0-divider)', + 'pre': 'var(--v0-pre)', + 'on-primary': 'var(--v0-on-primary)', + 'on-secondary': 'var(--v0-on-secondary)', + 'on-accent': 'var(--v0-on-accent)', + 'on-error': 'var(--v0-on-error)', + 'on-info': 'var(--v0-on-info)', + 'on-success': 'var(--v0-on-success)', + 'on-warning': 'var(--v0-on-warning)', + 'on-background': 'var(--v0-on-background)', + 'on-surface': 'var(--v0-on-surface)', + 'on-surface-variant': 'var(--v0-on-surface-variant)', + }, + borderColor: { + DEFAULT: 'var(--v0-divider)', + }, + }, +}) diff --git a/apps/builder/vite.config.ts b/apps/builder/vite.config.ts new file mode 100644 index 000000000..f83543757 --- /dev/null +++ b/apps/builder/vite.config.ts @@ -0,0 +1,44 @@ +import { fileURLToPath, URL } from 'node:url' + +import UnocssVitePlugin from 'unocss/vite' +import Components from 'unplugin-vue-components/vite' +import Vue from 'unplugin-vue/rolldown' +import { defineConfig } from 'vite' +import Layouts from 'vite-plugin-vue-layouts-next' +import VueRouter from 'vue-router/vite' + +export default defineConfig({ + plugins: [ + VueRouter({ + dts: './src/typed-router.d.ts', + }), + Vue(), + Components({ + dirs: ['src/components'], + extensions: ['vue'], + include: [/\.vue$/, /\.vue\?vue/], + dts: './src/components.d.ts', + }), + UnocssVitePlugin(), + Layouts(), + ], + define: { + 'process.env': {}, + '__DEV__': process.env.NODE_ENV !== 'production', + '__VUE_OPTIONS_API__': 'true', + '__VUE_PROD_DEVTOOLS__': 'false', + '__VUE_PROD_HYDRATION_MISMATCH_DETAILS__': 'false', + }, + resolve: { + alias: { + '@': fileURLToPath(new URL('src', import.meta.url)), + '@vuetify/v0': fileURLToPath(new URL('../../packages/0/src', import.meta.url)), + '#v0': fileURLToPath(new URL('../../packages/0/src', import.meta.url)), + }, + }, + server: { + fs: { + allow: ['../../packages/*', '../../node_modules', '.'], + }, + }, +}) diff --git a/package.json b/package.json index 5c9d679a1..abb55a589 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "build:dev": "pnpm --filter=dev build", "build:play": "pnpm --filter=@vuetify-private/playground build", "build:docs": "pnpm --filter=docs build", - "build:apps": "pnpm --filter=docs --filter=dev --filter=@vuetify-private/playground build", + "build:apps": "pnpm --filter=docs --filter=dev --filter=@vuetify-private/playground --filter=@vuetify-private/builder build", "build:all": "pnpm --filter=\"./packages/*\" --filter=docs --filter=dev --filter=@vuetify-private/playground build", "test": "vitest", "test:run": "vitest run", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a97f28389..4a87ab902 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -314,6 +314,46 @@ importers: specifier: 'catalog:' version: 3.2.7(typescript@6.0.3) + apps/builder: + dependencies: + '@mdi/js': + specifier: 'catalog:' + version: mdi-js-es@7.4.47 + '@vuetify/auth': + specifier: 'catalog:' + version: 0.1.8(pinia@3.0.4(typescript@6.0.2)(vue@3.5.31(typescript@6.0.2)))(vue@3.5.31(typescript@6.0.2)) + '@vuetify/v0': + specifier: workspace:* + version: link:../../packages/0 + fflate: + specifier: 'catalog:' + version: 0.8.2 + pinia: + specifier: 'catalog:' + version: 3.0.4(typescript@6.0.2)(vue@3.5.31(typescript@6.0.2)) + vue: + specifier: 'catalog:' + version: 3.5.31(typescript@6.0.2) + devDependencies: + unocss: + specifier: 'catalog:' + version: 66.6.7(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(vite@8.0.3(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + unplugin-vue: + specifier: 'catalog:' + version: 7.1.1(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(vue@3.5.31(typescript@6.0.2))(yaml@2.8.3) + unplugin-vue-components: + specifier: 'catalog:' + version: 32.0.0(vue@3.5.31(typescript@6.0.2)) + vite: + specifier: 'catalog:' + version: 8.0.3(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite-plugin-vue-layouts-next: + specifier: 'catalog:' + version: 2.1.0(vite@8.0.3(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@vue/compiler-sfc@3.5.31)(pinia@3.0.4(typescript@6.0.2)(vue@3.5.31(typescript@6.0.2)))(vue@3.5.31(typescript@6.0.2)))(vue@3.5.31(typescript@6.0.2)) + vue-router: + specifier: 'catalog:' + version: 5.0.4(@vue/compiler-sfc@3.5.31)(pinia@3.0.4(typescript@6.0.2)(vue@3.5.31(typescript@6.0.2)))(vue@3.5.31(typescript@6.0.2)) + apps/docs: dependencies: '@js-temporal/polyfill': From ca6d25f4b6f2150ad6d64c97af1d33c06ad49e05 Mon Sep 17 00:00:00 2001 From: John Leider Date: Sat, 4 Apr 2026 13:31:43 -0500 Subject: [PATCH 02/56] chore(builder): add tsconfig files matching playground pattern --- apps/builder/tsconfig.app.json | 36 +++++++++++++++++++++++++++++++++ apps/builder/tsconfig.json | 7 +++++++ apps/builder/tsconfig.node.json | 17 ++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 apps/builder/tsconfig.app.json create mode 100644 apps/builder/tsconfig.json create mode 100644 apps/builder/tsconfig.node.json diff --git a/apps/builder/tsconfig.app.json b/apps/builder/tsconfig.app.json new file mode 100644 index 000000000..b90448dbd --- /dev/null +++ b/apps/builder/tsconfig.app.json @@ -0,0 +1,36 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "composite": true, + "incremental": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "customConditions": ["development"], + "moduleResolution": "bundler", + "paths": { + "@/*": ["./src/*"], + "#v0": ["../../packages/0/src"], + "#v0/*": ["../../packages/0/src/*"] + }, + "lib": ["dom", "dom.iterable", "esnext"], + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "assumeChangesOnlyAffectDirectDependencies": true, + "verbatimModuleSyntax": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "target": "esnext" + }, + "include": [ + "src/**/*.ts", + "src/**/*.vue" + ], + "exclude": [ + "src/**/__tests__/*" + ], + "references": [ + { "path": "../../packages/0" } + ] +} diff --git a/apps/builder/tsconfig.json b/apps/builder/tsconfig.json new file mode 100644 index 000000000..ba5ccc4a2 --- /dev/null +++ b/apps/builder/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.app.json" } + ] +} diff --git a/apps/builder/tsconfig.node.json b/apps/builder/tsconfig.node.json new file mode 100644 index 000000000..52636e359 --- /dev/null +++ b/apps/builder/tsconfig.node.json @@ -0,0 +1,17 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*" + ], + "compilerOptions": { + "composite": true, + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "module": "ESNext", + "moduleResolution": "bundler", + "types": ["node"], + "target": "esnext", + "lib": ["esnext"] + } +} From e779602f3b7e31ab2a84711f2634edbe916f0ebb Mon Sep 17 00:00:00 2001 From: John Leider Date: Sat, 4 Apr 2026 13:34:16 -0500 Subject: [PATCH 03/56] chore(builder): add dependency graph generator Scans v0 composable and component imports to produce dependencies.json for the framework builder's resolution engine. --- apps/builder/build/generate-dependencies.ts | 70 +++ apps/builder/package.json | 6 +- apps/builder/src/data/dependencies.json | 446 ++++++++++++++++++++ apps/builder/src/data/types.ts | 47 +++ apps/builder/src/engine/resolve.test.ts | 64 +++ apps/builder/src/engine/resolve.ts | 43 ++ apps/builder/vitest.config.ts | 16 + pnpm-lock.yaml | 3 + vitest.config.ts | 2 +- 9 files changed, 694 insertions(+), 3 deletions(-) create mode 100644 apps/builder/build/generate-dependencies.ts create mode 100644 apps/builder/src/data/dependencies.json create mode 100644 apps/builder/src/data/types.ts create mode 100644 apps/builder/src/engine/resolve.test.ts create mode 100644 apps/builder/src/engine/resolve.ts create mode 100644 apps/builder/vitest.config.ts diff --git a/apps/builder/build/generate-dependencies.ts b/apps/builder/build/generate-dependencies.ts new file mode 100644 index 000000000..a0e4841f1 --- /dev/null +++ b/apps/builder/build/generate-dependencies.ts @@ -0,0 +1,70 @@ +import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const ROOT = resolve(__dirname, '../../..') +const V0_SRC = resolve(ROOT, 'packages/0/src') + +interface DependencyGraph { + composables: Record + components: Record +} + +function extractV0Imports (filePath: string): string[] { + let content: string + try { + content = readFileSync(filePath, 'utf-8') + } catch { + return [] + } + + const imports: string[] = [] + const pattern = /from\s+['"]#v0\/(composables|components)\/(\w+)['"]/g + let match: RegExpExecArray | null + + while ((match = pattern.exec(content)) !== null) { + imports.push(match[2]) + } + + return [...new Set(imports)] +} + +function scanDirectory (dir: string): Record { + const entries = readdirSync(dir) + const graph: Record = {} + + for (const entry of entries) { + const entryPath = resolve(dir, entry) + if (!statSync(entryPath).isDirectory()) continue + + const indexPath = resolve(entryPath, 'index.ts') + const deps = extractV0Imports(indexPath) + + // Also scan .vue files and non-index .ts files + try { + const files = readdirSync(entryPath) + for (const file of files) { + if (file.endsWith('.vue') || (file.endsWith('.ts') && file !== 'index.ts')) { + deps.push(...extractV0Imports(resolve(entryPath, file))) + } + } + } catch { /* empty */ } + + graph[entry] = [...new Set(deps)].filter(d => d !== entry).sort() + } + + return graph +} + +const graph: DependencyGraph = { + composables: scanDirectory(resolve(V0_SRC, 'composables')), + components: scanDirectory(resolve(V0_SRC, 'components')), +} + +const outPath = resolve(__dirname, '../src/data/dependencies.json') +writeFileSync(outPath, JSON.stringify(graph, null, 2) + '\n') + +console.log( + `Generated dependency graph: ${Object.keys(graph.composables).length} composables, ${Object.keys(graph.components).length} components`, +) diff --git a/apps/builder/package.json b/apps/builder/package.json index f374df16a..98b88b14b 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -4,8 +4,9 @@ "private": true, "type": "module", "scripts": { - "dev": "vite", - "build": "vite build", + "generate": "tsx build/generate-dependencies.ts", + "dev": "pnpm generate && vite", + "build": "pnpm generate && vite build", "preview": "vite preview" }, "dependencies": { @@ -17,6 +18,7 @@ "vue": "catalog:" }, "devDependencies": { + "tsx": "catalog:", "unocss": "catalog:", "unplugin-vue": "catalog:", "unplugin-vue-components": "catalog:", diff --git a/apps/builder/src/data/dependencies.json b/apps/builder/src/data/dependencies.json new file mode 100644 index 000000000..c6e9e76c6 --- /dev/null +++ b/apps/builder/src/data/dependencies.json @@ -0,0 +1,446 @@ +{ + "composables": { + "createBreadcrumbs": [ + "createContext", + "createSingle", + "createTrinity" + ], + "createCombobox": [ + "createContext", + "createSelection", + "createTrinity", + "toArray", + "usePopover", + "useVirtualFocus" + ], + "createContext": [], + "createDataTable": [ + "createContext", + "createFilter", + "createGroup", + "createPagination", + "createTrinity", + "useLocale" + ], + "createFilter": [ + "createContext", + "createTrinity", + "toArray" + ], + "createFocusTraversal": [], + "createForm": [ + "createContext", + "createRegistry", + "createTrinity", + "createValidation", + "toArray" + ], + "createGroup": [ + "createContext", + "createSelection", + "createTrinity", + "toArray", + "useProxyRegistry" + ], + "createModel": [ + "createRegistry" + ], + "createNested": [ + "createContext", + "createGroup", + "createTrinity", + "toArray", + "useLogger" + ], + "createObserver": [ + "toElement", + "useHydration" + ], + "createOverflow": [ + "createContext", + "createTrinity", + "useResizeObserver" + ], + "createPagination": [ + "createContext", + "createTrinity" + ], + "createPlugin": [ + "createContext", + "createTrinity", + "useStorage" + ], + "createQueue": [ + "createContext", + "createRegistry", + "createTrinity", + "useTimer" + ], + "createRating": [ + "createContext", + "createTrinity" + ], + "createRegistry": [ + "createContext", + "createTrinity", + "useLogger" + ], + "createSelection": [ + "createContext", + "createModel", + "createTrinity" + ], + "createSingle": [ + "createContext", + "createSelection", + "createTrinity" + ], + "createSlider": [ + "createModel" + ], + "createStep": [ + "createContext", + "createSingle", + "createTrinity" + ], + "createTimeline": [ + "createContext", + "createRegistry", + "createTrinity" + ], + "createTokens": [ + "createContext", + "createRegistry", + "createTrinity", + "useLogger" + ], + "createTrinity": [], + "createValidation": [ + "createForm", + "createGroup", + "useRules" + ], + "createVirtual": [ + "createContext", + "createTrinity", + "useResizeObserver" + ], + "toArray": [], + "toElement": [], + "toReactive": [], + "useBreakpoints": [ + "createPlugin", + "useEventListener", + "useHydration" + ], + "useClickOutside": [ + "toArray", + "useEventListener" + ], + "useDate": [ + "createContext", + "createPlugin", + "createTrinity", + "useLocale" + ], + "useEventListener": [], + "useFeatures": [ + "createGroup", + "createPlugin", + "createRegistry", + "createTokens" + ], + "useHotkey": [ + "useEventListener", + "useLogger" + ], + "useHydration": [ + "createPlugin" + ], + "useIntersectionObserver": [ + "createObserver", + "toElement" + ], + "useLazy": [ + "useTimer" + ], + "useLocale": [ + "createPlugin", + "createSingle", + "createTokens" + ], + "useLogger": [ + "createPlugin" + ], + "useMediaQuery": [ + "useHydration" + ], + "useMutationObserver": [ + "createObserver", + "toElement" + ], + "useNotifications": [ + "createPlugin", + "createQueue", + "createRegistry" + ], + "usePermissions": [ + "createPlugin", + "createTokens", + "toArray" + ], + "usePopover": [ + "useEventListener", + "useTimer" + ], + "usePresence": [], + "useProxyModel": [ + "createSelection", + "toArray" + ], + "useProxyRegistry": [ + "createRegistry" + ], + "useRaf": [], + "useResizeObserver": [ + "createObserver", + "toElement" + ], + "useRovingFocus": [ + "createFocusTraversal", + "useEventListener" + ], + "useRtl": [ + "createPlugin" + ], + "useRules": [ + "createForm", + "createPlugin", + "useLocale" + ], + "useStack": [ + "createContext", + "createPlugin", + "createSelection", + "createTrinity" + ], + "useStorage": [ + "createPlugin", + "useEventListener" + ], + "useTheme": [ + "createPlugin", + "createRegistry", + "createSingle", + "createTokens" + ], + "useTimer": [], + "useToggleScope": [], + "useVirtualFocus": [ + "createFocusTraversal", + "useEventListener" + ] + }, + "components": { + "AlertDialog": [ + "Atom", + "createContext", + "useClickOutside", + "useStack", + "useToggleScope" + ], + "Atom": [], + "Avatar": [ + "Atom", + "createContext", + "createSelection" + ], + "Breadcrumbs": [ + "Atom", + "createBreadcrumbs", + "createContext", + "createGroup", + "createOverflow", + "useLocale" + ], + "Button": [ + "Atom", + "createContext", + "createSelection", + "createSingle", + "useLocale", + "useProxyModel", + "useTimer" + ], + "Checkbox": [ + "Atom", + "createContext", + "createGroup", + "useProxyModel" + ], + "Collapsible": [ + "Atom", + "createContext", + "createSingle", + "useProxyModel" + ], + "Combobox": [ + "Atom", + "createCombobox", + "createContext", + "useClickOutside", + "useLazy", + "useProxyModel" + ], + "Dialog": [ + "Atom", + "createContext", + "useClickOutside", + "useStack", + "useToggleScope" + ], + "ExpansionPanel": [ + "Atom", + "createContext", + "createSelection", + "useProxyModel" + ], + "Form": [ + "Atom", + "createContext", + "createForm", + "createValidation" + ], + "Group": [ + "createContext", + "createGroup", + "useProxyModel" + ], + "Input": [ + "Atom", + "createContext", + "createForm", + "createValidation", + "toArray", + "useRules" + ], + "Locale": [ + "createContext", + "useLocale" + ], + "Pagination": [ + "Atom", + "createContext", + "createOverflow", + "createPagination", + "createRegistry", + "useLocale" + ], + "Popover": [ + "Atom", + "createContext", + "usePopover" + ], + "Portal": [ + "useStack" + ], + "Presence": [ + "usePresence" + ], + "Radio": [ + "Atom", + "createContext", + "createSingle", + "useProxyModel" + ], + "Rating": [ + "Atom", + "createContext", + "createRating" + ], + "Scrim": [ + "Atom", + "useStack" + ], + "Select": [ + "Atom", + "createContext", + "createSelection", + "useLazy", + "usePopover", + "useProxyModel", + "useVirtualFocus" + ], + "Selection": [ + "createContext", + "createSelection", + "useProxyModel" + ], + "Single": [ + "createContext", + "createSingle", + "useProxyModel" + ], + "Slider": [ + "Atom", + "createContext", + "createSlider", + "useEventListener", + "useProxyModel", + "useToggleScope" + ], + "Snackbar": [ + "Atom", + "Portal", + "createContext", + "useNotifications", + "useStack" + ], + "Splitter": [ + "Atom", + "createContext", + "createRegistry", + "createSelection", + "useEventListener", + "useRaf", + "useResizeObserver", + "useToggleScope" + ], + "Step": [ + "createContext", + "createStep", + "useProxyModel" + ], + "Switch": [ + "Atom", + "createContext", + "createGroup", + "useProxyModel" + ], + "Tabs": [ + "Atom", + "createContext", + "createStep", + "useProxyModel" + ], + "Theme": [ + "Atom", + "createContext", + "useTheme" + ], + "Toggle": [ + "Atom", + "createContext", + "createGroup", + "createSingle", + "useProxyModel" + ], + "Treeview": [ + "Atom", + "createContext", + "createNested", + "useProxyModel", + "useRovingFocus" + ] + } +} diff --git a/apps/builder/src/data/types.ts b/apps/builder/src/data/types.ts new file mode 100644 index 000000000..d2395925c --- /dev/null +++ b/apps/builder/src/data/types.ts @@ -0,0 +1,47 @@ +export interface Feature { + id: string + type: 'composable' | 'component' | 'adapter' + category: string + maturity: 'draft' | 'preview' | 'stable' + since: string + name: string + summary: string + useCases: string[] + dependencies: string[] + tags: string[] + icon?: string +} + +export interface FeatureMeta { + name: string + summary: string + useCases: string[] + tags: string[] + icon?: string +} + +export interface DependencyGraph { + composables: Record + components: Record +} + +export interface ResolvedSet { + selected: string[] + autoIncluded: string[] + warnings: Warning[] +} + +export interface Warning { + featureId: string + type: 'draft' | 'missing' + message: string +} + +export type Intent = 'spa' | 'component-library' | 'design-system' | 'admin-dashboard' | 'content-site' | 'mobile-first' + +export interface FrameworkManifest { + intent?: string + features: string[] + resolved: string[] + adapters: Record +} diff --git a/apps/builder/src/engine/resolve.test.ts b/apps/builder/src/engine/resolve.test.ts new file mode 100644 index 000000000..d503f930b --- /dev/null +++ b/apps/builder/src/engine/resolve.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from 'vitest' + +// Types +import type { DependencyGraph } from '@/data/types' + +import { resolve } from './resolve' + +const graph: DependencyGraph = { + composables: { + createContext: [], + createTrinity: [], + createModel: ['createContext'], + createSelection: ['createContext', 'createModel', 'createTrinity'], + createSingle: ['createContext', 'createSelection', 'createTrinity'], + createStep: ['createContext', 'createSingle', 'createTrinity'], + }, + components: {}, +} + +describe('resolve', () => { + it('returns empty for empty selection', () => { + const result = resolve([], graph) + expect(result.selected).toEqual([]) + expect(result.autoIncluded).toEqual([]) + expect(result.warnings).toEqual([]) + }) + + it('selects a feature with no dependencies', () => { + const result = resolve(['createContext'], graph) + expect(result.selected).toEqual(['createContext']) + expect(result.autoIncluded).toEqual([]) + }) + + it('auto-includes transitive dependencies', () => { + const result = resolve(['createSelection'], graph) + expect(result.selected).toEqual(['createSelection']) + expect(result.autoIncluded.sort()).toEqual(['createContext', 'createModel', 'createTrinity']) + }) + + it('does not duplicate features in selected and autoIncluded', () => { + const result = resolve(['createSelection', 'createContext'], graph) + expect(result.selected.sort()).toEqual(['createContext', 'createSelection']) + expect(result.autoIncluded.sort()).toEqual(['createModel', 'createTrinity']) + }) + + it('resolves deep transitive chains', () => { + const result = resolve(['createStep'], graph) + expect(result.selected).toEqual(['createStep']) + expect(result.autoIncluded.sort()).toEqual([ + 'createContext', + 'createModel', + 'createSelection', + 'createSingle', + 'createTrinity', + ]) + }) + + it('warns for features not in the graph', () => { + const result = resolve(['nonExistent'], graph) + expect(result.warnings).toEqual([ + { featureId: 'nonExistent', type: 'missing', message: 'Feature "nonExistent" not found in dependency graph' }, + ]) + }) +}) diff --git a/apps/builder/src/engine/resolve.ts b/apps/builder/src/engine/resolve.ts new file mode 100644 index 000000000..453c9d4d2 --- /dev/null +++ b/apps/builder/src/engine/resolve.ts @@ -0,0 +1,43 @@ +// Types +import type { DependencyGraph, ResolvedSet, Warning } from '@/data/types' + +export function resolve (selected: string[], graph: DependencyGraph): ResolvedSet { + const selectedSet = new Set(selected) + const allDeps = new Set() + const warnings: Warning[] = [] + + const allFeatures = { ...graph.composables, ...graph.components } + + function walk (id: string) { + if (allDeps.has(id)) return + + const deps = allFeatures[id] + if (!deps) { + warnings.push({ + featureId: id, + type: 'missing', + message: `Feature "${id}" not found in dependency graph`, + }) + return + } + + allDeps.add(id) + for (const dep of deps) { + walk(dep) + } + } + + for (const id of selected) { + walk(id) + } + + const autoIncluded = [...allDeps] + .filter(id => !selectedSet.has(id)) + .sort() + + return { + selected: [...selected], + autoIncluded, + warnings, + } +} diff --git a/apps/builder/vitest.config.ts b/apps/builder/vitest.config.ts new file mode 100644 index 000000000..0df66bea6 --- /dev/null +++ b/apps/builder/vitest.config.ts @@ -0,0 +1,16 @@ +import { fileURLToPath } from 'node:url' + +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + resolve: { + alias: { + '@': fileURLToPath(new URL('src', import.meta.url)), + }, + }, + test: { + globals: true, + include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + testTimeout: 20_000, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a87ab902..7b0f19eb8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -335,6 +335,9 @@ importers: specifier: 'catalog:' version: 3.5.31(typescript@6.0.2) devDependencies: + tsx: + specifier: 'catalog:' + version: 4.21.0 unocss: specifier: 'catalog:' version: 66.6.7(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(vite@8.0.3(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) diff --git a/vitest.config.ts b/vitest.config.ts index 51285ae6f..d4035a51c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { - projects: ['packages/*', 'apps/docs'], + projects: ['packages/*', 'apps/docs', 'apps/builder'], globals: true, include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], testTimeout: 20_000, From 6648dcbfcf19bd133e1ad3b1436c52a8dbf3787c Mon Sep 17 00:00:00 2001 From: John Leider Date: Sat, 4 Apr 2026 13:34:56 -0500 Subject: [PATCH 04/56] chore(builder): tighten vitest config include pattern Scope test discovery to src/ and set explicit node environment. --- apps/builder/vitest.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/builder/vitest.config.ts b/apps/builder/vitest.config.ts index 0df66bea6..f668e056c 100644 --- a/apps/builder/vitest.config.ts +++ b/apps/builder/vitest.config.ts @@ -9,8 +9,8 @@ export default defineConfig({ }, }, test: { + environment: 'node', globals: true, - include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], - testTimeout: 20_000, + include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'], }, }) From 018c03b22a1524afe7a1f295905dbc32b664dafc Mon Sep 17 00:00:00 2001 From: John Leider Date: Sat, 4 Apr 2026 13:35:23 -0500 Subject: [PATCH 05/56] feat(builder): add manifest generation and playground handoff Generates playground-compatible files from a feature manifest and encodes them as a fflate-compressed URL hash. --- apps/builder/src/engine/manifest.test.ts | 46 ++++++++ apps/builder/src/engine/manifest.ts | 136 +++++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 apps/builder/src/engine/manifest.test.ts create mode 100644 apps/builder/src/engine/manifest.ts diff --git a/apps/builder/src/engine/manifest.test.ts b/apps/builder/src/engine/manifest.test.ts new file mode 100644 index 000000000..09fdca6d1 --- /dev/null +++ b/apps/builder/src/engine/manifest.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest' + +import { generateFiles, generateImports } from './manifest' + +describe('generateFiles', () => { + it('generates main.ts with selected plugins', () => { + const files = generateFiles({ + intent: 'spa', + features: ['useTheme', 'createSelection'], + resolved: ['createContext', 'createModel', 'createTrinity'], + adapters: {}, + }) + + expect(files['src/main.ts']).toContain('createThemePlugin') + expect(files['src/App.vue']).toBeDefined() + }) + + it('generates App.vue with a starter template', () => { + const files = generateFiles({ + intent: 'spa', + features: ['createSelection'], + resolved: ['createContext', 'createModel', 'createTrinity'], + adapters: {}, + }) + + expect(files['src/App.vue']).toContain('createSelection') + }) + + it('omits theme plugin when useTheme is not selected', () => { + const files = generateFiles({ + intent: 'spa', + features: ['createSelection'], + resolved: ['createContext', 'createModel', 'createTrinity'], + adapters: {}, + }) + + expect(files['src/main.ts']).not.toContain('createThemePlugin') + }) +}) + +describe('generateImports', () => { + it('includes v0 CDN import', () => { + const imports = generateImports() + expect(imports['@vuetify/v0']).toContain('cdn.jsdelivr.net') + }) +}) diff --git a/apps/builder/src/engine/manifest.ts b/apps/builder/src/engine/manifest.ts new file mode 100644 index 000000000..0de831234 --- /dev/null +++ b/apps/builder/src/engine/manifest.ts @@ -0,0 +1,136 @@ +// Types +import type { FrameworkManifest } from '@/data/types' + +interface PlaygroundHashData { + files: Record + active?: string + imports?: Record + settings?: { + preset?: string + } +} + +const PLUGIN_FEATURES = new Set([ + 'useTheme', + 'useLocale', + 'useStorage', + 'useLogger', + 'useBreakpoints', + 'useFeatures', + 'usePermissions', +]) + +const PLUGIN_SETUP: Record = { + useTheme: `app.use(createThemePlugin({ + default: 'light', + themes: { + light: { dark: false, colors: { primary: '#3b82f6', background: '#ffffff', surface: '#ffffff' } }, + dark: { dark: true, colors: { primary: '#c4b5fd', background: '#121212', surface: '#1a1a1a' } }, + }, +}))`, + useLocale: 'app.use(createLocalePlugin())', + useStorage: 'app.use(createStoragePlugin())', + useLogger: 'app.use(createLoggerPlugin())', + useBreakpoints: 'app.use(createBreakpointsPlugin({ mobileBreakpoint: 768 }))', + useFeatures: 'app.use(createFeaturesPlugin())', + usePermissions: 'app.use(createPermissionsPlugin())', +} + +const PLUGIN_IMPORTS: Record = { + useTheme: 'createThemePlugin', + useLocale: 'createLocalePlugin', + useStorage: 'createStoragePlugin', + useLogger: 'createLoggerPlugin', + useBreakpoints: 'createBreakpointsPlugin', + useFeatures: 'createFeaturesPlugin', + usePermissions: 'createPermissionsPlugin', +} + +export function generateImports (): Record { + return { + '@vuetify/v0': 'https://cdn.jsdelivr.net/npm/@vuetify/v0@latest/dist/index.mjs', + '@vue/devtools-api': 'https://esm.sh/@vue/devtools-api@6', + } +} + +export function generateFiles (manifest: FrameworkManifest): Record { + const allFeatures = [...manifest.features, ...manifest.resolved] + const plugins = allFeatures.filter(f => PLUGIN_FEATURES.has(f)) + const composable = allFeatures.find(f => !PLUGIN_FEATURES.has(f) && f.startsWith('create')) + + const pluginImports = plugins + .map(p => PLUGIN_IMPORTS[p]) + .filter(Boolean) + + const pluginSetups = plugins + .map(p => PLUGIN_SETUP[p]) + .filter(Boolean) + + const mainTs = [ + 'import { createApp } from \'vue\'', + 'import App from \'./App.vue\'', + pluginImports.length > 0 + ? `import { ${pluginImports.join(', ')} } from '@vuetify/v0'` + : '', + 'import \'./uno.config.ts\'', + '', + 'const app = createApp(App)', + ...pluginSetups, + 'app.mount(\'#app\')', + ].filter(Boolean).join('\n') + + const demo = composable + const appVue = demo + ? ` + +` + : `` + + return { + 'src/main.ts': mainTs, + 'src/App.vue': appVue, + } +} + +export function toHashData (manifest: FrameworkManifest): PlaygroundHashData { + return { + files: generateFiles(manifest), + active: 'src/App.vue', + imports: generateImports(), + settings: { + preset: 'default', + }, + } +} + +export async function encodeHash (data: PlaygroundHashData): Promise { + const { strToU8, strFromU8, zlibSync } = await import('fflate') + const buffer = strToU8(JSON.stringify(data)) + const zipped = zlibSync(buffer, { level: 9 }) + const binary = strFromU8(zipped, true) + return btoa(binary) +} + +export async function toPlaygroundUrl (manifest: FrameworkManifest, baseUrl: string): Promise { + const data = toHashData(manifest) + const hash = await encodeHash(data) + return `${baseUrl}#${hash}` +} From ffffd18485a15b3d273a5447acc631f47ec3b74e Mon Sep 17 00:00:00 2001 From: John Leider Date: Sat, 4 Apr 2026 13:38:16 -0500 Subject: [PATCH 06/56] feat(builder): add landing page with mode selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three mode cards — Guided, Free Pick, and AI Builder (gated) — with navigation to their respective routes. --- apps/builder/src/components/ModeCard.vue | 38 ++++++++++++++++++++++ apps/builder/src/pages/index.vue | 41 +++++++++++++++++++++--- 2 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 apps/builder/src/components/ModeCard.vue diff --git a/apps/builder/src/components/ModeCard.vue b/apps/builder/src/components/ModeCard.vue new file mode 100644 index 000000000..833330b8f --- /dev/null +++ b/apps/builder/src/components/ModeCard.vue @@ -0,0 +1,38 @@ + + + diff --git a/apps/builder/src/pages/index.vue b/apps/builder/src/pages/index.vue index 8aa63de90..abfbd4a47 100644 --- a/apps/builder/src/pages/index.vue +++ b/apps/builder/src/pages/index.vue @@ -1,10 +1,43 @@ From ba214a3f363bb540f8ee0296f070050905bb58e5 Mon Sep 17 00:00:00 2001 From: John Leider Date: Sat, 4 Apr 2026 13:38:56 -0500 Subject: [PATCH 07/56] chore(builder): add feature catalog data layer Merges maturity.json, dependency graph, and human-authored metadata into a typed Feature catalog for the builder UI. --- apps/builder/src/data/features.ts | 233 ++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 apps/builder/src/data/features.ts diff --git a/apps/builder/src/data/features.ts b/apps/builder/src/data/features.ts new file mode 100644 index 000000000..ca05218ff --- /dev/null +++ b/apps/builder/src/data/features.ts @@ -0,0 +1,233 @@ +// Types +import type { DependencyGraph, Feature, FeatureMeta } from './types' + +import dependencyGraph from './dependencies.json' + +import maturity from '../../../../packages/0/src/maturity.json' + +const META: Record = { + // Foundation + createContext: { + name: 'Context', + summary: 'Dependency injection with Vue provide/inject', + useCases: ['Component communication', 'Shared state', 'Plugin architecture'], + tags: ['di', 'provide', 'inject'], + }, + createTrinity: { + name: 'Trinity', + summary: 'Structured tuple factory for composable APIs', + useCases: ['Composable design', 'API consistency'], + tags: ['pattern', 'api'], + }, + createPlugin: { + name: 'Plugin', + summary: 'Vue plugin wrapper with context handling', + useCases: ['App-level features', 'Global configuration'], + tags: ['plugin', 'app'], + }, + createRegistry: { + name: 'Registry', + summary: 'Track and manage child component instances', + useCases: ['Tab panels', 'Accordion items', 'Carousel slides'], + tags: ['registration', 'children', 'instances'], + }, + createSelection: { + name: 'Selection', + summary: 'Single and multi-select state management', + useCases: ['Dropdown menus', 'Tab selection', 'List filtering'], + tags: ['select', 'single', 'multi', 'state'], + }, + createSingle: { + name: 'Single Select', + summary: 'Exactly-one selection with mandatory support', + useCases: ['Tabs', 'Radio groups', 'Navigation'], + tags: ['select', 'single', 'mandatory'], + }, + createGroup: { + name: 'Group Select', + summary: 'Multi-select with grouped items', + useCases: ['Checkbox groups', 'Multi-tag selection', 'Filter panels'], + tags: ['select', 'multi', 'group'], + }, + createStep: { + name: 'Stepper', + summary: 'Sequential step navigation', + useCases: ['Wizards', 'Onboarding flows', 'Multi-step forms'], + tags: ['step', 'wizard', 'sequence'], + }, + createModel: { + name: 'Model', + summary: 'Reactive value store for selection state', + useCases: ['Form values', 'Controlled inputs'], + tags: ['model', 'value', 'state'], + }, + createForm: { + name: 'Form', + summary: 'Form state management with validation', + useCases: ['Login forms', 'Settings pages', 'Data entry'], + tags: ['form', 'validation', 'submit'], + }, + createCombobox: { + name: 'Combobox', + summary: 'Autocomplete with keyboard navigation', + useCases: ['Search inputs', 'Tag entry', 'Command palettes'], + tags: ['combobox', 'autocomplete', 'search'], + }, + createSlider: { + name: 'Slider', + summary: 'Range input with thumb control', + useCases: ['Volume controls', 'Price filters', 'Settings'], + tags: ['slider', 'range', 'input'], + }, + createRating: { + name: 'Rating', + summary: 'Star rating input', + useCases: ['Reviews', 'Feedback', 'Scoring'], + tags: ['rating', 'stars', 'input'], + }, + createDataTable: { + name: 'Data Table', + summary: 'Sortable, filterable table with pagination', + useCases: ['Admin dashboards', 'Reports', 'Data management'], + tags: ['table', 'sort', 'filter', 'paginate'], + }, + createFilter: { + name: 'Filter', + summary: 'Client-side data filtering', + useCases: ['Search results', 'List filtering', 'Table columns'], + tags: ['filter', 'search', 'data'], + }, + createPagination: { + name: 'Pagination', + summary: 'Page-based data navigation', + useCases: ['Table pages', 'Gallery pages', 'Search results'], + tags: ['pagination', 'pages', 'navigation'], + }, + createVirtual: { + name: 'Virtual Scroll', + summary: 'Render only visible items in large lists', + useCases: ['Long lists', 'Chat logs', 'Data grids'], + tags: ['virtual', 'scroll', 'performance'], + }, + useTheme: { + name: 'Theme', + summary: 'Light/dark mode with custom color tokens', + useCases: ['Dark mode toggle', 'Brand theming', 'User preferences'], + tags: ['theme', 'dark', 'light', 'colors'], + }, + useLocale: { + name: 'Locale', + summary: 'Internationalization with adapter support', + useCases: ['Multi-language apps', 'RTL support', 'Date formatting'], + tags: ['i18n', 'locale', 'translation'], + }, + useStorage: { + name: 'Storage', + summary: 'Persistent state with localStorage/sessionStorage', + useCases: ['User preferences', 'Draft saving', 'Cache'], + tags: ['storage', 'persist', 'local'], + }, + useFeatures: { + name: 'Feature Flags', + summary: 'Boolean feature flags with adapter support', + useCases: ['A/B testing', 'Progressive rollout', 'Beta features'], + tags: ['features', 'flags', 'toggle'], + }, + useLogger: { + name: 'Logger', + summary: 'Structured logging with adapter support', + useCases: ['Debug output', 'Error tracking', 'Analytics'], + tags: ['logging', 'debug', 'console'], + }, + usePermissions: { + name: 'Permissions', + summary: 'Role-based access control', + useCases: ['Admin panels', 'Feature gating', 'User roles'], + tags: ['permissions', 'rbac', 'access'], + }, + useBreakpoints: { + name: 'Breakpoints', + summary: 'Reactive viewport breakpoints', + useCases: ['Responsive layouts', 'Mobile detection', 'Adaptive UI'], + tags: ['responsive', 'viewport', 'mobile'], + }, + useDate: { + name: 'Date', + summary: 'Date manipulation with adapter support', + useCases: ['Date pickers', 'Calendars', 'Time formatting'], + tags: ['date', 'time', 'calendar'], + }, + useEventListener: { + name: 'Event Listener', + summary: 'Auto-cleanup event listener binding', + useCases: ['Keyboard shortcuts', 'Scroll handlers', 'Window events'], + tags: ['events', 'listener', 'cleanup'], + }, + useHotkey: { + name: 'Hotkey', + summary: 'Keyboard shortcut registration', + useCases: ['App shortcuts', 'Accessibility', 'Power user features'], + tags: ['keyboard', 'shortcut', 'hotkey'], + }, + useClickOutside: { + name: 'Click Outside', + summary: 'Detect clicks outside an element', + useCases: ['Dropdown close', 'Modal dismiss', 'Menu collapse'], + tags: ['click', 'outside', 'dismiss'], + }, + usePopover: { + name: 'Popover', + summary: 'Floating UI positioning and visibility', + useCases: ['Tooltips', 'Dropdowns', 'Context menus'], + tags: ['popover', 'float', 'position'], + }, + useStack: { + name: 'Stack', + summary: 'Z-index stacking order for overlays', + useCases: ['Modals', 'Dialogs', 'Drawers'], + tags: ['stack', 'zindex', 'overlay'], + }, + useResizeObserver: { + name: 'Resize Observer', + summary: 'Reactive element size tracking', + useCases: ['Responsive components', 'Chart resizing', 'Layout shifts'], + tags: ['resize', 'observer', 'size'], + }, + useIntersectionObserver: { + name: 'Intersection Observer', + summary: 'Detect element visibility in viewport', + useCases: ['Lazy loading', 'Infinite scroll', 'Analytics'], + tags: ['intersection', 'visibility', 'lazy'], + }, +} + +const graph = dependencyGraph as DependencyGraph + +export function buildCatalog (): Feature[] { + const features: Feature[] = [] + + for (const [id, meta] of Object.entries(META)) { + const composable = (maturity.composables as Record)[id] + const component = (maturity.components as Record)[id] + const entry = composable ?? component + + if (!entry) continue + + const type = composable ? 'composable' : 'component' + const deps = type === 'composable' + ? (graph.composables[id] ?? []) + : (graph.components[id] ?? []) + + features.push({ + id, + type, + category: entry.category, + maturity: entry.level as Feature['maturity'], + since: entry.since ?? '', + dependencies: deps, + ...meta, + }) + } + + return features +} From e8062ede2e47708a291f0e16b17248190397053b Mon Sep 17 00:00:00 2001 From: John Leider Date: Sat, 4 Apr 2026 13:41:28 -0500 Subject: [PATCH 08/56] feat(builder): add builder store Pinia store managing intent, feature selection, dependency resolution, and playground handoff for all builder modes. --- apps/builder/src/stores/builder.ts | 115 +++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 apps/builder/src/stores/builder.ts diff --git a/apps/builder/src/stores/builder.ts b/apps/builder/src/stores/builder.ts new file mode 100644 index 000000000..a504ea285 --- /dev/null +++ b/apps/builder/src/stores/builder.ts @@ -0,0 +1,115 @@ +// Utilities +import { defineStore } from 'pinia' +import { computed, shallowRef } from 'vue' + +// Types +import type { DependencyGraph, Feature, Intent, ResolvedSet } from '@/data/types' + +import dependencyGraph from '@/data/dependencies.json' +import { buildCatalog } from '@/data/features' +import { toPlaygroundUrl } from '@/engine/manifest' +import { resolve } from '@/engine/resolve' + +export const useBuilderStore = defineStore('builder', () => { + const catalog = buildCatalog() + const graph = dependencyGraph as DependencyGraph + + // State + const intent = shallowRef(null) + const selected = shallowRef(new Set()) + const mode = shallowRef<'guided' | 'free'>('guided') + const step = shallowRef(0) + + // Derived + const resolved = computed(() => { + return resolve([...selected.value], graph) + }) + + const categories = computed(() => { + const cats = new Map() + for (const feature of catalog) { + const list = cats.get(feature.category) ?? [] + list.push(feature) + cats.set(feature.category, list) + } + return cats + }) + + // Actions + function toggle (id: string) { + const next = new Set(selected.value) + if (next.has(id)) { + next.delete(id) + } else { + next.add(id) + } + selected.value = next + } + + function select (id: string) { + const next = new Set(selected.value) + next.add(id) + selected.value = next + } + + function deselect (id: string) { + const next = new Set(selected.value) + next.delete(id) + selected.value = next + } + + function reset () { + intent.value = null + selected.value = new Set() + step.value = 0 + } + + function setIntent (value: Intent) { + intent.value = value + selected.value = new Set(getRecommendations(value)) + } + + async function openInPlayground () { + const url = await toPlaygroundUrl( + { + intent: intent.value ?? undefined, + features: [...selected.value], + resolved: resolved.value.autoIncluded, + adapters: {}, + }, + 'https://play.vuetifyjs.com', + ) + window.open(url, '_blank') + } + + return { + catalog, + intent, + selected, + mode, + step, + resolved, + categories, + toggle, + select, + deselect, + reset, + setIntent, + openInPlayground, + } +}) + +function getRecommendations (intent: Intent): string[] { + const base = ['createContext', 'createTrinity', 'useTheme'] + + const presets: Record = { + 'spa': [...base, 'createSelection', 'createSingle', 'useStorage', 'useBreakpoints'], + 'component-library': [...base, 'createRegistry', 'createSelection', 'createGroup'], + 'design-system': [...base, 'createRegistry', 'createSelection', 'createGroup', 'useLocale', 'useBreakpoints'], + 'admin-dashboard': [...base, 'createDataTable', 'createForm', 'createSelection', 'useStorage', 'useBreakpoints', 'usePermissions'], + 'content-site': [...base, 'useBreakpoints', 'useIntersectionObserver', 'useStorage'], + 'mobile-first': [...base, 'createSelection', 'useBreakpoints', 'useStorage', 'useEventListener'], + } + + return presets[intent] ?? base +} From ff9542614bba5fb7708c2d5e732217ec59d98d43 Mon Sep 17 00:00:00 2001 From: John Leider Date: Sat, 4 Apr 2026 13:42:05 -0500 Subject: [PATCH 09/56] fix(builder): fix maturity.json type cast for null since values --- apps/builder/src/data/features.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/builder/src/data/features.ts b/apps/builder/src/data/features.ts index ca05218ff..1b15a955c 100644 --- a/apps/builder/src/data/features.ts +++ b/apps/builder/src/data/features.ts @@ -207,8 +207,8 @@ export function buildCatalog (): Feature[] { const features: Feature[] = [] for (const [id, meta] of Object.entries(META)) { - const composable = (maturity.composables as Record)[id] - const component = (maturity.components as Record)[id] + const composable = (maturity.composables as unknown as Record)[id] + const component = (maturity.components as unknown as Record)[id] const entry = composable ?? component if (!entry) continue From b6448a3e4f1a770572a0e34fa485cda73960522d Mon Sep 17 00:00:00 2001 From: John Leider Date: Sat, 4 Apr 2026 13:44:16 -0500 Subject: [PATCH 10/56] feat(builder): add IntentCard and FeatureCard components --- apps/builder/src/components/FeatureCard.vue | 66 +++++++++++++++++++++ apps/builder/src/components/IntentCard.vue | 21 +++++++ 2 files changed, 87 insertions(+) create mode 100644 apps/builder/src/components/FeatureCard.vue create mode 100644 apps/builder/src/components/IntentCard.vue diff --git a/apps/builder/src/components/FeatureCard.vue b/apps/builder/src/components/FeatureCard.vue new file mode 100644 index 000000000..7e88ccd48 --- /dev/null +++ b/apps/builder/src/components/FeatureCard.vue @@ -0,0 +1,66 @@ + + + diff --git a/apps/builder/src/components/IntentCard.vue b/apps/builder/src/components/IntentCard.vue new file mode 100644 index 000000000..86b779ec6 --- /dev/null +++ b/apps/builder/src/components/IntentCard.vue @@ -0,0 +1,21 @@ + + + From ead23e3beb360fe0860da0c8ba82a9baaa7d525b Mon Sep 17 00:00:00 2001 From: John Leider Date: Sat, 4 Apr 2026 13:44:28 -0500 Subject: [PATCH 11/56] feat(builder): add guided mode with intent and category steps Stepper wizard with intent selection that seeds recommended features, plus category walkthrough with feature cards. --- apps/builder/src/pages/guided.vue | 161 ++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 apps/builder/src/pages/guided.vue diff --git a/apps/builder/src/pages/guided.vue b/apps/builder/src/pages/guided.vue new file mode 100644 index 000000000..6465e216d --- /dev/null +++ b/apps/builder/src/pages/guided.vue @@ -0,0 +1,161 @@ + + + From 4a0435d49e70adb996a2ba7f2fd2521f343778da Mon Sep 17 00:00:00 2001 From: John Leider Date: Sat, 4 Apr 2026 13:44:45 -0500 Subject: [PATCH 12/56] feat(builder): add review page with playground handoff Shows selected vs auto-included features, warnings, and opens the resolved manifest in the v0 playground. --- apps/builder/src/pages/review.vue | 105 ++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 apps/builder/src/pages/review.vue diff --git a/apps/builder/src/pages/review.vue b/apps/builder/src/pages/review.vue new file mode 100644 index 000000000..dc545fad8 --- /dev/null +++ b/apps/builder/src/pages/review.vue @@ -0,0 +1,105 @@ + + + From 7b853ba9b58a1e6e85e197efe53777790070a466 Mon Sep 17 00:00:00 2001 From: John Leider Date: Sat, 4 Apr 2026 13:44:49 -0500 Subject: [PATCH 13/56] feat(builder): add free pick mode Filterable grid of all features grouped by category with search, selection toggling, and review navigation. --- apps/builder/src/pages/free.vue | 96 +++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 apps/builder/src/pages/free.vue diff --git a/apps/builder/src/pages/free.vue b/apps/builder/src/pages/free.vue new file mode 100644 index 000000000..a0d4a1b8d --- /dev/null +++ b/apps/builder/src/pages/free.vue @@ -0,0 +1,96 @@ + + + From b61246b21344ce76235825af7f15f7f7c3bb4f74 Mon Sep 17 00:00:00 2001 From: John Leider Date: Sat, 4 Apr 2026 13:46:40 -0500 Subject: [PATCH 14/56] feat(builder): add AI builder page with auth gate Chat shell for Vuetify One subscribers with auth gating, message display, and dev bypass. AI integration is a follow-up. --- apps/builder/src/pages/ai.vue | 130 +++++++++++++++++++++++++++++++++ apps/builder/src/vite-env.d.ts | 2 + 2 files changed, 132 insertions(+) create mode 100644 apps/builder/src/pages/ai.vue diff --git a/apps/builder/src/pages/ai.vue b/apps/builder/src/pages/ai.vue new file mode 100644 index 000000000..d5465126b --- /dev/null +++ b/apps/builder/src/pages/ai.vue @@ -0,0 +1,130 @@ + + + diff --git a/apps/builder/src/vite-env.d.ts b/apps/builder/src/vite-env.d.ts index 11f02fe2a..9ca0786df 100644 --- a/apps/builder/src/vite-env.d.ts +++ b/apps/builder/src/vite-env.d.ts @@ -1 +1,3 @@ /// + +declare const __DEV__: boolean From d820762496b6efecd2a08a432a730235e3cb8257 Mon Sep 17 00:00:00 2001 From: John Leider Date: Sat, 4 Apr 2026 13:48:32 -0500 Subject: [PATCH 15/56] chore(builder): fix lint issues Replace sort() with toSorted(), utf-8 with utf8. --- apps/builder/build/generate-dependencies.ts | 4 ++-- apps/builder/src/engine/resolve.test.ts | 8 ++++---- apps/builder/src/engine/resolve.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/builder/build/generate-dependencies.ts b/apps/builder/build/generate-dependencies.ts index a0e4841f1..94f7690b7 100644 --- a/apps/builder/build/generate-dependencies.ts +++ b/apps/builder/build/generate-dependencies.ts @@ -14,7 +14,7 @@ interface DependencyGraph { function extractV0Imports (filePath: string): string[] { let content: string try { - content = readFileSync(filePath, 'utf-8') + content = readFileSync(filePath, 'utf8') } catch { return [] } @@ -51,7 +51,7 @@ function scanDirectory (dir: string): Record { } } catch { /* empty */ } - graph[entry] = [...new Set(deps)].filter(d => d !== entry).sort() + graph[entry] = [...new Set(deps)].filter(d => d !== entry).toSorted() } return graph diff --git a/apps/builder/src/engine/resolve.test.ts b/apps/builder/src/engine/resolve.test.ts index d503f930b..013068470 100644 --- a/apps/builder/src/engine/resolve.test.ts +++ b/apps/builder/src/engine/resolve.test.ts @@ -34,19 +34,19 @@ describe('resolve', () => { it('auto-includes transitive dependencies', () => { const result = resolve(['createSelection'], graph) expect(result.selected).toEqual(['createSelection']) - expect(result.autoIncluded.sort()).toEqual(['createContext', 'createModel', 'createTrinity']) + expect(result.autoIncluded.toSorted()).toEqual(['createContext', 'createModel', 'createTrinity']) }) it('does not duplicate features in selected and autoIncluded', () => { const result = resolve(['createSelection', 'createContext'], graph) - expect(result.selected.sort()).toEqual(['createContext', 'createSelection']) - expect(result.autoIncluded.sort()).toEqual(['createModel', 'createTrinity']) + expect(result.selected.toSorted()).toEqual(['createContext', 'createSelection']) + expect(result.autoIncluded.toSorted()).toEqual(['createModel', 'createTrinity']) }) it('resolves deep transitive chains', () => { const result = resolve(['createStep'], graph) expect(result.selected).toEqual(['createStep']) - expect(result.autoIncluded.sort()).toEqual([ + expect(result.autoIncluded.toSorted()).toEqual([ 'createContext', 'createModel', 'createSelection', diff --git a/apps/builder/src/engine/resolve.ts b/apps/builder/src/engine/resolve.ts index 453c9d4d2..fcf0f0411 100644 --- a/apps/builder/src/engine/resolve.ts +++ b/apps/builder/src/engine/resolve.ts @@ -33,7 +33,7 @@ export function resolve (selected: string[], graph: DependencyGraph): ResolvedSe const autoIncluded = [...allDeps] .filter(id => !selectedSet.has(id)) - .sort() + .toSorted() return { selected: [...selected], From e0a335d9b57be4010a6b49b645908eebc35c018a Mon Sep 17 00:00:00 2001 From: John Leider Date: Sat, 4 Apr 2026 14:49:35 -0500 Subject: [PATCH 16/56] chore(builder): add knip workspace config Register builder app entries so knip recognizes file-based routing, auto-imported components, and build scripts. --- knip.json | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/knip.json b/knip.json index 12991419f..784f32ee5 100644 --- a/knip.json +++ b/knip.json @@ -83,6 +83,36 @@ "sass" ] }, + "apps/builder": { + "entry": [ + "src/App.vue", + "src/components/**/*.vue", + "src/data/*.ts", + "src/engine/*.ts", + "src/plugins/*.ts", + "src/stores/*.ts", + "src/pages/**/*.vue", + "build/**/*.ts", + "vite.config.*" + ], + "ignore": [ + "src/typed-router.d.ts" + ], + "ignoreDependencies": [ + "@vuetify/auth" + ], + "paths": { + "@/*": [ + "src/*" + ], + "@vuetify/v0": [ + "../../packages/0/src" + ], + "#v0": [ + "../../packages/0/src" + ] + } + }, "apps/playground": { "entry": [ "src/App.vue", From 24c3156ce5120e61450333716fdaa37f1e17945a Mon Sep 17 00:00:00 2001 From: John Leider Date: Sat, 4 Apr 2026 15:04:46 -0500 Subject: [PATCH 17/56] fix(builder): replace non-existent mdiShoeprint with mdiCompass --- apps/builder/src/pages/index.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/builder/src/pages/index.vue b/apps/builder/src/pages/index.vue index abfbd4a47..946148330 100644 --- a/apps/builder/src/pages/index.vue +++ b/apps/builder/src/pages/index.vue @@ -1,5 +1,5 @@ @@ -18,16 +20,23 @@ + + + +
@@ -74,7 +95,7 @@

- {{ category }} + {{ category }} ({{ features.length }})

-
-
+
+