@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.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
  ),
@@ -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 fallback = (key) => key;
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 = chunkLTFNSTAQ_cjs.useGameStore((s) => s.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
- } = 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
+ );
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 renderSquares = () => {
678
- const squares = [];
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
- isAmbiguous && "ring-[3px] ring-inset ring-yellow-400/90 bg-yellow-400/20",
708
- !isAmbiguous && isValidMove && "ring-[3px] ring-inset ring-yellow-400/80 bg-yellow-400/15",
709
- !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"
710
778
  ),
711
779
  children: [
712
- !isAmbiguous && isValidMove && !pieceAt && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-full bg-yellow-400/65" }),
713
- !isAmbiguous && isValidPass && !pieceAt && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-full bg-sky-400/65" }),
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-yellow-400/80" }),
716
- /* @__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" })
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 squares;
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
- "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",
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-[#1a3317]",
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
- keyboardNav && "focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-green focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary"
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) => e.stopPropagation(),
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("div", { className: "grid grid-cols-9 gap-[1px]", children: renderSquares() }),
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", "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(
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
- if (!src) return null;
994
- 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
+ );
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 { boardState } = chunkLTFNSTAQ_cjs.useGameStore();
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 ?? 5;
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("span", { className: cn(
1121
- "font-inter text-[13px] font-semibold leading-none transition-colors duration-300",
1122
- isActive ? "text-fg-primary" : "text-fg-muted"
1123
- ), children: name }),
1124
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex items-center gap-1", align === "right" && "flex-row-reverse"), children: [
1125
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[10px] text-fg-muted uppercase", children: team }),
1126
- 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" })
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 { boardState } = chunkLTFNSTAQ_cjs.useGameStore();
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.jsx(SideCell, { name: myName, team: myTeam, score: myScore, isActive: isMyTurn, align: "left" }),
1168
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-anton text-base text-fg-muted uppercase tracking-[2px]", "aria-hidden": "true", children: "vs" }),
1169
- /* @__PURE__ */ jsxRuntime.jsx(SideCell, { name: rivalName, team: rivalTeam, score: rivalScore, isActive: !isMyTurn, align: "right" })
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.jsxs("span", { className: cn(
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
- primary: "bg-accent-green text-fg-primary font-anton tracking-widest rounded-md hover:bg-accent-green-light active:bg-accent-green-dark",
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
- role: "dialog",
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
- boardState,
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("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
+ ),
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 } = chunkLTFNSTAQ_cjs.useGameStore();
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 { boardState } = chunkLTFNSTAQ_cjs.useGameStore();
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(() => chunkLTFNSTAQ_cjs.getInitialBoardState("white"), []);
1484
- 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: [
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 { boardState } = chunkLTFNSTAQ_cjs.useGameStore();
1765
+ const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
1529
1766
  if (!boardState) return null;
1530
- const { actionPoints, maxActionPoints } = boardState;
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.jsx(
1543
- "span",
1544
- {
1545
- className: cn(
1546
- "font-anton text-sm tracking-[1.5px] uppercase",
1547
- isMyTurn ? "text-accent-green" : "text-fg-muted"
1548
- ),
1549
- children: isMyTurn ? t("yourTurn") : waitingLabel ?? t("waitingRival")
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 { boardState } = chunkLTFNSTAQ_cjs.useGameStore();
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 { boardState } = chunkLTFNSTAQ_cjs.useGameStore();
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
- className: "fixed bottom-0 left-0 right-0 z-50 bg-bg-secondary rounded-t-2xl border-t border-border-subtle shadow-xl",
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": "Close",
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