@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/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
- import { useGameStore, getInitialBoardState } from './chunk-N24LGLLO.js';
1
+ import { useGameStore, DEFAULT_MAX_AP, getInitialBoardState } from './chunk-C5HCN7O5.js';
2
+ import { useShallow } from 'zustand/react/shallow';
2
3
  import { SHORT_KEY, LONG_KEY, getValidMoves, getValidPasses, isInEnemyArea, getPath, RANK_LABELS, FILE_LABELS, squareName } from '@scriptonita/chess-football-engine';
3
4
  import { clsx } from 'clsx';
4
5
  import { twMerge } from 'tailwind-merge';
@@ -83,7 +84,10 @@ function GamePiece({ piece, isSelected, hasBall, onClick, isMoving = false }) {
83
84
  style: piece.hasMovedThisTurn ? MOVED_STYLE : void 0,
84
85
  className: cn(
85
86
  "relative w-[88%] h-[88%] rounded-full cursor-pointer z-10 transition-transform duration-200 ease-out motion-reduce:transition-none",
86
- isSelected && "ring-4 ring-primary scale-110 z-30",
87
+ // `--primary` was the package's one remaining shadcn token, and
88
+ // CrazyGames had to define it by hand just to make the selection
89
+ // ring visible. It is part of the design system now.
90
+ isSelected && "ring-4 ring-accent-green scale-110 z-30",
87
91
  isMoving && "scale-[1.08] z-40",
88
92
  piece.hasMovedThisTurn && "opacity-70"
89
93
  ),
@@ -169,6 +173,7 @@ var STATIC_I18N_KEYS = [
169
173
  "actionPointsAriaLabel",
170
174
  "actionPointsShort",
171
175
  "ballHolder",
176
+ "boardAriaLabel",
172
177
  "endTurn",
173
178
  "endTurnConfirm",
174
179
  "endTurnConfirmDescription",
@@ -182,11 +187,13 @@ var STATIC_I18N_KEYS = [
182
187
  "history.pass",
183
188
  "history.tackle",
184
189
  "history.title",
190
+ "history.close",
185
191
  "history.turn",
186
192
  "history.viewAll",
187
193
  "move",
188
194
  "offsideWarning",
189
195
  "pass",
196
+ "scoreAriaLabel",
190
197
  "selectedPiece.alreadyMoved",
191
198
  "selectedPiece.atSquare",
192
199
  "selectedPiece.empty",
@@ -198,6 +205,14 @@ var STATIC_I18N_KEYS = [
198
205
  "shortcuts.pass",
199
206
  "shortcuts.select",
200
207
  "shortcuts.title",
208
+ "square.empty",
209
+ "square.emptyWithBall",
210
+ "square.moveOrPassTarget",
211
+ "square.moveTarget",
212
+ "square.passTarget",
213
+ "square.piece",
214
+ "square.pieceWithBall",
215
+ "square.selected",
201
216
  "teamBlack",
202
217
  "teamWhite",
203
218
  "waitingRival",
@@ -207,6 +222,7 @@ var PIECE_I18N_KEYS = [...Object.values(SHORT_KEY), ...Object.values(LONG_KEY)].
207
222
  (label) => `pieces.${label}`
208
223
  );
209
224
  var EVENT_TOAST_I18N_KEYS = [
225
+ "eventToast.goal",
210
226
  "eventToast.interception",
211
227
  "eventToast.offside",
212
228
  "eventToast.tackle"
@@ -216,7 +232,13 @@ var GAME_I18N_KEYS = [
216
232
  ...PIECE_I18N_KEYS,
217
233
  ...EVENT_TOAST_I18N_KEYS
218
234
  ];
219
- var fallback = (key) => key;
235
+ var nodeEnv = globalThis.process?.env?.NODE_ENV;
236
+ var fallback = (key) => {
237
+ if (nodeEnv && nodeEnv !== "production") {
238
+ console.warn(`[chess-football-ui] missing game i18n key: "${key}" (no GameI18nProvider?)`);
239
+ }
240
+ return key;
241
+ };
220
242
  var GameI18nContext = createContext(fallback);
221
243
  function GameI18nProvider({ t, children }) {
222
244
  return /* @__PURE__ */ jsx(GameI18nContext.Provider, { value: t, children: /* @__PURE__ */ jsx(MotionConfig, { reducedMotion: "user", children }) });
@@ -421,17 +443,29 @@ var cellX = (x) => `${x * 100}%`;
421
443
  var cellY = (y) => `${(ROWS - 1 - y) * 100}%`;
422
444
  var PIECE_SLIDE = { type: "spring", stiffness: 190, damping: 28, mass: 1 };
423
445
  var BALL_SLIDE = { type: "spring", stiffness: 300, damping: 26, mass: 0.6 };
424
- function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, toolbar = true }) {
446
+ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, toolbar = true, className }) {
425
447
  const t = useGameT();
448
+ const boardId = useId();
426
449
  const prefersReducedMotion = useReducedMotion();
427
450
  const {
428
451
  boardState,
429
452
  selectedPieceId,
453
+ interactionMode,
430
454
  setSelectedPieceId,
431
455
  setInteractionMode,
432
456
  movePiece,
433
457
  passBall
434
- } = useGameStore();
458
+ } = useGameStore(
459
+ useShallow((s) => ({
460
+ boardState: s.boardState,
461
+ selectedPieceId: s.selectedPieceId,
462
+ interactionMode: s.interactionMode,
463
+ setSelectedPieceId: s.setSelectedPieceId,
464
+ setInteractionMode: s.setInteractionMode,
465
+ movePiece: s.movePiece,
466
+ passBall: s.passBall
467
+ }))
468
+ );
435
469
  const [cursor, setCursor] = useState(null);
436
470
  const [disambiguateAt, setDisambiguateAt] = useState(null);
437
471
  const [shortcutsOpen, setShortcutsOpen] = useState(false);
@@ -499,21 +533,21 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
499
533
  return () => clearTimeout(id);
500
534
  }, [replay]);
501
535
  const validMoves = useMemo(() => {
502
- if (!boardState) return [];
536
+ if (!boardState || interactionMode === "pass") return [];
503
537
  const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
504
538
  if (sp && !sp.hasMovedThisTurn) {
505
539
  return getValidMoves(sp, boardState);
506
540
  }
507
541
  return [];
508
- }, [boardState, selectedPieceId]);
542
+ }, [boardState, selectedPieceId, interactionMode]);
509
543
  const validPasses = useMemo(() => {
510
- if (!boardState) return [];
544
+ if (!boardState || interactionMode === "move") return [];
511
545
  const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
512
546
  if (sp && boardState.ball.holderId === sp.id) {
513
547
  return getValidPasses(sp, boardState);
514
548
  }
515
549
  return [];
516
- }, [boardState, selectedPieceId]);
550
+ }, [boardState, selectedPieceId, interactionMode]);
517
551
  if (!boardState) return null;
518
552
  const lastMove = boardState.lastMove;
519
553
  const ball = boardState.ball;
@@ -627,8 +661,11 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
627
661
  activate(c.x, c.y);
628
662
  break;
629
663
  }
664
+ // Bare M / P only: Cmd+P is Print and Ctrl+M is a browser shortcut,
665
+ // and both used to resolve the popover behind the user's back.
630
666
  case "m":
631
667
  case "M":
668
+ if (e.ctrlKey || e.metaKey || e.altKey) break;
632
669
  if (disambiguateAt && selectedPieceId) {
633
670
  movePiece(selectedPieceId, disambiguateAt);
634
671
  setDisambiguateAt(null);
@@ -636,6 +673,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
636
673
  break;
637
674
  case "p":
638
675
  case "P":
676
+ if (e.ctrlKey || e.metaKey || e.altKey) break;
639
677
  if (disambiguateAt) {
640
678
  passBall(disambiguateAt);
641
679
  setDisambiguateAt(null);
@@ -653,9 +691,28 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
653
691
  break;
654
692
  }
655
693
  };
656
- const renderSquares = () => {
657
- const squares = [];
694
+ const cellId = (x, y) => `${boardId}-sq-${x}-${y}`;
695
+ const squareLabel = (x, y, pieceAt, isValidMove, isValidPass) => {
696
+ const square = squareName({ x, y });
697
+ const ballHere = ball.pos.x === x && ball.pos.y === y;
698
+ const parts = [];
699
+ if (pieceAt) {
700
+ const piece = t(`pieces.${LONG_KEY[pieceAt.type]}`);
701
+ const team = pieceAt.side === "white" ? t("teamWhite") : t("teamBlack");
702
+ parts.push(t(ballHere ? "square.pieceWithBall" : "square.piece", { square, piece, team }));
703
+ } else {
704
+ parts.push(t(ballHere ? "square.emptyWithBall" : "square.empty", { square }));
705
+ }
706
+ if (isValidMove && isValidPass) parts.push(t("square.moveOrPassTarget"));
707
+ else if (isValidMove) parts.push(t("square.moveTarget"));
708
+ else if (isValidPass) parts.push(t("square.passTarget"));
709
+ if (pieceAt && pieceAt.id === selectedPieceId) parts.push(t("square.selected"));
710
+ return parts.join(", ");
711
+ };
712
+ const renderRows = () => {
713
+ const rows = [];
658
714
  for (let y = ROWS - 1; y >= 0; y--) {
715
+ const squares = [];
659
716
  for (let x = 0; x < COLS; x++) {
660
717
  const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
661
718
  const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
@@ -672,27 +729,38 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
672
729
  /* @__PURE__ */ jsxs(
673
730
  "div",
674
731
  {
732
+ id: cellId(x, y),
733
+ role: "gridcell",
734
+ "aria-label": squareLabel(x, y, pieceAt, isValidMove, isValidPass),
735
+ "aria-selected": !!pieceAt && pieceAt.id === selectedPieceId,
675
736
  onClick: () => handleSquareClick(x, y),
676
737
  onMouseEnter: () => {
677
738
  if (isValidPass) setHoveredPassAt({ x, y });
678
739
  },
679
740
  onMouseLeave: () => setHoveredPassAt(null),
741
+ onTouchStart: () => {
742
+ if (isValidPass) setHoveredPassAt({ x, y });
743
+ },
680
744
  className: cn(
681
745
  pitchSquareClass(x, y),
682
746
  "flex items-center justify-center cursor-pointer",
683
747
  !isValidMove && !isValidPass && isLastMove && "bg-last-move-highlight/20",
684
748
  // §8: pulsing warning ring on enemy goal area
685
749
  isOffsideRisk && isEnemyGoalArea && !isValidMove && !isValidPass && "ring-2 ring-inset ring-warning/60",
686
- isAmbiguous && "ring-[3px] ring-inset ring-yellow-400/90 bg-yellow-400/20",
687
- !isAmbiguous && isValidMove && "ring-[3px] ring-inset ring-yellow-400/80 bg-yellow-400/15",
688
- !isAmbiguous && isValidPass && "ring-[3px] ring-inset ring-sky-400/80 bg-sky-400/15"
750
+ // Move and pass used raw yellow-400/sky-400 here while
751
+ // the popover a few lines below used the
752
+ // move-highlight/pass-highlight tokens — two palettes
753
+ // for the same two concepts. One palette now: tokens.
754
+ isAmbiguous && "ring-[3px] ring-inset ring-move-highlight/90 bg-move-highlight/20",
755
+ !isAmbiguous && isValidMove && "ring-[3px] ring-inset ring-move-highlight/80 bg-move-highlight/15",
756
+ !isAmbiguous && isValidPass && "ring-[3px] ring-inset ring-pass-highlight/80 bg-pass-highlight/15"
689
757
  ),
690
758
  children: [
691
- !isAmbiguous && isValidMove && !pieceAt && /* @__PURE__ */ jsx("div", { className: "w-3 h-3 rounded-full bg-yellow-400/65" }),
692
- !isAmbiguous && isValidPass && !pieceAt && /* @__PURE__ */ jsx("div", { className: "w-3 h-3 rounded-full bg-sky-400/65" }),
693
- isAmbiguous && !pieceAt && /* @__PURE__ */ jsxs("div", { className: "flex gap-0.5", children: [
694
- /* @__PURE__ */ jsx("div", { className: "w-2.5 h-2.5 rounded-full bg-yellow-400/80" }),
695
- /* @__PURE__ */ jsx("div", { className: "w-2.5 h-2.5 rounded-full bg-sky-400/80" })
759
+ !isAmbiguous && isValidMove && !pieceAt && /* @__PURE__ */ jsx("div", { "data-testid": "move-marker", className: "w-3 h-3 rounded-full bg-move-highlight/65" }),
760
+ !isAmbiguous && isValidPass && !pieceAt && /* @__PURE__ */ jsx("div", { "data-testid": "pass-marker", className: "w-3.5 h-3.5 rounded-full border-2 border-pass-highlight/80" }),
761
+ isAmbiguous && !pieceAt && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", children: [
762
+ /* @__PURE__ */ jsx("div", { "data-testid": "move-marker", className: "w-2.5 h-2.5 rounded-full bg-move-highlight/80" }),
763
+ /* @__PURE__ */ jsx("div", { "data-testid": "pass-marker", className: "w-2.5 h-2.5 rounded-full border-2 border-pass-highlight/90" })
696
764
  ] }),
697
765
  isCursor && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 ring-2 ring-inset ring-white pointer-events-none z-20", "aria-hidden": "true" }),
698
766
  isDisambiguateTarget && selectedPieceId && /* @__PURE__ */ jsxs(
@@ -745,26 +813,27 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
745
813
  )
746
814
  );
747
815
  }
816
+ rows.push(
817
+ /* @__PURE__ */ jsx("div", { role: "row", className: "contents", children: squares }, `row-${y}`)
818
+ );
748
819
  }
749
- return squares;
820
+ return rows;
750
821
  };
751
822
  const board = /* @__PURE__ */ jsxs(
752
823
  "div",
753
824
  {
825
+ "data-testid": "board-root",
754
826
  className: cn(
755
827
  // §6: Mobile edge-to-edge — thin 2px margin, no border, no radius
756
- "w-full mx-0.5 bg-[#1a3317] overflow-hidden",
828
+ // The frame was hardcoded #1a3317 in three files, hand-tuned for
829
+ // the webapp's darker grass and visibly wrong against the
830
+ // CrazyGames pitch. It is a token now (see README).
831
+ "w-full mx-0.5 bg-field-frame overflow-hidden",
757
832
  // §5+§6: Desktop — centered, rounded, bordered, height-based max-width
758
- "md:mx-auto md:rounded-xl md:shadow-2xl md:border-4 md:border-[#1a3317]",
833
+ "md:mx-auto md:rounded-xl md:shadow-2xl md:border-4 md:border-field-frame",
759
834
  "md:max-w-[min(700px,calc((100dvh_-_150px)*0.75))]",
760
- keyboardNav && "focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-green focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary"
835
+ !showCoordinates && className
761
836
  ),
762
- ...keyboardNav ? {
763
- tabIndex: 0,
764
- role: "application",
765
- "aria-label": "Game board \u2014 arrow keys to move the cursor, Enter to act",
766
- onKeyDown: handleKeyDown
767
- } : {},
768
837
  children: [
769
838
  toolbar && /* @__PURE__ */ jsxs("div", { className: "hidden md:flex items-center justify-between gap-2 px-2.5 py-1", "data-testid": "board-toolbar", children: [
770
839
  /* @__PURE__ */ jsx(BallHolderChip, {}),
@@ -773,7 +842,12 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
773
842
  {
774
843
  ref: shortcutsRef,
775
844
  className: "relative",
776
- onKeyDown: (e) => e.stopPropagation(),
845
+ onKeyDown: (e) => {
846
+ if (e.key === "Escape" && shortcutsOpen) {
847
+ e.stopPropagation();
848
+ setShortcutsOpen(false);
849
+ }
850
+ },
777
851
  children: [
778
852
  /* @__PURE__ */ jsx(
779
853
  "button",
@@ -805,7 +879,25 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
805
879
  )
806
880
  ] }),
807
881
  /* @__PURE__ */ jsxs("div", { className: "relative", style: { containerType: "inline-size" }, children: [
808
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-9 gap-[1px]", children: renderSquares() }),
882
+ /* @__PURE__ */ jsx(
883
+ "div",
884
+ {
885
+ role: "grid",
886
+ "aria-label": t("boardAriaLabel"),
887
+ "aria-rowcount": ROWS,
888
+ "aria-colcount": COLS,
889
+ className: cn(
890
+ "grid grid-cols-9 gap-[1px]",
891
+ keyboardNav && "focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent-green"
892
+ ),
893
+ ...keyboardNav ? {
894
+ tabIndex: 0,
895
+ onKeyDown: handleKeyDown,
896
+ "aria-activedescendant": cursor ? cellId(cursor.x, cursor.y) : void 0
897
+ } : {},
898
+ children: renderRows()
899
+ }
900
+ ),
809
901
  /* @__PURE__ */ jsx(GrassOverlay, {}),
810
902
  /* @__PURE__ */ jsx(PitchMarkings, {}),
811
903
  /* @__PURE__ */ jsx(
@@ -831,7 +923,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
831
923
  )
832
924
  }
833
925
  ),
834
- /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 pointer-events-none", "data-testid": "pieces-layer", children: [
926
+ /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 pointer-events-none", "data-testid": "pieces-layer", "aria-hidden": "true", children: [
835
927
  /* @__PURE__ */ jsx(AnimatePresence, { mode: "sync", children: boardState.pieces.map((piece) => {
836
928
  const isPickupCarrier = pickupCarrierId === piece.id;
837
929
  const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : PIECE_SLIDE;
@@ -937,7 +1029,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
937
1029
  );
938
1030
  if (!showCoordinates) return board;
939
1031
  const ranksTopToBottom = [...RANK_LABELS].reverse();
940
- return /* @__PURE__ */ jsx("div", { className: "w-full max-w-[540px] md:max-w-none mx-auto", "aria-hidden": "false", children: /* @__PURE__ */ jsxs(
1032
+ return /* @__PURE__ */ jsx("div", { className: cn("w-full max-w-[540px] md:max-w-none mx-auto", className), children: /* @__PURE__ */ jsxs(
941
1033
  "div",
942
1034
  {
943
1035
  className: "grid gap-1 select-none",
@@ -969,8 +1061,20 @@ function Avatar({ className, children }) {
969
1061
  return /* @__PURE__ */ jsx("div", { className: cn("relative w-9 h-9 rounded-full overflow-hidden shrink-0", className), children });
970
1062
  }
971
1063
  function AvatarImage({ src, alt, className }) {
972
- if (!src) return null;
973
- return /* @__PURE__ */ jsx("img", { src, alt: alt ?? "", className: cn("w-full h-full object-cover", className) });
1064
+ const [failed, setFailed] = useState(false);
1065
+ useEffect(() => {
1066
+ setFailed(false);
1067
+ }, [src]);
1068
+ if (!src || failed) return null;
1069
+ return /* @__PURE__ */ jsx(
1070
+ "img",
1071
+ {
1072
+ src,
1073
+ alt: alt ?? "",
1074
+ onError: () => setFailed(true),
1075
+ className: cn("w-full h-full object-cover", className)
1076
+ }
1077
+ );
974
1078
  }
975
1079
  function AvatarFallback({ className, children }) {
976
1080
  return /* @__PURE__ */ jsx("div", { className: cn("absolute inset-0 flex items-center justify-center text-xs font-semibold", className), children });
@@ -1026,6 +1130,51 @@ function ActionPoints({ total, remaining, size = 20, className, kingMustRelease
1026
1130
  );
1027
1131
  }) });
1028
1132
  }
1133
+ function PlayerIdentity({
1134
+ name,
1135
+ team,
1136
+ variant = "full",
1137
+ isActive = false,
1138
+ align = "left",
1139
+ adornment,
1140
+ className
1141
+ }) {
1142
+ if (variant === "compact") {
1143
+ return /* @__PURE__ */ jsxs(
1144
+ "span",
1145
+ {
1146
+ className: cn(
1147
+ "font-inter text-[11px] font-medium truncate max-w-[160px]",
1148
+ isActive ? "text-fg-secondary" : "text-fg-muted",
1149
+ className
1150
+ ),
1151
+ children: [
1152
+ name,
1153
+ /* @__PURE__ */ jsxs("span", { className: "font-mono uppercase tracking-[0.5px] text-fg-muted", children: [
1154
+ " \xB7 ",
1155
+ team
1156
+ ] })
1157
+ ]
1158
+ }
1159
+ );
1160
+ }
1161
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-1 min-w-0", align === "right" && "items-end", className), children: [
1162
+ /* @__PURE__ */ jsx(
1163
+ "span",
1164
+ {
1165
+ className: cn(
1166
+ "font-inter text-[13px] font-semibold leading-none truncate transition-colors duration-300",
1167
+ isActive ? "text-fg-primary" : "text-fg-muted"
1168
+ ),
1169
+ children: name
1170
+ }
1171
+ ),
1172
+ /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-1", align === "right" && "flex-row-reverse"), children: [
1173
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] text-fg-muted uppercase tracking-[0.5px]", children: team }),
1174
+ adornment
1175
+ ] })
1176
+ ] });
1177
+ }
1029
1178
  function initials(name) {
1030
1179
  return (name?.split(" ").map((w) => w[0]).join("").toUpperCase() || "?").slice(0, 2);
1031
1180
  }
@@ -1034,10 +1183,11 @@ function Scoreboard({
1034
1183
  opponentUsername,
1035
1184
  creatorAvatarUrl,
1036
1185
  opponentAvatarUrl,
1037
- userSide
1186
+ userSide,
1187
+ className
1038
1188
  }) {
1039
1189
  const t = useGameT();
1040
- const { boardState } = useGameStore();
1190
+ const boardState = useGameStore((s) => s.boardState);
1041
1191
  if (!boardState?.score) return null;
1042
1192
  const { score, turn, actionPoints, kingMustRelease } = boardState;
1043
1193
  const myScore = userSide === "white" ? score.white : score.black;
@@ -1051,8 +1201,8 @@ function Scoreboard({
1051
1201
  const isMyTurn = turn === userSide;
1052
1202
  const myKingMustRelease = isMyTurn && kingMustRelease === turn;
1053
1203
  const rivalKingMustRelease = !isMyTurn && kingMustRelease === turn;
1054
- const maxAP = boardState.maxActionPoints ?? 5;
1055
- return /* @__PURE__ */ jsxs("div", { className: "w-full bg-bg-secondary flex items-center justify-between px-5 py-2.5", children: [
1204
+ const maxAP = boardState.maxActionPoints ?? DEFAULT_MAX_AP;
1205
+ return /* @__PURE__ */ jsxs("div", { className: cn("w-full bg-bg-secondary flex items-center justify-between px-5 py-2.5", className), children: [
1056
1206
  /* @__PURE__ */ jsx(
1057
1207
  PlayerInfo,
1058
1208
  {
@@ -1066,10 +1216,11 @@ function Scoreboard({
1066
1216
  align: "left"
1067
1217
  }
1068
1218
  ),
1069
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1070
- /* @__PURE__ */ jsx("span", { className: "font-anton text-[32px] leading-none text-fg-primary tabular-nums", children: myScore }),
1071
- /* @__PURE__ */ jsx("span", { className: "font-anton text-2xl text-fg-muted", children: "\u2014" }),
1072
- /* @__PURE__ */ jsx("span", { className: "font-anton text-[32px] leading-none text-fg-primary tabular-nums", children: rivalScore })
1219
+ /* @__PURE__ */ jsxs("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: "flex items-center gap-3", children: [
1220
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "font-anton text-[32px] leading-none text-fg-primary tabular-nums", children: myScore }),
1221
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "font-anton text-2xl text-fg-muted", children: "\u2014" }),
1222
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "font-anton text-[32px] leading-none text-fg-primary tabular-nums", children: rivalScore }),
1223
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("scoreAriaLabel", { me: myName, myScore, rival: rivalName, rivalScore }) })
1073
1224
  ] }),
1074
1225
  /* @__PURE__ */ jsx(
1075
1226
  PlayerInfo,
@@ -1096,14 +1247,16 @@ function PlayerInfo({ name, team, avatarUrl, isActive, actionPoints, maxActionPo
1096
1247
  /* @__PURE__ */ jsx(AvatarFallback, { className: "bg-bg-surface-elevated text-fg-secondary text-xs font-semibold", children: initials(name) })
1097
1248
  ] }),
1098
1249
  /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-1", align === "right" && "items-end"), children: [
1099
- /* @__PURE__ */ jsx("span", { className: cn(
1100
- "font-inter text-[13px] font-semibold leading-none transition-colors duration-300",
1101
- isActive ? "text-fg-primary" : "text-fg-muted"
1102
- ), children: name }),
1103
- /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-1", align === "right" && "flex-row-reverse"), children: [
1104
- /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] text-fg-muted uppercase", children: team }),
1105
- isActive && /* @__PURE__ */ jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-accent-green animate-pulse motion-reduce:animate-none", "aria-hidden": "true" })
1106
- ] }),
1250
+ /* @__PURE__ */ jsx(
1251
+ PlayerIdentity,
1252
+ {
1253
+ name,
1254
+ team,
1255
+ isActive,
1256
+ align,
1257
+ adornment: isActive ? /* @__PURE__ */ 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
1258
+ }
1259
+ ),
1107
1260
  /* @__PURE__ */ jsx(
1108
1261
  ActionPoints,
1109
1262
  {
@@ -1124,7 +1277,7 @@ function MiniScoreboard({
1124
1277
  className
1125
1278
  }) {
1126
1279
  const t = useGameT();
1127
- const { boardState } = useGameStore();
1280
+ const boardState = useGameStore((s) => s.boardState);
1128
1281
  if (!boardState?.score) return null;
1129
1282
  const { score, turn } = boardState;
1130
1283
  const myScore = userSide === "white" ? score.white : score.black;
@@ -1137,29 +1290,28 @@ function MiniScoreboard({
1137
1290
  return /* @__PURE__ */ jsxs(
1138
1291
  "div",
1139
1292
  {
1293
+ role: "status",
1294
+ "aria-live": "polite",
1295
+ "aria-atomic": "true",
1140
1296
  className: cn(
1141
1297
  "w-full max-w-[540px] rounded-xl bg-bg-surface border border-border-subtle",
1142
1298
  "flex items-center justify-between gap-4 px-5 py-3",
1143
1299
  className
1144
1300
  ),
1145
1301
  children: [
1146
- /* @__PURE__ */ jsx(SideCell, { name: myName, team: myTeam, score: myScore, isActive: isMyTurn, align: "left" }),
1147
- /* @__PURE__ */ jsx("span", { className: "font-anton text-base text-fg-muted uppercase tracking-[2px]", "aria-hidden": "true", children: "vs" }),
1148
- /* @__PURE__ */ jsx(SideCell, { name: rivalName, team: rivalTeam, score: rivalScore, isActive: !isMyTurn, align: "right" })
1302
+ /* @__PURE__ */ jsxs("div", { className: "contents", "aria-hidden": "true", children: [
1303
+ /* @__PURE__ */ jsx(SideCell, { name: myName, team: myTeam, score: myScore, isActive: isMyTurn, align: "left" }),
1304
+ /* @__PURE__ */ jsx("span", { className: "font-anton text-base text-fg-muted uppercase tracking-[2px]", children: "vs" }),
1305
+ /* @__PURE__ */ jsx(SideCell, { name: rivalName, team: rivalTeam, score: rivalScore, isActive: !isMyTurn, align: "right" })
1306
+ ] }),
1307
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("scoreAriaLabel", { me: myName, myScore, rival: rivalName, rivalScore }) })
1149
1308
  ]
1150
1309
  }
1151
1310
  );
1152
1311
  }
1153
1312
  function SideCell({ name, team, score, isActive, align }) {
1154
1313
  return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col min-w-0", align === "right" ? "items-end" : "items-start"), children: [
1155
- /* @__PURE__ */ jsxs("span", { className: cn(
1156
- "font-mono text-[10px] uppercase tracking-[0.5px] truncate max-w-[160px]",
1157
- isActive ? "text-fg-secondary" : "text-fg-muted"
1158
- ), children: [
1159
- name,
1160
- " \xB7 ",
1161
- team
1162
- ] }),
1314
+ /* @__PURE__ */ jsx(PlayerIdentity, { name, team, variant: "compact", isActive }),
1163
1315
  /* @__PURE__ */ jsx("span", { className: cn(
1164
1316
  "font-anton text-[32px] leading-none tabular-nums",
1165
1317
  isActive ? "text-fg-primary" : "text-fg-secondary"
@@ -1167,7 +1319,12 @@ function SideCell({ name, team, score, isActive, align }) {
1167
1319
  ] });
1168
1320
  }
1169
1321
  var VARIANT = {
1170
- primary: "bg-accent-green text-fg-primary font-anton tracking-widest rounded-md hover:bg-accent-green-light active:bg-accent-green-dark",
1322
+ // White on `--accent-green` is 4.27:1 — under WCAG AA for the Anton 400 the
1323
+ // primary CTA is set in. `--accent-green-dark` clears it (~6.5:1), so the
1324
+ // resting state is the dark green and interaction brightens it rather than
1325
+ // darkening it. Hover/active are transient; the resting state is the one a
1326
+ // player reads.
1327
+ primary: "bg-accent-green-dark text-fg-primary font-anton tracking-widest rounded-md hover:bg-accent-green active:bg-accent-green-light",
1171
1328
  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",
1172
1329
  ghost: "bg-transparent text-fg-secondary font-inter font-medium rounded-md hover:text-fg-primary active:text-fg-muted",
1173
1330
  destructive: "bg-danger/10 text-danger font-inter font-semibold rounded-md border border-danger hover:bg-danger/20 active:bg-danger/30"
@@ -1197,24 +1354,97 @@ var Button = React.forwardRef(
1197
1354
  )
1198
1355
  );
1199
1356
  Button.displayName = "Button";
1357
+ var FOCUSABLE = [
1358
+ "a[href]",
1359
+ "button:not([disabled])",
1360
+ "input:not([disabled])",
1361
+ "select:not([disabled])",
1362
+ "textarea:not([disabled])",
1363
+ '[tabindex]:not([tabindex="-1"])'
1364
+ ].join(",");
1365
+ function focusableWithin(panel) {
1366
+ return Array.from(panel.querySelectorAll(FOCUSABLE)).filter(
1367
+ (el) => !el.closest("[hidden]") && !el.closest('[aria-hidden="true"]')
1368
+ );
1369
+ }
1370
+ function useDialogA11y({ open, onClose }) {
1371
+ const panelRef = useRef(null);
1372
+ const restoreToRef = useRef(null);
1373
+ const onCloseRef = useRef(onClose);
1374
+ useEffect(() => {
1375
+ onCloseRef.current = onClose;
1376
+ });
1377
+ useEffect(() => {
1378
+ if (!open) return;
1379
+ const panel = panelRef.current;
1380
+ if (!panel) return;
1381
+ restoreToRef.current = document.activeElement;
1382
+ const initial = focusableWithin(panel)[0];
1383
+ if (initial) {
1384
+ initial.focus();
1385
+ } else {
1386
+ panel.tabIndex = -1;
1387
+ panel.focus();
1388
+ }
1389
+ const onKeyDown = (e) => {
1390
+ if (e.key === "Escape") {
1391
+ e.stopPropagation();
1392
+ onCloseRef.current();
1393
+ return;
1394
+ }
1395
+ if (e.key !== "Tab") return;
1396
+ const items = focusableWithin(panel);
1397
+ if (items.length === 0) {
1398
+ e.preventDefault();
1399
+ return;
1400
+ }
1401
+ const first = items[0];
1402
+ const last = items[items.length - 1];
1403
+ const active = document.activeElement;
1404
+ if (e.shiftKey && (active === first || !panel.contains(active))) {
1405
+ e.preventDefault();
1406
+ last.focus();
1407
+ } else if (!e.shiftKey && (active === last || !panel.contains(active))) {
1408
+ e.preventDefault();
1409
+ first.focus();
1410
+ }
1411
+ };
1412
+ document.addEventListener("keydown", onKeyDown, true);
1413
+ return () => {
1414
+ document.removeEventListener("keydown", onKeyDown, true);
1415
+ restoreToRef.current?.focus?.();
1416
+ };
1417
+ }, [open]);
1418
+ return panelRef;
1419
+ }
1200
1420
  function ConfirmDialog({ open, title, description, confirmLabel, cancelLabel, onConfirm, onCancel }) {
1421
+ const baseId = useId();
1422
+ const titleId = `${baseId}-title`;
1423
+ const descId = `${baseId}-desc`;
1424
+ const panelRef = useDialogA11y({ open, onClose: onCancel });
1201
1425
  if (!open) return null;
1202
1426
  return /* @__PURE__ */ jsx(
1203
1427
  "div",
1204
1428
  {
1205
1429
  className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",
1206
- role: "dialog",
1207
- "aria-modal": "true",
1430
+ onClick: onCancel,
1208
1431
  children: /* @__PURE__ */ jsxs(
1209
1432
  "div",
1210
1433
  {
1434
+ ref: panelRef,
1435
+ role: "dialog",
1436
+ "aria-modal": "true",
1437
+ "aria-labelledby": titleId,
1438
+ "aria-describedby": description ? descId : void 0,
1439
+ onClick: (e) => e.stopPropagation(),
1211
1440
  className: cn(
1212
1441
  "bg-bg-surface border border-border-subtle rounded-xl p-6 mx-4 max-w-sm w-full",
1213
- "flex flex-col gap-4 shadow-2xl"
1442
+ "flex flex-col gap-4 shadow-2xl",
1443
+ "focus:outline-none"
1214
1444
  ),
1215
1445
  children: [
1216
- /* @__PURE__ */ jsx("h2", { className: "font-anton text-lg text-fg-primary uppercase tracking-wide", children: title }),
1217
- description && /* @__PURE__ */ jsx("p", { className: "font-inter text-sm text-fg-secondary", children: description }),
1446
+ /* @__PURE__ */ jsx("h2", { id: titleId, className: "font-anton text-lg text-fg-primary uppercase tracking-wide", children: title }),
1447
+ description && /* @__PURE__ */ jsx("p", { id: descId, className: "font-inter text-sm text-fg-secondary", children: description }),
1218
1448
  /* @__PURE__ */ jsxs("div", { className: "flex gap-2 pt-1", children: [
1219
1449
  /* @__PURE__ */ jsx(Button, { variant: "secondary", className: "flex-1", onClick: onCancel, children: cancelLabel }),
1220
1450
  /* @__PURE__ */ jsx(Button, { variant: "primary", className: "flex-1", onClick: onConfirm, children: confirmLabel })
@@ -1225,11 +1455,9 @@ function ConfirmDialog({ open, title, description, confirmLabel, cancelLabel, on
1225
1455
  }
1226
1456
  );
1227
1457
  }
1228
- function GameControls({ isMyTurn }) {
1229
- const {
1230
- boardState,
1231
- endTurn
1232
- } = useGameStore();
1458
+ function GameControls({ isMyTurn, className }) {
1459
+ const boardState = useGameStore((s) => s.boardState);
1460
+ const endTurn = useGameStore((s) => s.endTurn);
1233
1461
  const t = useGameT();
1234
1462
  const [showEndTurnConfirm, setShowEndTurnConfirm] = useState(false);
1235
1463
  if (!boardState) return null;
@@ -1241,7 +1469,7 @@ function GameControls({ isMyTurn }) {
1241
1469
  endTurn();
1242
1470
  }
1243
1471
  };
1244
- return /* @__PURE__ */ jsxs("div", { className: "w-full bg-bg-secondary flex flex-col gap-3 px-5 pt-3 pb-5", children: [
1472
+ return /* @__PURE__ */ jsxs("div", { className: cn("w-full bg-bg-secondary flex flex-col gap-3 px-5 pt-3 pb-5", className), children: [
1245
1473
  isMyTurn && /* @__PURE__ */ jsxs(
1246
1474
  Button,
1247
1475
  {
@@ -1261,7 +1489,15 @@ function GameControls({ isMyTurn }) {
1261
1489
  ]
1262
1490
  }
1263
1491
  ),
1264
- !isMyTurn && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-11 rounded-md bg-bg-surface border border-border-subtle", children: /* @__PURE__ */ jsx("span", { className: "font-inter text-sm text-fg-muted", children: t("waitingRival") }) }),
1492
+ !isMyTurn && /* @__PURE__ */ jsx(
1493
+ "div",
1494
+ {
1495
+ role: "status",
1496
+ "aria-live": "polite",
1497
+ className: "flex items-center justify-center h-11 rounded-md bg-bg-surface border border-border-subtle",
1498
+ children: /* @__PURE__ */ jsx("span", { className: "font-inter text-sm text-fg-muted", children: t("waitingRival") })
1499
+ }
1500
+ ),
1265
1501
  /* @__PURE__ */ jsx(
1266
1502
  ConfirmDialog,
1267
1503
  {
@@ -1288,7 +1524,9 @@ var PIECE_ICON2 = {
1288
1524
  };
1289
1525
  function SelectedPieceDetail({ isMyTurn, className }) {
1290
1526
  const t = useGameT();
1291
- const { boardState, selectedPieceId } = useGameStore();
1527
+ const { boardState, selectedPieceId } = useGameStore(
1528
+ useShallow((s) => ({ boardState: s.boardState, selectedPieceId: s.selectedPieceId }))
1529
+ );
1292
1530
  if (!boardState) return null;
1293
1531
  const piece = boardState.pieces.find((p) => p.id === selectedPieceId);
1294
1532
  if (!piece) {
@@ -1322,7 +1560,6 @@ function SelectedPieceDetail({ isMyTurn, className }) {
1322
1560
  ),
1323
1561
  role: "status",
1324
1562
  "aria-live": "polite",
1325
- "aria-label": `${longName} ${square}`,
1326
1563
  children: [
1327
1564
  /* @__PURE__ */ jsx(
1328
1565
  "div",
@@ -1351,7 +1588,7 @@ function SelectedPieceDetail({ isMyTurn, className }) {
1351
1588
  }
1352
1589
  function MoveHistory({ scrollable = true, limit, className }) {
1353
1590
  const t = useGameT();
1354
- const { boardState } = useGameStore();
1591
+ const boardState = useGameStore((s) => s.boardState);
1355
1592
  const scrollRef = useRef(null);
1356
1593
  const history = boardState?.moveHistory ?? [];
1357
1594
  const visible = limit ? history.slice(-limit) : history;
@@ -1458,9 +1695,9 @@ function HistoryRow({ entry, isLatest }) {
1458
1695
  }
1459
1696
  var COLS2 = 9;
1460
1697
  var ROWS2 = 12;
1461
- function StaticGameBoard() {
1698
+ function StaticGameBoard({ className } = {}) {
1462
1699
  const boardState = useMemo(() => getInitialBoardState("white"), []);
1463
- return /* @__PURE__ */ jsxs("div", { className: "relative w-full overflow-hidden rounded-xl shadow-2xl border-4 border-[#1a3317] bg-[#1a3317]", children: [
1700
+ return /* @__PURE__ */ jsxs("div", { className: cn("relative w-full overflow-hidden rounded-xl shadow-2xl border-4 border-field-frame bg-field-frame", className), children: [
1464
1701
  /* @__PURE__ */ jsx(PitchSurface, { children: /* @__PURE__ */ jsx("div", { className: "absolute inset-0 pointer-events-none", children: boardState.pieces.map((piece) => /* @__PURE__ */ jsx(
1465
1702
  "div",
1466
1703
  {
@@ -1504,9 +1741,10 @@ function StaticGameBoard() {
1504
1741
  }
1505
1742
  function TurnBanner({ isMyTurn, waitingLabel, className }) {
1506
1743
  const t = useGameT();
1507
- const { boardState } = useGameStore();
1744
+ const boardState = useGameStore((s) => s.boardState);
1508
1745
  if (!boardState) return null;
1509
- const { actionPoints, maxActionPoints } = boardState;
1746
+ const { actionPoints, turnNumber } = boardState;
1747
+ const maxActionPoints = boardState.maxActionPoints ?? DEFAULT_MAX_AP;
1510
1748
  return /* @__PURE__ */ jsxs(
1511
1749
  "div",
1512
1750
  {
@@ -1518,16 +1756,19 @@ function TurnBanner({ isMyTurn, waitingLabel, className }) {
1518
1756
  "aria-live": "polite",
1519
1757
  "aria-atomic": "true",
1520
1758
  children: [
1521
- /* @__PURE__ */ jsx(
1522
- "span",
1523
- {
1524
- className: cn(
1525
- "font-anton text-sm tracking-[1.5px] uppercase",
1526
- isMyTurn ? "text-accent-green" : "text-fg-muted"
1527
- ),
1528
- children: isMyTurn ? t("yourTurn") : waitingLabel ?? t("waitingRival")
1529
- }
1530
- ),
1759
+ /* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-2 min-w-0", children: [
1760
+ /* @__PURE__ */ jsx(
1761
+ "span",
1762
+ {
1763
+ className: cn(
1764
+ "font-anton text-sm tracking-[1.5px] uppercase truncate",
1765
+ isMyTurn ? "text-accent-green" : "text-fg-muted"
1766
+ ),
1767
+ children: isMyTurn ? t("yourTurn") : waitingLabel ?? t("waitingRival")
1768
+ }
1769
+ ),
1770
+ typeof turnNumber === "number" && /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] text-fg-muted uppercase tracking-[0.5px] shrink-0", children: t("history.turn", { n: turnNumber }) })
1771
+ ] }),
1531
1772
  isMyTurn && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", "aria-label": t("actionPointsAriaLabel", { remaining: actionPoints, total: maxActionPoints }), children: [
1532
1773
  /* @__PURE__ */ jsx(Zap, { size: 12, className: "text-accent-green", "aria-hidden": "true" }),
1533
1774
  /* @__PURE__ */ jsxs("span", { className: "font-mono text-xs text-fg-secondary tabular-nums", children: [
@@ -1540,15 +1781,16 @@ function TurnBanner({ isMyTurn, waitingLabel, className }) {
1540
1781
  }
1541
1782
  );
1542
1783
  }
1543
- var TOASTABLE = /* @__PURE__ */ new Set(["interception", "offside", "tackle"]);
1784
+ var TOASTABLE = /* @__PURE__ */ new Set(["goal", "interception", "offside", "tackle"]);
1544
1785
  var TOAST_CONFIG = {
1786
+ goal: "bg-accent-green/20 border-accent-green/60 text-accent-green-light",
1545
1787
  interception: "bg-danger/20 border-danger/50 text-danger",
1546
1788
  offside: "bg-warning/20 border-warning/50 text-warning",
1547
1789
  tackle: "bg-warning/20 border-warning/50 text-warning"
1548
1790
  };
1549
1791
  function EventToast({ className }) {
1550
1792
  const t = useGameT();
1551
- const { boardState } = useGameStore();
1793
+ const boardState = useGameStore((s) => s.boardState);
1552
1794
  const [toast, setToast] = useState(null);
1553
1795
  useEffect(() => {
1554
1796
  const lastMove = boardState?.lastMove;
@@ -1587,8 +1829,10 @@ function EventToast({ className }) {
1587
1829
  );
1588
1830
  }
1589
1831
  function MobileHistory({ className }) {
1590
- const { boardState } = useGameStore();
1832
+ const boardState = useGameStore((s) => s.boardState);
1591
1833
  const [open, setOpen] = useState(false);
1834
+ const titleId = `${useId()}-title`;
1835
+ const panelRef = useDialogA11y({ open, onClose: () => setOpen(false) });
1592
1836
  const lastMove = boardState?.moveHistory?.at(-1);
1593
1837
  if (!lastMove) return null;
1594
1838
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1614,11 +1858,13 @@ function MobileHistory({ className }) {
1614
1858
  animate: { y: 0 },
1615
1859
  exit: { y: "100%" },
1616
1860
  transition: { type: "spring", stiffness: 300, damping: 32 },
1617
- className: "fixed bottom-0 left-0 right-0 z-50 bg-bg-secondary rounded-t-2xl border-t border-border-subtle shadow-xl",
1861
+ ref: panelRef,
1862
+ 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",
1618
1863
  role: "dialog",
1619
1864
  "aria-modal": "true",
1865
+ "aria-labelledby": titleId,
1620
1866
  children: [
1621
- /* @__PURE__ */ jsx(SheetHeader, { onClose: () => setOpen(false) }),
1867
+ /* @__PURE__ */ jsx(SheetHeader, { titleId, onClose: () => setOpen(false) }),
1622
1868
  /* @__PURE__ */ jsx("div", { className: "px-4 pb-6 max-h-[50dvh] overflow-y-auto", children: /* @__PURE__ */ jsx(MoveHistory, { scrollable: false }) })
1623
1869
  ]
1624
1870
  },
@@ -1627,17 +1873,17 @@ function MobileHistory({ className }) {
1627
1873
  ] }) })
1628
1874
  ] });
1629
1875
  }
1630
- function SheetHeader({ onClose }) {
1876
+ function SheetHeader({ titleId, onClose }) {
1631
1877
  const t = useGameT();
1632
1878
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-5 pt-4 pb-3 border-b border-border-subtle", children: [
1633
- /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] tracking-[0.5px] uppercase text-fg-muted", children: t("history.title") }),
1879
+ /* @__PURE__ */ jsx("span", { id: titleId, className: "font-mono text-[10px] tracking-[0.5px] uppercase text-fg-muted", children: t("history.title") }),
1634
1880
  /* @__PURE__ */ jsx(
1635
1881
  "button",
1636
1882
  {
1637
1883
  onClick: onClose,
1638
1884
  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",
1639
- "aria-label": "Close",
1640
- children: /* @__PURE__ */ jsx(X, { size: 14 })
1885
+ "aria-label": t("history.close"),
1886
+ children: /* @__PURE__ */ jsx(X, { size: 14, "aria-hidden": "true" })
1641
1887
  }
1642
1888
  )
1643
1889
  ] });
@@ -1690,6 +1936,35 @@ function HistoryChip({ lastMove, onClick, className }) {
1690
1936
  );
1691
1937
  }
1692
1938
 
1693
- export { ActionPoints, Avatar, AvatarFallback, AvatarImage, BallHolderChip, Button, ConfirmDialog, EventToast, Football, GAME_I18N_KEYS, GOAL_AREA_SHADE_CLASS, GameBoard, GameControls, GameI18nProvider, GamePiece, GrassOverlay, KeyboardShortcutsList, MiniScoreboard, MobileHistory, MoveHistory, PITCH_COLS, PITCH_ROWS, PitchMarkings, PitchSurface, Scoreboard, SelectedPieceDetail, StaticGameBoard, TurnBanner, cn, isGoalAreaSquare, pitchSquareClass, useGameT };
1939
+ // src/tokens.ts
1940
+ var REQUIRED_TOKENS = [
1941
+ // Surfaces
1942
+ "--bg-primary",
1943
+ "--bg-secondary",
1944
+ "--bg-surface",
1945
+ "--bg-surface-elevated",
1946
+ // Text
1947
+ "--fg-primary",
1948
+ "--fg-secondary",
1949
+ "--fg-muted",
1950
+ // Accent (the primary CTA rests on the dark step — see Button)
1951
+ "--accent-green",
1952
+ "--accent-green-light",
1953
+ "--accent-green-dark",
1954
+ // Borders
1955
+ "--border-subtle",
1956
+ // Semantic
1957
+ "--danger",
1958
+ "--warning",
1959
+ // Board
1960
+ "--field-green-1",
1961
+ "--field-green-2",
1962
+ "--field-frame",
1963
+ "--move-highlight",
1964
+ "--pass-highlight",
1965
+ "--last-move-highlight"
1966
+ ];
1967
+
1968
+ export { ActionPoints, Avatar, AvatarFallback, AvatarImage, BallHolderChip, Button, ConfirmDialog, EventToast, Football, GAME_I18N_KEYS, GOAL_AREA_SHADE_CLASS, GameBoard, GameControls, GameI18nProvider, GamePiece, GrassOverlay, KeyboardShortcutsList, MiniScoreboard, MobileHistory, MoveHistory, PITCH_COLS, PITCH_ROWS, PitchMarkings, PitchSurface, PlayerIdentity, REQUIRED_TOKENS, Scoreboard, SelectedPieceDetail, StaticGameBoard, TurnBanner, cn, isGoalAreaSquare, pitchSquareClass, useDialogA11y, useGameT };
1694
1969
  //# sourceMappingURL=index.js.map
1695
1970
  //# sourceMappingURL=index.js.map