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.
Files changed (51) hide show
  1. package/README.md +102 -0
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +54 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/build.d.ts +2 -0
  7. package/dist/commands/build.d.ts.map +1 -0
  8. package/dist/commands/build.js +66 -0
  9. package/dist/commands/build.js.map +1 -0
  10. package/dist/commands/storybook.d.ts +2 -0
  11. package/dist/commands/storybook.d.ts.map +1 -0
  12. package/dist/commands/storybook.js +95 -0
  13. package/dist/commands/storybook.js.map +1 -0
  14. package/dist/discoverBlocks.d.ts +9 -0
  15. package/dist/discoverBlocks.d.ts.map +1 -0
  16. package/dist/discoverBlocks.js +63 -0
  17. package/dist/discoverBlocks.js.map +1 -0
  18. package/dist/extractProps.d.ts +10 -0
  19. package/dist/extractProps.d.ts.map +1 -0
  20. package/dist/extractProps.js +251 -0
  21. package/dist/extractProps.js.map +1 -0
  22. package/dist/extractProps.test.d.ts +2 -0
  23. package/dist/extractProps.test.d.ts.map +1 -0
  24. package/dist/extractProps.test.js +305 -0
  25. package/dist/extractProps.test.js.map +1 -0
  26. package/dist/generateBlocksModule.d.ts +3 -0
  27. package/dist/generateBlocksModule.d.ts.map +1 -0
  28. package/dist/generateBlocksModule.js +21 -0
  29. package/dist/generateBlocksModule.js.map +1 -0
  30. package/dist/index.d.ts +4 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +5 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/parseReadme.d.ts +15 -0
  35. package/dist/parseReadme.d.ts.map +1 -0
  36. package/dist/parseReadme.js +84 -0
  37. package/dist/parseReadme.js.map +1 -0
  38. package/dist/parseReadme.test.d.ts +2 -0
  39. package/dist/parseReadme.test.d.ts.map +1 -0
  40. package/dist/parseReadme.test.js +142 -0
  41. package/dist/parseReadme.test.js.map +1 -0
  42. package/dist/templates/App.tsx +236 -0
  43. package/dist/templates/ErrorBoundary.tsx +53 -0
  44. package/dist/templates/PropsEditor.tsx +707 -0
  45. package/dist/templates/index.html +27 -0
  46. package/dist/templates/index.tsx +10 -0
  47. package/dist/viteConfig.d.ts +12 -0
  48. package/dist/viteConfig.d.ts.map +1 -0
  49. package/dist/viteConfig.js +22 -0
  50. package/dist/viteConfig.js.map +1 -0
  51. 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
+ }