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.
Files changed (44) hide show
  1. package/README.md +123 -0
  2. package/package.json +85 -0
  3. package/scripts/create-builder-app.mjs +501 -0
  4. package/scripts/vb-dev.mjs +32 -0
  5. package/scripts/vb-generate.mjs +224 -0
  6. package/templates/default/app/README.md +21 -0
  7. package/templates/default/app/eslint.config.js +25 -0
  8. package/templates/default/app/index.html +15 -0
  9. package/templates/default/app/src/index.css +23 -0
  10. package/templates/default/app/src/main.tsx +10 -0
  11. package/templates/default/app/tsconfig.app.json +21 -0
  12. package/templates/default/app/tsconfig.json +7 -0
  13. package/templates/default/app/tsconfig.node.json +19 -0
  14. package/templates/default/app/visualbuild/generated-files.json +3 -0
  15. package/templates/default/app/visualbuild/pages.json +12 -0
  16. package/templates/default/app/vite.config.ts +7 -0
  17. package/templates/default/builder/README.md +21 -0
  18. package/templates/default/builder/eslint.config.js +25 -0
  19. package/templates/default/builder/tsconfig.app.json +21 -0
  20. package/templates/default/builder/tsconfig.json +7 -0
  21. package/templates/default/builder/tsconfig.node.json +22 -0
  22. package/visual-app-builder/index.html +15 -0
  23. package/visual-app-builder/server/parseReactPage.ts +571 -0
  24. package/visual-app-builder/shared/generateReactSource.d.mts +37 -0
  25. package/visual-app-builder/shared/generateReactSource.mjs +443 -0
  26. package/visual-app-builder/src/App.tsx +874 -0
  27. package/visual-app-builder/src/components/Canvas.tsx +1059 -0
  28. package/visual-app-builder/src/components/CodePanel.tsx +812 -0
  29. package/visual-app-builder/src/components/ComponentSidebar.tsx +302 -0
  30. package/visual-app-builder/src/components/PageSwitcher.tsx +161 -0
  31. package/visual-app-builder/src/components/ProjectExplorer.tsx +1054 -0
  32. package/visual-app-builder/src/components/PropertiesPanel.tsx +692 -0
  33. package/visual-app-builder/src/components/Topbar.tsx +128 -0
  34. package/visual-app-builder/src/data/componentRegistry.tsx +292 -0
  35. package/visual-app-builder/src/data/primitiveRegistry.ts +368 -0
  36. package/visual-app-builder/src/index.css +111 -0
  37. package/visual-app-builder/src/main.tsx +10 -0
  38. package/visual-app-builder/src/stores/useAppStore.ts +1265 -0
  39. package/visual-app-builder/src/tailwindGeneratedSafelist.ts +36 -0
  40. package/visual-app-builder/src/types/index.ts +261 -0
  41. package/visual-app-builder/src/utils/codegen.ts +66 -0
  42. package/visual-app-builder/src/utils/projectFiles.ts +146 -0
  43. package/visual-app-builder/src/utils/projectPersistence.ts +177 -0
  44. 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)