@gamepark/react-game 7.5.7 → 7.5.9
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/dist/components/material/GameTable/DevToolsHub.js +644 -43
- package/dist/components/material/GameTable/DevToolsHub.js.map +1 -1
- package/dist/components/material/Wheel/WheelContent.d.ts +13 -0
- package/dist/components/material/Wheel/WheelContent.js +37 -0
- package/dist/components/material/Wheel/WheelContent.js.map +1 -0
- package/dist/components/material/animations/CreateItemAnimations.js +36 -52
- package/dist/components/material/animations/CreateItemAnimations.js.map +1 -1
- package/dist/css/backgroundCss.js +3 -3
- package/dist/css/cursorCss.js +6 -6
- package/dist/css/fadeIn.js +6 -6
- package/dist/css/shineEffect.js +28 -28
- package/dist/css/transformCss.js +4 -4
- package/dist/hooks/useFailures.d.ts +1 -0
- package/dist/hooks/useFailures.js +11 -0
- package/dist/hooks/useFailures.js.map +1 -0
- package/dist/hooks/useWebP.d.ts +1 -0
- package/dist/hooks/useWebP.js +13 -0
- package/dist/hooks/useWebP.js.map +1 -0
- package/package.json +1 -1
- package/dist/components/material/Dices/OctahedralDiceDescription.d.ts +0 -48
- package/dist/components/material/Dices/OctahedralDiceDescription.js +0 -142
- package/dist/components/material/Dices/OctahedralDiceDescription.js.map +0 -1
|
@@ -1,14 +1,55 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@emotion/react/jsx-runtime";
|
|
2
2
|
/** @jsxImportSource @emotion/react */
|
|
3
3
|
import { css, keyframes } from '@emotion/react';
|
|
4
|
-
import { useCallback, useRef, useState } from 'react';
|
|
4
|
+
import { useCallback, useContext, useRef, useState } from 'react';
|
|
5
5
|
import { createPortal } from 'react-dom';
|
|
6
6
|
import { useGame } from '../../../hooks/useGame';
|
|
7
7
|
import { usePlayerId, usePlayerIds } from '../../../hooks/usePlayerId';
|
|
8
|
+
import { gameContext } from '../../GameProvider/GameContext';
|
|
8
9
|
const GP_PRIMARY = '#28B8CE';
|
|
9
10
|
const GP_DARK = '#002448';
|
|
10
11
|
const GP_SURFACE = '#0a1929';
|
|
11
12
|
const GP_ACCENT = '#9fe2f7';
|
|
13
|
+
// ═══════════════════════════════════════
|
|
14
|
+
// JSON syntax highlighting (pure React)
|
|
15
|
+
// ═══════════════════════════════════════
|
|
16
|
+
const highlightJson = (json) => {
|
|
17
|
+
const parts = [];
|
|
18
|
+
// Regex to match JSON tokens
|
|
19
|
+
const tokenRegex = /("(?:\\.|[^"\\])*"\s*:)|("(?:\\.|[^"\\])*")|(true|false|null)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|([{}[\],])/g;
|
|
20
|
+
let lastIndex = 0;
|
|
21
|
+
let match;
|
|
22
|
+
while ((match = tokenRegex.exec(json)) !== null) {
|
|
23
|
+
// Add whitespace between tokens
|
|
24
|
+
if (match.index > lastIndex) {
|
|
25
|
+
parts.push(json.slice(lastIndex, match.index));
|
|
26
|
+
}
|
|
27
|
+
const [full, key, str, bool, num, punct] = match;
|
|
28
|
+
if (key) {
|
|
29
|
+
// key: "xxx":
|
|
30
|
+
const colonIdx = key.lastIndexOf(':');
|
|
31
|
+
parts.push(_jsx("span", { css: jsonKeyCss, children: key.slice(0, colonIdx) }, `k${match.index}`));
|
|
32
|
+
parts.push(key.slice(colonIdx));
|
|
33
|
+
}
|
|
34
|
+
else if (str) {
|
|
35
|
+
parts.push(_jsx("span", { css: jsonStringCss, children: full }, `s${match.index}`));
|
|
36
|
+
}
|
|
37
|
+
else if (bool) {
|
|
38
|
+
parts.push(_jsx("span", { css: jsonBoolCss, children: full }, `b${match.index}`));
|
|
39
|
+
}
|
|
40
|
+
else if (num) {
|
|
41
|
+
parts.push(_jsx("span", { css: jsonNumCss, children: full }, `n${match.index}`));
|
|
42
|
+
}
|
|
43
|
+
else if (punct) {
|
|
44
|
+
parts.push(_jsx("span", { css: jsonPunctCss, children: full }, `p${match.index}`));
|
|
45
|
+
}
|
|
46
|
+
lastIndex = match.index + full.length;
|
|
47
|
+
}
|
|
48
|
+
if (lastIndex < json.length) {
|
|
49
|
+
parts.push(json.slice(lastIndex));
|
|
50
|
+
}
|
|
51
|
+
return parts;
|
|
52
|
+
};
|
|
12
53
|
/**
|
|
13
54
|
* A single entry in the DevToolsHub panel.
|
|
14
55
|
* Use this instead of raw `<button>` when adding custom tools via `children`.
|
|
@@ -19,17 +60,47 @@ const GP_ACCENT = '#9fe2f7';
|
|
|
19
60
|
* </DevToolsHub>
|
|
20
61
|
*/
|
|
21
62
|
export const DevToolEntry = ({ icon, label, desc, onClick }) => (_jsxs("button", { css: devToolBtnCss, onClick: onClick, children: [_jsx("span", { css: devToolIconCss, children: icon }), _jsx("span", { css: devToolLabelCss, children: label }), desc && _jsx("span", { css: devToolDescCss, children: desc })] }));
|
|
63
|
+
// ═══════════════════════════════════════
|
|
64
|
+
// Save/Load helpers
|
|
65
|
+
// ═══════════════════════════════════════
|
|
66
|
+
const SAVE_PREFIX = ':save:';
|
|
67
|
+
const getSaveKeys = (gameName) => {
|
|
68
|
+
const prefix = gameName + SAVE_PREFIX;
|
|
69
|
+
const keys = [];
|
|
70
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
71
|
+
const key = localStorage.key(i);
|
|
72
|
+
if (key?.startsWith(prefix))
|
|
73
|
+
keys.push(key);
|
|
74
|
+
}
|
|
75
|
+
return keys.sort();
|
|
76
|
+
};
|
|
77
|
+
const getSaveLabel = (key, gameName) => {
|
|
78
|
+
return key.slice((gameName + SAVE_PREFIX).length);
|
|
79
|
+
};
|
|
80
|
+
const menuItems = [
|
|
81
|
+
{ id: 'game', icon: '\u2699', label: 'Game' },
|
|
82
|
+
{ id: 'save', icon: '\u2B73', label: 'Save / Load' },
|
|
83
|
+
{ id: 'export', icon: '\u2398', label: 'Export' },
|
|
84
|
+
];
|
|
22
85
|
export const DevToolsHub = ({ children, fabBottom, gameOptions }) => {
|
|
23
86
|
const [isOpen, setIsOpen] = useState(false);
|
|
87
|
+
const [activeMenu, setActiveMenu] = useState(null);
|
|
24
88
|
const [newGamePlayers, setNewGamePlayers] = useState(2);
|
|
25
89
|
const [options, setOptions] = useState({});
|
|
26
90
|
const [undoCount, setUndoCount] = useState(1);
|
|
27
91
|
const [botActive, setBotActive] = useState(false);
|
|
28
92
|
const [flash, setFlash] = useState(null);
|
|
93
|
+
const [saveLabel, setSaveLabel] = useState('');
|
|
94
|
+
const [importText, setImportText] = useState('');
|
|
95
|
+
const [pasteError, setPasteError] = useState(null);
|
|
96
|
+
const [showPasteModal, setShowPasteModal] = useState(false);
|
|
97
|
+
const [saveRefresh, setSaveRefresh] = useState(0);
|
|
29
98
|
const flashTimeout = useRef(null);
|
|
99
|
+
const fileInputRef = useRef(null);
|
|
30
100
|
const gameState = useGame();
|
|
31
101
|
const currentPlayer = usePlayerId();
|
|
32
102
|
const players = usePlayerIds();
|
|
103
|
+
const gameName = useContext(gameContext)?.game ?? 'unknown';
|
|
33
104
|
const doFlash = useCallback((msg) => {
|
|
34
105
|
if (flashTimeout.current)
|
|
35
106
|
clearTimeout(flashTimeout.current);
|
|
@@ -54,30 +125,171 @@ export const DevToolsHub = ({ children, fabBottom, gameOptions }) => {
|
|
|
54
125
|
action();
|
|
55
126
|
doFlash(successMsg);
|
|
56
127
|
}, [g, doFlash]);
|
|
128
|
+
// Save/Load handlers
|
|
129
|
+
const saveKeys = getSaveKeys(gameName);
|
|
130
|
+
void saveRefresh; // force re-read
|
|
131
|
+
const handleSave = useCallback(() => {
|
|
132
|
+
if (!gameState) {
|
|
133
|
+
doFlash('No game state');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const label = saveLabel.trim() || new Date().toLocaleTimeString();
|
|
137
|
+
const key = gameName + SAVE_PREFIX + label;
|
|
138
|
+
localStorage.setItem(key, JSON.stringify(gameState));
|
|
139
|
+
setSaveLabel('');
|
|
140
|
+
setSaveRefresh(n => n + 1);
|
|
141
|
+
doFlash(`Saved: ${label}`);
|
|
142
|
+
}, [gameState, saveLabel, gameName, doFlash]);
|
|
143
|
+
const handleLoad = useCallback((key) => {
|
|
144
|
+
const raw = localStorage.getItem(key);
|
|
145
|
+
if (!raw) {
|
|
146
|
+
doFlash('Save not found');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const state = JSON.parse(raw);
|
|
151
|
+
if (g) {
|
|
152
|
+
g.new(state);
|
|
153
|
+
doFlash(`Loaded: ${getSaveLabel(key, gameName)}`);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
doFlash('game helper not available');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
doFlash('Invalid save data');
|
|
161
|
+
}
|
|
162
|
+
}, [g, gameName, doFlash]);
|
|
163
|
+
const handleDownloadSave = useCallback((key) => {
|
|
164
|
+
const raw = localStorage.getItem(key);
|
|
165
|
+
if (!raw) {
|
|
166
|
+
doFlash('Save not found');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const label = getSaveLabel(key, gameName);
|
|
170
|
+
const blob = new Blob([raw], { type: 'application/json' });
|
|
171
|
+
const url = URL.createObjectURL(blob);
|
|
172
|
+
const a = document.createElement('a');
|
|
173
|
+
a.href = url;
|
|
174
|
+
a.download = `${gameName}-${label.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`;
|
|
175
|
+
a.click();
|
|
176
|
+
URL.revokeObjectURL(url);
|
|
177
|
+
doFlash(`Downloaded: ${label}`);
|
|
178
|
+
}, [gameName, doFlash]);
|
|
179
|
+
const handleDelete = useCallback((key) => {
|
|
180
|
+
localStorage.removeItem(key);
|
|
181
|
+
setSaveRefresh(n => n + 1);
|
|
182
|
+
doFlash(`Deleted: ${getSaveLabel(key, gameName)}`);
|
|
183
|
+
}, [gameName, doFlash]);
|
|
184
|
+
const handleImportFile = useCallback((e) => {
|
|
185
|
+
const file = e.target.files?.[0];
|
|
186
|
+
if (!file)
|
|
187
|
+
return;
|
|
188
|
+
const reader = new FileReader();
|
|
189
|
+
reader.onload = () => {
|
|
190
|
+
try {
|
|
191
|
+
const state = JSON.parse(reader.result);
|
|
192
|
+
if (g) {
|
|
193
|
+
g.new(state);
|
|
194
|
+
doFlash(`Imported: ${file.name}`);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
doFlash('game helper not available');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
doFlash('Invalid JSON file');
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
reader.readAsText(file);
|
|
205
|
+
e.target.value = '';
|
|
206
|
+
}, [g, doFlash]);
|
|
207
|
+
const handleImportPaste = useCallback(() => {
|
|
208
|
+
if (!importText.trim()) {
|
|
209
|
+
setPasteError('Paste a JSON state first');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const state = JSON.parse(importText);
|
|
214
|
+
if (g) {
|
|
215
|
+
g.new(state);
|
|
216
|
+
setImportText('');
|
|
217
|
+
setPasteError(null);
|
|
218
|
+
setShowPasteModal(false);
|
|
219
|
+
doFlash('State imported!');
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
setPasteError('game helper not available');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
setPasteError('Invalid JSON — check syntax');
|
|
227
|
+
}
|
|
228
|
+
}, [g, importText, doFlash]);
|
|
229
|
+
const handlePasteChange = useCallback((value) => {
|
|
230
|
+
setImportText(value);
|
|
231
|
+
setPasteError(null);
|
|
232
|
+
// Try to pretty-print if valid
|
|
233
|
+
try {
|
|
234
|
+
const parsed = JSON.parse(value);
|
|
235
|
+
setImportText(JSON.stringify(parsed, null, 2));
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// Keep raw text if invalid
|
|
239
|
+
}
|
|
240
|
+
}, []);
|
|
241
|
+
const handleDownloadState = useCallback(() => {
|
|
242
|
+
if (!gameState) {
|
|
243
|
+
doFlash('No game state');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const json = JSON.stringify(gameState, null, 2);
|
|
247
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
248
|
+
const url = URL.createObjectURL(blob);
|
|
249
|
+
const a = document.createElement('a');
|
|
250
|
+
a.href = url;
|
|
251
|
+
a.download = `${gameName}-state-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
|
|
252
|
+
a.click();
|
|
253
|
+
URL.revokeObjectURL(url);
|
|
254
|
+
doFlash('State downloaded');
|
|
255
|
+
}, [gameState, gameName, doFlash]);
|
|
256
|
+
const handleMenuClick = (id) => {
|
|
257
|
+
setActiveMenu(prev => prev === id ? null : id);
|
|
258
|
+
};
|
|
259
|
+
const handleMenuHover = (id) => {
|
|
260
|
+
setActiveMenu(id);
|
|
261
|
+
};
|
|
262
|
+
const allMenuItems = children
|
|
263
|
+
? [...menuItems, { id: 'custom', icon: '\u2726', label: 'Custom' }]
|
|
264
|
+
: menuItems;
|
|
57
265
|
const root = document.getElementById('root');
|
|
58
266
|
if (!root)
|
|
59
267
|
return null;
|
|
60
|
-
return createPortal(_jsxs(_Fragment, { children: [_jsx("button", { css: fabCss, onClick: () => setIsOpen(o => !o)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
268
|
+
return createPortal(_jsxs(_Fragment, { children: [_jsx("button", { css: fabCss, onClick: () => { setIsOpen(o => !o); if (isOpen)
|
|
269
|
+
setActiveMenu(null); }, "data-open": isOpen, style: fabBottom ? { bottom: fabBottom } : undefined, children: _jsxs("svg", { css: logoCss, viewBox: "0 0 46 46", "data-open": isOpen, children: [_jsx("circle", { cx: "11", cy: "11", r: "7" }), _jsx("circle", { cx: "35", cy: "11", r: "7" }), _jsx("circle", { cx: "11", cy: "35", r: "7" }), _jsx("circle", { cx: "35", cy: "35", r: "7" })] }) }), isOpen && (_jsxs(_Fragment, { children: [_jsx("div", { css: backdropCss, onClick: () => { setIsOpen(false); setActiveMenu(null); } }), _jsxs("div", { css: hubContainerCss, style: fabBottom ? { bottom: `calc(${fabBottom} + 48px)` } : undefined, children: [_jsxs("div", { css: mainMenuCss, children: [_jsxs("div", { css: panelHeaderCss, children: [_jsxs("svg", { css: headerLogoCss, viewBox: "0 0 46 46", children: [_jsx("circle", { cx: "11", cy: "11", r: "7" }), _jsx("circle", { cx: "35", cy: "11", r: "7" }), _jsx("circle", { cx: "11", cy: "35", r: "7" }), _jsx("circle", { cx: "35", cy: "35", r: "7" })] }), _jsx("span", { css: panelTitleCss, children: "Dev Tools" }), _jsx("span", { css: panelBadgeCss, children: "GP" })] }), _jsx("div", { css: menuListCss, children: allMenuItems.map(item => (_jsxs("button", { css: [menuItemCss, activeMenu === item.id && menuItemActiveCss], onClick: () => handleMenuClick(item.id), onMouseEnter: () => handleMenuHover(item.id), children: [_jsx("span", { css: menuItemIconCss, children: item.icon }), _jsx("span", { css: menuItemLabelCss, children: item.label }), _jsx("span", { css: menuChevronCss, children: '\u25B8' })] }, item.id))) }), flash && _jsx("div", { css: flashCss, children: flash }, flash)] }), activeMenu && (_jsxs("div", { css: subPanelCss, children: [_jsx("div", { css: subPanelHeaderCss, children: _jsx("span", { css: subPanelTitleCss, children: allMenuItems.find(m => m.id === activeMenu)?.label }) }), _jsxs("div", { css: subPanelContentCss, children: [activeMenu === 'game' && (_jsxs(_Fragment, { children: [_jsxs("div", { css: devToolBtnCss, children: [_jsx("span", { css: devToolIconCss, children: '\u21BB' }), _jsx("span", { css: devToolLabelCss, children: "New Game" }), _jsx("span", { css: devToolDescCss, children: "Reset with N players" }), _jsxs("div", { css: inlineRowCss, onClick: e => e.stopPropagation(), children: [_jsx("button", { css: stepBtnCss, onClick: () => setNewGamePlayers(c => Math.max(1, c - 1)), children: "-" }), _jsx("input", { type: "number", min: 1, max: 10, value: newGamePlayers, onChange: e => setNewGamePlayers(Math.max(1, parseInt(e.target.value) || 2)), css: numberInputCss }), _jsx("button", { css: stepBtnCss, onClick: () => setNewGamePlayers(c => Math.min(10, c + 1)), children: "+" }), _jsx("button", { css: goBtnCss, onClick: () => exec(() => {
|
|
270
|
+
const hasOptions = gameOptions?.length && Object.values(options).some(Boolean);
|
|
271
|
+
g.new(hasOptions ? { players: newGamePlayers, ...options } : newGamePlayers);
|
|
272
|
+
}, `New game ${newGamePlayers}p`), children: "Go" })] }), gameOptions?.map(opt => (_jsxs("label", { css: toggleRowCss, onClick: e => e.stopPropagation(), children: [_jsx("input", { type: "checkbox", checked: options[opt.key] ?? false, onChange: e => setOptions(prev => ({ ...prev, [opt.key]: e.target.checked })), css: checkboxCss }), _jsx("span", { css: toggleLabelCss, children: opt.label })] }, opt.key)))] }), _jsxs("div", { css: devToolBtnCss, children: [_jsx("span", { css: devToolIconCss, children: '\u238C' }), _jsx("span", { css: devToolLabelCss, children: "Undo" }), _jsx("span", { css: devToolDescCss, children: "Revert N moves" }), _jsxs("div", { css: inlineRowCss, onClick: e => e.stopPropagation(), children: [_jsx("button", { css: stepBtnCss, onClick: () => setUndoCount(c => Math.max(1, c - 1)), children: "-" }), _jsx("input", { type: "number", min: 1, max: 999, value: undoCount, onChange: e => setUndoCount(Math.max(1, parseInt(e.target.value) || 1)), css: numberInputCss }), _jsx("button", { css: stepBtnCss, onClick: () => setUndoCount(c => c + 1), children: "+" }), _jsx("button", { css: goBtnCss, onClick: () => exec(() => g.undo(undoCount), `Undo ${undoCount} move${undoCount > 1 ? 's' : ''}`), children: "Go" })] })] }), _jsxs("div", { css: devToolBtnCss, children: [_jsx("span", { css: devToolIconCss, children: '\u2194' }), _jsx("span", { css: devToolLabelCss, children: "Switch Player" }), _jsx("span", { css: devToolDescCss, children: "View as another player" }), _jsxs("div", { css: inlineRowCss, onClick: e => e.stopPropagation(), children: [players.map(pid => (_jsxs("button", { css: [playerBtnCss, pid === currentPlayer && playerBtnActiveCss], onClick: () => exec(() => g.changePlayer(pid), `Switched to P${pid}`), children: ["P", String(pid)] }, String(pid)))), _jsx("button", { css: [playerBtnCss, currentPlayer === undefined && playerBtnActiveCss], onClick: () => exec(() => g.changePlayer(), 'Spectator mode'), children: "Spect" })] })] }), _jsxs("button", { css: [devToolBtnCss, botActive && toolBtnActiveCss], onClick: () => {
|
|
273
|
+
const next = !botActive;
|
|
274
|
+
exec(() => g.bot(next), next ? 'Bots enabled' : 'Bots disabled');
|
|
275
|
+
setBotActive(next);
|
|
276
|
+
}, children: [_jsx("span", { css: devToolIconCss, children: '\u2699' }), _jsx("span", { css: devToolLabelCss, children: botActive ? 'Disable Bots' : 'Enable Bots' }), _jsx("span", { css: devToolDescCss, children: botActive ? 'Stop auto-play' : 'Auto-play all moves' }), botActive && _jsx("span", { css: activeIndicatorCss })] }), _jsxs("button", { css: devToolBtnCss, onClick: () => exec(() => g.tutorial(), 'Tutorial started'), children: [_jsx("span", { css: devToolIconCss, children: "?" }), _jsx("span", { css: devToolLabelCss, children: "Tutorial" }), _jsx("span", { css: devToolDescCss, children: "Start tutorial mode" })] })] })), activeMenu === 'save' && (_jsxs(_Fragment, { children: [_jsxs("div", { css: devToolBtnCss, children: [_jsx("span", { css: devToolIconCss, children: '\u2B73' }), _jsx("span", { css: devToolLabelCss, children: "Save State" }), _jsx("span", { css: devToolDescCss, children: "Save current game state" }), _jsxs("div", { css: inlineRowCss, onClick: e => e.stopPropagation(), children: [_jsx("input", { type: "text", placeholder: "label (optional)", value: saveLabel, onChange: e => setSaveLabel(e.target.value), onKeyDown: e => { if (e.key === 'Enter')
|
|
277
|
+
handleSave(); }, css: textInputCss }), _jsx("button", { css: goBtnCss, onClick: handleSave, children: "Save" })] })] }), saveKeys.length > 0 && (_jsx("div", { css: savedListCss, children: saveKeys.map(key => (_jsxs("div", { css: savedEntryCss, children: [_jsx("span", { css: savedLabelCss, children: getSaveLabel(key, gameName) }), _jsxs("div", { css: savedActionsCss, children: [_jsx("button", { css: smallBtnCss, onClick: () => handleLoad(key), title: "Load", children: '\u25B6' }), _jsx("button", { css: smallBtnCss, onClick: () => handleDownloadSave(key), title: "Download", children: '\u2913' }), _jsx("button", { css: [smallBtnCss, smallBtnDangerCss], onClick: () => handleDelete(key), title: "Delete", children: '\u2715' })] })] }, key))) })), _jsx("div", { css: dividerCss }), _jsxs("button", { css: devToolBtnCss, onClick: () => fileInputRef.current?.click(), children: [_jsx("span", { css: devToolIconCss, children: '\u2912' }), _jsx("span", { css: devToolLabelCss, children: "Import File" }), _jsx("span", { css: devToolDescCss, children: "Load state from JSON file" })] }), _jsx("input", { ref: fileInputRef, type: "file", accept: ".json,application/json", onChange: handleImportFile, style: { display: 'none' } }), _jsxs("button", { css: devToolBtnCss, onClick: () => { setShowPasteModal(true); setPasteError(null); }, children: [_jsx("span", { css: devToolIconCss, children: '\u2398' }), _jsx("span", { css: devToolLabelCss, children: "Paste State" }), _jsx("span", { css: devToolDescCss, children: "Open editor to paste JSON" })] })] })), activeMenu === 'export' && (_jsxs(_Fragment, { children: [_jsxs("button", { css: devToolBtnCss, onClick: handleDownloadState, children: [_jsx("span", { css: devToolIconCss, children: '\u2913' }), _jsx("span", { css: devToolLabelCss, children: "Download State" }), _jsx("span", { css: devToolDescCss, children: "Save state as .json file" })] }), _jsxs("button", { css: devToolBtnCss, onClick: () => {
|
|
278
|
+
const raw = localStorage.getItem(gameName);
|
|
279
|
+
if (raw) {
|
|
280
|
+
copyToClipboard(raw, `${gameName} state`);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
doFlash(`No "${gameName}" key in localStorage`);
|
|
284
|
+
}
|
|
285
|
+
}, children: [_jsx("span", { css: devToolIconCss, children: '\u29C9' }), _jsx("span", { css: devToolLabelCss, children: "Copy LocalStorage" }), _jsxs("span", { css: devToolDescCss, children: ["Copy \"", gameName, "\" key to clipboard"] })] })] })), activeMenu === 'custom' && children] })] }))] })] })), showPasteModal && (_jsxs(_Fragment, { children: [_jsx("div", { css: modalBackdropCss, onClick: () => { setShowPasteModal(false); setPasteError(null); } }), _jsxs("div", { css: modalCss, children: [_jsxs("div", { css: modalHeaderCss, children: [_jsx("span", { css: modalTitleCss, children: "Paste Game State" }), _jsx("button", { css: modalCloseBtnCss, onClick: () => { setShowPasteModal(false); setPasteError(null); }, children: '\u2715' })] }), _jsxs("div", { css: modalBodyCss, children: [_jsx("textarea", { css: modalTextareaCss, placeholder: 'Paste your JSON game state here...', value: importText, onChange: e => handlePasteChange(e.target.value), spellCheck: false }), importText.trim() && (_jsxs("div", { css: jsonPreviewCss, children: [_jsx("div", { css: jsonPreviewLabelCss, children: "Preview" }), _jsx("pre", { css: jsonPreviewCodeCss, children: (() => {
|
|
286
|
+
try {
|
|
287
|
+
return highlightJson(JSON.stringify(JSON.parse(importText), null, 2));
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return _jsx("span", { css: jsonErrorTextCss, children: importText });
|
|
291
|
+
}
|
|
292
|
+
})() })] })), pasteError && _jsx("div", { css: pasteErrorCss, children: pasteError })] }), _jsxs("div", { css: modalFooterCss, children: [_jsx("button", { css: modalCancelBtnCss, onClick: () => { setShowPasteModal(false); setPasteError(null); }, children: "Cancel" }), _jsx("button", { css: modalLoadBtnCss, onClick: handleImportPaste, children: "Load State" })] })] })] }))] }), root);
|
|
81
293
|
};
|
|
82
294
|
// ═══════════════════════════════════════
|
|
83
295
|
// Styles — GamePark branded
|
|
@@ -126,23 +338,26 @@ const logoCss = css `
|
|
|
126
338
|
transform: rotate(90deg);
|
|
127
339
|
}
|
|
128
340
|
`;
|
|
129
|
-
const backdropCss = css `
|
|
130
|
-
position: fixed;
|
|
131
|
-
inset: 0;
|
|
132
|
-
z-index: 899;
|
|
133
|
-
background: rgba(0, 0, 0, 0.3);
|
|
134
|
-
backdrop-filter: blur(2px);
|
|
135
|
-
`;
|
|
136
341
|
const slideUp = keyframes `
|
|
137
342
|
from { opacity: 0; transform: translateY(8px) scale(0.97); }
|
|
138
343
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
139
344
|
`;
|
|
140
|
-
const
|
|
345
|
+
const slideRight = keyframes `
|
|
346
|
+
from { opacity: 0; transform: translateX(-8px) scale(0.97); }
|
|
347
|
+
to { opacity: 1; transform: translateX(0) scale(1); }
|
|
348
|
+
`;
|
|
349
|
+
const hubContainerCss = css `
|
|
141
350
|
position: fixed;
|
|
142
351
|
bottom: 64px;
|
|
143
352
|
left: 16px;
|
|
144
353
|
z-index: 900;
|
|
145
|
-
|
|
354
|
+
display: flex;
|
|
355
|
+
align-items: flex-end;
|
|
356
|
+
gap: 6px;
|
|
357
|
+
`;
|
|
358
|
+
const mainMenuCss = css `
|
|
359
|
+
position: relative;
|
|
360
|
+
width: 200px;
|
|
146
361
|
background: linear-gradient(170deg, #0f2035 0%, ${GP_SURFACE} 100%);
|
|
147
362
|
border: 1px solid rgba(40, 184, 206, 0.25);
|
|
148
363
|
border-radius: 12px;
|
|
@@ -150,9 +365,47 @@ const panelCss = css `
|
|
|
150
365
|
0 12px 40px rgba(0, 0, 0, 0.5),
|
|
151
366
|
0 0 0 1px rgba(0, 0, 0, 0.3),
|
|
152
367
|
inset 0 1px 0 rgba(159, 226, 247, 0.05);
|
|
153
|
-
overflow: hidden;
|
|
154
368
|
animation: ${slideUp} 0.2s ease-out;
|
|
155
369
|
font-family: 'Mulish', sans-serif;
|
|
370
|
+
overflow: hidden;
|
|
371
|
+
`;
|
|
372
|
+
const subPanelCss = css `
|
|
373
|
+
width: 320px;
|
|
374
|
+
max-height: calc(100vh - 80px);
|
|
375
|
+
display: flex;
|
|
376
|
+
flex-direction: column;
|
|
377
|
+
background: linear-gradient(170deg, #0f2035 0%, ${GP_SURFACE} 100%);
|
|
378
|
+
border: 1px solid rgba(40, 184, 206, 0.25);
|
|
379
|
+
border-radius: 12px;
|
|
380
|
+
box-shadow:
|
|
381
|
+
0 12px 40px rgba(0, 0, 0, 0.5),
|
|
382
|
+
0 0 0 1px rgba(0, 0, 0, 0.3),
|
|
383
|
+
inset 0 1px 0 rgba(159, 226, 247, 0.05);
|
|
384
|
+
animation: ${slideRight} 0.15s ease-out;
|
|
385
|
+
font-family: 'Mulish', sans-serif;
|
|
386
|
+
overflow: hidden;
|
|
387
|
+
`;
|
|
388
|
+
const subPanelHeaderCss = css `
|
|
389
|
+
padding: 10px 16px;
|
|
390
|
+
border-bottom: 1px solid rgba(40, 184, 206, 0.15);
|
|
391
|
+
background: rgba(40, 184, 206, 0.04);
|
|
392
|
+
flex-shrink: 0;
|
|
393
|
+
`;
|
|
394
|
+
const subPanelTitleCss = css `
|
|
395
|
+
font-size: 12px;
|
|
396
|
+
font-weight: 800;
|
|
397
|
+
color: #5a8a98;
|
|
398
|
+
text-transform: uppercase;
|
|
399
|
+
letter-spacing: 0.1em;
|
|
400
|
+
`;
|
|
401
|
+
const subPanelContentCss = css `
|
|
402
|
+
display: flex;
|
|
403
|
+
flex-direction: column;
|
|
404
|
+
padding: 6px;
|
|
405
|
+
gap: 2px;
|
|
406
|
+
overflow-y: auto;
|
|
407
|
+
flex: 1;
|
|
408
|
+
min-height: 0;
|
|
156
409
|
`;
|
|
157
410
|
const panelHeaderCss = css `
|
|
158
411
|
display: flex;
|
|
@@ -185,16 +438,71 @@ const panelBadgeCss = css `
|
|
|
185
438
|
color: ${GP_PRIMARY};
|
|
186
439
|
letter-spacing: 0.1em;
|
|
187
440
|
`;
|
|
188
|
-
const
|
|
189
|
-
from { opacity: 0; transform: translateX(-6px); }
|
|
190
|
-
to { opacity: 1; transform: translateX(0); }
|
|
191
|
-
`;
|
|
192
|
-
const toolListCss = css `
|
|
441
|
+
const menuListCss = css `
|
|
193
442
|
display: flex;
|
|
194
443
|
flex-direction: column;
|
|
195
444
|
padding: 6px;
|
|
196
445
|
gap: 2px;
|
|
197
446
|
`;
|
|
447
|
+
const menuItemCss = css `
|
|
448
|
+
position: relative;
|
|
449
|
+
display: flex;
|
|
450
|
+
align-items: center;
|
|
451
|
+
gap: 10px;
|
|
452
|
+
padding: 10px 12px;
|
|
453
|
+
border: none;
|
|
454
|
+
border-radius: 8px;
|
|
455
|
+
background: transparent;
|
|
456
|
+
cursor: pointer;
|
|
457
|
+
text-align: left;
|
|
458
|
+
transition: background 0.15s;
|
|
459
|
+
font-family: inherit;
|
|
460
|
+
|
|
461
|
+
&:hover {
|
|
462
|
+
background: rgba(40, 184, 206, 0.08);
|
|
463
|
+
}
|
|
464
|
+
`;
|
|
465
|
+
const menuItemActiveCss = css `
|
|
466
|
+
background: rgba(40, 184, 206, 0.12);
|
|
467
|
+
|
|
468
|
+
&::before {
|
|
469
|
+
content: '';
|
|
470
|
+
position: absolute;
|
|
471
|
+
left: 0;
|
|
472
|
+
top: 50%;
|
|
473
|
+
transform: translateY(-50%);
|
|
474
|
+
width: 3px;
|
|
475
|
+
height: 18px;
|
|
476
|
+
border-radius: 0 3px 3px 0;
|
|
477
|
+
background: ${GP_PRIMARY};
|
|
478
|
+
}
|
|
479
|
+
`;
|
|
480
|
+
const menuItemIconCss = css `
|
|
481
|
+
font-size: 15px;
|
|
482
|
+
color: ${GP_PRIMARY};
|
|
483
|
+
display: flex;
|
|
484
|
+
align-items: center;
|
|
485
|
+
justify-content: center;
|
|
486
|
+
width: 28px;
|
|
487
|
+
height: 28px;
|
|
488
|
+
border-radius: 6px;
|
|
489
|
+
background: rgba(40, 184, 206, 0.08);
|
|
490
|
+
flex-shrink: 0;
|
|
491
|
+
`;
|
|
492
|
+
const menuItemLabelCss = css `
|
|
493
|
+
font-size: 13px;
|
|
494
|
+
font-weight: 700;
|
|
495
|
+
color: #e0f0f4;
|
|
496
|
+
flex: 1;
|
|
497
|
+
`;
|
|
498
|
+
const menuChevronCss = css `
|
|
499
|
+
font-size: 10px;
|
|
500
|
+
color: #3a6070;
|
|
501
|
+
`;
|
|
502
|
+
const toolReveal = keyframes `
|
|
503
|
+
from { opacity: 0; transform: translateX(-6px); }
|
|
504
|
+
to { opacity: 1; transform: translateX(0); }
|
|
505
|
+
`;
|
|
198
506
|
export const devToolBtnCss = css `
|
|
199
507
|
position: relative;
|
|
200
508
|
display: grid;
|
|
@@ -324,6 +632,28 @@ const numberInputCss = css `
|
|
|
324
632
|
margin: 0;
|
|
325
633
|
}
|
|
326
634
|
`;
|
|
635
|
+
const textInputCss = css `
|
|
636
|
+
flex: 1;
|
|
637
|
+
height: 26px;
|
|
638
|
+
border-radius: 5px;
|
|
639
|
+
border: 1px solid rgba(40, 184, 206, 0.25);
|
|
640
|
+
background: rgba(0, 0, 0, 0.3);
|
|
641
|
+
color: #e0f0f4;
|
|
642
|
+
font-size: 12px;
|
|
643
|
+
font-weight: 600;
|
|
644
|
+
padding: 0 8px;
|
|
645
|
+
font-family: inherit;
|
|
646
|
+
|
|
647
|
+
&::placeholder {
|
|
648
|
+
color: #3a6070;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
&:focus {
|
|
652
|
+
outline: none;
|
|
653
|
+
border-color: ${GP_PRIMARY};
|
|
654
|
+
box-shadow: 0 0 0 2px rgba(40, 184, 206, 0.15);
|
|
655
|
+
}
|
|
656
|
+
`;
|
|
327
657
|
const goBtnCss = css `
|
|
328
658
|
height: 26px;
|
|
329
659
|
padding: 0 12px;
|
|
@@ -339,6 +669,7 @@ const goBtnCss = css `
|
|
|
339
669
|
margin-left: auto;
|
|
340
670
|
font-family: inherit;
|
|
341
671
|
transition: all 0.15s;
|
|
672
|
+
flex-shrink: 0;
|
|
342
673
|
|
|
343
674
|
&:hover {
|
|
344
675
|
background: rgba(40, 184, 206, 0.25);
|
|
@@ -369,21 +700,291 @@ const playerBtnActiveCss = css `
|
|
|
369
700
|
border-color: ${GP_PRIMARY};
|
|
370
701
|
color: ${GP_PRIMARY};
|
|
371
702
|
`;
|
|
703
|
+
// ── Save/Load styles ──
|
|
704
|
+
const savedListCss = css `
|
|
705
|
+
display: flex;
|
|
706
|
+
flex-direction: column;
|
|
707
|
+
gap: 2px;
|
|
708
|
+
padding: 0 8px;
|
|
709
|
+
`;
|
|
710
|
+
const savedEntryCss = css `
|
|
711
|
+
display: flex;
|
|
712
|
+
align-items: center;
|
|
713
|
+
justify-content: space-between;
|
|
714
|
+
padding: 5px 8px;
|
|
715
|
+
border-radius: 5px;
|
|
716
|
+
background: rgba(40, 184, 206, 0.04);
|
|
717
|
+
border: 1px solid rgba(40, 184, 206, 0.08);
|
|
718
|
+
`;
|
|
719
|
+
const savedLabelCss = css `
|
|
720
|
+
font-size: 12px;
|
|
721
|
+
font-weight: 600;
|
|
722
|
+
color: #8ab8c8;
|
|
723
|
+
overflow: hidden;
|
|
724
|
+
text-overflow: ellipsis;
|
|
725
|
+
white-space: nowrap;
|
|
726
|
+
flex: 1;
|
|
727
|
+
margin-right: 8px;
|
|
728
|
+
`;
|
|
729
|
+
const savedActionsCss = css `
|
|
730
|
+
display: flex;
|
|
731
|
+
gap: 4px;
|
|
732
|
+
flex-shrink: 0;
|
|
733
|
+
`;
|
|
734
|
+
const smallBtnCss = css `
|
|
735
|
+
width: 22px;
|
|
736
|
+
height: 22px;
|
|
737
|
+
border-radius: 4px;
|
|
738
|
+
border: 1px solid rgba(40, 184, 206, 0.25);
|
|
739
|
+
background: rgba(40, 184, 206, 0.08);
|
|
740
|
+
color: ${GP_PRIMARY};
|
|
741
|
+
font-size: 10px;
|
|
742
|
+
cursor: pointer;
|
|
743
|
+
display: flex;
|
|
744
|
+
align-items: center;
|
|
745
|
+
justify-content: center;
|
|
746
|
+
font-family: inherit;
|
|
747
|
+
transition: all 0.15s;
|
|
748
|
+
|
|
749
|
+
&:hover {
|
|
750
|
+
background: rgba(40, 184, 206, 0.2);
|
|
751
|
+
border-color: rgba(40, 184, 206, 0.4);
|
|
752
|
+
}
|
|
753
|
+
`;
|
|
754
|
+
const smallBtnDangerCss = css `
|
|
755
|
+
&:hover {
|
|
756
|
+
background: rgba(220, 60, 60, 0.2);
|
|
757
|
+
border-color: rgba(220, 60, 60, 0.4);
|
|
758
|
+
color: #dc3c3c;
|
|
759
|
+
}
|
|
760
|
+
`;
|
|
761
|
+
// ── Backdrop (click-outside to close) ──
|
|
762
|
+
const backdropCss = css `
|
|
763
|
+
position: fixed;
|
|
764
|
+
inset: 0;
|
|
765
|
+
z-index: 899;
|
|
766
|
+
`;
|
|
767
|
+
// ── Paste Modal ──
|
|
768
|
+
const modalBackdropCss = css `
|
|
769
|
+
position: fixed;
|
|
770
|
+
inset: 0;
|
|
771
|
+
z-index: 1000;
|
|
772
|
+
background: rgba(0, 0, 0, 0.6);
|
|
773
|
+
backdrop-filter: blur(2px);
|
|
774
|
+
`;
|
|
775
|
+
const modalFadeIn = keyframes `
|
|
776
|
+
from { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }
|
|
777
|
+
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
|
778
|
+
`;
|
|
779
|
+
const modalCss = css `
|
|
780
|
+
position: fixed;
|
|
781
|
+
top: 50%;
|
|
782
|
+
left: 50%;
|
|
783
|
+
transform: translate(-50%, -50%);
|
|
784
|
+
z-index: 1001;
|
|
785
|
+
width: min(700px, 90vw);
|
|
786
|
+
max-height: 80vh;
|
|
787
|
+
display: flex;
|
|
788
|
+
flex-direction: column;
|
|
789
|
+
background: linear-gradient(170deg, #0f2035 0%, ${GP_SURFACE} 100%);
|
|
790
|
+
border: 1px solid rgba(40, 184, 206, 0.3);
|
|
791
|
+
border-radius: 12px;
|
|
792
|
+
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.7);
|
|
793
|
+
font-family: 'Mulish', sans-serif;
|
|
794
|
+
animation: ${modalFadeIn} 0.2s ease-out;
|
|
795
|
+
`;
|
|
796
|
+
const modalHeaderCss = css `
|
|
797
|
+
display: flex;
|
|
798
|
+
align-items: center;
|
|
799
|
+
justify-content: space-between;
|
|
800
|
+
padding: 14px 20px;
|
|
801
|
+
border-bottom: 1px solid rgba(40, 184, 206, 0.15);
|
|
802
|
+
background: rgba(40, 184, 206, 0.04);
|
|
803
|
+
border-radius: 12px 12px 0 0;
|
|
804
|
+
`;
|
|
805
|
+
const modalTitleCss = css `
|
|
806
|
+
font-size: 14px;
|
|
807
|
+
font-weight: 800;
|
|
808
|
+
color: #e0f0f4;
|
|
809
|
+
text-transform: uppercase;
|
|
810
|
+
letter-spacing: 0.08em;
|
|
811
|
+
`;
|
|
812
|
+
const modalCloseBtnCss = css `
|
|
813
|
+
width: 28px;
|
|
814
|
+
height: 28px;
|
|
815
|
+
border: none;
|
|
816
|
+
border-radius: 6px;
|
|
817
|
+
background: rgba(40, 184, 206, 0.08);
|
|
818
|
+
color: #5a8a98;
|
|
819
|
+
font-size: 12px;
|
|
820
|
+
cursor: pointer;
|
|
821
|
+
display: flex;
|
|
822
|
+
align-items: center;
|
|
823
|
+
justify-content: center;
|
|
824
|
+
transition: all 0.15s;
|
|
825
|
+
|
|
826
|
+
&:hover {
|
|
827
|
+
background: rgba(220, 60, 60, 0.2);
|
|
828
|
+
color: #dc3c3c;
|
|
829
|
+
}
|
|
830
|
+
`;
|
|
831
|
+
const modalBodyCss = css `
|
|
832
|
+
flex: 1;
|
|
833
|
+
overflow-y: auto;
|
|
834
|
+
padding: 16px 20px;
|
|
835
|
+
display: flex;
|
|
836
|
+
flex-direction: column;
|
|
837
|
+
gap: 12px;
|
|
838
|
+
min-height: 0;
|
|
839
|
+
`;
|
|
840
|
+
const modalTextareaCss = css `
|
|
841
|
+
width: 100%;
|
|
842
|
+
min-height: 100px;
|
|
843
|
+
border-radius: 8px;
|
|
844
|
+
border: 1px solid rgba(40, 184, 206, 0.25);
|
|
845
|
+
background: rgba(0, 0, 0, 0.4);
|
|
846
|
+
color: #e0f0f4;
|
|
847
|
+
font-size: 13px;
|
|
848
|
+
font-weight: 500;
|
|
849
|
+
padding: 12px;
|
|
850
|
+
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
851
|
+
resize: vertical;
|
|
852
|
+
line-height: 1.5;
|
|
853
|
+
|
|
854
|
+
&::placeholder {
|
|
855
|
+
color: #3a6070;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
&:focus {
|
|
859
|
+
outline: none;
|
|
860
|
+
border-color: ${GP_PRIMARY};
|
|
861
|
+
box-shadow: 0 0 0 2px rgba(40, 184, 206, 0.15);
|
|
862
|
+
}
|
|
863
|
+
`;
|
|
864
|
+
const jsonPreviewCss = css `
|
|
865
|
+
border-radius: 8px;
|
|
866
|
+
border: 1px solid rgba(40, 184, 206, 0.15);
|
|
867
|
+
background: rgba(0, 0, 0, 0.3);
|
|
868
|
+
overflow: hidden;
|
|
869
|
+
`;
|
|
870
|
+
const jsonPreviewLabelCss = css `
|
|
871
|
+
font-size: 10px;
|
|
872
|
+
font-weight: 800;
|
|
873
|
+
color: #3a6070;
|
|
874
|
+
text-transform: uppercase;
|
|
875
|
+
letter-spacing: 0.1em;
|
|
876
|
+
padding: 6px 12px;
|
|
877
|
+
border-bottom: 1px solid rgba(40, 184, 206, 0.08);
|
|
878
|
+
`;
|
|
879
|
+
const jsonPreviewCodeCss = css `
|
|
880
|
+
padding: 12px;
|
|
881
|
+
margin: 0;
|
|
882
|
+
font-size: 12px;
|
|
883
|
+
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
884
|
+
line-height: 1.6;
|
|
885
|
+
color: #8ab8c8;
|
|
886
|
+
max-height: 300px;
|
|
887
|
+
overflow-y: auto;
|
|
888
|
+
white-space: pre;
|
|
889
|
+
tab-size: 2;
|
|
890
|
+
`;
|
|
891
|
+
const pasteErrorCss = css `
|
|
892
|
+
font-size: 12px;
|
|
893
|
+
font-weight: 700;
|
|
894
|
+
color: #dc3c3c;
|
|
895
|
+
padding: 6px 10px;
|
|
896
|
+
border-radius: 6px;
|
|
897
|
+
background: rgba(220, 60, 60, 0.1);
|
|
898
|
+
border: 1px solid rgba(220, 60, 60, 0.2);
|
|
899
|
+
`;
|
|
900
|
+
const modalFooterCss = css `
|
|
901
|
+
display: flex;
|
|
902
|
+
justify-content: flex-end;
|
|
903
|
+
gap: 8px;
|
|
904
|
+
padding: 12px 20px;
|
|
905
|
+
border-top: 1px solid rgba(40, 184, 206, 0.15);
|
|
906
|
+
`;
|
|
907
|
+
const modalCancelBtnCss = css `
|
|
908
|
+
height: 32px;
|
|
909
|
+
padding: 0 16px;
|
|
910
|
+
border-radius: 6px;
|
|
911
|
+
border: 1px solid rgba(40, 184, 206, 0.25);
|
|
912
|
+
background: transparent;
|
|
913
|
+
color: #5a8a98;
|
|
914
|
+
font-size: 13px;
|
|
915
|
+
font-weight: 700;
|
|
916
|
+
cursor: pointer;
|
|
917
|
+
font-family: inherit;
|
|
918
|
+
transition: all 0.15s;
|
|
919
|
+
|
|
920
|
+
&:hover {
|
|
921
|
+
background: rgba(40, 184, 206, 0.08);
|
|
922
|
+
color: #e0f0f4;
|
|
923
|
+
}
|
|
924
|
+
`;
|
|
925
|
+
const modalLoadBtnCss = css `
|
|
926
|
+
height: 32px;
|
|
927
|
+
padding: 0 20px;
|
|
928
|
+
border-radius: 6px;
|
|
929
|
+
border: 1px solid rgba(40, 184, 206, 0.4);
|
|
930
|
+
background: rgba(40, 184, 206, 0.2);
|
|
931
|
+
color: ${GP_PRIMARY};
|
|
932
|
+
font-size: 13px;
|
|
933
|
+
font-weight: 800;
|
|
934
|
+
text-transform: uppercase;
|
|
935
|
+
letter-spacing: 0.05em;
|
|
936
|
+
cursor: pointer;
|
|
937
|
+
font-family: inherit;
|
|
938
|
+
transition: all 0.15s;
|
|
939
|
+
|
|
940
|
+
&:hover {
|
|
941
|
+
background: rgba(40, 184, 206, 0.35);
|
|
942
|
+
border-color: ${GP_PRIMARY};
|
|
943
|
+
}
|
|
944
|
+
`;
|
|
945
|
+
// ── JSON syntax colors ──
|
|
946
|
+
const jsonKeyCss = css `
|
|
947
|
+
color: ${GP_ACCENT};
|
|
948
|
+
`;
|
|
949
|
+
const jsonStringCss = css `
|
|
950
|
+
color: #8bbb6a;
|
|
951
|
+
`;
|
|
952
|
+
const jsonBoolCss = css `
|
|
953
|
+
color: #d4872a;
|
|
954
|
+
`;
|
|
955
|
+
const jsonNumCss = css `
|
|
956
|
+
color: #c4a0e8;
|
|
957
|
+
`;
|
|
958
|
+
const jsonPunctCss = css `
|
|
959
|
+
color: #5a6a78;
|
|
960
|
+
`;
|
|
961
|
+
const jsonErrorTextCss = css `
|
|
962
|
+
color: #dc3c3c;
|
|
963
|
+
`;
|
|
372
964
|
// ── Flash ──
|
|
373
965
|
const flashFade = keyframes `
|
|
374
|
-
0% { opacity: 0; transform: translateY(
|
|
966
|
+
0% { opacity: 0; transform: translateY(6px); }
|
|
375
967
|
15% { opacity: 1; transform: translateY(0); }
|
|
376
968
|
85% { opacity: 1; transform: translateY(0); }
|
|
377
|
-
100% { opacity: 0; transform: translateY(-
|
|
969
|
+
100% { opacity: 0; transform: translateY(-6px); }
|
|
378
970
|
`;
|
|
379
971
|
const flashCss = css `
|
|
380
|
-
|
|
972
|
+
position: absolute;
|
|
973
|
+
bottom: 100%;
|
|
974
|
+
left: 0;
|
|
975
|
+
right: 0;
|
|
976
|
+
margin-bottom: 8px;
|
|
977
|
+
padding: 6px 14px;
|
|
381
978
|
font-size: 12px;
|
|
382
979
|
font-weight: 700;
|
|
383
980
|
color: ${GP_PRIMARY};
|
|
384
981
|
text-align: center;
|
|
385
|
-
|
|
386
|
-
|
|
982
|
+
background: linear-gradient(145deg, ${GP_SURFACE}, ${GP_DARK});
|
|
983
|
+
border: 1px solid rgba(40, 184, 206, 0.3);
|
|
984
|
+
border-radius: 8px;
|
|
985
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
|
986
|
+
white-space: nowrap;
|
|
987
|
+
pointer-events: none;
|
|
387
988
|
animation: ${flashFade} 1.5s ease-out forwards;
|
|
388
989
|
`;
|
|
389
990
|
// ── Game Options toggles ──
|
|
@@ -414,7 +1015,7 @@ const checkboxCss = css `
|
|
|
414
1015
|
}
|
|
415
1016
|
|
|
416
1017
|
&:checked::after {
|
|
417
|
-
content: '
|
|
1018
|
+
content: '\u2713';
|
|
418
1019
|
position: absolute;
|
|
419
1020
|
top: 50%;
|
|
420
1021
|
left: 50%;
|