create-visualbuild-app 0.1.0 → 1.0.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 +306 -123
  2. package/docs/user-guide.md +759 -0
  3. package/package.json +92 -85
  4. package/scripts/create-builder-app.mjs +513 -501
  5. package/scripts/vb-dev.mjs +41 -32
  6. package/scripts/vb-generate.mjs +332 -224
  7. package/templates/default/app/.vercelignore +6 -0
  8. package/templates/default/app/README.md +56 -21
  9. package/templates/default/app/visualbuild/components.json +3 -0
  10. package/templates/default/app/visualbuild/config.d.ts +48 -0
  11. package/templates/default/app/visualbuild.config.ts +38 -0
  12. package/templates/default/builder/README.md +37 -21
  13. package/visual-app-builder/server/parseReactPage.ts +776 -571
  14. package/visual-app-builder/shared/createReactComponentSource.d.mts +2 -0
  15. package/visual-app-builder/shared/createReactComponentSource.mjs +45 -0
  16. package/visual-app-builder/shared/generateReactSource.d.mts +57 -37
  17. package/visual-app-builder/shared/generateReactSource.mjs +608 -443
  18. package/visual-app-builder/shared/npmBuildCommand.d.mts +17 -0
  19. package/visual-app-builder/shared/npmBuildCommand.mjs +26 -0
  20. package/visual-app-builder/shared/syncCustomComponentSource.d.mts +13 -0
  21. package/visual-app-builder/shared/syncCustomComponentSource.mjs +345 -0
  22. package/visual-app-builder/shared/vercelDeployment.d.mts +31 -0
  23. package/visual-app-builder/shared/vercelDeployment.mjs +266 -0
  24. package/visual-app-builder/shared/visualbuildConfig.d.mts +144 -0
  25. package/visual-app-builder/shared/visualbuildConfig.mjs +578 -0
  26. package/visual-app-builder/src/App.tsx +1090 -874
  27. package/visual-app-builder/src/components/Canvas.tsx +1116 -1059
  28. package/visual-app-builder/src/components/CodePanel.tsx +1147 -812
  29. package/visual-app-builder/src/components/ComponentSidebar.tsx +365 -302
  30. package/visual-app-builder/src/components/DeploymentModal.tsx +295 -0
  31. package/visual-app-builder/src/components/PageSwitcher.tsx +133 -133
  32. package/visual-app-builder/src/components/ProjectExplorer.tsx +1054 -1054
  33. package/visual-app-builder/src/components/PropertiesPanel.tsx +792 -692
  34. package/visual-app-builder/src/components/Topbar.tsx +257 -128
  35. package/visual-app-builder/src/data/componentRegistry.tsx +613 -292
  36. package/visual-app-builder/src/data/tailwindClassCatalog.ts +95 -0
  37. package/visual-app-builder/src/index.css +383 -111
  38. package/visual-app-builder/src/stores/useAppStore.ts +2385 -1265
  39. package/visual-app-builder/src/theme.ts +71 -0
  40. package/visual-app-builder/src/types/index.ts +350 -261
  41. package/visual-app-builder/src/utils/codegen.ts +90 -66
  42. package/visual-app-builder/src/utils/deployment.ts +54 -0
  43. package/visual-app-builder/src/utils/projectPersistence.ts +218 -177
  44. package/visual-app-builder/vite.config.ts +1946 -1479
@@ -1,292 +1,613 @@
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)
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
+ function presetDefinition(
12
+ definition: Omit<ComponentDefinition, 'generator' | 'source'>
13
+ ): ComponentDefinition {
14
+ return {
15
+ ...definition,
16
+ generator: (props) => {
17
+ const attributes = Object.entries({
18
+ ...definition.defaultProps,
19
+ ...props,
20
+ })
21
+ .map(([name, value]) => `${name}={${JSON.stringify(value)}}`)
22
+ .join(' ')
23
+
24
+ return `<${definition.type}${attributes ? ` ${attributes}` : ''} />`
25
+ },
26
+ source: () => `import type { ReactNode } from 'react'
27
+
28
+ export function ${definition.type}({
29
+ children,
30
+ ...props
31
+ }: {
32
+ children?: ReactNode
33
+ } & Record<string, unknown>) {
34
+ void props
35
+ return <section>{children}</section>
36
+ }`,
37
+ }
38
+ }
39
+
40
+ export const componentRegistry: Record<ComponentType, ComponentDefinition> = {
41
+ Navbar: {
42
+ type: 'Navbar',
43
+ label: 'Navbar',
44
+ category: 'Layout',
45
+ icon: '▬',
46
+ description: 'Logo + nav links',
47
+ defaultProps: {
48
+ logo: 'My App',
49
+ },
50
+ propSchema: {
51
+ logo: { type: 'string', label: 'Logo', default: 'My App' },
52
+ },
53
+ renderer: ({ logo }, { pages, activePageId, setActivePage }) => (
54
+ <nav className="flex items-center justify-between px-6 py-3 bg-gray-900 text-white rounded-lg">
55
+ <span className="font-bold text-sm">{String(logo ?? 'Logo')}</span>
56
+ <div className="flex gap-4">
57
+ {pages.map((page) => (
58
+ <button
59
+ key={page.id}
60
+ type="button"
61
+ onClick={(event) => {
62
+ event.stopPropagation()
63
+ setActivePage(page.id)
64
+ }}
65
+ className={`text-xs transition-colors ${
66
+ page.id === activePageId
67
+ ? 'text-white'
68
+ : 'text-gray-300 hover:text-white'
69
+ }`}
70
+ >
71
+ {page.name}
72
+ </button>
73
+ ))}
74
+ </div>
75
+ </nav>
76
+ ),
77
+ generator: ({ logo, links }) =>
78
+ `<Navbar logo={${stringProp(logo, 'My App')}} links={${arrayProp(links, [])}} />`,
79
+ source: () => `import type { ReactNode } from 'react'
80
+
81
+ export function Navbar({
82
+ logo,
83
+ links,
84
+ children,
85
+ }: {
86
+ logo: string
87
+ links: { label: string; route: string }[]
88
+ children?: ReactNode
89
+ }) {
90
+ return (
91
+ <nav className="flex items-center justify-between px-6 py-3 bg-gray-900 text-white">
92
+ <span className="font-bold text-sm">{logo}</span>
93
+ <div className="flex items-center gap-6">
94
+ <div className="flex gap-4">
95
+ {links.map((link) => (
96
+ <a key={link.route} href={link.route} className="text-xs text-gray-300">
97
+ {link.label}
98
+ </a>
99
+ ))}
100
+ </div>
101
+ {children ? <div className="flex items-center gap-2">{children}</div> : null}
102
+ </div>
103
+ </nav>
104
+ )
105
+ }`,
106
+ },
107
+
108
+ Hero: {
109
+ type: 'Hero',
110
+ label: 'Hero',
111
+ category: 'Layout',
112
+ icon: '',
113
+ description: 'Title, subtitle, CTA',
114
+ defaultProps: {
115
+ title: 'Welcome to My App',
116
+ subtitle: 'Built visually. Owned as React code.',
117
+ cta: 'Start Building',
118
+ },
119
+ propSchema: {
120
+ title: { type: 'string', label: 'Title', default: 'Welcome to My App' },
121
+ subtitle: {
122
+ type: 'text',
123
+ label: 'Subtitle',
124
+ default: 'Built visually. Owned as React code.',
125
+ },
126
+ cta: { type: 'string', label: 'CTA text', default: 'Start Building' },
127
+ },
128
+ renderer: ({ title, subtitle, cta }) => (
129
+ <div className="bg-gradient-to-br from-blue-600 to-indigo-700 text-white px-8 py-12 text-center rounded-lg">
130
+ <h1 className="text-2xl font-bold mb-2">{String(title ?? 'Title')}</h1>
131
+ <p className="text-sm text-blue-100 mb-6">
132
+ {String(subtitle ?? 'Subtitle')}
133
+ </p>
134
+ <button className="px-5 py-2 bg-white text-blue-700 rounded-full text-sm font-semibold">
135
+ {String(cta ?? 'Start Building')}
136
+ </button>
137
+ </div>
138
+ ),
139
+ generator: ({ title, subtitle, cta }) =>
140
+ `<Hero title={${stringProp(title, 'Welcome to My App')}} subtitle={${stringProp(subtitle, 'Built visually. Owned as React code.')}} cta={${stringProp(cta, 'Start Building')}} />`,
141
+ source: () => `import type { ReactNode } from 'react'
142
+
143
+ export function Hero({
144
+ title,
145
+ subtitle,
146
+ cta,
147
+ children,
148
+ }: {
149
+ title: string
150
+ subtitle: string
151
+ cta: string
152
+ children?: ReactNode
153
+ }) {
154
+ return (
155
+ <section className="bg-gradient-to-br from-blue-600 to-indigo-700 text-white px-8 py-12 text-center">
156
+ <h1 className="text-2xl font-bold mb-2">{title}</h1>
157
+ <p className="text-sm text-blue-100 mb-6">{subtitle}</p>
158
+ <button className="px-5 py-2 bg-white text-blue-700 rounded-full text-sm font-semibold">
159
+ {cta}
160
+ </button>
161
+ {children ? <div className="mt-6">{children}</div> : null}
162
+ </section>
163
+ )
164
+ }`,
165
+ },
166
+
167
+ Card: {
168
+ type: 'Card',
169
+ label: 'Card',
170
+ category: 'Content',
171
+ icon: '▭',
172
+ description: 'Heading and body text',
173
+ defaultProps: {
174
+ title: 'Card Title',
175
+ body: 'Card description goes here.',
176
+ },
177
+ propSchema: {
178
+ title: { type: 'string', label: 'Title', default: 'Card Title' },
179
+ body: {
180
+ type: 'text',
181
+ label: 'Body',
182
+ default: 'Card description goes here.',
183
+ },
184
+ },
185
+ renderer: ({ title, body }) => (
186
+ <div className="bg-white rounded-lg border border-gray-200 p-5 shadow-sm">
187
+ <h3 className="font-semibold text-gray-900 mb-1">
188
+ {String(title ?? 'Card Title')}
189
+ </h3>
190
+ <p className="text-sm text-gray-500">
191
+ {String(body ?? 'Card description.')}
192
+ </p>
193
+ </div>
194
+ ),
195
+ generator: ({ title, body }) =>
196
+ `<Card title={${stringProp(title, 'Card Title')}} body={${stringProp(body, 'Card description goes here.')}} />`,
197
+ source: () => `import type { ReactNode } from 'react'
198
+
199
+ export function Card({
200
+ title,
201
+ body,
202
+ children,
203
+ }: {
204
+ title: string
205
+ body: string
206
+ children?: ReactNode
207
+ }) {
208
+ return (
209
+ <article className="bg-white border border-gray-200 p-5 shadow-sm">
210
+ <h3 className="font-semibold text-gray-900 mb-1">{title}</h3>
211
+ <p className="text-sm text-gray-500">{body}</p>
212
+ {children ? <div className="mt-4">{children}</div> : null}
213
+ </article>
214
+ )
215
+ }`,
216
+ },
217
+
218
+ Button: {
219
+ type: 'Button',
220
+ label: 'Button',
221
+ category: 'Actions',
222
+ icon: '◻',
223
+ description: 'Action button',
224
+ defaultProps: {
225
+ text: 'Click me',
226
+ variant: 'primary',
227
+ },
228
+ propSchema: {
229
+ text: { type: 'string', label: 'Text', default: 'Click me' },
230
+ variant: {
231
+ type: 'select',
232
+ label: 'Variant',
233
+ default: 'primary',
234
+ options: ['primary', 'outline', 'secondary'],
235
+ },
236
+ },
237
+ renderer: ({ text, variant }) => (
238
+ <div className="px-4 py-3 bg-white rounded-lg border border-gray-200 flex items-center gap-2">
239
+ <button
240
+ className={`px-4 py-1.5 rounded text-sm font-medium ${variant === 'primary'
241
+ ? 'bg-blue-600 text-white'
242
+ : variant === 'outline'
243
+ ? 'border border-blue-600 text-blue-600'
244
+ : 'bg-gray-100 text-gray-700'
245
+ }`}
246
+ >
247
+ {String(text ?? 'Button')}
248
+ </button>
249
+ </div>
250
+ ),
251
+ generator: ({ text, variant }) =>
252
+ `<Button text={${stringProp(text, 'Click me')}} variant={${stringProp(variant, 'primary')}} />`,
253
+ source: () => `import type { ReactNode } from 'react'
254
+
255
+ export function Button({
256
+ text,
257
+ variant,
258
+ children,
259
+ }: {
260
+ text: string
261
+ variant: 'primary' | 'outline' | 'secondary'
262
+ children?: ReactNode
263
+ }) {
264
+ const className =
265
+ variant === 'primary'
266
+ ? 'bg-blue-600 text-white'
267
+ : variant === 'outline'
268
+ ? 'border border-blue-600 text-blue-600'
269
+ : 'bg-gray-100 text-gray-700'
270
+
271
+ return (
272
+ <button className={\`px-4 py-1.5 rounded text-sm font-medium \${className}\`}>
273
+ {children ?? text}
274
+ </button>
275
+ )
276
+ }`,
277
+ },
278
+
279
+ Footer: {
280
+ type: 'Footer',
281
+ label: 'Footer',
282
+ category: 'Layout',
283
+ icon: '▬',
284
+ description: 'Copyright text',
285
+ defaultProps: {
286
+ copyright: 'Copyright 2026 MySite',
287
+ },
288
+ propSchema: {
289
+ copyright: {
290
+ type: 'string',
291
+ label: 'Copyright',
292
+ default: 'Copyright 2026 MySite',
293
+ },
294
+ },
295
+ renderer: ({ copyright }) => (
296
+ <footer className="bg-gray-900 text-gray-400 text-center py-4 text-xs rounded-lg">
297
+ {String(copyright ?? 'Copyright 2026 MySite')}
298
+ </footer>
299
+ ),
300
+ generator: ({ copyright }) =>
301
+ `<Footer copyright={${stringProp(copyright, 'Copyright 2026 MySite')}} />`,
302
+ source: () => `import type { ReactNode } from 'react'
303
+
304
+ export function Footer({
305
+ copyright,
306
+ children,
307
+ }: {
308
+ copyright: string
309
+ children?: ReactNode
310
+ }) {
311
+ return (
312
+ <footer className="bg-gray-900 text-gray-400 text-center py-4 text-xs">
313
+ {copyright}
314
+ {children ? <div className="mt-2">{children}</div> : null}
315
+ </footer>
316
+ )
317
+ }`,
318
+ },
319
+
320
+ Grid: presetDefinition({
321
+ type: 'Grid',
322
+ label: 'Grid',
323
+ category: 'Layout',
324
+ icon: '#',
325
+ description: 'Responsive three-column content grid',
326
+ defaultProps: {
327
+ heading: 'Explore the collection',
328
+ },
329
+ propSchema: {
330
+ heading: {
331
+ type: 'string',
332
+ label: 'Heading',
333
+ default: 'Explore the collection',
334
+ },
335
+ },
336
+ renderer: ({ heading }) => (
337
+ <section className="px-6 py-10">
338
+ <h2 className="mb-6 text-2xl font-semibold text-gray-900">
339
+ {String(heading)}
340
+ </h2>
341
+ <div className="grid grid-cols-3 gap-4">
342
+ {['One', 'Two', 'Three'].map((item) => (
343
+ <article
344
+ key={item}
345
+ className="rounded-lg border border-gray-200 bg-white p-5"
346
+ >
347
+ Item {item}
348
+ </article>
349
+ ))}
350
+ </div>
351
+ </section>
352
+ ),
353
+ }),
354
+
355
+ Testimonials: presetDefinition({
356
+ type: 'Testimonials',
357
+ label: 'Testimonials',
358
+ category: 'Marketing',
359
+ icon: '"',
360
+ description: 'Customer quotes in a responsive grid',
361
+ defaultProps: {
362
+ heading: 'What customers say',
363
+ },
364
+ propSchema: {
365
+ heading: {
366
+ type: 'string',
367
+ label: 'Heading',
368
+ default: 'What customers say',
369
+ },
370
+ },
371
+ renderer: ({ heading }) => (
372
+ <section className="bg-gray-50 px-6 py-12">
373
+ <h2 className="mb-8 text-center text-2xl font-semibold text-gray-900">
374
+ {String(heading)}
375
+ </h2>
376
+ <div className="grid grid-cols-3 gap-5">
377
+ {['Alex', 'Sam', 'Jamie'].map((name) => (
378
+ <blockquote
379
+ key={name}
380
+ className="rounded-lg border border-gray-200 bg-white p-6"
381
+ >
382
+ Great product.
383
+ <cite className="mt-3 block text-sm font-semibold">{name}</cite>
384
+ </blockquote>
385
+ ))}
386
+ </div>
387
+ </section>
388
+ ),
389
+ }),
390
+
391
+ Pricing: presetDefinition({
392
+ type: 'Pricing',
393
+ label: 'Pricing',
394
+ category: 'Marketing',
395
+ icon: '$',
396
+ description: 'Three editable pricing plans',
397
+ defaultProps: {
398
+ heading: 'Simple pricing',
399
+ },
400
+ propSchema: {
401
+ heading: {
402
+ type: 'string',
403
+ label: 'Heading',
404
+ default: 'Simple pricing',
405
+ },
406
+ },
407
+ renderer: ({ heading }) => (
408
+ <section className="px-6 py-12">
409
+ <h2 className="mb-8 text-center text-2xl font-semibold text-gray-900">
410
+ {String(heading)}
411
+ </h2>
412
+ <div className="grid grid-cols-3 gap-5">
413
+ {['Starter', 'Professional', 'Team'].map((name) => (
414
+ <article
415
+ key={name}
416
+ className="rounded-lg border border-gray-200 bg-white p-6 text-center"
417
+ >
418
+ <h3 className="font-semibold">{name}</h3>
419
+ <p className="my-4 text-2xl font-bold text-blue-600">$29</p>
420
+ </article>
421
+ ))}
422
+ </div>
423
+ </section>
424
+ ),
425
+ }),
426
+
427
+ Form: presetDefinition({
428
+ type: 'Form',
429
+ label: 'Form',
430
+ category: 'Forms',
431
+ icon: '=',
432
+ description: 'General-purpose form starter',
433
+ defaultProps: {
434
+ heading: 'Tell us about yourself',
435
+ },
436
+ propSchema: {
437
+ heading: {
438
+ type: 'string',
439
+ label: 'Heading',
440
+ default: 'Tell us about yourself',
441
+ },
442
+ },
443
+ renderer: ({ heading }) => (
444
+ <form className="mx-auto max-w-xl space-y-4 rounded-lg border border-gray-200 bg-white p-6">
445
+ <h2 className="text-xl font-semibold">{String(heading)}</h2>
446
+ <input
447
+ aria-label="Name"
448
+ className="w-full rounded-md border border-gray-300 px-3 py-2"
449
+ />
450
+ <button type="button" className="rounded-md bg-blue-600 px-4 py-2 text-white">
451
+ Submit
452
+ </button>
453
+ </form>
454
+ ),
455
+ }),
456
+
457
+ Image: presetDefinition({
458
+ type: 'Image',
459
+ label: 'Image',
460
+ category: 'Media',
461
+ icon: 'I',
462
+ description: 'Responsive image with caption',
463
+ defaultProps: {
464
+ caption: 'Replace this image and caption with your own.',
465
+ },
466
+ propSchema: {
467
+ caption: {
468
+ type: 'string',
469
+ label: 'Caption',
470
+ default: 'Replace this image and caption with your own.',
471
+ },
472
+ },
473
+ renderer: ({ caption }) => (
474
+ <figure className="overflow-hidden rounded-lg bg-gray-100">
475
+ <div className="h-40 bg-gray-300" />
476
+ <figcaption className="px-4 py-3 text-sm text-gray-600">
477
+ {String(caption)}
478
+ </figcaption>
479
+ </figure>
480
+ ),
481
+ }),
482
+
483
+ Features: presetDefinition({
484
+ type: 'Features',
485
+ label: 'Features',
486
+ category: 'Marketing',
487
+ icon: '+',
488
+ description: 'Three product benefit cards',
489
+ defaultProps: {
490
+ heading: 'Everything you need',
491
+ },
492
+ propSchema: {
493
+ heading: {
494
+ type: 'string',
495
+ label: 'Heading',
496
+ default: 'Everything you need',
497
+ },
498
+ },
499
+ renderer: ({ heading }) => (
500
+ <section className="px-6 py-12">
501
+ <h2 className="mb-8 text-center text-2xl font-semibold">
502
+ {String(heading)}
503
+ </h2>
504
+ <div className="grid grid-cols-3 gap-6">
505
+ {['Fast setup', 'Flexible design', 'Owned source'].map((feature) => (
506
+ <article key={feature} className="rounded-lg bg-gray-50 p-5">
507
+ {feature}
508
+ </article>
509
+ ))}
510
+ </div>
511
+ </section>
512
+ ),
513
+ }),
514
+
515
+ Gallery: presetDefinition({
516
+ type: 'Gallery',
517
+ label: 'Gallery',
518
+ category: 'Media',
519
+ icon: 'G',
520
+ description: 'Responsive image gallery',
521
+ defaultProps: {
522
+ heading: 'Gallery',
523
+ },
524
+ propSchema: {
525
+ heading: { type: 'string', label: 'Heading', default: 'Gallery' },
526
+ },
527
+ renderer: ({ heading }) => (
528
+ <section className="px-6 py-12">
529
+ <h2 className="mb-6 text-2xl font-semibold">{String(heading)}</h2>
530
+ <div className="grid grid-cols-3 gap-3">
531
+ {[1, 2, 3].map((item) => (
532
+ <div key={item} className="aspect-[4/3] rounded-lg bg-gray-200" />
533
+ ))}
534
+ </div>
535
+ </section>
536
+ ),
537
+ }),
538
+
539
+ FAQ: presetDefinition({
540
+ type: 'FAQ',
541
+ label: 'FAQ',
542
+ category: 'Content',
543
+ icon: '?',
544
+ description: 'Expandable questions and answers',
545
+ defaultProps: {
546
+ heading: 'Frequently asked questions',
547
+ },
548
+ propSchema: {
549
+ heading: {
550
+ type: 'string',
551
+ label: 'Heading',
552
+ default: 'Frequently asked questions',
553
+ },
554
+ },
555
+ renderer: ({ heading }) => (
556
+ <section className="mx-auto max-w-3xl px-6 py-12">
557
+ <h2 className="mb-6 text-2xl font-semibold">{String(heading)}</h2>
558
+ <details className="border-b border-gray-200 py-4">
559
+ <summary>Can I edit every element?</summary>
560
+ <p className="mt-3 text-sm text-gray-600">Yes, every node is editable.</p>
561
+ </details>
562
+ </section>
563
+ ),
564
+ }),
565
+
566
+ ContactForm: presetDefinition({
567
+ type: 'ContactForm',
568
+ label: 'Contact Form',
569
+ category: 'Forms',
570
+ icon: '@',
571
+ description: 'Name, email, and message fields',
572
+ defaultProps: {
573
+ heading: 'Contact us',
574
+ },
575
+ propSchema: {
576
+ heading: { type: 'string', label: 'Heading', default: 'Contact us' },
577
+ },
578
+ renderer: ({ heading }) => (
579
+ <form className="mx-auto max-w-xl space-y-4 rounded-lg bg-white p-6">
580
+ <h2 className="text-2xl font-semibold">{String(heading)}</h2>
581
+ <input aria-label="Name" className="w-full border px-3 py-2" />
582
+ <input aria-label="Email" className="w-full border px-3 py-2" />
583
+ <textarea aria-label="Message" className="w-full border px-3 py-2" />
584
+ </form>
585
+ ),
586
+ }),
587
+
588
+ LoginForm: presetDefinition({
589
+ type: 'LoginForm',
590
+ label: 'Login Form',
591
+ category: 'Forms',
592
+ icon: 'L',
593
+ description: 'Email and password sign-in form',
594
+ defaultProps: {
595
+ heading: 'Welcome back',
596
+ },
597
+ propSchema: {
598
+ heading: { type: 'string', label: 'Heading', default: 'Welcome back' },
599
+ },
600
+ renderer: ({ heading }) => (
601
+ <form className="mx-auto max-w-sm space-y-4 rounded-lg bg-white p-6">
602
+ <h2 className="text-2xl font-semibold">{String(heading)}</h2>
603
+ <input aria-label="Email" className="w-full border px-3 py-2" />
604
+ <input aria-label="Password" className="w-full border px-3 py-2" />
605
+ <button type="button" className="w-full bg-gray-900 px-4 py-2 text-white">
606
+ Sign in
607
+ </button>
608
+ </form>
609
+ ),
610
+ }),
611
+ }
612
+
613
+ export const components = Object.values(componentRegistry)