react-three-game 0.0.14 → 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.
@@ -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) => {
@@ -39,8 +128,6 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
39
128
  const handleAddChild = (parentId: string) => {
40
129
  const newNode: GameObject = {
41
130
  id: crypto.randomUUID(),
42
- enabled: true,
43
- visible: true,
44
131
  components: {
45
132
  transform: {
46
133
  type: "Transform",
@@ -154,24 +241,42 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
154
241
  const hasChildren = node.children && node.children.length > 0;
155
242
 
156
243
  return (
157
- <div key={node.id} className="select-none">
244
+ <div key={node.id}>
158
245
  <div
159
- 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'}`}
160
- style={{ paddingLeft: `${depth * 8 + 4}px` }}
246
+ style={{
247
+ ...styles.row,
248
+ ...(isSelected ? styles.rowSelected : null),
249
+ paddingLeft: `${depth * 10 + 6}px`,
250
+ }}
161
251
  onClick={(e) => { e.stopPropagation(); setSelectedId(node.id); }}
162
252
  onContextMenu={(e) => handleContextMenu(e, node.id)}
163
253
  draggable={node.id !== prefabData.root.id}
164
254
  onDragStart={(e) => handleDragStart(e, node.id)}
165
255
  onDragOver={(e) => handleDragOver(e, node.id)}
166
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
+ }}
167
263
  >
168
264
  <span
169
- 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
+ }}
170
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
+ }}
171
276
  >
172
277
  {isCollapsed ? '▶' : '▼'}
173
278
  </span>
174
- <span className="text-[10px] truncate font-mono text-cyan-300">
279
+ <span style={styles.idText}>
175
280
  {node.id}
176
281
  </span>
177
282
  </div>
@@ -186,16 +291,28 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
186
291
 
187
292
  return (
188
293
  <>
189
- <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
+ >
190
301
  <div
191
- 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}
192
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
+ }}
193
310
  >
194
311
  <span>Prefab Graph</span>
195
- <span className="text-[8px]">{isTreeCollapsed ? '▶' : '◀'}</span>
312
+ <span style={{ fontSize: 10, opacity: 0.8 }}>{isTreeCollapsed ? '▶' : '◀'}</span>
196
313
  </div>
197
314
  {!isTreeCollapsed && (
198
- <div className="flex-1 py-0.5">
315
+ <div style={{ ...styles.scroll, padding: 2 }}>
199
316
  {renderNode(prefabData.root)}
200
317
  </div>
201
318
  )}
@@ -203,28 +320,49 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
203
320
 
204
321
  {contextMenu && (
205
322
  <div
206
- className="fixed bg-black/90 backdrop-blur-sm border border-cyan-500/40 z-50 min-w-[100px]"
207
- style={{ top: contextMenu.y, left: contextMenu.x }}
323
+ style={{
324
+ ...styles.contextMenu,
325
+ top: contextMenu.y,
326
+ left: contextMenu.x,
327
+ }}
208
328
  onClick={(e) => e.stopPropagation()}
209
329
  onPointerLeave={closeContextMenu}
210
330
  >
211
331
  <button
212
- 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 }}
213
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
+ }}
214
340
  >
215
341
  Add Child
216
342
  </button>
217
343
  {contextMenu.nodeId !== prefabData.root.id && (
218
344
  <>
219
345
  <button
220
- 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 }}
221
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
+ }}
222
354
  >
223
355
  Duplicate
224
356
  </button>
225
357
  <button
226
- 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 }}
227
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
+ }}
228
366
  >
229
367
  Delete
230
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>