@scriptonita/chess-football-ui 0.8.0 → 0.10.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');
@@ -43,12 +44,17 @@ var EMBOSS_FILTER = {
43
44
  white: "drop-shadow(0 -0.75px 0 rgba(255,255,255,0.65)) drop-shadow(0 1.25px 1px rgba(0,0,0,0.45))",
44
45
  black: "drop-shadow(0 -0.5px 0 rgba(255,255,255,0.3)) drop-shadow(0 1.25px 1.5px rgba(0,0,0,0.8))"
45
46
  };
46
- var PieceIcon = ({ type, side }) => {
47
+ var MOVED_GLYPH_COLOR = {
48
+ white: "#8b8b94",
49
+ black: "#bdbdc3"
50
+ };
51
+ var PieceIcon = ({ type, side, moved = false }) => {
47
52
  const Icon = PIECE_ICON[type] ?? lucideReact.ShieldQuestion;
48
53
  const style = {
49
54
  width: "clamp(16px, 7.4cqw, 44px)",
50
55
  height: "clamp(16px, 7.4cqw, 44px)",
51
- filter: EMBOSS_FILTER[side]
56
+ filter: EMBOSS_FILTER[side],
57
+ ...moved ? { color: MOVED_GLYPH_COLOR[side] } : null
52
58
  };
53
59
  return /* @__PURE__ */ jsxRuntime.jsx(Icon, { style, strokeWidth: 2, "aria-hidden": "true" });
54
60
  };
@@ -92,21 +98,20 @@ var ON_BOARD_CHIP_STYLE = {
92
98
  ].join(", ")
93
99
  }
94
100
  };
95
- var MOVED_STYLE = {
96
- filter: "saturate(0.15) brightness(0.75)"
97
- };
98
101
  function GamePiece({ piece, isSelected, hasBall, onClick, isMoving = false }) {
99
102
  const chipStyle = ON_BOARD_CHIP_STYLE[piece.side];
100
103
  return /* @__PURE__ */ jsxRuntime.jsxs(
101
104
  "div",
102
105
  {
103
106
  onClick,
104
- style: piece.hasMovedThisTurn ? MOVED_STYLE : void 0,
107
+ "data-moved": piece.hasMovedThisTurn ? "true" : "false",
105
108
  className: cn(
106
109
  "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
- isMoving && "scale-[1.08] z-40",
109
- piece.hasMovedThisTurn && "opacity-70"
110
+ // `--primary` was the package's one remaining shadcn token, and
111
+ // CrazyGames had to define it by hand just to make the selection
112
+ // ring visible. It is part of the design system now.
113
+ isSelected && "ring-4 ring-accent-green scale-110 z-30",
114
+ isMoving && "scale-[1.08] z-40"
110
115
  ),
111
116
  children: [
112
117
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -128,7 +133,7 @@ function GamePiece({ piece, isSelected, hasBall, onClick, isMoving = false }) {
128
133
  style: chipStyle,
129
134
  className: "absolute inset-0 rounded-full flex items-center justify-center",
130
135
  children: [
131
- /* @__PURE__ */ jsxRuntime.jsx(PieceIcon, { type: piece.type, side: piece.side }),
136
+ /* @__PURE__ */ jsxRuntime.jsx(PieceIcon, { type: piece.type, side: piece.side, moved: piece.hasMovedThisTurn }),
132
137
  hasBall && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 rounded-full ring-2 ring-orange-400 pointer-events-none" }),
133
138
  isSelected && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 rounded-full animate-pulse motion-reduce:animate-none ring-2 ring-yellow-400 ring-offset-2 ring-offset-transparent" })
134
139
  ]
@@ -138,6 +143,8 @@ function GamePiece({ piece, isSelected, hasBall, onClick, isMoving = false }) {
138
143
  }
139
144
  );
140
145
  }
146
+ var BALL_COLOR = "var(--ball-color, #ffffff)";
147
+ var BALL_SHADE = "color-mix(in srgb, var(--ball-color, #ffffff) 74%, #000000)";
141
148
  var Football = ({ className }) => {
142
149
  const bodyId = React.useId();
143
150
  const hiId = `${bodyId}-h`;
@@ -151,9 +158,9 @@ var Football = ({ className }) => {
151
158
  children: [
152
159
  /* @__PURE__ */ jsxRuntime.jsxs("defs", { children: [
153
160
  /* @__PURE__ */ jsxRuntime.jsxs("radialGradient", { id: bodyId, cx: "36%", cy: "32%", r: "68%", children: [
154
- /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: "#f0f9ff" }),
155
- /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "55%", stopColor: "#bae6fd" }),
156
- /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", stopColor: "#38bdf8" })
161
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", style: { stopColor: "#ffffff" } }),
162
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "55%", style: { stopColor: BALL_COLOR } }),
163
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", style: { stopColor: BALL_SHADE } })
157
164
  ] }),
158
165
  /* @__PURE__ */ jsxRuntime.jsxs("radialGradient", { id: hiId, cx: "32%", cy: "28%", r: "22%", children: [
159
166
  /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: "#ffffff", stopOpacity: "0.85" }),
@@ -196,6 +203,11 @@ var STATIC_I18N_KEYS = [
196
203
  "endTurnConfirmDescription",
197
204
  "endTurnConfirmYes",
198
205
  "endTurnKeepPlaying",
206
+ "hints.dismiss",
207
+ "hints.hasBall",
208
+ "hints.lastActionPoint",
209
+ "hints.selectPiece",
210
+ "hints.start",
199
211
  "history.empty",
200
212
  "history.goal",
201
213
  "history.interception",
@@ -265,7 +277,7 @@ function useGameT() {
265
277
  }
266
278
  function BallHolderChip({ className }) {
267
279
  const t = useGameT();
268
- const boardState = chunkLTFNSTAQ_cjs.useGameStore((s) => s.boardState);
280
+ const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
269
281
  const holder = boardState?.pieces.find((p) => p.id === boardState.ball.holderId) ?? null;
270
282
  if (!holder) return null;
271
283
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex items-center gap-1.5 min-h-[1.75rem]", className), "aria-live": "polite", children: [
@@ -458,20 +470,32 @@ var ROWS = 12;
458
470
  var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
459
471
  var cellX = (x) => `${x * 100}%`;
460
472
  var cellY = (y) => `${(ROWS - 1 - y) * 100}%`;
473
+ var TOUCH_CLICK_WINDOW_MS = 800;
461
474
  var PIECE_SLIDE = { type: "spring", stiffness: 190, damping: 28, mass: 1 };
462
475
  var BALL_SLIDE = { type: "spring", stiffness: 300, damping: 26, mass: 0.6 };
463
- function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, toolbar = true }) {
476
+ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, toolbar = true, forgivingTaps = false, className }) {
464
477
  const t = useGameT();
465
478
  const boardId = React.useId();
466
479
  const prefersReducedMotion = framerMotion.useReducedMotion();
467
480
  const {
468
481
  boardState,
469
482
  selectedPieceId,
483
+ interactionMode,
470
484
  setSelectedPieceId,
471
485
  setInteractionMode,
472
486
  movePiece,
473
487
  passBall
474
- } = chunkLTFNSTAQ_cjs.useGameStore();
488
+ } = chunkBSE4DUXM_cjs.useGameStore(
489
+ shallow.useShallow((s) => ({
490
+ boardState: s.boardState,
491
+ selectedPieceId: s.selectedPieceId,
492
+ interactionMode: s.interactionMode,
493
+ setSelectedPieceId: s.setSelectedPieceId,
494
+ setInteractionMode: s.setInteractionMode,
495
+ movePiece: s.movePiece,
496
+ passBall: s.passBall
497
+ }))
498
+ );
475
499
  const [cursor, setCursor] = React.useState(null);
476
500
  const [disambiguateAt, setDisambiguateAt] = React.useState(null);
477
501
  const [shortcutsOpen, setShortcutsOpen] = React.useState(false);
@@ -493,6 +517,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
493
517
  setInvalidClickAt({ x, y, nonce: invalidClickNonceRef.current });
494
518
  };
495
519
  const [hoveredPassAt, setHoveredPassAt] = React.useState(null);
520
+ const lastTouchAtRef = React.useRef(0);
496
521
  const [movingIds, setMovingIds] = React.useState(() => /* @__PURE__ */ new Set());
497
522
  const setMoving = (id, moving) => setMovingIds((prev) => {
498
523
  if (prev.has(id) === moving) return prev;
@@ -539,21 +564,21 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
539
564
  return () => clearTimeout(id);
540
565
  }, [replay]);
541
566
  const validMoves = React.useMemo(() => {
542
- if (!boardState) return [];
567
+ if (!boardState || interactionMode === "pass") return [];
543
568
  const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
544
569
  if (sp && !sp.hasMovedThisTurn) {
545
570
  return chessFootballEngine.getValidMoves(sp, boardState);
546
571
  }
547
572
  return [];
548
- }, [boardState, selectedPieceId]);
573
+ }, [boardState, selectedPieceId, interactionMode]);
549
574
  const validPasses = React.useMemo(() => {
550
- if (!boardState) return [];
575
+ if (!boardState || interactionMode === "move") return [];
551
576
  const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
552
577
  if (sp && boardState.ball.holderId === sp.id) {
553
578
  return chessFootballEngine.getValidPasses(sp, boardState);
554
579
  }
555
580
  return [];
556
- }, [boardState, selectedPieceId]);
581
+ }, [boardState, selectedPieceId, interactionMode]);
557
582
  if (!boardState) return null;
558
583
  const lastMove = boardState.lastMove;
559
584
  const ball = boardState.ball;
@@ -578,11 +603,24 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
578
603
  const passCarrier = boardState.pieces.find((p) => p.id === selectedPieceId && boardState.ball.holderId === p.id) ?? null;
579
604
  const trajectoryPath = hoveredPassAt && passCarrier ? passCarrier.type === "knight" ? [hoveredPassAt] : chessFootballEngine.getPath(passCarrier.pos, hoveredPassAt) : null;
580
605
  const trajectoryIntercepted = !!(trajectoryPath && passCarrier && trajectoryPath.some((sq) => boardState.pieces.some((p) => p.pos.x === sq.x && p.pos.y === sq.y && p.side !== passCarrier.side)));
581
- const handleSquareClick = (x, y) => {
606
+ const soleAdjacentTarget = (x, y) => {
607
+ const near = [...validMoves, ...validPasses].filter(
608
+ (t2, i, all) => Math.max(Math.abs(t2.x - x), Math.abs(t2.y - y)) === 1 && all.findIndex((o) => o.x === t2.x && o.y === t2.y) === i
609
+ );
610
+ return near.length === 1 ? near[0] : null;
611
+ };
612
+ const handleSquareClick = (x, y, fromTouch = false) => {
582
613
  if (replay) return;
583
614
  setCursor(null);
584
615
  const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
585
616
  const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
617
+ if (!isValidMove && !isValidPass && fromTouch && forgivingTaps && selectedPieceId) {
618
+ const target = soleAdjacentTarget(x, y);
619
+ if (target) {
620
+ handleSquareClick(target.x, target.y);
621
+ return;
622
+ }
623
+ }
586
624
  if (isValidMove && isValidPass) {
587
625
  setDisambiguateAt({ x, y });
588
626
  return;
@@ -667,8 +705,11 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
667
705
  activate(c.x, c.y);
668
706
  break;
669
707
  }
708
+ // Bare M / P only: Cmd+P is Print and Ctrl+M is a browser shortcut,
709
+ // and both used to resolve the popover behind the user's back.
670
710
  case "m":
671
711
  case "M":
712
+ if (e.ctrlKey || e.metaKey || e.altKey) break;
672
713
  if (disambiguateAt && selectedPieceId) {
673
714
  movePiece(selectedPieceId, disambiguateAt);
674
715
  setDisambiguateAt(null);
@@ -676,6 +717,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
676
717
  break;
677
718
  case "p":
678
719
  case "P":
720
+ if (e.ctrlKey || e.metaKey || e.altKey) break;
679
721
  if (disambiguateAt) {
680
722
  passBall(disambiguateAt);
681
723
  setDisambiguateAt(null);
@@ -735,27 +777,35 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
735
777
  role: "gridcell",
736
778
  "aria-label": squareLabel(x, y, pieceAt, isValidMove, isValidPass),
737
779
  "aria-selected": !!pieceAt && pieceAt.id === selectedPieceId,
738
- onClick: () => handleSquareClick(x, y),
780
+ onClick: () => handleSquareClick(x, y, Date.now() - lastTouchAtRef.current < TOUCH_CLICK_WINDOW_MS),
739
781
  onMouseEnter: () => {
740
782
  if (isValidPass) setHoveredPassAt({ x, y });
741
783
  },
742
784
  onMouseLeave: () => setHoveredPassAt(null),
785
+ onTouchStart: () => {
786
+ lastTouchAtRef.current = Date.now();
787
+ if (isValidPass) setHoveredPassAt({ x, y });
788
+ },
743
789
  className: cn(
744
790
  pitchSquareClass(x, y),
745
791
  "flex items-center justify-center cursor-pointer",
746
792
  !isValidMove && !isValidPass && isLastMove && "bg-last-move-highlight/20",
747
793
  // §8: pulsing warning ring on enemy goal area
748
794
  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"
795
+ // Move and pass used raw yellow-400/sky-400 here while
796
+ // the popover a few lines below used the
797
+ // move-highlight/pass-highlight tokens — two palettes
798
+ // for the same two concepts. One palette now: tokens.
799
+ isAmbiguous && "ring-[3px] ring-inset ring-move-highlight/90 bg-move-highlight/20",
800
+ !isAmbiguous && isValidMove && "ring-[3px] ring-inset ring-move-highlight/80 bg-move-highlight/15",
801
+ !isAmbiguous && isValidPass && "ring-[3px] ring-inset ring-pass-highlight/80 bg-pass-highlight/15"
752
802
  ),
753
803
  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" })
804
+ !isAmbiguous && isValidMove && !pieceAt && /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "move-marker", className: "w-3 h-3 rounded-full bg-move-highlight/65" }),
805
+ !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" }),
806
+ isAmbiguous && !pieceAt && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5", children: [
807
+ /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "move-marker", className: "w-2.5 h-2.5 rounded-full bg-move-highlight/80" }),
808
+ /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "pass-marker", className: "w-2.5 h-2.5 rounded-full border-2 border-pass-highlight/90" })
759
809
  ] }),
760
810
  isCursor && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 ring-2 ring-inset ring-white pointer-events-none z-20", "aria-hidden": "true" }),
761
811
  isDisambiguateTarget && selectedPieceId && /* @__PURE__ */ jsxRuntime.jsxs(
@@ -820,10 +870,14 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
820
870
  "data-testid": "board-root",
821
871
  className: cn(
822
872
  // §6: Mobile edge-to-edge — thin 2px margin, no border, no radius
823
- "w-full mx-0.5 bg-[#1a3317] overflow-hidden",
873
+ // The frame was hardcoded #1a3317 in three files, hand-tuned for
874
+ // the webapp's darker grass and visibly wrong against the
875
+ // CrazyGames pitch. It is a token now (see README).
876
+ "w-full mx-0.5 bg-field-frame overflow-hidden",
824
877
  // §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))]"
878
+ "md:mx-auto md:rounded-xl md:shadow-2xl md:border-4 md:border-field-frame",
879
+ "md:max-w-[min(700px,calc((100dvh_-_150px)*0.75))]",
880
+ !showCoordinates && className
827
881
  ),
828
882
  children: [
829
883
  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: [
@@ -850,7 +904,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
850
904
  },
851
905
  "aria-label": t("shortcuts.buttonLabel"),
852
906
  "aria-expanded": shortcutsOpen,
853
- className: "w-8 h-8 flex items-center justify-center rounded-full bg-bg-secondary/80 border border-border-subtle text-fg-muted hover:text-fg-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-green",
907
+ className: "w-11 h-11 flex items-center justify-center rounded-full bg-bg-secondary/80 border border-border-subtle text-fg-muted hover:text-fg-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-green",
854
908
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.HelpCircle, { size: 16, strokeWidth: 2, "aria-hidden": "true" })
855
909
  }
856
910
  ),
@@ -1020,7 +1074,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
1020
1074
  );
1021
1075
  if (!showCoordinates) return board;
1022
1076
  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(
1077
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("w-full max-w-[540px] md:max-w-none mx-auto", className), children: /* @__PURE__ */ jsxRuntime.jsxs(
1024
1078
  "div",
1025
1079
  {
1026
1080
  className: "grid gap-1 select-none",
@@ -1052,8 +1106,20 @@ function Avatar({ className, children }) {
1052
1106
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("relative w-9 h-9 rounded-full overflow-hidden shrink-0", className), children });
1053
1107
  }
1054
1108
  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) });
1109
+ const [failed, setFailed] = React.useState(false);
1110
+ React.useEffect(() => {
1111
+ setFailed(false);
1112
+ }, [src]);
1113
+ if (!src || failed) return null;
1114
+ return /* @__PURE__ */ jsxRuntime.jsx(
1115
+ "img",
1116
+ {
1117
+ src,
1118
+ alt: alt ?? "",
1119
+ onError: () => setFailed(true),
1120
+ className: cn("w-full h-full object-cover", className)
1121
+ }
1122
+ );
1057
1123
  }
1058
1124
  function AvatarFallback({ className, children }) {
1059
1125
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("absolute inset-0 flex items-center justify-center text-xs font-semibold", className), children });
@@ -1109,6 +1175,51 @@ function ActionPoints({ total, remaining, size = 20, className, kingMustRelease
1109
1175
  );
1110
1176
  }) });
1111
1177
  }
1178
+ function PlayerIdentity({
1179
+ name,
1180
+ team,
1181
+ variant = "full",
1182
+ isActive = false,
1183
+ align = "left",
1184
+ adornment,
1185
+ className
1186
+ }) {
1187
+ if (variant === "compact") {
1188
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1189
+ "span",
1190
+ {
1191
+ className: cn(
1192
+ "font-inter text-[11px] font-medium truncate max-w-[160px]",
1193
+ isActive ? "text-fg-secondary" : "text-fg-muted",
1194
+ className
1195
+ ),
1196
+ children: [
1197
+ name,
1198
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-mono uppercase tracking-[0.5px] text-fg-muted", children: [
1199
+ " \xB7 ",
1200
+ team
1201
+ ] })
1202
+ ]
1203
+ }
1204
+ );
1205
+ }
1206
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col gap-1 min-w-0", align === "right" && "items-end", className), children: [
1207
+ /* @__PURE__ */ jsxRuntime.jsx(
1208
+ "span",
1209
+ {
1210
+ className: cn(
1211
+ "font-inter text-[13px] font-semibold leading-none truncate transition-colors duration-300",
1212
+ isActive ? "text-fg-primary" : "text-fg-muted"
1213
+ ),
1214
+ children: name
1215
+ }
1216
+ ),
1217
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex items-center gap-1", align === "right" && "flex-row-reverse"), children: [
1218
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[10px] text-fg-muted uppercase tracking-[0.5px]", children: team }),
1219
+ adornment
1220
+ ] })
1221
+ ] });
1222
+ }
1112
1223
  function initials(name) {
1113
1224
  return (name?.split(" ").map((w) => w[0]).join("").toUpperCase() || "?").slice(0, 2);
1114
1225
  }
@@ -1117,10 +1228,11 @@ function Scoreboard({
1117
1228
  opponentUsername,
1118
1229
  creatorAvatarUrl,
1119
1230
  opponentAvatarUrl,
1120
- userSide
1231
+ userSide,
1232
+ className
1121
1233
  }) {
1122
1234
  const t = useGameT();
1123
- const { boardState } = chunkLTFNSTAQ_cjs.useGameStore();
1235
+ const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
1124
1236
  if (!boardState?.score) return null;
1125
1237
  const { score, turn, actionPoints, kingMustRelease } = boardState;
1126
1238
  const myScore = userSide === "white" ? score.white : score.black;
@@ -1134,8 +1246,8 @@ function Scoreboard({
1134
1246
  const isMyTurn = turn === userSide;
1135
1247
  const myKingMustRelease = isMyTurn && kingMustRelease === turn;
1136
1248
  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: [
1249
+ const maxAP = boardState.maxActionPoints ?? chunkBSE4DUXM_cjs.DEFAULT_MAX_AP;
1250
+ 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
1251
  /* @__PURE__ */ jsxRuntime.jsx(
1140
1252
  PlayerInfo,
1141
1253
  {
@@ -1180,14 +1292,16 @@ function PlayerInfo({ name, team, avatarUrl, isActive, actionPoints, maxActionPo
1180
1292
  /* @__PURE__ */ jsxRuntime.jsx(AvatarFallback, { className: "bg-bg-surface-elevated text-fg-secondary text-xs font-semibold", children: initials(name) })
1181
1293
  ] }),
1182
1294
  /* @__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
- ] }),
1295
+ /* @__PURE__ */ jsxRuntime.jsx(
1296
+ PlayerIdentity,
1297
+ {
1298
+ name,
1299
+ team,
1300
+ isActive,
1301
+ align,
1302
+ 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
1303
+ }
1304
+ ),
1191
1305
  /* @__PURE__ */ jsxRuntime.jsx(
1192
1306
  ActionPoints,
1193
1307
  {
@@ -1208,7 +1322,7 @@ function MiniScoreboard({
1208
1322
  className
1209
1323
  }) {
1210
1324
  const t = useGameT();
1211
- const { boardState } = chunkLTFNSTAQ_cjs.useGameStore();
1325
+ const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
1212
1326
  if (!boardState?.score) return null;
1213
1327
  const { score, turn } = boardState;
1214
1328
  const myScore = userSide === "white" ? score.white : score.black;
@@ -1242,14 +1356,7 @@ function MiniScoreboard({
1242
1356
  }
1243
1357
  function SideCell({ name, team, score, isActive, align }) {
1244
1358
  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
- ] }),
1359
+ /* @__PURE__ */ jsxRuntime.jsx(PlayerIdentity, { name, team, variant: "compact", isActive }),
1253
1360
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(
1254
1361
  "font-anton text-[32px] leading-none tabular-nums",
1255
1362
  isActive ? "text-fg-primary" : "text-fg-secondary"
@@ -1393,11 +1500,9 @@ function ConfirmDialog({ open, title, description, confirmLabel, cancelLabel, on
1393
1500
  }
1394
1501
  );
1395
1502
  }
1396
- function GameControls({ isMyTurn }) {
1397
- const {
1398
- boardState,
1399
- endTurn
1400
- } = chunkLTFNSTAQ_cjs.useGameStore();
1503
+ function GameControls({ isMyTurn, className }) {
1504
+ const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
1505
+ const endTurn = chunkBSE4DUXM_cjs.useGameStore((s) => s.endTurn);
1401
1506
  const t = useGameT();
1402
1507
  const [showEndTurnConfirm, setShowEndTurnConfirm] = React.useState(false);
1403
1508
  if (!boardState) return null;
@@ -1409,7 +1514,7 @@ function GameControls({ isMyTurn }) {
1409
1514
  endTurn();
1410
1515
  }
1411
1516
  };
1412
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full bg-bg-secondary flex flex-col gap-3 px-5 pt-3 pb-5", children: [
1517
+ 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
1518
  isMyTurn && /* @__PURE__ */ jsxRuntime.jsxs(
1414
1519
  Button,
1415
1520
  {
@@ -1429,7 +1534,15 @@ function GameControls({ isMyTurn }) {
1429
1534
  ]
1430
1535
  }
1431
1536
  ),
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") }) }),
1537
+ !isMyTurn && /* @__PURE__ */ jsxRuntime.jsx(
1538
+ "div",
1539
+ {
1540
+ role: "status",
1541
+ "aria-live": "polite",
1542
+ className: "flex items-center justify-center h-11 rounded-md bg-bg-surface border border-border-subtle",
1543
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-inter text-sm text-fg-muted", children: t("waitingRival") })
1544
+ }
1545
+ ),
1433
1546
  /* @__PURE__ */ jsxRuntime.jsx(
1434
1547
  ConfirmDialog,
1435
1548
  {
@@ -1456,7 +1569,9 @@ var PIECE_ICON2 = {
1456
1569
  };
1457
1570
  function SelectedPieceDetail({ isMyTurn, className }) {
1458
1571
  const t = useGameT();
1459
- const { boardState, selectedPieceId } = chunkLTFNSTAQ_cjs.useGameStore();
1572
+ const { boardState, selectedPieceId } = chunkBSE4DUXM_cjs.useGameStore(
1573
+ shallow.useShallow((s) => ({ boardState: s.boardState, selectedPieceId: s.selectedPieceId }))
1574
+ );
1460
1575
  if (!boardState) return null;
1461
1576
  const piece = boardState.pieces.find((p) => p.id === selectedPieceId);
1462
1577
  if (!piece) {
@@ -1490,7 +1605,6 @@ function SelectedPieceDetail({ isMyTurn, className }) {
1490
1605
  ),
1491
1606
  role: "status",
1492
1607
  "aria-live": "polite",
1493
- "aria-label": `${longName} ${square}`,
1494
1608
  children: [
1495
1609
  /* @__PURE__ */ jsxRuntime.jsx(
1496
1610
  "div",
@@ -1519,7 +1633,7 @@ function SelectedPieceDetail({ isMyTurn, className }) {
1519
1633
  }
1520
1634
  function MoveHistory({ scrollable = true, limit, className }) {
1521
1635
  const t = useGameT();
1522
- const { boardState } = chunkLTFNSTAQ_cjs.useGameStore();
1636
+ const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
1523
1637
  const scrollRef = React.useRef(null);
1524
1638
  const history = boardState?.moveHistory ?? [];
1525
1639
  const visible = limit ? history.slice(-limit) : history;
@@ -1626,9 +1740,9 @@ function HistoryRow({ entry, isLatest }) {
1626
1740
  }
1627
1741
  var COLS2 = 9;
1628
1742
  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: [
1743
+ function StaticGameBoard({ className } = {}) {
1744
+ const boardState = React.useMemo(() => chunkBSE4DUXM_cjs.getInitialBoardState("white"), []);
1745
+ 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
1746
  /* @__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
1747
  "div",
1634
1748
  {
@@ -1672,9 +1786,10 @@ function StaticGameBoard() {
1672
1786
  }
1673
1787
  function TurnBanner({ isMyTurn, waitingLabel, className }) {
1674
1788
  const t = useGameT();
1675
- const { boardState } = chunkLTFNSTAQ_cjs.useGameStore();
1789
+ const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
1676
1790
  if (!boardState) return null;
1677
- const { actionPoints, maxActionPoints } = boardState;
1791
+ const { actionPoints, turnNumber } = boardState;
1792
+ const maxActionPoints = boardState.maxActionPoints ?? chunkBSE4DUXM_cjs.DEFAULT_MAX_AP;
1678
1793
  return /* @__PURE__ */ jsxRuntime.jsxs(
1679
1794
  "div",
1680
1795
  {
@@ -1686,16 +1801,19 @@ function TurnBanner({ isMyTurn, waitingLabel, className }) {
1686
1801
  "aria-live": "polite",
1687
1802
  "aria-atomic": "true",
1688
1803
  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
- ),
1804
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2 min-w-0", children: [
1805
+ /* @__PURE__ */ jsxRuntime.jsx(
1806
+ "span",
1807
+ {
1808
+ className: cn(
1809
+ "font-anton text-sm tracking-[1.5px] uppercase truncate",
1810
+ isMyTurn ? "text-accent-green" : "text-fg-muted"
1811
+ ),
1812
+ children: isMyTurn ? t("yourTurn") : waitingLabel ?? t("waitingRival")
1813
+ }
1814
+ ),
1815
+ 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 }) })
1816
+ ] }),
1699
1817
  isMyTurn && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", "aria-label": t("actionPointsAriaLabel", { remaining: actionPoints, total: maxActionPoints }), children: [
1700
1818
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Zap, { size: 12, className: "text-accent-green", "aria-hidden": "true" }),
1701
1819
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-mono text-xs text-fg-secondary tabular-nums", children: [
@@ -1717,7 +1835,7 @@ var TOAST_CONFIG = {
1717
1835
  };
1718
1836
  function EventToast({ className }) {
1719
1837
  const t = useGameT();
1720
- const { boardState } = chunkLTFNSTAQ_cjs.useGameStore();
1838
+ const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
1721
1839
  const [toast, setToast] = React.useState(null);
1722
1840
  React.useEffect(() => {
1723
1841
  const lastMove = boardState?.lastMove;
@@ -1756,7 +1874,7 @@ function EventToast({ className }) {
1756
1874
  );
1757
1875
  }
1758
1876
  function MobileHistory({ className }) {
1759
- const { boardState } = chunkLTFNSTAQ_cjs.useGameStore();
1877
+ const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
1760
1878
  const [open, setOpen] = React.useState(false);
1761
1879
  const titleId = `${React.useId()}-title`;
1762
1880
  const panelRef = useDialogA11y({ open, onClose: () => setOpen(false) });
@@ -1808,7 +1926,7 @@ function SheetHeader({ titleId, onClose }) {
1808
1926
  "button",
1809
1927
  {
1810
1928
  onClick: onClose,
1811
- 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",
1929
+ className: "w-11 h-11 -mr-2 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",
1812
1930
  "aria-label": t("history.close"),
1813
1931
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 14, "aria-hidden": "true" })
1814
1932
  }
@@ -1862,20 +1980,209 @@ function HistoryChip({ lastMove, onClick, className }) {
1862
1980
  }
1863
1981
  );
1864
1982
  }
1983
+ var GAME_HINT_IDS = ["lastActionPoint", "hasBall", "selectPiece", "start"];
1984
+ function GameHints({ userSide, seen, onSeen, className }) {
1985
+ const t = useGameT();
1986
+ const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
1987
+ const selectedPieceId = chunkBSE4DUXM_cjs.useGameStore((s) => s.selectedPieceId);
1988
+ const [done, setDone] = React.useState([]);
1989
+ const myTurn = !!boardState && userSide !== null && boardState.turn === userSide;
1990
+ const selected = myTurn ? boardState.pieces.find((p) => p.id === selectedPieceId) : void 0;
1991
+ const applies = {
1992
+ lastActionPoint: myTurn && boardState.actionPoints === 1,
1993
+ hasBall: !!selected && boardState?.ball.holderId === selected.id,
1994
+ selectPiece: !!selected,
1995
+ start: myTurn && !selected
1996
+ };
1997
+ const current = GAME_HINT_IDS.find((id) => applies[id] && !seen.includes(id) && !done.includes(id)) ?? null;
1998
+ const onSeenRef = React.useRef(onSeen);
1999
+ onSeenRef.current = onSeen;
2000
+ const markSeen = (id) => {
2001
+ setDone((d) => d.includes(id) ? d : [...d, id]);
2002
+ onSeenRef.current(id);
2003
+ };
2004
+ const shownRef = React.useRef(null);
2005
+ React.useEffect(() => {
2006
+ const prev = shownRef.current;
2007
+ shownRef.current = current;
2008
+ if (prev && prev !== current && !done.includes(prev)) markSeen(prev);
2009
+ }, [current]);
2010
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: cn("pointer-events-none flex justify-center", className), children: current && /* @__PURE__ */ jsxRuntime.jsxs(
2011
+ "div",
2012
+ {
2013
+ "data-testid": "game-hint",
2014
+ "data-hint": current,
2015
+ className: "pointer-events-auto flex items-center gap-1 max-w-sm pl-4 pr-0.5 rounded-xl bg-bg-secondary/95 border border-border-subtle shadow-lg backdrop-blur-sm",
2016
+ children: [
2017
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "py-2 font-inter text-sm leading-snug text-fg-primary", children: t(`hints.${current}`) }),
2018
+ /* @__PURE__ */ jsxRuntime.jsx(
2019
+ "button",
2020
+ {
2021
+ type: "button",
2022
+ onClick: () => markSeen(current),
2023
+ "aria-label": t("hints.dismiss"),
2024
+ className: "shrink-0 w-11 h-11 flex items-center justify-center rounded-full text-fg-muted hover:text-fg-primary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-green",
2025
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 16, "aria-hidden": "true" })
2026
+ }
2027
+ )
2028
+ ]
2029
+ }
2030
+ ) });
2031
+ }
2032
+ var BOT_TURN_START_DELAY_MS = 400;
2033
+ var BOT_TURN_STEP_MS = 900;
2034
+ var BOT_TURN_SETTLE_MS = 500;
2035
+ var PROVIDER_DEFER_MS = 30;
2036
+ function useBotTurn(opts) {
2037
+ const [botThinking, setBotThinking] = React.useState(false);
2038
+ const [error, setError] = React.useState(null);
2039
+ const [retryNonce, setRetryNonce] = React.useState(0);
2040
+ const optsRef = React.useRef(opts);
2041
+ optsRef.current = opts;
2042
+ const pending = React.useRef(false);
2043
+ const runId = React.useRef(0);
2044
+ const timers = React.useRef(/* @__PURE__ */ new Set());
2045
+ const skipRef = React.useRef(null);
2046
+ const clearTimers = () => {
2047
+ timers.current.forEach(clearTimeout);
2048
+ timers.current.clear();
2049
+ };
2050
+ React.useEffect(() => () => {
2051
+ runId.current++;
2052
+ clearTimers();
2053
+ pending.current = false;
2054
+ skipRef.current = null;
2055
+ }, []);
2056
+ const { boardState, enabled = true, matchOver = false, goalPending = false } = opts;
2057
+ React.useEffect(() => {
2058
+ const o = optsRef.current;
2059
+ const board = o.boardState;
2060
+ if (!board || !enabled || board.turn !== o.botSide) return;
2061
+ if (pending.current || matchOver || goalPending) return;
2062
+ if (chessFootballEngine.checkGoal(board)) return;
2063
+ const run = ++runId.current;
2064
+ const alive = () => runId.current === run;
2065
+ const wait = (ms) => new Promise((resolve) => {
2066
+ const id = setTimeout(() => {
2067
+ timers.current.delete(id);
2068
+ resolve();
2069
+ }, ms);
2070
+ timers.current.add(id);
2071
+ });
2072
+ const settle = () => {
2073
+ pending.current = false;
2074
+ skipRef.current = null;
2075
+ setBotThinking(false);
2076
+ optsRef.current.onProcessingChange?.(false);
2077
+ };
2078
+ pending.current = true;
2079
+ setBotThinking(true);
2080
+ setError(null);
2081
+ o.onProcessingChange?.(true);
2082
+ let skipRequested = false;
2083
+ skipRef.current = () => {
2084
+ skipRequested = true;
2085
+ };
2086
+ const startDelay = o.startDelayMs ?? BOT_TURN_START_DELAY_MS;
2087
+ const stepMs = o.stepMs ?? BOT_TURN_STEP_MS;
2088
+ const settleMs = o.settleMs ?? BOT_TURN_SETTLE_MS;
2089
+ (async () => {
2090
+ try {
2091
+ const [turn] = await Promise.all([
2092
+ wait(Math.min(PROVIDER_DEFER_MS, startDelay)).then(() => o.getTurn(board, o.botSide)),
2093
+ wait(startDelay)
2094
+ ]);
2095
+ if (!alive()) return;
2096
+ if (!turn?.states?.length) throw new Error("Bot turn returned no states");
2097
+ const { states } = turn;
2098
+ const last = states[states.length - 1];
2099
+ const shown = turn.closedByEndTurn ? states.length - 1 : states.length;
2100
+ const finish = () => {
2101
+ clearTimers();
2102
+ optsRef.current.onApplyState(last);
2103
+ settle();
2104
+ const scorer = turn.goalScored ?? null;
2105
+ if (scorer) optsRef.current.onGoal?.(scorer, last);
2106
+ };
2107
+ skipRef.current = () => {
2108
+ if (alive()) finish();
2109
+ };
2110
+ if (skipRequested) {
2111
+ finish();
2112
+ return;
2113
+ }
2114
+ for (let i = 0; i < shown; i++) {
2115
+ optsRef.current.onApplyState(states[i]);
2116
+ await wait(i === shown - 1 ? settleMs : stepMs);
2117
+ if (!alive()) return;
2118
+ }
2119
+ finish();
2120
+ } catch (err) {
2121
+ if (!alive()) return;
2122
+ clearTimers();
2123
+ settle();
2124
+ setError(err instanceof Error ? err : new Error(String(err)));
2125
+ }
2126
+ })();
2127
+ }, [boardState?.turn, enabled, matchOver, goalPending, retryNonce]);
2128
+ const retry = React.useCallback(() => {
2129
+ setError(null);
2130
+ setRetryNonce((n) => n + 1);
2131
+ }, []);
2132
+ const skip = React.useCallback(() => {
2133
+ skipRef.current?.();
2134
+ }, []);
2135
+ return { botThinking, error, retry, skip };
2136
+ }
2137
+
2138
+ // src/tokens.ts
2139
+ var REQUIRED_TOKENS = [
2140
+ // Surfaces
2141
+ "--bg-primary",
2142
+ "--bg-secondary",
2143
+ "--bg-surface",
2144
+ "--bg-surface-elevated",
2145
+ // Text
2146
+ "--fg-primary",
2147
+ "--fg-secondary",
2148
+ "--fg-muted",
2149
+ // Accent (the primary CTA rests on the dark step — see Button)
2150
+ "--accent-green",
2151
+ "--accent-green-light",
2152
+ "--accent-green-dark",
2153
+ // Borders
2154
+ "--border-subtle",
2155
+ // Semantic
2156
+ "--danger",
2157
+ "--warning",
2158
+ // Board
2159
+ "--field-green-1",
2160
+ "--field-green-2",
2161
+ "--field-frame",
2162
+ "--move-highlight",
2163
+ "--pass-highlight",
2164
+ "--last-move-highlight",
2165
+ "--ball-color"
2166
+ ];
1865
2167
 
1866
2168
  exports.ActionPoints = ActionPoints;
1867
2169
  exports.Avatar = Avatar;
1868
2170
  exports.AvatarFallback = AvatarFallback;
1869
2171
  exports.AvatarImage = AvatarImage;
2172
+ exports.BOT_TURN_SETTLE_MS = BOT_TURN_SETTLE_MS;
2173
+ exports.BOT_TURN_START_DELAY_MS = BOT_TURN_START_DELAY_MS;
2174
+ exports.BOT_TURN_STEP_MS = BOT_TURN_STEP_MS;
1870
2175
  exports.BallHolderChip = BallHolderChip;
1871
2176
  exports.Button = Button;
1872
2177
  exports.ConfirmDialog = ConfirmDialog;
1873
2178
  exports.EventToast = EventToast;
1874
2179
  exports.Football = Football;
2180
+ exports.GAME_HINT_IDS = GAME_HINT_IDS;
1875
2181
  exports.GAME_I18N_KEYS = GAME_I18N_KEYS;
1876
2182
  exports.GOAL_AREA_SHADE_CLASS = GOAL_AREA_SHADE_CLASS;
1877
2183
  exports.GameBoard = GameBoard;
1878
2184
  exports.GameControls = GameControls;
2185
+ exports.GameHints = GameHints;
1879
2186
  exports.GameI18nProvider = GameI18nProvider;
1880
2187
  exports.GamePiece = GamePiece;
1881
2188
  exports.GrassOverlay = GrassOverlay;
@@ -1887,6 +2194,8 @@ exports.PITCH_COLS = PITCH_COLS;
1887
2194
  exports.PITCH_ROWS = PITCH_ROWS;
1888
2195
  exports.PitchMarkings = PitchMarkings;
1889
2196
  exports.PitchSurface = PitchSurface;
2197
+ exports.PlayerIdentity = PlayerIdentity;
2198
+ exports.REQUIRED_TOKENS = REQUIRED_TOKENS;
1890
2199
  exports.Scoreboard = Scoreboard;
1891
2200
  exports.SelectedPieceDetail = SelectedPieceDetail;
1892
2201
  exports.StaticGameBoard = StaticGameBoard;
@@ -1894,6 +2203,7 @@ exports.TurnBanner = TurnBanner;
1894
2203
  exports.cn = cn;
1895
2204
  exports.isGoalAreaSquare = isGoalAreaSquare;
1896
2205
  exports.pitchSquareClass = pitchSquareClass;
2206
+ exports.useBotTurn = useBotTurn;
1897
2207
  exports.useDialogA11y = useDialogA11y;
1898
2208
  exports.useGameT = useGameT;
1899
2209
  //# sourceMappingURL=index.cjs.map