@siteable/core 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/LICENSE +21 -0
- package/NOTICE +4 -0
- package/README.md +82 -0
- package/dist/index.d.ts +401 -0
- package/dist/index.js +5389 -0
- package/package.json +66 -0
- package/src/blocks/BlockWrapper.tsx +149 -0
- package/src/blocks/banner/BannerBlock.tsx +38 -0
- package/src/blocks/contact/ContactBlock.tsx +63 -0
- package/src/blocks/content/ContentBlock.tsx +38 -0
- package/src/blocks/cta/CtaBlock.tsx +67 -0
- package/src/blocks/divider/DividerBlock.tsx +30 -0
- package/src/blocks/faq/FaqBlock.tsx +82 -0
- package/src/blocks/features/FeaturesBlock.tsx +170 -0
- package/src/blocks/footer/FooterBlock.tsx +136 -0
- package/src/blocks/gallery/GalleryBlock.tsx +67 -0
- package/src/blocks/hero/HeroBlock.tsx +163 -0
- package/src/blocks/image/ImageBlock.tsx +78 -0
- package/src/blocks/logocloud/LogoCloudBlock.tsx +70 -0
- package/src/blocks/navbar/NavbarBlock.tsx +102 -0
- package/src/blocks/newsletter/NewsletterBlock.tsx +51 -0
- package/src/blocks/pricing/PricingBlock.tsx +173 -0
- package/src/blocks/registry.tsx +90 -0
- package/src/blocks/stats/StatsBlock.tsx +96 -0
- package/src/blocks/team/TeamBlock.tsx +50 -0
- package/src/blocks/testimonials/TestimonialsBlock.tsx +203 -0
- package/src/blocks/types.ts +72 -0
- package/src/blocks/video/VideoBlock.tsx +58 -0
- package/src/editor/AgentPanel.tsx +318 -0
- package/src/editor/Canvas.tsx +95 -0
- package/src/editor/CanvasEmpty.tsx +40 -0
- package/src/editor/CanvasToolbar.tsx +366 -0
- package/src/editor/DesignPanel.tsx +224 -0
- package/src/editor/EditorLayout.tsx +196 -0
- package/src/editor/GenerationOverlay.tsx +201 -0
- package/src/editor/JsonDrawer.tsx +139 -0
- package/src/editor/LayersPanel.tsx +259 -0
- package/src/editor/LeftSidebar.tsx +136 -0
- package/src/editor/PropertiesPanel.tsx +564 -0
- package/src/editor/RightSidebar.tsx +85 -0
- package/src/editor/ShortcutsModal.tsx +126 -0
- package/src/editor/VersionHistory.tsx +110 -0
- package/src/index.ts +133 -0
- package/src/lib/block-metadata.ts +167 -0
- package/src/lib/export-html.ts +1424 -0
- package/src/lib/generate-site.ts +206 -0
- package/src/lib/generation-prompt.ts +64 -0
- package/src/lib/markdown.ts +88 -0
- package/src/lib/templates.ts +139 -0
- package/src/lib/theme-presets.ts +330 -0
- package/src/lib/useGoogleFonts.ts +49 -0
- package/src/lib/useScrollReveal.ts +28 -0
- package/src/settings/GeminiKeyInputField.tsx +82 -0
- package/src/store/configStore.ts +344 -0
- package/src/store/editorStore.ts +50 -0
- package/src/styles.css +210 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { ChevronDown, ChevronRight, Code } from 'lucide-react'
|
|
3
|
+
import type { BlockConfig, BlockType } from '@/blocks/types'
|
|
4
|
+
import { useConfigStore } from '@/store/configStore'
|
|
5
|
+
|
|
6
|
+
interface FieldDef {
|
|
7
|
+
key: string
|
|
8
|
+
label: string
|
|
9
|
+
type: 'text' | 'textarea' | 'select' | 'array-strings' | 'array-items'
|
|
10
|
+
options?: string[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const blockFields: Partial<Record<BlockType, { sections: { title: string; fields: FieldDef[] }[] }>> = {
|
|
14
|
+
navbar: {
|
|
15
|
+
sections: [
|
|
16
|
+
{
|
|
17
|
+
title: 'Content',
|
|
18
|
+
fields: [
|
|
19
|
+
{ key: 'logo', label: 'Logo Text', type: 'text' },
|
|
20
|
+
{ key: 'logoImage', label: 'Logo Image URL', type: 'text' },
|
|
21
|
+
{ key: 'ctaText', label: 'CTA Button', type: 'text' },
|
|
22
|
+
{ key: 'links', label: 'Nav Links', type: 'array-strings' },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
title: 'Style',
|
|
27
|
+
fields: [
|
|
28
|
+
{ key: 'variant', label: 'Variant', type: 'select', options: ['default', 'centered'] },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
hero: {
|
|
34
|
+
sections: [
|
|
35
|
+
{
|
|
36
|
+
title: 'Content',
|
|
37
|
+
fields: [
|
|
38
|
+
{ key: 'badge', label: 'Badge', type: 'text' },
|
|
39
|
+
{ key: 'headline', label: 'Headline', type: 'text' },
|
|
40
|
+
{ key: 'subheadline', label: 'Subheadline', type: 'textarea' },
|
|
41
|
+
{ key: 'primaryCta', label: 'Primary CTA', type: 'text' },
|
|
42
|
+
{ key: 'primaryCtaUrl', label: 'Primary CTA URL', type: 'text' },
|
|
43
|
+
{ key: 'secondaryCta', label: 'Secondary CTA', type: 'text' },
|
|
44
|
+
{ key: 'secondaryCtaUrl', label: 'Secondary CTA URL', type: 'text' },
|
|
45
|
+
{ key: 'heroImage', label: 'Hero Image URL', type: 'text' },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
title: 'Style',
|
|
50
|
+
fields: [
|
|
51
|
+
{ key: 'variant', label: 'Variant', type: 'select', options: ['centered', 'split', 'gradient', 'minimal'] },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
features: {
|
|
57
|
+
sections: [
|
|
58
|
+
{
|
|
59
|
+
title: 'Content',
|
|
60
|
+
fields: [
|
|
61
|
+
{ key: 'label', label: 'Section Label', type: 'text' },
|
|
62
|
+
{ key: 'title', label: 'Title', type: 'text' },
|
|
63
|
+
{ key: 'subtitle', label: 'Subtitle', type: 'text' },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
title: 'Items',
|
|
68
|
+
fields: [
|
|
69
|
+
{ key: 'items', label: 'Feature Cards', type: 'array-items' },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
title: 'Style',
|
|
74
|
+
fields: [
|
|
75
|
+
{ key: 'variant', label: 'Variant', type: 'select', options: ['grid', 'list', 'alternating'] },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
pricing: {
|
|
81
|
+
sections: [
|
|
82
|
+
{
|
|
83
|
+
title: 'Content',
|
|
84
|
+
fields: [
|
|
85
|
+
{ key: 'title', label: 'Title', type: 'text' },
|
|
86
|
+
{ key: 'subtitle', label: 'Subtitle', type: 'text' },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
title: 'Style',
|
|
91
|
+
fields: [
|
|
92
|
+
{ key: 'variant', label: 'Variant', type: 'select', options: ['simple', 'comparison'] },
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
cta: {
|
|
98
|
+
sections: [
|
|
99
|
+
{
|
|
100
|
+
title: 'Content',
|
|
101
|
+
fields: [
|
|
102
|
+
{ key: 'headline', label: 'Headline', type: 'text' },
|
|
103
|
+
{ key: 'subheadline', label: 'Subheadline', type: 'text' },
|
|
104
|
+
{ key: 'buttonText', label: 'Button Text', type: 'text' },
|
|
105
|
+
{ key: 'buttonUrl', label: 'Button URL', type: 'text' },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
title: 'Style',
|
|
110
|
+
fields: [
|
|
111
|
+
{ key: 'variant', label: 'Variant', type: 'select', options: ['simple', 'split'] },
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
footer: {
|
|
117
|
+
sections: [
|
|
118
|
+
{
|
|
119
|
+
title: 'Content',
|
|
120
|
+
fields: [
|
|
121
|
+
{ key: 'logo', label: 'Logo Text', type: 'text' },
|
|
122
|
+
{ key: 'logoImage', label: 'Logo Image URL', type: 'text' },
|
|
123
|
+
{ key: 'copyright', label: 'Copyright', type: 'text' },
|
|
124
|
+
{ key: 'links', label: 'Links', type: 'array-strings' },
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
title: 'Style',
|
|
129
|
+
fields: [
|
|
130
|
+
{ key: 'variant', label: 'Variant', type: 'select', options: ['simple', 'multi-column', 'minimal'] },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
testimonials: {
|
|
136
|
+
sections: [
|
|
137
|
+
{
|
|
138
|
+
title: 'Content',
|
|
139
|
+
fields: [
|
|
140
|
+
{ key: 'title', label: 'Title', type: 'text' },
|
|
141
|
+
{ key: 'subtitle', label: 'Subtitle', type: 'text' },
|
|
142
|
+
{ key: 'items', label: 'Testimonials', type: 'array-items' },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
title: 'Style',
|
|
147
|
+
fields: [
|
|
148
|
+
{ key: 'variant', label: 'Variant', type: 'select', options: ['cards', 'carousel', 'spotlight'] },
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
stats: {
|
|
154
|
+
sections: [
|
|
155
|
+
{
|
|
156
|
+
title: 'Content',
|
|
157
|
+
fields: [
|
|
158
|
+
{ key: 'title', label: 'Title', type: 'text' },
|
|
159
|
+
{ key: 'items', label: 'Stats', type: 'array-items' },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
title: 'Style',
|
|
164
|
+
fields: [
|
|
165
|
+
{ key: 'variant', label: 'Variant', type: 'select', options: ['grid', 'bar', 'counter'] },
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
faq: {
|
|
171
|
+
sections: [
|
|
172
|
+
{
|
|
173
|
+
title: 'Content',
|
|
174
|
+
fields: [
|
|
175
|
+
{ key: 'title', label: 'Title', type: 'text' },
|
|
176
|
+
{ key: 'subtitle', label: 'Subtitle', type: 'text' },
|
|
177
|
+
{ key: 'items', label: 'Questions', type: 'array-items' },
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
},
|
|
182
|
+
team: {
|
|
183
|
+
sections: [
|
|
184
|
+
{
|
|
185
|
+
title: 'Content',
|
|
186
|
+
fields: [
|
|
187
|
+
{ key: 'title', label: 'Title', type: 'text' },
|
|
188
|
+
{ key: 'subtitle', label: 'Subtitle', type: 'text' },
|
|
189
|
+
{ key: 'members', label: 'Members', type: 'array-items' },
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
contact: {
|
|
195
|
+
sections: [
|
|
196
|
+
{
|
|
197
|
+
title: 'Content',
|
|
198
|
+
fields: [
|
|
199
|
+
{ key: 'title', label: 'Title', type: 'text' },
|
|
200
|
+
{ key: 'subtitle', label: 'Subtitle', type: 'text' },
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
newsletter: {
|
|
206
|
+
sections: [
|
|
207
|
+
{
|
|
208
|
+
title: 'Content',
|
|
209
|
+
fields: [
|
|
210
|
+
{ key: 'title', label: 'Title', type: 'text' },
|
|
211
|
+
{ key: 'subtitle', label: 'Subtitle', type: 'text' },
|
|
212
|
+
{ key: 'buttonText', label: 'Button Text', type: 'text' },
|
|
213
|
+
{ key: 'socialProof', label: 'Social Proof', type: 'text' },
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
logocloud: {
|
|
219
|
+
sections: [
|
|
220
|
+
{
|
|
221
|
+
title: 'Content',
|
|
222
|
+
fields: [
|
|
223
|
+
{ key: 'title', label: 'Title', type: 'text' },
|
|
224
|
+
{ key: 'logos', label: 'Logos', type: 'array-strings' },
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
content: {
|
|
230
|
+
sections: [
|
|
231
|
+
{
|
|
232
|
+
title: 'Content',
|
|
233
|
+
fields: [
|
|
234
|
+
{ key: 'body', label: 'Body', type: 'textarea' },
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
title: 'Style',
|
|
239
|
+
fields: [
|
|
240
|
+
{ key: 'variant', label: 'Variant', type: 'select', options: ['prose', 'columns', 'highlight'] },
|
|
241
|
+
],
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
image: {
|
|
246
|
+
sections: [
|
|
247
|
+
{
|
|
248
|
+
title: 'Content',
|
|
249
|
+
fields: [
|
|
250
|
+
{ key: 'src', label: 'Image URL', type: 'text' },
|
|
251
|
+
{ key: 'alt', label: 'Alt Text', type: 'text' },
|
|
252
|
+
{ key: 'title', label: 'Title', type: 'text' },
|
|
253
|
+
{ key: 'subtitle', label: 'Subtitle', type: 'text' },
|
|
254
|
+
{ key: 'imageSide', label: 'Image Side', type: 'select', options: ['left', 'right'] },
|
|
255
|
+
],
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
title: 'Grid Images',
|
|
259
|
+
fields: [
|
|
260
|
+
{ key: 'images', label: 'Images', type: 'array-items' },
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
title: 'Style',
|
|
265
|
+
fields: [
|
|
266
|
+
{ key: 'variant', label: 'Variant', type: 'select', options: ['hero-image', 'side-by-side', 'grid'] },
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
video: {
|
|
272
|
+
sections: [
|
|
273
|
+
{
|
|
274
|
+
title: 'Content',
|
|
275
|
+
fields: [
|
|
276
|
+
{ key: 'url', label: 'Video URL', type: 'text' },
|
|
277
|
+
{ key: 'title', label: 'Title', type: 'text' },
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
title: 'Style',
|
|
282
|
+
fields: [
|
|
283
|
+
{ key: 'variant', label: 'Platform', type: 'select', options: ['youtube', 'vimeo'] },
|
|
284
|
+
],
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
gallery: {
|
|
289
|
+
sections: [
|
|
290
|
+
{
|
|
291
|
+
title: 'Content',
|
|
292
|
+
fields: [
|
|
293
|
+
{ key: 'title', label: 'Title', type: 'text' },
|
|
294
|
+
{ key: 'images', label: 'Images', type: 'array-items' },
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
title: 'Style',
|
|
299
|
+
fields: [
|
|
300
|
+
{ key: 'variant', label: 'Variant', type: 'select', options: ['grid', 'masonry'] },
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
divider: {
|
|
306
|
+
sections: [
|
|
307
|
+
{
|
|
308
|
+
title: 'Style',
|
|
309
|
+
fields: [
|
|
310
|
+
{ key: 'variant', label: 'Variant', type: 'select', options: ['line', 'space', 'dots'] },
|
|
311
|
+
{ key: 'width', label: 'Width', type: 'select', options: ['full', 'centered', 'narrow'] },
|
|
312
|
+
{ key: 'height', label: 'Height (px)', type: 'text' },
|
|
313
|
+
],
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
banner: {
|
|
318
|
+
sections: [
|
|
319
|
+
{
|
|
320
|
+
title: 'Content',
|
|
321
|
+
fields: [
|
|
322
|
+
{ key: 'text', label: 'Text', type: 'text' },
|
|
323
|
+
{ key: 'linkText', label: 'Link Text', type: 'text' },
|
|
324
|
+
{ key: 'linkUrl', label: 'Link URL', type: 'text' },
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
title: 'Style',
|
|
329
|
+
fields: [
|
|
330
|
+
{ key: 'variant', label: 'Variant', type: 'select', options: ['ribbon', 'bar'] },
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
},
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function PropertyField({ field, block }: { field: FieldDef; block: BlockConfig }) {
|
|
338
|
+
const updateBlockProps = useConfigStore((s) => s.updateBlockProps)
|
|
339
|
+
const updateBlock = useConfigStore((s) => s.updateBlock)
|
|
340
|
+
|
|
341
|
+
// For variant field, it's on the block itself
|
|
342
|
+
const value = field.key === 'variant'
|
|
343
|
+
? block.variant
|
|
344
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
345
|
+
: (block.props as any)[field.key]
|
|
346
|
+
|
|
347
|
+
const onChange = (newValue: unknown) => {
|
|
348
|
+
if (field.key === 'variant') {
|
|
349
|
+
updateBlock(block.id, { variant: newValue as string })
|
|
350
|
+
} else {
|
|
351
|
+
updateBlockProps(block.id, { [field.key]: newValue })
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
switch (field.type) {
|
|
356
|
+
case 'text':
|
|
357
|
+
return (
|
|
358
|
+
<div className="mb-2.5">
|
|
359
|
+
<label className="block text-[11.5px] text-text-2 mb-1 font-medium">{field.label}</label>
|
|
360
|
+
<input
|
|
361
|
+
type="text"
|
|
362
|
+
value={String(value || '')}
|
|
363
|
+
onChange={(e) => onChange(e.target.value)}
|
|
364
|
+
className="w-full px-2 py-1.5 rounded border border-border-default bg-bg-2 text-text-0 text-xs outline-none focus:border-green"
|
|
365
|
+
/>
|
|
366
|
+
</div>
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
case 'textarea':
|
|
370
|
+
return (
|
|
371
|
+
<div className="mb-2.5">
|
|
372
|
+
<label className="block text-[11.5px] text-text-2 mb-1 font-medium">{field.label}</label>
|
|
373
|
+
<textarea
|
|
374
|
+
value={String(value || '')}
|
|
375
|
+
onChange={(e) => onChange(e.target.value)}
|
|
376
|
+
rows={3}
|
|
377
|
+
className="w-full px-2 py-1.5 rounded border border-border-default bg-bg-2 text-text-0 text-xs outline-none focus:border-green resize-y"
|
|
378
|
+
/>
|
|
379
|
+
</div>
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
case 'select':
|
|
383
|
+
return (
|
|
384
|
+
<div className="mb-2.5">
|
|
385
|
+
<label className="block text-[11.5px] text-text-2 mb-1 font-medium">{field.label}</label>
|
|
386
|
+
<select
|
|
387
|
+
value={String(value || '')}
|
|
388
|
+
onChange={(e) => onChange(e.target.value)}
|
|
389
|
+
className="w-full px-2 py-1.5 rounded border border-border-default bg-bg-2 text-text-0 text-xs outline-none focus:border-green cursor-pointer"
|
|
390
|
+
>
|
|
391
|
+
{field.options?.map((opt) => (
|
|
392
|
+
<option key={opt} value={opt}>{opt}</option>
|
|
393
|
+
))}
|
|
394
|
+
</select>
|
|
395
|
+
</div>
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
case 'array-strings': {
|
|
399
|
+
const items = (Array.isArray(value) ? value : []) as string[]
|
|
400
|
+
return (
|
|
401
|
+
<div className="mb-2.5">
|
|
402
|
+
<label className="block text-[11.5px] text-text-2 mb-1 font-medium">{field.label}</label>
|
|
403
|
+
{items.map((item, i) => (
|
|
404
|
+
<div key={i} className="flex gap-1 mb-1">
|
|
405
|
+
<input
|
|
406
|
+
type="text"
|
|
407
|
+
value={item}
|
|
408
|
+
onChange={(e) => {
|
|
409
|
+
const updated = [...items]
|
|
410
|
+
updated[i] = e.target.value
|
|
411
|
+
onChange(updated)
|
|
412
|
+
}}
|
|
413
|
+
className="flex-1 px-2 py-1 rounded border border-border-default bg-bg-2 text-text-0 text-xs outline-none focus:border-green"
|
|
414
|
+
/>
|
|
415
|
+
<button
|
|
416
|
+
onClick={() => onChange(items.filter((_, idx) => idx !== i))}
|
|
417
|
+
className="px-1.5 text-text-3 hover:text-status-red text-xs transition-colors"
|
|
418
|
+
>
|
|
419
|
+
x
|
|
420
|
+
</button>
|
|
421
|
+
</div>
|
|
422
|
+
))}
|
|
423
|
+
<button
|
|
424
|
+
onClick={() => onChange([...items, ''])}
|
|
425
|
+
className="text-[10px] text-green hover:text-green-dim transition-colors mt-0.5"
|
|
426
|
+
>
|
|
427
|
+
+ Add item
|
|
428
|
+
</button>
|
|
429
|
+
</div>
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
case 'array-items': {
|
|
434
|
+
const items = (Array.isArray(value) ? value : []) as Array<Record<string, string>>
|
|
435
|
+
|
|
436
|
+
// Infer new item shape from existing items, or use sensible defaults per field key
|
|
437
|
+
function createEmptyItem(): Record<string, string> {
|
|
438
|
+
if (items.length > 0) {
|
|
439
|
+
const template: Record<string, string> = {}
|
|
440
|
+
for (const key of Object.keys(items[0])) template[key] = ''
|
|
441
|
+
return template
|
|
442
|
+
}
|
|
443
|
+
// Fallback templates by block type + field key
|
|
444
|
+
const blockTemplates: Partial<Record<string, Record<string, Record<string, string>>>> = {
|
|
445
|
+
testimonials: { items: { name: '', role: '', quote: '' } },
|
|
446
|
+
stats: { items: { value: '', label: '' } },
|
|
447
|
+
faq: { items: { question: '', answer: '' } },
|
|
448
|
+
team: { members: { name: '', role: '' } },
|
|
449
|
+
features: { items: { title: '', description: '' } },
|
|
450
|
+
image: { images: { src: '', alt: '' } },
|
|
451
|
+
gallery: { images: { src: '', alt: '', caption: '' } },
|
|
452
|
+
}
|
|
453
|
+
return blockTemplates[block.type]?.[field.key] || { title: '', description: '' }
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return (
|
|
457
|
+
<div className="mb-2.5">
|
|
458
|
+
<label className="block text-[11.5px] text-text-2 mb-1 font-medium">{field.label}</label>
|
|
459
|
+
{items.map((item, i) => (
|
|
460
|
+
<div key={i} className="bg-bg-2 border border-border-default rounded p-2 mb-1.5">
|
|
461
|
+
<div className="flex items-center justify-between mb-1.5">
|
|
462
|
+
<span className="text-[10px] text-text-3 font-medium">Item {i + 1}</span>
|
|
463
|
+
<button
|
|
464
|
+
onClick={() => onChange(items.filter((_, idx) => idx !== i))}
|
|
465
|
+
className="text-[10px] text-text-3 hover:text-status-red transition-colors"
|
|
466
|
+
>
|
|
467
|
+
Remove
|
|
468
|
+
</button>
|
|
469
|
+
</div>
|
|
470
|
+
{Object.entries(item).map(([key, val]) => (
|
|
471
|
+
<div key={key} className="mb-1">
|
|
472
|
+
<label className="block text-[10px] text-text-3 mb-0.5">{key}</label>
|
|
473
|
+
<input
|
|
474
|
+
type="text"
|
|
475
|
+
value={String(val)}
|
|
476
|
+
onChange={(e) => {
|
|
477
|
+
const updated = [...items]
|
|
478
|
+
updated[i] = { ...updated[i], [key]: e.target.value }
|
|
479
|
+
onChange(updated)
|
|
480
|
+
}}
|
|
481
|
+
className="w-full px-1.5 py-1 rounded border border-border-subtle bg-bg-3 text-text-0 text-[11px] outline-none focus:border-green"
|
|
482
|
+
/>
|
|
483
|
+
</div>
|
|
484
|
+
))}
|
|
485
|
+
</div>
|
|
486
|
+
))}
|
|
487
|
+
<button
|
|
488
|
+
onClick={() => onChange([...items, createEmptyItem()])}
|
|
489
|
+
className="text-[10px] text-green hover:text-green-dim transition-colors"
|
|
490
|
+
>
|
|
491
|
+
+ Add item
|
|
492
|
+
</button>
|
|
493
|
+
</div>
|
|
494
|
+
)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
default:
|
|
498
|
+
return null
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
|
503
|
+
const [open, setOpen] = useState(true)
|
|
504
|
+
return (
|
|
505
|
+
<div className="border-b border-border-subtle">
|
|
506
|
+
<button
|
|
507
|
+
onClick={() => setOpen(!open)}
|
|
508
|
+
className="w-full px-3.5 py-2.5 flex items-center gap-1 text-[10px] font-semibold uppercase tracking-wider text-text-3 hover:text-text-2 transition-colors"
|
|
509
|
+
>
|
|
510
|
+
{open ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
|
511
|
+
{title}
|
|
512
|
+
</button>
|
|
513
|
+
{open && <div className="px-3.5 pb-3">{children}</div>}
|
|
514
|
+
</div>
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export function PropertiesPanel({ block }: { block: BlockConfig }) {
|
|
519
|
+
const [showJson, setShowJson] = useState(false)
|
|
520
|
+
const schema = blockFields[block.type]
|
|
521
|
+
|
|
522
|
+
return (
|
|
523
|
+
<>
|
|
524
|
+
{/* Header */}
|
|
525
|
+
<div className="px-3.5 py-3 border-b border-border-default flex items-center justify-between">
|
|
526
|
+
<span className="text-[11px] font-semibold uppercase tracking-wider text-text-2">
|
|
527
|
+
Properties
|
|
528
|
+
</span>
|
|
529
|
+
<span className="text-[10px] px-2 py-0.5 rounded-full bg-green-glow text-green font-semibold">
|
|
530
|
+
{block.type}
|
|
531
|
+
</span>
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
{/* Property sections */}
|
|
535
|
+
{schema?.sections.map((section) => (
|
|
536
|
+
<Section key={section.title} title={section.title}>
|
|
537
|
+
{section.fields.map((field) => (
|
|
538
|
+
<PropertyField key={field.key} field={field} block={block} />
|
|
539
|
+
))}
|
|
540
|
+
</Section>
|
|
541
|
+
)) || (
|
|
542
|
+
<div className="p-3.5 text-[11px] text-text-3">
|
|
543
|
+
No editable properties defined for this block type.
|
|
544
|
+
</div>
|
|
545
|
+
)}
|
|
546
|
+
|
|
547
|
+
{/* View JSON toggle */}
|
|
548
|
+
<div className="border-t border-border-subtle">
|
|
549
|
+
<button
|
|
550
|
+
onClick={() => setShowJson(!showJson)}
|
|
551
|
+
className="w-full px-3.5 py-2 flex items-center gap-1.5 text-[10px] text-text-3 hover:text-text-2 transition-colors"
|
|
552
|
+
>
|
|
553
|
+
<Code size={11} />
|
|
554
|
+
{showJson ? 'Hide' : 'View'} Block JSON
|
|
555
|
+
</button>
|
|
556
|
+
{showJson && (
|
|
557
|
+
<pre className="px-3.5 pb-3 text-[10px] font-mono text-text-2 leading-relaxed overflow-x-auto max-h-48 overflow-y-auto">
|
|
558
|
+
{JSON.stringify({ id: block.id, type: block.type, variant: block.variant, props: block.props }, null, 2)}
|
|
559
|
+
</pre>
|
|
560
|
+
)}
|
|
561
|
+
</div>
|
|
562
|
+
</>
|
|
563
|
+
)
|
|
564
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { MousePointer2 } from 'lucide-react'
|
|
3
|
+
import { useEditorStore } from '@/store/editorStore'
|
|
4
|
+
import { useConfigStore } from '@/store/configStore'
|
|
5
|
+
import { PropertiesPanel } from './PropertiesPanel'
|
|
6
|
+
import { DesignPanel } from './DesignPanel'
|
|
7
|
+
|
|
8
|
+
type Tab = 'properties' | 'design'
|
|
9
|
+
|
|
10
|
+
export function RightSidebar() {
|
|
11
|
+
const selectedBlockId = useEditorStore((s) => s.selectedBlockId)
|
|
12
|
+
const blocks = useConfigStore((s) => {
|
|
13
|
+
const pages = s.config.pages
|
|
14
|
+
if (!pages || pages.length === 0) return s.config.blocks
|
|
15
|
+
const page = pages.find((p) => p.id === s.activePageId) ?? pages[0]
|
|
16
|
+
return page.blocks
|
|
17
|
+
})
|
|
18
|
+
const selectedBlock = blocks.find((b) => b.id === selectedBlockId)
|
|
19
|
+
const [tab, setTab] = useState<Tab>('properties')
|
|
20
|
+
|
|
21
|
+
// Auto-switch to Properties when a (newly) selected block changes, computed
|
|
22
|
+
// during render instead of via an effect + setState (avoids cascading renders).
|
|
23
|
+
// Pattern: https://react.dev/reference/react/useState#storing-information-from-previous-renders
|
|
24
|
+
const [lastSelectedId, setLastSelectedId] = useState<string | null>(selectedBlockId)
|
|
25
|
+
let activeTab = tab
|
|
26
|
+
if (selectedBlockId !== lastSelectedId) {
|
|
27
|
+
setLastSelectedId(selectedBlockId)
|
|
28
|
+
if (selectedBlock) {
|
|
29
|
+
activeTab = 'properties'
|
|
30
|
+
if (tab !== 'properties') setTab('properties')
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="hidden md:flex w-[280px] bg-bg-1 border-l border-border-default flex-col shrink-0">
|
|
36
|
+
{/* Tabs */}
|
|
37
|
+
<div className="flex border-b border-border-default shrink-0">
|
|
38
|
+
<button
|
|
39
|
+
onClick={() => setTab('properties')}
|
|
40
|
+
className={`flex-1 py-2 text-[11px] font-medium transition-colors ${
|
|
41
|
+
activeTab === 'properties'
|
|
42
|
+
? 'text-text-0 border-b border-green'
|
|
43
|
+
: 'text-text-3 hover:text-text-1'
|
|
44
|
+
}`}
|
|
45
|
+
>
|
|
46
|
+
Properties
|
|
47
|
+
</button>
|
|
48
|
+
<button
|
|
49
|
+
onClick={() => setTab('design')}
|
|
50
|
+
className={`flex-1 py-2 text-[11px] font-medium transition-colors ${
|
|
51
|
+
activeTab === 'design'
|
|
52
|
+
? 'text-text-0 border-b border-green'
|
|
53
|
+
: 'text-text-3 hover:text-text-1'
|
|
54
|
+
}`}
|
|
55
|
+
>
|
|
56
|
+
Design
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{/* Content */}
|
|
61
|
+
<div className="flex-1 overflow-y-auto">
|
|
62
|
+
{activeTab === 'design' ? (
|
|
63
|
+
<DesignPanel />
|
|
64
|
+
) : selectedBlock ? (
|
|
65
|
+
<>
|
|
66
|
+
<PropertiesPanel block={selectedBlock} />
|
|
67
|
+
<div className="mt-auto px-3.5 py-2.5 font-mono text-[10.5px] text-text-3 break-all border-t border-border-subtle">
|
|
68
|
+
config.blocks[{blocks.indexOf(selectedBlock)}]
|
|
69
|
+
</div>
|
|
70
|
+
</>
|
|
71
|
+
) : (
|
|
72
|
+
<div className="flex flex-col items-center justify-center text-center px-6 py-16 gap-3">
|
|
73
|
+
<div className="w-10 h-10 rounded-lg bg-bg-3 border border-border-default flex items-center justify-center">
|
|
74
|
+
<MousePointer2 size={16} className="text-text-3" />
|
|
75
|
+
</div>
|
|
76
|
+
<div>
|
|
77
|
+
<p className="text-text-1 text-[12px] font-medium">Click a block to edit</p>
|
|
78
|
+
<p className="text-text-3 text-[11px] mt-1">Select any block on the canvas to see its properties here</p>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
}
|