@scriptonita/chess-football-ui 0.4.2 → 0.5.0
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/index.cjs +220 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +223 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -159,7 +159,7 @@ type GameTranslator = (key: string, values?: Record<string, unknown>) => string;
|
|
|
159
159
|
* grep -rhoE "t\('[a-zA-Z0-9_.]+'" src/components | sed -E "s/t\('//;s/'//" | sort -u
|
|
160
160
|
* (the dynamic `pieces.*` / `eventToast.*` families are derived separately below).
|
|
161
161
|
*/
|
|
162
|
-
declare const STATIC_I18N_KEYS: readonly ["actionPointsAriaLabel", "actionPointsShort", "endTurn", "endTurnConfirm", "endTurnConfirmDescription", "endTurnConfirmYes", "endTurnKeepPlaying", "history.empty", "history.goal", "history.interception", "history.move", "history.offside", "history.pass", "history.tackle", "history.title", "history.turn", "history.viewAll", "move", "offsideWarning", "pass", "selectedPiece.alreadyMoved", "selectedPiece.atSquare", "selectedPiece.empty", "selectedPiece.hasBall", "teamBlack", "teamWhite", "waitingRival", "yourTurn"];
|
|
162
|
+
declare const STATIC_I18N_KEYS: readonly ["actionPointsAriaLabel", "actionPointsShort", "endTurn", "endTurnConfirm", "endTurnConfirmDescription", "endTurnConfirmYes", "endTurnKeepPlaying", "history.empty", "history.goal", "history.interception", "history.move", "history.offside", "history.pass", "history.tackle", "history.title", "history.turn", "history.viewAll", "move", "offsideWarning", "pass", "selectedPiece.alreadyMoved", "selectedPiece.atSquare", "selectedPiece.empty", "selectedPiece.hasBall", "shortcuts.arrows", "shortcuts.buttonLabel", "shortcuts.cancel", "shortcuts.move", "shortcuts.pass", "shortcuts.select", "shortcuts.title", "teamBlack", "teamWhite", "waitingRival", "yourTurn"];
|
|
163
163
|
/** `pieces.*` — one key per short and long piece label the engine exposes (move-history, selected-piece-detail). */
|
|
164
164
|
type PieceI18nKey = `pieces.${PieceShortKey | PieceLongKey}`;
|
|
165
165
|
/** `eventToast.*` — one key per toastable event (see `ToastEvent` in `event-toast.tsx`). */
|
package/dist/index.d.ts
CHANGED
|
@@ -159,7 +159,7 @@ type GameTranslator = (key: string, values?: Record<string, unknown>) => string;
|
|
|
159
159
|
* grep -rhoE "t\('[a-zA-Z0-9_.]+'" src/components | sed -E "s/t\('//;s/'//" | sort -u
|
|
160
160
|
* (the dynamic `pieces.*` / `eventToast.*` families are derived separately below).
|
|
161
161
|
*/
|
|
162
|
-
declare const STATIC_I18N_KEYS: readonly ["actionPointsAriaLabel", "actionPointsShort", "endTurn", "endTurnConfirm", "endTurnConfirmDescription", "endTurnConfirmYes", "endTurnKeepPlaying", "history.empty", "history.goal", "history.interception", "history.move", "history.offside", "history.pass", "history.tackle", "history.title", "history.turn", "history.viewAll", "move", "offsideWarning", "pass", "selectedPiece.alreadyMoved", "selectedPiece.atSquare", "selectedPiece.empty", "selectedPiece.hasBall", "teamBlack", "teamWhite", "waitingRival", "yourTurn"];
|
|
162
|
+
declare const STATIC_I18N_KEYS: readonly ["actionPointsAriaLabel", "actionPointsShort", "endTurn", "endTurnConfirm", "endTurnConfirmDescription", "endTurnConfirmYes", "endTurnKeepPlaying", "history.empty", "history.goal", "history.interception", "history.move", "history.offside", "history.pass", "history.tackle", "history.title", "history.turn", "history.viewAll", "move", "offsideWarning", "pass", "selectedPiece.alreadyMoved", "selectedPiece.atSquare", "selectedPiece.empty", "selectedPiece.hasBall", "shortcuts.arrows", "shortcuts.buttonLabel", "shortcuts.cancel", "shortcuts.move", "shortcuts.pass", "shortcuts.select", "shortcuts.title", "teamBlack", "teamWhite", "waitingRival", "yourTurn"];
|
|
163
163
|
/** `pieces.*` — one key per short and long piece label the engine exposes (move-history, selected-piece-detail). */
|
|
164
164
|
type PieceI18nKey = `pieces.${PieceShortKey | PieceLongKey}`;
|
|
165
165
|
/** `eventToast.*` — one key per toastable event (see `ToastEvent` in `event-toast.tsx`). */
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useGameStore, getInitialBoardState } from './chunk-N24LGLLO.js';
|
|
2
|
-
import { SHORT_KEY, LONG_KEY, getValidMoves, getValidPasses, isInEnemyArea, RANK_LABELS, FILE_LABELS, squareName } from '@scriptonita/chess-football-engine';
|
|
2
|
+
import { SHORT_KEY, LONG_KEY, getValidMoves, getValidPasses, isInEnemyArea, getPath, RANK_LABELS, FILE_LABELS, squareName } from '@scriptonita/chess-football-engine';
|
|
3
3
|
import { clsx } from 'clsx';
|
|
4
4
|
import { twMerge } from 'tailwind-merge';
|
|
5
|
-
import { MotionConfig, AnimatePresence, motion } from 'framer-motion';
|
|
6
|
-
import { Crown, Zap, Flag, ShieldQuestion, ChessKnight, ChessBishop, ChessRook, ChessQueen, ChessKing, X, Move } from 'lucide-react';
|
|
5
|
+
import { MotionConfig, useReducedMotion, AnimatePresence, motion } from 'framer-motion';
|
|
6
|
+
import { HelpCircle, Crown, Zap, Flag, ShieldQuestion, ChessKnight, ChessBishop, ChessRook, ChessQueen, ChessKing, X, Move } from 'lucide-react';
|
|
7
7
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
8
8
|
import * as React from 'react';
|
|
9
9
|
import { createContext, useId, useContext, useState, useRef, useEffect, useMemo } from 'react';
|
|
@@ -150,6 +150,13 @@ var STATIC_I18N_KEYS = [
|
|
|
150
150
|
"selectedPiece.atSquare",
|
|
151
151
|
"selectedPiece.empty",
|
|
152
152
|
"selectedPiece.hasBall",
|
|
153
|
+
"shortcuts.arrows",
|
|
154
|
+
"shortcuts.buttonLabel",
|
|
155
|
+
"shortcuts.cancel",
|
|
156
|
+
"shortcuts.move",
|
|
157
|
+
"shortcuts.pass",
|
|
158
|
+
"shortcuts.select",
|
|
159
|
+
"shortcuts.title",
|
|
153
160
|
"teamBlack",
|
|
154
161
|
"teamWhite",
|
|
155
162
|
"waitingRival",
|
|
@@ -181,6 +188,7 @@ var ROWS = 12;
|
|
|
181
188
|
var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
182
189
|
function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
183
190
|
const t = useGameT();
|
|
191
|
+
const prefersReducedMotion = useReducedMotion();
|
|
184
192
|
const {
|
|
185
193
|
boardState,
|
|
186
194
|
selectedPieceId,
|
|
@@ -191,11 +199,62 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
191
199
|
} = useGameStore();
|
|
192
200
|
const [cursor, setCursor] = useState(null);
|
|
193
201
|
const [disambiguateAt, setDisambiguateAt] = useState(null);
|
|
202
|
+
const [shortcutsOpen, setShortcutsOpen] = useState(false);
|
|
203
|
+
const shortcutsRef = useRef(null);
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
if (!shortcutsOpen) return;
|
|
206
|
+
const onPointerDown = (e) => {
|
|
207
|
+
if (shortcutsRef.current && !shortcutsRef.current.contains(e.target)) {
|
|
208
|
+
setShortcutsOpen(false);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
document.addEventListener("mousedown", onPointerDown);
|
|
212
|
+
return () => document.removeEventListener("mousedown", onPointerDown);
|
|
213
|
+
}, [shortcutsOpen]);
|
|
214
|
+
const [invalidClickAt, setInvalidClickAt] = useState(null);
|
|
215
|
+
const invalidClickNonceRef = useRef(0);
|
|
216
|
+
const triggerInvalidClickShake = (x, y) => {
|
|
217
|
+
invalidClickNonceRef.current += 1;
|
|
218
|
+
setInvalidClickAt({ x, y, nonce: invalidClickNonceRef.current });
|
|
219
|
+
};
|
|
220
|
+
const [hoveredPassAt, setHoveredPassAt] = useState(null);
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
if (!invalidClickAt) return;
|
|
223
|
+
const id = setTimeout(() => setInvalidClickAt(null), 100);
|
|
224
|
+
return () => clearTimeout(id);
|
|
225
|
+
}, [invalidClickAt]);
|
|
194
226
|
const prevBallRef = useRef(boardState?.ball);
|
|
195
227
|
const prevBall = prevBallRef.current;
|
|
196
228
|
useEffect(() => {
|
|
197
229
|
prevBallRef.current = boardState?.ball;
|
|
198
230
|
});
|
|
231
|
+
const prevTurnRef = useRef(boardState?.turn);
|
|
232
|
+
const [replay, setReplay] = useState(null);
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
const prevTurn = prevTurnRef.current;
|
|
235
|
+
prevTurnRef.current = boardState?.turn;
|
|
236
|
+
if (!boardState || prevTurn === boardState.turn) return;
|
|
237
|
+
if (boardState.turn !== userSide) {
|
|
238
|
+
setReplay(null);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (prefersReducedMotion || userSide === null) return;
|
|
242
|
+
const lastEntry = boardState.moveHistory.at(-1);
|
|
243
|
+
if (!lastEntry) return;
|
|
244
|
+
const steps = boardState.moveHistory.filter(
|
|
245
|
+
(m) => m.turnNumber === lastEntry.turnNumber && m.pieceSide !== userSide
|
|
246
|
+
);
|
|
247
|
+
if (steps.length > 1) setReplay({ steps, index: 0 });
|
|
248
|
+
}, [boardState, userSide, prefersReducedMotion]);
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
if (!replay) return;
|
|
251
|
+
if (replay.index >= replay.steps.length - 1) {
|
|
252
|
+
const id2 = setTimeout(() => setReplay(null), 350);
|
|
253
|
+
return () => clearTimeout(id2);
|
|
254
|
+
}
|
|
255
|
+
const id = setTimeout(() => setReplay((r) => r ? { ...r, index: r.index + 1 } : null), 350);
|
|
256
|
+
return () => clearTimeout(id);
|
|
257
|
+
}, [replay]);
|
|
199
258
|
const validMoves = useMemo(() => {
|
|
200
259
|
if (!boardState) return [];
|
|
201
260
|
const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
|
|
@@ -233,7 +292,11 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
233
292
|
}
|
|
234
293
|
const ballCarrier = ball.holderId ? boardState.pieces.find((p) => p.id === ball.holderId) ?? null : null;
|
|
235
294
|
const isOffsideRisk = !!ballCarrier && ballCarrier.type !== "king" && userSide !== null && ballCarrier.side === userSide && boardState.turn === userSide && isInEnemyArea(ballCarrier.pos, ballCarrier.side);
|
|
295
|
+
const passCarrier = boardState.pieces.find((p) => p.id === selectedPieceId && boardState.ball.holderId === p.id) ?? null;
|
|
296
|
+
const trajectoryPath = hoveredPassAt && passCarrier ? passCarrier.type === "knight" ? [hoveredPassAt] : getPath(passCarrier.pos, hoveredPassAt) : null;
|
|
297
|
+
const trajectoryIntercepted = !!(trajectoryPath && passCarrier && trajectoryPath.some((sq) => boardState.pieces.some((p) => p.pos.x === sq.x && p.pos.y === sq.y && p.side !== passCarrier.side)));
|
|
236
298
|
const handleSquareClick = (x, y) => {
|
|
299
|
+
if (replay) return;
|
|
237
300
|
setCursor(null);
|
|
238
301
|
const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
|
|
239
302
|
const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
|
|
@@ -244,15 +307,21 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
244
307
|
if (isValidMove && selectedPieceId) {
|
|
245
308
|
movePiece(selectedPieceId, { x, y });
|
|
246
309
|
setDisambiguateAt(null);
|
|
310
|
+
setInvalidClickAt(null);
|
|
247
311
|
} else if (isValidPass) {
|
|
248
312
|
passBall({ x, y });
|
|
249
313
|
setDisambiguateAt(null);
|
|
314
|
+
setInvalidClickAt(null);
|
|
250
315
|
} else {
|
|
316
|
+
if (selectedPieceId) {
|
|
317
|
+
triggerInvalidClickShake(x, y);
|
|
318
|
+
}
|
|
251
319
|
setDisambiguateAt(null);
|
|
252
320
|
}
|
|
253
321
|
};
|
|
254
322
|
const handlePieceClick = (pieceId, x, y, e) => {
|
|
255
323
|
e.stopPropagation();
|
|
324
|
+
if (replay) return;
|
|
256
325
|
const pieceAt = boardState.pieces.find((p) => p.id === pieceId);
|
|
257
326
|
if (!pieceAt) return;
|
|
258
327
|
const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
|
|
@@ -266,6 +335,10 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
266
335
|
setSelectedPieceId(pieceAt.id);
|
|
267
336
|
}
|
|
268
337
|
setDisambiguateAt(null);
|
|
338
|
+
setInvalidClickAt(null);
|
|
339
|
+
} else if (selectedPieceId) {
|
|
340
|
+
triggerInvalidClickShake(x, y);
|
|
341
|
+
setDisambiguateAt(null);
|
|
269
342
|
}
|
|
270
343
|
};
|
|
271
344
|
const defaultCursor = () => {
|
|
@@ -282,7 +355,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
282
355
|
}
|
|
283
356
|
};
|
|
284
357
|
const handleKeyDown = (e) => {
|
|
285
|
-
if (!keyboardNav) return;
|
|
358
|
+
if (!keyboardNav || replay) return;
|
|
286
359
|
const step = (dx, dy) => {
|
|
287
360
|
e.preventDefault();
|
|
288
361
|
setCursor((c) => {
|
|
@@ -326,7 +399,9 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
326
399
|
}
|
|
327
400
|
break;
|
|
328
401
|
case "Escape":
|
|
329
|
-
if (
|
|
402
|
+
if (shortcutsOpen) {
|
|
403
|
+
setShortcutsOpen(false);
|
|
404
|
+
} else if (disambiguateAt) {
|
|
330
405
|
setDisambiguateAt(null);
|
|
331
406
|
} else {
|
|
332
407
|
setSelectedPieceId(null);
|
|
@@ -344,8 +419,9 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
344
419
|
const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
|
|
345
420
|
const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
|
|
346
421
|
const isAmbiguous = isValidMove && isValidPass;
|
|
347
|
-
const
|
|
348
|
-
const
|
|
422
|
+
const displayedLastMove = replay ? replay.steps[replay.index] : lastMove;
|
|
423
|
+
const isLastMoveOrigin = displayedLastMove?.from && displayedLastMove.from.x === x && displayedLastMove.from.y === y;
|
|
424
|
+
const isLastMoveDest = displayedLastMove?.to && displayedLastMove.to.x === x && displayedLastMove.to.y === y;
|
|
349
425
|
const isLastMove = isLastMoveOrigin || isLastMoveDest;
|
|
350
426
|
const isEnemyGoalArea = userSide !== null && (x >= 2 && x <= 6) && (userSide === "white" ? y >= 10 && y <= 11 : y >= 0 && y <= 1);
|
|
351
427
|
const pieceAt = boardState.pieces.find((p) => p.pos.x === x && p.pos.y === y);
|
|
@@ -356,6 +432,10 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
356
432
|
"div",
|
|
357
433
|
{
|
|
358
434
|
onClick: () => handleSquareClick(x, y),
|
|
435
|
+
onMouseEnter: () => {
|
|
436
|
+
if (isValidPass) setHoveredPassAt({ x, y });
|
|
437
|
+
},
|
|
438
|
+
onMouseLeave: () => setHoveredPassAt(null),
|
|
359
439
|
className: cn(
|
|
360
440
|
"relative aspect-square w-full flex items-center justify-center cursor-pointer",
|
|
361
441
|
isEven ? "bg-field-green-1" : "bg-field-green-2",
|
|
@@ -463,7 +543,21 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
463
543
|
/* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "1.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
|
|
464
544
|
/* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "10.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
|
|
465
545
|
/* @__PURE__ */ jsx("path", { d: "M 3.086,2 A 1.5,1.5 0 0 0 5.914,2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
|
|
466
|
-
/* @__PURE__ */ jsx("path", { d: "M 3.086,10 A 1.5,1.5 0 0 1 5.914,10", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" })
|
|
546
|
+
/* @__PURE__ */ jsx("path", { d: "M 3.086,10 A 1.5,1.5 0 0 1 5.914,10", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
|
|
547
|
+
hoveredPassAt && passCarrier && /* @__PURE__ */ jsx(
|
|
548
|
+
"line",
|
|
549
|
+
{
|
|
550
|
+
"data-testid": "pass-trajectory-line",
|
|
551
|
+
x1: passCarrier.pos.x + 0.5,
|
|
552
|
+
y1: ROWS - 1 - passCarrier.pos.y + 0.5,
|
|
553
|
+
x2: hoveredPassAt.x + 0.5,
|
|
554
|
+
y2: ROWS - 1 - hoveredPassAt.y + 0.5,
|
|
555
|
+
stroke: trajectoryIntercepted ? "var(--danger)" : "var(--pass-highlight)",
|
|
556
|
+
strokeWidth: "0.08",
|
|
557
|
+
strokeDasharray: "0.15 0.1",
|
|
558
|
+
strokeLinecap: "round"
|
|
559
|
+
}
|
|
560
|
+
)
|
|
467
561
|
]
|
|
468
562
|
}
|
|
469
563
|
),
|
|
@@ -536,12 +630,92 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
|
|
|
536
630
|
},
|
|
537
631
|
"ball"
|
|
538
632
|
);
|
|
539
|
-
})()
|
|
633
|
+
})(),
|
|
634
|
+
invalidClickAt && /* @__PURE__ */ jsx(
|
|
635
|
+
motion.div,
|
|
636
|
+
{
|
|
637
|
+
"data-testid": "invalid-action-shake",
|
|
638
|
+
"aria-hidden": "true",
|
|
639
|
+
initial: false,
|
|
640
|
+
animate: prefersReducedMotion ? { opacity: [0.9, 0] } : { x: [0, -3, 3, -3, 3, 0] },
|
|
641
|
+
transition: { duration: 0.08 },
|
|
642
|
+
style: {
|
|
643
|
+
position: "absolute",
|
|
644
|
+
left: `${invalidClickAt.x / COLS * 100}%`,
|
|
645
|
+
top: `${(ROWS - 1 - invalidClickAt.y) / ROWS * 100}%`,
|
|
646
|
+
width: `${100 / COLS}%`,
|
|
647
|
+
height: `${100 / ROWS}%`
|
|
648
|
+
},
|
|
649
|
+
className: "ring-2 ring-inset ring-danger/70 rounded-sm"
|
|
650
|
+
},
|
|
651
|
+
invalidClickAt.nonce
|
|
652
|
+
)
|
|
540
653
|
] }),
|
|
541
654
|
isOffsideRisk && /* @__PURE__ */ jsxs("div", { className: "absolute bottom-2 left-1/2 -translate-x-1/2 z-30 flex items-center gap-1.5 px-3 py-1.5 bg-warning/20 border border-warning/50 rounded-full font-inter text-xs font-semibold text-warning pointer-events-none whitespace-nowrap backdrop-blur-sm", children: [
|
|
542
655
|
"\u26A0 ",
|
|
543
656
|
t("offsideWarning")
|
|
544
|
-
] })
|
|
657
|
+
] }),
|
|
658
|
+
keyboardNav && /* @__PURE__ */ jsxs(
|
|
659
|
+
"div",
|
|
660
|
+
{
|
|
661
|
+
ref: shortcutsRef,
|
|
662
|
+
className: "hidden md:block absolute top-2 right-2 z-40",
|
|
663
|
+
onKeyDown: (e) => e.stopPropagation(),
|
|
664
|
+
children: [
|
|
665
|
+
/* @__PURE__ */ jsx(
|
|
666
|
+
"button",
|
|
667
|
+
{
|
|
668
|
+
type: "button",
|
|
669
|
+
onClick: (e) => {
|
|
670
|
+
e.stopPropagation();
|
|
671
|
+
setShortcutsOpen((o) => !o);
|
|
672
|
+
},
|
|
673
|
+
"aria-label": t("shortcuts.buttonLabel"),
|
|
674
|
+
"aria-expanded": shortcutsOpen,
|
|
675
|
+
className: "w-11 h-11 flex items-center justify-center rounded-full bg-bg-secondary/80 border border-border-subtle text-fg-muted hover:text-fg-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-green backdrop-blur-sm",
|
|
676
|
+
children: /* @__PURE__ */ jsx(HelpCircle, { size: 16, strokeWidth: 2, "aria-hidden": "true" })
|
|
677
|
+
}
|
|
678
|
+
),
|
|
679
|
+
shortcutsOpen && /* @__PURE__ */ jsxs(
|
|
680
|
+
"div",
|
|
681
|
+
{
|
|
682
|
+
className: "absolute top-full right-0 mt-1 z-50 w-max max-w-[220px] bg-bg-secondary border border-border-subtle rounded-md shadow-xl p-2.5 pointer-events-auto",
|
|
683
|
+
onClick: (e) => e.stopPropagation(),
|
|
684
|
+
children: [
|
|
685
|
+
/* @__PURE__ */ jsx("p", { className: "font-inter text-[11px] font-semibold text-fg-primary mb-1.5", children: t("shortcuts.title") }),
|
|
686
|
+
/* @__PURE__ */ jsxs("ul", { className: "font-mono text-[10px] text-fg-secondary leading-relaxed", children: [
|
|
687
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
688
|
+
/* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "\u2191\u2193\u2190\u2192" }),
|
|
689
|
+
" ",
|
|
690
|
+
t("shortcuts.arrows")
|
|
691
|
+
] }),
|
|
692
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
693
|
+
/* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "Enter" }),
|
|
694
|
+
" ",
|
|
695
|
+
t("shortcuts.select")
|
|
696
|
+
] }),
|
|
697
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
698
|
+
/* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "M" }),
|
|
699
|
+
" ",
|
|
700
|
+
t("shortcuts.move")
|
|
701
|
+
] }),
|
|
702
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
703
|
+
/* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "P" }),
|
|
704
|
+
" ",
|
|
705
|
+
t("shortcuts.pass")
|
|
706
|
+
] }),
|
|
707
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
708
|
+
/* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "Esc" }),
|
|
709
|
+
" ",
|
|
710
|
+
t("shortcuts.cancel")
|
|
711
|
+
] })
|
|
712
|
+
] })
|
|
713
|
+
]
|
|
714
|
+
}
|
|
715
|
+
)
|
|
716
|
+
]
|
|
717
|
+
}
|
|
718
|
+
)
|
|
545
719
|
] })
|
|
546
720
|
}
|
|
547
721
|
);
|
|
@@ -587,31 +761,50 @@ function AvatarFallback({ className, children }) {
|
|
|
587
761
|
}
|
|
588
762
|
function ActionPoints({ total, remaining, size = 20, className, kingMustRelease }) {
|
|
589
763
|
const t = useGameT();
|
|
764
|
+
const prevRemainingRef = useRef(remaining);
|
|
765
|
+
const [spentRange, setSpentRange] = useState(null);
|
|
766
|
+
useEffect(() => {
|
|
767
|
+
const prev = prevRemainingRef.current;
|
|
768
|
+
prevRemainingRef.current = remaining;
|
|
769
|
+
if (remaining < prev) {
|
|
770
|
+
setSpentRange([remaining, prev - 1]);
|
|
771
|
+
const id = setTimeout(() => setSpentRange(null), 300);
|
|
772
|
+
return () => clearTimeout(id);
|
|
773
|
+
}
|
|
774
|
+
setSpentRange(null);
|
|
775
|
+
}, [remaining]);
|
|
590
776
|
return /* @__PURE__ */ jsx("div", { className: cn("flex items-center gap-1.5", className), "aria-label": t("actionPointsAriaLabel", { remaining, total }), children: Array.from({ length: total }).map((_, i) => {
|
|
591
777
|
const isActive = i < remaining;
|
|
592
778
|
const showCrown = kingMustRelease && i === total - 1;
|
|
593
|
-
|
|
594
|
-
return /* @__PURE__ */ jsx(
|
|
595
|
-
Crown,
|
|
596
|
-
{
|
|
597
|
-
size,
|
|
598
|
-
strokeWidth: 2,
|
|
599
|
-
className: "text-yellow-400 fill-yellow-400 transition-colors duration-150",
|
|
600
|
-
"aria-hidden": "true"
|
|
601
|
-
},
|
|
602
|
-
i
|
|
603
|
-
);
|
|
604
|
-
}
|
|
779
|
+
const justSpent = !!spentRange && i >= spentRange[0] && i <= spentRange[1];
|
|
605
780
|
return /* @__PURE__ */ jsx(
|
|
606
|
-
|
|
781
|
+
motion.span,
|
|
607
782
|
{
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
className:
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
783
|
+
"data-testid": `ap-pip-${i}`,
|
|
784
|
+
"data-spent": justSpent || void 0,
|
|
785
|
+
className: "inline-flex",
|
|
786
|
+
animate: justSpent ? { scale: [1.5, 1] } : void 0,
|
|
787
|
+
transition: { duration: 0.3 },
|
|
788
|
+
children: showCrown ? /* @__PURE__ */ jsx(
|
|
789
|
+
Crown,
|
|
790
|
+
{
|
|
791
|
+
size,
|
|
792
|
+
strokeWidth: 2,
|
|
793
|
+
className: "text-yellow-400 fill-yellow-400 transition-colors duration-150",
|
|
794
|
+
"aria-hidden": "true"
|
|
795
|
+
}
|
|
796
|
+
) : /* @__PURE__ */ jsx(
|
|
797
|
+
Zap,
|
|
798
|
+
{
|
|
799
|
+
size,
|
|
800
|
+
strokeWidth: 2,
|
|
801
|
+
className: cn(
|
|
802
|
+
"transition-colors duration-150",
|
|
803
|
+
isActive ? "text-accent-green fill-accent-green" : "text-fg-muted fill-none"
|
|
804
|
+
),
|
|
805
|
+
"aria-hidden": "true"
|
|
806
|
+
}
|
|
807
|
+
)
|
|
615
808
|
},
|
|
616
809
|
i
|
|
617
810
|
);
|