react-three-game 0.0.17 → 0.0.19

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.
Files changed (39) hide show
  1. package/.github/copilot-instructions.md +54 -183
  2. package/README.md +69 -214
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.js +3 -0
  5. package/dist/tools/prefabeditor/EditorTree.d.ts +2 -4
  6. package/dist/tools/prefabeditor/EditorTree.js +20 -194
  7. package/dist/tools/prefabeditor/EditorUI.js +43 -224
  8. package/dist/tools/prefabeditor/InstanceProvider.d.ts +4 -4
  9. package/dist/tools/prefabeditor/InstanceProvider.js +21 -13
  10. package/dist/tools/prefabeditor/PrefabEditor.js +33 -99
  11. package/dist/tools/prefabeditor/PrefabRoot.d.ts +0 -1
  12. package/dist/tools/prefabeditor/PrefabRoot.js +33 -50
  13. package/dist/tools/prefabeditor/components/DirectionalLightComponent.d.ts +3 -0
  14. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +102 -0
  15. package/dist/tools/prefabeditor/components/ModelComponent.js +12 -4
  16. package/dist/tools/prefabeditor/components/SpotLightComponent.js +10 -5
  17. package/dist/tools/prefabeditor/components/index.js +2 -0
  18. package/dist/tools/prefabeditor/hooks/useModelLoader.d.ts +10 -0
  19. package/dist/tools/prefabeditor/hooks/useModelLoader.js +40 -0
  20. package/dist/tools/prefabeditor/styles.d.ts +1809 -0
  21. package/dist/tools/prefabeditor/styles.js +168 -0
  22. package/dist/tools/prefabeditor/types.d.ts +3 -14
  23. package/dist/tools/prefabeditor/types.js +0 -1
  24. package/dist/tools/prefabeditor/utils.d.ts +19 -0
  25. package/dist/tools/prefabeditor/utils.js +72 -0
  26. package/package.json +3 -3
  27. package/src/index.ts +5 -1
  28. package/src/tools/prefabeditor/EditorTree.tsx +38 -270
  29. package/src/tools/prefabeditor/EditorUI.tsx +105 -322
  30. package/src/tools/prefabeditor/InstanceProvider.tsx +43 -32
  31. package/src/tools/prefabeditor/PrefabEditor.tsx +40 -151
  32. package/src/tools/prefabeditor/PrefabRoot.tsx +41 -73
  33. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +317 -0
  34. package/src/tools/prefabeditor/components/ModelComponent.tsx +14 -4
  35. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +27 -7
  36. package/src/tools/prefabeditor/components/index.ts +2 -0
  37. package/src/tools/prefabeditor/styles.ts +195 -0
  38. package/src/tools/prefabeditor/types.ts +4 -12
  39. package/src/tools/prefabeditor/utils.ts +80 -0
@@ -1,7 +1,7 @@
1
1
  import React, { createContext, useContext, useMemo, useRef, useState, useEffect, useCallback } from "react";
2
2
  import { Merged } from '@react-three/drei';
3
- import * as THREE from 'three';
4
3
  import { InstancedRigidBodies } from "@react-three/rapier";
4
+ import { Mesh, Matrix4, Object3D, Group } from "three";
5
5
 
6
6
  // --- Types ---
7
7
  export type InstanceData = {
@@ -13,7 +13,8 @@ export type InstanceData = {
13
13
  physics?: { type: 'dynamic' | 'fixed' };
14
14
  };
15
15
 
16
- function arrayEquals(a: number[], b: number[]) {
16
+ // Helper functions for comparison
17
+ function arrayEquals(a: number[], b: number[]): boolean {
17
18
  if (a === b) return true;
18
19
  if (a.length !== b.length) return false;
19
20
  for (let i = 0; i < a.length; i++) {
@@ -22,7 +23,7 @@ function arrayEquals(a: number[], b: number[]) {
22
23
  return true;
23
24
  }
24
25
 
25
- function instanceEquals(a: InstanceData, b: InstanceData) {
26
+ function instanceEquals(a: InstanceData, b: InstanceData): boolean {
26
27
  return a.id === b.id &&
27
28
  a.meshPath === b.meshPath &&
28
29
  arrayEquals(a.position, b.position) &&
@@ -36,7 +37,7 @@ type GameInstanceContextType = {
36
37
  addInstance: (instance: InstanceData) => void;
37
38
  removeInstance: (id: string) => void;
38
39
  instances: InstanceData[];
39
- meshes: Record<string, THREE.Mesh>;
40
+ meshes: Record<string, Mesh>;
40
41
  instancesMap?: Record<string, React.ComponentType<any>>;
41
42
  modelParts?: Record<string, number>;
42
43
  };
@@ -44,13 +45,14 @@ const GameInstanceContext = createContext<GameInstanceContextType | null>(null);
44
45
 
45
46
  export function GameInstanceProvider({
46
47
  children,
47
- models
48
- , onSelect, registerRef
48
+ models,
49
+ onSelect,
50
+ registerRef
49
51
  }: {
50
52
  children: React.ReactNode,
51
- models: { [filename: string]: THREE.Object3D },
53
+ models: { [filename: string]: Object3D },
52
54
  onSelect?: (id: string | null) => void,
53
- registerRef?: (id: string, obj: THREE.Object3D | null) => void,
55
+ registerRef?: (id: string, obj: Object3D | null) => void,
54
56
  }) {
55
57
  const [instances, setInstances] = useState<InstanceData[]>([]);
56
58
 
@@ -58,6 +60,7 @@ export function GameInstanceProvider({
58
60
  setInstances(prev => {
59
61
  const idx = prev.findIndex(i => i.id === instance.id);
60
62
  if (idx !== -1) {
63
+ // Update existing if changed
61
64
  if (instanceEquals(prev[idx], instance)) {
62
65
  return prev;
63
66
  }
@@ -65,6 +68,7 @@ export function GameInstanceProvider({
65
68
  copy[idx] = instance;
66
69
  return copy;
67
70
  }
71
+ // Add new
68
72
  return [...prev, instance];
69
73
  });
70
74
  }, []);
@@ -76,27 +80,26 @@ export function GameInstanceProvider({
76
80
  });
77
81
  }, []);
78
82
 
79
- // Flatten all model meshes once
83
+ // Flatten all model meshes once (models → flat mesh parts)
80
84
  const { flatMeshes, modelParts } = useMemo(() => {
81
- const flatMeshes: Record<string, THREE.Mesh> = {};
85
+ const flatMeshes: Record<string, Mesh> = {};
82
86
  const modelParts: Record<string, number> = {};
83
87
 
84
88
  Object.entries(models).forEach(([modelKey, model]) => {
85
89
  const root = model;
86
90
  root.updateWorldMatrix(false, true);
87
- const rootInverse = new THREE.Matrix4().copy(root.matrixWorld).invert();
91
+ const rootInverse = new Matrix4().copy(root.matrixWorld).invert();
88
92
 
89
93
  let partIndex = 0;
90
94
 
91
95
  root.traverse((obj: any) => {
92
96
  if (obj.isMesh) {
93
97
  const geom = obj.geometry.clone();
94
-
95
98
  const relativeTransform = obj.matrixWorld.clone().premultiply(rootInverse);
96
99
  geom.applyMatrix4(relativeTransform);
97
100
 
98
101
  const partKey = `${modelKey}__${partIndex}`;
99
- flatMeshes[partKey] = new THREE.Mesh(geom, obj.material);
102
+ flatMeshes[partKey] = new Mesh(geom, obj.material);
100
103
  partIndex++;
101
104
  }
102
105
  });
@@ -106,7 +109,7 @@ export function GameInstanceProvider({
106
109
  return { flatMeshes, modelParts };
107
110
  }, [models]);
108
111
 
109
- // Group instances by meshPath + physics type
112
+ // Group instances by meshPath + physics type for batch rendering
110
113
  const grouped = useMemo(() => {
111
114
  const groups: Record<string, { physicsType: string, instances: InstanceData[] }> = {};
112
115
  for (const inst of instances) {
@@ -128,10 +131,10 @@ export function GameInstanceProvider({
128
131
  modelParts
129
132
  }}
130
133
  >
131
- {/* 1) Normal prefab hierarchy: NOT inside any <Merged> */}
134
+ {/* Render normal prefab hierarchy (non-instanced objects) */}
132
135
  {children}
133
136
 
134
- {/* 2) Physics instanced groups: no <Merged>, just InstancedRigidBodies */}
137
+ {/* Render physics-enabled instanced groups using InstancedRigidBodies */}
135
138
  {Object.entries(grouped).map(([key, group]) => {
136
139
  if (group.physicsType === 'none') return null;
137
140
  const modelKey = group.instances[0].meshPath;
@@ -149,7 +152,7 @@ export function GameInstanceProvider({
149
152
  );
150
153
  })}
151
154
 
152
- {/* 3) Non-physics instanced visuals: own <Merged> per model */}
155
+ {/* Render non-physics instanced visuals using Merged (one per model type) */}
153
156
  {Object.entries(grouped).map(([key, group]) => {
154
157
  if (group.physicsType !== 'none') return null;
155
158
 
@@ -157,8 +160,8 @@ export function GameInstanceProvider({
157
160
  const partCount = modelParts[modelKey] || 0;
158
161
  if (partCount === 0) return null;
159
162
 
160
- // Restrict meshes to just this model's parts for this Merged
161
- const meshesForModel: Record<string, THREE.Mesh> = {};
163
+ // Create mesh subset for this specific model
164
+ const meshesForModel: Record<string, Mesh> = {};
162
165
  for (let i = 0; i < partCount; i++) {
163
166
  const partKey = `${modelKey}__${i}`;
164
167
  meshesForModel[partKey] = flatMeshes[partKey];
@@ -188,7 +191,7 @@ export function GameInstanceProvider({
188
191
  );
189
192
  }
190
193
 
191
- // Physics instancing stays the same
194
+ // Render physics-enabled instances using InstancedRigidBodies
192
195
  function InstancedRigidGroup({
193
196
  group,
194
197
  modelKey,
@@ -198,7 +201,7 @@ function InstancedRigidGroup({
198
201
  group: { physicsType: string, instances: InstanceData[] },
199
202
  modelKey: string,
200
203
  partCount: number,
201
- flatMeshes: Record<string, THREE.Mesh>
204
+ flatMeshes: Record<string, Mesh>
202
205
  }) {
203
206
  const instances = useMemo(
204
207
  () => group.instances.map(inst => ({
@@ -232,24 +235,33 @@ function InstancedRigidGroup({
232
235
  );
233
236
  }
234
237
 
235
- // Non-physics instanced visuals: per-instance group using Merged's Instance components
238
+ // Render non-physics instances using Merged's per-instance groups
236
239
  function NonPhysicsInstancedGroup({
237
240
  modelKey,
238
241
  group,
239
242
  partCount,
240
- instancesMap
241
- , onSelect, registerRef
243
+ instancesMap,
244
+ onSelect,
245
+ registerRef
242
246
  }: {
243
247
  modelKey: string;
244
248
  group: { physicsType: string, instances: InstanceData[] };
245
249
  partCount: number;
246
250
  instancesMap: Record<string, React.ComponentType<any>>;
247
251
  onSelect?: (id: string | null) => void;
248
- registerRef?: (id: string, obj: THREE.Object3D | null) => void;
252
+ registerRef?: (id: string, obj: Object3D | null) => void;
249
253
  }) {
250
254
  const clickValid = useRef(false);
251
- const handlePointerDown = (e: any) => { e.stopPropagation(); clickValid.current = true; };
252
- const handlePointerMove = () => { if (clickValid.current) clickValid.current = false; };
255
+
256
+ const handlePointerDown = (e: any) => {
257
+ e.stopPropagation();
258
+ clickValid.current = true;
259
+ };
260
+
261
+ const handlePointerMove = () => {
262
+ if (clickValid.current) clickValid.current = false;
263
+ };
264
+
253
265
  const handlePointerUp = (e: any, id: string) => {
254
266
  if (clickValid.current) {
255
267
  e.stopPropagation();
@@ -263,7 +275,7 @@ function NonPhysicsInstancedGroup({
263
275
  {group.instances.map(inst => (
264
276
  <group
265
277
  key={inst.id}
266
- ref={(el) => { registerRef?.(inst.id, el as unknown as THREE.Object3D | null); }}
278
+ ref={(el) => { registerRef?.(inst.id, el as unknown as Object3D | null); }}
267
279
  position={inst.position}
268
280
  rotation={inst.rotation}
269
281
  scale={inst.scale}
@@ -283,8 +295,8 @@ function NonPhysicsInstancedGroup({
283
295
  }
284
296
 
285
297
 
286
- // --- GameInstance: just registers an instance, renders nothing ---
287
- export const GameInstance = React.forwardRef<THREE.Group, {
298
+ // GameInstance component: registers an instance for batch rendering (renders nothing itself)
299
+ export const GameInstance = React.forwardRef<Group, {
288
300
  id: string;
289
301
  modelUrl: string;
290
302
  position: [number, number, number];
@@ -320,7 +332,6 @@ export const GameInstance = React.forwardRef<THREE.Group, {
320
332
  };
321
333
  }, [addInstance, removeInstance, instance]);
322
334
 
323
-
324
- // No visual here – provider will render visuals for all instances
335
+ // No visual rendering - provider handles all instanced visuals
325
336
  return null;
326
337
  });
@@ -2,34 +2,29 @@
2
2
 
3
3
  import GameCanvas from "../../shared/GameCanvas";
4
4
  import { useState, useRef, useEffect } from "react";
5
- import { Group, } from "three";
6
- import { Prefab, } from "./types";
5
+ import { Prefab } from "./types";
7
6
  import PrefabRoot from "./PrefabRoot";
8
7
  import { Physics } from "@react-three/rapier";
9
8
  import EditorUI from "./EditorUI";
9
+ import { base, toolbar } from "./styles";
10
10
 
11
11
  const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }: { basePath?: string, initialPrefab?: Prefab, onPrefabChange?: (prefab: Prefab) => void, children?: React.ReactNode }) => {
12
12
  const [editMode, setEditMode] = useState(true);
13
13
  const [loadedPrefab, setLoadedPrefab] = useState<Prefab>(initialPrefab ?? {
14
- "id": "prefab-default",
15
- "name": "New Prefab",
16
- "root": {
17
- "id": "root",
18
- "components": {
19
- "transform": {
20
- "type": "Transform",
21
- "properties": {
22
- "position": [0, 0, 0],
23
- "rotation": [0, 0, 0],
24
- "scale": [1, 1, 1]
25
- }
14
+ id: "prefab-default",
15
+ name: "New Prefab",
16
+ root: {
17
+ id: "root",
18
+ components: {
19
+ transform: {
20
+ type: "Transform",
21
+ properties: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
26
22
  }
27
23
  }
28
24
  }
29
25
  });
30
26
  const [selectedId, setSelectedId] = useState<string | null>(null);
31
27
  const [transformMode, setTransformMode] = useState<"translate" | "rotate" | "scale">("translate");
32
- const prefabRef = useRef<Group>(null);
33
28
 
34
29
  // Sync internal state with external initialPrefab prop
35
30
  useEffect(() => {
@@ -52,15 +47,11 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }: { b
52
47
  <gridHelper args={[10, 10]} position={[0, -1, 0]} />
53
48
  <PrefabRoot
54
49
  data={loadedPrefab}
55
- ref={prefabRef}
56
-
57
- // props for edit mode
58
50
  editMode={editMode}
59
51
  onPrefabChange={updatePrefab}
60
52
  selectedId={selectedId}
61
53
  onSelect={setSelectedId}
62
54
  transformMode={transformMode}
63
- setTransformMode={setTransformMode}
64
55
  basePath={basePath}
65
56
  />
66
57
  {children}
@@ -98,189 +89,87 @@ const SaveDataPanel = ({
98
89
  }) => {
99
90
  const [history, setHistory] = useState<Prefab[]>([currentData]);
100
91
  const [historyIndex, setHistoryIndex] = useState(0);
101
- const throttleTimeoutRef = useRef<NodeJS.Timeout | null>(null);
102
- const lastSavedDataRef = useRef<string>(JSON.stringify(currentData));
92
+ const throttleRef = useRef<NodeJS.Timeout | null>(null);
93
+ const lastDataRef = useRef<string>(JSON.stringify(currentData));
103
94
 
104
- // Define undo/redo handlers
105
- const handleUndo = () => {
95
+ const undo = () => {
106
96
  if (historyIndex > 0) {
107
97
  const newIndex = historyIndex - 1;
108
98
  setHistoryIndex(newIndex);
109
- lastSavedDataRef.current = JSON.stringify(history[newIndex]);
99
+ lastDataRef.current = JSON.stringify(history[newIndex]);
110
100
  onDataChange(history[newIndex]);
111
101
  }
112
102
  };
113
103
 
114
- const handleRedo = () => {
104
+ const redo = () => {
115
105
  if (historyIndex < history.length - 1) {
116
106
  const newIndex = historyIndex + 1;
117
107
  setHistoryIndex(newIndex);
118
- lastSavedDataRef.current = JSON.stringify(history[newIndex]);
108
+ lastDataRef.current = JSON.stringify(history[newIndex]);
119
109
  onDataChange(history[newIndex]);
120
110
  }
121
111
  };
122
112
 
123
- // Keyboard shortcuts for undo/redo
124
113
  useEffect(() => {
125
114
  const handleKeyDown = (e: KeyboardEvent) => {
126
- // Undo: Ctrl+Z (Cmd+Z on Mac)
127
115
  if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
128
116
  e.preventDefault();
129
- handleUndo();
130
- }
131
- // Redo: Ctrl+Shift+Z or Ctrl+Y (Cmd+Shift+Z or Cmd+Y on Mac)
132
- else if ((e.ctrlKey || e.metaKey) && (e.shiftKey && e.key === 'z' || e.key === 'y')) {
117
+ undo();
118
+ } else if ((e.ctrlKey || e.metaKey) && (e.shiftKey && e.key === 'z' || e.key === 'y')) {
133
119
  e.preventDefault();
134
- handleRedo();
120
+ redo();
135
121
  }
136
122
  };
137
-
138
123
  window.addEventListener('keydown', handleKeyDown);
139
124
  return () => window.removeEventListener('keydown', handleKeyDown);
140
125
  }, [historyIndex, history]);
141
126
 
142
- // Throttled history update when currentData changes
143
127
  useEffect(() => {
144
- const currentDataStr = JSON.stringify(currentData);
145
-
146
- // Skip if data hasn't actually changed
147
- if (currentDataStr === lastSavedDataRef.current) {
148
- return;
149
- }
128
+ const currentStr = JSON.stringify(currentData);
129
+ if (currentStr === lastDataRef.current) return;
150
130
 
151
- // Clear existing throttle timeout
152
- if (throttleTimeoutRef.current) {
153
- clearTimeout(throttleTimeoutRef.current);
154
- }
155
-
156
- // Set new throttled update
157
- throttleTimeoutRef.current = setTimeout(() => {
158
- lastSavedDataRef.current = currentDataStr;
131
+ if (throttleRef.current) clearTimeout(throttleRef.current);
159
132
 
133
+ throttleRef.current = setTimeout(() => {
134
+ lastDataRef.current = currentStr;
160
135
  setHistory(prev => {
161
- // Slice history at current index (discard future states)
162
- const newHistory = prev.slice(0, historyIndex + 1);
163
- // Add new state
164
- newHistory.push(currentData);
165
- // Limit history size to 50 states
166
- if (newHistory.length > 50) {
167
- newHistory.shift();
168
- return newHistory;
169
- }
170
- return newHistory;
136
+ const newHistory = [...prev.slice(0, historyIndex + 1), currentData];
137
+ return newHistory.length > 50 ? newHistory.slice(1) : newHistory;
171
138
  });
172
-
173
- setHistoryIndex(prev => {
174
- const newHistory = history.slice(0, prev + 1);
175
- newHistory.push(currentData);
176
- return Math.min(newHistory.length - 1, 49);
177
- });
178
- }, 500); // 500ms throttle
139
+ setHistoryIndex(prev => Math.min(prev + 1, 49));
140
+ }, 500);
179
141
 
180
142
  return () => {
181
- if (throttleTimeoutRef.current) {
182
- clearTimeout(throttleTimeoutRef.current);
183
- }
143
+ if (throttleRef.current) clearTimeout(throttleRef.current);
184
144
  };
185
- }, [currentData, historyIndex, history]);
145
+ }, [currentData]);
186
146
 
187
147
  const handleLoad = async () => {
188
148
  const prefab = await loadJson();
189
149
  if (prefab) {
190
150
  onDataChange(prefab);
191
- // Reset history when loading new file
192
151
  setHistory([prefab]);
193
152
  setHistoryIndex(0);
194
- lastSavedDataRef.current = JSON.stringify(prefab);
153
+ lastDataRef.current = JSON.stringify(prefab);
195
154
  }
196
155
  };
197
156
 
198
157
  const canUndo = historyIndex > 0;
199
158
  const canRedo = historyIndex < history.length - 1;
200
159
 
201
- return <div style={{
202
- position: "absolute",
203
- top: 8,
204
- left: "50%",
205
- transform: "translateX(-50%)",
206
- display: "flex",
207
- alignItems: "center",
208
- gap: 6,
209
- padding: "2px 4px",
210
- background: "rgba(0,0,0,0.55)",
211
- border: "1px solid rgba(255,255,255,0.12)",
212
- borderRadius: 4,
213
- color: "rgba(255,255,255,0.9)",
214
- fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
215
- fontSize: 11,
216
- lineHeight: 1,
217
- WebkitUserSelect: "none",
218
- userSelect: "none",
219
- }}>
220
- <PanelButton onClick={() => onEditModeChange(!editMode)}>
160
+ return <div style={toolbar.panel}>
161
+ <button style={base.btn} onClick={() => onEditModeChange(!editMode)}>
221
162
  {editMode ? "▶" : "⏸"}
222
- </PanelButton>
223
-
224
- <span style={{ opacity: 0.35 }}>|</span>
225
-
226
- <PanelButton onClick={handleUndo} disabled={!canUndo} title="Undo (Ctrl+Z)">
227
-
228
- </PanelButton>
229
-
230
- <PanelButton onClick={handleRedo} disabled={!canRedo} title="Redo (Ctrl+Shift+Z)">
231
-
232
- </PanelButton>
233
-
234
- <span style={{ opacity: 0.35 }}>|</span>
235
-
236
- <PanelButton onClick={handleLoad} title="Load JSON">
237
- 📥
238
- </PanelButton>
239
-
240
- <PanelButton onClick={() => saveJson(currentData, "prefab")} title="Save JSON">
241
- 💾
242
- </PanelButton>
163
+ </button>
164
+ <div style={toolbar.divider} />
165
+ <button style={{ ...base.btn, ...(canUndo ? {} : toolbar.disabled) }} onClick={undo} disabled={!canUndo}>↶</button>
166
+ <button style={{ ...base.btn, ...(canRedo ? {} : toolbar.disabled) }} onClick={redo} disabled={!canRedo}>↷</button>
167
+ <div style={toolbar.divider} />
168
+ <button style={base.btn} onClick={handleLoad}>📥</button>
169
+ <button style={base.btn} onClick={() => saveJson(currentData, "prefab")}>💾</button>
243
170
  </div>;
244
171
  };
245
172
 
246
- const PanelButton = ({
247
- onClick,
248
- disabled,
249
- title,
250
- children
251
- }: {
252
- onClick: () => void;
253
- disabled?: boolean;
254
- title?: string;
255
- children: React.ReactNode;
256
- }) => {
257
- return <button
258
- style={{
259
- padding: "2px 6px",
260
- font: "inherit",
261
- background: "transparent",
262
- color: disabled ? "rgba(255,255,255,0.3)" : "inherit",
263
- border: "1px solid rgba(255,255,255,0.18)",
264
- borderRadius: 3,
265
- cursor: disabled ? "not-allowed" : "pointer",
266
- opacity: disabled ? 0.5 : 1,
267
- }}
268
- onClick={onClick}
269
- disabled={disabled}
270
- title={title}
271
- onPointerEnter={(e) => {
272
- if (!disabled) {
273
- (e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
274
- }
275
- }}
276
- onPointerLeave={(e) => {
277
- (e.currentTarget as HTMLButtonElement).style.background = "transparent";
278
- }}
279
- >
280
- {children}
281
- </button>;
282
- };
283
-
284
173
  const saveJson = (data: any, filename: string) => {
285
174
  const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
286
175
  const downloadAnchorNode = document.createElement('a');