create-visualbuild-app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -0
- package/package.json +85 -0
- package/scripts/create-builder-app.mjs +501 -0
- package/scripts/vb-dev.mjs +32 -0
- package/scripts/vb-generate.mjs +224 -0
- package/templates/default/app/README.md +21 -0
- package/templates/default/app/eslint.config.js +25 -0
- package/templates/default/app/index.html +15 -0
- package/templates/default/app/src/index.css +23 -0
- package/templates/default/app/src/main.tsx +10 -0
- package/templates/default/app/tsconfig.app.json +21 -0
- package/templates/default/app/tsconfig.json +7 -0
- package/templates/default/app/tsconfig.node.json +19 -0
- package/templates/default/app/visualbuild/generated-files.json +3 -0
- package/templates/default/app/visualbuild/pages.json +12 -0
- package/templates/default/app/vite.config.ts +7 -0
- package/templates/default/builder/README.md +21 -0
- package/templates/default/builder/eslint.config.js +25 -0
- package/templates/default/builder/tsconfig.app.json +21 -0
- package/templates/default/builder/tsconfig.json +7 -0
- package/templates/default/builder/tsconfig.node.json +22 -0
- package/visual-app-builder/index.html +15 -0
- package/visual-app-builder/server/parseReactPage.ts +571 -0
- package/visual-app-builder/shared/generateReactSource.d.mts +37 -0
- package/visual-app-builder/shared/generateReactSource.mjs +443 -0
- package/visual-app-builder/src/App.tsx +874 -0
- package/visual-app-builder/src/components/Canvas.tsx +1059 -0
- package/visual-app-builder/src/components/CodePanel.tsx +812 -0
- package/visual-app-builder/src/components/ComponentSidebar.tsx +302 -0
- package/visual-app-builder/src/components/PageSwitcher.tsx +161 -0
- package/visual-app-builder/src/components/ProjectExplorer.tsx +1054 -0
- package/visual-app-builder/src/components/PropertiesPanel.tsx +692 -0
- package/visual-app-builder/src/components/Topbar.tsx +128 -0
- package/visual-app-builder/src/data/componentRegistry.tsx +292 -0
- package/visual-app-builder/src/data/primitiveRegistry.ts +368 -0
- package/visual-app-builder/src/index.css +111 -0
- package/visual-app-builder/src/main.tsx +10 -0
- package/visual-app-builder/src/stores/useAppStore.ts +1265 -0
- package/visual-app-builder/src/tailwindGeneratedSafelist.ts +36 -0
- package/visual-app-builder/src/types/index.ts +261 -0
- package/visual-app-builder/src/utils/codegen.ts +66 -0
- package/visual-app-builder/src/utils/projectFiles.ts +146 -0
- package/visual-app-builder/src/utils/projectPersistence.ts +177 -0
- package/visual-app-builder/vite.config.ts +1479 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export type SaveState = 'idle' | 'saving' | 'saved' | 'error'
|
|
2
|
+
|
|
3
|
+
export default function Topbar({
|
|
4
|
+
showCode,
|
|
5
|
+
previewMode,
|
|
6
|
+
saveState,
|
|
7
|
+
onToggleCode,
|
|
8
|
+
onTogglePreview,
|
|
9
|
+
onSave,
|
|
10
|
+
}: {
|
|
11
|
+
showCode: boolean
|
|
12
|
+
previewMode: boolean
|
|
13
|
+
saveState: SaveState
|
|
14
|
+
onToggleCode: () => void
|
|
15
|
+
onTogglePreview: () => void
|
|
16
|
+
onSave: () => void
|
|
17
|
+
}) {
|
|
18
|
+
return (
|
|
19
|
+
<header className="flex items-center justify-between px-4 h-12 bg-topbar border-b border-white/[0.06] shrink-0 z-10">
|
|
20
|
+
<div className="flex items-center gap-2">
|
|
21
|
+
<div className="w-6 h-6 rounded bg-accent flex items-center justify-center">
|
|
22
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
|
23
|
+
<rect x="1" y="1" width="5" height="5" rx="1" fill="white" />
|
|
24
|
+
<rect
|
|
25
|
+
x="8"
|
|
26
|
+
y="1"
|
|
27
|
+
width="5"
|
|
28
|
+
height="5"
|
|
29
|
+
rx="1"
|
|
30
|
+
fill="white"
|
|
31
|
+
opacity="0.6"
|
|
32
|
+
/>
|
|
33
|
+
<rect
|
|
34
|
+
x="1"
|
|
35
|
+
y="8"
|
|
36
|
+
width="5"
|
|
37
|
+
height="5"
|
|
38
|
+
rx="1"
|
|
39
|
+
fill="white"
|
|
40
|
+
opacity="0.6"
|
|
41
|
+
/>
|
|
42
|
+
<rect
|
|
43
|
+
x="8"
|
|
44
|
+
y="8"
|
|
45
|
+
width="5"
|
|
46
|
+
height="5"
|
|
47
|
+
rx="1"
|
|
48
|
+
fill="white"
|
|
49
|
+
opacity="0.3"
|
|
50
|
+
/>
|
|
51
|
+
</svg>
|
|
52
|
+
</div>
|
|
53
|
+
<span className="text-sm font-semibold tracking-tight text-white">
|
|
54
|
+
VisualBuild
|
|
55
|
+
</span>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div className="flex items-center gap-1">
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
onClick={onTogglePreview}
|
|
62
|
+
className={`px-3 py-1.5 text-xs rounded transition-colors cursor-pointer select-none ${
|
|
63
|
+
previewMode
|
|
64
|
+
? 'text-white bg-white/[0.1]'
|
|
65
|
+
: 'text-gray-400 hover:text-white hover:bg-white/[0.06]'
|
|
66
|
+
}`}
|
|
67
|
+
>
|
|
68
|
+
Preview
|
|
69
|
+
</button>
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
onClick={onToggleCode}
|
|
73
|
+
className={`px-3 py-1.5 text-xs rounded transition-colors cursor-pointer select-none ${
|
|
74
|
+
!previewMode && showCode
|
|
75
|
+
? 'text-white bg-white/[0.1]'
|
|
76
|
+
: 'text-gray-400 hover:text-white hover:bg-white/[0.06]'
|
|
77
|
+
}`}
|
|
78
|
+
>
|
|
79
|
+
Code
|
|
80
|
+
</button>
|
|
81
|
+
<DisabledButton>Undo</DisabledButton>
|
|
82
|
+
<DisabledButton>Redo</DisabledButton>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div className="flex items-center gap-2">
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
disabled={saveState === 'saving'}
|
|
89
|
+
onClick={onSave}
|
|
90
|
+
className={`px-3 py-1.5 text-xs bg-white/[0.06] hover:bg-white/[0.1] disabled:opacity-60 rounded transition-colors cursor-pointer select-none ${
|
|
91
|
+
saveState === 'error' ? 'text-red-300' : 'text-gray-300'
|
|
92
|
+
}`}
|
|
93
|
+
>
|
|
94
|
+
{saveState === 'saving'
|
|
95
|
+
? 'Saving...'
|
|
96
|
+
: saveState === 'saved'
|
|
97
|
+
? 'Saved'
|
|
98
|
+
: saveState === 'error'
|
|
99
|
+
? 'Save failed'
|
|
100
|
+
: 'Save'}
|
|
101
|
+
</button>
|
|
102
|
+
<DisabledButton emphasized>Export ZIP</DisabledButton>
|
|
103
|
+
</div>
|
|
104
|
+
</header>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function DisabledButton({
|
|
109
|
+
children,
|
|
110
|
+
emphasized = false,
|
|
111
|
+
}: {
|
|
112
|
+
children: React.ReactNode
|
|
113
|
+
emphasized?: boolean
|
|
114
|
+
}) {
|
|
115
|
+
return (
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
disabled
|
|
119
|
+
className={`px-3 py-1.5 text-xs rounded cursor-pointer select-none ${
|
|
120
|
+
emphasized
|
|
121
|
+
? 'text-white/45 bg-accent/35'
|
|
122
|
+
: 'text-gray-700 bg-transparent'
|
|
123
|
+
}`}
|
|
124
|
+
>
|
|
125
|
+
{children}
|
|
126
|
+
</button>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import type { ComponentDefinition, ComponentType } from '../types'
|
|
2
|
+
|
|
3
|
+
function stringProp(value: unknown, fallback: string) {
|
|
4
|
+
return JSON.stringify(String(value ?? fallback))
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function arrayProp(value: unknown, fallback: string[]) {
|
|
8
|
+
return JSON.stringify(Array.isArray(value) ? value : fallback)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const componentRegistry: Record<ComponentType, ComponentDefinition> = {
|
|
12
|
+
Navbar: {
|
|
13
|
+
type: 'Navbar',
|
|
14
|
+
label: 'Navbar',
|
|
15
|
+
category: 'Layout',
|
|
16
|
+
icon: '▬',
|
|
17
|
+
description: 'Logo + nav links',
|
|
18
|
+
defaultProps: {
|
|
19
|
+
logo: 'My Blog',
|
|
20
|
+
},
|
|
21
|
+
propSchema: {
|
|
22
|
+
logo: { type: 'string', label: 'Logo', default: 'My Blog' },
|
|
23
|
+
},
|
|
24
|
+
renderer: ({ logo }, { pages, activePageId, setActivePage }) => (
|
|
25
|
+
<nav className="flex items-center justify-between px-6 py-3 bg-gray-900 text-white rounded-lg">
|
|
26
|
+
<span className="font-bold text-sm">{String(logo ?? 'Logo')}</span>
|
|
27
|
+
<div className="flex gap-4">
|
|
28
|
+
{pages.map((page) => (
|
|
29
|
+
<button
|
|
30
|
+
key={page.id}
|
|
31
|
+
type="button"
|
|
32
|
+
onClick={(event) => {
|
|
33
|
+
event.stopPropagation()
|
|
34
|
+
setActivePage(page.id)
|
|
35
|
+
}}
|
|
36
|
+
className={`text-xs transition-colors ${
|
|
37
|
+
page.id === activePageId
|
|
38
|
+
? 'text-white'
|
|
39
|
+
: 'text-gray-300 hover:text-white'
|
|
40
|
+
}`}
|
|
41
|
+
>
|
|
42
|
+
{page.name}
|
|
43
|
+
</button>
|
|
44
|
+
))}
|
|
45
|
+
</div>
|
|
46
|
+
</nav>
|
|
47
|
+
),
|
|
48
|
+
generator: ({ logo, links }) =>
|
|
49
|
+
`<Navbar logo={${stringProp(logo, 'My Blog')}} links={${arrayProp(links, [])}} />`,
|
|
50
|
+
source: () => `import type { ReactNode } from 'react'
|
|
51
|
+
|
|
52
|
+
export function Navbar({
|
|
53
|
+
logo,
|
|
54
|
+
links,
|
|
55
|
+
children,
|
|
56
|
+
}: {
|
|
57
|
+
logo: string
|
|
58
|
+
links: { label: string; route: string }[]
|
|
59
|
+
children?: ReactNode
|
|
60
|
+
}) {
|
|
61
|
+
return (
|
|
62
|
+
<nav className="flex items-center justify-between px-6 py-3 bg-gray-900 text-white">
|
|
63
|
+
<span className="font-bold text-sm">{logo}</span>
|
|
64
|
+
<div className="flex items-center gap-6">
|
|
65
|
+
<div className="flex gap-4">
|
|
66
|
+
{links.map((link) => (
|
|
67
|
+
<a key={link.route} href={link.route} className="text-xs text-gray-300">
|
|
68
|
+
{link.label}
|
|
69
|
+
</a>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
{children ? <div className="flex items-center gap-2">{children}</div> : null}
|
|
73
|
+
</div>
|
|
74
|
+
</nav>
|
|
75
|
+
)
|
|
76
|
+
}`,
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
Hero: {
|
|
80
|
+
type: 'Hero',
|
|
81
|
+
label: 'Hero',
|
|
82
|
+
category: 'Layout',
|
|
83
|
+
icon: '⬛',
|
|
84
|
+
description: 'Title, subtitle, CTA',
|
|
85
|
+
defaultProps: {
|
|
86
|
+
title: 'Welcome to My Blog',
|
|
87
|
+
subtitle: 'Built visually. Owned as React code.',
|
|
88
|
+
cta: 'Start Building',
|
|
89
|
+
},
|
|
90
|
+
propSchema: {
|
|
91
|
+
title: { type: 'string', label: 'Title', default: 'Welcome to My Blog' },
|
|
92
|
+
subtitle: {
|
|
93
|
+
type: 'text',
|
|
94
|
+
label: 'Subtitle',
|
|
95
|
+
default: 'Built visually. Owned as React code.',
|
|
96
|
+
},
|
|
97
|
+
cta: { type: 'string', label: 'CTA text', default: 'Start Building' },
|
|
98
|
+
},
|
|
99
|
+
renderer: ({ title, subtitle, cta }) => (
|
|
100
|
+
<div className="bg-gradient-to-br from-blue-600 to-indigo-700 text-white px-8 py-12 text-center rounded-lg">
|
|
101
|
+
<h1 className="text-2xl font-bold mb-2">{String(title ?? 'Title')}</h1>
|
|
102
|
+
<p className="text-sm text-blue-100 mb-6">
|
|
103
|
+
{String(subtitle ?? 'Subtitle')}
|
|
104
|
+
</p>
|
|
105
|
+
<button className="px-5 py-2 bg-white text-blue-700 rounded-full text-sm font-semibold">
|
|
106
|
+
{String(cta ?? 'Start Building')}
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
),
|
|
110
|
+
generator: ({ title, subtitle, cta }) =>
|
|
111
|
+
`<Hero title={${stringProp(title, 'Welcome to My Blog')}} subtitle={${stringProp(subtitle, 'Built visually. Owned as React code.')}} cta={${stringProp(cta, 'Start Building')}} />`,
|
|
112
|
+
source: () => `import type { ReactNode } from 'react'
|
|
113
|
+
|
|
114
|
+
export function Hero({
|
|
115
|
+
title,
|
|
116
|
+
subtitle,
|
|
117
|
+
cta,
|
|
118
|
+
children,
|
|
119
|
+
}: {
|
|
120
|
+
title: string
|
|
121
|
+
subtitle: string
|
|
122
|
+
cta: string
|
|
123
|
+
children?: ReactNode
|
|
124
|
+
}) {
|
|
125
|
+
return (
|
|
126
|
+
<section className="bg-gradient-to-br from-blue-600 to-indigo-700 text-white px-8 py-12 text-center">
|
|
127
|
+
<h1 className="text-2xl font-bold mb-2">{title}</h1>
|
|
128
|
+
<p className="text-sm text-blue-100 mb-6">{subtitle}</p>
|
|
129
|
+
<button className="px-5 py-2 bg-white text-blue-700 rounded-full text-sm font-semibold">
|
|
130
|
+
{cta}
|
|
131
|
+
</button>
|
|
132
|
+
{children ? <div className="mt-6">{children}</div> : null}
|
|
133
|
+
</section>
|
|
134
|
+
)
|
|
135
|
+
}`,
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
Card: {
|
|
139
|
+
type: 'Card',
|
|
140
|
+
label: 'Card',
|
|
141
|
+
category: 'Content',
|
|
142
|
+
icon: '▭',
|
|
143
|
+
description: 'Heading and body text',
|
|
144
|
+
defaultProps: {
|
|
145
|
+
title: 'Card Title',
|
|
146
|
+
body: 'Card description goes here.',
|
|
147
|
+
},
|
|
148
|
+
propSchema: {
|
|
149
|
+
title: { type: 'string', label: 'Title', default: 'Card Title' },
|
|
150
|
+
body: {
|
|
151
|
+
type: 'text',
|
|
152
|
+
label: 'Body',
|
|
153
|
+
default: 'Card description goes here.',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
renderer: ({ title, body }) => (
|
|
157
|
+
<div className="bg-white rounded-lg border border-gray-200 p-5 shadow-sm">
|
|
158
|
+
<h3 className="font-semibold text-gray-900 mb-1">
|
|
159
|
+
{String(title ?? 'Card Title')}
|
|
160
|
+
</h3>
|
|
161
|
+
<p className="text-sm text-gray-500">
|
|
162
|
+
{String(body ?? 'Card description.')}
|
|
163
|
+
</p>
|
|
164
|
+
</div>
|
|
165
|
+
),
|
|
166
|
+
generator: ({ title, body }) =>
|
|
167
|
+
`<Card title={${stringProp(title, 'Card Title')}} body={${stringProp(body, 'Card description goes here.')}} />`,
|
|
168
|
+
source: () => `import type { ReactNode } from 'react'
|
|
169
|
+
|
|
170
|
+
export function Card({
|
|
171
|
+
title,
|
|
172
|
+
body,
|
|
173
|
+
children,
|
|
174
|
+
}: {
|
|
175
|
+
title: string
|
|
176
|
+
body: string
|
|
177
|
+
children?: ReactNode
|
|
178
|
+
}) {
|
|
179
|
+
return (
|
|
180
|
+
<article className="bg-white border border-gray-200 p-5 shadow-sm">
|
|
181
|
+
<h3 className="font-semibold text-gray-900 mb-1">{title}</h3>
|
|
182
|
+
<p className="text-sm text-gray-500">{body}</p>
|
|
183
|
+
{children ? <div className="mt-4">{children}</div> : null}
|
|
184
|
+
</article>
|
|
185
|
+
)
|
|
186
|
+
}`,
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
Button: {
|
|
190
|
+
type: 'Button',
|
|
191
|
+
label: 'Button',
|
|
192
|
+
category: 'Actions',
|
|
193
|
+
icon: '◻',
|
|
194
|
+
description: 'Action button',
|
|
195
|
+
defaultProps: {
|
|
196
|
+
text: 'Click me',
|
|
197
|
+
variant: 'primary',
|
|
198
|
+
},
|
|
199
|
+
propSchema: {
|
|
200
|
+
text: { type: 'string', label: 'Text', default: 'Click me' },
|
|
201
|
+
variant: {
|
|
202
|
+
type: 'select',
|
|
203
|
+
label: 'Variant',
|
|
204
|
+
default: 'primary',
|
|
205
|
+
options: ['primary', 'outline', 'secondary'],
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
renderer: ({ text, variant }) => (
|
|
209
|
+
<div className="px-4 py-3 bg-white rounded-lg border border-gray-200 flex items-center gap-2">
|
|
210
|
+
<button
|
|
211
|
+
className={`px-4 py-1.5 rounded text-sm font-medium ${variant === 'primary'
|
|
212
|
+
? 'bg-blue-600 text-white'
|
|
213
|
+
: variant === 'outline'
|
|
214
|
+
? 'border border-blue-600 text-blue-600'
|
|
215
|
+
: 'bg-gray-100 text-gray-700'
|
|
216
|
+
}`}
|
|
217
|
+
>
|
|
218
|
+
{String(text ?? 'Button')}
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
221
|
+
),
|
|
222
|
+
generator: ({ text, variant }) =>
|
|
223
|
+
`<Button text={${stringProp(text, 'Click me')}} variant={${stringProp(variant, 'primary')}} />`,
|
|
224
|
+
source: () => `import type { ReactNode } from 'react'
|
|
225
|
+
|
|
226
|
+
export function Button({
|
|
227
|
+
text,
|
|
228
|
+
variant,
|
|
229
|
+
children,
|
|
230
|
+
}: {
|
|
231
|
+
text: string
|
|
232
|
+
variant: 'primary' | 'outline' | 'secondary'
|
|
233
|
+
children?: ReactNode
|
|
234
|
+
}) {
|
|
235
|
+
const className =
|
|
236
|
+
variant === 'primary'
|
|
237
|
+
? 'bg-blue-600 text-white'
|
|
238
|
+
: variant === 'outline'
|
|
239
|
+
? 'border border-blue-600 text-blue-600'
|
|
240
|
+
: 'bg-gray-100 text-gray-700'
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<button className={\`px-4 py-1.5 rounded text-sm font-medium \${className}\`}>
|
|
244
|
+
{children ?? text}
|
|
245
|
+
</button>
|
|
246
|
+
)
|
|
247
|
+
}`,
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
Footer: {
|
|
251
|
+
type: 'Footer',
|
|
252
|
+
label: 'Footer',
|
|
253
|
+
category: 'Layout',
|
|
254
|
+
icon: '▬',
|
|
255
|
+
description: 'Copyright text',
|
|
256
|
+
defaultProps: {
|
|
257
|
+
copyright: 'Copyright 2026 MySite',
|
|
258
|
+
},
|
|
259
|
+
propSchema: {
|
|
260
|
+
copyright: {
|
|
261
|
+
type: 'string',
|
|
262
|
+
label: 'Copyright',
|
|
263
|
+
default: 'Copyright 2026 MySite',
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
renderer: ({ copyright }) => (
|
|
267
|
+
<footer className="bg-gray-900 text-gray-400 text-center py-4 text-xs rounded-lg">
|
|
268
|
+
{String(copyright ?? 'Copyright 2026 MySite')}
|
|
269
|
+
</footer>
|
|
270
|
+
),
|
|
271
|
+
generator: ({ copyright }) =>
|
|
272
|
+
`<Footer copyright={${stringProp(copyright, 'Copyright 2026 MySite')}} />`,
|
|
273
|
+
source: () => `import type { ReactNode } from 'react'
|
|
274
|
+
|
|
275
|
+
export function Footer({
|
|
276
|
+
copyright,
|
|
277
|
+
children,
|
|
278
|
+
}: {
|
|
279
|
+
copyright: string
|
|
280
|
+
children?: ReactNode
|
|
281
|
+
}) {
|
|
282
|
+
return (
|
|
283
|
+
<footer className="bg-gray-900 text-gray-400 text-center py-4 text-xs">
|
|
284
|
+
{copyright}
|
|
285
|
+
{children ? <div className="mt-2">{children}</div> : null}
|
|
286
|
+
</footer>
|
|
287
|
+
)
|
|
288
|
+
}`,
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export const components = Object.values(componentRegistry)
|