react-three-game 0.0.30 → 0.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -138,15 +138,28 @@ React 19 · Three.js WebGPU · TypeScript 5 · Rapier WASM · MIT License
138
138
 
139
139
  A small helper script is included to auto-generate asset manifests from the `public` folder. See `docs/generate-manifests.sh`.
140
140
 
141
- - What it does: searches `public/models` for `.glb`/`.fbx`, `public/textures` for `.jpg`/`.png`, and `public/sound` for `.mp3`/`.wav`, then writes JSON arrays to `public/models/manifest.json`, `public/textures/manifest.json`, and `public/sound/manifest.json`. These manifest files are used top populate the Asset Viewer in the the Editor.
142
- - How to run:
141
+ - **What it does:**
142
+ Searches `public/models` for `.glb`/`.fbx`, `public/textures` for `.jpg`/`.png`, and `public/sound` for `.mp3`/`.wav`, then writes JSON arrays to:
143
+ - `public/models/manifest.json`
144
+ - `public/textures/manifest.json`
145
+ - `public/sound/manifest.json`
146
+
147
+ These manifest files are used to populate the Asset Viewer in the Editor.
148
+
149
+ - **How to run:**
143
150
 
144
151
  1. Make it executable (once):
145
152
 
146
- chmod +x docs/generate-manifests.sh
153
+ ```sh
154
+ chmod +x docs/generate-manifests.sh
155
+ ```
147
156
 
148
157
  2. Run the script from the repo root (zsh/bash):
149
158
 
150
- ./docs/generate-manifests.sh
159
+ ```sh
160
+ ./docs/generate-manifests.sh
161
+ ```
162
+
151
163
 
152
- The script is intentionally simple and portable (uses `find`/`sed`). If you need different file types or output formatting, edit `docs/generate-manifests.sh`.
164
+ The script is intentionally simple and portable (uses `find`/`sed`).
165
+ If you need different file types or output formatting, edit `docs/generate-manifests.sh`.
@@ -6,6 +6,12 @@ import { Suspense, useEffect, useState, useRef } from "react";
6
6
  import { TextureLoader } from "three";
7
7
  import { loadModel } from "../dragdrop/modelLoader";
8
8
  // view models and textures in manifest, onselect callback
9
+ const styles = {
10
+ errorIcon: { color: '#fca5a5', fontSize: 12 }, // text-red-400 text-xs
11
+ flexFillRelative: { flex: 1, position: 'relative' },
12
+ bottomLabel: { backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' },
13
+ iconLarge: { fontSize: 20 }
14
+ };
9
15
  function getItemsInPath(files, currentPath) {
10
16
  // Remove the leading category folder (e.g., /textures/, /models/, /sounds/)
11
17
  const filesWithoutCategory = files.map(file => {
@@ -30,7 +36,15 @@ function getItemsInPath(files, currentPath) {
30
36
  return { folders: Array.from(folders), filesInCurrentPath };
31
37
  }
32
38
  function FolderTile({ name, onClick }) {
33
- return (_jsxs("div", { onClick: onClick, className: "aspect-square bg-gray-800 cursor-pointer hover:bg-gray-700 flex flex-col items-center justify-center", children: [_jsx("div", { className: "text-3xl", children: "\uD83D\uDCC1" }), _jsx("div", { className: "text-xs text-center truncate w-full px-1 mt-1", children: name })] }));
39
+ return (_jsxs("div", { onClick: onClick, style: {
40
+ aspectRatio: '1 / 1',
41
+ backgroundColor: '#1f2937', /* gray-800 */
42
+ cursor: 'pointer',
43
+ display: 'flex',
44
+ flexDirection: 'column',
45
+ alignItems: 'center',
46
+ justifyContent: 'center'
47
+ }, children: [_jsx("div", { style: { fontSize: 24 }, children: "\uD83D\uDCC1" }), _jsx("div", { style: { fontSize: 10, textAlign: 'center', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', width: '100%', padding: '0 4px', marginTop: 4 }, children: name })] }));
34
48
  }
35
49
  function useInView() {
36
50
  const [isInView, setIsInView] = useState(false);
@@ -56,13 +70,13 @@ function AssetListViewer({ files, selected, onSelect, renderCard }) {
56
70
  const { folders, filesInCurrentPath } = getItemsInPath(files, currentPath);
57
71
  const showCompactView = selected && !showPicker;
58
72
  if (showCompactView) {
59
- return (_jsxs("div", { className: "flex gap-1 items-center", children: [renderCard(selected, onSelect), _jsx("button", { onClick: () => setShowPicker(true), className: "px-2 py-1 bg-gray-800 hover:bg-gray-700 text-xs", children: "Change" })] }));
73
+ return (_jsxs("div", { style: { display: 'flex', gap: 4, alignItems: 'center' }, children: [renderCard(selected, onSelect), _jsx("button", { onClick: () => setShowPicker(true), style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 12, cursor: 'pointer', border: 'none' }, children: "Change" })] }));
60
74
  }
61
75
  return (_jsxs("div", { children: [currentPath && (_jsx("button", { onClick: () => {
62
76
  const pathParts = currentPath.split('/').filter(Boolean);
63
77
  pathParts.pop();
64
78
  setCurrentPath(pathParts.join('/'));
65
- }, className: "mb-1 px-2 py-1 bg-gray-800 hover:bg-gray-700 text-xs", children: "\u2190 Back" })), _jsxs("div", { className: "grid grid-cols-3 gap-1", children: [folders.map((folder) => (_jsx(FolderTile, { name: folder, onClick: () => setCurrentPath(currentPath ? `${currentPath}/${folder}` : folder) }, folder))), filesInCurrentPath.map((file) => (_jsx("div", { children: renderCard(file, (f) => {
79
+ }, style: { marginBottom: 4, padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 12, cursor: 'pointer', border: 'none' }, children: "\u2190 Back" })), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4 }, children: [folders.map((folder) => (_jsx(FolderTile, { name: folder, onClick: () => setCurrentPath(currentPath ? `${currentPath}/${folder}` : folder) }, folder))), filesInCurrentPath.map((file) => (_jsx("div", { children: renderCard(file, (f) => {
66
80
  onSelect(f);
67
81
  if (selected)
68
82
  setShowPicker(false);
@@ -77,9 +91,9 @@ function TextureCard({ file, onSelect, basePath = "" }) {
77
91
  const { ref, isInView } = useInView();
78
92
  const fullPath = basePath ? `/${basePath}${file}` : file;
79
93
  if (error) {
80
- return (_jsx("div", { ref: ref, className: "aspect-square bg-gray-700 cursor-pointer hover:bg-gray-600 flex items-center justify-center", onClick: () => onSelect(file), children: _jsx("div", { className: "text-red-400 text-xs", children: "\u2717" }) }));
94
+ return (_jsx("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: () => onSelect(file), children: _jsx("div", { style: styles.errorIcon, children: "\u2717" }) }));
81
95
  }
82
- return (_jsxs("div", { ref: ref, className: "aspect-square bg-gray-800 cursor-pointer hover:bg-gray-700 flex flex-col", onClick: () => onSelect(file), onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), children: [_jsx("div", { className: "flex-1 relative", children: isInView ? (_jsxs(View, { className: "w-full h-full", children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 0, 2.5], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx("ambientLight", { intensity: 0.8 }), _jsx("pointLight", { position: [5, 5, 5], intensity: 0.5 }), _jsx(TextureSphere, { url: fullPath, onError: () => setError(true) }), _jsx(OrbitControls, { enableZoom: false, enablePan: false, autoRotate: isHovered, autoRotateSpeed: 2 })] })] })) : null }), _jsx("div", { className: "bg-black/60 text-[10px] px-1 truncate text-center", children: file.split('/').pop() })] }));
96
+ return (_jsxs("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#1f2937', cursor: 'pointer', display: 'flex', flexDirection: 'column' }, onClick: () => onSelect(file), onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), children: [_jsx("div", { style: { flex: 1, position: 'relative' }, children: isInView ? (_jsxs(View, { style: { width: '100%', height: '100%' }, children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 0, 2.5], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx("ambientLight", { intensity: 0.8 }), _jsx("pointLight", { position: [5, 5, 5], intensity: 0.5 }), _jsx(TextureSphere, { url: fullPath, onError: () => setError(true) }), _jsx(OrbitControls, { enableZoom: false, enablePan: false, autoRotate: isHovered, autoRotateSpeed: 2 })] })] })) : null }), _jsx("div", { style: { backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' }, children: file.split('/').pop() })] }));
83
97
  }
84
98
  function TextureSphere({ url, onError }) {
85
99
  const texture = useLoader(TextureLoader, url, undefined, (error) => {
@@ -96,9 +110,9 @@ function ModelCard({ file, onSelect, basePath = "" }) {
96
110
  const { ref, isInView } = useInView();
97
111
  const fullPath = basePath ? `/${basePath}${file}` : file;
98
112
  if (error) {
99
- return (_jsx("div", { ref: ref, className: "aspect-square bg-gray-700 cursor-pointer hover:bg-gray-600 flex items-center justify-center", onClick: () => onSelect(file), children: _jsx("div", { className: "text-red-400 text-xs", children: "\u2717" }) }));
113
+ return (_jsx("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: () => onSelect(file), children: _jsx("div", { style: styles.errorIcon, children: "\u2717" }) }));
100
114
  }
101
- return (_jsxs("div", { ref: ref, className: "aspect-square bg-gray-900 cursor-pointer hover:bg-gray-800 flex flex-col", onClick: () => onSelect(file), children: [_jsx("div", { className: "flex-1 relative", children: isInView ? (_jsxs(View, { className: "w-full h-full", children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 1, 3], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx(Stage, { intensity: 0.5, environment: "city", children: _jsx(ModelPreview, { url: fullPath, onError: () => setError(true) }) }), _jsx(OrbitControls, { enableZoom: false })] })] })) : null }), _jsx("div", { className: "bg-black/60 text-[10px] px-1 truncate text-center", children: file.split('/').pop() })] }));
115
+ return (_jsxs("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#111827', cursor: 'pointer', display: 'flex', flexDirection: 'column' }, onClick: () => onSelect(file), children: [_jsx("div", { style: styles.flexFillRelative, children: isInView ? (_jsxs(View, { style: { width: '100%', height: '100%' }, children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 1, 3], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx(Stage, { intensity: 0.5, environment: "city", children: _jsx(ModelPreview, { url: fullPath, onError: () => setError(true) }) }), _jsx(OrbitControls, { enableZoom: false })] })] })) : null }), _jsx("div", { style: { backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' }, children: file.split('/').pop() })] }));
102
116
  }
103
117
  function ModelPreview({ url, onError }) {
104
118
  const [model, setModel] = useState(null);
@@ -130,7 +144,7 @@ export function SoundListViewer({ files, selected, onSelect, basePath = "" }) {
130
144
  function SoundCard({ file, onSelect, basePath = "" }) {
131
145
  const fileName = file.split('/').pop() || '';
132
146
  const fullPath = basePath ? `/${basePath}${file}` : file;
133
- return (_jsxs("div", { onClick: () => onSelect(file), className: "aspect-square bg-gray-700 cursor-pointer hover:bg-gray-600 flex flex-col items-center justify-center", children: [_jsx("div", { className: "text-2xl", children: "\uD83D\uDD0A" }), _jsx("div", { className: "text-[10px] px-1 mt-1 truncate text-center w-full", children: fileName })] }));
147
+ return (_jsxs("div", { onClick: () => onSelect(file), style: { aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }, children: [_jsx("div", { style: styles.iconLarge, children: "\uD83D\uDD0A" }), _jsx("div", { style: { fontSize: 12, padding: '0 4px', marginTop: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center', width: '100%' }, children: fileName })] }));
134
148
  }
135
149
  // Shared Canvas Component - can be used independently in any viewer
136
150
  export function SharedCanvas() {
@@ -29,7 +29,11 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
29
29
  setSelectedId(null);
30
30
  };
31
31
  const selectedNode = selectedId && prefabData ? findNode(prefabData.root, selectedId) : null;
32
- return _jsxs(_Fragment, { children: [_jsxs("div", { style: inspector.panel, children: [_jsxs("div", { style: base.header, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { children: collapsed ? '◀' : '▼' })] }), !collapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNodeHandler, deleteNode: deleteNodeHandler, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: 8, left: 8, zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId }) })] });
32
+ return _jsxs(_Fragment, { children: [_jsx("style", { children: `.prefab-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
33
+ .prefab-scroll::-webkit-scrollbar-track { background: transparent; }
34
+ .prefab-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
35
+ .prefab-scroll { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.06) transparent; }
36
+ ` }), _jsxs("div", { style: inspector.panel, children: [_jsxs("div", { style: base.header, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { children: collapsed ? '◀' : '▼' })] }), !collapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNodeHandler, deleteNode: deleteNodeHandler, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: 8, left: 8, zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId }) })] });
33
37
  }
34
38
  function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransformMode, basePath }) {
35
39
  var _a;
@@ -42,7 +46,7 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
42
46
  if (!newAvailable.includes(addType))
43
47
  setAddType(newAvailable[0] || "");
44
48
  }, [Object.keys(node.components || {}).join(',')]);
45
- return _jsxs("div", { style: inspector.content, children: [_jsxs("div", { style: base.section, children: [_jsx("div", { style: base.label, children: "Node ID" }), _jsx("input", { style: base.input, value: (_a = node.name) !== null && _a !== void 0 ? _a : "", onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { name: e.target.value }))) })] }), _jsxs("div", { style: base.section, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: [_jsx("div", { style: base.label, children: "Components" }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), base.btnDanger), onClick: deleteNode, children: "Delete Node" })] }), node.components && Object.entries(node.components).map(([key, comp]) => {
49
+ return _jsxs("div", { style: inspector.content, className: "prefab-scroll", children: [_jsxs("div", { style: base.section, children: [_jsx("div", { style: base.label, children: "Node ID" }), _jsx("input", { style: base.input, value: (_a = node.name) !== null && _a !== void 0 ? _a : "", onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { name: e.target.value }))) })] }), _jsxs("div", { style: base.section, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: [_jsx("div", { style: base.label, children: "Components" }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), base.btnDanger), onClick: deleteNode, children: "Delete Node" })] }), node.components && Object.entries(node.components).map(([key, comp]) => {
46
50
  if (!comp)
47
51
  return null;
48
52
  const def = ALL_COMPONENTS[comp.type];
@@ -1,6 +1,35 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- function GeometryComponentEditor({ component, onUpdate }) {
3
- return _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Type" }), _jsxs("select", { style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: component.properties.geometryType, onChange: e => onUpdate({ geometryType: e.target.value }), children: [_jsx("option", { value: "box", children: "Box" }), _jsx("option", { value: "sphere", children: "Sphere" }), _jsx("option", { value: "plane", children: "Plane" })] })] });
2
+ const GEOMETRY_ARGS = {
3
+ box: {
4
+ labels: ["Width", "Height", "Depth"],
5
+ defaults: [1, 1, 1],
6
+ },
7
+ sphere: {
8
+ labels: ["Radius", "Width Segments", "Height Segments"],
9
+ defaults: [1, 32, 16],
10
+ },
11
+ plane: {
12
+ labels: ["Width", "Height"],
13
+ defaults: [1, 1],
14
+ },
15
+ };
16
+ function GeometryComponentEditor({ component, onUpdate, }) {
17
+ const { geometryType, args = [] } = component.properties;
18
+ const schema = GEOMETRY_ARGS[geometryType];
19
+ return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "label", children: "Type" }), _jsxs("select", { className: "select", value: geometryType, onChange: e => {
20
+ const type = e.target.value;
21
+ onUpdate({
22
+ geometryType: type,
23
+ args: GEOMETRY_ARGS[type].defaults,
24
+ });
25
+ }, children: [_jsx("option", { value: "box", children: "Box" }), _jsx("option", { value: "sphere", children: "Sphere" }), _jsx("option", { value: "plane", children: "Plane" })] }), schema.labels.map((label, i) => {
26
+ var _a;
27
+ return (_jsxs("div", { children: [_jsx("label", { className: "label", children: label }), _jsx("input", { type: "number", className: "input", value: (_a = args[i]) !== null && _a !== void 0 ? _a : schema.defaults[i], step: "0.1", onChange: e => {
28
+ const next = [...args];
29
+ next[i] = parseFloat(e.target.value);
30
+ onUpdate({ args: next });
31
+ } })] }, label));
32
+ })] }));
4
33
  }
5
34
  // View for Geometry component
6
35
  function GeometryComponentView({ properties, children }) {
@@ -22,7 +51,8 @@ const GeometryComponent = {
22
51
  Editor: GeometryComponentEditor,
23
52
  View: GeometryComponentView,
24
53
  defaultProperties: {
25
- geometryType: 'box'
54
+ geometryType: 'box',
55
+ args: GEOMETRY_ARGS.box.defaults,
26
56
  }
27
57
  };
28
58
  export default GeometryComponent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -8,6 +8,13 @@ import { loadModel } from "../dragdrop/modelLoader";
8
8
 
9
9
  // view models and textures in manifest, onselect callback
10
10
 
11
+ const styles: Record<string, any> = {
12
+ errorIcon: { color: '#fca5a5', fontSize: 12 }, // text-red-400 text-xs
13
+ flexFillRelative: { flex: 1, position: 'relative' },
14
+ bottomLabel: { backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' },
15
+ iconLarge: { fontSize: 20 }
16
+ };
17
+
11
18
  function getItemsInPath(files: string[], currentPath: string) {
12
19
  // Remove the leading category folder (e.g., /textures/, /models/, /sounds/)
13
20
  const filesWithoutCategory = files.map(file => {
@@ -40,10 +47,18 @@ function FolderTile({ name, onClick }: { name: string; onClick: () => void }) {
40
47
  return (
41
48
  <div
42
49
  onClick={onClick}
43
- className="aspect-square bg-gray-800 cursor-pointer hover:bg-gray-700 flex flex-col items-center justify-center"
50
+ style={{
51
+ aspectRatio: '1 / 1',
52
+ backgroundColor: '#1f2937', /* gray-800 */
53
+ cursor: 'pointer',
54
+ display: 'flex',
55
+ flexDirection: 'column',
56
+ alignItems: 'center',
57
+ justifyContent: 'center'
58
+ }}
44
59
  >
45
- <div className="text-3xl">📁</div>
46
- <div className="text-xs text-center truncate w-full px-1 mt-1">{name}</div>
60
+ <div style={{ fontSize: 24 }}>📁</div>
61
+ <div style={{ fontSize: 10, textAlign: 'center', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', width: '100%', padding: '0 4px', marginTop: 4 }}>{name}</div>
47
62
  </div>
48
63
  );
49
64
  }
@@ -90,11 +105,11 @@ function AssetListViewer({ files, selected, onSelect, renderCard }: AssetListVie
90
105
 
91
106
  if (showCompactView) {
92
107
  return (
93
- <div className="flex gap-1 items-center">
108
+ <div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
94
109
  {renderCard(selected, onSelect)}
95
110
  <button
96
111
  onClick={() => setShowPicker(true)}
97
- className="px-2 py-1 bg-gray-800 hover:bg-gray-700 text-xs"
112
+ style={{ padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 12, cursor: 'pointer', border: 'none' }}
98
113
  >
99
114
  Change
100
115
  </button>
@@ -111,12 +126,12 @@ function AssetListViewer({ files, selected, onSelect, renderCard }: AssetListVie
111
126
  pathParts.pop();
112
127
  setCurrentPath(pathParts.join('/'));
113
128
  }}
114
- className="mb-1 px-2 py-1 bg-gray-800 hover:bg-gray-700 text-xs"
129
+ style={{ marginBottom: 4, padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 12, cursor: 'pointer', border: 'none' }}
115
130
  >
116
131
  ← Back
117
132
  </button>
118
133
  )}
119
- <div className="grid grid-cols-3 gap-1">
134
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4 }}>
120
135
  {folders.map((folder) => (
121
136
  <FolderTile
122
137
  key={folder}
@@ -170,10 +185,10 @@ function TextureCard({ file, onSelect, basePath = "" }: { file: string; onSelect
170
185
  return (
171
186
  <div
172
187
  ref={ref}
173
- className="aspect-square bg-gray-700 cursor-pointer hover:bg-gray-600 flex items-center justify-center"
188
+ style={{ aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
174
189
  onClick={() => onSelect(file)}
175
190
  >
176
- <div className="text-red-400 text-xs">✗</div>
191
+ <div style={styles.errorIcon}>✗</div>
177
192
  </div>
178
193
  );
179
194
  }
@@ -181,14 +196,14 @@ function TextureCard({ file, onSelect, basePath = "" }: { file: string; onSelect
181
196
  return (
182
197
  <div
183
198
  ref={ref}
184
- className="aspect-square bg-gray-800 cursor-pointer hover:bg-gray-700 flex flex-col"
199
+ style={{ aspectRatio: '1 / 1', backgroundColor: '#1f2937', cursor: 'pointer', display: 'flex', flexDirection: 'column' }}
185
200
  onClick={() => onSelect(file)}
186
201
  onMouseEnter={() => setIsHovered(true)}
187
202
  onMouseLeave={() => setIsHovered(false)}
188
203
  >
189
- <div className="flex-1 relative">
204
+ <div style={{ flex: 1, position: 'relative' }}>
190
205
  {isInView ? (
191
- <View className="w-full h-full">
206
+ <View style={{ width: '100%', height: '100%' }}>
192
207
  <PerspectiveCamera makeDefault position={[0, 0, 2.5]} fov={50} />
193
208
  <Suspense fallback={null}>
194
209
  <ambientLight intensity={0.8} />
@@ -204,7 +219,7 @@ function TextureCard({ file, onSelect, basePath = "" }: { file: string; onSelect
204
219
  </View>
205
220
  ) : null}
206
221
  </div>
207
- <div className="bg-black/60 text-[10px] px-1 truncate text-center">
222
+ <div style={{ backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' }}>
208
223
  {file.split('/').pop()}
209
224
  </div>
210
225
  </div>
@@ -256,10 +271,10 @@ function ModelCard({ file, onSelect, basePath = "" }: { file: string; onSelect:
256
271
  return (
257
272
  <div
258
273
  ref={ref}
259
- className="aspect-square bg-gray-700 cursor-pointer hover:bg-gray-600 flex items-center justify-center"
274
+ style={{ aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
260
275
  onClick={() => onSelect(file)}
261
276
  >
262
- <div className="text-red-400 text-xs">✗</div>
277
+ <div style={styles.errorIcon}>✗</div>
263
278
  </div>
264
279
  );
265
280
  }
@@ -267,12 +282,12 @@ function ModelCard({ file, onSelect, basePath = "" }: { file: string; onSelect:
267
282
  return (
268
283
  <div
269
284
  ref={ref}
270
- className="aspect-square bg-gray-900 cursor-pointer hover:bg-gray-800 flex flex-col"
285
+ style={{ aspectRatio: '1 / 1', backgroundColor: '#111827', cursor: 'pointer', display: 'flex', flexDirection: 'column' }}
271
286
  onClick={() => onSelect(file)}
272
287
  >
273
- <div className="flex-1 relative">
288
+ <div style={styles.flexFillRelative}>
274
289
  {isInView ? (
275
- <View className="w-full h-full">
290
+ <View style={{ width: '100%', height: '100%' }}>
276
291
  <PerspectiveCamera makeDefault position={[0, 1, 3]} fov={50} />
277
292
  <Suspense fallback={null}>
278
293
  <Stage intensity={0.5} environment="city">
@@ -283,7 +298,7 @@ function ModelCard({ file, onSelect, basePath = "" }: { file: string; onSelect:
283
298
  </View>
284
299
  ) : null}
285
300
  </div>
286
- <div className="bg-black/60 text-[10px] px-1 truncate text-center">
301
+ <div style={{ backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' }}>
287
302
  {file.split('/').pop()}
288
303
  </div>
289
304
  </div>
@@ -341,10 +356,10 @@ function SoundCard({ file, onSelect, basePath = "" }: { file: string; onSelect:
341
356
  return (
342
357
  <div
343
358
  onClick={() => onSelect(file)}
344
- className="aspect-square bg-gray-700 cursor-pointer hover:bg-gray-600 flex flex-col items-center justify-center"
359
+ style={{ aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}
345
360
  >
346
- <div className="text-2xl">🔊</div>
347
- <div className="text-[10px] px-1 mt-1 truncate text-center w-full">{fileName}</div>
361
+ <div style={styles.iconLarge}>🔊</div>
362
+ <div style={{ fontSize: 12, padding: '0 4px', marginTop: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center', width: '100%' }}>{fileName}</div>
348
363
  </div>
349
364
  );
350
365
  }
@@ -33,6 +33,11 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
33
33
  const selectedNode = selectedId && prefabData ? findNode(prefabData.root, selectedId) : null;
34
34
 
35
35
  return <>
36
+ <style>{`.prefab-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
37
+ .prefab-scroll::-webkit-scrollbar-track { background: transparent; }
38
+ .prefab-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
39
+ .prefab-scroll { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.06) transparent; }
40
+ `}</style>
36
41
  <div style={inspector.panel}>
37
42
  <div style={base.header} onClick={() => setCollapsed(!collapsed)}>
38
43
  <span>Inspector</span>
@@ -46,6 +51,7 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
46
51
  transformMode={transformMode}
47
52
  setTransformMode={setTransformMode}
48
53
  basePath={basePath}
54
+ // add class to make scrollbar gutter transparent via CSS above
49
55
  />
50
56
  )}
51
57
  </div>
@@ -79,7 +85,7 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
79
85
  if (!newAvailable.includes(addType)) setAddType(newAvailable[0] || "");
80
86
  }, [Object.keys(node.components || {}).join(',')]);
81
87
 
82
- return <div style={inspector.content}>
88
+ return <div style={inspector.content} className="prefab-scroll">
83
89
  {/* Node ID */}
84
90
  <div style={base.section}>
85
91
  <div style={base.label}>Node ID</div>
@@ -1,20 +1,75 @@
1
1
  import { Component } from "./ComponentRegistry";
2
2
 
3
- function GeometryComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
4
- return <div>
5
- <label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Type</label>
6
- <select
7
- style={{ width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
8
- value={component.properties.geometryType}
9
- onChange={e => onUpdate({ geometryType: e.target.value })}
10
- >
11
- <option value="box">Box</option>
12
- <option value="sphere">Sphere</option>
13
- <option value="plane">Plane</option>
14
- </select>
15
- </div>;
3
+ const GEOMETRY_ARGS: Record<string, {
4
+ labels: string[];
5
+ defaults: number[];
6
+ }> = {
7
+ box: {
8
+ labels: ["Width", "Height", "Depth"],
9
+ defaults: [1, 1, 1],
10
+ },
11
+ sphere: {
12
+ labels: ["Radius", "Width Segments", "Height Segments"],
13
+ defaults: [1, 32, 16],
14
+ },
15
+ plane: {
16
+ labels: ["Width", "Height"],
17
+ defaults: [1, 1],
18
+ },
19
+ };
20
+
21
+ function GeometryComponentEditor({
22
+ component,
23
+ onUpdate,
24
+ }: {
25
+ component: any;
26
+ onUpdate: (newProps: any) => void;
27
+ }) {
28
+ const { geometryType, args = [] } = component.properties;
29
+ const schema = GEOMETRY_ARGS[geometryType];
30
+
31
+ return (
32
+ <div className="flex flex-col gap-1">
33
+ {/* Geometry Type */}
34
+ <label className="label">Type</label>
35
+ <select
36
+ className="select"
37
+ value={geometryType}
38
+ onChange={e => {
39
+ const type = e.target.value;
40
+ onUpdate({
41
+ geometryType: type,
42
+ args: GEOMETRY_ARGS[type].defaults,
43
+ });
44
+ }}
45
+ >
46
+ <option value="box">Box</option>
47
+ <option value="sphere">Sphere</option>
48
+ <option value="plane">Plane</option>
49
+ </select>
50
+
51
+ {/* Args */}
52
+ {schema.labels.map((label, i) => (
53
+ <div key={label}>
54
+ <label className="label">{label}</label>
55
+ <input
56
+ type="number"
57
+ className="input"
58
+ value={args[i] ?? schema.defaults[i]}
59
+ step="0.1"
60
+ onChange={e => {
61
+ const next = [...args];
62
+ next[i] = parseFloat(e.target.value);
63
+ onUpdate({ args: next });
64
+ }}
65
+ />
66
+ </div>
67
+ ))}
68
+ </div>
69
+ );
16
70
  }
17
71
 
72
+
18
73
  // View for Geometry component
19
74
  function GeometryComponentView({ properties, children }: { properties: any, children?: React.ReactNode }) {
20
75
  const { geometryType, args = [] } = properties;
@@ -36,7 +91,8 @@ const GeometryComponent: Component = {
36
91
  Editor: GeometryComponentEditor,
37
92
  View: GeometryComponentView,
38
93
  defaultProperties: {
39
- geometryType: 'box'
94
+ geometryType: 'box',
95
+ args: GEOMETRY_ARGS.box.defaults,
40
96
  }
41
97
  };
42
98