react-three-game 0.0.60 → 0.0.62

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 (66) hide show
  1. package/README.md +56 -0
  2. package/dist/index.d.ts +1 -1
  3. package/dist/shared/GameCanvas.d.ts +2 -1
  4. package/dist/shared/GameCanvas.js +7 -2
  5. package/dist/tools/prefabeditor/PrefabEditor.d.ts +18 -4
  6. package/dist/tools/prefabeditor/PrefabEditor.js +90 -36
  7. package/dist/tools/prefabeditor/utils.d.ts +2 -0
  8. package/dist/tools/prefabeditor/utils.js +15 -0
  9. package/package.json +9 -3
  10. package/.gitattributes +0 -2
  11. package/.github/copilot-instructions.md +0 -83
  12. package/.github/workflows/nextjs.yml +0 -99
  13. package/.gitmodules +0 -3
  14. package/assets/architecture.png +0 -0
  15. package/assets/editor.gif +0 -0
  16. package/assets/favicon.ico +0 -0
  17. package/assets/react-three-game-logo.png +0 -0
  18. package/dist/tools/dragdrop/page.d.ts +0 -1
  19. package/dist/tools/dragdrop/page.js +0 -11
  20. package/dist/tools/prefabeditor/EntityEvents.d.ts +0 -54
  21. package/dist/tools/prefabeditor/EntityEvents.js +0 -85
  22. package/dist/tools/prefabeditor/page.d.ts +0 -1
  23. package/dist/tools/prefabeditor/page.js +0 -5
  24. package/react-three-game-skill/.gitattributes +0 -2
  25. package/react-three-game-skill/README.md +0 -7
  26. package/react-three-game-skill/react-three-game/SKILL.md +0 -514
  27. package/react-three-game-skill/react-three-game/rules/ADVANCED_PHYSICS.md +0 -472
  28. package/react-three-game-skill/react-three-game/rules/LIGHTING.md +0 -6
  29. package/src/helpers/SoundManager.ts +0 -130
  30. package/src/helpers/index.ts +0 -91
  31. package/src/index.ts +0 -59
  32. package/src/shared/ContactShadow.tsx +0 -74
  33. package/src/shared/GameCanvas.tsx +0 -52
  34. package/src/tools/assetviewer/page.tsx +0 -425
  35. package/src/tools/dragdrop/DragDropLoader.tsx +0 -159
  36. package/src/tools/dragdrop/index.ts +0 -4
  37. package/src/tools/dragdrop/modelLoader.ts +0 -204
  38. package/src/tools/dragdrop/page.tsx +0 -45
  39. package/src/tools/prefabeditor/Dropdown.tsx +0 -112
  40. package/src/tools/prefabeditor/EditorContext.tsx +0 -25
  41. package/src/tools/prefabeditor/EditorTree.tsx +0 -452
  42. package/src/tools/prefabeditor/EditorTreeMenus.tsx +0 -307
  43. package/src/tools/prefabeditor/EditorUI.tsx +0 -204
  44. package/src/tools/prefabeditor/EventSystem.tsx +0 -36
  45. package/src/tools/prefabeditor/GameEvents.ts +0 -191
  46. package/src/tools/prefabeditor/InstanceProvider.tsx +0 -466
  47. package/src/tools/prefabeditor/PrefabEditor.tsx +0 -256
  48. package/src/tools/prefabeditor/PrefabRoot.tsx +0 -767
  49. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +0 -34
  50. package/src/tools/prefabeditor/components/CameraComponent.tsx +0 -117
  51. package/src/tools/prefabeditor/components/ComponentRegistry.ts +0 -40
  52. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +0 -210
  53. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +0 -47
  54. package/src/tools/prefabeditor/components/GeometryComponent.tsx +0 -133
  55. package/src/tools/prefabeditor/components/Input.tsx +0 -820
  56. package/src/tools/prefabeditor/components/MaterialComponent.tsx +0 -431
  57. package/src/tools/prefabeditor/components/ModelComponent.tsx +0 -176
  58. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +0 -188
  59. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +0 -109
  60. package/src/tools/prefabeditor/components/TextComponent.tsx +0 -137
  61. package/src/tools/prefabeditor/components/TransformComponent.tsx +0 -173
  62. package/src/tools/prefabeditor/components/index.ts +0 -26
  63. package/src/tools/prefabeditor/page.tsx +0 -10
  64. package/src/tools/prefabeditor/styles.ts +0 -235
  65. package/src/tools/prefabeditor/types.ts +0 -20
  66. package/src/tools/prefabeditor/utils.ts +0 -312
@@ -1,820 +0,0 @@
1
- import React, { useEffect, useRef, useState } from 'react';
2
- import { colors } from '../styles';
3
-
4
- // ============================================================================
5
- // Field Definition Types
6
- // ============================================================================
7
-
8
- export type FieldType = 'vector3' | 'number' | 'string' | 'color' | 'boolean' | 'select';
9
-
10
- interface BaseFieldDefinition {
11
- name: string;
12
- label: string;
13
- }
14
-
15
- interface Vector3FieldDefinition extends BaseFieldDefinition {
16
- type: 'vector3';
17
- snap?: number;
18
- }
19
-
20
- interface NumberFieldDefinition extends BaseFieldDefinition {
21
- type: 'number';
22
- min?: number;
23
- max?: number;
24
- step?: number;
25
- }
26
-
27
- interface StringFieldDefinition extends BaseFieldDefinition {
28
- type: 'string';
29
- placeholder?: string;
30
- }
31
-
32
- interface ColorFieldDefinition extends BaseFieldDefinition {
33
- type: 'color';
34
- }
35
-
36
- interface BooleanFieldDefinition extends BaseFieldDefinition {
37
- type: 'boolean';
38
- }
39
-
40
- interface SelectFieldDefinition extends BaseFieldDefinition {
41
- type: 'select';
42
- options: { value: string; label: string }[];
43
- }
44
-
45
- interface CustomFieldDefinition extends BaseFieldDefinition {
46
- type: 'custom';
47
- render: (props: {
48
- value: any;
49
- onChange: (value: any) => void;
50
- values: Record<string, any>;
51
- onChangeMultiple: (values: Record<string, any>) => void;
52
- }) => React.ReactNode;
53
- }
54
-
55
- export type FieldDefinition =
56
- | Vector3FieldDefinition
57
- | NumberFieldDefinition
58
- | StringFieldDefinition
59
- | ColorFieldDefinition
60
- | BooleanFieldDefinition
61
- | SelectFieldDefinition
62
- | CustomFieldDefinition;
63
-
64
- // ============================================================================
65
- // Shared Styles (derived from shared color tokens)
66
- // ============================================================================
67
-
68
- const styles = {
69
- input: {
70
- width: '80px',
71
- backgroundColor: colors.bgInput,
72
- border: `1px solid ${colors.border}`,
73
- padding: '3px 6px',
74
- fontSize: '11px',
75
- color: colors.text,
76
- fontFamily: 'monospace',
77
- outline: 'none',
78
- textAlign: 'right',
79
- borderRadius: 3,
80
- } as React.CSSProperties,
81
- label: {
82
- display: 'block',
83
- fontSize: '10px',
84
- color: colors.textMuted,
85
- textTransform: 'uppercase',
86
- letterSpacing: '0.05em',
87
- marginBottom: 2,
88
- fontWeight: 500,
89
- } as React.CSSProperties,
90
- };
91
-
92
- function getNumericStep(step: string | number | undefined, fallback: number) {
93
- if (typeof step === 'number' && Number.isFinite(step) && step > 0) return step;
94
-
95
- if (typeof step === 'string') {
96
- const parsed = parseFloat(step);
97
- if (Number.isFinite(parsed) && parsed > 0) return parsed;
98
- }
99
-
100
- return fallback;
101
- }
102
-
103
- function getStepPrecision(step: number) {
104
- if (!Number.isFinite(step) || step <= 0) return 3;
105
-
106
- const stepString = step.toString();
107
- if (stepString.includes('e-')) {
108
- const exponent = stepString.split('e-')[1];
109
- return exponent ? parseInt(exponent, 10) : 3;
110
- }
111
-
112
- const decimal = stepString.split('.')[1];
113
- return decimal?.length ?? 0;
114
- }
115
-
116
- interface InputProps {
117
- value: number;
118
- onChange: (value: number) => void;
119
- step?: string | number;
120
- min?: number;
121
- max?: number;
122
- style?: React.CSSProperties;
123
- label?: string;
124
- }
125
-
126
- export function Input({ value, onChange, step, min, max, style, label }: InputProps) {
127
- const [draft, setDraft] = useState<string>(() => value.toString());
128
-
129
- useEffect(() => {
130
- setDraft(value.toString());
131
- }, [value]);
132
-
133
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
134
- const inputValue = e.target.value;
135
- setDraft(inputValue);
136
-
137
- const num = parseFloat(inputValue);
138
- if (Number.isFinite(num)) {
139
- onChange(num);
140
- }
141
- };
142
-
143
- const handleBlur = () => {
144
- const num = parseFloat(draft);
145
- if (!Number.isFinite(num)) {
146
- setDraft(value.toString());
147
- }
148
- };
149
-
150
- const dragState = useRef<{
151
- startX: number;
152
- startValue: number;
153
- } | null>(null);
154
-
155
- const startScrub = (e: React.PointerEvent) => {
156
- dragState.current = {
157
- startX: e.clientX,
158
- startValue: value
159
- };
160
-
161
- e.currentTarget.setPointerCapture(e.pointerId);
162
- document.body.style.cursor = "ew-resize";
163
- };
164
-
165
- const onScrubMove = (e: React.PointerEvent) => {
166
- if (!dragState.current) return;
167
-
168
- const { startX, startValue } = dragState.current;
169
- const dx = e.clientX - startX;
170
- const baseStep = getNumericStep(step, 0.1);
171
- let scrubStep = baseStep;
172
- if (e.shiftKey) scrubStep /= 10;
173
- if (e.altKey) scrubStep *= 10;
174
-
175
- const precision = getStepPrecision(scrubStep);
176
- const deltaSteps = Math.round(dx / 8);
177
- let nextValue = startValue + deltaSteps * scrubStep;
178
-
179
- // Apply min/max constraints
180
- if (min !== undefined && nextValue < min) nextValue = min;
181
- if (max !== undefined && nextValue > max) nextValue = max;
182
-
183
- setDraft(nextValue.toFixed(precision));
184
- onChange(nextValue);
185
- };
186
-
187
- const endScrub = (e: React.PointerEvent) => {
188
- if (!dragState.current) return;
189
-
190
- dragState.current = null;
191
- document.body.style.cursor = "";
192
- e.currentTarget.releasePointerCapture(e.pointerId);
193
- };
194
-
195
- if (label) {
196
- return (
197
- <div style={{
198
- display: 'flex',
199
- alignItems: 'center',
200
- justifyContent: 'space-between',
201
- }}>
202
- <span
203
- style={{
204
- ...styles.label,
205
- marginBottom: 0,
206
- userSelect: 'none',
207
- flex: '0 0 auto',
208
- minWidth: 20,
209
- }}
210
- >
211
- {label}
212
- </span>
213
- <input
214
- type="text"
215
- value={draft}
216
- onChange={handleChange}
217
- onBlur={handleBlur}
218
- onKeyDown={e => {
219
- if (e.key === 'Enter') {
220
- (e.target as HTMLInputElement).blur();
221
- }
222
- }}
223
- step={step}
224
- min={min}
225
- max={max}
226
- style={{ ...styles.input, cursor: 'ew-resize', ...style }}
227
- onPointerDown={startScrub}
228
- onPointerMove={onScrubMove}
229
- onPointerUp={endScrub}
230
- />
231
- </div>
232
- );
233
- }
234
-
235
- return (
236
- <input
237
- type="text"
238
- value={draft}
239
- onChange={handleChange}
240
- onBlur={handleBlur}
241
- onKeyDown={e => {
242
- if (e.key === 'Enter') {
243
- (e.target as HTMLInputElement).blur();
244
- }
245
- }}
246
- step={step}
247
- min={min}
248
- max={max}
249
- style={{ ...styles.input, cursor: 'ew-resize', ...style }}
250
- onPointerDown={startScrub}
251
- onPointerMove={onScrubMove}
252
- onPointerUp={endScrub}
253
- />
254
- );
255
- }
256
-
257
- export function Label({ children }: { children: React.ReactNode }) {
258
- return <label style={styles.label}>{children}</label>;
259
- }
260
-
261
- export function Vector3Input({
262
- label,
263
- value,
264
- onChange,
265
- snap,
266
- labelExtra
267
- }: {
268
- label: string;
269
- value: [number, number, number];
270
- onChange: (v: [number, number, number]) => void;
271
- snap?: number;
272
- labelExtra?: React.ReactNode;
273
- }) {
274
- const snapValue = (num: number) => {
275
- if (!snap) return num;
276
- return Math.round(num / snap) * snap;
277
- };
278
-
279
- const [draft, setDraft] = useState<[string, string, string]>(
280
- () => value.map(v => v.toString()) as any
281
- );
282
-
283
- // Sync external changes (gizmo, undo, etc.)
284
- useEffect(() => {
285
- setDraft(value.map(v => v.toString()) as any);
286
- }, [value[0], value[1], value[2]]);
287
-
288
- const dragState = useRef<{
289
- index: number;
290
- startX: number;
291
- startValue: number;
292
- } | null>(null);
293
-
294
- const commit = (index: number) => {
295
- const num = parseFloat(draft[index]);
296
- if (Number.isFinite(num)) {
297
- const next = [...value] as [number, number, number];
298
- next[index] = snapValue(num);
299
- onChange(next);
300
- }
301
- };
302
-
303
- const startScrub = (e: React.PointerEvent, index: number) => {
304
- dragState.current = {
305
- index,
306
- startX: e.clientX,
307
- startValue: value[index]
308
- };
309
-
310
- e.currentTarget.setPointerCapture(e.pointerId);
311
- document.body.style.cursor = "ew-resize";
312
- };
313
-
314
- const onScrubMove = (e: React.PointerEvent) => {
315
- if (!dragState.current) return;
316
-
317
- const { index, startX, startValue } = dragState.current;
318
- const dx = e.clientX - startX;
319
-
320
- let speed = 0.02;
321
- if (e.shiftKey) speed *= 0.1; // fine
322
- if (e.altKey) speed *= 5; // coarse
323
-
324
- const rawValue = startValue + dx * speed;
325
- const nextValue = snapValue(rawValue);
326
- const next = [...value] as [number, number, number];
327
- next[index] = nextValue;
328
-
329
- setDraft(d => {
330
- const copy = [...d] as any;
331
- copy[index] = nextValue.toFixed(3);
332
- return copy;
333
- });
334
-
335
- onChange(next);
336
- };
337
-
338
- const endScrub = (e: React.PointerEvent) => {
339
- if (!dragState.current) return;
340
-
341
- dragState.current = null;
342
- document.body.style.cursor = "";
343
- e.currentTarget.releasePointerCapture(e.pointerId);
344
- };
345
-
346
- const axes = [
347
- { key: "x", color: '#e06c75', index: 0 },
348
- { key: "y", color: '#98c379', index: 1 },
349
- { key: "z", color: '#61afef', index: 2 }
350
- ] as const;
351
-
352
- return (
353
- <div style={{ marginBottom: 8 }}>
354
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
355
- <label style={{ ...styles.label, marginBottom: 0 }}>{label}</label>
356
- {labelExtra}
357
- </div>
358
- <div style={{ display: 'flex', gap: 4 }}>
359
- {axes.map(({ key, color, index }) => (
360
- <div
361
- key={key}
362
- style={{
363
- flex: 1,
364
- display: 'flex',
365
- alignItems: 'center',
366
- gap: 4,
367
- backgroundColor: colors.bgInput,
368
- border: `1px solid ${colors.border}`,
369
- borderRadius: 3,
370
- padding: '4px 6px',
371
- minHeight: 28,
372
- cursor: 'ew-resize',
373
- }}
374
- onPointerDown={e => startScrub(e, index)}
375
- onPointerMove={onScrubMove}
376
- onPointerUp={endScrub}
377
- >
378
- <span
379
- style={{
380
- fontSize: 11,
381
- fontWeight: 600,
382
- color,
383
- width: 12,
384
- userSelect: 'none',
385
- }}
386
- >
387
- {key.toUpperCase()}
388
- </span>
389
- <input
390
- style={{
391
- flex: 1,
392
- backgroundColor: 'transparent',
393
- border: 'none',
394
- fontSize: 11,
395
- color: colors.text,
396
- fontFamily: 'monospace',
397
- outline: 'none',
398
- width: '100%',
399
- minWidth: 0,
400
- cursor: 'inherit',
401
- }}
402
- type="text"
403
- value={draft[index]}
404
- onChange={e => {
405
- const next = [...draft] as any;
406
- next[index] = e.target.value;
407
- setDraft(next);
408
- }}
409
- onBlur={() => commit(index)}
410
- onKeyDown={e => {
411
- if (e.key === "Enter") {
412
- (e.target as HTMLInputElement).blur();
413
- }
414
- }}
415
- />
416
- </div>
417
- ))}
418
- </div>
419
- </div>
420
- );
421
- }
422
-
423
- // ============================================================================
424
- // Additional Input Components
425
- // ============================================================================
426
-
427
- export function ColorInput({
428
- label,
429
- value,
430
- onChange
431
- }: {
432
- label?: string;
433
- value: string;
434
- onChange: (value: string) => void;
435
- }) {
436
- return (
437
- <div>
438
- {label && <Label>{label}</Label>}
439
- <div style={{ display: 'flex', gap: 4, justifyContent: 'space-between' }}>
440
- <input
441
- type="color"
442
- style={{
443
- height: 32,
444
- width: 48,
445
- backgroundColor: colors.bgInput,
446
- border: `1px solid ${colors.border}`,
447
- borderRadius: 3,
448
- cursor: 'pointer',
449
- padding: 2,
450
- flexShrink: 0,
451
- }}
452
- value={value}
453
- onChange={e => onChange(e.target.value)}
454
- />
455
- <input
456
- type="text"
457
- style={{ ...styles.input, }}
458
- value={value}
459
- onChange={e => onChange(e.target.value)}
460
- />
461
- </div>
462
- </div>
463
- );
464
- }
465
-
466
- export function StringInput({
467
- label,
468
- value,
469
- onChange,
470
- placeholder
471
- }: {
472
- label?: string;
473
- value: string;
474
- onChange: (value: string) => void;
475
- placeholder?: string;
476
- }) {
477
- return (
478
- <div>
479
- {label && <Label>{label}</Label>}
480
- <input
481
- type="text"
482
- style={styles.input}
483
- value={value}
484
- onChange={e => onChange(e.target.value)}
485
- placeholder={placeholder}
486
- />
487
- </div>
488
- );
489
- }
490
-
491
- export function BooleanInput({
492
- label,
493
- value,
494
- onChange
495
- }: {
496
- label?: string;
497
- value: boolean;
498
- onChange: (value: boolean) => void;
499
- }) {
500
- return (
501
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
502
- {label && <Label>{label}</Label>}
503
- <input
504
- type="checkbox"
505
- style={{
506
- height: 16,
507
- width: 16,
508
- accentColor: colors.accent,
509
- cursor: 'pointer',
510
- }}
511
- checked={value}
512
- onChange={e => onChange(e.target.checked)}
513
- />
514
- </div>
515
- );
516
- }
517
-
518
- export function SelectInput({
519
- label,
520
- value,
521
- onChange,
522
- options
523
- }: {
524
- label?: string;
525
- value: string;
526
- onChange: (value: string) => void;
527
- options: { value: string; label: string }[];
528
- }) {
529
- return (
530
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
531
- {label && <Label>{label}</Label>}
532
- <select
533
- style={styles.input as React.CSSProperties}
534
- value={value}
535
- onChange={e => onChange(e.target.value)}
536
- >
537
- {options.map(opt => (
538
- <option key={opt.value} value={opt.value}>
539
- {opt.label}
540
- </option>
541
- ))}
542
- </select>
543
- </div>
544
- );
545
- }
546
-
547
- interface BoundFieldProps {
548
- name: string;
549
- values: Record<string, any>;
550
- onChange: (values: Record<string, any>) => void;
551
- }
552
-
553
- interface BoundNumberFieldProps extends BoundFieldProps {
554
- label: string;
555
- fallback?: number;
556
- step?: string | number;
557
- min?: number;
558
- max?: number;
559
- style?: React.CSSProperties;
560
- }
561
-
562
- interface BoundStringFieldProps extends BoundFieldProps {
563
- label: string;
564
- fallback?: string;
565
- placeholder?: string;
566
- }
567
-
568
- interface BoundColorFieldProps extends BoundFieldProps {
569
- label: string;
570
- fallback?: string;
571
- }
572
-
573
- interface BoundBooleanFieldProps extends BoundFieldProps {
574
- label: string;
575
- fallback?: boolean;
576
- }
577
-
578
- interface BoundSelectFieldProps extends BoundFieldProps {
579
- label: string;
580
- fallback?: string;
581
- options: { value: string; label: string }[];
582
- }
583
-
584
- interface BoundVector3FieldProps extends BoundFieldProps {
585
- label: string;
586
- fallback?: [number, number, number];
587
- snap?: number;
588
- labelExtra?: React.ReactNode;
589
- }
590
-
591
- function bindFieldChange(name: string, onChange: (values: Record<string, any>) => void) {
592
- return (value: any) => onChange({ [name]: value });
593
- }
594
-
595
- export function FieldGroup({ children }: { children: React.ReactNode }) {
596
- return <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>{children}</div>;
597
- }
598
-
599
- export function NumberField({
600
- name,
601
- label,
602
- values,
603
- onChange,
604
- fallback = 0,
605
- step,
606
- min,
607
- max,
608
- style,
609
- }: BoundNumberFieldProps) {
610
- return (
611
- <Input
612
- label={label}
613
- value={values[name] ?? fallback}
614
- onChange={bindFieldChange(name, onChange)}
615
- step={step}
616
- min={min}
617
- max={max}
618
- style={style}
619
- />
620
- );
621
- }
622
-
623
- export function StringField({
624
- name,
625
- label,
626
- values,
627
- onChange,
628
- fallback = '',
629
- placeholder,
630
- }: BoundStringFieldProps) {
631
- return (
632
- <StringInput
633
- label={label}
634
- value={values[name] ?? fallback}
635
- onChange={bindFieldChange(name, onChange)}
636
- placeholder={placeholder}
637
- />
638
- );
639
- }
640
-
641
- export function ColorField({
642
- name,
643
- label,
644
- values,
645
- onChange,
646
- fallback = '#ffffff',
647
- }: BoundColorFieldProps) {
648
- return (
649
- <ColorInput
650
- label={label}
651
- value={values[name] ?? fallback}
652
- onChange={bindFieldChange(name, onChange)}
653
- />
654
- );
655
- }
656
-
657
- export function BooleanField({
658
- name,
659
- label,
660
- values,
661
- onChange,
662
- fallback = false,
663
- }: BoundBooleanFieldProps) {
664
- return (
665
- <BooleanInput
666
- label={label}
667
- value={values[name] ?? fallback}
668
- onChange={bindFieldChange(name, onChange)}
669
- />
670
- );
671
- }
672
-
673
- export function SelectField({
674
- name,
675
- label,
676
- values,
677
- onChange,
678
- fallback,
679
- options,
680
- }: BoundSelectFieldProps) {
681
- return (
682
- <SelectInput
683
- label={label}
684
- value={values[name] ?? fallback ?? options[0]?.value ?? ''}
685
- onChange={bindFieldChange(name, onChange)}
686
- options={options}
687
- />
688
- );
689
- }
690
-
691
- export function Vector3Field({
692
- name,
693
- label,
694
- values,
695
- onChange,
696
- fallback = [0, 0, 0],
697
- snap,
698
- labelExtra,
699
- }: BoundVector3FieldProps) {
700
- return (
701
- <Vector3Input
702
- label={label}
703
- value={values[name] ?? fallback}
704
- onChange={bindFieldChange(name, onChange)}
705
- snap={snap}
706
- labelExtra={labelExtra}
707
- />
708
- );
709
- }
710
-
711
- // ============================================================================
712
- // Field Renderer - Schema-driven UI generation
713
- // ============================================================================
714
-
715
- interface FieldRendererProps {
716
- fields: FieldDefinition[];
717
- values: Record<string, any>;
718
- onChange: (values: Record<string, any>) => void;
719
- }
720
-
721
- export function FieldRenderer({ fields, values, onChange }: FieldRendererProps) {
722
- const updateField = (name: string, value: any) => {
723
- onChange({ [name]: value });
724
- };
725
-
726
- return (
727
- <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
728
- {fields.map(field => {
729
- const value = values[field.name];
730
-
731
- switch (field.type) {
732
- case 'vector3':
733
- return (
734
- <Vector3Input
735
- key={field.name}
736
- label={field.label}
737
- value={value ?? [0, 0, 0]}
738
- onChange={v => updateField(field.name, v)}
739
- snap={field.snap}
740
- />
741
- );
742
-
743
- case 'number':
744
- return (
745
- <Input
746
- key={field.name}
747
- label={field.label}
748
- value={value ?? 0}
749
- onChange={v => updateField(field.name, v)}
750
- min={field.min}
751
- max={field.max}
752
- step={field.step}
753
- />
754
- );
755
-
756
- case 'string':
757
- return (
758
- <StringInput
759
- key={field.name}
760
- label={field.label}
761
- value={value ?? ''}
762
- onChange={v => updateField(field.name, v)}
763
- placeholder={field.placeholder}
764
- />
765
- );
766
-
767
- case 'color':
768
- return (
769
- <ColorInput
770
- key={field.name}
771
- label={field.label}
772
- value={value ?? '#ffffff'}
773
- onChange={v => updateField(field.name, v)}
774
- />
775
- );
776
-
777
- case 'boolean':
778
- return (
779
- <BooleanInput
780
- key={field.name}
781
- label={field.label}
782
- value={value ?? false}
783
- onChange={v => updateField(field.name, v)}
784
- />
785
- );
786
-
787
- case 'select':
788
- return (
789
- <SelectInput
790
- key={field.name}
791
- label={field.label}
792
- value={value ?? field.options[0]?.value ?? ''}
793
- onChange={v => updateField(field.name, v)}
794
- options={field.options}
795
- />
796
- );
797
-
798
- case 'custom':
799
- return (
800
- <div key={field.name}>
801
- {field.label && <Label>{field.label}</Label>}
802
- {field.render({
803
- value,
804
- onChange: v => updateField(field.name, v),
805
- values,
806
- onChangeMultiple: onChange,
807
- })}
808
- </div>
809
- );
810
-
811
- default:
812
- return null;
813
- }
814
- })}
815
- </div>
816
- );
817
- }
818
-
819
- // Export styles for use in custom field renderers
820
- export { styles };