cmssy-cli 0.21.0 → 0.24.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.
Files changed (70) hide show
  1. package/config.d.ts +1 -1
  2. package/dist/cli.js +131 -30
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/build.d.ts +1 -0
  5. package/dist/commands/build.d.ts.map +1 -1
  6. package/dist/commands/build.js +56 -12
  7. package/dist/commands/build.js.map +1 -1
  8. package/dist/commands/create.d.ts.map +1 -1
  9. package/dist/commands/create.js +22 -2
  10. package/dist/commands/create.js.map +1 -1
  11. package/dist/commands/dev.d.ts.map +1 -1
  12. package/dist/commands/dev.js +652 -410
  13. package/dist/commands/dev.js.map +1 -1
  14. package/dist/commands/init.d.ts.map +1 -1
  15. package/dist/commands/init.js +3 -1
  16. package/dist/commands/init.js.map +1 -1
  17. package/dist/commands/migrate.d.ts.map +1 -1
  18. package/dist/commands/migrate.js +3 -1
  19. package/dist/commands/migrate.js.map +1 -1
  20. package/dist/dev-ui/app.js +166 -19
  21. package/dist/dev-ui/index.html +138 -0
  22. package/dist/dev-ui-react/App.tsx +164 -0
  23. package/dist/dev-ui-react/__tests__/previewData.test.ts +193 -0
  24. package/dist/dev-ui-react/components/BlocksList.tsx +232 -0
  25. package/dist/dev-ui-react/components/Editor.tsx +469 -0
  26. package/dist/dev-ui-react/components/Preview.tsx +146 -0
  27. package/dist/dev-ui-react/hooks/useBlocks.ts +80 -0
  28. package/dist/dev-ui-react/index.html +13 -0
  29. package/dist/dev-ui-react/main.tsx +8 -0
  30. package/dist/dev-ui-react/styles.css +856 -0
  31. package/dist/dev-ui-react/types.ts +45 -0
  32. package/dist/types/block-config.d.ts +100 -2
  33. package/dist/types/block-config.d.ts.map +1 -1
  34. package/dist/types/block-config.js +6 -1
  35. package/dist/types/block-config.js.map +1 -1
  36. package/dist/utils/block-config.js +3 -3
  37. package/dist/utils/block-config.js.map +1 -1
  38. package/dist/utils/blocks-meta-cache.d.ts +28 -0
  39. package/dist/utils/blocks-meta-cache.d.ts.map +1 -0
  40. package/dist/utils/blocks-meta-cache.js +72 -0
  41. package/dist/utils/blocks-meta-cache.js.map +1 -0
  42. package/dist/utils/builder.d.ts +3 -0
  43. package/dist/utils/builder.d.ts.map +1 -1
  44. package/dist/utils/builder.js +17 -14
  45. package/dist/utils/builder.js.map +1 -1
  46. package/dist/utils/field-schema.d.ts +2 -0
  47. package/dist/utils/field-schema.d.ts.map +1 -1
  48. package/dist/utils/field-schema.js +21 -4
  49. package/dist/utils/field-schema.js.map +1 -1
  50. package/dist/utils/scanner.d.ts +5 -3
  51. package/dist/utils/scanner.d.ts.map +1 -1
  52. package/dist/utils/scanner.js +23 -16
  53. package/dist/utils/scanner.js.map +1 -1
  54. package/dist/utils/type-generator.d.ts +7 -1
  55. package/dist/utils/type-generator.d.ts.map +1 -1
  56. package/dist/utils/type-generator.js +58 -41
  57. package/dist/utils/type-generator.js.map +1 -1
  58. package/package.json +8 -3
  59. package/dist/commands/deploy.d.ts +0 -9
  60. package/dist/commands/deploy.d.ts.map +0 -1
  61. package/dist/commands/deploy.js +0 -226
  62. package/dist/commands/deploy.js.map +0 -1
  63. package/dist/commands/push.d.ts +0 -9
  64. package/dist/commands/push.d.ts.map +0 -1
  65. package/dist/commands/push.js +0 -199
  66. package/dist/commands/push.js.map +0 -1
  67. package/dist/utils/blockforge-config.d.ts +0 -19
  68. package/dist/utils/blockforge-config.d.ts.map +0 -1
  69. package/dist/utils/blockforge-config.js +0 -19
  70. package/dist/utils/blockforge-config.js.map +0 -1
@@ -0,0 +1,469 @@
1
+ import { useState } from 'react';
2
+ import { Block, FieldConfig } from '../types';
3
+
4
+ interface EditorProps {
5
+ block: Block | null;
6
+ loading: boolean;
7
+ previewData: Record<string, unknown>;
8
+ onPreviewDataChange: (data: Record<string, unknown>) => void;
9
+ onNavigateToPage?: (pageSlug: string) => void;
10
+ }
11
+
12
+ export function Editor({
13
+ block,
14
+ loading,
15
+ previewData,
16
+ onPreviewDataChange,
17
+ onNavigateToPage,
18
+ }: EditorProps) {
19
+ const [collapsed, setCollapsed] = useState(false);
20
+
21
+ if (collapsed) {
22
+ return (
23
+ <div className="editor-panel collapsed">
24
+ <div className="editor-header">
25
+ <button className="panel-toggle" onClick={() => setCollapsed(false)} title="Expand">
26
+ <span className="toggle-icon">☰</span>
27
+ </button>
28
+ </div>
29
+ </div>
30
+ );
31
+ }
32
+
33
+ const isTemplate = block?.type === 'template' && block.pages && block.pages.length > 0;
34
+
35
+ return (
36
+ <div className="editor-panel">
37
+ <div className="editor-header">
38
+ <button className="panel-toggle" onClick={() => setCollapsed(true)} title="Collapse">
39
+ <span className="toggle-icon">☰</span>
40
+ </button>
41
+ <div className="editor-header-content">
42
+ <h2>Properties</h2>
43
+ <p>{block?.name || 'No block selected'}</p>
44
+ </div>
45
+ </div>
46
+ <div className="editor-content">
47
+ {loading ? (
48
+ <div className="loading">
49
+ <div className="spinner" />
50
+ <span>Loading properties...</span>
51
+ </div>
52
+ ) : !block ? (
53
+ <div className="editor-empty">Select a block to edit its properties</div>
54
+ ) : isTemplate ? (
55
+ <TemplateEditor block={block} onNavigateToPage={onNavigateToPage} />
56
+ ) : block.schema ? (
57
+ <SchemaEditor
58
+ schema={block.schema}
59
+ data={previewData}
60
+ onChange={onPreviewDataChange}
61
+ />
62
+ ) : (
63
+ <div className="editor-empty">No schema defined for this block</div>
64
+ )}
65
+ </div>
66
+ </div>
67
+ );
68
+ }
69
+
70
+ // Template editor component
71
+ function TemplateEditor({
72
+ block,
73
+ onNavigateToPage,
74
+ }: {
75
+ block: Block;
76
+ onNavigateToPage?: (pageSlug: string) => void;
77
+ }) {
78
+ const pages = block.pages || [];
79
+ const layoutSlots = block.layoutSlots || [];
80
+
81
+ return (
82
+ <div className="template-editor">
83
+ <div className="template-info">
84
+ <div className="template-info-badge">Template</div>
85
+ <p className="template-info-desc">{block.description || 'No description'}</p>
86
+ </div>
87
+
88
+ <div className="template-section">
89
+ <h4 className="template-section-title">Pages ({pages.length})</h4>
90
+ <div className="template-pages-list">
91
+ {pages.length === 0 ? (
92
+ <div className="editor-empty">No pages defined</div>
93
+ ) : (
94
+ pages.map((page) => (
95
+ <div
96
+ key={page.slug}
97
+ className="template-page-item"
98
+ onClick={() => onNavigateToPage?.(page.slug)}
99
+ >
100
+ <div className="template-page-header">
101
+ <span className="template-page-name">{page.name}</span>
102
+ <span className="template-page-blocks">{page.blocksCount} blocks</span>
103
+ </div>
104
+ <div className="template-page-slug">/{page.slug}</div>
105
+ </div>
106
+ ))
107
+ )}
108
+ </div>
109
+ </div>
110
+
111
+ {layoutSlots.length > 0 && (
112
+ <div className="template-section">
113
+ <h4 className="template-section-title">Layout Slots</h4>
114
+ {layoutSlots.map((slot) => (
115
+ <div key={slot.slot} className="template-layout-slot">
116
+ <span className="slot-type">{slot.slot}</span>
117
+ <span className="slot-block">{slot.type}</span>
118
+ </div>
119
+ ))}
120
+ </div>
121
+ )}
122
+
123
+ <div className="template-hint">
124
+ <p>Click on a page to preview it, or use the tabs in the preview header.</p>
125
+ </div>
126
+ </div>
127
+ );
128
+ }
129
+
130
+ // Schema-based field editor
131
+ function SchemaEditor({
132
+ schema,
133
+ data,
134
+ onChange,
135
+ prefix = '',
136
+ }: {
137
+ schema: Record<string, FieldConfig>;
138
+ data: Record<string, unknown>;
139
+ onChange: (data: Record<string, unknown>) => void;
140
+ prefix?: string;
141
+ }) {
142
+ const updateField = (key: string, value: unknown) => {
143
+ const newData = { ...data, [key]: value };
144
+ onChange(newData);
145
+ };
146
+
147
+ return (
148
+ <>
149
+ {Object.entries(schema).map(([key, field]) => (
150
+ <FieldEditor
151
+ key={prefix + key}
152
+ fieldKey={key}
153
+ field={field}
154
+ value={data[key]}
155
+ onChange={(value) => updateField(key, value)}
156
+ />
157
+ ))}
158
+ </>
159
+ );
160
+ }
161
+
162
+ // Individual field editor
163
+ function FieldEditor({
164
+ fieldKey,
165
+ field,
166
+ value,
167
+ onChange,
168
+ }: {
169
+ fieldKey: string;
170
+ field: FieldConfig;
171
+ value: unknown;
172
+ onChange: (value: unknown) => void;
173
+ }) {
174
+ const required = field.required ? <span className="field-required">*</span> : null;
175
+
176
+ // Boolean field
177
+ if (field.type === 'boolean') {
178
+ return (
179
+ <div className="field-group">
180
+ <label className="field-checkbox-label">
181
+ <input
182
+ type="checkbox"
183
+ checked={Boolean(value ?? field.defaultValue)}
184
+ onChange={(e) => onChange(e.target.checked)}
185
+ />
186
+ <span>{field.label}</span>
187
+ </label>
188
+ {field.helpText && <div className="field-help">{field.helpText}</div>}
189
+ </div>
190
+ );
191
+ }
192
+
193
+ // Text fields
194
+ if (field.type === 'singleLine' || field.type === 'text' || field.type === 'string') {
195
+ return (
196
+ <div className="field-group">
197
+ <label className="field-label">
198
+ {field.label}
199
+ {required}
200
+ </label>
201
+ <input
202
+ type="text"
203
+ className="field-input"
204
+ value={String(value ?? field.defaultValue ?? '')}
205
+ placeholder={field.placeholder}
206
+ onChange={(e) => onChange(e.target.value)}
207
+ />
208
+ {field.helpText && <div className="field-help">{field.helpText}</div>}
209
+ </div>
210
+ );
211
+ }
212
+
213
+ // Multiline text
214
+ if (field.type === 'multiLine' || field.type === 'richText') {
215
+ return (
216
+ <div className="field-group">
217
+ <label className="field-label">
218
+ {field.label}
219
+ {required}
220
+ </label>
221
+ <textarea
222
+ className="field-input field-textarea"
223
+ value={String(value ?? field.defaultValue ?? '')}
224
+ placeholder={field.placeholder}
225
+ onChange={(e) => onChange(e.target.value)}
226
+ />
227
+ {field.helpText && <div className="field-help">{field.helpText}</div>}
228
+ </div>
229
+ );
230
+ }
231
+
232
+ // Number field
233
+ if (field.type === 'number' || field.type === 'numeric') {
234
+ return (
235
+ <div className="field-group">
236
+ <label className="field-label">
237
+ {field.label}
238
+ {required}
239
+ </label>
240
+ <input
241
+ type="number"
242
+ className="field-input"
243
+ value={value !== undefined ? String(value) : String(field.defaultValue ?? '')}
244
+ placeholder={field.placeholder}
245
+ onChange={(e) => onChange(e.target.value ? parseFloat(e.target.value) : '')}
246
+ />
247
+ {field.helpText && <div className="field-help">{field.helpText}</div>}
248
+ </div>
249
+ );
250
+ }
251
+
252
+ // Date field
253
+ if (field.type === 'date') {
254
+ return (
255
+ <div className="field-group">
256
+ <label className="field-label">
257
+ {field.label}
258
+ {required}
259
+ </label>
260
+ <input
261
+ type="date"
262
+ className="field-input"
263
+ value={String(value ?? field.defaultValue ?? '')}
264
+ onChange={(e) => onChange(e.target.value)}
265
+ />
266
+ {field.helpText && <div className="field-help">{field.helpText}</div>}
267
+ </div>
268
+ );
269
+ }
270
+
271
+ // Link field
272
+ if (field.type === 'link') {
273
+ const linkValue = (value as { url?: string; text?: string; target?: string }) ?? {};
274
+ return (
275
+ <div className="field-group">
276
+ <label className="field-label">
277
+ {field.label}
278
+ {required}
279
+ </label>
280
+ <div className="link-field">
281
+ <input
282
+ type="url"
283
+ className="field-input"
284
+ placeholder="URL"
285
+ value={linkValue.url || ''}
286
+ onChange={(e) => onChange({ ...linkValue, url: e.target.value })}
287
+ style={{ marginBottom: 8 }}
288
+ />
289
+ <input
290
+ type="text"
291
+ className="field-input"
292
+ placeholder="Link text"
293
+ value={linkValue.text || ''}
294
+ onChange={(e) => onChange({ ...linkValue, text: e.target.value })}
295
+ style={{ marginBottom: 8 }}
296
+ />
297
+ <label className="field-checkbox-label">
298
+ <input
299
+ type="checkbox"
300
+ checked={linkValue.target === '_blank'}
301
+ onChange={(e) => onChange({ ...linkValue, target: e.target.checked ? '_blank' : '_self' })}
302
+ />
303
+ <span>Open in new tab</span>
304
+ </label>
305
+ </div>
306
+ {field.helpText && <div className="field-help">{field.helpText}</div>}
307
+ </div>
308
+ );
309
+ }
310
+
311
+ // Select field
312
+ if (field.type === 'select') {
313
+ return (
314
+ <div className="field-group">
315
+ <label className="field-label">
316
+ {field.label}
317
+ {required}
318
+ </label>
319
+ <select
320
+ className="field-input field-select"
321
+ value={String(value ?? field.defaultValue ?? '')}
322
+ onChange={(e) => onChange(e.target.value)}
323
+ >
324
+ <option value="">Select an option...</option>
325
+ {field.options?.map((opt) => (
326
+ <option key={opt.value} value={opt.value}>
327
+ {opt.label}
328
+ </option>
329
+ ))}
330
+ </select>
331
+ {field.helpText && <div className="field-help">{field.helpText}</div>}
332
+ </div>
333
+ );
334
+ }
335
+
336
+ // Color field
337
+ if (field.type === 'color') {
338
+ const colorValue = String(value ?? field.defaultValue ?? '#000000');
339
+ return (
340
+ <div className="field-group">
341
+ <label className="field-label">
342
+ {field.label}
343
+ {required}
344
+ </label>
345
+ <div className="color-field">
346
+ <input
347
+ type="color"
348
+ className="color-preview"
349
+ value={colorValue}
350
+ onChange={(e) => onChange(e.target.value)}
351
+ />
352
+ <input
353
+ type="text"
354
+ className="field-input color-input"
355
+ value={colorValue}
356
+ onChange={(e) => onChange(e.target.value)}
357
+ />
358
+ </div>
359
+ {field.helpText && <div className="field-help">{field.helpText}</div>}
360
+ </div>
361
+ );
362
+ }
363
+
364
+ // Media field (simple URL string)
365
+ if (field.type === 'media') {
366
+ const mediaUrl = (value as string) || '';
367
+ return (
368
+ <div className="field-group">
369
+ <label className="field-label">
370
+ {field.label}
371
+ {required}
372
+ </label>
373
+ <div className="media-field">
374
+ <div className="media-preview">
375
+ {mediaUrl ? (
376
+ <img src={mediaUrl} alt={field.label} />
377
+ ) : (
378
+ <div className="media-placeholder">No image</div>
379
+ )}
380
+ </div>
381
+ <input
382
+ type="url"
383
+ className="field-input"
384
+ placeholder="Image URL"
385
+ value={mediaUrl}
386
+ onChange={(e) => onChange(e.target.value)}
387
+ />
388
+ </div>
389
+ {field.helpText && <div className="field-help">{field.helpText}</div>}
390
+ </div>
391
+ );
392
+ }
393
+
394
+ // Repeater field
395
+ if (field.type === 'repeater' && field.schema) {
396
+ const items = (value as Record<string, unknown>[]) ?? [];
397
+ const minItems = field.minItems ?? 0;
398
+ const maxItems = field.maxItems ?? 999;
399
+
400
+ const addItem = () => {
401
+ const newItem: Record<string, unknown> = {};
402
+ Object.entries(field.schema!).forEach(([k, f]) => {
403
+ if (f.defaultValue !== undefined) newItem[k] = f.defaultValue;
404
+ });
405
+ onChange([...items, newItem]);
406
+ };
407
+
408
+ const removeItem = (index: number) => {
409
+ onChange(items.filter((_, i) => i !== index));
410
+ };
411
+
412
+ const updateItem = (index: number, newItemData: Record<string, unknown>) => {
413
+ const newItems = [...items];
414
+ newItems[index] = newItemData;
415
+ onChange(newItems);
416
+ };
417
+
418
+ return (
419
+ <div className="field-group">
420
+ <label className="field-label">
421
+ {field.label}
422
+ {required}
423
+ </label>
424
+ <div className="repeater-items">
425
+ {items.length === 0 ? (
426
+ <div style={{ padding: 12, color: '#999', textAlign: 'center' }}>No items yet</div>
427
+ ) : (
428
+ items.map((item, index) => (
429
+ <div key={index} className="repeater-item">
430
+ <div className="repeater-item-header">
431
+ <div className="repeater-item-title">Item {index + 1}</div>
432
+ {items.length > minItems && (
433
+ <button
434
+ type="button"
435
+ className="repeater-item-remove"
436
+ onClick={() => removeItem(index)}
437
+ >
438
+ Remove
439
+ </button>
440
+ )}
441
+ </div>
442
+ <SchemaEditor
443
+ schema={field.schema!}
444
+ data={item}
445
+ onChange={(newData) => updateItem(index, newData)}
446
+ prefix={`${fieldKey}.${index}.`}
447
+ />
448
+ </div>
449
+ ))
450
+ )}
451
+ </div>
452
+ {items.length < maxItems && (
453
+ <button type="button" className="repeater-add" onClick={addItem}>
454
+ + Add Item
455
+ </button>
456
+ )}
457
+ {field.helpText && <div className="field-help">{field.helpText}</div>}
458
+ </div>
459
+ );
460
+ }
461
+
462
+ // Fallback for unsupported types
463
+ return (
464
+ <div className="field-group">
465
+ <label className="field-label">{field.label}</label>
466
+ <div style={{ color: '#999', fontSize: 12 }}>Unsupported field type: {field.type}</div>
467
+ </div>
468
+ );
469
+ }
@@ -0,0 +1,146 @@
1
+ import { useRef, useEffect, memo } from 'react';
2
+ import { Block } from '../types';
3
+
4
+ interface PreviewProps {
5
+ block: Block | null;
6
+ previewData: Record<string, unknown>;
7
+ currentPage?: string;
8
+ loading?: boolean;
9
+ }
10
+
11
+ export const Preview = memo(function Preview({ block, previewData, currentPage, loading }: PreviewProps) {
12
+ const iframeRef = useRef<HTMLIFrameElement>(null);
13
+ const iframeLoadedRef = useRef(false);
14
+ const lastSentDataRef = useRef<string>('');
15
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
16
+
17
+ // Send props to iframe with debounce to prevent flickering
18
+ useEffect(() => {
19
+ if (!iframeRef.current?.contentWindow || !iframeLoadedRef.current) {
20
+ return;
21
+ }
22
+
23
+ // Don't send if previewData is empty (initial state)
24
+ if (!previewData || Object.keys(previewData).length === 0) {
25
+ return;
26
+ }
27
+
28
+ // Don't send if data hasn't changed
29
+ const dataString = JSON.stringify(previewData);
30
+ if (dataString === lastSentDataRef.current) {
31
+ return;
32
+ }
33
+
34
+ // Clear previous debounce
35
+ if (debounceRef.current) {
36
+ clearTimeout(debounceRef.current);
37
+ }
38
+
39
+ // Debounce postMessage to reduce flickering
40
+ debounceRef.current = setTimeout(() => {
41
+ if (iframeRef.current?.contentWindow) {
42
+ lastSentDataRef.current = dataString;
43
+ iframeRef.current.contentWindow.postMessage(
44
+ { type: 'UPDATE_PROPS', props: previewData },
45
+ '*'
46
+ );
47
+ }
48
+ }, 150);
49
+
50
+ return () => {
51
+ if (debounceRef.current) {
52
+ clearTimeout(debounceRef.current);
53
+ }
54
+ };
55
+ }, [previewData]);
56
+
57
+ // Handle iframe load - mark as loaded but don't send data
58
+ // The iframe already has correct data from the server
59
+ const handleIframeLoad = () => {
60
+ iframeLoadedRef.current = true;
61
+ // Store current data as "sent" so we don't re-send it
62
+ if (previewData && Object.keys(previewData).length > 0) {
63
+ lastSentDataRef.current = JSON.stringify(previewData);
64
+ }
65
+ };
66
+
67
+ // Reset loaded state when URL changes
68
+ useEffect(() => {
69
+ iframeLoadedRef.current = false;
70
+ lastSentDataRef.current = '';
71
+ }, [block?.name, currentPage]);
72
+
73
+ if (!block) {
74
+ return (
75
+ <div className="preview-panel">
76
+ <div className="preview-header">
77
+ <div className="preview-info">
78
+ <div className="preview-title">Preview</div>
79
+ </div>
80
+ </div>
81
+ <div className="preview-content">
82
+ <div className="preview-empty">
83
+ <div className="preview-empty-icon">👈</div>
84
+ <p>Select a block to preview</p>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ );
89
+ }
90
+
91
+ if (loading) {
92
+ return (
93
+ <div className="preview-panel">
94
+ <div className="preview-header">
95
+ <div className="preview-info">
96
+ <div className="preview-title">{block.displayName}</div>
97
+ </div>
98
+ </div>
99
+ <div className="preview-content">
100
+ <div className="preview-empty">
101
+ <div className="spinner" />
102
+ <p>Loading preview...</p>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ );
107
+ }
108
+
109
+ // Guard against invalid block name
110
+ if (!block.name || typeof block.name !== 'string') {
111
+ console.error('[Preview] Invalid block.name:', block.name, 'block:', block);
112
+ return null;
113
+ }
114
+
115
+ const isTemplate = block.type === 'template' && block.pages && block.pages.length > 0;
116
+ const previewUrl = isTemplate
117
+ ? `/preview/template/${block.name}/${currentPage || block.pages?.[0]?.slug || ''}`
118
+ : `/preview/${block.name}`;
119
+
120
+ return (
121
+ <div className="preview-panel">
122
+ <div className="preview-header">
123
+ <div className="preview-info">
124
+ <div className="preview-title">{block.displayName}</div>
125
+ </div>
126
+ <div className="preview-actions">
127
+ <div className="preview-badge">Ready</div>
128
+ <button className="btn-publish" onClick={() => window.openPublishModal?.()}>
129
+ Publish
130
+ </button>
131
+ </div>
132
+ </div>
133
+ <div className="preview-content">
134
+ <div className={`preview-iframe-wrapper ${isTemplate ? 'template-preview' : ''}`}>
135
+ <iframe
136
+ ref={iframeRef}
137
+ className="preview-iframe"
138
+ src={previewUrl}
139
+ key={previewUrl}
140
+ onLoad={handleIframeLoad}
141
+ />
142
+ </div>
143
+ </div>
144
+ </div>
145
+ );
146
+ });
@@ -0,0 +1,80 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { Block } from '../types';
3
+
4
+ export function useBlocks() {
5
+ const [blocks, setBlocks] = useState<Block[]>([]);
6
+ const [loading, setLoading] = useState(true);
7
+ const [error, setError] = useState<string | null>(null);
8
+
9
+ const loadBlocks = useCallback(async () => {
10
+ try {
11
+ setLoading(true);
12
+ const response = await fetch('/api/blocks');
13
+ if (!response.ok) throw new Error('Failed to load blocks');
14
+ const data = await response.json();
15
+ setBlocks(data);
16
+ setError(null);
17
+ } catch (err) {
18
+ setError(err instanceof Error ? err.message : 'Unknown error');
19
+ } finally {
20
+ setLoading(false);
21
+ }
22
+ }, []);
23
+
24
+ useEffect(() => {
25
+ loadBlocks();
26
+ }, [loadBlocks]);
27
+
28
+ return { blocks, loading, error, refresh: loadBlocks, setBlocks };
29
+ }
30
+
31
+ export function useBlockConfig(blockName: string | null, _blockType: string | null) {
32
+ const [config, setConfig] = useState<{
33
+ schema?: Record<string, unknown>;
34
+ previewData?: Record<string, unknown>;
35
+ pages?: Array<{ name: string; slug: string; blocksCount: number }>;
36
+ layoutSlots?: Array<{ slot: string; type: string }>;
37
+ } | null>(null);
38
+ const [loading, setLoading] = useState(false);
39
+ const [error, setError] = useState<string | null>(null);
40
+
41
+ useEffect(() => {
42
+ if (!blockName) {
43
+ setConfig(null);
44
+ return;
45
+ }
46
+
47
+ const loadConfig = async () => {
48
+ setLoading(true);
49
+ setError(null);
50
+
51
+ try {
52
+ // Load block config (includes pages/layoutSlots for templates)
53
+ const configResponse = await fetch(`/api/blocks/${blockName}/config`);
54
+ if (!configResponse.ok) throw new Error('Failed to load config');
55
+ const configData = await configResponse.json();
56
+
57
+ // Debug: log raw API response (remove after debugging)
58
+ console.log(`[useBlockConfig] Raw config for "${blockName}":`, configData);
59
+ console.log('[useBlockConfig] previewData from API:', configData.previewData);
60
+ console.log('[useBlockConfig] previewData keys:', Object.keys(configData.previewData || {}));
61
+ console.log('[useBlockConfig] previewData.values:', configData.previewData?.values);
62
+
63
+ setConfig({
64
+ schema: configData.schema,
65
+ previewData: configData.previewData,
66
+ pages: configData.pages,
67
+ layoutSlots: configData.layoutSlots,
68
+ });
69
+ } catch (err) {
70
+ setError(err instanceof Error ? err.message : 'Unknown error');
71
+ } finally {
72
+ setLoading(false);
73
+ }
74
+ };
75
+
76
+ loadConfig();
77
+ }, [blockName]);
78
+
79
+ return { config, loading, error };
80
+ }