react-three-game 0.0.60 → 0.0.61

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 (58) hide show
  1. package/package.json +9 -3
  2. package/.gitattributes +0 -2
  3. package/.github/copilot-instructions.md +0 -83
  4. package/.github/workflows/nextjs.yml +0 -99
  5. package/.gitmodules +0 -3
  6. package/assets/architecture.png +0 -0
  7. package/assets/editor.gif +0 -0
  8. package/assets/favicon.ico +0 -0
  9. package/assets/react-three-game-logo.png +0 -0
  10. package/dist/tools/dragdrop/page.d.ts +0 -1
  11. package/dist/tools/dragdrop/page.js +0 -11
  12. package/dist/tools/prefabeditor/EntityEvents.d.ts +0 -54
  13. package/dist/tools/prefabeditor/EntityEvents.js +0 -85
  14. package/dist/tools/prefabeditor/page.d.ts +0 -1
  15. package/dist/tools/prefabeditor/page.js +0 -5
  16. package/react-three-game-skill/.gitattributes +0 -2
  17. package/react-three-game-skill/README.md +0 -7
  18. package/react-three-game-skill/react-three-game/SKILL.md +0 -514
  19. package/react-three-game-skill/react-three-game/rules/ADVANCED_PHYSICS.md +0 -472
  20. package/react-three-game-skill/react-three-game/rules/LIGHTING.md +0 -6
  21. package/src/helpers/SoundManager.ts +0 -130
  22. package/src/helpers/index.ts +0 -91
  23. package/src/index.ts +0 -59
  24. package/src/shared/ContactShadow.tsx +0 -74
  25. package/src/shared/GameCanvas.tsx +0 -52
  26. package/src/tools/assetviewer/page.tsx +0 -425
  27. package/src/tools/dragdrop/DragDropLoader.tsx +0 -159
  28. package/src/tools/dragdrop/index.ts +0 -4
  29. package/src/tools/dragdrop/modelLoader.ts +0 -204
  30. package/src/tools/dragdrop/page.tsx +0 -45
  31. package/src/tools/prefabeditor/Dropdown.tsx +0 -112
  32. package/src/tools/prefabeditor/EditorContext.tsx +0 -25
  33. package/src/tools/prefabeditor/EditorTree.tsx +0 -452
  34. package/src/tools/prefabeditor/EditorTreeMenus.tsx +0 -307
  35. package/src/tools/prefabeditor/EditorUI.tsx +0 -204
  36. package/src/tools/prefabeditor/EventSystem.tsx +0 -36
  37. package/src/tools/prefabeditor/GameEvents.ts +0 -191
  38. package/src/tools/prefabeditor/InstanceProvider.tsx +0 -466
  39. package/src/tools/prefabeditor/PrefabEditor.tsx +0 -256
  40. package/src/tools/prefabeditor/PrefabRoot.tsx +0 -767
  41. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +0 -34
  42. package/src/tools/prefabeditor/components/CameraComponent.tsx +0 -117
  43. package/src/tools/prefabeditor/components/ComponentRegistry.ts +0 -40
  44. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +0 -210
  45. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +0 -47
  46. package/src/tools/prefabeditor/components/GeometryComponent.tsx +0 -133
  47. package/src/tools/prefabeditor/components/Input.tsx +0 -820
  48. package/src/tools/prefabeditor/components/MaterialComponent.tsx +0 -431
  49. package/src/tools/prefabeditor/components/ModelComponent.tsx +0 -176
  50. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +0 -188
  51. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +0 -109
  52. package/src/tools/prefabeditor/components/TextComponent.tsx +0 -137
  53. package/src/tools/prefabeditor/components/TransformComponent.tsx +0 -173
  54. package/src/tools/prefabeditor/components/index.ts +0 -26
  55. package/src/tools/prefabeditor/page.tsx +0 -10
  56. package/src/tools/prefabeditor/styles.ts +0 -235
  57. package/src/tools/prefabeditor/types.ts +0 -20
  58. 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 };