blockparty 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/README.md +102 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +54 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/build.d.ts +2 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +66 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/storybook.d.ts +2 -0
- package/dist/commands/storybook.d.ts.map +1 -0
- package/dist/commands/storybook.js +95 -0
- package/dist/commands/storybook.js.map +1 -0
- package/dist/discoverBlocks.d.ts +9 -0
- package/dist/discoverBlocks.d.ts.map +1 -0
- package/dist/discoverBlocks.js +63 -0
- package/dist/discoverBlocks.js.map +1 -0
- package/dist/extractProps.d.ts +10 -0
- package/dist/extractProps.d.ts.map +1 -0
- package/dist/extractProps.js +251 -0
- package/dist/extractProps.js.map +1 -0
- package/dist/extractProps.test.d.ts +2 -0
- package/dist/extractProps.test.d.ts.map +1 -0
- package/dist/extractProps.test.js +305 -0
- package/dist/extractProps.test.js.map +1 -0
- package/dist/generateBlocksModule.d.ts +3 -0
- package/dist/generateBlocksModule.d.ts.map +1 -0
- package/dist/generateBlocksModule.js +21 -0
- package/dist/generateBlocksModule.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/parseReadme.d.ts +15 -0
- package/dist/parseReadme.d.ts.map +1 -0
- package/dist/parseReadme.js +84 -0
- package/dist/parseReadme.js.map +1 -0
- package/dist/parseReadme.test.d.ts +2 -0
- package/dist/parseReadme.test.d.ts.map +1 -0
- package/dist/parseReadme.test.js +142 -0
- package/dist/parseReadme.test.js.map +1 -0
- package/dist/templates/App.tsx +236 -0
- package/dist/templates/ErrorBoundary.tsx +53 -0
- package/dist/templates/PropsEditor.tsx +707 -0
- package/dist/templates/index.html +27 -0
- package/dist/templates/index.tsx +10 -0
- package/dist/viteConfig.d.ts +12 -0
- package/dist/viteConfig.d.ts.map +1 -0
- package/dist/viteConfig.js +22 -0
- package/dist/viteConfig.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
interface ReactNodeValue {
|
|
4
|
+
__block: string // Block name
|
|
5
|
+
__props: Record<string, any> // Props for the block
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface PropDefinition {
|
|
9
|
+
name: string
|
|
10
|
+
type: string
|
|
11
|
+
optional: boolean
|
|
12
|
+
properties?: PropDefinition[]
|
|
13
|
+
description?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RuntimeBlockInfo {
|
|
17
|
+
name: string
|
|
18
|
+
description?: string
|
|
19
|
+
propDefinitions: PropDefinition[]
|
|
20
|
+
Component: React.ComponentType<any>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface PropsEditorProps {
|
|
24
|
+
propDefinitions: PropDefinition[]
|
|
25
|
+
props: Record<string, string>
|
|
26
|
+
onPropsChange: (props: Record<string, string>) => void
|
|
27
|
+
availableBlocks?: RuntimeBlockInfo[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const isComplexType = (type: string) => {
|
|
31
|
+
// String unions like "foo" | "bar" are not complex, they get a dropdown
|
|
32
|
+
if (isStringUnion(type)) {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
return type.includes('[') || type.includes('{') || type.includes('|')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const isStringUnion = (type: string): boolean => {
|
|
39
|
+
// Check if it's a union of string literals: "foo" | "bar" | "baz"
|
|
40
|
+
if (!type.includes('|')) return false
|
|
41
|
+
|
|
42
|
+
// Split by | and check if all parts are string literals
|
|
43
|
+
const parts = type.split('|').map(p => p.trim())
|
|
44
|
+
return parts.every(part =>
|
|
45
|
+
(part.startsWith('"') && part.endsWith('"')) ||
|
|
46
|
+
(part.startsWith("'") && part.endsWith("'"))
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const parseStringUnion = (type: string): string[] => {
|
|
51
|
+
return type.split('|').map(part => {
|
|
52
|
+
const trimmed = part.trim()
|
|
53
|
+
// Remove quotes
|
|
54
|
+
return trimmed.slice(1, -1)
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const getDefaultValue = (type: string, optional: boolean) => {
|
|
59
|
+
if (optional) return ''
|
|
60
|
+
|
|
61
|
+
if (type.includes('[]')) {
|
|
62
|
+
return '[]'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (type.includes('{') || type.includes('object')) {
|
|
66
|
+
return '{}'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (type === 'number') {
|
|
70
|
+
return '0'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (type === 'boolean') {
|
|
74
|
+
return 'false'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (isStringUnion(type)) {
|
|
78
|
+
const options = parseStringUnion(type)
|
|
79
|
+
return options[0] || ''
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return ''
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function PropsEditor({ propDefinitions, props, onPropsChange, availableBlocks = [] }: PropsEditorProps) {
|
|
86
|
+
const [jsonMode, setJsonMode] = useState<Record<string, boolean>>({})
|
|
87
|
+
|
|
88
|
+
const toggleJsonMode = (propName: string) => {
|
|
89
|
+
setJsonMode(prev => ({ ...prev, [propName]: !prev[propName] }))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const updateProp = (name: string, value: string, optional: boolean) => {
|
|
93
|
+
// If the value is empty and the prop is optional, remove it
|
|
94
|
+
if (value === '' && optional) {
|
|
95
|
+
const { [name]: _, ...rest } = props
|
|
96
|
+
onPropsChange(rest)
|
|
97
|
+
} else {
|
|
98
|
+
onPropsChange({ ...props, [name]: value })
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const renderPropEditor = (propDef: PropDefinition) => {
|
|
103
|
+
const isComplex = isComplexType(propDef.type)
|
|
104
|
+
const isJson = jsonMode[propDef.name]
|
|
105
|
+
|
|
106
|
+
if (!isComplex) {
|
|
107
|
+
// Simple type - just render input
|
|
108
|
+
return (
|
|
109
|
+
<ItemEditor
|
|
110
|
+
propDef={propDef}
|
|
111
|
+
value={props[propDef.name] || ''}
|
|
112
|
+
onChange={(value) => updateProp(propDef.name, value, propDef.optional)}
|
|
113
|
+
availableBlocks={availableBlocks}
|
|
114
|
+
/>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Complex type - show editor based on mode
|
|
119
|
+
return (
|
|
120
|
+
<>
|
|
121
|
+
{isJson ? (
|
|
122
|
+
<textarea
|
|
123
|
+
value={props[propDef.name] || ''}
|
|
124
|
+
onChange={(e) => updateProp(propDef.name, e.target.value, propDef.optional)}
|
|
125
|
+
style={{
|
|
126
|
+
width: '100%',
|
|
127
|
+
padding: '6px 8px',
|
|
128
|
+
border: '1px solid #ddd',
|
|
129
|
+
borderRadius: '4px',
|
|
130
|
+
fontSize: '12px',
|
|
131
|
+
fontFamily: 'monospace',
|
|
132
|
+
boxSizing: 'border-box',
|
|
133
|
+
minHeight: '100px',
|
|
134
|
+
resize: 'vertical'
|
|
135
|
+
}}
|
|
136
|
+
placeholder={`Enter JSON for ${propDef.name}`}
|
|
137
|
+
/>
|
|
138
|
+
) : (
|
|
139
|
+
<RichEditor
|
|
140
|
+
type={propDef.type}
|
|
141
|
+
value={props[propDef.name] || ''}
|
|
142
|
+
onChange={(value) => updateProp(propDef.name, value, propDef.optional)}
|
|
143
|
+
properties={propDef.properties}
|
|
144
|
+
availableBlocks={availableBlocks}
|
|
145
|
+
/>
|
|
146
|
+
)}
|
|
147
|
+
</>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
|
153
|
+
{propDefinitions.length > 0 ? propDefinitions.map(propDef => {
|
|
154
|
+
const isComplex = isComplexType(propDef.type)
|
|
155
|
+
const isJson = jsonMode[propDef.name]
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div key={propDef.name}>
|
|
159
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px', flexWrap: 'wrap', gap: '4px' }}>
|
|
160
|
+
<label style={{ fontSize: '12px', fontWeight: 500 }}>
|
|
161
|
+
{propDef.name}{propDef.optional ? '' : ' *'}
|
|
162
|
+
<span style={{ color: '#999', fontWeight: 'normal', marginLeft: '4px' }}>
|
|
163
|
+
{propDef.type}
|
|
164
|
+
</span>
|
|
165
|
+
</label>
|
|
166
|
+
{isComplex && (
|
|
167
|
+
<button
|
|
168
|
+
onClick={() => toggleJsonMode(propDef.name)}
|
|
169
|
+
style={{
|
|
170
|
+
fontSize: '10px',
|
|
171
|
+
padding: '2px 6px',
|
|
172
|
+
background: '#f5f5f5',
|
|
173
|
+
border: '1px solid #ddd',
|
|
174
|
+
borderRadius: '3px',
|
|
175
|
+
cursor: 'pointer'
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
{isJson ? 'Rich Editor' : 'JSON'}
|
|
179
|
+
</button>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
{propDef.description && (
|
|
183
|
+
<div style={{ fontSize: '11px', color: '#666', marginBottom: '4px', fontStyle: 'italic' }}>
|
|
184
|
+
{propDef.description}
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
{renderPropEditor(propDef)}
|
|
188
|
+
</div>
|
|
189
|
+
)
|
|
190
|
+
}) : (
|
|
191
|
+
<p style={{ fontSize: '12px', color: '#999' }}>No props defined</p>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
interface RichEditorProps {
|
|
198
|
+
type: string
|
|
199
|
+
value: string
|
|
200
|
+
onChange: (value: string, optional: boolean) => void
|
|
201
|
+
properties?: PropDefinition[]
|
|
202
|
+
availableBlocks?: RuntimeBlockInfo[]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function RichEditor({ type, value, onChange, properties, availableBlocks = [] }: RichEditorProps) {
|
|
206
|
+
// Parse the current value
|
|
207
|
+
let parsedValue: any
|
|
208
|
+
try {
|
|
209
|
+
parsedValue = value ? JSON.parse(value) : (type.includes('[]') ? [] : {})
|
|
210
|
+
} catch {
|
|
211
|
+
parsedValue = type.includes('[]') ? [] : {}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Array type
|
|
215
|
+
if (type.includes('[]')) {
|
|
216
|
+
const items = Array.isArray(parsedValue) ? parsedValue : []
|
|
217
|
+
const elementType = type.replace('[]', '').trim()
|
|
218
|
+
const elementPropDef: PropDefinition = {
|
|
219
|
+
name: '',
|
|
220
|
+
type: elementType,
|
|
221
|
+
optional: false, // FIXME
|
|
222
|
+
properties
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const addItem = () => {
|
|
226
|
+
const newItems = [...items, getDefaultValue(elementType, false)]
|
|
227
|
+
onChange(JSON.stringify(newItems), false)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const removeItem = (index: number) => {
|
|
231
|
+
const newItems = items.filter((_, i) => i !== index)
|
|
232
|
+
onChange(JSON.stringify(newItems), items.length === 1)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const updateItem = (index: number, newValue: any) => {
|
|
236
|
+
const newItems = [...items]
|
|
237
|
+
newItems[index] = newValue
|
|
238
|
+
onChange(JSON.stringify(newItems), false)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div style={{
|
|
243
|
+
border: '1px solid #ddd',
|
|
244
|
+
borderRadius: '4px',
|
|
245
|
+
padding: '8px',
|
|
246
|
+
background: '#fafafa'
|
|
247
|
+
}}>
|
|
248
|
+
<div style={{ marginBottom: '8px', fontSize: '11px', color: '#666' }}>
|
|
249
|
+
Array of {elementType} ({items.length} item{items.length !== 1 ? 's' : ''})
|
|
250
|
+
</div>
|
|
251
|
+
{items.map((item, index) => (
|
|
252
|
+
<div key={index} style={{ display: 'flex', gap: '4px', marginBottom: '4px' }}>
|
|
253
|
+
<div style={{ flex: 1 }}>
|
|
254
|
+
<ItemEditor
|
|
255
|
+
propDef={elementPropDef}
|
|
256
|
+
value={item}
|
|
257
|
+
onChange={(newValue) => updateItem(index, newValue)}
|
|
258
|
+
availableBlocks={availableBlocks}
|
|
259
|
+
/>
|
|
260
|
+
</div>
|
|
261
|
+
<button
|
|
262
|
+
onClick={() => removeItem(index)}
|
|
263
|
+
style={{
|
|
264
|
+
padding: '4px 8px',
|
|
265
|
+
background: '#fee',
|
|
266
|
+
border: '1px solid #fcc',
|
|
267
|
+
borderRadius: '3px',
|
|
268
|
+
cursor: 'pointer',
|
|
269
|
+
fontSize: '12px'
|
|
270
|
+
}}
|
|
271
|
+
>
|
|
272
|
+
✕
|
|
273
|
+
</button>
|
|
274
|
+
</div>
|
|
275
|
+
))}
|
|
276
|
+
<button
|
|
277
|
+
onClick={addItem}
|
|
278
|
+
style={{
|
|
279
|
+
width: '100%',
|
|
280
|
+
padding: '6px',
|
|
281
|
+
background: '#f0f0f0',
|
|
282
|
+
border: '1px solid #ddd',
|
|
283
|
+
borderRadius: '3px',
|
|
284
|
+
cursor: 'pointer',
|
|
285
|
+
fontSize: '12px',
|
|
286
|
+
marginTop: '4px'
|
|
287
|
+
}}
|
|
288
|
+
>
|
|
289
|
+
+ Add Item
|
|
290
|
+
</button>
|
|
291
|
+
</div>
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Object type - show rich editor if we have property definitions
|
|
296
|
+
if (properties && properties.length > 0) {
|
|
297
|
+
return (
|
|
298
|
+
<ObjectEditor
|
|
299
|
+
value={value}
|
|
300
|
+
onChange={(v) => onChange(v, false)}
|
|
301
|
+
properties={properties}
|
|
302
|
+
availableBlocks={availableBlocks}
|
|
303
|
+
/>
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Fallback to JSON textarea for unknown object types
|
|
308
|
+
return (
|
|
309
|
+
<textarea
|
|
310
|
+
value={value}
|
|
311
|
+
onChange={(e) => onChange(e.target.value, false)}
|
|
312
|
+
style={{
|
|
313
|
+
width: '100%',
|
|
314
|
+
padding: '6px 8px',
|
|
315
|
+
border: '1px solid #ddd',
|
|
316
|
+
borderRadius: '4px',
|
|
317
|
+
fontSize: '12px',
|
|
318
|
+
fontFamily: 'monospace',
|
|
319
|
+
boxSizing: 'border-box',
|
|
320
|
+
minHeight: '60px',
|
|
321
|
+
resize: 'vertical'
|
|
322
|
+
}}
|
|
323
|
+
placeholder={`Enter JSON for ${type}`}
|
|
324
|
+
/>
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
interface ObjectEditorProps {
|
|
329
|
+
value: string
|
|
330
|
+
onChange: (value: string) => void
|
|
331
|
+
properties: PropDefinition[]
|
|
332
|
+
availableBlocks?: RuntimeBlockInfo[]
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function ObjectEditor({ value, onChange, properties, availableBlocks = [] }: ObjectEditorProps) {
|
|
336
|
+
let parsedValue: Record<string, any>
|
|
337
|
+
try {
|
|
338
|
+
parsedValue = value ? JSON.parse(value) : {}
|
|
339
|
+
} catch {
|
|
340
|
+
parsedValue = {}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const updateField = (fieldName: string, fieldValue: any) => {
|
|
344
|
+
const newObj = { ...parsedValue, [fieldName]: fieldValue }
|
|
345
|
+
onChange(JSON.stringify(newObj))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<div style={{
|
|
350
|
+
border: '1px solid #ddd',
|
|
351
|
+
borderRadius: '4px',
|
|
352
|
+
padding: '8px',
|
|
353
|
+
background: '#fafafa',
|
|
354
|
+
display: 'flex',
|
|
355
|
+
flexDirection: 'column',
|
|
356
|
+
gap: '8px'
|
|
357
|
+
}}>
|
|
358
|
+
{properties.map(prop => (
|
|
359
|
+
<div key={prop.name}>
|
|
360
|
+
<label style={{ display: 'block', fontSize: '11px', marginBottom: '2px', fontWeight: 500 }}>
|
|
361
|
+
{prop.name}{prop.optional ? '' : ' *'}
|
|
362
|
+
<span style={{ color: '#999', fontWeight: 'normal', marginLeft: '4px' }}>
|
|
363
|
+
{prop.type}
|
|
364
|
+
</span>
|
|
365
|
+
</label>
|
|
366
|
+
{prop.description && (
|
|
367
|
+
<div style={{ fontSize: '10px', color: '#666', marginBottom: '2px', fontStyle: 'italic' }}>
|
|
368
|
+
{prop.description}
|
|
369
|
+
</div>
|
|
370
|
+
)}
|
|
371
|
+
<ItemEditor
|
|
372
|
+
propDef={prop}
|
|
373
|
+
value={parsedValue[prop.name]}
|
|
374
|
+
onChange={(newValue) => updateField(prop.name, newValue)}
|
|
375
|
+
availableBlocks={availableBlocks}
|
|
376
|
+
/>
|
|
377
|
+
</div>
|
|
378
|
+
))}
|
|
379
|
+
</div>
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
interface ReactNodeEditorProps {
|
|
384
|
+
value: any
|
|
385
|
+
onChange: (value: any) => void
|
|
386
|
+
optional: boolean
|
|
387
|
+
availableBlocks: RuntimeBlockInfo[]
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function ReactNodeEditor({ value, onChange, optional, availableBlocks }: ReactNodeEditorProps) {
|
|
391
|
+
const [expandedIndices, setExpandedIndices] = useState<Set<number>>(new Set([0]))
|
|
392
|
+
|
|
393
|
+
// Parse the value - always an array
|
|
394
|
+
let blocks: ReactNodeValue[] = []
|
|
395
|
+
try {
|
|
396
|
+
const parsed = typeof value === 'string' ? JSON.parse(value) : value
|
|
397
|
+
if (Array.isArray(parsed)) {
|
|
398
|
+
blocks = parsed.filter(item => item && typeof item === 'object' && '__block' in item)
|
|
399
|
+
}
|
|
400
|
+
} catch {
|
|
401
|
+
// Invalid value
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const toggleExpanded = (index: number) => {
|
|
405
|
+
setExpandedIndices(prev => {
|
|
406
|
+
const next = new Set(prev)
|
|
407
|
+
if (next.has(index)) {
|
|
408
|
+
next.delete(index)
|
|
409
|
+
} else {
|
|
410
|
+
next.add(index)
|
|
411
|
+
}
|
|
412
|
+
return next
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const addBlock = () => {
|
|
417
|
+
const newBlocks = [...blocks, { __block: '', __props: {} }]
|
|
418
|
+
onChange(newBlocks)
|
|
419
|
+
setExpandedIndices(prev => new Set([...prev, newBlocks.length - 1]))
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const removeBlock = (index: number) => {
|
|
423
|
+
const newBlocks = blocks.filter((_, i) => i !== index)
|
|
424
|
+
onChange(newBlocks.length === 0 ? (optional ? undefined : []) : newBlocks)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const updateBlock = (index: number, blockName: string) => {
|
|
428
|
+
const newBlocks = [...blocks]
|
|
429
|
+
newBlocks[index] = {
|
|
430
|
+
__block: blockName,
|
|
431
|
+
__props: {}
|
|
432
|
+
}
|
|
433
|
+
onChange(newBlocks)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const updateBlockProps = (index: number, newProps: Record<string, any>) => {
|
|
437
|
+
const newBlocks = [...blocks]
|
|
438
|
+
newBlocks[index] = {
|
|
439
|
+
...newBlocks[index],
|
|
440
|
+
__props: newProps
|
|
441
|
+
}
|
|
442
|
+
onChange(newBlocks)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return (
|
|
446
|
+
<div style={{
|
|
447
|
+
border: '1px solid #ddd',
|
|
448
|
+
borderRadius: '4px',
|
|
449
|
+
padding: '8px',
|
|
450
|
+
background: '#fafafa'
|
|
451
|
+
}}>
|
|
452
|
+
{blocks.length === 0 && optional && (
|
|
453
|
+
<div style={{ fontSize: '11px', color: '#999', marginBottom: '8px' }}>
|
|
454
|
+
No blocks added
|
|
455
|
+
</div>
|
|
456
|
+
)}
|
|
457
|
+
|
|
458
|
+
{blocks.map((block, index) => {
|
|
459
|
+
const selectedBlock = availableBlocks.find(b => b.name === block.__block)
|
|
460
|
+
const isExpanded = expandedIndices.has(index)
|
|
461
|
+
|
|
462
|
+
return (
|
|
463
|
+
<div key={index} style={{
|
|
464
|
+
marginBottom: '8px',
|
|
465
|
+
border: '1px solid #ddd',
|
|
466
|
+
borderRadius: '4px',
|
|
467
|
+
background: 'white'
|
|
468
|
+
}}>
|
|
469
|
+
<div style={{
|
|
470
|
+
display: 'flex',
|
|
471
|
+
gap: '4px',
|
|
472
|
+
padding: '6px',
|
|
473
|
+
alignItems: 'center'
|
|
474
|
+
}}>
|
|
475
|
+
<button
|
|
476
|
+
onClick={() => toggleExpanded(index)}
|
|
477
|
+
style={{
|
|
478
|
+
padding: '2px 6px',
|
|
479
|
+
background: '#f0f0f0',
|
|
480
|
+
border: '1px solid #ddd',
|
|
481
|
+
borderRadius: '3px',
|
|
482
|
+
cursor: 'pointer',
|
|
483
|
+
fontSize: '10px',
|
|
484
|
+
minWidth: '20px'
|
|
485
|
+
}}
|
|
486
|
+
>
|
|
487
|
+
{isExpanded ? '▼' : '▶'}
|
|
488
|
+
</button>
|
|
489
|
+
|
|
490
|
+
<select
|
|
491
|
+
value={block.__block || ''}
|
|
492
|
+
onChange={(e) => updateBlock(index, e.target.value)}
|
|
493
|
+
style={{
|
|
494
|
+
flex: 1,
|
|
495
|
+
padding: '4px 6px',
|
|
496
|
+
border: '1px solid #ddd',
|
|
497
|
+
borderRadius: '3px',
|
|
498
|
+
fontSize: '12px'
|
|
499
|
+
}}
|
|
500
|
+
>
|
|
501
|
+
<option value="">-- Select a block --</option>
|
|
502
|
+
{availableBlocks.map(b => (
|
|
503
|
+
<option key={b.name} value={b.name}>{b.name}</option>
|
|
504
|
+
))}
|
|
505
|
+
</select>
|
|
506
|
+
|
|
507
|
+
<button
|
|
508
|
+
onClick={() => removeBlock(index)}
|
|
509
|
+
style={{
|
|
510
|
+
padding: '4px 8px',
|
|
511
|
+
background: '#fee',
|
|
512
|
+
border: '1px solid #fcc',
|
|
513
|
+
borderRadius: '3px',
|
|
514
|
+
cursor: 'pointer',
|
|
515
|
+
fontSize: '12px'
|
|
516
|
+
}}
|
|
517
|
+
>
|
|
518
|
+
✕
|
|
519
|
+
</button>
|
|
520
|
+
</div>
|
|
521
|
+
|
|
522
|
+
{isExpanded && selectedBlock && (
|
|
523
|
+
<div style={{
|
|
524
|
+
padding: '8px',
|
|
525
|
+
borderTop: '1px solid #ddd'
|
|
526
|
+
}}>
|
|
527
|
+
<div style={{ fontSize: '10px', color: '#666', marginBottom: '6px', fontWeight: 500 }}>
|
|
528
|
+
Props for {selectedBlock.name}:
|
|
529
|
+
</div>
|
|
530
|
+
<PropsEditor
|
|
531
|
+
propDefinitions={selectedBlock.propDefinitions}
|
|
532
|
+
props={block.__props || {}}
|
|
533
|
+
onPropsChange={(newProps) => updateBlockProps(index, newProps)}
|
|
534
|
+
availableBlocks={availableBlocks}
|
|
535
|
+
/>
|
|
536
|
+
</div>
|
|
537
|
+
)}
|
|
538
|
+
</div>
|
|
539
|
+
)
|
|
540
|
+
})}
|
|
541
|
+
|
|
542
|
+
<button
|
|
543
|
+
onClick={addBlock}
|
|
544
|
+
style={{
|
|
545
|
+
width: '100%',
|
|
546
|
+
padding: '6px',
|
|
547
|
+
background: '#f0f0f0',
|
|
548
|
+
border: '1px solid #ddd',
|
|
549
|
+
borderRadius: '3px',
|
|
550
|
+
cursor: 'pointer',
|
|
551
|
+
fontSize: '12px',
|
|
552
|
+
marginTop: blocks.length > 0 ? '4px' : '0'
|
|
553
|
+
}}
|
|
554
|
+
>
|
|
555
|
+
+ Add Block
|
|
556
|
+
</button>
|
|
557
|
+
</div>
|
|
558
|
+
)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
interface ItemEditorProps {
|
|
562
|
+
propDef: PropDefinition
|
|
563
|
+
value: any
|
|
564
|
+
onChange: (value: any) => void
|
|
565
|
+
availableBlocks?: RuntimeBlockInfo[]
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function ItemEditor({ value, onChange, propDef: { type, properties, optional}, availableBlocks = [] }: ItemEditorProps) {
|
|
569
|
+
// For React.ReactNode, show block selector
|
|
570
|
+
if (type === 'React.ReactNode' || type === 'ReactNode') {
|
|
571
|
+
return (
|
|
572
|
+
<ReactNodeEditor
|
|
573
|
+
value={value}
|
|
574
|
+
onChange={onChange}
|
|
575
|
+
optional={optional}
|
|
576
|
+
availableBlocks={availableBlocks}
|
|
577
|
+
/>
|
|
578
|
+
)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// For string unions, show dropdown
|
|
582
|
+
if (isStringUnion(type)) {
|
|
583
|
+
const options = parseStringUnion(type)
|
|
584
|
+
return (
|
|
585
|
+
<select
|
|
586
|
+
value={value || ''}
|
|
587
|
+
onChange={(e) => onChange(e.target.value)}
|
|
588
|
+
style={{
|
|
589
|
+
width: '100%',
|
|
590
|
+
padding: '4px 6px',
|
|
591
|
+
border: '1px solid #ddd',
|
|
592
|
+
borderRadius: '3px',
|
|
593
|
+
fontSize: '12px'
|
|
594
|
+
}}
|
|
595
|
+
>
|
|
596
|
+
{optional && <option value="">-- Select --</option>}
|
|
597
|
+
{options.map(option => (
|
|
598
|
+
<option key={option} value={option}>{option}</option>
|
|
599
|
+
))}
|
|
600
|
+
</select>
|
|
601
|
+
)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// For primitive types
|
|
605
|
+
if (type === 'string') {
|
|
606
|
+
return (
|
|
607
|
+
<input
|
|
608
|
+
type="text"
|
|
609
|
+
value={value || ''}
|
|
610
|
+
onChange={(e) => onChange(e.target.value)}
|
|
611
|
+
style={{
|
|
612
|
+
width: '100%',
|
|
613
|
+
padding: '4px 6px',
|
|
614
|
+
border: '1px solid #ddd',
|
|
615
|
+
borderRadius: '3px',
|
|
616
|
+
fontSize: '12px'
|
|
617
|
+
}}
|
|
618
|
+
/>
|
|
619
|
+
)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (type === 'number') {
|
|
623
|
+
return (
|
|
624
|
+
<input
|
|
625
|
+
type="number"
|
|
626
|
+
value={value ?? ''}
|
|
627
|
+
onChange={(e) => {
|
|
628
|
+
const val = e.target.value
|
|
629
|
+
// If empty and optional, pass undefined
|
|
630
|
+
if (val === '' && optional) {
|
|
631
|
+
onChange(undefined)
|
|
632
|
+
} else {
|
|
633
|
+
onChange(val === '' ? 0 : Number(val))
|
|
634
|
+
}
|
|
635
|
+
}}
|
|
636
|
+
style={{
|
|
637
|
+
width: '100%',
|
|
638
|
+
padding: '4px 6px',
|
|
639
|
+
border: '1px solid #ddd',
|
|
640
|
+
borderRadius: '3px',
|
|
641
|
+
fontSize: '12px'
|
|
642
|
+
}}
|
|
643
|
+
/>
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (type === 'boolean') {
|
|
648
|
+
return (
|
|
649
|
+
<select
|
|
650
|
+
value={value ? 'true' : 'false'}
|
|
651
|
+
onChange={(e) => onChange(e.target.value === 'true')}
|
|
652
|
+
style={{
|
|
653
|
+
width: '100%',
|
|
654
|
+
padding: '4px 6px',
|
|
655
|
+
border: '1px solid #ddd',
|
|
656
|
+
borderRadius: '3px',
|
|
657
|
+
fontSize: '12px'
|
|
658
|
+
}}
|
|
659
|
+
>
|
|
660
|
+
<option value="true">true</option>
|
|
661
|
+
<option value="false">false</option>
|
|
662
|
+
</select>
|
|
663
|
+
)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// For object types with known properties, show structured editor
|
|
667
|
+
if (properties && properties.length > 0) {
|
|
668
|
+
return (
|
|
669
|
+
<ObjectEditor
|
|
670
|
+
value={typeof value === 'string' ? value : JSON.stringify(value)}
|
|
671
|
+
onChange={(newValue) => {
|
|
672
|
+
try {
|
|
673
|
+
onChange(JSON.parse(newValue))
|
|
674
|
+
} catch {
|
|
675
|
+
onChange(newValue)
|
|
676
|
+
}
|
|
677
|
+
}}
|
|
678
|
+
properties={properties}
|
|
679
|
+
availableBlocks={availableBlocks}
|
|
680
|
+
/>
|
|
681
|
+
)
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// For unknown object types, show JSON editor
|
|
685
|
+
return (
|
|
686
|
+
<textarea
|
|
687
|
+
value={typeof value === 'string' ? value : JSON.stringify(value, null, 2)}
|
|
688
|
+
onChange={(e) => {
|
|
689
|
+
try {
|
|
690
|
+
onChange(JSON.parse(e.target.value))
|
|
691
|
+
} catch {
|
|
692
|
+
onChange(e.target.value)
|
|
693
|
+
}
|
|
694
|
+
}}
|
|
695
|
+
style={{
|
|
696
|
+
width: '100%',
|
|
697
|
+
padding: '4px 6px',
|
|
698
|
+
border: '1px solid #ddd',
|
|
699
|
+
borderRadius: '3px',
|
|
700
|
+
fontSize: '11px',
|
|
701
|
+
fontFamily: 'monospace',
|
|
702
|
+
minHeight: '40px',
|
|
703
|
+
resize: 'vertical'
|
|
704
|
+
}}
|
|
705
|
+
/>
|
|
706
|
+
)
|
|
707
|
+
}
|