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.
@@ -15,6 +15,106 @@ 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
+ dragHandle: {
78
+ width: 14,
79
+ height: 14,
80
+ display: "flex",
81
+ alignItems: "center",
82
+ justifyContent: "center",
83
+ marginRight: 4,
84
+ opacity: 0.4,
85
+ cursor: "grab",
86
+ fontSize: 10,
87
+ },
88
+ contextMenu: {
89
+ position: "fixed",
90
+ zIndex: 50,
91
+ minWidth: 120,
92
+ background: "rgba(0,0,0,0.82)",
93
+ border: "1px solid rgba(255,255,255,0.16)",
94
+ borderRadius: 6,
95
+ overflow: "hidden",
96
+ boxShadow: "0 12px 32px rgba(0,0,0,0.45)",
97
+ backdropFilter: "blur(6px)",
98
+ WebkitBackdropFilter: "blur(6px)",
99
+ },
100
+ menuItem: {
101
+ width: "100%",
102
+ textAlign: "left",
103
+ padding: "6px 8px",
104
+ background: "transparent",
105
+ border: "none",
106
+ color: "rgba(255,255,255,0.9)",
107
+ font: "inherit",
108
+ cursor: "pointer",
109
+ },
110
+ menuItemDanger: {
111
+ color: "rgba(255,120,120,0.95)",
112
+ },
113
+ menuDivider: {
114
+ borderTop: "1px solid rgba(255,255,255,0.10)",
115
+ }
116
+ };
117
+
18
118
  if (!prefabData || !setPrefabData) return null;
19
119
 
20
120
  const handleContextMenu = (e: MouseEvent, nodeId: string) => {
@@ -90,40 +190,38 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
90
190
 
91
191
  // Drag and Drop
92
192
  const handleDragStart = (e: React.DragEvent, id: string) => {
93
- e.stopPropagation();
94
193
  if (id === prefabData.root.id) {
95
- e.preventDefault(); // Cannot drag root
194
+ e.preventDefault();
96
195
  return;
97
196
  }
98
- setDraggedId(id);
99
197
  e.dataTransfer.effectAllowed = "move";
198
+ e.dataTransfer.setData("text/plain", id);
199
+ setDraggedId(id);
200
+ };
201
+
202
+ const handleDragEnd = () => {
203
+ setDraggedId(null);
100
204
  };
101
205
 
102
206
  const handleDragOver = (e: React.DragEvent, targetId: string) => {
103
- e.preventDefault();
104
- e.stopPropagation();
105
207
  if (!draggedId || draggedId === targetId) return;
106
-
107
- // Check for cycles: target cannot be a descendant of dragged node
108
208
  const draggedNode = findNode(prefabData.root, draggedId);
109
209
  if (draggedNode && findNode(draggedNode, targetId)) return;
110
210
 
211
+ e.preventDefault();
111
212
  e.dataTransfer.dropEffect = "move";
112
213
  };
113
214
 
114
215
  const handleDrop = (e: React.DragEvent, targetId: string) => {
115
- e.preventDefault();
116
- e.stopPropagation();
117
216
  if (!draggedId || draggedId === targetId) return;
118
217
 
218
+ e.preventDefault();
219
+
119
220
  setPrefabData(prev => {
120
221
  const newRoot = JSON.parse(JSON.stringify(prev.root));
222
+ const draggedNode = findNode(newRoot, draggedId);
223
+ if (draggedNode && findNode(draggedNode, targetId)) return prev;
121
224
 
122
- // Check cycle again on the fresh tree
123
- const draggedNodeRef = findNode(newRoot, draggedId);
124
- if (draggedNodeRef && findNode(draggedNodeRef, targetId)) return prev;
125
-
126
- // Remove from old parent
127
225
  const parent = findParent(newRoot, draggedId);
128
226
  if (!parent) return prev;
129
227
 
@@ -132,7 +230,6 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
132
230
 
133
231
  parent.children = parent.children!.filter(c => c.id !== draggedId);
134
232
 
135
- // Add to new parent
136
233
  const target = findNode(newRoot, targetId);
137
234
  if (target) {
138
235
  target.children = target.children || [];
@@ -152,24 +249,57 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
152
249
  const hasChildren = node.children && node.children.length > 0;
153
250
 
154
251
  return (
155
- <div key={node.id} className="select-none">
252
+ <div key={node.id}>
156
253
  <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` }}
254
+ style={{
255
+ ...styles.row,
256
+ ...(isSelected ? styles.rowSelected : null),
257
+ paddingLeft: `${depth * 10 + 6}px`,
258
+ cursor: node.id !== prefabData.root.id ? "grab" : "pointer",
259
+ }}
260
+ draggable={node.id !== prefabData.root.id}
159
261
  onClick={(e) => { e.stopPropagation(); setSelectedId(node.id); }}
160
262
  onContextMenu={(e) => handleContextMenu(e, node.id)}
161
- draggable={node.id !== prefabData.root.id}
162
263
  onDragStart={(e) => handleDragStart(e, node.id)}
264
+ onDragEnd={handleDragEnd}
163
265
  onDragOver={(e) => handleDragOver(e, node.id)}
164
266
  onDrop={(e) => handleDrop(e, node.id)}
267
+ onPointerEnter={(e) => {
268
+ if (!isSelected) (e.currentTarget as HTMLDivElement).style.background = "rgba(255,255,255,0.06)";
269
+ }}
270
+ onPointerLeave={(e) => {
271
+ if (!isSelected) (e.currentTarget as HTMLDivElement).style.background = "transparent";
272
+ }}
165
273
  >
166
274
  <span
167
- className={`mr-0.5 w-3 text-center text-cyan-400/50 hover:text-cyan-400 cursor-pointer text-[8px] ${hasChildren ? '' : 'invisible'}`}
275
+ style={{
276
+ ...styles.chevron,
277
+ visibility: hasChildren ? 'visible' : 'hidden',
278
+ }}
168
279
  onClick={(e) => hasChildren && toggleCollapse(e, node.id)}
280
+ onPointerEnter={(e) => {
281
+ (e.currentTarget as HTMLSpanElement).style.opacity = "0.9";
282
+ }}
283
+ onPointerLeave={(e) => {
284
+ (e.currentTarget as HTMLSpanElement).style.opacity = "0.55";
285
+ }}
169
286
  >
170
287
  {isCollapsed ? '▶' : '▼'}
171
288
  </span>
172
- <span className="text-[10px] truncate font-mono text-cyan-300">
289
+ {node.id !== prefabData.root.id && (
290
+ <span
291
+ style={styles.dragHandle}
292
+ onPointerEnter={(e) => {
293
+ (e.currentTarget as HTMLSpanElement).style.opacity = "0.9";
294
+ }}
295
+ onPointerLeave={(e) => {
296
+ (e.currentTarget as HTMLSpanElement).style.opacity = "0.4";
297
+ }}
298
+ >
299
+ ⋮⋮
300
+ </span>
301
+ )}
302
+ <span style={styles.idText}>
173
303
  {node.id}
174
304
  </span>
175
305
  </div>
@@ -184,16 +314,28 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
184
314
 
185
315
  return (
186
316
  <>
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}>
317
+ <div
318
+ style={{
319
+ ...styles.panel,
320
+ width: isTreeCollapsed ? 'auto' : '14rem',
321
+ }}
322
+ onClick={closeContextMenu}
323
+ >
188
324
  <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"
325
+ style={styles.panelHeader}
190
326
  onClick={(e) => { e.stopPropagation(); setIsTreeCollapsed(!isTreeCollapsed); }}
327
+ onPointerEnter={(e) => {
328
+ (e.currentTarget as HTMLDivElement).style.background = "rgba(255,255,255,0.08)";
329
+ }}
330
+ onPointerLeave={(e) => {
331
+ (e.currentTarget as HTMLDivElement).style.background = "rgba(255,255,255,0.05)";
332
+ }}
191
333
  >
192
334
  <span>Prefab Graph</span>
193
- <span className="text-[8px]">{isTreeCollapsed ? '▶' : '◀'}</span>
335
+ <span style={{ fontSize: 10, opacity: 0.8 }}>{isTreeCollapsed ? '▶' : '◀'}</span>
194
336
  </div>
195
337
  {!isTreeCollapsed && (
196
- <div className="flex-1 py-0.5">
338
+ <div style={{ ...styles.scroll, padding: 2 }}>
197
339
  {renderNode(prefabData.root)}
198
340
  </div>
199
341
  )}
@@ -201,28 +343,49 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
201
343
 
202
344
  {contextMenu && (
203
345
  <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 }}
346
+ style={{
347
+ ...styles.contextMenu,
348
+ top: contextMenu.y,
349
+ left: contextMenu.x,
350
+ }}
206
351
  onClick={(e) => e.stopPropagation()}
207
352
  onPointerLeave={closeContextMenu}
208
353
  >
209
354
  <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"
355
+ style={{ ...styles.menuItem, ...styles.menuDivider }}
211
356
  onClick={() => handleAddChild(contextMenu.nodeId)}
357
+ onPointerEnter={(e) => {
358
+ (e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
359
+ }}
360
+ onPointerLeave={(e) => {
361
+ (e.currentTarget as HTMLButtonElement).style.background = "transparent";
362
+ }}
212
363
  >
213
364
  Add Child
214
365
  </button>
215
366
  {contextMenu.nodeId !== prefabData.root.id && (
216
367
  <>
217
368
  <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"
369
+ style={{ ...styles.menuItem, ...styles.menuDivider }}
219
370
  onClick={() => handleDuplicate(contextMenu.nodeId)}
371
+ onPointerEnter={(e) => {
372
+ (e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
373
+ }}
374
+ onPointerLeave={(e) => {
375
+ (e.currentTarget as HTMLButtonElement).style.background = "transparent";
376
+ }}
220
377
  >
221
378
  Duplicate
222
379
  </button>
223
380
  <button
224
- className="w-full text-left px-2 py-1 hover:bg-red-500/20 text-[10px] text-red-400 font-mono"
381
+ style={{ ...styles.menuItem, ...styles.menuItemDanger }}
225
382
  onClick={() => handleDelete(contextMenu.nodeId)}
383
+ onPointerEnter={(e) => {
384
+ (e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
385
+ }}
386
+ onPointerLeave={(e) => {
387
+ (e.currentTarget as HTMLButtonElement).style.background = "transparent";
388
+ }}
226
389
  >
227
390
  Delete
228
391
  </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,82 +236,47 @@ 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>
107
- </div>
108
-
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">
112
- {["translate", "rotate", "scale"].map(mode => (
113
- <button
114
- key={mode}
115
- 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'}`}
117
- >
118
- {mode[0].toUpperCase()}
119
- </button>
120
- ))}
121
- </div>
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>
122
257
  </div>
123
258
 
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
- })} */}
151
-
152
259
  {node.components && Object.entries(node.components).map(([key, comp]: [string, any]) => {
153
260
  if (!comp) return null;
154
261
  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}
262
+ if (!componentDef) return <div key={key} style={{ padding: '4px 0', color: 'rgba(255,120,120,0.95)', fontSize: 11 }}>
263
+ Unknown component type: {comp.type}
156
264
  <textarea defaultValue={JSON.stringify(comp)} />
157
265
  </div>;
158
266
 
159
267
  const EditorComp = componentDef.Editor;
160
268
  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>
269
+ <div key={key} style={{ padding: '0 2px' }}>
270
+ <div style={s.componentHeader}>
271
+ <span style={s.componentTitle}>{key}</span>
164
272
  <button
165
273
  onClick={() => updateNode(n => {
166
274
  const components = { ...n.components };
167
275
  delete components[key as keyof typeof components];
168
276
  return { ...n, components };
169
277
  })}
170
- className="text-[9px] text-red-400/60 hover:text-red-400"
278
+ style={s.smallDanger}
279
+ title="Remove component"
171
280
  >
172
281
 
173
282
  </button>
@@ -186,6 +295,8 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
186
295
  }
187
296
  }))}
188
297
  basePath={basePath}
298
+ transformMode={transformMode}
299
+ setTransformMode={setTransformMode}
189
300
  />
190
301
  ) : null}
191
302
  </div>
@@ -193,11 +304,11 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
193
304
  })}
194
305
 
195
306
  {/* 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">
307
+ <div style={{ ...s.section, borderBottom: 'none', paddingBottom: 0 }}>
308
+ <label style={s.label}>Add Component</label>
309
+ <div style={{ display: 'flex', gap: 6 }}>
199
310
  <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"
311
+ style={s.select}
201
312
  value={addComponentType}
202
313
  onChange={e => setAddComponentType(e.target.value)}
203
314
  >
@@ -206,7 +317,10 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
206
317
  ))}
207
318
  </select>
208
319
  <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"
320
+ style={{
321
+ ...s.addButton,
322
+ ...(!addComponentType ? s.disabled : null),
323
+ }}
210
324
  disabled={!addComponentType}
211
325
  onClick={() => {
212
326
  if (!addComponentType) return;
@@ -222,6 +336,14 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
222
336
  }));
223
337
  }
224
338
  }}
339
+ onPointerEnter={(e) => {
340
+ if (!addComponentType) return;
341
+ (e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.12)';
342
+ }}
343
+ onPointerLeave={(e) => {
344
+ if (!addComponentType) return;
345
+ (e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.08)';
346
+ }}
225
347
  >
226
348
  +
227
349
  </button>