@scriptonita/chess-football-ui 0.7.0 → 0.9.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/README.md +32 -0
- package/dist/{chunk-LTFNSTAQ.cjs → chunk-BSE4DUXM.cjs} +18 -5
- package/dist/chunk-BSE4DUXM.cjs.map +1 -0
- package/dist/{chunk-N24LGLLO.js → chunk-C5HCN7O5.js} +18 -6
- package/dist/chunk-C5HCN7O5.js.map +1 -0
- package/dist/index.cjs +382 -104
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -7
- package/dist/index.d.ts +77 -7
- package/dist/index.js +378 -103
- package/dist/index.js.map +1 -1
- package/dist/store.cjs +7 -3
- package/dist/store.d.cts +9 -1
- package/dist/store.d.ts +9 -1
- package/dist/store.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-LTFNSTAQ.cjs.map +0 -1
- package/dist/chunk-N24LGLLO.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkBSE4DUXM_cjs = require('./chunk-BSE4DUXM.cjs');
|
|
4
|
+
var shallow = require('zustand/react/shallow');
|
|
4
5
|
var chessFootballEngine = require('@scriptonita/chess-football-engine');
|
|
5
6
|
var clsx = require('clsx');
|
|
6
7
|
var tailwindMerge = require('tailwind-merge');
|
|
@@ -104,7 +105,10 @@ function GamePiece({ piece, isSelected, hasBall, onClick, isMoving = false }) {
|
|
|
104
105
|
style: piece.hasMovedThisTurn ? MOVED_STYLE : void 0,
|
|
105
106
|
className: cn(
|
|
106
107
|
"relative w-[88%] h-[88%] rounded-full cursor-pointer z-10 transition-transform duration-200 ease-out motion-reduce:transition-none",
|
|
107
|
-
|
|
108
|
+
// `--primary` was the package's one remaining shadcn token, and
|
|
109
|
+
// CrazyGames had to define it by hand just to make the selection
|
|
110
|
+
// ring visible. It is part of the design system now.
|
|
111
|
+
isSelected && "ring-4 ring-accent-green scale-110 z-30",
|
|
108
112
|
isMoving && "scale-[1.08] z-40",
|
|
109
113
|
piece.hasMovedThisTurn && "opacity-70"
|
|
110
114
|
),
|
|
@@ -190,6 +194,7 @@ var STATIC_I18N_KEYS = [
|
|
|
190
194
|
"actionPointsAriaLabel",
|
|
191
195
|
"actionPointsShort",
|
|
192
196
|
"ballHolder",
|
|
197
|
+
"boardAriaLabel",
|
|
193
198
|
"endTurn",
|
|
194
199
|
"endTurnConfirm",
|
|
195
200
|
"endTurnConfirmDescription",
|
|
@@ -203,11 +208,13 @@ var STATIC_I18N_KEYS = [
|
|
|
203
208
|
"history.pass",
|
|
204
209
|
"history.tackle",
|
|
205
210
|
"history.title",
|
|
211
|
+
"history.close",
|
|
206
212
|
"history.turn",
|
|
207
213
|
"history.viewAll",
|
|
208
214
|
"move",
|
|
209
215
|
"offsideWarning",
|
|
210
216
|
"pass",
|
|
217
|
+
"scoreAriaLabel",
|
|
211
218
|
"selectedPiece.alreadyMoved",
|
|
212
219
|
"selectedPiece.atSquare",
|
|
213
220
|
"selectedPiece.empty",
|
|
@@ -219,6 +226,14 @@ var STATIC_I18N_KEYS = [
|
|
|
219
226
|
"shortcuts.pass",
|
|
220
227
|
"shortcuts.select",
|
|
221
228
|
"shortcuts.title",
|
|
229
|
+
"square.empty",
|
|
230
|
+
"square.emptyWithBall",
|
|
231
|
+
"square.moveOrPassTarget",
|
|
232
|
+
"square.moveTarget",
|
|
233
|
+
"square.passTarget",
|
|
234
|
+
"square.piece",
|
|
235
|
+
"square.pieceWithBall",
|
|
236
|
+
"square.selected",
|
|
222
237
|
"teamBlack",
|
|
223
238
|
"teamWhite",
|
|
224
239
|
"waitingRival",
|
|
@@ -228,6 +243,7 @@ var PIECE_I18N_KEYS = [...Object.values(chessFootballEngine.SHORT_KEY), ...Objec
|
|
|
228
243
|
(label) => `pieces.${label}`
|
|
229
244
|
);
|
|
230
245
|
var EVENT_TOAST_I18N_KEYS = [
|
|
246
|
+
"eventToast.goal",
|
|
231
247
|
"eventToast.interception",
|
|
232
248
|
"eventToast.offside",
|
|
233
249
|
"eventToast.tackle"
|
|
@@ -237,7 +253,13 @@ var GAME_I18N_KEYS = [
|
|
|
237
253
|
...PIECE_I18N_KEYS,
|
|
238
254
|
...EVENT_TOAST_I18N_KEYS
|
|
239
255
|
];
|
|
240
|
-
var
|
|
256
|
+
var nodeEnv = globalThis.process?.env?.NODE_ENV;
|
|
257
|
+
var fallback = (key) => {
|
|
258
|
+
if (nodeEnv && nodeEnv !== "production") {
|
|
259
|
+
console.warn(`[chess-football-ui] missing game i18n key: "${key}" (no GameI18nProvider?)`);
|
|
260
|
+
}
|
|
261
|
+
return key;
|
|
262
|
+
};
|
|
241
263
|
var GameI18nContext = React.createContext(fallback);
|
|
242
264
|
function GameI18nProvider({ t, children }) {
|
|
243
265
|
return /* @__PURE__ */ jsxRuntime.jsx(GameI18nContext.Provider, { value: t, children: /* @__PURE__ */ jsxRuntime.jsx(framerMotion.MotionConfig, { reducedMotion: "user", children }) });
|
|
@@ -247,7 +269,7 @@ function useGameT() {
|
|
|
247
269
|
}
|
|
248
270
|
function BallHolderChip({ className }) {
|
|
249
271
|
const t = useGameT();
|
|
250
|
-
const boardState =
|
|
272
|
+
const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
|
|
251
273
|
const holder = boardState?.pieces.find((p) => p.id === boardState.ball.holderId) ?? null;
|
|
252
274
|
if (!holder) return null;
|
|
253
275
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex items-center gap-1.5 min-h-[1.75rem]", className), "aria-live": "polite", children: [
|
|
@@ -442,17 +464,29 @@ var cellX = (x) => `${x * 100}%`;
|
|
|
442
464
|
var cellY = (y) => `${(ROWS - 1 - y) * 100}%`;
|
|
443
465
|
var PIECE_SLIDE = { type: "spring", stiffness: 190, damping: 28, mass: 1 };
|
|
444
466
|
var BALL_SLIDE = { type: "spring", stiffness: 300, damping: 26, mass: 0.6 };
|
|
445
|
-
function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, toolbar = true }) {
|
|
467
|
+
function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, toolbar = true, className }) {
|
|
446
468
|
const t = useGameT();
|
|
469
|
+
const boardId = React.useId();
|
|
447
470
|
const prefersReducedMotion = framerMotion.useReducedMotion();
|
|
448
471
|
const {
|
|
449
472
|
boardState,
|
|
450
473
|
selectedPieceId,
|
|
474
|
+
interactionMode,
|
|
451
475
|
setSelectedPieceId,
|
|
452
476
|
setInteractionMode,
|
|
453
477
|
movePiece,
|
|
454
478
|
passBall
|
|
455
|
-
} =
|
|
479
|
+
} = chunkBSE4DUXM_cjs.useGameStore(
|
|
480
|
+
shallow.useShallow((s) => ({
|
|
481
|
+
boardState: s.boardState,
|
|
482
|
+
selectedPieceId: s.selectedPieceId,
|
|
483
|
+
interactionMode: s.interactionMode,
|
|
484
|
+
setSelectedPieceId: s.setSelectedPieceId,
|
|
485
|
+
setInteractionMode: s.setInteractionMode,
|
|
486
|
+
movePiece: s.movePiece,
|
|
487
|
+
passBall: s.passBall
|
|
488
|
+
}))
|
|
489
|
+
);
|
|
456
490
|
const [cursor, setCursor] = React.useState(null);
|
|
457
491
|
const [disambiguateAt, setDisambiguateAt] = React.useState(null);
|
|
458
492
|
const [shortcutsOpen, setShortcutsOpen] = React.useState(false);
|
|
@@ -520,21 +554,21 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
520
554
|
return () => clearTimeout(id);
|
|
521
555
|
}, [replay]);
|
|
522
556
|
const validMoves = React.useMemo(() => {
|
|
523
|
-
if (!boardState) return [];
|
|
557
|
+
if (!boardState || interactionMode === "pass") return [];
|
|
524
558
|
const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
|
|
525
559
|
if (sp && !sp.hasMovedThisTurn) {
|
|
526
560
|
return chessFootballEngine.getValidMoves(sp, boardState);
|
|
527
561
|
}
|
|
528
562
|
return [];
|
|
529
|
-
}, [boardState, selectedPieceId]);
|
|
563
|
+
}, [boardState, selectedPieceId, interactionMode]);
|
|
530
564
|
const validPasses = React.useMemo(() => {
|
|
531
|
-
if (!boardState) return [];
|
|
565
|
+
if (!boardState || interactionMode === "move") return [];
|
|
532
566
|
const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
|
|
533
567
|
if (sp && boardState.ball.holderId === sp.id) {
|
|
534
568
|
return chessFootballEngine.getValidPasses(sp, boardState);
|
|
535
569
|
}
|
|
536
570
|
return [];
|
|
537
|
-
}, [boardState, selectedPieceId]);
|
|
571
|
+
}, [boardState, selectedPieceId, interactionMode]);
|
|
538
572
|
if (!boardState) return null;
|
|
539
573
|
const lastMove = boardState.lastMove;
|
|
540
574
|
const ball = boardState.ball;
|
|
@@ -648,8 +682,11 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
648
682
|
activate(c.x, c.y);
|
|
649
683
|
break;
|
|
650
684
|
}
|
|
685
|
+
// Bare M / P only: Cmd+P is Print and Ctrl+M is a browser shortcut,
|
|
686
|
+
// and both used to resolve the popover behind the user's back.
|
|
651
687
|
case "m":
|
|
652
688
|
case "M":
|
|
689
|
+
if (e.ctrlKey || e.metaKey || e.altKey) break;
|
|
653
690
|
if (disambiguateAt && selectedPieceId) {
|
|
654
691
|
movePiece(selectedPieceId, disambiguateAt);
|
|
655
692
|
setDisambiguateAt(null);
|
|
@@ -657,6 +694,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
657
694
|
break;
|
|
658
695
|
case "p":
|
|
659
696
|
case "P":
|
|
697
|
+
if (e.ctrlKey || e.metaKey || e.altKey) break;
|
|
660
698
|
if (disambiguateAt) {
|
|
661
699
|
passBall(disambiguateAt);
|
|
662
700
|
setDisambiguateAt(null);
|
|
@@ -674,9 +712,28 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
674
712
|
break;
|
|
675
713
|
}
|
|
676
714
|
};
|
|
677
|
-
const
|
|
678
|
-
|
|
715
|
+
const cellId = (x, y) => `${boardId}-sq-${x}-${y}`;
|
|
716
|
+
const squareLabel = (x, y, pieceAt, isValidMove, isValidPass) => {
|
|
717
|
+
const square = chessFootballEngine.squareName({ x, y });
|
|
718
|
+
const ballHere = ball.pos.x === x && ball.pos.y === y;
|
|
719
|
+
const parts = [];
|
|
720
|
+
if (pieceAt) {
|
|
721
|
+
const piece = t(`pieces.${chessFootballEngine.LONG_KEY[pieceAt.type]}`);
|
|
722
|
+
const team = pieceAt.side === "white" ? t("teamWhite") : t("teamBlack");
|
|
723
|
+
parts.push(t(ballHere ? "square.pieceWithBall" : "square.piece", { square, piece, team }));
|
|
724
|
+
} else {
|
|
725
|
+
parts.push(t(ballHere ? "square.emptyWithBall" : "square.empty", { square }));
|
|
726
|
+
}
|
|
727
|
+
if (isValidMove && isValidPass) parts.push(t("square.moveOrPassTarget"));
|
|
728
|
+
else if (isValidMove) parts.push(t("square.moveTarget"));
|
|
729
|
+
else if (isValidPass) parts.push(t("square.passTarget"));
|
|
730
|
+
if (pieceAt && pieceAt.id === selectedPieceId) parts.push(t("square.selected"));
|
|
731
|
+
return parts.join(", ");
|
|
732
|
+
};
|
|
733
|
+
const renderRows = () => {
|
|
734
|
+
const rows = [];
|
|
679
735
|
for (let y = ROWS - 1; y >= 0; y--) {
|
|
736
|
+
const squares = [];
|
|
680
737
|
for (let x = 0; x < COLS; x++) {
|
|
681
738
|
const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
|
|
682
739
|
const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
|
|
@@ -693,27 +750,38 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
693
750
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
694
751
|
"div",
|
|
695
752
|
{
|
|
753
|
+
id: cellId(x, y),
|
|
754
|
+
role: "gridcell",
|
|
755
|
+
"aria-label": squareLabel(x, y, pieceAt, isValidMove, isValidPass),
|
|
756
|
+
"aria-selected": !!pieceAt && pieceAt.id === selectedPieceId,
|
|
696
757
|
onClick: () => handleSquareClick(x, y),
|
|
697
758
|
onMouseEnter: () => {
|
|
698
759
|
if (isValidPass) setHoveredPassAt({ x, y });
|
|
699
760
|
},
|
|
700
761
|
onMouseLeave: () => setHoveredPassAt(null),
|
|
762
|
+
onTouchStart: () => {
|
|
763
|
+
if (isValidPass) setHoveredPassAt({ x, y });
|
|
764
|
+
},
|
|
701
765
|
className: cn(
|
|
702
766
|
pitchSquareClass(x, y),
|
|
703
767
|
"flex items-center justify-center cursor-pointer",
|
|
704
768
|
!isValidMove && !isValidPass && isLastMove && "bg-last-move-highlight/20",
|
|
705
769
|
// §8: pulsing warning ring on enemy goal area
|
|
706
770
|
isOffsideRisk && isEnemyGoalArea && !isValidMove && !isValidPass && "ring-2 ring-inset ring-warning/60",
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
771
|
+
// Move and pass used raw yellow-400/sky-400 here while
|
|
772
|
+
// the popover a few lines below used the
|
|
773
|
+
// move-highlight/pass-highlight tokens — two palettes
|
|
774
|
+
// for the same two concepts. One palette now: tokens.
|
|
775
|
+
isAmbiguous && "ring-[3px] ring-inset ring-move-highlight/90 bg-move-highlight/20",
|
|
776
|
+
!isAmbiguous && isValidMove && "ring-[3px] ring-inset ring-move-highlight/80 bg-move-highlight/15",
|
|
777
|
+
!isAmbiguous && isValidPass && "ring-[3px] ring-inset ring-pass-highlight/80 bg-pass-highlight/15"
|
|
710
778
|
),
|
|
711
779
|
children: [
|
|
712
|
-
!isAmbiguous && isValidMove && !pieceAt && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-full bg-
|
|
713
|
-
!isAmbiguous && isValidPass && !pieceAt && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-full
|
|
714
|
-
isAmbiguous && !pieceAt && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-0.5", children: [
|
|
715
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2.5 h-2.5 rounded-full bg-
|
|
716
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2.5 h-2.5 rounded-full
|
|
780
|
+
!isAmbiguous && isValidMove && !pieceAt && /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "move-marker", className: "w-3 h-3 rounded-full bg-move-highlight/65" }),
|
|
781
|
+
!isAmbiguous && isValidPass && !pieceAt && /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "pass-marker", className: "w-3.5 h-3.5 rounded-full border-2 border-pass-highlight/80" }),
|
|
782
|
+
isAmbiguous && !pieceAt && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5", children: [
|
|
783
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "move-marker", className: "w-2.5 h-2.5 rounded-full bg-move-highlight/80" }),
|
|
784
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "pass-marker", className: "w-2.5 h-2.5 rounded-full border-2 border-pass-highlight/90" })
|
|
717
785
|
] }),
|
|
718
786
|
isCursor && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 ring-2 ring-inset ring-white pointer-events-none z-20", "aria-hidden": "true" }),
|
|
719
787
|
isDisambiguateTarget && selectedPieceId && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -766,26 +834,27 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
766
834
|
)
|
|
767
835
|
);
|
|
768
836
|
}
|
|
837
|
+
rows.push(
|
|
838
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { role: "row", className: "contents", children: squares }, `row-${y}`)
|
|
839
|
+
);
|
|
769
840
|
}
|
|
770
|
-
return
|
|
841
|
+
return rows;
|
|
771
842
|
};
|
|
772
843
|
const board = /* @__PURE__ */ jsxRuntime.jsxs(
|
|
773
844
|
"div",
|
|
774
845
|
{
|
|
846
|
+
"data-testid": "board-root",
|
|
775
847
|
className: cn(
|
|
776
848
|
// §6: Mobile edge-to-edge — thin 2px margin, no border, no radius
|
|
777
|
-
|
|
849
|
+
// The frame was hardcoded #1a3317 in three files, hand-tuned for
|
|
850
|
+
// the webapp's darker grass and visibly wrong against the
|
|
851
|
+
// CrazyGames pitch. It is a token now (see README).
|
|
852
|
+
"w-full mx-0.5 bg-field-frame overflow-hidden",
|
|
778
853
|
// §5+§6: Desktop — centered, rounded, bordered, height-based max-width
|
|
779
|
-
"md:mx-auto md:rounded-xl md:shadow-2xl md:border-4 md:border-
|
|
854
|
+
"md:mx-auto md:rounded-xl md:shadow-2xl md:border-4 md:border-field-frame",
|
|
780
855
|
"md:max-w-[min(700px,calc((100dvh_-_150px)*0.75))]",
|
|
781
|
-
|
|
856
|
+
!showCoordinates && className
|
|
782
857
|
),
|
|
783
|
-
...keyboardNav ? {
|
|
784
|
-
tabIndex: 0,
|
|
785
|
-
role: "application",
|
|
786
|
-
"aria-label": "Game board \u2014 arrow keys to move the cursor, Enter to act",
|
|
787
|
-
onKeyDown: handleKeyDown
|
|
788
|
-
} : {},
|
|
789
858
|
children: [
|
|
790
859
|
toolbar && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "hidden md:flex items-center justify-between gap-2 px-2.5 py-1", "data-testid": "board-toolbar", children: [
|
|
791
860
|
/* @__PURE__ */ jsxRuntime.jsx(BallHolderChip, {}),
|
|
@@ -794,7 +863,12 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
794
863
|
{
|
|
795
864
|
ref: shortcutsRef,
|
|
796
865
|
className: "relative",
|
|
797
|
-
onKeyDown: (e) =>
|
|
866
|
+
onKeyDown: (e) => {
|
|
867
|
+
if (e.key === "Escape" && shortcutsOpen) {
|
|
868
|
+
e.stopPropagation();
|
|
869
|
+
setShortcutsOpen(false);
|
|
870
|
+
}
|
|
871
|
+
},
|
|
798
872
|
children: [
|
|
799
873
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
800
874
|
"button",
|
|
@@ -826,7 +900,25 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
826
900
|
)
|
|
827
901
|
] }),
|
|
828
902
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", style: { containerType: "inline-size" }, children: [
|
|
829
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
903
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
904
|
+
"div",
|
|
905
|
+
{
|
|
906
|
+
role: "grid",
|
|
907
|
+
"aria-label": t("boardAriaLabel"),
|
|
908
|
+
"aria-rowcount": ROWS,
|
|
909
|
+
"aria-colcount": COLS,
|
|
910
|
+
className: cn(
|
|
911
|
+
"grid grid-cols-9 gap-[1px]",
|
|
912
|
+
keyboardNav && "focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent-green"
|
|
913
|
+
),
|
|
914
|
+
...keyboardNav ? {
|
|
915
|
+
tabIndex: 0,
|
|
916
|
+
onKeyDown: handleKeyDown,
|
|
917
|
+
"aria-activedescendant": cursor ? cellId(cursor.x, cursor.y) : void 0
|
|
918
|
+
} : {},
|
|
919
|
+
children: renderRows()
|
|
920
|
+
}
|
|
921
|
+
),
|
|
830
922
|
/* @__PURE__ */ jsxRuntime.jsx(GrassOverlay, {}),
|
|
831
923
|
/* @__PURE__ */ jsxRuntime.jsx(PitchMarkings, {}),
|
|
832
924
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -852,7 +944,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
852
944
|
)
|
|
853
945
|
}
|
|
854
946
|
),
|
|
855
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 pointer-events-none", "data-testid": "pieces-layer", children: [
|
|
947
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 pointer-events-none", "data-testid": "pieces-layer", "aria-hidden": "true", children: [
|
|
856
948
|
/* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { mode: "sync", children: boardState.pieces.map((piece) => {
|
|
857
949
|
const isPickupCarrier = pickupCarrierId === piece.id;
|
|
858
950
|
const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : PIECE_SLIDE;
|
|
@@ -958,7 +1050,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
958
1050
|
);
|
|
959
1051
|
if (!showCoordinates) return board;
|
|
960
1052
|
const ranksTopToBottom = [...chessFootballEngine.RANK_LABELS].reverse();
|
|
961
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full max-w-[540px] md:max-w-none mx-auto",
|
|
1053
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("w-full max-w-[540px] md:max-w-none mx-auto", className), children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
962
1054
|
"div",
|
|
963
1055
|
{
|
|
964
1056
|
className: "grid gap-1 select-none",
|
|
@@ -990,8 +1082,20 @@ function Avatar({ className, children }) {
|
|
|
990
1082
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("relative w-9 h-9 rounded-full overflow-hidden shrink-0", className), children });
|
|
991
1083
|
}
|
|
992
1084
|
function AvatarImage({ src, alt, className }) {
|
|
993
|
-
|
|
994
|
-
|
|
1085
|
+
const [failed, setFailed] = React.useState(false);
|
|
1086
|
+
React.useEffect(() => {
|
|
1087
|
+
setFailed(false);
|
|
1088
|
+
}, [src]);
|
|
1089
|
+
if (!src || failed) return null;
|
|
1090
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1091
|
+
"img",
|
|
1092
|
+
{
|
|
1093
|
+
src,
|
|
1094
|
+
alt: alt ?? "",
|
|
1095
|
+
onError: () => setFailed(true),
|
|
1096
|
+
className: cn("w-full h-full object-cover", className)
|
|
1097
|
+
}
|
|
1098
|
+
);
|
|
995
1099
|
}
|
|
996
1100
|
function AvatarFallback({ className, children }) {
|
|
997
1101
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("absolute inset-0 flex items-center justify-center text-xs font-semibold", className), children });
|
|
@@ -1047,6 +1151,51 @@ function ActionPoints({ total, remaining, size = 20, className, kingMustRelease
|
|
|
1047
1151
|
);
|
|
1048
1152
|
}) });
|
|
1049
1153
|
}
|
|
1154
|
+
function PlayerIdentity({
|
|
1155
|
+
name,
|
|
1156
|
+
team,
|
|
1157
|
+
variant = "full",
|
|
1158
|
+
isActive = false,
|
|
1159
|
+
align = "left",
|
|
1160
|
+
adornment,
|
|
1161
|
+
className
|
|
1162
|
+
}) {
|
|
1163
|
+
if (variant === "compact") {
|
|
1164
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1165
|
+
"span",
|
|
1166
|
+
{
|
|
1167
|
+
className: cn(
|
|
1168
|
+
"font-inter text-[11px] font-medium truncate max-w-[160px]",
|
|
1169
|
+
isActive ? "text-fg-secondary" : "text-fg-muted",
|
|
1170
|
+
className
|
|
1171
|
+
),
|
|
1172
|
+
children: [
|
|
1173
|
+
name,
|
|
1174
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-mono uppercase tracking-[0.5px] text-fg-muted", children: [
|
|
1175
|
+
" \xB7 ",
|
|
1176
|
+
team
|
|
1177
|
+
] })
|
|
1178
|
+
]
|
|
1179
|
+
}
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col gap-1 min-w-0", align === "right" && "items-end", className), children: [
|
|
1183
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1184
|
+
"span",
|
|
1185
|
+
{
|
|
1186
|
+
className: cn(
|
|
1187
|
+
"font-inter text-[13px] font-semibold leading-none truncate transition-colors duration-300",
|
|
1188
|
+
isActive ? "text-fg-primary" : "text-fg-muted"
|
|
1189
|
+
),
|
|
1190
|
+
children: name
|
|
1191
|
+
}
|
|
1192
|
+
),
|
|
1193
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex items-center gap-1", align === "right" && "flex-row-reverse"), children: [
|
|
1194
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[10px] text-fg-muted uppercase tracking-[0.5px]", children: team }),
|
|
1195
|
+
adornment
|
|
1196
|
+
] })
|
|
1197
|
+
] });
|
|
1198
|
+
}
|
|
1050
1199
|
function initials(name) {
|
|
1051
1200
|
return (name?.split(" ").map((w) => w[0]).join("").toUpperCase() || "?").slice(0, 2);
|
|
1052
1201
|
}
|
|
@@ -1055,10 +1204,11 @@ function Scoreboard({
|
|
|
1055
1204
|
opponentUsername,
|
|
1056
1205
|
creatorAvatarUrl,
|
|
1057
1206
|
opponentAvatarUrl,
|
|
1058
|
-
userSide
|
|
1207
|
+
userSide,
|
|
1208
|
+
className
|
|
1059
1209
|
}) {
|
|
1060
1210
|
const t = useGameT();
|
|
1061
|
-
const
|
|
1211
|
+
const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
|
|
1062
1212
|
if (!boardState?.score) return null;
|
|
1063
1213
|
const { score, turn, actionPoints, kingMustRelease } = boardState;
|
|
1064
1214
|
const myScore = userSide === "white" ? score.white : score.black;
|
|
@@ -1072,8 +1222,8 @@ function Scoreboard({
|
|
|
1072
1222
|
const isMyTurn = turn === userSide;
|
|
1073
1223
|
const myKingMustRelease = isMyTurn && kingMustRelease === turn;
|
|
1074
1224
|
const rivalKingMustRelease = !isMyTurn && kingMustRelease === turn;
|
|
1075
|
-
const maxAP = boardState.maxActionPoints ??
|
|
1076
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full bg-bg-secondary flex items-center justify-between px-5 py-2.5", children: [
|
|
1225
|
+
const maxAP = boardState.maxActionPoints ?? chunkBSE4DUXM_cjs.DEFAULT_MAX_AP;
|
|
1226
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("w-full bg-bg-secondary flex items-center justify-between px-5 py-2.5", className), children: [
|
|
1077
1227
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1078
1228
|
PlayerInfo,
|
|
1079
1229
|
{
|
|
@@ -1087,10 +1237,11 @@ function Scoreboard({
|
|
|
1087
1237
|
align: "left"
|
|
1088
1238
|
}
|
|
1089
1239
|
),
|
|
1090
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1091
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-anton text-[32px] leading-none text-fg-primary tabular-nums", children: myScore }),
|
|
1092
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-anton text-2xl text-fg-muted", children: "\u2014" }),
|
|
1093
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-anton text-[32px] leading-none text-fg-primary tabular-nums", children: rivalScore })
|
|
1240
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: "flex items-center gap-3", children: [
|
|
1241
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: "font-anton text-[32px] leading-none text-fg-primary tabular-nums", children: myScore }),
|
|
1242
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: "font-anton text-2xl text-fg-muted", children: "\u2014" }),
|
|
1243
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: "font-anton text-[32px] leading-none text-fg-primary tabular-nums", children: rivalScore }),
|
|
1244
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: t("scoreAriaLabel", { me: myName, myScore, rival: rivalName, rivalScore }) })
|
|
1094
1245
|
] }),
|
|
1095
1246
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1096
1247
|
PlayerInfo,
|
|
@@ -1117,14 +1268,16 @@ function PlayerInfo({ name, team, avatarUrl, isActive, actionPoints, maxActionPo
|
|
|
1117
1268
|
/* @__PURE__ */ jsxRuntime.jsx(AvatarFallback, { className: "bg-bg-surface-elevated text-fg-secondary text-xs font-semibold", children: initials(name) })
|
|
1118
1269
|
] }),
|
|
1119
1270
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col gap-1", align === "right" && "items-end"), children: [
|
|
1120
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1271
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1272
|
+
PlayerIdentity,
|
|
1273
|
+
{
|
|
1274
|
+
name,
|
|
1275
|
+
team,
|
|
1276
|
+
isActive,
|
|
1277
|
+
align,
|
|
1278
|
+
adornment: isActive ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-accent-green animate-pulse motion-reduce:animate-none", "aria-hidden": "true" }) : void 0
|
|
1279
|
+
}
|
|
1280
|
+
),
|
|
1128
1281
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1129
1282
|
ActionPoints,
|
|
1130
1283
|
{
|
|
@@ -1145,7 +1298,7 @@ function MiniScoreboard({
|
|
|
1145
1298
|
className
|
|
1146
1299
|
}) {
|
|
1147
1300
|
const t = useGameT();
|
|
1148
|
-
const
|
|
1301
|
+
const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
|
|
1149
1302
|
if (!boardState?.score) return null;
|
|
1150
1303
|
const { score, turn } = boardState;
|
|
1151
1304
|
const myScore = userSide === "white" ? score.white : score.black;
|
|
@@ -1158,29 +1311,28 @@ function MiniScoreboard({
|
|
|
1158
1311
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1159
1312
|
"div",
|
|
1160
1313
|
{
|
|
1314
|
+
role: "status",
|
|
1315
|
+
"aria-live": "polite",
|
|
1316
|
+
"aria-atomic": "true",
|
|
1161
1317
|
className: cn(
|
|
1162
1318
|
"w-full max-w-[540px] rounded-xl bg-bg-surface border border-border-subtle",
|
|
1163
1319
|
"flex items-center justify-between gap-4 px-5 py-3",
|
|
1164
1320
|
className
|
|
1165
1321
|
),
|
|
1166
1322
|
children: [
|
|
1167
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1168
|
-
|
|
1169
|
-
|
|
1323
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "contents", "aria-hidden": "true", children: [
|
|
1324
|
+
/* @__PURE__ */ jsxRuntime.jsx(SideCell, { name: myName, team: myTeam, score: myScore, isActive: isMyTurn, align: "left" }),
|
|
1325
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-anton text-base text-fg-muted uppercase tracking-[2px]", children: "vs" }),
|
|
1326
|
+
/* @__PURE__ */ jsxRuntime.jsx(SideCell, { name: rivalName, team: rivalTeam, score: rivalScore, isActive: !isMyTurn, align: "right" })
|
|
1327
|
+
] }),
|
|
1328
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: t("scoreAriaLabel", { me: myName, myScore, rival: rivalName, rivalScore }) })
|
|
1170
1329
|
]
|
|
1171
1330
|
}
|
|
1172
1331
|
);
|
|
1173
1332
|
}
|
|
1174
1333
|
function SideCell({ name, team, score, isActive, align }) {
|
|
1175
1334
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col min-w-0", align === "right" ? "items-end" : "items-start"), children: [
|
|
1176
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1177
|
-
"font-mono text-[10px] uppercase tracking-[0.5px] truncate max-w-[160px]",
|
|
1178
|
-
isActive ? "text-fg-secondary" : "text-fg-muted"
|
|
1179
|
-
), children: [
|
|
1180
|
-
name,
|
|
1181
|
-
" \xB7 ",
|
|
1182
|
-
team
|
|
1183
|
-
] }),
|
|
1335
|
+
/* @__PURE__ */ jsxRuntime.jsx(PlayerIdentity, { name, team, variant: "compact", isActive }),
|
|
1184
1336
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(
|
|
1185
1337
|
"font-anton text-[32px] leading-none tabular-nums",
|
|
1186
1338
|
isActive ? "text-fg-primary" : "text-fg-secondary"
|
|
@@ -1188,7 +1340,12 @@ function SideCell({ name, team, score, isActive, align }) {
|
|
|
1188
1340
|
] });
|
|
1189
1341
|
}
|
|
1190
1342
|
var VARIANT = {
|
|
1191
|
-
|
|
1343
|
+
// White on `--accent-green` is 4.27:1 — under WCAG AA for the Anton 400 the
|
|
1344
|
+
// primary CTA is set in. `--accent-green-dark` clears it (~6.5:1), so the
|
|
1345
|
+
// resting state is the dark green and interaction brightens it rather than
|
|
1346
|
+
// darkening it. Hover/active are transient; the resting state is the one a
|
|
1347
|
+
// player reads.
|
|
1348
|
+
primary: "bg-accent-green-dark text-fg-primary font-anton tracking-widest rounded-md hover:bg-accent-green active:bg-accent-green-light",
|
|
1192
1349
|
secondary: "bg-bg-surface text-fg-primary font-inter font-semibold rounded-md border border-border-subtle hover:bg-bg-surface-elevated active:bg-bg-secondary",
|
|
1193
1350
|
ghost: "bg-transparent text-fg-secondary font-inter font-medium rounded-md hover:text-fg-primary active:text-fg-muted",
|
|
1194
1351
|
destructive: "bg-danger/10 text-danger font-inter font-semibold rounded-md border border-danger hover:bg-danger/20 active:bg-danger/30"
|
|
@@ -1218,24 +1375,97 @@ var Button = React__namespace.forwardRef(
|
|
|
1218
1375
|
)
|
|
1219
1376
|
);
|
|
1220
1377
|
Button.displayName = "Button";
|
|
1378
|
+
var FOCUSABLE = [
|
|
1379
|
+
"a[href]",
|
|
1380
|
+
"button:not([disabled])",
|
|
1381
|
+
"input:not([disabled])",
|
|
1382
|
+
"select:not([disabled])",
|
|
1383
|
+
"textarea:not([disabled])",
|
|
1384
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
1385
|
+
].join(",");
|
|
1386
|
+
function focusableWithin(panel) {
|
|
1387
|
+
return Array.from(panel.querySelectorAll(FOCUSABLE)).filter(
|
|
1388
|
+
(el) => !el.closest("[hidden]") && !el.closest('[aria-hidden="true"]')
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
function useDialogA11y({ open, onClose }) {
|
|
1392
|
+
const panelRef = React.useRef(null);
|
|
1393
|
+
const restoreToRef = React.useRef(null);
|
|
1394
|
+
const onCloseRef = React.useRef(onClose);
|
|
1395
|
+
React.useEffect(() => {
|
|
1396
|
+
onCloseRef.current = onClose;
|
|
1397
|
+
});
|
|
1398
|
+
React.useEffect(() => {
|
|
1399
|
+
if (!open) return;
|
|
1400
|
+
const panel = panelRef.current;
|
|
1401
|
+
if (!panel) return;
|
|
1402
|
+
restoreToRef.current = document.activeElement;
|
|
1403
|
+
const initial = focusableWithin(panel)[0];
|
|
1404
|
+
if (initial) {
|
|
1405
|
+
initial.focus();
|
|
1406
|
+
} else {
|
|
1407
|
+
panel.tabIndex = -1;
|
|
1408
|
+
panel.focus();
|
|
1409
|
+
}
|
|
1410
|
+
const onKeyDown = (e) => {
|
|
1411
|
+
if (e.key === "Escape") {
|
|
1412
|
+
e.stopPropagation();
|
|
1413
|
+
onCloseRef.current();
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
if (e.key !== "Tab") return;
|
|
1417
|
+
const items = focusableWithin(panel);
|
|
1418
|
+
if (items.length === 0) {
|
|
1419
|
+
e.preventDefault();
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
const first = items[0];
|
|
1423
|
+
const last = items[items.length - 1];
|
|
1424
|
+
const active = document.activeElement;
|
|
1425
|
+
if (e.shiftKey && (active === first || !panel.contains(active))) {
|
|
1426
|
+
e.preventDefault();
|
|
1427
|
+
last.focus();
|
|
1428
|
+
} else if (!e.shiftKey && (active === last || !panel.contains(active))) {
|
|
1429
|
+
e.preventDefault();
|
|
1430
|
+
first.focus();
|
|
1431
|
+
}
|
|
1432
|
+
};
|
|
1433
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
1434
|
+
return () => {
|
|
1435
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
1436
|
+
restoreToRef.current?.focus?.();
|
|
1437
|
+
};
|
|
1438
|
+
}, [open]);
|
|
1439
|
+
return panelRef;
|
|
1440
|
+
}
|
|
1221
1441
|
function ConfirmDialog({ open, title, description, confirmLabel, cancelLabel, onConfirm, onCancel }) {
|
|
1442
|
+
const baseId = React.useId();
|
|
1443
|
+
const titleId = `${baseId}-title`;
|
|
1444
|
+
const descId = `${baseId}-desc`;
|
|
1445
|
+
const panelRef = useDialogA11y({ open, onClose: onCancel });
|
|
1222
1446
|
if (!open) return null;
|
|
1223
1447
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1224
1448
|
"div",
|
|
1225
1449
|
{
|
|
1226
1450
|
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",
|
|
1227
|
-
|
|
1228
|
-
"aria-modal": "true",
|
|
1451
|
+
onClick: onCancel,
|
|
1229
1452
|
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1230
1453
|
"div",
|
|
1231
1454
|
{
|
|
1455
|
+
ref: panelRef,
|
|
1456
|
+
role: "dialog",
|
|
1457
|
+
"aria-modal": "true",
|
|
1458
|
+
"aria-labelledby": titleId,
|
|
1459
|
+
"aria-describedby": description ? descId : void 0,
|
|
1460
|
+
onClick: (e) => e.stopPropagation(),
|
|
1232
1461
|
className: cn(
|
|
1233
1462
|
"bg-bg-surface border border-border-subtle rounded-xl p-6 mx-4 max-w-sm w-full",
|
|
1234
|
-
"flex flex-col gap-4 shadow-2xl"
|
|
1463
|
+
"flex flex-col gap-4 shadow-2xl",
|
|
1464
|
+
"focus:outline-none"
|
|
1235
1465
|
),
|
|
1236
1466
|
children: [
|
|
1237
|
-
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "font-anton text-lg text-fg-primary uppercase tracking-wide", children: title }),
|
|
1238
|
-
description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-inter text-sm text-fg-secondary", children: description }),
|
|
1467
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { id: titleId, className: "font-anton text-lg text-fg-primary uppercase tracking-wide", children: title }),
|
|
1468
|
+
description && /* @__PURE__ */ jsxRuntime.jsx("p", { id: descId, className: "font-inter text-sm text-fg-secondary", children: description }),
|
|
1239
1469
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2 pt-1", children: [
|
|
1240
1470
|
/* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "secondary", className: "flex-1", onClick: onCancel, children: cancelLabel }),
|
|
1241
1471
|
/* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "primary", className: "flex-1", onClick: onConfirm, children: confirmLabel })
|
|
@@ -1246,11 +1476,9 @@ function ConfirmDialog({ open, title, description, confirmLabel, cancelLabel, on
|
|
|
1246
1476
|
}
|
|
1247
1477
|
);
|
|
1248
1478
|
}
|
|
1249
|
-
function GameControls({ isMyTurn }) {
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
endTurn
|
|
1253
|
-
} = chunkLTFNSTAQ_cjs.useGameStore();
|
|
1479
|
+
function GameControls({ isMyTurn, className }) {
|
|
1480
|
+
const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
|
|
1481
|
+
const endTurn = chunkBSE4DUXM_cjs.useGameStore((s) => s.endTurn);
|
|
1254
1482
|
const t = useGameT();
|
|
1255
1483
|
const [showEndTurnConfirm, setShowEndTurnConfirm] = React.useState(false);
|
|
1256
1484
|
if (!boardState) return null;
|
|
@@ -1262,7 +1490,7 @@ function GameControls({ isMyTurn }) {
|
|
|
1262
1490
|
endTurn();
|
|
1263
1491
|
}
|
|
1264
1492
|
};
|
|
1265
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full bg-bg-secondary flex flex-col gap-3 px-5 pt-3 pb-5", children: [
|
|
1493
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("w-full bg-bg-secondary flex flex-col gap-3 px-5 pt-3 pb-5", className), children: [
|
|
1266
1494
|
isMyTurn && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1267
1495
|
Button,
|
|
1268
1496
|
{
|
|
@@ -1282,7 +1510,15 @@ function GameControls({ isMyTurn }) {
|
|
|
1282
1510
|
]
|
|
1283
1511
|
}
|
|
1284
1512
|
),
|
|
1285
|
-
!isMyTurn && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1513
|
+
!isMyTurn && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1514
|
+
"div",
|
|
1515
|
+
{
|
|
1516
|
+
role: "status",
|
|
1517
|
+
"aria-live": "polite",
|
|
1518
|
+
className: "flex items-center justify-center h-11 rounded-md bg-bg-surface border border-border-subtle",
|
|
1519
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-inter text-sm text-fg-muted", children: t("waitingRival") })
|
|
1520
|
+
}
|
|
1521
|
+
),
|
|
1286
1522
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1287
1523
|
ConfirmDialog,
|
|
1288
1524
|
{
|
|
@@ -1309,7 +1545,9 @@ var PIECE_ICON2 = {
|
|
|
1309
1545
|
};
|
|
1310
1546
|
function SelectedPieceDetail({ isMyTurn, className }) {
|
|
1311
1547
|
const t = useGameT();
|
|
1312
|
-
const { boardState, selectedPieceId } =
|
|
1548
|
+
const { boardState, selectedPieceId } = chunkBSE4DUXM_cjs.useGameStore(
|
|
1549
|
+
shallow.useShallow((s) => ({ boardState: s.boardState, selectedPieceId: s.selectedPieceId }))
|
|
1550
|
+
);
|
|
1313
1551
|
if (!boardState) return null;
|
|
1314
1552
|
const piece = boardState.pieces.find((p) => p.id === selectedPieceId);
|
|
1315
1553
|
if (!piece) {
|
|
@@ -1343,7 +1581,6 @@ function SelectedPieceDetail({ isMyTurn, className }) {
|
|
|
1343
1581
|
),
|
|
1344
1582
|
role: "status",
|
|
1345
1583
|
"aria-live": "polite",
|
|
1346
|
-
"aria-label": `${longName} ${square}`,
|
|
1347
1584
|
children: [
|
|
1348
1585
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1349
1586
|
"div",
|
|
@@ -1372,7 +1609,7 @@ function SelectedPieceDetail({ isMyTurn, className }) {
|
|
|
1372
1609
|
}
|
|
1373
1610
|
function MoveHistory({ scrollable = true, limit, className }) {
|
|
1374
1611
|
const t = useGameT();
|
|
1375
|
-
const
|
|
1612
|
+
const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
|
|
1376
1613
|
const scrollRef = React.useRef(null);
|
|
1377
1614
|
const history = boardState?.moveHistory ?? [];
|
|
1378
1615
|
const visible = limit ? history.slice(-limit) : history;
|
|
@@ -1479,9 +1716,9 @@ function HistoryRow({ entry, isLatest }) {
|
|
|
1479
1716
|
}
|
|
1480
1717
|
var COLS2 = 9;
|
|
1481
1718
|
var ROWS2 = 12;
|
|
1482
|
-
function StaticGameBoard() {
|
|
1483
|
-
const boardState = React.useMemo(() =>
|
|
1484
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full overflow-hidden rounded-xl shadow-2xl border-4 border-
|
|
1719
|
+
function StaticGameBoard({ className } = {}) {
|
|
1720
|
+
const boardState = React.useMemo(() => chunkBSE4DUXM_cjs.getInitialBoardState("white"), []);
|
|
1721
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("relative w-full overflow-hidden rounded-xl shadow-2xl border-4 border-field-frame bg-field-frame", className), children: [
|
|
1485
1722
|
/* @__PURE__ */ jsxRuntime.jsx(PitchSurface, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 pointer-events-none", children: boardState.pieces.map((piece) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1486
1723
|
"div",
|
|
1487
1724
|
{
|
|
@@ -1525,9 +1762,10 @@ function StaticGameBoard() {
|
|
|
1525
1762
|
}
|
|
1526
1763
|
function TurnBanner({ isMyTurn, waitingLabel, className }) {
|
|
1527
1764
|
const t = useGameT();
|
|
1528
|
-
const
|
|
1765
|
+
const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
|
|
1529
1766
|
if (!boardState) return null;
|
|
1530
|
-
const { actionPoints,
|
|
1767
|
+
const { actionPoints, turnNumber } = boardState;
|
|
1768
|
+
const maxActionPoints = boardState.maxActionPoints ?? chunkBSE4DUXM_cjs.DEFAULT_MAX_AP;
|
|
1531
1769
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1532
1770
|
"div",
|
|
1533
1771
|
{
|
|
@@ -1539,16 +1777,19 @@ function TurnBanner({ isMyTurn, waitingLabel, className }) {
|
|
|
1539
1777
|
"aria-live": "polite",
|
|
1540
1778
|
"aria-atomic": "true",
|
|
1541
1779
|
children: [
|
|
1542
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1780
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2 min-w-0", children: [
|
|
1781
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1782
|
+
"span",
|
|
1783
|
+
{
|
|
1784
|
+
className: cn(
|
|
1785
|
+
"font-anton text-sm tracking-[1.5px] uppercase truncate",
|
|
1786
|
+
isMyTurn ? "text-accent-green" : "text-fg-muted"
|
|
1787
|
+
),
|
|
1788
|
+
children: isMyTurn ? t("yourTurn") : waitingLabel ?? t("waitingRival")
|
|
1789
|
+
}
|
|
1790
|
+
),
|
|
1791
|
+
typeof turnNumber === "number" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[10px] text-fg-muted uppercase tracking-[0.5px] shrink-0", children: t("history.turn", { n: turnNumber }) })
|
|
1792
|
+
] }),
|
|
1552
1793
|
isMyTurn && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", "aria-label": t("actionPointsAriaLabel", { remaining: actionPoints, total: maxActionPoints }), children: [
|
|
1553
1794
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Zap, { size: 12, className: "text-accent-green", "aria-hidden": "true" }),
|
|
1554
1795
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-mono text-xs text-fg-secondary tabular-nums", children: [
|
|
@@ -1561,15 +1802,16 @@ function TurnBanner({ isMyTurn, waitingLabel, className }) {
|
|
|
1561
1802
|
}
|
|
1562
1803
|
);
|
|
1563
1804
|
}
|
|
1564
|
-
var TOASTABLE = /* @__PURE__ */ new Set(["interception", "offside", "tackle"]);
|
|
1805
|
+
var TOASTABLE = /* @__PURE__ */ new Set(["goal", "interception", "offside", "tackle"]);
|
|
1565
1806
|
var TOAST_CONFIG = {
|
|
1807
|
+
goal: "bg-accent-green/20 border-accent-green/60 text-accent-green-light",
|
|
1566
1808
|
interception: "bg-danger/20 border-danger/50 text-danger",
|
|
1567
1809
|
offside: "bg-warning/20 border-warning/50 text-warning",
|
|
1568
1810
|
tackle: "bg-warning/20 border-warning/50 text-warning"
|
|
1569
1811
|
};
|
|
1570
1812
|
function EventToast({ className }) {
|
|
1571
1813
|
const t = useGameT();
|
|
1572
|
-
const
|
|
1814
|
+
const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
|
|
1573
1815
|
const [toast, setToast] = React.useState(null);
|
|
1574
1816
|
React.useEffect(() => {
|
|
1575
1817
|
const lastMove = boardState?.lastMove;
|
|
@@ -1608,8 +1850,10 @@ function EventToast({ className }) {
|
|
|
1608
1850
|
);
|
|
1609
1851
|
}
|
|
1610
1852
|
function MobileHistory({ className }) {
|
|
1611
|
-
const
|
|
1853
|
+
const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
|
|
1612
1854
|
const [open, setOpen] = React.useState(false);
|
|
1855
|
+
const titleId = `${React.useId()}-title`;
|
|
1856
|
+
const panelRef = useDialogA11y({ open, onClose: () => setOpen(false) });
|
|
1613
1857
|
const lastMove = boardState?.moveHistory?.at(-1);
|
|
1614
1858
|
if (!lastMove) return null;
|
|
1615
1859
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
@@ -1635,11 +1879,13 @@ function MobileHistory({ className }) {
|
|
|
1635
1879
|
animate: { y: 0 },
|
|
1636
1880
|
exit: { y: "100%" },
|
|
1637
1881
|
transition: { type: "spring", stiffness: 300, damping: 32 },
|
|
1638
|
-
|
|
1882
|
+
ref: panelRef,
|
|
1883
|
+
className: "fixed bottom-0 left-0 right-0 z-50 bg-bg-secondary rounded-t-2xl border-t border-border-subtle shadow-xl focus:outline-none",
|
|
1639
1884
|
role: "dialog",
|
|
1640
1885
|
"aria-modal": "true",
|
|
1886
|
+
"aria-labelledby": titleId,
|
|
1641
1887
|
children: [
|
|
1642
|
-
/* @__PURE__ */ jsxRuntime.jsx(SheetHeader, { onClose: () => setOpen(false) }),
|
|
1888
|
+
/* @__PURE__ */ jsxRuntime.jsx(SheetHeader, { titleId, onClose: () => setOpen(false) }),
|
|
1643
1889
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 pb-6 max-h-[50dvh] overflow-y-auto", children: /* @__PURE__ */ jsxRuntime.jsx(MoveHistory, { scrollable: false }) })
|
|
1644
1890
|
]
|
|
1645
1891
|
},
|
|
@@ -1648,17 +1894,17 @@ function MobileHistory({ className }) {
|
|
|
1648
1894
|
] }) })
|
|
1649
1895
|
] });
|
|
1650
1896
|
}
|
|
1651
|
-
function SheetHeader({ onClose }) {
|
|
1897
|
+
function SheetHeader({ titleId, onClose }) {
|
|
1652
1898
|
const t = useGameT();
|
|
1653
1899
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-5 pt-4 pb-3 border-b border-border-subtle", children: [
|
|
1654
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[10px] tracking-[0.5px] uppercase text-fg-muted", children: t("history.title") }),
|
|
1900
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { id: titleId, className: "font-mono text-[10px] tracking-[0.5px] uppercase text-fg-muted", children: t("history.title") }),
|
|
1655
1901
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1656
1902
|
"button",
|
|
1657
1903
|
{
|
|
1658
1904
|
onClick: onClose,
|
|
1659
1905
|
className: "w-8 h-8 flex items-center justify-center rounded-full bg-bg-surface hover:bg-bg-surface-elevated text-fg-muted hover:text-fg-primary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-green",
|
|
1660
|
-
"aria-label": "
|
|
1661
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 14 })
|
|
1906
|
+
"aria-label": t("history.close"),
|
|
1907
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 14, "aria-hidden": "true" })
|
|
1662
1908
|
}
|
|
1663
1909
|
)
|
|
1664
1910
|
] });
|
|
@@ -1711,6 +1957,35 @@ function HistoryChip({ lastMove, onClick, className }) {
|
|
|
1711
1957
|
);
|
|
1712
1958
|
}
|
|
1713
1959
|
|
|
1960
|
+
// src/tokens.ts
|
|
1961
|
+
var REQUIRED_TOKENS = [
|
|
1962
|
+
// Surfaces
|
|
1963
|
+
"--bg-primary",
|
|
1964
|
+
"--bg-secondary",
|
|
1965
|
+
"--bg-surface",
|
|
1966
|
+
"--bg-surface-elevated",
|
|
1967
|
+
// Text
|
|
1968
|
+
"--fg-primary",
|
|
1969
|
+
"--fg-secondary",
|
|
1970
|
+
"--fg-muted",
|
|
1971
|
+
// Accent (the primary CTA rests on the dark step — see Button)
|
|
1972
|
+
"--accent-green",
|
|
1973
|
+
"--accent-green-light",
|
|
1974
|
+
"--accent-green-dark",
|
|
1975
|
+
// Borders
|
|
1976
|
+
"--border-subtle",
|
|
1977
|
+
// Semantic
|
|
1978
|
+
"--danger",
|
|
1979
|
+
"--warning",
|
|
1980
|
+
// Board
|
|
1981
|
+
"--field-green-1",
|
|
1982
|
+
"--field-green-2",
|
|
1983
|
+
"--field-frame",
|
|
1984
|
+
"--move-highlight",
|
|
1985
|
+
"--pass-highlight",
|
|
1986
|
+
"--last-move-highlight"
|
|
1987
|
+
];
|
|
1988
|
+
|
|
1714
1989
|
exports.ActionPoints = ActionPoints;
|
|
1715
1990
|
exports.Avatar = Avatar;
|
|
1716
1991
|
exports.AvatarFallback = AvatarFallback;
|
|
@@ -1735,6 +2010,8 @@ exports.PITCH_COLS = PITCH_COLS;
|
|
|
1735
2010
|
exports.PITCH_ROWS = PITCH_ROWS;
|
|
1736
2011
|
exports.PitchMarkings = PitchMarkings;
|
|
1737
2012
|
exports.PitchSurface = PitchSurface;
|
|
2013
|
+
exports.PlayerIdentity = PlayerIdentity;
|
|
2014
|
+
exports.REQUIRED_TOKENS = REQUIRED_TOKENS;
|
|
1738
2015
|
exports.Scoreboard = Scoreboard;
|
|
1739
2016
|
exports.SelectedPieceDetail = SelectedPieceDetail;
|
|
1740
2017
|
exports.StaticGameBoard = StaticGameBoard;
|
|
@@ -1742,6 +2019,7 @@ exports.TurnBanner = TurnBanner;
|
|
|
1742
2019
|
exports.cn = cn;
|
|
1743
2020
|
exports.isGoalAreaSquare = isGoalAreaSquare;
|
|
1744
2021
|
exports.pitchSquareClass = pitchSquareClass;
|
|
2022
|
+
exports.useDialogA11y = useDialogA11y;
|
|
1745
2023
|
exports.useGameT = useGameT;
|
|
1746
2024
|
//# sourceMappingURL=index.cjs.map
|
|
1747
2025
|
//# sourceMappingURL=index.cjs.map
|