react-three-game 0.0.15 → 0.0.17

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,44 +17,17 @@ 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}";
44
- ```
45
-
46
- ### Tailwind v3
47
-
48
- Add the library path to your `tailwind.config.js`:
49
-
50
- ```js
51
- module.exports = {
52
- content: [
53
- // ...
54
- "./node_modules/react-three-game/dist/**/*.{js,ts,jsx,tsx}",
55
- ],
56
- // ...
57
- }
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
+ }} />
58
31
  ```
59
32
 
60
33
  ## Quick Start
@@ -64,39 +37,44 @@ npm install react-three-game @react-three/fiber @react-three/rapier three
64
37
  ```
65
38
 
66
39
  ```jsx
67
- import { GameCanvas, PrefabRoot, ground } from 'react-three-game';
40
+ import { Physics } from '@react-three/rapier';
41
+ import { GameCanvas, PrefabRoot } from 'react-three-game';
68
42
 
69
43
  export default function App() {
70
44
  return (
71
45
  <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" } }
46
+ <Physics>
47
+ <ambientLight intensity={0.8} />
48
+ <PrefabRoot
49
+ data={{
50
+ id: "scene",
51
+ name: "scene",
52
+ root: {
53
+ id: "root",
54
+ children: [
55
+ {
56
+ id: "ground",
57
+ components: {
58
+ transform: { type: "Transform", properties: { position: [0, 0, 0], rotation: [-1.57, 0, 0] } },
59
+ geometry: { type: "Geometry", properties: { geometryType: "plane", args: [50, 50] } },
60
+ material: { type: "Material", properties: { color: "green" } },
61
+ physics: { type: "Physics", properties: { type: "fixed" } }
62
+ }
63
+ },
64
+ {
65
+ id: "player",
66
+ components: {
67
+ transform: { type: "Transform", properties: { position: [0, 2, 0] } },
68
+ geometry: { type: "Geometry", properties: { geometryType: "sphere" } },
69
+ material: { type: "Material", properties: { color: "#ff6b6b" } },
70
+ physics: { type: "Physics", properties: { type: "dynamic" } }
71
+ }
72
+ }
73
+ ]
95
74
  }
96
- }
97
- ]
98
- }
99
- }} />
75
+ }}
76
+ />
77
+ </Physics>
100
78
  </GameCanvas>
101
79
  );
102
80
  }
@@ -122,28 +100,100 @@ interface GameObject {
122
100
 
123
101
  ## Custom Components
124
102
 
125
- ```tsx
103
+ Extend the engine by registering your own components. Components have two parts:
104
+ - **Editor**: UI for inspector panel (edit mode)
105
+ - **View**: Three.js runtime renderer (play mode)
106
+
107
+ ### Component Interface
108
+
109
+ ```typescript
126
110
  import { Component } from 'react-three-game';
127
111
 
128
- const LaserComponent: Component = {
129
- name: 'Laser',
112
+ interface Component {
113
+ name: string;
114
+ Editor: FC<{ component: any; onUpdate: (newComp: any) => void }>;
115
+ View?: FC<{ properties: any; children?: React.ReactNode }>;
116
+ defaultProperties: any;
117
+ }
118
+ ```
119
+
120
+ ### Example: Rotator Component
121
+
122
+ ```tsx
123
+ import { Component, registerComponent } from 'react-three-game';
124
+ import { useFrame } from '@react-three/fiber';
125
+ import { useRef } from 'react';
126
+
127
+ const RotatorComponent: Component = {
128
+ name: 'Rotator',
129
+
130
130
  Editor: ({ component, onUpdate }) => (
131
- <input
132
- value={component.properties.damage}
133
- onChange={e => onUpdate({ damage: +e.target.value })}
134
- />
131
+ <div>
132
+ <label>Speed</label>
133
+ <input
134
+ type="number"
135
+ value={component.properties.speed ?? 1.0}
136
+ onChange={e => onUpdate({ ...component.properties, speed: parseFloat(e.target.value) })}
137
+ />
138
+ <label>Axis</label>
139
+ <select
140
+ value={component.properties.axis ?? 'y'}
141
+ onChange={e => onUpdate({ ...component.properties, axis: e.target.value })}
142
+ >
143
+ <option value="x">X</option>
144
+ <option value="y">Y</option>
145
+ <option value="z">Z</option>
146
+ </select>
147
+ </div>
135
148
  ),
136
- View: ({ properties }) => (
137
- <pointLight color="red" intensity={properties.damage} />
138
- ),
139
- defaultProperties: { damage: 10 }
149
+
150
+ View: ({ properties, children }) => {
151
+ const ref = useRef();
152
+ const speed = properties.speed ?? 1.0;
153
+ const axis = properties.axis ?? 'y';
154
+
155
+ useFrame((state, delta) => {
156
+ if (ref.current) {
157
+ ref.current.rotation[axis] += delta * speed;
158
+ }
159
+ });
160
+
161
+ return <group ref={ref}>{children}</group>;
162
+ },
163
+
164
+ defaultProperties: { speed: 1.0, axis: 'y' }
140
165
  };
141
166
 
142
- // Register
143
- import { registerComponent } from 'react-three-game';
144
- registerComponent(LaserComponent);
167
+ // Register before using PrefabEditor
168
+ registerComponent(RotatorComponent);
169
+ ```
170
+
171
+ ### Usage in Prefab JSON
172
+
173
+ ```json
174
+ {
175
+ "id": "spinning-cube",
176
+ "components": {
177
+ "transform": { "type": "Transform", "properties": { "position": [0, 1, 0] } },
178
+ "geometry": { "type": "Geometry", "properties": { "geometryType": "box" } },
179
+ "material": { "type": "Material", "properties": { "color": "#ff6b6b" } },
180
+ "rotator": { "type": "Rotator", "properties": { "speed": 2.0, "axis": "y" } }
181
+ }
182
+ }
145
183
  ```
146
184
 
185
+ ### Wrapper vs Leaf Components
186
+
187
+ **Wrapper components** (accept `children`) wrap the rendered content:
188
+ - Use for behaviors that need to manipulate the scene graph (animations, controllers)
189
+ - Example: Rotator wraps mesh to apply rotation
190
+
191
+ **Leaf components** (no `children`) render as siblings:
192
+ - Use for standalone effects (lights, particles, audio sources)
193
+ - Example: SpotLight renders a `<spotLight>` element
194
+
195
+ The engine automatically detects component type by checking if `View` accepts `children` prop.
196
+
147
197
  ## Built-in Components
148
198
 
149
199
  | Component | Properties |
@@ -172,41 +222,13 @@ Transform gizmos (T/R/S keys), drag-to-reorder tree, import/export JSON, edit/pl
172
222
  ### Transform Hierarchy
173
223
  - Local transforms stored in JSON (relative to parent)
174
224
  - World transforms computed at runtime via matrix multiplication
175
- - `computeParentWorldMatrix(root, targetId)` traverses tree for parent's world matrix
176
225
  - TransformControls extract world matrix → compute parent inverse → derive new local transform
177
226
 
178
227
  ### GPU Instancing
179
- Enable with `model.properties.instanced = true`:
180
- ```json
181
- {
182
- "components": {
183
- "model": {
184
- "type": "Model",
185
- "properties": {
186
- "filename": "tree.glb",
187
- "instanced": true
188
- }
189
- }
190
- }
191
- }
192
- ```
193
- Uses drei's `<Merged>` + `<InstancedRigidBodies>` for physics. World-space transforms, terminal nodes.
228
+ Enable with `model.properties.instanced = true` for optimized repeated geometry. Uses drei's `<Merged>` + `<InstancedRigidBodies>`.
194
229
 
195
230
  ### Model Loading
196
- - Supports GLB/GLTF (Draco compression) and FBX
197
- - Singleton loaders in `modelLoader.ts`
198
- - Draco decoder from `https://www.gstatic.com/draco/v1/decoders/`
199
- - Auto-loads when `model.properties.filename` detected
200
-
201
- ### WebGPU Renderer
202
- ```tsx
203
- <Canvas gl={async ({ canvas }) => {
204
- const renderer = new WebGPURenderer({ canvas, shadowMap: true });
205
- await renderer.init(); // Required
206
- return renderer;
207
- }}>
208
- ```
209
- Use `MeshStandardNodeMaterial` not `MeshStandardMaterial`.
231
+ Supports GLB/GLTF (with Draco compression) and FBX. Models auto-load when `model.properties.filename` is detected.
210
232
 
211
233
  ## Patterns
212
234
 
@@ -216,15 +238,6 @@ import levelData from './prefabs/arena.json';
216
238
  <PrefabRoot data={levelData} />
217
239
  ```
218
240
 
219
- ### Mix with React Components
220
- ```jsx
221
- <Physics>
222
- <PrefabRoot data={environment} />
223
- <Player />
224
- <AIEnemies />
225
- </Physics>
226
- ```
227
-
228
241
  ### Update Prefab Nodes
229
242
  ```typescript
230
243
  function updatePrefabNode(root: GameObject, id: string, update: (node: GameObject) => GameObject): GameObject {
@@ -236,58 +249,6 @@ function updatePrefabNode(root: GameObject, id: string, update: (node: GameObjec
236
249
  }
237
250
  ```
238
251
 
239
- ## AI Agent Reference
240
-
241
- ### Prefab JSON Schema
242
- ```typescript
243
- {
244
- "id": "unique-id",
245
- "root": {
246
- "id": "root-id",
247
- "components": {
248
- "transform": {
249
- "type": "Transform",
250
- "properties": {
251
- "position": [x, y, z], // world units
252
- "rotation": [x, y, z], // radians
253
- "scale": [x, y, z] // multipliers
254
- }
255
- },
256
- "geometry": {
257
- "type": "Geometry",
258
- "properties": {
259
- "geometryType": "box" | "sphere" | "plane" | "cylinder" | "cone" | "torus",
260
- "args": [/* geometry-specific */]
261
- }
262
- },
263
- "material": {
264
- "type": "Material",
265
- "properties": {
266
- "color": "#rrggbb",
267
- "texture": "/path/to/texture.jpg",
268
- "metalness": 0.0-1.0,
269
- "roughness": 0.0-1.0
270
- }
271
- },
272
- "physics": {
273
- "type": "Physics",
274
- "properties": {
275
- "type": "dynamic" | "fixed"
276
- }
277
- },
278
- "model": {
279
- "type": "Model",
280
- "properties": {
281
- "filename": "/models/asset.glb",
282
- "instanced": true
283
- }
284
- }
285
- },
286
- "children": [/* recursive GameObjects */]
287
- }
288
- }
289
- ```
290
-
291
252
  ## Development
292
253
 
293
254
  ```bash
package/dist/index.d.ts CHANGED
@@ -3,5 +3,7 @@ export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
3
3
  export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
4
4
  export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
5
5
  export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
6
+ export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
7
+ export type { Component } from './tools/prefabeditor/components/ComponentRegistry';
6
8
  export * from './helpers';
7
9
  export type { Prefab, GameObject } from './tools/prefabeditor/types';
package/dist/index.js CHANGED
@@ -4,5 +4,7 @@ export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
4
4
  export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
5
5
  export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
6
6
  export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
7
+ // Component Registry
8
+ export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
7
9
  // Helpers
8
10
  export * from './helpers';
@@ -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,105 @@ 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
+ dragHandle: {
69
+ width: 14,
70
+ height: 14,
71
+ display: "flex",
72
+ alignItems: "center",
73
+ justifyContent: "center",
74
+ marginRight: 4,
75
+ opacity: 0.4,
76
+ cursor: "grab",
77
+ fontSize: 10,
78
+ },
79
+ contextMenu: {
80
+ position: "fixed",
81
+ zIndex: 50,
82
+ minWidth: 120,
83
+ background: "rgba(0,0,0,0.82)",
84
+ border: "1px solid rgba(255,255,255,0.16)",
85
+ borderRadius: 6,
86
+ overflow: "hidden",
87
+ boxShadow: "0 12px 32px rgba(0,0,0,0.45)",
88
+ backdropFilter: "blur(6px)",
89
+ WebkitBackdropFilter: "blur(6px)",
90
+ },
91
+ menuItem: {
92
+ width: "100%",
93
+ textAlign: "left",
94
+ padding: "6px 8px",
95
+ background: "transparent",
96
+ border: "none",
97
+ color: "rgba(255,255,255,0.9)",
98
+ font: "inherit",
99
+ cursor: "pointer",
100
+ },
101
+ menuItemDanger: {
102
+ color: "rgba(255,120,120,0.95)",
103
+ },
104
+ menuDivider: {
105
+ borderTop: "1px solid rgba(255,255,255,0.10)",
106
+ }
107
+ };
9
108
  if (!prefabData || !setPrefabData)
10
109
  return null;
11
110
  const handleContextMenu = (e, nodeId) => {
@@ -77,38 +176,36 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
77
176
  };
78
177
  // Drag and Drop
79
178
  const handleDragStart = (e, id) => {
80
- e.stopPropagation();
81
179
  if (id === prefabData.root.id) {
82
- e.preventDefault(); // Cannot drag root
180
+ e.preventDefault();
83
181
  return;
84
182
  }
85
- setDraggedId(id);
86
183
  e.dataTransfer.effectAllowed = "move";
184
+ e.dataTransfer.setData("text/plain", id);
185
+ setDraggedId(id);
186
+ };
187
+ const handleDragEnd = () => {
188
+ setDraggedId(null);
87
189
  };
88
190
  const handleDragOver = (e, targetId) => {
89
- e.preventDefault();
90
- e.stopPropagation();
91
191
  if (!draggedId || draggedId === targetId)
92
192
  return;
93
- // Check for cycles: target cannot be a descendant of dragged node
94
193
  const draggedNode = findNode(prefabData.root, draggedId);
95
194
  if (draggedNode && findNode(draggedNode, targetId))
96
195
  return;
196
+ e.preventDefault();
97
197
  e.dataTransfer.dropEffect = "move";
98
198
  };
99
199
  const handleDrop = (e, targetId) => {
100
- e.preventDefault();
101
- e.stopPropagation();
102
200
  if (!draggedId || draggedId === targetId)
103
201
  return;
202
+ e.preventDefault();
104
203
  setPrefabData(prev => {
105
204
  var _a;
106
205
  const newRoot = JSON.parse(JSON.stringify(prev.root));
107
- // Check cycle again on the fresh tree
108
- const draggedNodeRef = findNode(newRoot, draggedId);
109
- if (draggedNodeRef && findNode(draggedNodeRef, targetId))
206
+ const draggedNode = findNode(newRoot, draggedId);
207
+ if (draggedNode && findNode(draggedNode, targetId))
110
208
  return prev;
111
- // Remove from old parent
112
209
  const parent = findParent(newRoot, draggedId);
113
210
  if (!parent)
114
211
  return prev;
@@ -116,7 +213,6 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
116
213
  if (!nodeToMove)
117
214
  return prev;
118
215
  parent.children = parent.children.filter(c => c.id !== draggedId);
119
- // Add to new parent
120
216
  const target = findNode(newRoot, targetId);
121
217
  if (target) {
122
218
  target.children = target.children || [];
@@ -132,9 +228,39 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
132
228
  const isSelected = node.id === selectedId;
133
229
  const isCollapsed = collapsedIds.has(node.id);
134
230
  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));
231
+ return (_jsxs("div", { children: [_jsxs("div", { style: Object.assign(Object.assign(Object.assign({}, styles.row), (isSelected ? styles.rowSelected : null)), { paddingLeft: `${depth * 10 + 6}px`, cursor: node.id !== prefabData.root.id ? "grab" : "pointer" }), draggable: node.id !== prefabData.root.id, onClick: (e) => { e.stopPropagation(); setSelectedId(node.id); }, onContextMenu: (e) => handleContextMenu(e, node.id), onDragStart: (e) => handleDragStart(e, node.id), onDragEnd: handleDragEnd, onDragOver: (e) => handleDragOver(e, node.id), onDrop: (e) => handleDrop(e, node.id), onPointerEnter: (e) => {
232
+ if (!isSelected)
233
+ e.currentTarget.style.background = "rgba(255,255,255,0.06)";
234
+ }, onPointerLeave: (e) => {
235
+ if (!isSelected)
236
+ e.currentTarget.style.background = "transparent";
237
+ }, children: [_jsx("span", { style: Object.assign(Object.assign({}, styles.chevron), { visibility: hasChildren ? 'visible' : 'hidden' }), onClick: (e) => hasChildren && toggleCollapse(e, node.id), onPointerEnter: (e) => {
238
+ e.currentTarget.style.opacity = "0.9";
239
+ }, onPointerLeave: (e) => {
240
+ e.currentTarget.style.opacity = "0.55";
241
+ }, children: isCollapsed ? '▶' : '▼' }), node.id !== prefabData.root.id && (_jsx("span", { style: styles.dragHandle, onPointerEnter: (e) => {
242
+ e.currentTarget.style.opacity = "0.9";
243
+ }, onPointerLeave: (e) => {
244
+ e.currentTarget.style.opacity = "0.4";
245
+ }, children: "\u22EE\u22EE" })), _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
246
  };
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" })] }))] }))] }));
247
+ 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) => {
248
+ e.currentTarget.style.background = "rgba(255,255,255,0.08)";
249
+ }, onPointerLeave: (e) => {
250
+ e.currentTarget.style.background = "rgba(255,255,255,0.05)";
251
+ }, 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) => {
252
+ e.currentTarget.style.background = "rgba(255,255,255,0.08)";
253
+ }, onPointerLeave: (e) => {
254
+ e.currentTarget.style.background = "transparent";
255
+ }, 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) => {
256
+ e.currentTarget.style.background = "rgba(255,255,255,0.08)";
257
+ }, onPointerLeave: (e) => {
258
+ e.currentTarget.style.background = "transparent";
259
+ }, children: "Duplicate" }), _jsx("button", { style: Object.assign(Object.assign({}, styles.menuItem), styles.menuItemDanger), onClick: () => handleDelete(contextMenu.nodeId), onPointerEnter: (e) => {
260
+ e.currentTarget.style.background = "rgba(255,255,255,0.08)";
261
+ }, onPointerLeave: (e) => {
262
+ e.currentTarget.style.background = "transparent";
263
+ }, children: "Delete" })] }))] }))] }));
138
264
  }
139
265
  // --- Helpers ---
140
266
  function findNode(root, id) {