react-three-game 0.0.55 → 0.0.57

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 (68) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +1 -1
  3. package/dist/shared/ContactShadow.d.ts +8 -0
  4. package/dist/shared/ContactShadow.js +32 -0
  5. package/dist/shared/GameCanvas.js +1 -3
  6. package/dist/tools/assetviewer/page.js +36 -15
  7. package/dist/tools/dragdrop/DragDropLoader.js +17 -40
  8. package/dist/tools/dragdrop/modelLoader.d.ts +5 -0
  9. package/dist/tools/dragdrop/modelLoader.js +39 -0
  10. package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
  11. package/dist/tools/prefabeditor/Dropdown.js +82 -0
  12. package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
  13. package/dist/tools/prefabeditor/EditorTree.js +139 -70
  14. package/dist/tools/prefabeditor/EditorUI.js +5 -10
  15. package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
  16. package/dist/tools/prefabeditor/PrefabEditor.js +70 -3
  17. package/dist/tools/prefabeditor/PrefabRoot.d.ts +3 -0
  18. package/dist/tools/prefabeditor/PrefabRoot.js +136 -35
  19. package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
  20. package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
  21. package/dist/tools/prefabeditor/components/CameraComponent.js +25 -0
  22. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +2 -2
  23. package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
  24. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
  25. package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
  26. package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
  27. package/dist/tools/prefabeditor/components/Input.js +100 -47
  28. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +8 -2
  29. package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -14
  30. package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
  31. package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
  32. package/dist/tools/prefabeditor/components/SpotLightComponent.js +6 -11
  33. package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
  34. package/dist/tools/prefabeditor/components/TransformComponent.js +31 -19
  35. package/dist/tools/prefabeditor/components/index.js +5 -1
  36. package/dist/tools/prefabeditor/styles.d.ts +17 -4
  37. package/dist/tools/prefabeditor/styles.js +69 -32
  38. package/dist/tools/prefabeditor/utils.d.ts +8 -3
  39. package/dist/tools/prefabeditor/utils.js +92 -6
  40. package/package.json +1 -1
  41. package/react-three-game-skill/react-three-game/rules/LIGHTING.md +6 -0
  42. package/src/index.ts +7 -0
  43. package/src/shared/ContactShadow.tsx +74 -0
  44. package/src/shared/GameCanvas.tsx +0 -3
  45. package/src/tools/assetviewer/page.tsx +78 -46
  46. package/src/tools/dragdrop/DragDropLoader.tsx +7 -39
  47. package/src/tools/dragdrop/modelLoader.ts +36 -0
  48. package/src/tools/prefabeditor/Dropdown.tsx +112 -0
  49. package/src/tools/prefabeditor/EditorContext.tsx +5 -0
  50. package/src/tools/prefabeditor/EditorTree.tsx +237 -115
  51. package/src/tools/prefabeditor/EditorUI.tsx +6 -11
  52. package/src/tools/prefabeditor/PrefabEditor.tsx +77 -5
  53. package/src/tools/prefabeditor/PrefabRoot.tsx +228 -59
  54. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
  55. package/src/tools/prefabeditor/components/CameraComponent.tsx +80 -0
  56. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +2 -2
  57. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
  58. package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
  59. package/src/tools/prefabeditor/components/Input.tsx +247 -53
  60. package/src/tools/prefabeditor/components/MaterialComponent.tsx +191 -20
  61. package/src/tools/prefabeditor/components/ModelComponent.tsx +52 -5
  62. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
  63. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +14 -16
  64. package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
  65. package/src/tools/prefabeditor/components/TransformComponent.tsx +78 -20
  66. package/src/tools/prefabeditor/components/index.ts +5 -1
  67. package/src/tools/prefabeditor/styles.ts +71 -32
  68. package/src/tools/prefabeditor/utils.ts +96 -5
@@ -0,0 +1,47 @@
1
+ import { Environment } from '@react-three/drei';
2
+ import { Component } from './ComponentRegistry';
3
+ import { FieldGroup, NumberField } from './Input';
4
+ import { Object3D, Texture } from 'three';
5
+
6
+ function EnvironmentView({
7
+ properties,
8
+ children,
9
+ editMode,
10
+ loadedTextures,
11
+ loadedModels,
12
+ }: {
13
+ properties: any;
14
+ children?: React.ReactNode;
15
+ editMode?: boolean;
16
+ loadedTextures?: Record<string, Texture>;
17
+ loadedModels?: Record<string, Object3D>;
18
+ }) {
19
+ const { intensity = 1, resolution = 256 } = properties;
20
+ const assetRevision = `${Object.keys(loadedTextures ?? {}).sort().join('|')}::${Object.keys(loadedModels ?? {}).sort().join('|')}`;
21
+
22
+ return (
23
+ <Environment
24
+ key={assetRevision}
25
+ background={true}
26
+ environmentIntensity={intensity}
27
+ resolution={resolution}
28
+ frames={editMode ? undefined : 1}
29
+ >
30
+ {children}
31
+ </Environment>
32
+ );
33
+ }
34
+
35
+ const EnvironmentComponent: Component = {
36
+ name: 'Environment',
37
+ Editor: ({ component, onUpdate }) => (
38
+ <FieldGroup>
39
+ <NumberField name="intensity" label="Intensity" values={component.properties} onChange={onUpdate} min={0} step={0.1} fallback={1} />
40
+ <NumberField name="resolution" label="Resolution" values={component.properties} onChange={onUpdate} min={64} step={64} fallback={256} />
41
+ </FieldGroup>
42
+ ),
43
+ View: EnvironmentView,
44
+ defaultProperties: {},
45
+ };
46
+
47
+ export default EnvironmentComponent;
@@ -1,28 +1,49 @@
1
1
  import { Component } from "./ComponentRegistry";
2
- import { FieldRenderer, FieldDefinition, Input, Label } from "./Input";
2
+ import { FieldGroup, NumberField, SelectField } from "./Input";
3
3
 
4
4
  const GEOMETRY_ARGS: Record<string, {
5
- labels: string[];
6
- defaults: number[];
5
+ fields: Array<{
6
+ name: string;
7
+ label: string;
8
+ defaultValue: number;
9
+ min?: number;
10
+ step?: number;
11
+ }>;
7
12
  }> = {
8
13
  box: {
9
- labels: ["Width", "Height", "Depth"],
10
- defaults: [1, 1, 1],
14
+ fields: [
15
+ { name: 'width', label: 'Width', defaultValue: 1, min: 0.01, step: 0.1 },
16
+ { name: 'height', label: 'Height', defaultValue: 1, min: 0.01, step: 0.1 },
17
+ { name: 'depth', label: 'Depth', defaultValue: 1, min: 0.01, step: 0.1 },
18
+ ],
11
19
  },
12
20
  sphere: {
13
- labels: ["Radius", "Width Segments", "Height Segments"],
14
- defaults: [1, 32, 16],
21
+ fields: [
22
+ { name: 'radius', label: 'Radius', defaultValue: 1, min: 0.01, step: 0.1 },
23
+ { name: 'widthSegments', label: 'Width Segments', defaultValue: 32, min: 3, step: 1 },
24
+ { name: 'heightSegments', label: 'Height Segments', defaultValue: 16, min: 2, step: 1 },
25
+ ],
15
26
  },
16
27
  plane: {
17
- labels: ["Width", "Height"],
18
- defaults: [1, 1],
28
+ fields: [
29
+ { name: 'width', label: 'Width', defaultValue: 1, min: 0.01, step: 0.1 },
30
+ { name: 'height', label: 'Height', defaultValue: 1, min: 0.01, step: 0.1 },
31
+ ],
19
32
  },
20
33
  cylinder: {
21
- labels: ["Radius Top", "Radius Bottom", "Height", "Radial Segments"],
22
- defaults: [1, 1, 1, 32],
34
+ fields: [
35
+ { name: 'radiusTop', label: 'Radius Top', defaultValue: 1, min: 0.01, step: 0.1 },
36
+ { name: 'radiusBottom', label: 'Radius Bottom', defaultValue: 1, min: 0.01, step: 0.1 },
37
+ { name: 'height', label: 'Height', defaultValue: 1, min: 0.01, step: 0.1 },
38
+ { name: 'radialSegments', label: 'Radial Segments', defaultValue: 32, min: 3, step: 1 },
39
+ ],
23
40
  },
24
41
  };
25
42
 
43
+ function getDefaultArgs(geometryType: string) {
44
+ return (GEOMETRY_ARGS[geometryType]?.fields ?? []).map(field => field.defaultValue);
45
+ }
46
+
26
47
  function GeometryComponentEditor({
27
48
  component,
28
49
  onUpdate,
@@ -30,67 +51,52 @@ function GeometryComponentEditor({
30
51
  component: any;
31
52
  onUpdate: (newProps: any) => void;
32
53
  }) {
33
- const { geometryType, args = [] } = component.properties;
34
- const schema = GEOMETRY_ARGS[geometryType];
35
-
36
- const fields: FieldDefinition[] = [
37
- {
38
- name: 'geometryType',
39
- type: 'select',
40
- label: 'Type',
41
- options: [
42
- { value: 'box', label: 'Box' },
43
- { value: 'sphere', label: 'Sphere' },
44
- { value: 'plane', label: 'Plane' },
45
- { value: 'cylinder', label: 'Cylinder' },
46
- ],
47
- },
48
- {
49
- name: 'args',
50
- type: 'custom',
51
- label: '',
52
- render: ({ values, onChangeMultiple }) => {
53
- const currentType = values.geometryType;
54
- const currentSchema = GEOMETRY_ARGS[currentType];
55
- const currentArgs = values.args || currentSchema.defaults;
56
-
57
- return (
58
- <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
59
- {currentSchema.labels.map((label, i) => (
60
- <Input
61
- key={label}
62
- label={label}
63
- value={currentArgs[i] ?? currentSchema.defaults[i]}
64
- step={0.1}
65
- min={0.01}
66
- onChange={value => {
67
- const next = [...currentArgs];
68
- next[i] = value;
69
- onChangeMultiple({ args: next });
70
- }}
71
- />
72
- ))}
73
- </div>
74
- );
75
- },
76
- },
77
- ];
54
+ const geometryType = component.properties.geometryType ?? 'box';
55
+ const schema = GEOMETRY_ARGS[geometryType] ?? GEOMETRY_ARGS.box;
56
+ const args = component.properties.args ?? getDefaultArgs(geometryType);
78
57
 
79
58
  // Handle geometry type change to reset args
80
59
  const handleChange = (newValues: Record<string, any>) => {
81
60
  if ('geometryType' in newValues && newValues.geometryType !== geometryType) {
82
- onUpdate({ geometryType: newValues.geometryType, args: GEOMETRY_ARGS[newValues.geometryType].defaults });
61
+ onUpdate({ geometryType: newValues.geometryType, args: getDefaultArgs(newValues.geometryType) });
83
62
  } else {
84
63
  onUpdate(newValues);
85
64
  }
86
65
  };
87
66
 
67
+ const updateArg = (index: number, value: number) => {
68
+ const next = [...args];
69
+ next[index] = value;
70
+ onUpdate({ args: next });
71
+ };
72
+
88
73
  return (
89
- <FieldRenderer
90
- fields={fields}
91
- values={component.properties}
92
- onChange={handleChange}
93
- />
74
+ <FieldGroup>
75
+ <SelectField
76
+ name="geometryType"
77
+ label="Type"
78
+ values={component.properties}
79
+ onChange={handleChange}
80
+ options={[
81
+ { value: 'box', label: 'Box' },
82
+ { value: 'sphere', label: 'Sphere' },
83
+ { value: 'plane', label: 'Plane' },
84
+ { value: 'cylinder', label: 'Cylinder' },
85
+ ]}
86
+ />
87
+ {schema.fields.map((field, index) => (
88
+ <NumberField
89
+ key={field.name}
90
+ name={field.name}
91
+ label={field.label}
92
+ values={{ [field.name]: args[index] ?? field.defaultValue }}
93
+ onChange={(next) => updateArg(index, next[field.name])}
94
+ fallback={field.defaultValue}
95
+ min={field.min}
96
+ step={field.step}
97
+ />
98
+ ))}
99
+ </FieldGroup>
94
100
  );
95
101
  }
96
102
 
@@ -120,7 +126,7 @@ const GeometryComponent: Component = {
120
126
  nonComposable: true,
121
127
  defaultProperties: {
122
128
  geometryType: 'box',
123
- args: GEOMETRY_ARGS.box.defaults,
129
+ args: getDefaultArgs('box'),
124
130
  }
125
131
  };
126
132
 
@@ -1,4 +1,5 @@
1
1
  import React, { useEffect, useRef, useState } from 'react';
2
+ import { colors } from '../styles';
2
3
 
3
4
  // ============================================================================
4
5
  // Field Definition Types
@@ -61,32 +62,57 @@ export type FieldDefinition =
61
62
  | CustomFieldDefinition;
62
63
 
63
64
  // ============================================================================
64
- // Shared Styles
65
+ // Shared Styles (derived from shared color tokens)
65
66
  // ============================================================================
66
67
 
67
- // Shared styles
68
68
  const styles = {
69
69
  input: {
70
70
  width: '80px',
71
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
72
- border: '1px solid rgba(34, 211, 238, 0.3)',
73
- padding: '2px 4px',
74
- fontSize: '10px',
75
- color: 'rgba(165, 243, 252, 1)',
71
+ backgroundColor: colors.bgInput,
72
+ border: `1px solid ${colors.border}`,
73
+ padding: '3px 6px',
74
+ fontSize: '11px',
75
+ color: colors.text,
76
76
  fontFamily: 'monospace',
77
77
  outline: 'none',
78
78
  textAlign: 'right',
79
+ borderRadius: 3,
79
80
  } as React.CSSProperties,
80
81
  label: {
81
82
  display: 'block',
82
- fontSize: '9px',
83
- color: 'rgba(34, 211, 238, 0.9)',
83
+ fontSize: '10px',
84
+ color: colors.textMuted,
84
85
  textTransform: 'uppercase',
85
86
  letterSpacing: '0.05em',
86
87
  marginBottom: 2,
88
+ fontWeight: 500,
87
89
  } as React.CSSProperties,
88
90
  };
89
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
+
90
116
  interface InputProps {
91
117
  value: number;
92
118
  onChange: (value: number) => void;
@@ -127,15 +153,12 @@ export function Input({ value, onChange, step, min, max, style, label }: InputPr
127
153
  } | null>(null);
128
154
 
129
155
  const startScrub = (e: React.PointerEvent) => {
130
- if (!label) return;
131
- e.preventDefault();
132
-
133
156
  dragState.current = {
134
157
  startX: e.clientX,
135
158
  startValue: value
136
159
  };
137
160
 
138
- (e.target as HTMLElement).setPointerCapture(e.pointerId);
161
+ e.currentTarget.setPointerCapture(e.pointerId);
139
162
  document.body.style.cursor = "ew-resize";
140
163
  };
141
164
 
@@ -144,18 +167,20 @@ export function Input({ value, onChange, step, min, max, style, label }: InputPr
144
167
 
145
168
  const { startX, startValue } = dragState.current;
146
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;
147
174
 
148
- let speed = 0.02;
149
- if (e.shiftKey) speed *= 0.1; // fine
150
- if (e.altKey) speed *= 5; // coarse
151
-
152
- let nextValue = startValue + dx * speed;
175
+ const precision = getStepPrecision(scrubStep);
176
+ const deltaSteps = Math.round(dx / 8);
177
+ let nextValue = startValue + deltaSteps * scrubStep;
153
178
 
154
179
  // Apply min/max constraints
155
180
  if (min !== undefined && nextValue < min) nextValue = min;
156
181
  if (max !== undefined && nextValue > max) nextValue = max;
157
182
 
158
- setDraft(nextValue.toFixed(3));
183
+ setDraft(nextValue.toFixed(precision));
159
184
  onChange(nextValue);
160
185
  };
161
186
 
@@ -164,7 +189,7 @@ export function Input({ value, onChange, step, min, max, style, label }: InputPr
164
189
 
165
190
  dragState.current = null;
166
191
  document.body.style.cursor = "";
167
- (e.target as HTMLElement).releasePointerCapture(e.pointerId);
192
+ e.currentTarget.releasePointerCapture(e.pointerId);
168
193
  };
169
194
 
170
195
  if (label) {
@@ -178,14 +203,10 @@ export function Input({ value, onChange, step, min, max, style, label }: InputPr
178
203
  style={{
179
204
  ...styles.label,
180
205
  marginBottom: 0,
181
- cursor: 'ew-resize',
182
206
  userSelect: 'none',
183
207
  flex: '0 0 auto',
184
208
  minWidth: 20,
185
209
  }}
186
- onPointerDown={startScrub}
187
- onPointerMove={onScrubMove}
188
- onPointerUp={endScrub}
189
210
  >
190
211
  {label}
191
212
  </span>
@@ -202,7 +223,10 @@ export function Input({ value, onChange, step, min, max, style, label }: InputPr
202
223
  step={step}
203
224
  min={min}
204
225
  max={max}
205
- style={{ ...styles.input, ...style }}
226
+ style={{ ...styles.input, cursor: 'ew-resize', ...style }}
227
+ onPointerDown={startScrub}
228
+ onPointerMove={onScrubMove}
229
+ onPointerUp={endScrub}
206
230
  />
207
231
  </div>
208
232
  );
@@ -222,7 +246,10 @@ export function Input({ value, onChange, step, min, max, style, label }: InputPr
222
246
  step={step}
223
247
  min={min}
224
248
  max={max}
225
- style={{ ...styles.input, ...style }}
249
+ style={{ ...styles.input, cursor: 'ew-resize', ...style }}
250
+ onPointerDown={startScrub}
251
+ onPointerMove={onScrubMove}
252
+ onPointerUp={endScrub}
226
253
  />
227
254
  );
228
255
  }
@@ -235,12 +262,14 @@ export function Vector3Input({
235
262
  label,
236
263
  value,
237
264
  onChange,
238
- snap
265
+ snap,
266
+ labelExtra
239
267
  }: {
240
268
  label: string;
241
269
  value: [number, number, number];
242
270
  onChange: (v: [number, number, number]) => void;
243
271
  snap?: number;
272
+ labelExtra?: React.ReactNode;
244
273
  }) {
245
274
  const snapValue = (num: number) => {
246
275
  if (!snap) return num;
@@ -272,15 +301,13 @@ export function Vector3Input({
272
301
  };
273
302
 
274
303
  const startScrub = (e: React.PointerEvent, index: number) => {
275
- e.preventDefault();
276
-
277
304
  dragState.current = {
278
305
  index,
279
306
  startX: e.clientX,
280
307
  startValue: value[index]
281
308
  };
282
309
 
283
- (e.target as HTMLElement).setPointerCapture(e.pointerId);
310
+ e.currentTarget.setPointerCapture(e.pointerId);
284
311
  document.body.style.cursor = "ew-resize";
285
312
  };
286
313
 
@@ -313,18 +340,21 @@ export function Vector3Input({
313
340
 
314
341
  dragState.current = null;
315
342
  document.body.style.cursor = "";
316
- (e.target as HTMLElement).releasePointerCapture(e.pointerId);
343
+ e.currentTarget.releasePointerCapture(e.pointerId);
317
344
  };
318
345
 
319
346
  const axes = [
320
- { key: "x", color: 'rgba(248, 113, 113, 1)', index: 0 },
321
- { key: "y", color: 'rgba(134, 239, 172, 1)', index: 1 },
322
- { key: "z", color: 'rgba(96, 165, 250, 1)', index: 2 }
347
+ { key: "x", color: '#e06c75', index: 0 },
348
+ { key: "y", color: '#98c379', index: 1 },
349
+ { key: "z", color: '#61afef', index: 2 }
323
350
  ] as const;
324
351
 
325
352
  return (
326
353
  <div style={{ marginBottom: 8 }}>
327
- <label style={{ ...styles.label, marginBottom: 4 }}>{label}</label>
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>
328
358
  <div style={{ display: 'flex', gap: 4 }}>
329
359
  {axes.map(({ key, color, index }) => (
330
360
  <div
@@ -334,25 +364,25 @@ export function Vector3Input({
334
364
  display: 'flex',
335
365
  alignItems: 'center',
336
366
  gap: 4,
337
- backgroundColor: 'rgba(0, 0, 0, 0.3)',
338
- border: '1px solid rgba(34, 211, 238, 0.2)',
339
- borderRadius: 4,
367
+ backgroundColor: colors.bgInput,
368
+ border: `1px solid ${colors.border}`,
369
+ borderRadius: 3,
340
370
  padding: '4px 6px',
341
- minHeight: 32,
371
+ minHeight: 28,
372
+ cursor: 'ew-resize',
342
373
  }}
374
+ onPointerDown={e => startScrub(e, index)}
375
+ onPointerMove={onScrubMove}
376
+ onPointerUp={endScrub}
343
377
  >
344
378
  <span
345
379
  style={{
346
- fontSize: '12px',
347
- fontWeight: 'bold',
380
+ fontSize: 11,
381
+ fontWeight: 600,
348
382
  color,
349
383
  width: 12,
350
- cursor: 'ew-resize',
351
384
  userSelect: 'none',
352
385
  }}
353
- onPointerDown={e => startScrub(e, index)}
354
- onPointerMove={onScrubMove}
355
- onPointerUp={endScrub}
356
386
  >
357
387
  {key.toUpperCase()}
358
388
  </span>
@@ -361,12 +391,13 @@ export function Vector3Input({
361
391
  flex: 1,
362
392
  backgroundColor: 'transparent',
363
393
  border: 'none',
364
- fontSize: '12px',
365
- color: 'rgba(165, 243, 252, 1)',
394
+ fontSize: 11,
395
+ color: colors.text,
366
396
  fontFamily: 'monospace',
367
397
  outline: 'none',
368
398
  width: '100%',
369
399
  minWidth: 0,
400
+ cursor: 'inherit',
370
401
  }}
371
402
  type="text"
372
403
  value={draft[index]}
@@ -411,11 +442,11 @@ export function ColorInput({
411
442
  style={{
412
443
  height: 32,
413
444
  width: 48,
414
- backgroundColor: 'transparent',
415
- border: '1px solid rgba(34, 211, 238, 0.3)',
416
- borderRadius: 4,
445
+ backgroundColor: colors.bgInput,
446
+ border: `1px solid ${colors.border}`,
447
+ borderRadius: 3,
417
448
  cursor: 'pointer',
418
- padding: 0,
449
+ padding: 2,
419
450
  flexShrink: 0,
420
451
  }}
421
452
  value={value}
@@ -474,8 +505,7 @@ export function BooleanInput({
474
505
  style={{
475
506
  height: 16,
476
507
  width: 16,
477
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
478
- border: '1px solid rgba(34, 211, 238, 0.3)',
508
+ accentColor: colors.accent,
479
509
  cursor: 'pointer',
480
510
  }}
481
511
  checked={value}
@@ -514,6 +544,170 @@ export function SelectInput({
514
544
  );
515
545
  }
516
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
+
517
711
  // ============================================================================
518
712
  // Field Renderer - Schema-driven UI generation
519
713
  // ============================================================================