create-visualbuild-app 0.1.0 → 1.0.1
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 +919 -58
- package/docs/user-guide.md +759 -0
- package/package.json +92 -85
- package/scripts/create-builder-app.mjs +513 -501
- package/scripts/vb-dev.mjs +41 -32
- package/scripts/vb-generate.mjs +332 -224
- package/templates/default/app/.vercelignore +6 -0
- package/templates/default/app/README.md +56 -21
- package/templates/default/app/visualbuild/components.json +3 -0
- package/templates/default/app/visualbuild/config.d.ts +48 -0
- package/templates/default/app/visualbuild.config.ts +38 -0
- package/templates/default/builder/README.md +37 -21
- package/visual-app-builder/server/parseReactPage.ts +776 -571
- package/visual-app-builder/shared/createReactComponentSource.d.mts +2 -0
- package/visual-app-builder/shared/createReactComponentSource.mjs +45 -0
- package/visual-app-builder/shared/generateReactSource.d.mts +57 -37
- package/visual-app-builder/shared/generateReactSource.mjs +608 -443
- package/visual-app-builder/shared/npmBuildCommand.d.mts +17 -0
- package/visual-app-builder/shared/npmBuildCommand.mjs +26 -0
- package/visual-app-builder/shared/syncCustomComponentSource.d.mts +13 -0
- package/visual-app-builder/shared/syncCustomComponentSource.mjs +345 -0
- package/visual-app-builder/shared/vercelDeployment.d.mts +31 -0
- package/visual-app-builder/shared/vercelDeployment.mjs +266 -0
- package/visual-app-builder/shared/visualbuildConfig.d.mts +144 -0
- package/visual-app-builder/shared/visualbuildConfig.mjs +578 -0
- package/visual-app-builder/src/App.tsx +1090 -874
- package/visual-app-builder/src/components/Canvas.tsx +1116 -1059
- package/visual-app-builder/src/components/CodePanel.tsx +1147 -812
- package/visual-app-builder/src/components/ComponentSidebar.tsx +365 -302
- package/visual-app-builder/src/components/DeploymentModal.tsx +295 -0
- package/visual-app-builder/src/components/PageSwitcher.tsx +133 -133
- package/visual-app-builder/src/components/ProjectExplorer.tsx +1054 -1054
- package/visual-app-builder/src/components/PropertiesPanel.tsx +792 -692
- package/visual-app-builder/src/components/Topbar.tsx +257 -128
- package/visual-app-builder/src/data/componentRegistry.tsx +613 -292
- package/visual-app-builder/src/data/tailwindClassCatalog.ts +95 -0
- package/visual-app-builder/src/index.css +383 -111
- package/visual-app-builder/src/stores/useAppStore.ts +2385 -1265
- package/visual-app-builder/src/theme.ts +71 -0
- package/visual-app-builder/src/types/index.ts +350 -261
- package/visual-app-builder/src/utils/codegen.ts +90 -66
- package/visual-app-builder/src/utils/deployment.ts +54 -0
- package/visual-app-builder/src/utils/projectPersistence.ts +218 -177
- 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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
<
|
|
130
|
-
{
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
+
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)
|