Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
083ba68
chore(builder): scaffold builder app
johnleider May 19, 2026
55c619a
chore(builder): add feature catalog and dependency resolver
johnleider May 19, 2026
8629157
chore(builder): add wizard UX and selection store
johnleider May 19, 2026
97eb80e
chore(builder): add useStack and useRules to plugin catalog
johnleider May 20, 2026
83a4d5a
chore(builder): add canonical plugin order and route metadata
johnleider May 20, 2026
ed38c55
chore(builder): extend store with pluginConfig, selectedComponents, c…
johnleider May 20, 2026
f1525ed
chore(builder): persist wizard state to localStorage
johnleider May 20, 2026
f2aa564
chore(builder): add PluginConfigShell with prev/skip/save&next chrome
johnleider May 20, 2026
d89c89f
chore(builder): refactor wizard into /builder routes with dynamic plu…
johnleider May 20, 2026
1171ea0
chore(builder): add route guards for wizard navigation
johnleider May 20, 2026
830693c
chore(builder): add Theme plugin config form
johnleider May 20, 2026
a6f2902
chore(builder): add Breakpoints plugin config form
johnleider May 20, 2026
f4a0d1d
chore(builder): add Locale plugin config form
johnleider May 20, 2026
d6c4f71
chore(builder): add Rtl plugin config form
johnleider May 20, 2026
2d69842
chore(builder): add Storage plugin config form
johnleider May 20, 2026
c440a9a
chore(builder): add Hydration confirm-only screen
johnleider May 20, 2026
23c6754
chore(builder): add Logger plugin config form
johnleider May 20, 2026
f8ca2b9
chore(builder): add Features plugin config form
johnleider May 20, 2026
f91f4de
chore(builder): add Permissions plugin config form
johnleider May 20, 2026
cd5caa0
chore(builder): add Notifications plugin config form
johnleider May 20, 2026
7dd699e
chore(builder): add Date plugin config form
johnleider May 20, 2026
77b9237
chore(builder): add Stack plugin config form (real options per scout)
johnleider May 20, 2026
6cfb460
chore(builder): add Rules plugin config form
johnleider May 20, 2026
b187ce6
chore(builder): thread per-plugin config into generated main.ts
johnleider May 20, 2026
0f6bfd7
chore(builder): add zip starter generator (fflate-based)
johnleider May 20, 2026
e87993d
chore(builder): build review screen with playground + zip outputs
johnleider May 20, 2026
1e1a3e3
chore(builder): add plugin-to-component recommendation mapping
johnleider May 20, 2026
43b1562
chore(builder): build smart-filter component selection page
johnleider May 20, 2026
fd57896
chore(builder): fix adapter emission in generated main.ts
johnleider May 20, 2026
9206ea9
chore(builder): prefer toRef over computed per project style
johnleider May 20, 2026
b8841a5
chore(builder): use #v0 alias for cross-package imports
johnleider May 20, 2026
b2ab497
chore(builder): replace destructure-rest with explicit delete for sta…
johnleider May 20, 2026
2b03ac9
chore(builder): regenerate auto-imports + typed-router dts
johnleider May 20, 2026
dd0e4b7
chore(builder): tighten exports and prune dead code
johnleider May 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions apps/builder/build/generate-dependencies.ts
Original file line number Diff line number Diff line change
@@ -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<string, string[]>
components: Record<string, string[]>
}

function extractV0Imports (filePath: string): string[] {
let content: string
try {
content = readFileSync(filePath, 'utf8')
} 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<string, string[]> {
const entries = readdirSync(dir)
const graph: Record<string, string[]> = {}

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).toSorted()
}

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`,
)
12 changes: 12 additions & 0 deletions apps/builder/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>v0 Framework Builder</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
28 changes: 28 additions & 0 deletions apps/builder/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@vuetify-private/builder",
"version": "1.0.0-alpha.0",
"private": true,
"type": "module",
"scripts": {
"generate": "tsx build/generate-dependencies.ts",
"dev": "pnpm generate && vite",
"build": "pnpm generate && vite build",
"preview": "vite preview"
},
"dependencies": {
"@mdi/js": "catalog:",
"@vuetify/v0": "workspace:*",
"fflate": "catalog:",
"pinia": "catalog:",
"vue": "catalog:"
},
"devDependencies": {
"tsx": "catalog:",
"unocss": "catalog:",
"unplugin-vue": "catalog:",
"unplugin-vue-components": "catalog:",
"vite": "catalog:",
"vite-plugin-vue-layouts-next": "catalog:",
"vue-router": "catalog:"
}
}
10 changes: 10 additions & 0 deletions apps/builder/src/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script setup lang="ts">
// Utilities
import { RouterView } from 'vue-router'
</script>

<template>
<div class="min-h-screen bg-background text-on-surface">
<RouterView />
</div>
</template>
18 changes: 18 additions & 0 deletions apps/builder/src/components.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* 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 {
PluginConfigShell: typeof import('./components/PluginConfigShell.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}
103 changes: 103 additions & 0 deletions apps/builder/src/components/PluginConfigShell.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<script setup lang="ts">
import { mdiArrowLeft, mdiArrowRight, mdiClose } from '@mdi/js'

import { getPluginById, PLUGINS } from '@/data/plugins'

// Stores
import { useBuilderStore } from '@/stores/builder'

// Utilities
import { toRef } from 'vue'
import { useRouter } from 'vue-router'

const { pluginId } = defineProps<{
pluginId: string
}>()

const emit = defineEmits<{
save: []
}>()

const store = useBuilderStore()
const router = useRouter()

const meta = toRef(() => getPluginById(pluginId))

const sequence = toRef(() => PLUGINS.filter(p => store.isPluginSelected(p.id)))
const position = toRef(() => sequence.value.findIndex(p => p.id === pluginId))
const isFirst = toRef(() => position.value === 0)
const isLast = toRef(() => position.value === sequence.value.length - 1)

function goToPrev () {
if (isFirst.value) {
router.push('/builder')
return
}
router.push(`/builder/${sequence.value[position.value - 1].slug}`)
}

function goToNext () {
if (isLast.value) {
router.push('/builder/components')
return
}
router.push(`/builder/${sequence.value[position.value + 1].slug}`)
}

function onSkip () {
goToNext()
}

function onSave () {
emit('save')
goToNext()
}
</script>

<template>
<div v-if="meta" class="max-w-4xl mx-auto px-6 py-12">
<p class="text-xs text-on-surface-variant uppercase tracking-wide mb-1">
Configuring {{ meta.title }} ({{ position + 1 }} of {{ sequence.length }})
</p>

<h2 class="text-2xl font-bold mb-2">{{ meta.title }}</h2>

<slot name="description">
<p class="text-on-surface-variant mb-8">
{{ meta.title }} configuration
</p>
</slot>

<div class="mb-8">
<slot />
</div>

<div class="flex items-center justify-between border-t pt-6">
<button
class="text-sm text-on-surface-variant hover:text-on-surface inline-flex items-center gap-1"
@click="goToPrev"
>
<svg class="w-4 h-4" viewBox="0 0 24 24"><path :d="mdiArrowLeft" fill="currentColor" /></svg>
{{ isFirst ? 'Back to plugin selection' : 'Prev' }}
</button>

<div class="flex items-center gap-3">
<button
class="text-sm text-on-surface-variant hover:text-on-surface inline-flex items-center gap-1"
@click="onSkip"
>
<svg class="w-4 h-4" viewBox="0 0 24 24"><path :d="mdiClose" fill="currentColor" /></svg>
Skip (use defaults)
</button>

<button
class="px-6 py-2.5 bg-primary text-on-primary rounded-lg font-semibold text-sm hover:opacity-90 transition-opacity inline-flex items-center gap-2"
@click="onSave"
>
{{ isLast ? 'Save & Continue to Components' : 'Save & Next' }}
<svg class="w-4 h-4" viewBox="0 0 24 24"><path :d="mdiArrowRight" fill="currentColor" /></svg>
</button>
</div>
</div>
</div>
</template>
48 changes: 48 additions & 0 deletions apps/builder/src/data/component-recommendations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// apps/builder/src/data/component-recommendations.ts

// Hand-curated mapping from plugin id to the components that meaningfully benefit
// from that plugin being installed. Components are listed by id (e.g., 'Button', 'Dialog').
//
// This is a recommendation layer: it doesn't enforce dependencies (the resolver does that).
// Components that don't exist in v0 yet (DatePicker, Calendar) are still listed —
// they're tracked for when v0 ships them.
const PLUGIN_TO_COMPONENTS: Record<string, string[]> = {
useTheme: [
'Alert', 'Avatar', 'Badge', 'Button', 'Card', 'Checkbox', 'Chip',
'Combobox', 'Dialog', 'Drawer', 'Form', 'Image', 'Input', 'List',
'Menu', 'Pagination', 'Popover', 'Progress', 'Radio', 'Rating',
'Select', 'Slider', 'Switch', 'Tabs', 'Toast', 'Tooltip',
],
useStack: ['Dialog', 'Drawer', 'Menu', 'Popover', 'Tooltip', 'Toast'],
useRules: ['Form', 'Input', 'Combobox', 'Select', 'Slider', 'Numeric', 'Rating'],
useNotifications: ['Toast'],
useLocale: ['Alert', 'Button', 'Dialog', 'Input', 'Select', 'Combobox', 'Form'],
useDate: ['DatePicker', 'Calendar'],
useFeatures: [],
usePermissions: [],
useHydration: [],
useStorage: [],
useBreakpoints: [],
useLogger: [],
useRtl: [],
}

export function recommendedFor (selectedPlugins: Set<string>): Set<string> {
const out = new Set<string>()
for (const pluginId of selectedPlugins) {
for (const componentId of PLUGIN_TO_COMPONENTS[pluginId] ?? []) {
out.add(componentId)
}
}
return out
}

export function reasonsFor (componentId: string, selectedPlugins: Set<string>): string[] {
const reasons: string[] = []
for (const pluginId of selectedPlugins) {
if ((PLUGIN_TO_COMPONENTS[pluginId] ?? []).includes(componentId)) {
reasons.push(pluginId)
}
}
return reasons
}
Loading
Loading