react-three-game 0.0.15 → 0.0.16

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
@@ -17,45 +17,26 @@ npm i react-three-game @react-three/fiber three
17
17
  Scenes are JSON prefabs. Components are registered modules. Hierarchy is declarative.
18
18
 
19
19
  ```jsx
20
- <PrefabRoot data={{
21
- root: {
22
- id: "cube",
23
- components: {
24
- transform: { type: "Transform", properties: { position: [0, 1, 0] } },
25
- geometry: { type: "Geometry", properties: { geometryType: "box" } },
26
- material: { type: "Material", properties: { color: "green" } },
27
- physics: { type: "Physics", properties: { type: "dynamic" } }
28
- }
29
- }
30
- }} />
31
- ```
32
-
33
- ## Tailwind CSS Support
34
-
35
- This library uses Tailwind CSS for styling its editor components. To ensure styles are correctly applied in your application, you need to configure Tailwind to scan the library's source files.
36
-
37
- ### Tailwind v4
38
-
39
- Add the library path to your CSS entry point using the `@source` directive:
40
-
41
- ```css
42
- @import "tailwindcss";
43
- @source "../../node_modules/react-three-game/dist/**/*.{js,ts,jsx,tsx}";
20
+ <PrefabRoot data={{
21
+ root: {
22
+ id: "cube",
23
+ components: {
24
+ transform: { type: "Transform", properties: { position: [0, 1, 0] } },
25
+ geometry: { type: "Geometry", properties: { geometryType: "box" } },
26
+ material: { type: "Material", properties: { color: "green" } },
27
+ physics: { type: "Physics", properties: { type: "dynamic" } }
28
+ }
29
+ }
30
+ }} />
44
31
  ```
45
32
 
46
- ### Tailwind v3
33
+ ## Styling
47
34
 
48
- Add the library path to your `tailwind.config.js`:
35
+ The prefab editor UI ships with **inline styles** (no Tailwind / CSS framework required). That means you can install and render it without any additional build-time CSS configuration.
49
36
 
50
- ```js
51
- module.exports = {
52
- content: [
53
- // ...
54
- "./node_modules/react-three-game/dist/**/*.{js,ts,jsx,tsx}",
55
- ],
56
- // ...
57
- }
58
- ```
37
+ If you want to fully restyle the editor, you can:
38
+ - Wrap `PrefabEditor` in your own layout and override positioning.
39
+ - Fork/compose the editor UI components (they’re plain React components).
59
40
 
60
41
  ## Quick Start
61
42
 
@@ -64,39 +45,44 @@ npm install react-three-game @react-three/fiber @react-three/rapier three
64
45
  ```
65
46
 
66
47
  ```jsx
67
- import { GameCanvas, PrefabRoot, ground } from 'react-three-game';
48
+ import { Physics } from '@react-three/rapier';
49
+ import { GameCanvas, PrefabRoot } from 'react-three-game';
68
50
 
69
51
  export default function App() {
70
52
  return (
71
53
  <GameCanvas>
72
- <ambientLight intensity={0.5} />
73
- <PrefabRoot data={{
74
- id: "scene",
75
- root: {
76
- id: "root",
77
- components: {
78
- transform: { type: "Transform", properties: { position: [0, 0, 0] } }
79
- },
80
- children: [
81
- ground({
82
- id: "floor",
83
- position: [0, -1, 0],
84
- size: 50,
85
- texture: "/textures/GreyboxTextures/greybox_light_grid.png",
86
- repeatCount: [25, 25]
87
- }),
88
- {
89
- id: "player",
90
- components: {
91
- transform: { type: "Transform", properties: { position: [0, 2, 0] } },
92
- geometry: { type: "Geometry", properties: { geometryType: "sphere" } },
93
- material: { type: "Material", properties: { color: "#ff6b6b" } },
94
- physics: { type: "Physics", properties: { type: "dynamic" } }
54
+ <Physics>
55
+ <ambientLight intensity={0.8} />
56
+ <PrefabRoot
57
+ data={{
58
+ id: "scene",
59
+ name: "scene",
60
+ root: {
61
+ id: "root",
62
+ children: [
63
+ {
64
+ id: "ground",
65
+ components: {
66
+ transform: { type: "Transform", properties: { position: [0, 0, 0], rotation: [-1.57, 0, 0] } },
67
+ geometry: { type: "Geometry", properties: { geometryType: "plane", args: [50, 50] } },
68
+ material: { type: "Material", properties: { color: "green" } },
69
+ physics: { type: "Physics", properties: { type: "fixed" } }
70
+ }
71
+ },
72
+ {
73
+ id: "player",
74
+ components: {
75
+ transform: { type: "Transform", properties: { position: [0, 2, 0] } },
76
+ geometry: { type: "Geometry", properties: { geometryType: "sphere" } },
77
+ material: { type: "Material", properties: { color: "#ff6b6b" } },
78
+ physics: { type: "Physics", properties: { type: "dynamic" } }
79
+ }
80
+ }
81
+ ]
95
82
  }
96
- }
97
- ]
98
- }
99
- }} />
83
+ }}
84
+ />
85
+ </Physics>
100
86
  </GameCanvas>
101
87
  );
102
88
  }
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  // DragDropLoader.tsx
3
4
  import { useEffect } from "react";
@@ -6,6 +6,94 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
6
6
  const [draggedId, setDraggedId] = useState(null);
7
7
  const [collapsedIds, setCollapsedIds] = useState(new Set());
8
8
  const [isTreeCollapsed, setIsTreeCollapsed] = useState(false);
9
+ const styles = {
10
+ panel: {
11
+ background: "rgba(0,0,0,0.55)",
12
+ color: "rgba(255,255,255,0.9)",
13
+ border: "1px solid rgba(255,255,255,0.12)",
14
+ borderRadius: 6,
15
+ overflow: "hidden",
16
+ maxHeight: "85vh",
17
+ display: "flex",
18
+ flexDirection: "column",
19
+ backdropFilter: "blur(6px)",
20
+ WebkitBackdropFilter: "blur(6px)",
21
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
22
+ fontSize: 11,
23
+ lineHeight: 1.2,
24
+ userSelect: "none",
25
+ WebkitUserSelect: "none",
26
+ },
27
+ panelHeader: {
28
+ padding: "4px 6px",
29
+ borderBottom: "1px solid rgba(255,255,255,0.10)",
30
+ display: "flex",
31
+ gap: 8,
32
+ alignItems: "center",
33
+ justifyContent: "space-between",
34
+ cursor: "pointer",
35
+ background: "rgba(255,255,255,0.05)",
36
+ textTransform: "uppercase",
37
+ letterSpacing: "0.08em",
38
+ fontSize: 10,
39
+ color: "rgba(255,255,255,0.7)",
40
+ },
41
+ scroll: {
42
+ overflowY: "auto",
43
+ },
44
+ row: {
45
+ display: "flex",
46
+ alignItems: "center",
47
+ padding: "2px 6px",
48
+ borderBottom: "1px solid rgba(255,255,255,0.07)",
49
+ cursor: "pointer",
50
+ whiteSpace: "nowrap",
51
+ },
52
+ rowSelected: {
53
+ background: "rgba(255,255,255,0.10)",
54
+ },
55
+ chevron: {
56
+ width: 12,
57
+ textAlign: "center",
58
+ opacity: 0.55,
59
+ fontSize: 10,
60
+ marginRight: 4,
61
+ cursor: "pointer",
62
+ },
63
+ idText: {
64
+ fontSize: 11,
65
+ overflow: "hidden",
66
+ textOverflow: "ellipsis",
67
+ },
68
+ contextMenu: {
69
+ position: "fixed",
70
+ zIndex: 50,
71
+ minWidth: 120,
72
+ background: "rgba(0,0,0,0.82)",
73
+ border: "1px solid rgba(255,255,255,0.16)",
74
+ borderRadius: 6,
75
+ overflow: "hidden",
76
+ boxShadow: "0 12px 32px rgba(0,0,0,0.45)",
77
+ backdropFilter: "blur(6px)",
78
+ WebkitBackdropFilter: "blur(6px)",
79
+ },
80
+ menuItem: {
81
+ width: "100%",
82
+ textAlign: "left",
83
+ padding: "6px 8px",
84
+ background: "transparent",
85
+ border: "none",
86
+ color: "rgba(255,255,255,0.9)",
87
+ font: "inherit",
88
+ cursor: "pointer",
89
+ },
90
+ menuItemDanger: {
91
+ color: "rgba(255,120,120,0.95)",
92
+ },
93
+ menuDivider: {
94
+ borderTop: "1px solid rgba(255,255,255,0.10)",
95
+ }
96
+ };
9
97
  if (!prefabData || !setPrefabData)
10
98
  return null;
11
99
  const handleContextMenu = (e, nodeId) => {
@@ -132,9 +220,35 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
132
220
  const isSelected = node.id === selectedId;
133
221
  const isCollapsed = collapsedIds.has(node.id);
134
222
  const hasChildren = node.children && node.children.length > 0;
135
- return (_jsxs("div", { className: "select-none", children: [_jsxs("div", { className: `flex items-center py-0.5 px-1 cursor-pointer border-b border-cyan-500/10 ${isSelected ? 'bg-cyan-500/30 hover:bg-cyan-500/40 border-cyan-400/30' : 'hover:bg-cyan-500/10'}`, style: { paddingLeft: `${depth * 8 + 4}px` }, onClick: (e) => { e.stopPropagation(); setSelectedId(node.id); }, onContextMenu: (e) => handleContextMenu(e, node.id), draggable: node.id !== prefabData.root.id, onDragStart: (e) => handleDragStart(e, node.id), onDragOver: (e) => handleDragOver(e, node.id), onDrop: (e) => handleDrop(e, node.id), children: [_jsx("span", { className: `mr-0.5 w-3 text-center text-cyan-400/50 hover:text-cyan-400 cursor-pointer text-[8px] ${hasChildren ? '' : 'invisible'}`, onClick: (e) => hasChildren && toggleCollapse(e, node.id), children: isCollapsed ? '▶' : '▼' }), _jsx("span", { className: "text-[10px] truncate font-mono text-cyan-300", children: node.id })] }), !isCollapsed && node.children && (_jsx("div", { children: node.children.map(child => renderNode(child, depth + 1)) }))] }, node.id));
223
+ return (_jsxs("div", { children: [_jsxs("div", { style: Object.assign(Object.assign(Object.assign({}, styles.row), (isSelected ? styles.rowSelected : null)), { paddingLeft: `${depth * 10 + 6}px` }), onClick: (e) => { e.stopPropagation(); setSelectedId(node.id); }, onContextMenu: (e) => handleContextMenu(e, node.id), draggable: node.id !== prefabData.root.id, onDragStart: (e) => handleDragStart(e, node.id), onDragOver: (e) => handleDragOver(e, node.id), onDrop: (e) => handleDrop(e, node.id), onPointerEnter: (e) => {
224
+ if (!isSelected)
225
+ e.currentTarget.style.background = "rgba(255,255,255,0.06)";
226
+ }, onPointerLeave: (e) => {
227
+ if (!isSelected)
228
+ e.currentTarget.style.background = "transparent";
229
+ }, children: [_jsx("span", { style: Object.assign(Object.assign({}, styles.chevron), { visibility: hasChildren ? 'visible' : 'hidden' }), onClick: (e) => hasChildren && toggleCollapse(e, node.id), onPointerEnter: (e) => {
230
+ e.currentTarget.style.opacity = "0.9";
231
+ }, onPointerLeave: (e) => {
232
+ e.currentTarget.style.opacity = "0.55";
233
+ }, children: isCollapsed ? '▶' : '▼' }), _jsx("span", { style: styles.idText, children: node.id })] }), !isCollapsed && node.children && (_jsx("div", { children: node.children.map(child => renderNode(child, depth + 1)) }))] }, node.id));
136
234
  };
137
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "bg-black/70 backdrop-blur-sm text-white border border-cyan-500/30 max-h-[85vh] overflow-y-auto flex flex-col", style: { width: isTreeCollapsed ? 'auto' : '14rem' }, onClick: closeContextMenu, children: [_jsxs("div", { className: "px-1.5 py-1 font-mono text-[10px] bg-cyan-500/10 border-b border-cyan-500/30 sticky top-0 uppercase tracking-wider text-cyan-400/80 cursor-pointer hover:bg-cyan-500/20 flex items-center justify-between", onClick: (e) => { e.stopPropagation(); setIsTreeCollapsed(!isTreeCollapsed); }, children: [_jsx("span", { children: "Prefab Graph" }), _jsx("span", { className: "text-[8px]", children: isTreeCollapsed ? '▶' : '◀' })] }), !isTreeCollapsed && (_jsx("div", { className: "flex-1 py-0.5", children: renderNode(prefabData.root) }))] }), contextMenu && (_jsxs("div", { className: "fixed bg-black/90 backdrop-blur-sm border border-cyan-500/40 z-50 min-w-[100px]", style: { top: contextMenu.y, left: contextMenu.x }, onClick: (e) => e.stopPropagation(), onPointerLeave: closeContextMenu, children: [_jsx("button", { className: "w-full text-left px-2 py-1 hover:bg-cyan-500/20 text-[10px] text-cyan-300 font-mono border-b border-cyan-500/20", onClick: () => handleAddChild(contextMenu.nodeId), children: "Add Child" }), contextMenu.nodeId !== prefabData.root.id && (_jsxs(_Fragment, { children: [_jsx("button", { className: "w-full text-left px-2 py-1 hover:bg-cyan-500/20 text-[10px] text-cyan-300 font-mono border-b border-cyan-500/20", onClick: () => handleDuplicate(contextMenu.nodeId), children: "Duplicate" }), _jsx("button", { className: "w-full text-left px-2 py-1 hover:bg-red-500/20 text-[10px] text-red-400 font-mono", onClick: () => handleDelete(contextMenu.nodeId), children: "Delete" })] }))] }))] }));
235
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { style: Object.assign(Object.assign({}, styles.panel), { width: isTreeCollapsed ? 'auto' : '14rem' }), onClick: closeContextMenu, children: [_jsxs("div", { style: styles.panelHeader, onClick: (e) => { e.stopPropagation(); setIsTreeCollapsed(!isTreeCollapsed); }, onPointerEnter: (e) => {
236
+ e.currentTarget.style.background = "rgba(255,255,255,0.08)";
237
+ }, onPointerLeave: (e) => {
238
+ e.currentTarget.style.background = "rgba(255,255,255,0.05)";
239
+ }, children: [_jsx("span", { children: "Prefab Graph" }), _jsx("span", { style: { fontSize: 10, opacity: 0.8 }, children: isTreeCollapsed ? '▶' : '◀' })] }), !isTreeCollapsed && (_jsx("div", { style: Object.assign(Object.assign({}, styles.scroll), { padding: 2 }), children: renderNode(prefabData.root) }))] }), contextMenu && (_jsxs("div", { style: Object.assign(Object.assign({}, styles.contextMenu), { top: contextMenu.y, left: contextMenu.x }), onClick: (e) => e.stopPropagation(), onPointerLeave: closeContextMenu, children: [_jsx("button", { style: Object.assign(Object.assign({}, styles.menuItem), styles.menuDivider), onClick: () => handleAddChild(contextMenu.nodeId), onPointerEnter: (e) => {
240
+ e.currentTarget.style.background = "rgba(255,255,255,0.08)";
241
+ }, onPointerLeave: (e) => {
242
+ e.currentTarget.style.background = "transparent";
243
+ }, children: "Add Child" }), contextMenu.nodeId !== prefabData.root.id && (_jsxs(_Fragment, { children: [_jsx("button", { style: Object.assign(Object.assign({}, styles.menuItem), styles.menuDivider), onClick: () => handleDuplicate(contextMenu.nodeId), onPointerEnter: (e) => {
244
+ e.currentTarget.style.background = "rgba(255,255,255,0.08)";
245
+ }, onPointerLeave: (e) => {
246
+ e.currentTarget.style.background = "transparent";
247
+ }, children: "Duplicate" }), _jsx("button", { style: Object.assign(Object.assign({}, styles.menuItem), styles.menuItemDanger), onClick: () => handleDelete(contextMenu.nodeId), onPointerEnter: (e) => {
248
+ e.currentTarget.style.background = "rgba(255,255,255,0.08)";
249
+ }, onPointerLeave: (e) => {
250
+ e.currentTarget.style.background = "transparent";
251
+ }, children: "Delete" })] }))] }))] }));
138
252
  }
139
253
  // --- Helpers ---
140
254
  function findNode(root, id) {
@@ -4,6 +4,46 @@ import EditorTree from './EditorTree';
4
4
  import { getAllComponents } from './components/ComponentRegistry';
5
5
  function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode, basePath }) {
6
6
  const [isInspectorCollapsed, setIsInspectorCollapsed] = useState(false);
7
+ const ui = {
8
+ panel: {
9
+ position: 'absolute',
10
+ top: 8,
11
+ right: 8,
12
+ zIndex: 20,
13
+ width: 260,
14
+ background: 'rgba(0,0,0,0.55)',
15
+ color: 'rgba(255,255,255,0.9)',
16
+ border: '1px solid rgba(255,255,255,0.12)',
17
+ borderRadius: 6,
18
+ overflow: 'hidden',
19
+ backdropFilter: 'blur(6px)',
20
+ WebkitBackdropFilter: 'blur(6px)',
21
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
22
+ fontSize: 11,
23
+ lineHeight: 1.2,
24
+ },
25
+ header: {
26
+ padding: '4px 6px',
27
+ display: 'flex',
28
+ alignItems: 'center',
29
+ justifyContent: 'space-between',
30
+ cursor: 'pointer',
31
+ background: 'rgba(255,255,255,0.05)',
32
+ borderBottom: '1px solid rgba(255,255,255,0.10)',
33
+ textTransform: 'uppercase',
34
+ letterSpacing: '0.08em',
35
+ fontSize: 10,
36
+ color: 'rgba(255,255,255,0.7)',
37
+ userSelect: 'none',
38
+ WebkitUserSelect: 'none',
39
+ },
40
+ left: {
41
+ position: 'absolute',
42
+ top: 8,
43
+ left: 8,
44
+ zIndex: 20,
45
+ },
46
+ };
7
47
  const updateNode = (updater) => {
8
48
  if (!prefabData || !setPrefabData || !selectedId)
9
49
  return;
@@ -24,12 +64,112 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
24
64
  };
25
65
  const selectedNode = selectedId && prefabData ? findNode(prefabData.root, selectedId) : null;
26
66
  // if (!selectedNode) return null;
27
- return _jsxs(_Fragment, { children: [_jsxs("div", { style: { position: 'absolute', top: "0.5rem", right: "0.5rem", zIndex: 20, backgroundColor: "rgba(0,0,0,0.7)", backdropFilter: "blur(4px)", color: "white", border: "1px solid rgba(0,255,255,0.3)" }, children: [_jsxs("div", { className: "px-1.5 py-1 font-mono text-[10px] bg-cyan-500/10 border-b border-cyan-500/30 sticky top-0 uppercase tracking-wider text-cyan-400/80 cursor-pointer hover:bg-cyan-500/20 flex items-center justify-between", onClick: () => setIsInspectorCollapsed(!isInspectorCollapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { className: "text-[8px]", children: isInspectorCollapsed ? '◀' : '▶' })] }), !isInspectorCollapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNode, deleteNode: deleteNode, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: "0.5rem", left: "0.5rem", zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId }) })] });
67
+ return _jsxs(_Fragment, { children: [_jsxs("div", { style: ui.panel, children: [_jsxs("div", { style: ui.header, onClick: () => setIsInspectorCollapsed(!isInspectorCollapsed), onPointerEnter: (e) => {
68
+ e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
69
+ }, onPointerLeave: (e) => {
70
+ e.currentTarget.style.background = 'rgba(255,255,255,0.05)';
71
+ }, children: [_jsx("span", { children: "Inspector" }), _jsx("span", { style: { fontSize: 10, opacity: 0.8 }, children: isInspectorCollapsed ? '◀' : '▶' })] }), !isInspectorCollapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNode, deleteNode: deleteNode, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: ui.left, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId }) })] });
28
72
  }
29
73
  function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransformMode, basePath }) {
30
74
  const ALL_COMPONENTS = getAllComponents();
31
75
  const allComponentKeys = Object.keys(ALL_COMPONENTS);
32
76
  const [addComponentType, setAddComponentType] = useState(allComponentKeys[0]);
77
+ const s = {
78
+ root: {
79
+ display: 'flex',
80
+ flexDirection: 'column',
81
+ gap: 6,
82
+ padding: 6,
83
+ maxHeight: '80vh',
84
+ overflowY: 'auto',
85
+ },
86
+ section: {
87
+ paddingBottom: 6,
88
+ borderBottom: '1px solid rgba(255,255,255,0.10)',
89
+ },
90
+ label: {
91
+ display: 'block',
92
+ fontSize: 10,
93
+ opacity: 0.7,
94
+ textTransform: 'uppercase',
95
+ letterSpacing: '0.08em',
96
+ marginBottom: 4,
97
+ },
98
+ input: {
99
+ width: '100%',
100
+ background: 'rgba(255,255,255,0.06)',
101
+ border: '1px solid rgba(255,255,255,0.14)',
102
+ borderRadius: 4,
103
+ padding: '4px 6px',
104
+ color: 'rgba(255,255,255,0.92)',
105
+ font: 'inherit',
106
+ outline: 'none',
107
+ },
108
+ row: {
109
+ display: 'flex',
110
+ alignItems: 'center',
111
+ justifyContent: 'space-between',
112
+ gap: 8,
113
+ },
114
+ button: {
115
+ padding: '2px 6px',
116
+ background: 'transparent',
117
+ color: 'rgba(255,255,255,0.9)',
118
+ border: '1px solid rgba(255,255,255,0.14)',
119
+ borderRadius: 4,
120
+ cursor: 'pointer',
121
+ font: 'inherit',
122
+ },
123
+ buttonActive: {
124
+ background: 'rgba(255,255,255,0.10)',
125
+ },
126
+ smallDanger: {
127
+ background: 'transparent',
128
+ border: 'none',
129
+ cursor: 'pointer',
130
+ color: 'rgba(255,120,120,0.95)',
131
+ font: 'inherit',
132
+ padding: '2px 4px',
133
+ },
134
+ componentHeader: {
135
+ display: 'flex',
136
+ alignItems: 'center',
137
+ justifyContent: 'space-between',
138
+ padding: '4px 0',
139
+ borderBottom: '1px solid rgba(255,255,255,0.08)',
140
+ marginBottom: 4,
141
+ },
142
+ componentTitle: {
143
+ fontSize: 10,
144
+ textTransform: 'uppercase',
145
+ letterSpacing: '0.08em',
146
+ opacity: 0.8,
147
+ },
148
+ select: {
149
+ flex: 1,
150
+ background: 'rgba(255,255,255,0.06)',
151
+ border: '1px solid rgba(255,255,255,0.14)',
152
+ borderRadius: 4,
153
+ padding: '4px 6px',
154
+ color: 'rgba(255,255,255,0.92)',
155
+ font: 'inherit',
156
+ outline: 'none',
157
+ },
158
+ addButton: {
159
+ width: 28,
160
+ padding: '4px 0',
161
+ background: 'rgba(255,255,255,0.08)',
162
+ color: 'rgba(255,255,255,0.92)',
163
+ border: '1px solid rgba(255,255,255,0.14)',
164
+ borderRadius: 4,
165
+ cursor: 'pointer',
166
+ font: 'inherit',
167
+ },
168
+ disabled: {
169
+ opacity: 0.35,
170
+ cursor: 'not-allowed',
171
+ },
172
+ };
33
173
  const componentKeys = Object.keys(node.components || {}).join(',');
34
174
  useEffect(() => {
35
175
  // Components stored on nodes use lowercase keys (e.g. 'geometry'),
@@ -39,19 +179,25 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
39
179
  setAddComponentType(available[0] || "");
40
180
  }
41
181
  }, [componentKeys, addComponentType, node.components, allComponentKeys]);
42
- return _jsxs("div", { className: "flex flex-col gap-1 text-[11px] max-w-[250px] max-h-[80vh] overflow-y-auto", children: [_jsx("div", { className: "border-b border-cyan-500/20 pb-1 px-1.5 pt-1", children: _jsx("input", { className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[11px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: node.id, onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { id: e.target.value }))) }) }), _jsxs("div", { className: "flex justify-between items-center px-1.5 py-0.5 border-b border-cyan-500/20", children: [_jsx("label", { className: "text-[10px] font-mono text-cyan-400/80 uppercase tracking-wider", children: "Components" }), _jsx("button", { onClick: deleteNode, className: "text-[10px] text-red-400/80 hover:text-red-400", children: "\u2715" })] }), _jsxs("div", { className: "px-1.5 py-1 border-b border-cyan-500/20", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Mode" }), _jsx("div", { className: "flex gap-0.5", children: ["translate", "rotate", "scale"].map(mode => (_jsx("button", { onClick: () => setTransformMode(mode), className: `flex-1 px-1 py-0.5 text-[10px] font-mono border ${transformMode === mode ? 'bg-cyan-500/30 border-cyan-400/50 text-cyan-200' : 'bg-black/30 border-cyan-500/20 text-cyan-400/60 hover:border-cyan-400/30'}`, children: mode[0].toUpperCase() }, mode))) })] }), node.components && Object.entries(node.components).map(([key, comp]) => {
182
+ return _jsxs("div", { style: s.root, children: [_jsx("div", { style: s.section, children: _jsx("input", { style: s.input, value: node.id, onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { id: e.target.value }))) }) }), _jsxs("div", { style: Object.assign(Object.assign(Object.assign({}, s.row), s.section), { paddingBottom: 6 }), children: [_jsx("label", { style: Object.assign(Object.assign({}, s.label), { marginBottom: 0 }), children: "Components" }), _jsx("button", { onClick: deleteNode, style: s.smallDanger, title: "Delete node", children: "\u2715" })] }), _jsxs("div", { style: s.section, children: [_jsx("label", { style: s.label, children: "Mode" }), _jsx("div", { style: { display: 'flex', gap: 6 }, children: ["translate", "rotate", "scale"].map(mode => (_jsx("button", { onClick: () => setTransformMode(mode), style: Object.assign(Object.assign(Object.assign({}, s.button), { flex: 1 }), (transformMode === mode ? s.buttonActive : null)), onPointerEnter: (e) => {
183
+ if (transformMode !== mode)
184
+ e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
185
+ }, onPointerLeave: (e) => {
186
+ if (transformMode !== mode)
187
+ e.currentTarget.style.background = 'transparent';
188
+ }, children: mode[0].toUpperCase() }, mode))) })] }), node.components && Object.entries(node.components).map(([key, comp]) => {
43
189
  if (!comp)
44
190
  return null;
45
191
  const componentDef = ALL_COMPONENTS[comp.type];
46
192
  if (!componentDef)
47
- return _jsxs("div", { className: "px-1 py-0.5 text-red-400 text-[10px]", children: ["Unknown component type: ", comp.type, _jsx("textarea", { defaultValue: JSON.stringify(comp) })] }, key);
193
+ return _jsxs("div", { style: { padding: '4px 0', color: 'rgba(255,120,120,0.95)', fontSize: 11 }, children: ["Unknown component type: ", comp.type, _jsx("textarea", { defaultValue: JSON.stringify(comp) })] }, key);
48
194
  const EditorComp = componentDef.Editor;
49
- return (_jsxs("div", { className: 'px-1', children: [_jsxs("div", { className: "flex justify-between items-center py-0.5 border-b border-cyan-500/20 bg-cyan-500/5", children: [_jsx("span", { className: "font-mono text-[10px] text-cyan-300 uppercase", children: key }), _jsx("button", { onClick: () => updateNode(n => {
195
+ return (_jsxs("div", { style: { padding: '0 2px' }, children: [_jsxs("div", { style: s.componentHeader, children: [_jsx("span", { style: s.componentTitle, children: key }), _jsx("button", { onClick: () => updateNode(n => {
50
196
  const components = Object.assign({}, n.components);
51
197
  delete components[key];
52
198
  return Object.assign(Object.assign({}, n), { components });
53
- }), className: "text-[9px] text-red-400/60 hover:text-red-400", children: "\u2715" })] }), EditorComp ? (_jsx(EditorComp, { component: comp, onUpdate: (newProps) => updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: Object.assign(Object.assign({}, comp), { properties: Object.assign(Object.assign({}, comp.properties), newProps) }) }) }))), basePath: basePath })) : null] }, key));
54
- }), _jsxs("div", { className: "px-1.5 py-1 border-t border-cyan-500/20", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Add Component" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("select", { className: "bg-black/40 border border-cyan-500/30 px-1 py-0.5 flex-1 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: addComponentType, onChange: e => setAddComponentType(e.target.value), children: allComponentKeys.filter(k => { var _a; return !((_a = node.components) === null || _a === void 0 ? void 0 : _a[k.toLowerCase()]); }).map(k => (_jsx("option", { value: k, children: k }, k))) }), _jsx("button", { className: "bg-cyan-500/20 hover:bg-cyan-500/30 border border-cyan-500/30 px-2 py-0.5 text-[10px] text-cyan-300 font-mono disabled:opacity-30", disabled: !addComponentType, onClick: () => {
199
+ }), style: s.smallDanger, title: "Remove component", children: "\u2715" })] }), EditorComp ? (_jsx(EditorComp, { component: comp, onUpdate: (newProps) => updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: Object.assign(Object.assign({}, comp), { properties: Object.assign(Object.assign({}, comp.properties), newProps) }) }) }))), basePath: basePath })) : null] }, key));
200
+ }), _jsxs("div", { style: Object.assign(Object.assign({}, s.section), { borderBottom: 'none', paddingBottom: 0 }), children: [_jsx("label", { style: s.label, children: "Add Component" }), _jsxs("div", { style: { display: 'flex', gap: 6 }, children: [_jsx("select", { style: s.select, value: addComponentType, onChange: e => setAddComponentType(e.target.value), children: allComponentKeys.filter(k => { var _a; return !((_a = node.components) === null || _a === void 0 ? void 0 : _a[k.toLowerCase()]); }).map(k => (_jsx("option", { value: k, children: k }, k))) }), _jsx("button", { style: Object.assign(Object.assign({}, s.addButton), (!addComponentType ? s.disabled : null)), disabled: !addComponentType, onClick: () => {
55
201
  var _a;
56
202
  if (!addComponentType)
57
203
  return;
@@ -60,6 +206,14 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
60
206
  const key = addComponentType.toLowerCase();
61
207
  updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: { type: def.name, properties: def.defaultProperties } }) })));
62
208
  }
209
+ }, onPointerEnter: (e) => {
210
+ if (!addComponentType)
211
+ return;
212
+ e.currentTarget.style.background = 'rgba(255,255,255,0.12)';
213
+ }, onPointerLeave: (e) => {
214
+ if (!addComponentType)
215
+ return;
216
+ e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
63
217
  }, children: "+" })] })] })] });
64
218
  }
65
219
  function findNode(root, id) {
@@ -50,11 +50,65 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }) =>
50
50
  };
51
51
  return _jsxs(_Fragment, { children: [_jsx(GameCanvas, { children: _jsxs(Physics, { paused: editMode, children: [_jsx("ambientLight", { intensity: 1.5 }), _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { data: loadedPrefab, ref: prefabRef,
52
52
  // props for edit mode
53
- editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }), children] }) }), _jsxs("div", { style: { position: "absolute", top: "0.5rem", left: "50%", transform: "translateX(-50%)" }, className: "bg-black/70 backdrop-blur-sm border border-cyan-500/30 px-2 py-1 flex items-center gap-1", children: [_jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }), _jsx("span", { className: "text-cyan-500/30 text-[10px]", children: "|" }), _jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => __awaiter(void 0, void 0, void 0, function* () {
53
+ editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }), children] }) }), _jsxs("div", { style: {
54
+ position: "absolute",
55
+ top: 8,
56
+ left: "50%",
57
+ transform: "translateX(-50%)",
58
+ display: "flex",
59
+ alignItems: "center",
60
+ gap: 6,
61
+ padding: "2px 4px",
62
+ background: "rgba(0,0,0,0.55)",
63
+ border: "1px solid rgba(255,255,255,0.12)",
64
+ borderRadius: 4,
65
+ color: "rgba(255,255,255,0.9)",
66
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
67
+ fontSize: 11,
68
+ lineHeight: 1,
69
+ WebkitUserSelect: "none",
70
+ userSelect: "none",
71
+ }, children: [_jsx("button", { style: {
72
+ padding: "2px 6px",
73
+ font: "inherit",
74
+ background: "transparent",
75
+ color: "inherit",
76
+ border: "1px solid rgba(255,255,255,0.18)",
77
+ borderRadius: 3,
78
+ cursor: "pointer",
79
+ }, onClick: () => setEditMode(!editMode), onPointerEnter: (e) => {
80
+ e.currentTarget.style.background = "rgba(255,255,255,0.08)";
81
+ }, onPointerLeave: (e) => {
82
+ e.currentTarget.style.background = "transparent";
83
+ }, children: editMode ? "▶" : "⏸" }), _jsx("span", { style: { opacity: 0.35 }, children: "|" }), _jsx("button", { style: {
84
+ padding: "2px 6px",
85
+ font: "inherit",
86
+ background: "transparent",
87
+ color: "inherit",
88
+ border: "1px solid rgba(255,255,255,0.18)",
89
+ borderRadius: 3,
90
+ cursor: "pointer",
91
+ }, onClick: () => __awaiter(void 0, void 0, void 0, function* () {
54
92
  const prefab = yield loadJson();
55
93
  if (prefab)
56
94
  setLoadedPrefab(prefab);
57
- }), children: "\uD83D\uDCE5" }), _jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => saveJson(loadedPrefab, "prefab"), children: "\uD83D\uDCBE" })] }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath })] });
95
+ }), onPointerEnter: (e) => {
96
+ e.currentTarget.style.background = "rgba(255,255,255,0.08)";
97
+ }, onPointerLeave: (e) => {
98
+ e.currentTarget.style.background = "transparent";
99
+ }, children: "\uD83D\uDCE5" }), _jsx("button", { style: {
100
+ padding: "2px 6px",
101
+ font: "inherit",
102
+ background: "transparent",
103
+ color: "inherit",
104
+ border: "1px solid rgba(255,255,255,0.18)",
105
+ borderRadius: 3,
106
+ cursor: "pointer",
107
+ }, onClick: () => saveJson(loadedPrefab, "prefab"), onPointerEnter: (e) => {
108
+ e.currentTarget.style.background = "rgba(255,255,255,0.08)";
109
+ }, onPointerLeave: (e) => {
110
+ e.currentTarget.style.background = "transparent";
111
+ }, children: "\uD83D\uDCBE" })] }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath })] });
58
112
  };
59
113
  const saveJson = (data, filename) => {
60
114
  const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
@@ -1,6 +1,6 @@
1
1
  export interface Prefab {
2
- id: string;
3
- name: string;
2
+ id?: string;
3
+ name?: string;
4
4
  description?: string;
5
5
  author?: string;
6
6
  version?: string;
@@ -1 +1,2 @@
1
+ // import { ThreeElements } from "@react-three/fiber"
1
2
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  // DragDropLoader.tsx
2
3
  import { useEffect, ChangeEvent } from "react";
3
4
  import { DRACOLoader, FBXLoader, GLTFLoader } from "three/examples/jsm/Addons.js";
@@ -15,6 +15,95 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
15
15
  const [collapsedIds, setCollapsedIds] = useState<Set<string>>(new Set());
16
16
  const [isTreeCollapsed, setIsTreeCollapsed] = useState(false);
17
17
 
18
+ const styles: Record<string, React.CSSProperties> = {
19
+ panel: {
20
+ background: "rgba(0,0,0,0.55)",
21
+ color: "rgba(255,255,255,0.9)",
22
+ border: "1px solid rgba(255,255,255,0.12)",
23
+ borderRadius: 6,
24
+ overflow: "hidden",
25
+ maxHeight: "85vh",
26
+ display: "flex",
27
+ flexDirection: "column",
28
+ backdropFilter: "blur(6px)",
29
+ WebkitBackdropFilter: "blur(6px)",
30
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
31
+ fontSize: 11,
32
+ lineHeight: 1.2,
33
+ userSelect: "none",
34
+ WebkitUserSelect: "none",
35
+ },
36
+ panelHeader: {
37
+ padding: "4px 6px",
38
+ borderBottom: "1px solid rgba(255,255,255,0.10)",
39
+ display: "flex",
40
+ gap: 8,
41
+ alignItems: "center",
42
+ justifyContent: "space-between",
43
+ cursor: "pointer",
44
+ background: "rgba(255,255,255,0.05)",
45
+ textTransform: "uppercase",
46
+ letterSpacing: "0.08em",
47
+ fontSize: 10,
48
+ color: "rgba(255,255,255,0.7)",
49
+ },
50
+ scroll: {
51
+ overflowY: "auto",
52
+ },
53
+ row: {
54
+ display: "flex",
55
+ alignItems: "center",
56
+ padding: "2px 6px",
57
+ borderBottom: "1px solid rgba(255,255,255,0.07)",
58
+ cursor: "pointer",
59
+ whiteSpace: "nowrap",
60
+ },
61
+ rowSelected: {
62
+ background: "rgba(255,255,255,0.10)",
63
+ },
64
+ chevron: {
65
+ width: 12,
66
+ textAlign: "center",
67
+ opacity: 0.55,
68
+ fontSize: 10,
69
+ marginRight: 4,
70
+ cursor: "pointer",
71
+ },
72
+ idText: {
73
+ fontSize: 11,
74
+ overflow: "hidden",
75
+ textOverflow: "ellipsis",
76
+ },
77
+ contextMenu: {
78
+ position: "fixed",
79
+ zIndex: 50,
80
+ minWidth: 120,
81
+ background: "rgba(0,0,0,0.82)",
82
+ border: "1px solid rgba(255,255,255,0.16)",
83
+ borderRadius: 6,
84
+ overflow: "hidden",
85
+ boxShadow: "0 12px 32px rgba(0,0,0,0.45)",
86
+ backdropFilter: "blur(6px)",
87
+ WebkitBackdropFilter: "blur(6px)",
88
+ },
89
+ menuItem: {
90
+ width: "100%",
91
+ textAlign: "left",
92
+ padding: "6px 8px",
93
+ background: "transparent",
94
+ border: "none",
95
+ color: "rgba(255,255,255,0.9)",
96
+ font: "inherit",
97
+ cursor: "pointer",
98
+ },
99
+ menuItemDanger: {
100
+ color: "rgba(255,120,120,0.95)",
101
+ },
102
+ menuDivider: {
103
+ borderTop: "1px solid rgba(255,255,255,0.10)",
104
+ }
105
+ };
106
+
18
107
  if (!prefabData || !setPrefabData) return null;
19
108
 
20
109
  const handleContextMenu = (e: MouseEvent, nodeId: string) => {
@@ -152,24 +241,42 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
152
241
  const hasChildren = node.children && node.children.length > 0;
153
242
 
154
243
  return (
155
- <div key={node.id} className="select-none">
244
+ <div key={node.id}>
156
245
  <div
157
- className={`flex items-center py-0.5 px-1 cursor-pointer border-b border-cyan-500/10 ${isSelected ? 'bg-cyan-500/30 hover:bg-cyan-500/40 border-cyan-400/30' : 'hover:bg-cyan-500/10'}`}
158
- style={{ paddingLeft: `${depth * 8 + 4}px` }}
246
+ style={{
247
+ ...styles.row,
248
+ ...(isSelected ? styles.rowSelected : null),
249
+ paddingLeft: `${depth * 10 + 6}px`,
250
+ }}
159
251
  onClick={(e) => { e.stopPropagation(); setSelectedId(node.id); }}
160
252
  onContextMenu={(e) => handleContextMenu(e, node.id)}
161
253
  draggable={node.id !== prefabData.root.id}
162
254
  onDragStart={(e) => handleDragStart(e, node.id)}
163
255
  onDragOver={(e) => handleDragOver(e, node.id)}
164
256
  onDrop={(e) => handleDrop(e, node.id)}
257
+ onPointerEnter={(e) => {
258
+ if (!isSelected) (e.currentTarget as HTMLDivElement).style.background = "rgba(255,255,255,0.06)";
259
+ }}
260
+ onPointerLeave={(e) => {
261
+ if (!isSelected) (e.currentTarget as HTMLDivElement).style.background = "transparent";
262
+ }}
165
263
  >
166
264
  <span
167
- className={`mr-0.5 w-3 text-center text-cyan-400/50 hover:text-cyan-400 cursor-pointer text-[8px] ${hasChildren ? '' : 'invisible'}`}
265
+ style={{
266
+ ...styles.chevron,
267
+ visibility: hasChildren ? 'visible' : 'hidden',
268
+ }}
168
269
  onClick={(e) => hasChildren && toggleCollapse(e, node.id)}
270
+ onPointerEnter={(e) => {
271
+ (e.currentTarget as HTMLSpanElement).style.opacity = "0.9";
272
+ }}
273
+ onPointerLeave={(e) => {
274
+ (e.currentTarget as HTMLSpanElement).style.opacity = "0.55";
275
+ }}
169
276
  >
170
277
  {isCollapsed ? '▶' : '▼'}
171
278
  </span>
172
- <span className="text-[10px] truncate font-mono text-cyan-300">
279
+ <span style={styles.idText}>
173
280
  {node.id}
174
281
  </span>
175
282
  </div>
@@ -184,16 +291,28 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
184
291
 
185
292
  return (
186
293
  <>
187
- <div className="bg-black/70 backdrop-blur-sm text-white border border-cyan-500/30 max-h-[85vh] overflow-y-auto flex flex-col" style={{ width: isTreeCollapsed ? 'auto' : '14rem' }} onClick={closeContextMenu}>
294
+ <div
295
+ style={{
296
+ ...styles.panel,
297
+ width: isTreeCollapsed ? 'auto' : '14rem',
298
+ }}
299
+ onClick={closeContextMenu}
300
+ >
188
301
  <div
189
- className="px-1.5 py-1 font-mono text-[10px] bg-cyan-500/10 border-b border-cyan-500/30 sticky top-0 uppercase tracking-wider text-cyan-400/80 cursor-pointer hover:bg-cyan-500/20 flex items-center justify-between"
302
+ style={styles.panelHeader}
190
303
  onClick={(e) => { e.stopPropagation(); setIsTreeCollapsed(!isTreeCollapsed); }}
304
+ onPointerEnter={(e) => {
305
+ (e.currentTarget as HTMLDivElement).style.background = "rgba(255,255,255,0.08)";
306
+ }}
307
+ onPointerLeave={(e) => {
308
+ (e.currentTarget as HTMLDivElement).style.background = "rgba(255,255,255,0.05)";
309
+ }}
191
310
  >
192
311
  <span>Prefab Graph</span>
193
- <span className="text-[8px]">{isTreeCollapsed ? '▶' : '◀'}</span>
312
+ <span style={{ fontSize: 10, opacity: 0.8 }}>{isTreeCollapsed ? '▶' : '◀'}</span>
194
313
  </div>
195
314
  {!isTreeCollapsed && (
196
- <div className="flex-1 py-0.5">
315
+ <div style={{ ...styles.scroll, padding: 2 }}>
197
316
  {renderNode(prefabData.root)}
198
317
  </div>
199
318
  )}
@@ -201,28 +320,49 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
201
320
 
202
321
  {contextMenu && (
203
322
  <div
204
- className="fixed bg-black/90 backdrop-blur-sm border border-cyan-500/40 z-50 min-w-[100px]"
205
- style={{ top: contextMenu.y, left: contextMenu.x }}
323
+ style={{
324
+ ...styles.contextMenu,
325
+ top: contextMenu.y,
326
+ left: contextMenu.x,
327
+ }}
206
328
  onClick={(e) => e.stopPropagation()}
207
329
  onPointerLeave={closeContextMenu}
208
330
  >
209
331
  <button
210
- className="w-full text-left px-2 py-1 hover:bg-cyan-500/20 text-[10px] text-cyan-300 font-mono border-b border-cyan-500/20"
332
+ style={{ ...styles.menuItem, ...styles.menuDivider }}
211
333
  onClick={() => handleAddChild(contextMenu.nodeId)}
334
+ onPointerEnter={(e) => {
335
+ (e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
336
+ }}
337
+ onPointerLeave={(e) => {
338
+ (e.currentTarget as HTMLButtonElement).style.background = "transparent";
339
+ }}
212
340
  >
213
341
  Add Child
214
342
  </button>
215
343
  {contextMenu.nodeId !== prefabData.root.id && (
216
344
  <>
217
345
  <button
218
- className="w-full text-left px-2 py-1 hover:bg-cyan-500/20 text-[10px] text-cyan-300 font-mono border-b border-cyan-500/20"
346
+ style={{ ...styles.menuItem, ...styles.menuDivider }}
219
347
  onClick={() => handleDuplicate(contextMenu.nodeId)}
348
+ onPointerEnter={(e) => {
349
+ (e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
350
+ }}
351
+ onPointerLeave={(e) => {
352
+ (e.currentTarget as HTMLButtonElement).style.background = "transparent";
353
+ }}
220
354
  >
221
355
  Duplicate
222
356
  </button>
223
357
  <button
224
- className="w-full text-left px-2 py-1 hover:bg-red-500/20 text-[10px] text-red-400 font-mono"
358
+ style={{ ...styles.menuItem, ...styles.menuItemDanger }}
225
359
  onClick={() => handleDelete(contextMenu.nodeId)}
360
+ onPointerEnter={(e) => {
361
+ (e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
362
+ }}
363
+ onPointerLeave={(e) => {
364
+ (e.currentTarget as HTMLButtonElement).style.background = "transparent";
365
+ }}
226
366
  >
227
367
  Delete
228
368
  </button>
@@ -15,6 +15,47 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
15
15
  }) {
16
16
  const [isInspectorCollapsed, setIsInspectorCollapsed] = useState(false);
17
17
 
18
+ const ui: Record<string, React.CSSProperties> = {
19
+ panel: {
20
+ position: 'absolute',
21
+ top: 8,
22
+ right: 8,
23
+ zIndex: 20,
24
+ width: 260,
25
+ background: 'rgba(0,0,0,0.55)',
26
+ color: 'rgba(255,255,255,0.9)',
27
+ border: '1px solid rgba(255,255,255,0.12)',
28
+ borderRadius: 6,
29
+ overflow: 'hidden',
30
+ backdropFilter: 'blur(6px)',
31
+ WebkitBackdropFilter: 'blur(6px)',
32
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
33
+ fontSize: 11,
34
+ lineHeight: 1.2,
35
+ },
36
+ header: {
37
+ padding: '4px 6px',
38
+ display: 'flex',
39
+ alignItems: 'center',
40
+ justifyContent: 'space-between',
41
+ cursor: 'pointer',
42
+ background: 'rgba(255,255,255,0.05)',
43
+ borderBottom: '1px solid rgba(255,255,255,0.10)',
44
+ textTransform: 'uppercase',
45
+ letterSpacing: '0.08em',
46
+ fontSize: 10,
47
+ color: 'rgba(255,255,255,0.7)',
48
+ userSelect: 'none',
49
+ WebkitUserSelect: 'none',
50
+ },
51
+ left: {
52
+ position: 'absolute',
53
+ top: 8,
54
+ left: 8,
55
+ zIndex: 20,
56
+ },
57
+ };
58
+
18
59
  const updateNode = (updater: (n: GameObjectType) => GameObjectType) => {
19
60
  if (!prefabData || !setPrefabData || !selectedId) return;
20
61
  setPrefabData(prev => ({
@@ -40,13 +81,19 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
40
81
 
41
82
  // if (!selectedNode) return null;
42
83
  return <>
43
- <div style={{ position: 'absolute', top: "0.5rem", right: "0.5rem", zIndex: 20, backgroundColor: "rgba(0,0,0,0.7)", backdropFilter: "blur(4px)", color: "white", border: "1px solid rgba(0,255,255,0.3)" }} >
84
+ <div style={ui.panel}>
44
85
  <div
45
- className="px-1.5 py-1 font-mono text-[10px] bg-cyan-500/10 border-b border-cyan-500/30 sticky top-0 uppercase tracking-wider text-cyan-400/80 cursor-pointer hover:bg-cyan-500/20 flex items-center justify-between"
86
+ style={ui.header}
46
87
  onClick={() => setIsInspectorCollapsed(!isInspectorCollapsed)}
88
+ onPointerEnter={(e) => {
89
+ (e.currentTarget as HTMLDivElement).style.background = 'rgba(255,255,255,0.08)';
90
+ }}
91
+ onPointerLeave={(e) => {
92
+ (e.currentTarget as HTMLDivElement).style.background = 'rgba(255,255,255,0.05)';
93
+ }}
47
94
  >
48
95
  <span>Inspector</span>
49
- <span className="text-[8px]">{isInspectorCollapsed ? '◀' : '▶'}</span>
96
+ <span style={{ fontSize: 10, opacity: 0.8 }}>{isInspectorCollapsed ? '◀' : '▶'}</span>
50
97
  </div>
51
98
  {!isInspectorCollapsed && selectedNode && (
52
99
  <NodeInspector
@@ -59,7 +106,7 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
59
106
  />
60
107
  )}
61
108
  </div>
62
- <div style={{ position: 'absolute', top: "0.5rem", left: "0.5rem", zIndex: 20 }} >
109
+ <div style={ui.left}>
63
110
  <EditorTree
64
111
  prefabData={prefabData}
65
112
  setPrefabData={setPrefabData}
@@ -82,6 +129,103 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
82
129
  const allComponentKeys = Object.keys(ALL_COMPONENTS);
83
130
  const [addComponentType, setAddComponentType] = useState(allComponentKeys[0]);
84
131
 
132
+ const s: Record<string, React.CSSProperties> = {
133
+ root: {
134
+ display: 'flex',
135
+ flexDirection: 'column',
136
+ gap: 6,
137
+ padding: 6,
138
+ maxHeight: '80vh',
139
+ overflowY: 'auto',
140
+ },
141
+ section: {
142
+ paddingBottom: 6,
143
+ borderBottom: '1px solid rgba(255,255,255,0.10)',
144
+ },
145
+ label: {
146
+ display: 'block',
147
+ fontSize: 10,
148
+ opacity: 0.7,
149
+ textTransform: 'uppercase',
150
+ letterSpacing: '0.08em',
151
+ marginBottom: 4,
152
+ },
153
+ input: {
154
+ width: '100%',
155
+ background: 'rgba(255,255,255,0.06)',
156
+ border: '1px solid rgba(255,255,255,0.14)',
157
+ borderRadius: 4,
158
+ padding: '4px 6px',
159
+ color: 'rgba(255,255,255,0.92)',
160
+ font: 'inherit',
161
+ outline: 'none',
162
+ },
163
+ row: {
164
+ display: 'flex',
165
+ alignItems: 'center',
166
+ justifyContent: 'space-between',
167
+ gap: 8,
168
+ },
169
+ button: {
170
+ padding: '2px 6px',
171
+ background: 'transparent',
172
+ color: 'rgba(255,255,255,0.9)',
173
+ border: '1px solid rgba(255,255,255,0.14)',
174
+ borderRadius: 4,
175
+ cursor: 'pointer',
176
+ font: 'inherit',
177
+ },
178
+ buttonActive: {
179
+ background: 'rgba(255,255,255,0.10)',
180
+ },
181
+ smallDanger: {
182
+ background: 'transparent',
183
+ border: 'none',
184
+ cursor: 'pointer',
185
+ color: 'rgba(255,120,120,0.95)',
186
+ font: 'inherit',
187
+ padding: '2px 4px',
188
+ },
189
+ componentHeader: {
190
+ display: 'flex',
191
+ alignItems: 'center',
192
+ justifyContent: 'space-between',
193
+ padding: '4px 0',
194
+ borderBottom: '1px solid rgba(255,255,255,0.08)',
195
+ marginBottom: 4,
196
+ },
197
+ componentTitle: {
198
+ fontSize: 10,
199
+ textTransform: 'uppercase',
200
+ letterSpacing: '0.08em',
201
+ opacity: 0.8,
202
+ },
203
+ select: {
204
+ flex: 1,
205
+ background: 'rgba(255,255,255,0.06)',
206
+ border: '1px solid rgba(255,255,255,0.14)',
207
+ borderRadius: 4,
208
+ padding: '4px 6px',
209
+ color: 'rgba(255,255,255,0.92)',
210
+ font: 'inherit',
211
+ outline: 'none',
212
+ },
213
+ addButton: {
214
+ width: 28,
215
+ padding: '4px 0',
216
+ background: 'rgba(255,255,255,0.08)',
217
+ color: 'rgba(255,255,255,0.92)',
218
+ border: '1px solid rgba(255,255,255,0.14)',
219
+ borderRadius: 4,
220
+ cursor: 'pointer',
221
+ font: 'inherit',
222
+ },
223
+ disabled: {
224
+ opacity: 0.35,
225
+ cursor: 'not-allowed',
226
+ },
227
+ };
228
+
85
229
  const componentKeys = Object.keys(node.components || {}).join(',');
86
230
  useEffect(() => {
87
231
  // Components stored on nodes use lowercase keys (e.g. 'geometry'),
@@ -92,28 +236,44 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
92
236
  }
93
237
  }, [componentKeys, addComponentType, node.components, allComponentKeys]);
94
238
 
95
- return <div className="flex flex-col gap-1 text-[11px] max-w-[250px] max-h-[80vh] overflow-y-auto">
96
- <div className="border-b border-cyan-500/20 pb-1 px-1.5 pt-1">
239
+ return <div style={s.root}>
240
+ <div style={s.section}>
97
241
  <input
98
- className="w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[11px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50"
242
+ style={s.input}
99
243
  value={node.id}
100
244
  onChange={e => updateNode(n => ({ ...n, id: e.target.value }))}
101
245
  />
102
246
  </div>
103
247
 
104
- <div className="flex justify-between items-center px-1.5 py-0.5 border-b border-cyan-500/20">
105
- <label className="text-[10px] font-mono text-cyan-400/80 uppercase tracking-wider">Components</label>
106
- <button onClick={deleteNode} className="text-[10px] text-red-400/80 hover:text-red-400">✕</button>
248
+ <div style={{ ...s.row, ...s.section, paddingBottom: 6 }}>
249
+ <label style={{ ...s.label, marginBottom: 0 }}>Components</label>
250
+ <button
251
+ onClick={deleteNode}
252
+ style={s.smallDanger}
253
+ title="Delete node"
254
+ >
255
+
256
+ </button>
107
257
  </div>
108
258
 
109
- <div className="px-1.5 py-1 border-b border-cyan-500/20">
110
- <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Mode</label>
111
- <div className="flex gap-0.5">
259
+ <div style={s.section}>
260
+ <label style={s.label}>Mode</label>
261
+ <div style={{ display: 'flex', gap: 6 }}>
112
262
  {["translate", "rotate", "scale"].map(mode => (
113
263
  <button
114
264
  key={mode}
115
265
  onClick={() => setTransformMode(mode as any)}
116
- className={`flex-1 px-1 py-0.5 text-[10px] font-mono border ${transformMode === mode ? 'bg-cyan-500/30 border-cyan-400/50 text-cyan-200' : 'bg-black/30 border-cyan-500/20 text-cyan-400/60 hover:border-cyan-400/30'}`}
266
+ style={{
267
+ ...s.button,
268
+ flex: 1,
269
+ ...(transformMode === mode ? s.buttonActive : null),
270
+ }}
271
+ onPointerEnter={(e) => {
272
+ if (transformMode !== mode) (e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.08)';
273
+ }}
274
+ onPointerLeave={(e) => {
275
+ if (transformMode !== mode) (e.currentTarget as HTMLButtonElement).style.background = 'transparent';
276
+ }}
117
277
  >
118
278
  {mode[0].toUpperCase()}
119
279
  </button>
@@ -121,53 +281,29 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
121
281
  </div>
122
282
  </div>
123
283
 
124
- {/* Components */}
125
- {/* {node.components && Object.entries(node.components).map(([key, comp]: [string, any]) => {
126
- if (!comp) return null;
127
- return (
128
- <div key={key} className="border border-cyan-500/20 mx-1 my-0.5 bg-black/20">
129
- <div className="flex justify-between items-center px-1 py-0.5 border-b border-cyan-500/20 bg-cyan-500/5">
130
- <span className="font-mono text-[10px] text-cyan-300 uppercase">{key}</span>
131
- <button
132
- onClick={() => updateNode(n => {
133
- const components = { ...n.components };
134
- delete components[key as keyof typeof components];
135
- return { ...n, components };
136
- })}
137
- className="text-[9px] text-red-400/60 hover:text-red-400"
138
- >
139
-
140
- </button>
141
- </div>
142
- <div className="px-1 py-0.5">
143
- <ComponentEditor component={comp} onChange={(newComp: any) => updateNode(n => ({
144
- ...n,
145
- components: { ...n.components, [key]: newComp }
146
- }))} />
147
- </div>
148
- </div>
149
- );
150
- })} */}
284
+ {/* Components (legacy renderer removed) */}
151
285
 
152
286
  {node.components && Object.entries(node.components).map(([key, comp]: [string, any]) => {
153
287
  if (!comp) return null;
154
288
  const componentDef = ALL_COMPONENTS[comp.type];
155
- if (!componentDef) return <div key={key} className="px-1 py-0.5 text-red-400 text-[10px]">Unknown component type: {comp.type}
289
+ if (!componentDef) return <div key={key} style={{ padding: '4px 0', color: 'rgba(255,120,120,0.95)', fontSize: 11 }}>
290
+ Unknown component type: {comp.type}
156
291
  <textarea defaultValue={JSON.stringify(comp)} />
157
292
  </div>;
158
293
 
159
294
  const EditorComp = componentDef.Editor;
160
295
  return (
161
- <div key={key} className='px-1'>
162
- <div className="flex justify-between items-center py-0.5 border-b border-cyan-500/20 bg-cyan-500/5">
163
- <span className="font-mono text-[10px] text-cyan-300 uppercase">{key}</span>
296
+ <div key={key} style={{ padding: '0 2px' }}>
297
+ <div style={s.componentHeader}>
298
+ <span style={s.componentTitle}>{key}</span>
164
299
  <button
165
300
  onClick={() => updateNode(n => {
166
301
  const components = { ...n.components };
167
302
  delete components[key as keyof typeof components];
168
303
  return { ...n, components };
169
304
  })}
170
- className="text-[9px] text-red-400/60 hover:text-red-400"
305
+ style={s.smallDanger}
306
+ title="Remove component"
171
307
  >
172
308
 
173
309
  </button>
@@ -193,11 +329,11 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
193
329
  })}
194
330
 
195
331
  {/* Add Component */}
196
- <div className="px-1.5 py-1 border-t border-cyan-500/20">
197
- <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Add Component</label>
198
- <div className="flex gap-0.5">
332
+ <div style={{ ...s.section, borderBottom: 'none', paddingBottom: 0 }}>
333
+ <label style={s.label}>Add Component</label>
334
+ <div style={{ display: 'flex', gap: 6 }}>
199
335
  <select
200
- className="bg-black/40 border border-cyan-500/30 px-1 py-0.5 flex-1 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50"
336
+ style={s.select}
201
337
  value={addComponentType}
202
338
  onChange={e => setAddComponentType(e.target.value)}
203
339
  >
@@ -206,7 +342,10 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
206
342
  ))}
207
343
  </select>
208
344
  <button
209
- className="bg-cyan-500/20 hover:bg-cyan-500/30 border border-cyan-500/30 px-2 py-0.5 text-[10px] text-cyan-300 font-mono disabled:opacity-30"
345
+ style={{
346
+ ...s.addButton,
347
+ ...(!addComponentType ? s.disabled : null),
348
+ }}
210
349
  disabled={!addComponentType}
211
350
  onClick={() => {
212
351
  if (!addComponentType) return;
@@ -222,6 +361,14 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
222
361
  }));
223
362
  }
224
363
  }}
364
+ onPointerEnter={(e) => {
365
+ if (!addComponentType) return;
366
+ (e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.12)';
367
+ }}
368
+ onPointerLeave={(e) => {
369
+ if (!addComponentType) return;
370
+ (e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.08)';
371
+ }}
225
372
  >
226
373
  +
227
374
  </button>
@@ -67,26 +67,88 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }: { b
67
67
  </Physics>
68
68
  </GameCanvas>
69
69
 
70
- <div style={{ position: "absolute", top: "0.5rem", left: "50%", transform: "translateX(-50%)" }} className="bg-black/70 backdrop-blur-sm border border-cyan-500/30 px-2 py-1 flex items-center gap-1">
70
+ <div
71
+ style={{
72
+ position: "absolute",
73
+ top: 8,
74
+ left: "50%",
75
+ transform: "translateX(-50%)",
76
+ display: "flex",
77
+ alignItems: "center",
78
+ gap: 6,
79
+ padding: "2px 4px",
80
+ background: "rgba(0,0,0,0.55)",
81
+ border: "1px solid rgba(255,255,255,0.12)",
82
+ borderRadius: 4,
83
+ color: "rgba(255,255,255,0.9)",
84
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
85
+ fontSize: 11,
86
+ lineHeight: 1,
87
+ WebkitUserSelect: "none",
88
+ userSelect: "none",
89
+ }}
90
+ >
71
91
  <button
72
- className="px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30"
92
+ style={{
93
+ padding: "2px 6px",
94
+ font: "inherit",
95
+ background: "transparent",
96
+ color: "inherit",
97
+ border: "1px solid rgba(255,255,255,0.18)",
98
+ borderRadius: 3,
99
+ cursor: "pointer",
100
+ }}
73
101
  onClick={() => setEditMode(!editMode)}
102
+ onPointerEnter={(e) => {
103
+ (e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
104
+ }}
105
+ onPointerLeave={(e) => {
106
+ (e.currentTarget as HTMLButtonElement).style.background = "transparent";
107
+ }}
74
108
  >
75
109
  {editMode ? "▶" : "⏸"}
76
110
  </button>
77
- <span className="text-cyan-500/30 text-[10px]">|</span>
111
+ <span style={{ opacity: 0.35 }}>|</span>
78
112
  <button
79
- className="px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30"
113
+ style={{
114
+ padding: "2px 6px",
115
+ font: "inherit",
116
+ background: "transparent",
117
+ color: "inherit",
118
+ border: "1px solid rgba(255,255,255,0.18)",
119
+ borderRadius: 3,
120
+ cursor: "pointer",
121
+ }}
80
122
  onClick={async () => {
81
123
  const prefab = await loadJson();
82
124
  if (prefab) setLoadedPrefab(prefab);
83
125
  }}
126
+ onPointerEnter={(e) => {
127
+ (e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
128
+ }}
129
+ onPointerLeave={(e) => {
130
+ (e.currentTarget as HTMLButtonElement).style.background = "transparent";
131
+ }}
84
132
  >
85
133
  📥
86
134
  </button>
87
135
  <button
88
- className="px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30"
136
+ style={{
137
+ padding: "2px 6px",
138
+ font: "inherit",
139
+ background: "transparent",
140
+ color: "inherit",
141
+ border: "1px solid rgba(255,255,255,0.18)",
142
+ borderRadius: 3,
143
+ cursor: "pointer",
144
+ }}
89
145
  onClick={() => saveJson(loadedPrefab, "prefab")}
146
+ onPointerEnter={(e) => {
147
+ (e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
148
+ }}
149
+ onPointerLeave={(e) => {
150
+ (e.currentTarget as HTMLButtonElement).style.background = "transparent";
151
+ }}
90
152
  >
91
153
  💾
92
154
  </button>
@@ -1,8 +1,8 @@
1
- import { ThreeElements } from "@react-three/fiber"
1
+ // import { ThreeElements } from "@react-three/fiber"
2
2
 
3
3
  export interface Prefab {
4
- id: string;
5
- name: string;
4
+ id?: string;
5
+ name?: string;
6
6
  description?: string;
7
7
  author?: string;
8
8
  version?: string;