@scriptonita/chess-football-ui 0.8.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 CHANGED
@@ -52,9 +52,20 @@ expect(missing).toEqual([])
52
52
  ```
53
53
 
54
54
  **2. Design tokens.** Components emit Tailwind classes bound to tokens the app defines. A
55
- token that is missing fails silently — the rule simply does not apply. The board's
56
- `--last-move-highlight`, `--move-highlight`, `--pass-highlight`, `--accent-green`,
57
- `--accent-green-light` and `--accent-green-dark` are all required.
55
+ token that is missing fails silently — Tailwind emits the rule, the variable resolves to
56
+ nothing, and the declaration is dropped. That is how `--last-move-highlight` went missing
57
+ in the CrazyGames SPA: the last-move highlight and the bot's turn replay never rendered,
58
+ with no error anywhere. `REQUIRED_TOKENS` is the manifest; assert it the same way:
59
+
60
+ ```ts
61
+ import { REQUIRED_TOKENS } from '@scriptonita/chess-football-ui'
62
+
63
+ const css = readFileSync('src/index.css', 'utf8')
64
+ expect(REQUIRED_TOKENS.filter(t => !css.includes(`${t}:`))).toEqual([])
65
+ ```
66
+
67
+ `--field-frame` is new in 0.9.0: the board frame was hardcoded `#1a3317` in three places,
68
+ hand-tuned for the webapp's grass and visibly wrong against the CrazyGames pitch.
58
69
 
59
70
  ## Peer dependencies
60
71
 
@@ -4,19 +4,31 @@ var zustand = require('zustand');
4
4
  var chessFootballEngine = require('@scriptonita/chess-football-engine');
5
5
 
6
6
  // src/store/use-game-store.ts
7
- var useGameStore = zustand.create((set) => ({
7
+ var DEFAULT_MAX_AP = 5;
8
+ var INITIAL = {
8
9
  boardState: null,
9
10
  selectedPieceId: null,
10
- interactionMode: null,
11
+ interactionMode: null
12
+ };
13
+ var useGameStore = zustand.create((set) => ({
14
+ ...INITIAL,
11
15
  setBoardState: (state) => set({ boardState: state }),
12
16
  setSelectedPieceId: (id) => set({ selectedPieceId: id }),
13
17
  setInteractionMode: (mode) => set({ interactionMode: mode }),
18
+ /**
19
+ * The store is a module-global singleton, so a new match opens on the
20
+ * previous match's final board — which ended on a goal, and was therefore
21
+ * re-detected as a fresh goal (the bug behind webapp PR #42). Apps were
22
+ * each reaching for `useGameStore.setState({...})` by hand to work around
23
+ * it; this makes the intent explicit and keeps the shape in one place.
24
+ */
25
+ reset: () => set({ ...INITIAL }),
14
26
  resetTurn: () => set((state) => {
15
27
  if (!state.boardState) return state;
16
28
  return {
17
29
  boardState: {
18
30
  ...state.boardState,
19
- actionPoints: state.boardState.maxActionPoints ?? 5,
31
+ actionPoints: state.boardState.maxActionPoints ?? DEFAULT_MAX_AP,
20
32
  pieces: state.boardState.pieces.map((p) => ({ ...p, hasMovedThisTurn: false }))
21
33
  }
22
34
  };
@@ -50,6 +62,7 @@ Object.defineProperty(exports, "getInitialBoardState", {
50
62
  enumerable: true,
51
63
  get: function () { return chessFootballEngine.getInitialBoardState; }
52
64
  });
65
+ exports.DEFAULT_MAX_AP = DEFAULT_MAX_AP;
53
66
  exports.useGameStore = useGameStore;
54
- //# sourceMappingURL=chunk-LTFNSTAQ.cjs.map
55
- //# sourceMappingURL=chunk-LTFNSTAQ.cjs.map
67
+ //# sourceMappingURL=chunk-BSE4DUXM.cjs.map
68
+ //# sourceMappingURL=chunk-BSE4DUXM.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/store/use-game-store.ts"],"names":["create","applyMove","applyPass","applyEndTurn"],"mappings":"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","file":"chunk-BSE4DUXM.cjs","sourcesContent":["import { create } from 'zustand'\nimport type { BoardState, Position } from '@scriptonita/chess-football-engine'\nimport { applyMove, applyPass, applyEndTurn } from '@scriptonita/chess-football-engine'\n\n// The canonical kickoff position lives in the engine (rules-adjacent). Re-exported\n// here so app imports from `@scriptonita/chess-football-ui/store` keep working.\nexport { getInitialBoardState } from '@scriptonita/chess-football-engine'\n\n/**\n * Fallback when a board predates configurable action points. `maxActionPoints`\n * is optional on `BoardState`, and every consumer was inventing its own `?? 5`\n * — except `TurnBanner`, which had none and rendered \"3/\" on an old match.\n */\nexport const DEFAULT_MAX_AP = 5\n\ninterface GameStore {\n boardState: BoardState | null\n selectedPieceId: string | null\n interactionMode: 'move' | 'pass' | null\n setBoardState: (state: BoardState) => void\n setSelectedPieceId: (id: string | null) => void\n setInteractionMode: (mode: 'move' | 'pass' | null) => void\n resetTurn: () => void\n /** Return the store to its initial state. */\n reset: () => void\n movePiece: (pieceId: string, to: Position) => void\n passBall: (to: Position) => void\n endTurn: () => void\n}\n\nconst INITIAL = {\n boardState: null,\n selectedPieceId: null,\n interactionMode: null,\n} satisfies Pick<GameStore, 'boardState' | 'selectedPieceId' | 'interactionMode'>\n\nexport const useGameStore = create<GameStore>((set) => ({\n ...INITIAL,\n\n setBoardState: (state) => set({ boardState: state }),\n setSelectedPieceId: (id) => set({ selectedPieceId: id }),\n setInteractionMode: (mode) => set({ interactionMode: mode }),\n\n /**\n * The store is a module-global singleton, so a new match opens on the\n * previous match's final board — which ended on a goal, and was therefore\n * re-detected as a fresh goal (the bug behind webapp PR #42). Apps were\n * each reaching for `useGameStore.setState({...})` by hand to work around\n * it; this makes the intent explicit and keeps the shape in one place.\n */\n reset: () => set({ ...INITIAL }),\n\n resetTurn: () => set((state) => {\n if (!state.boardState) return state\n return {\n boardState: {\n ...state.boardState,\n actionPoints: state.boardState.maxActionPoints ?? DEFAULT_MAX_AP,\n pieces: state.boardState.pieces.map(p => ({ ...p, hasMovedThisTurn: false })),\n },\n }\n }),\n\n movePiece: (pieceId, to) => set((state) => {\n if (!state.boardState || state.boardState.actionPoints <= 0) return state\n const piece = state.boardState.pieces.find(p => p.id === pieceId)\n if (!piece || piece.hasMovedThisTurn) return state\n\n const { boardState } = applyMove(state.boardState, pieceId, to)\n return { boardState, selectedPieceId: null, interactionMode: null }\n }),\n\n passBall: (to) => set((state) => {\n if (!state.boardState || state.boardState.actionPoints <= 0 || !state.boardState.ball.holderId) return state\n const holder = state.boardState.pieces.find(p => p.id === state.boardState!.ball.holderId)\n if (!holder) return state\n\n const { boardState, forcedTurnEnd } = applyPass(state.boardState, to)\n const keepSelection = !forcedTurnEnd && boardState.ball.holderId\n return {\n boardState,\n selectedPieceId: keepSelection ? boardState.ball.holderId : null,\n interactionMode: keepSelection ? 'pass' : null,\n }\n }),\n\n endTurn: () => set((state) => {\n if (!state.boardState) return state\n return { boardState: applyEndTurn(state.boardState) }\n }),\n}))\n"]}
@@ -3,19 +3,31 @@ import { applyEndTurn, applyPass, applyMove } from '@scriptonita/chess-football-
3
3
  export { getInitialBoardState } from '@scriptonita/chess-football-engine';
4
4
 
5
5
  // src/store/use-game-store.ts
6
- var useGameStore = create((set) => ({
6
+ var DEFAULT_MAX_AP = 5;
7
+ var INITIAL = {
7
8
  boardState: null,
8
9
  selectedPieceId: null,
9
- interactionMode: null,
10
+ interactionMode: null
11
+ };
12
+ var useGameStore = create((set) => ({
13
+ ...INITIAL,
10
14
  setBoardState: (state) => set({ boardState: state }),
11
15
  setSelectedPieceId: (id) => set({ selectedPieceId: id }),
12
16
  setInteractionMode: (mode) => set({ interactionMode: mode }),
17
+ /**
18
+ * The store is a module-global singleton, so a new match opens on the
19
+ * previous match's final board — which ended on a goal, and was therefore
20
+ * re-detected as a fresh goal (the bug behind webapp PR #42). Apps were
21
+ * each reaching for `useGameStore.setState({...})` by hand to work around
22
+ * it; this makes the intent explicit and keeps the shape in one place.
23
+ */
24
+ reset: () => set({ ...INITIAL }),
13
25
  resetTurn: () => set((state) => {
14
26
  if (!state.boardState) return state;
15
27
  return {
16
28
  boardState: {
17
29
  ...state.boardState,
18
- actionPoints: state.boardState.maxActionPoints ?? 5,
30
+ actionPoints: state.boardState.maxActionPoints ?? DEFAULT_MAX_AP,
19
31
  pieces: state.boardState.pieces.map((p) => ({ ...p, hasMovedThisTurn: false }))
20
32
  }
21
33
  };
@@ -45,6 +57,6 @@ var useGameStore = create((set) => ({
45
57
  })
46
58
  }));
47
59
 
48
- export { useGameStore };
49
- //# sourceMappingURL=chunk-N24LGLLO.js.map
50
- //# sourceMappingURL=chunk-N24LGLLO.js.map
60
+ export { DEFAULT_MAX_AP, useGameStore };
61
+ //# sourceMappingURL=chunk-C5HCN7O5.js.map
62
+ //# sourceMappingURL=chunk-C5HCN7O5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/store/use-game-store.ts"],"names":[],"mappings":";;;;;AAaO,IAAM,cAAA,GAAiB;AAiB9B,IAAM,OAAA,GAAU;AAAA,EACZ,UAAA,EAAY,IAAA;AAAA,EACZ,eAAA,EAAiB,IAAA;AAAA,EACjB,eAAA,EAAiB;AACrB,CAAA;AAEO,IAAM,YAAA,GAAe,MAAA,CAAkB,CAAC,GAAA,MAAS;AAAA,EACpD,GAAG,OAAA;AAAA,EAEH,eAAe,CAAC,KAAA,KAAU,IAAI,EAAE,UAAA,EAAY,OAAO,CAAA;AAAA,EACnD,oBAAoB,CAAC,EAAA,KAAO,IAAI,EAAE,eAAA,EAAiB,IAAI,CAAA;AAAA,EACvD,oBAAoB,CAAC,IAAA,KAAS,IAAI,EAAE,eAAA,EAAiB,MAAM,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAS3D,OAAO,MAAM,GAAA,CAAI,EAAE,GAAG,SAAS,CAAA;AAAA,EAE/B,SAAA,EAAW,MAAM,GAAA,CAAI,CAAC,KAAA,KAAU;AAC5B,IAAA,IAAI,CAAC,KAAA,CAAM,UAAA,EAAY,OAAO,KAAA;AAC9B,IAAA,OAAO;AAAA,MACH,UAAA,EAAY;AAAA,QACR,GAAG,KAAA,CAAM,UAAA;AAAA,QACT,YAAA,EAAc,KAAA,CAAM,UAAA,CAAW,eAAA,IAAmB,cAAA;AAAA,QAClD,MAAA,EAAQ,KAAA,CAAM,UAAA,CAAW,MAAA,CAAO,GAAA,CAAI,CAAA,CAAA,MAAM,EAAE,GAAG,CAAA,EAAG,gBAAA,EAAkB,KAAA,EAAM,CAAE;AAAA;AAChF,KACJ;AAAA,EACJ,CAAC,CAAA;AAAA,EAED,WAAW,CAAC,OAAA,EAAS,EAAA,KAAO,GAAA,CAAI,CAAC,KAAA,KAAU;AACvC,IAAA,IAAI,CAAC,KAAA,CAAM,UAAA,IAAc,MAAM,UAAA,CAAW,YAAA,IAAgB,GAAG,OAAO,KAAA;AACpE,IAAA,MAAM,KAAA,GAAQ,MAAM,UAAA,CAAW,MAAA,CAAO,KAAK,CAAA,CAAA,KAAK,CAAA,CAAE,OAAO,OAAO,CAAA;AAChE,IAAA,IAAI,CAAC,KAAA,IAAS,KAAA,CAAM,gBAAA,EAAkB,OAAO,KAAA;AAE7C,IAAA,MAAM,EAAE,UAAA,EAAW,GAAI,UAAU,KAAA,CAAM,UAAA,EAAY,SAAS,EAAE,CAAA;AAC9D,IAAA,OAAO,EAAE,UAAA,EAAY,eAAA,EAAiB,IAAA,EAAM,iBAAiB,IAAA,EAAK;AAAA,EACtE,CAAC,CAAA;AAAA,EAED,QAAA,EAAU,CAAC,EAAA,KAAO,GAAA,CAAI,CAAC,KAAA,KAAU;AAC7B,IAAA,IAAI,CAAC,KAAA,CAAM,UAAA,IAAc,KAAA,CAAM,UAAA,CAAW,YAAA,IAAgB,CAAA,IAAK,CAAC,KAAA,CAAM,UAAA,CAAW,IAAA,CAAK,QAAA,EAAU,OAAO,KAAA;AACvG,IAAA,MAAM,MAAA,GAAS,KAAA,CAAM,UAAA,CAAW,MAAA,CAAO,IAAA,CAAK,CAAA,CAAA,KAAK,CAAA,CAAE,EAAA,KAAO,KAAA,CAAM,UAAA,CAAY,IAAA,CAAK,QAAQ,CAAA;AACzF,IAAA,IAAI,CAAC,QAAQ,OAAO,KAAA;AAEpB,IAAA,MAAM,EAAE,UAAA,EAAY,aAAA,KAAkB,SAAA,CAAU,KAAA,CAAM,YAAY,EAAE,CAAA;AACpE,IAAA,MAAM,aAAA,GAAgB,CAAC,aAAA,IAAiB,UAAA,CAAW,IAAA,CAAK,QAAA;AACxD,IAAA,OAAO;AAAA,MACH,UAAA;AAAA,MACA,eAAA,EAAiB,aAAA,GAAgB,UAAA,CAAW,IAAA,CAAK,QAAA,GAAW,IAAA;AAAA,MAC5D,eAAA,EAAiB,gBAAgB,MAAA,GAAS;AAAA,KAC9C;AAAA,EACJ,CAAC,CAAA;AAAA,EAED,OAAA,EAAS,MAAM,GAAA,CAAI,CAAC,KAAA,KAAU;AAC1B,IAAA,IAAI,CAAC,KAAA,CAAM,UAAA,EAAY,OAAO,KAAA;AAC9B,IAAA,OAAO,EAAE,UAAA,EAAY,YAAA,CAAa,KAAA,CAAM,UAAU,CAAA,EAAE;AAAA,EACxD,CAAC;AACL,CAAA,CAAE","file":"chunk-C5HCN7O5.js","sourcesContent":["import { create } from 'zustand'\nimport type { BoardState, Position } from '@scriptonita/chess-football-engine'\nimport { applyMove, applyPass, applyEndTurn } from '@scriptonita/chess-football-engine'\n\n// The canonical kickoff position lives in the engine (rules-adjacent). Re-exported\n// here so app imports from `@scriptonita/chess-football-ui/store` keep working.\nexport { getInitialBoardState } from '@scriptonita/chess-football-engine'\n\n/**\n * Fallback when a board predates configurable action points. `maxActionPoints`\n * is optional on `BoardState`, and every consumer was inventing its own `?? 5`\n * — except `TurnBanner`, which had none and rendered \"3/\" on an old match.\n */\nexport const DEFAULT_MAX_AP = 5\n\ninterface GameStore {\n boardState: BoardState | null\n selectedPieceId: string | null\n interactionMode: 'move' | 'pass' | null\n setBoardState: (state: BoardState) => void\n setSelectedPieceId: (id: string | null) => void\n setInteractionMode: (mode: 'move' | 'pass' | null) => void\n resetTurn: () => void\n /** Return the store to its initial state. */\n reset: () => void\n movePiece: (pieceId: string, to: Position) => void\n passBall: (to: Position) => void\n endTurn: () => void\n}\n\nconst INITIAL = {\n boardState: null,\n selectedPieceId: null,\n interactionMode: null,\n} satisfies Pick<GameStore, 'boardState' | 'selectedPieceId' | 'interactionMode'>\n\nexport const useGameStore = create<GameStore>((set) => ({\n ...INITIAL,\n\n setBoardState: (state) => set({ boardState: state }),\n setSelectedPieceId: (id) => set({ selectedPieceId: id }),\n setInteractionMode: (mode) => set({ interactionMode: mode }),\n\n /**\n * The store is a module-global singleton, so a new match opens on the\n * previous match's final board — which ended on a goal, and was therefore\n * re-detected as a fresh goal (the bug behind webapp PR #42). Apps were\n * each reaching for `useGameStore.setState({...})` by hand to work around\n * it; this makes the intent explicit and keeps the shape in one place.\n */\n reset: () => set({ ...INITIAL }),\n\n resetTurn: () => set((state) => {\n if (!state.boardState) return state\n return {\n boardState: {\n ...state.boardState,\n actionPoints: state.boardState.maxActionPoints ?? DEFAULT_MAX_AP,\n pieces: state.boardState.pieces.map(p => ({ ...p, hasMovedThisTurn: false })),\n },\n }\n }),\n\n movePiece: (pieceId, to) => set((state) => {\n if (!state.boardState || state.boardState.actionPoints <= 0) return state\n const piece = state.boardState.pieces.find(p => p.id === pieceId)\n if (!piece || piece.hasMovedThisTurn) return state\n\n const { boardState } = applyMove(state.boardState, pieceId, to)\n return { boardState, selectedPieceId: null, interactionMode: null }\n }),\n\n passBall: (to) => set((state) => {\n if (!state.boardState || state.boardState.actionPoints <= 0 || !state.boardState.ball.holderId) return state\n const holder = state.boardState.pieces.find(p => p.id === state.boardState!.ball.holderId)\n if (!holder) return state\n\n const { boardState, forcedTurnEnd } = applyPass(state.boardState, to)\n const keepSelection = !forcedTurnEnd && boardState.ball.holderId\n return {\n boardState,\n selectedPieceId: keepSelection ? boardState.ball.holderId : null,\n interactionMode: keepSelection ? 'pass' : null,\n }\n }),\n\n endTurn: () => set((state) => {\n if (!state.boardState) return state\n return { boardState: applyEndTurn(state.boardState) }\n }),\n}))\n"]}
package/dist/index.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var chunkLTFNSTAQ_cjs = require('./chunk-LTFNSTAQ.cjs');
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
- isSelected && "ring-4 ring-primary scale-110 z-30",
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
  ),
@@ -265,7 +269,7 @@ function useGameT() {
265
269
  }
266
270
  function BallHolderChip({ className }) {
267
271
  const t = useGameT();
268
- const boardState = chunkLTFNSTAQ_cjs.useGameStore((s) => s.boardState);
272
+ const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
269
273
  const holder = boardState?.pieces.find((p) => p.id === boardState.ball.holderId) ?? null;
270
274
  if (!holder) return null;
271
275
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex items-center gap-1.5 min-h-[1.75rem]", className), "aria-live": "polite", children: [
@@ -460,18 +464,29 @@ var cellX = (x) => `${x * 100}%`;
460
464
  var cellY = (y) => `${(ROWS - 1 - y) * 100}%`;
461
465
  var PIECE_SLIDE = { type: "spring", stiffness: 190, damping: 28, mass: 1 };
462
466
  var BALL_SLIDE = { type: "spring", stiffness: 300, damping: 26, mass: 0.6 };
463
- function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, toolbar = true }) {
467
+ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, toolbar = true, className }) {
464
468
  const t = useGameT();
465
469
  const boardId = React.useId();
466
470
  const prefersReducedMotion = framerMotion.useReducedMotion();
467
471
  const {
468
472
  boardState,
469
473
  selectedPieceId,
474
+ interactionMode,
470
475
  setSelectedPieceId,
471
476
  setInteractionMode,
472
477
  movePiece,
473
478
  passBall
474
- } = chunkLTFNSTAQ_cjs.useGameStore();
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
+ );
475
490
  const [cursor, setCursor] = React.useState(null);
476
491
  const [disambiguateAt, setDisambiguateAt] = React.useState(null);
477
492
  const [shortcutsOpen, setShortcutsOpen] = React.useState(false);
@@ -539,21 +554,21 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
539
554
  return () => clearTimeout(id);
540
555
  }, [replay]);
541
556
  const validMoves = React.useMemo(() => {
542
- if (!boardState) return [];
557
+ if (!boardState || interactionMode === "pass") return [];
543
558
  const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
544
559
  if (sp && !sp.hasMovedThisTurn) {
545
560
  return chessFootballEngine.getValidMoves(sp, boardState);
546
561
  }
547
562
  return [];
548
- }, [boardState, selectedPieceId]);
563
+ }, [boardState, selectedPieceId, interactionMode]);
549
564
  const validPasses = React.useMemo(() => {
550
- if (!boardState) return [];
565
+ if (!boardState || interactionMode === "move") return [];
551
566
  const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
552
567
  if (sp && boardState.ball.holderId === sp.id) {
553
568
  return chessFootballEngine.getValidPasses(sp, boardState);
554
569
  }
555
570
  return [];
556
- }, [boardState, selectedPieceId]);
571
+ }, [boardState, selectedPieceId, interactionMode]);
557
572
  if (!boardState) return null;
558
573
  const lastMove = boardState.lastMove;
559
574
  const ball = boardState.ball;
@@ -667,8 +682,11 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
667
682
  activate(c.x, c.y);
668
683
  break;
669
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.
670
687
  case "m":
671
688
  case "M":
689
+ if (e.ctrlKey || e.metaKey || e.altKey) break;
672
690
  if (disambiguateAt && selectedPieceId) {
673
691
  movePiece(selectedPieceId, disambiguateAt);
674
692
  setDisambiguateAt(null);
@@ -676,6 +694,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
676
694
  break;
677
695
  case "p":
678
696
  case "P":
697
+ if (e.ctrlKey || e.metaKey || e.altKey) break;
679
698
  if (disambiguateAt) {
680
699
  passBall(disambiguateAt);
681
700
  setDisambiguateAt(null);
@@ -740,22 +759,29 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
740
759
  if (isValidPass) setHoveredPassAt({ x, y });
741
760
  },
742
761
  onMouseLeave: () => setHoveredPassAt(null),
762
+ onTouchStart: () => {
763
+ if (isValidPass) setHoveredPassAt({ x, y });
764
+ },
743
765
  className: cn(
744
766
  pitchSquareClass(x, y),
745
767
  "flex items-center justify-center cursor-pointer",
746
768
  !isValidMove && !isValidPass && isLastMove && "bg-last-move-highlight/20",
747
769
  // §8: pulsing warning ring on enemy goal area
748
770
  isOffsideRisk && isEnemyGoalArea && !isValidMove && !isValidPass && "ring-2 ring-inset ring-warning/60",
749
- isAmbiguous && "ring-[3px] ring-inset ring-yellow-400/90 bg-yellow-400/20",
750
- !isAmbiguous && isValidMove && "ring-[3px] ring-inset ring-yellow-400/80 bg-yellow-400/15",
751
- !isAmbiguous && isValidPass && "ring-[3px] ring-inset ring-sky-400/80 bg-sky-400/15"
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"
752
778
  ),
753
779
  children: [
754
- !isAmbiguous && isValidMove && !pieceAt && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-full bg-yellow-400/65" }),
755
- !isAmbiguous && isValidPass && !pieceAt && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-full bg-sky-400/65" }),
756
- isAmbiguous && !pieceAt && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-0.5", children: [
757
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2.5 h-2.5 rounded-full bg-yellow-400/80" }),
758
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2.5 h-2.5 rounded-full bg-sky-400/80" })
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" })
759
785
  ] }),
760
786
  isCursor && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 ring-2 ring-inset ring-white pointer-events-none z-20", "aria-hidden": "true" }),
761
787
  isDisambiguateTarget && selectedPieceId && /* @__PURE__ */ jsxRuntime.jsxs(
@@ -820,10 +846,14 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
820
846
  "data-testid": "board-root",
821
847
  className: cn(
822
848
  // §6: Mobile edge-to-edge — thin 2px margin, no border, no radius
823
- "w-full mx-0.5 bg-[#1a3317] overflow-hidden",
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",
824
853
  // §5+§6: Desktop — centered, rounded, bordered, height-based max-width
825
- "md:mx-auto md:rounded-xl md:shadow-2xl md:border-4 md:border-[#1a3317]",
826
- "md:max-w-[min(700px,calc((100dvh_-_150px)*0.75))]"
854
+ "md:mx-auto md:rounded-xl md:shadow-2xl md:border-4 md:border-field-frame",
855
+ "md:max-w-[min(700px,calc((100dvh_-_150px)*0.75))]",
856
+ !showCoordinates && className
827
857
  ),
828
858
  children: [
829
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: [
@@ -1020,7 +1050,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
1020
1050
  );
1021
1051
  if (!showCoordinates) return board;
1022
1052
  const ranksTopToBottom = [...chessFootballEngine.RANK_LABELS].reverse();
1023
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full max-w-[540px] md:max-w-none mx-auto", "aria-hidden": "false", children: /* @__PURE__ */ jsxRuntime.jsxs(
1053
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("w-full max-w-[540px] md:max-w-none mx-auto", className), children: /* @__PURE__ */ jsxRuntime.jsxs(
1024
1054
  "div",
1025
1055
  {
1026
1056
  className: "grid gap-1 select-none",
@@ -1052,8 +1082,20 @@ function Avatar({ className, children }) {
1052
1082
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("relative w-9 h-9 rounded-full overflow-hidden shrink-0", className), children });
1053
1083
  }
1054
1084
  function AvatarImage({ src, alt, className }) {
1055
- if (!src) return null;
1056
- return /* @__PURE__ */ jsxRuntime.jsx("img", { src, alt: alt ?? "", className: cn("w-full h-full object-cover", className) });
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
+ );
1057
1099
  }
1058
1100
  function AvatarFallback({ className, children }) {
1059
1101
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("absolute inset-0 flex items-center justify-center text-xs font-semibold", className), children });
@@ -1109,6 +1151,51 @@ function ActionPoints({ total, remaining, size = 20, className, kingMustRelease
1109
1151
  );
1110
1152
  }) });
1111
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
+ }
1112
1199
  function initials(name) {
1113
1200
  return (name?.split(" ").map((w) => w[0]).join("").toUpperCase() || "?").slice(0, 2);
1114
1201
  }
@@ -1117,10 +1204,11 @@ function Scoreboard({
1117
1204
  opponentUsername,
1118
1205
  creatorAvatarUrl,
1119
1206
  opponentAvatarUrl,
1120
- userSide
1207
+ userSide,
1208
+ className
1121
1209
  }) {
1122
1210
  const t = useGameT();
1123
- const { boardState } = chunkLTFNSTAQ_cjs.useGameStore();
1211
+ const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
1124
1212
  if (!boardState?.score) return null;
1125
1213
  const { score, turn, actionPoints, kingMustRelease } = boardState;
1126
1214
  const myScore = userSide === "white" ? score.white : score.black;
@@ -1134,8 +1222,8 @@ function Scoreboard({
1134
1222
  const isMyTurn = turn === userSide;
1135
1223
  const myKingMustRelease = isMyTurn && kingMustRelease === turn;
1136
1224
  const rivalKingMustRelease = !isMyTurn && kingMustRelease === turn;
1137
- const maxAP = boardState.maxActionPoints ?? 5;
1138
- 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: [
1139
1227
  /* @__PURE__ */ jsxRuntime.jsx(
1140
1228
  PlayerInfo,
1141
1229
  {
@@ -1180,14 +1268,16 @@ function PlayerInfo({ name, team, avatarUrl, isActive, actionPoints, maxActionPo
1180
1268
  /* @__PURE__ */ jsxRuntime.jsx(AvatarFallback, { className: "bg-bg-surface-elevated text-fg-secondary text-xs font-semibold", children: initials(name) })
1181
1269
  ] }),
1182
1270
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col gap-1", align === "right" && "items-end"), children: [
1183
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(
1184
- "font-inter text-[13px] font-semibold leading-none transition-colors duration-300",
1185
- isActive ? "text-fg-primary" : "text-fg-muted"
1186
- ), children: name }),
1187
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex items-center gap-1", align === "right" && "flex-row-reverse"), children: [
1188
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[10px] text-fg-muted uppercase", children: team }),
1189
- 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" })
1190
- ] }),
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
+ ),
1191
1281
  /* @__PURE__ */ jsxRuntime.jsx(
1192
1282
  ActionPoints,
1193
1283
  {
@@ -1208,7 +1298,7 @@ function MiniScoreboard({
1208
1298
  className
1209
1299
  }) {
1210
1300
  const t = useGameT();
1211
- const { boardState } = chunkLTFNSTAQ_cjs.useGameStore();
1301
+ const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
1212
1302
  if (!boardState?.score) return null;
1213
1303
  const { score, turn } = boardState;
1214
1304
  const myScore = userSide === "white" ? score.white : score.black;
@@ -1242,14 +1332,7 @@ function MiniScoreboard({
1242
1332
  }
1243
1333
  function SideCell({ name, team, score, isActive, align }) {
1244
1334
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col min-w-0", align === "right" ? "items-end" : "items-start"), children: [
1245
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: cn(
1246
- "font-mono text-[10px] uppercase tracking-[0.5px] truncate max-w-[160px]",
1247
- isActive ? "text-fg-secondary" : "text-fg-muted"
1248
- ), children: [
1249
- name,
1250
- " \xB7 ",
1251
- team
1252
- ] }),
1335
+ /* @__PURE__ */ jsxRuntime.jsx(PlayerIdentity, { name, team, variant: "compact", isActive }),
1253
1336
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(
1254
1337
  "font-anton text-[32px] leading-none tabular-nums",
1255
1338
  isActive ? "text-fg-primary" : "text-fg-secondary"
@@ -1393,11 +1476,9 @@ function ConfirmDialog({ open, title, description, confirmLabel, cancelLabel, on
1393
1476
  }
1394
1477
  );
1395
1478
  }
1396
- function GameControls({ isMyTurn }) {
1397
- const {
1398
- boardState,
1399
- endTurn
1400
- } = 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);
1401
1482
  const t = useGameT();
1402
1483
  const [showEndTurnConfirm, setShowEndTurnConfirm] = React.useState(false);
1403
1484
  if (!boardState) return null;
@@ -1409,7 +1490,7 @@ function GameControls({ isMyTurn }) {
1409
1490
  endTurn();
1410
1491
  }
1411
1492
  };
1412
- 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: [
1413
1494
  isMyTurn && /* @__PURE__ */ jsxRuntime.jsxs(
1414
1495
  Button,
1415
1496
  {
@@ -1429,7 +1510,15 @@ function GameControls({ isMyTurn }) {
1429
1510
  ]
1430
1511
  }
1431
1512
  ),
1432
- !isMyTurn && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-11 rounded-md bg-bg-surface border border-border-subtle", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-inter text-sm text-fg-muted", children: t("waitingRival") }) }),
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
+ ),
1433
1522
  /* @__PURE__ */ jsxRuntime.jsx(
1434
1523
  ConfirmDialog,
1435
1524
  {
@@ -1456,7 +1545,9 @@ var PIECE_ICON2 = {
1456
1545
  };
1457
1546
  function SelectedPieceDetail({ isMyTurn, className }) {
1458
1547
  const t = useGameT();
1459
- const { boardState, selectedPieceId } = chunkLTFNSTAQ_cjs.useGameStore();
1548
+ const { boardState, selectedPieceId } = chunkBSE4DUXM_cjs.useGameStore(
1549
+ shallow.useShallow((s) => ({ boardState: s.boardState, selectedPieceId: s.selectedPieceId }))
1550
+ );
1460
1551
  if (!boardState) return null;
1461
1552
  const piece = boardState.pieces.find((p) => p.id === selectedPieceId);
1462
1553
  if (!piece) {
@@ -1490,7 +1581,6 @@ function SelectedPieceDetail({ isMyTurn, className }) {
1490
1581
  ),
1491
1582
  role: "status",
1492
1583
  "aria-live": "polite",
1493
- "aria-label": `${longName} ${square}`,
1494
1584
  children: [
1495
1585
  /* @__PURE__ */ jsxRuntime.jsx(
1496
1586
  "div",
@@ -1519,7 +1609,7 @@ function SelectedPieceDetail({ isMyTurn, className }) {
1519
1609
  }
1520
1610
  function MoveHistory({ scrollable = true, limit, className }) {
1521
1611
  const t = useGameT();
1522
- const { boardState } = chunkLTFNSTAQ_cjs.useGameStore();
1612
+ const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
1523
1613
  const scrollRef = React.useRef(null);
1524
1614
  const history = boardState?.moveHistory ?? [];
1525
1615
  const visible = limit ? history.slice(-limit) : history;
@@ -1626,9 +1716,9 @@ function HistoryRow({ entry, isLatest }) {
1626
1716
  }
1627
1717
  var COLS2 = 9;
1628
1718
  var ROWS2 = 12;
1629
- function StaticGameBoard() {
1630
- const boardState = React.useMemo(() => chunkLTFNSTAQ_cjs.getInitialBoardState("white"), []);
1631
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full overflow-hidden rounded-xl shadow-2xl border-4 border-[#1a3317] bg-[#1a3317]", children: [
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: [
1632
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(
1633
1723
  "div",
1634
1724
  {
@@ -1672,9 +1762,10 @@ function StaticGameBoard() {
1672
1762
  }
1673
1763
  function TurnBanner({ isMyTurn, waitingLabel, className }) {
1674
1764
  const t = useGameT();
1675
- const { boardState } = chunkLTFNSTAQ_cjs.useGameStore();
1765
+ const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
1676
1766
  if (!boardState) return null;
1677
- const { actionPoints, maxActionPoints } = boardState;
1767
+ const { actionPoints, turnNumber } = boardState;
1768
+ const maxActionPoints = boardState.maxActionPoints ?? chunkBSE4DUXM_cjs.DEFAULT_MAX_AP;
1678
1769
  return /* @__PURE__ */ jsxRuntime.jsxs(
1679
1770
  "div",
1680
1771
  {
@@ -1686,16 +1777,19 @@ function TurnBanner({ isMyTurn, waitingLabel, className }) {
1686
1777
  "aria-live": "polite",
1687
1778
  "aria-atomic": "true",
1688
1779
  children: [
1689
- /* @__PURE__ */ jsxRuntime.jsx(
1690
- "span",
1691
- {
1692
- className: cn(
1693
- "font-anton text-sm tracking-[1.5px] uppercase",
1694
- isMyTurn ? "text-accent-green" : "text-fg-muted"
1695
- ),
1696
- children: isMyTurn ? t("yourTurn") : waitingLabel ?? t("waitingRival")
1697
- }
1698
- ),
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
+ ] }),
1699
1793
  isMyTurn && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", "aria-label": t("actionPointsAriaLabel", { remaining: actionPoints, total: maxActionPoints }), children: [
1700
1794
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Zap, { size: 12, className: "text-accent-green", "aria-hidden": "true" }),
1701
1795
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-mono text-xs text-fg-secondary tabular-nums", children: [
@@ -1717,7 +1811,7 @@ var TOAST_CONFIG = {
1717
1811
  };
1718
1812
  function EventToast({ className }) {
1719
1813
  const t = useGameT();
1720
- const { boardState } = chunkLTFNSTAQ_cjs.useGameStore();
1814
+ const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
1721
1815
  const [toast, setToast] = React.useState(null);
1722
1816
  React.useEffect(() => {
1723
1817
  const lastMove = boardState?.lastMove;
@@ -1756,7 +1850,7 @@ function EventToast({ className }) {
1756
1850
  );
1757
1851
  }
1758
1852
  function MobileHistory({ className }) {
1759
- const { boardState } = chunkLTFNSTAQ_cjs.useGameStore();
1853
+ const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
1760
1854
  const [open, setOpen] = React.useState(false);
1761
1855
  const titleId = `${React.useId()}-title`;
1762
1856
  const panelRef = useDialogA11y({ open, onClose: () => setOpen(false) });
@@ -1863,6 +1957,35 @@ function HistoryChip({ lastMove, onClick, className }) {
1863
1957
  );
1864
1958
  }
1865
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
+
1866
1989
  exports.ActionPoints = ActionPoints;
1867
1990
  exports.Avatar = Avatar;
1868
1991
  exports.AvatarFallback = AvatarFallback;
@@ -1887,6 +2010,8 @@ exports.PITCH_COLS = PITCH_COLS;
1887
2010
  exports.PITCH_ROWS = PITCH_ROWS;
1888
2011
  exports.PitchMarkings = PitchMarkings;
1889
2012
  exports.PitchSurface = PitchSurface;
2013
+ exports.PlayerIdentity = PlayerIdentity;
2014
+ exports.REQUIRED_TOKENS = REQUIRED_TOKENS;
1890
2015
  exports.Scoreboard = Scoreboard;
1891
2016
  exports.SelectedPieceDetail = SelectedPieceDetail;
1892
2017
  exports.StaticGameBoard = StaticGameBoard;