@scriptonita/chess-football-ui 0.1.1 → 0.2.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,12 +1,12 @@
1
1
  import { useGameStore, getInitialBoardState } from './chunk-YLNDQ4HG.js';
2
- import { getValidMoves, getValidPasses, RANK_LABELS, FILE_LABELS, LONG_KEY, squareName, SHORT_KEY } from '@scriptonita/chess-football-engine';
2
+ import { getValidMoves, getValidPasses, isInEnemyArea, RANK_LABELS, FILE_LABELS, LONG_KEY, squareName, SHORT_KEY } from '@scriptonita/chess-football-engine';
3
3
  import { clsx } from 'clsx';
4
4
  import { twMerge } from 'tailwind-merge';
5
- import { AnimatePresence, motion } from 'framer-motion';
6
- import { Crown, Zap, Move, Flag, ShieldQuestion, ChessKnight, ChessBishop, ChessRook, ChessQueen, ChessKing } from 'lucide-react';
7
- import { jsx, jsxs } from 'react/jsx-runtime';
5
+ import { MotionConfig, AnimatePresence, motion } from 'framer-motion';
6
+ import { Crown, Zap, Flag, ShieldQuestion, ChessKnight, ChessBishop, ChessRook, ChessQueen, ChessKing, X, Move } from 'lucide-react';
7
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
8
8
  import * as React from 'react';
9
- import { createContext, useId, useState, useRef, useEffect, useMemo, useContext } from 'react';
9
+ import { createContext, useId, useContext, useState, useRef, useEffect, useMemo } from 'react';
10
10
 
11
11
  function cn(...inputs) {
12
12
  return twMerge(clsx(inputs));
@@ -18,14 +18,18 @@ var PIECE_ICON = {
18
18
  bishop: ChessBishop,
19
19
  knight: ChessKnight
20
20
  };
21
- var PieceIcon = ({ type }) => {
21
+ var EMBOSS_FILTER = {
22
+ 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))",
23
+ 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))"
24
+ };
25
+ var PieceIcon = ({ type, side }) => {
22
26
  const Icon = PIECE_ICON[type] ?? ShieldQuestion;
23
27
  const style = {
24
28
  width: "clamp(16px, 7.4cqw, 44px)",
25
29
  height: "clamp(16px, 7.4cqw, 44px)",
26
- filter: "drop-shadow(0 1px 1px rgba(0,0,0,0.35))"
30
+ filter: EMBOSS_FILTER[side]
27
31
  };
28
- return /* @__PURE__ */ jsx(Icon, { style, strokeWidth: 1.75, "aria-hidden": "true" });
32
+ return /* @__PURE__ */ jsx(Icon, { style, strokeWidth: 2, "aria-hidden": "true" });
29
33
  };
30
34
  var WHITE_PIECE_STYLE = {
31
35
  background: "radial-gradient(circle at 30% 25%, #ffffff 0%, #f4f4f5 55%, #d4d4d8 100%)",
@@ -66,8 +70,16 @@ function GamePiece({ piece, isSelected, hasBall, onClick }) {
66
70
  piece.hasMovedThisTurn && "opacity-70"
67
71
  ),
68
72
  children: [
69
- /* @__PURE__ */ jsx(PieceIcon, { type: piece.type }),
73
+ /* @__PURE__ */ jsx(PieceIcon, { type: piece.type, side: piece.side }),
70
74
  hasBall && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 rounded-full ring-2 ring-orange-400 pointer-events-none" }),
75
+ piece.hasMovedThisTurn && /* @__PURE__ */ jsx(
76
+ "span",
77
+ {
78
+ "aria-hidden": "true",
79
+ className: "absolute top-0.5 right-0.5 w-[14px] h-[14px] rounded-full bg-fg-muted/80 flex items-center justify-center text-[9px] text-bg-primary font-bold leading-none pointer-events-none",
80
+ children: "\u2713"
81
+ }
82
+ ),
71
83
  isSelected && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 rounded-full animate-pulse ring-2 ring-yellow-400 ring-offset-2 ring-offset-transparent" })
72
84
  ]
73
85
  }
@@ -121,20 +133,29 @@ var Football = ({ className }) => {
121
133
  }
122
134
  );
123
135
  };
136
+ var fallback = (key) => key;
137
+ var GameI18nContext = createContext(fallback);
138
+ function GameI18nProvider({ t, children }) {
139
+ return /* @__PURE__ */ jsx(GameI18nContext.Provider, { value: t, children: /* @__PURE__ */ jsx(MotionConfig, { reducedMotion: "user", children }) });
140
+ }
141
+ function useGameT() {
142
+ return useContext(GameI18nContext);
143
+ }
124
144
  var COLS = 9;
125
145
  var ROWS = 12;
126
146
  var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
127
- function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
147
+ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
148
+ const t = useGameT();
128
149
  const {
129
150
  boardState,
130
151
  selectedPieceId,
131
152
  setSelectedPieceId,
132
- interactionMode,
133
153
  setInteractionMode,
134
154
  movePiece,
135
155
  passBall
136
156
  } = useGameStore();
137
157
  const [cursor, setCursor] = useState(null);
158
+ const [disambiguateAt, setDisambiguateAt] = useState(null);
138
159
  const prevBallRef = useRef(boardState?.ball);
139
160
  const prevBall = prevBallRef.current;
140
161
  useEffect(() => {
@@ -143,19 +164,19 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
143
164
  const validMoves = useMemo(() => {
144
165
  if (!boardState) return [];
145
166
  const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
146
- if (sp && interactionMode === "move" && !sp.hasMovedThisTurn) {
167
+ if (sp && !sp.hasMovedThisTurn) {
147
168
  return getValidMoves(sp, boardState);
148
169
  }
149
170
  return [];
150
- }, [boardState, selectedPieceId, interactionMode]);
171
+ }, [boardState, selectedPieceId]);
151
172
  const validPasses = useMemo(() => {
152
173
  if (!boardState) return [];
153
174
  const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
154
- if (sp && interactionMode === "pass") {
175
+ if (sp && boardState.ball.holderId === sp.id) {
155
176
  return getValidPasses(sp, boardState);
156
177
  }
157
178
  return [];
158
- }, [boardState, selectedPieceId, interactionMode]);
179
+ }, [boardState, selectedPieceId]);
159
180
  if (!boardState) return null;
160
181
  const lastMove = boardState.lastMove;
161
182
  const ball = boardState.ball;
@@ -175,33 +196,40 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
175
196
  );
176
197
  pickupProportion = totalSteps > 0 ? Math.min(0.85, Math.max(0.15, stepsToBall / totalSteps)) : 0.5;
177
198
  }
199
+ const ballCarrier = ball.holderId ? boardState.pieces.find((p) => p.id === ball.holderId) ?? null : null;
200
+ const isOffsideRisk = !!ballCarrier && ballCarrier.type !== "king" && userSide !== null && ballCarrier.side === userSide && boardState.turn === userSide && isInEnemyArea(ballCarrier.pos, ballCarrier.side);
178
201
  const handleSquareClick = (x, y) => {
179
- if (interactionMode === "move" && selectedPieceId) {
180
- if (validMoves.some((m) => m.x === x && m.y === y)) {
181
- movePiece(selectedPieceId, { x, y });
182
- }
183
- } else if (interactionMode === "pass" && selectedPieceId) {
184
- if (validPasses.some((p) => p.x === x && p.y === y)) {
185
- passBall({ x, y });
186
- }
202
+ const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
203
+ const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
204
+ if (isValidMove && isValidPass) {
205
+ setDisambiguateAt({ x, y });
206
+ return;
207
+ }
208
+ if (isValidMove && selectedPieceId) {
209
+ movePiece(selectedPieceId, { x, y });
210
+ setDisambiguateAt(null);
211
+ } else if (isValidPass) {
212
+ passBall({ x, y });
213
+ setDisambiguateAt(null);
214
+ } else {
215
+ setDisambiguateAt(null);
187
216
  }
188
217
  };
189
218
  const handlePieceClick = (pieceId, x, y, e) => {
190
219
  e.stopPropagation();
191
220
  const pieceAt = boardState.pieces.find((p) => p.id === pieceId);
192
221
  if (!pieceAt) return;
193
- const isValidDest = validMoves.some((m) => m.x === x && m.y === y) || validPasses.some((p) => p.x === x && p.y === y);
194
- if (isValidDest) {
222
+ const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
223
+ const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
224
+ if (isValidMove || isValidPass) {
195
225
  handleSquareClick(x, y);
196
226
  } else if (boardState.turn === pieceAt.side && userSide === pieceAt.side) {
197
227
  if (selectedPieceId === pieceAt.id) {
198
- if (boardState.ball.holderId === pieceAt.id) {
199
- setInteractionMode(interactionMode === "move" ? "pass" : "move");
200
- }
228
+ setSelectedPieceId(null);
201
229
  } else {
202
230
  setSelectedPieceId(pieceAt.id);
203
- setInteractionMode("move");
204
231
  }
232
+ setDisambiguateAt(null);
205
233
  }
206
234
  };
207
235
  const defaultCursor = () => {
@@ -230,7 +258,6 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
230
258
  case "ArrowUp":
231
259
  step(0, 1);
232
260
  break;
233
- // visually up = higher rank (y grows toward the top)
234
261
  case "ArrowDown":
235
262
  step(0, -1);
236
263
  break;
@@ -250,15 +277,25 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
250
277
  }
251
278
  case "m":
252
279
  case "M":
253
- setInteractionMode("move");
280
+ if (disambiguateAt && selectedPieceId) {
281
+ movePiece(selectedPieceId, disambiguateAt);
282
+ setDisambiguateAt(null);
283
+ }
254
284
  break;
255
285
  case "p":
256
286
  case "P":
257
- setInteractionMode("pass");
287
+ if (disambiguateAt) {
288
+ passBall(disambiguateAt);
289
+ setDisambiguateAt(null);
290
+ }
258
291
  break;
259
292
  case "Escape":
260
- setSelectedPieceId(null);
261
- setInteractionMode(null);
293
+ if (disambiguateAt) {
294
+ setDisambiguateAt(null);
295
+ } else {
296
+ setSelectedPieceId(null);
297
+ setInteractionMode(null);
298
+ }
262
299
  break;
263
300
  }
264
301
  };
@@ -268,9 +305,16 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
268
305
  for (let x = 0; x < COLS; x++) {
269
306
  const isGoalArea = x >= 2 && x <= 6 && (y >= 0 && y <= 1 || y >= 10 && y <= 11);
270
307
  const isEven = (x + y) % 2 === 0;
271
- const isValidDest = validMoves.some((m) => m.x === x && m.y === y) || validPasses.some((p) => p.x === x && p.y === y);
308
+ const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
309
+ const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
310
+ const isAmbiguous = isValidMove && isValidPass;
311
+ const isLastMoveOrigin = lastMove?.from && lastMove.from.x === x && lastMove.from.y === y;
312
+ const isLastMoveDest = lastMove?.to && lastMove.to.x === x && lastMove.to.y === y;
313
+ const isLastMove = isLastMoveOrigin || isLastMoveDest;
314
+ const isEnemyGoalArea = userSide !== null && (x >= 2 && x <= 6) && (userSide === "white" ? y >= 10 && y <= 11 : y >= 0 && y <= 1);
272
315
  const pieceAt = boardState.pieces.find((p) => p.pos.x === x && p.pos.y === y);
273
316
  const isCursor = keyboardNav && cursor?.x === x && cursor?.y === y;
317
+ const isDisambiguateTarget = disambiguateAt?.x === x && disambiguateAt?.y === y;
274
318
  squares.push(
275
319
  /* @__PURE__ */ jsxs(
276
320
  "div",
@@ -278,16 +322,60 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
278
322
  onClick: () => handleSquareClick(x, y),
279
323
  className: cn(
280
324
  "relative aspect-square w-full flex items-center justify-center cursor-pointer",
281
- isEven ? "bg-[#2d5a27]" : "bg-[#356a2d]",
325
+ isEven ? "bg-field-green-1" : "bg-field-green-2",
282
326
  isGoalArea && "bg-[#1a3a18] ring-1 ring-inset ring-emerald-500/30",
283
- isValidDest && (interactionMode === "move" ? "ring-2 ring-inset ring-yellow-400/50 bg-yellow-400/10" : "ring-2 ring-inset ring-sky-400/50 bg-sky-400/10")
327
+ !isValidMove && !isValidPass && isLastMove && "bg-last-move-highlight/20",
328
+ // §8: pulsing warning ring on enemy goal area
329
+ isOffsideRisk && isEnemyGoalArea && !isValidMove && !isValidPass && "ring-2 ring-inset ring-warning/60",
330
+ isAmbiguous && "ring-[3px] ring-inset ring-yellow-400/90 bg-yellow-400/20",
331
+ !isAmbiguous && isValidMove && "ring-[3px] ring-inset ring-yellow-400/80 bg-yellow-400/15",
332
+ !isAmbiguous && isValidPass && "ring-[3px] ring-inset ring-sky-400/80 bg-sky-400/15"
284
333
  ),
285
334
  children: [
286
- isValidDest && !pieceAt && /* @__PURE__ */ jsx("div", { className: cn(
287
- "w-3 h-3 rounded-full",
288
- interactionMode === "move" ? "bg-yellow-400/40" : "bg-sky-400/40"
289
- ) }),
290
- isCursor && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 ring-2 ring-inset ring-white pointer-events-none z-20", "aria-hidden": "true" })
335
+ !isAmbiguous && isValidMove && !pieceAt && /* @__PURE__ */ jsx("div", { className: "w-3 h-3 rounded-full bg-yellow-400/65" }),
336
+ !isAmbiguous && isValidPass && !pieceAt && /* @__PURE__ */ jsx("div", { className: "w-3 h-3 rounded-full bg-sky-400/65" }),
337
+ isAmbiguous && !pieceAt && /* @__PURE__ */ jsxs("div", { className: "flex gap-0.5", children: [
338
+ /* @__PURE__ */ jsx("div", { className: "w-2.5 h-2.5 rounded-full bg-yellow-400/80" }),
339
+ /* @__PURE__ */ jsx("div", { className: "w-2.5 h-2.5 rounded-full bg-sky-400/80" })
340
+ ] }),
341
+ isCursor && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 ring-2 ring-inset ring-white pointer-events-none z-20", "aria-hidden": "true" }),
342
+ isDisambiguateTarget && selectedPieceId && /* @__PURE__ */ jsxs(
343
+ "div",
344
+ {
345
+ className: "absolute z-50 bottom-full mb-1 left-1/2 -translate-x-1/2 flex gap-1 bg-bg-secondary border border-border-subtle rounded-md p-1 shadow-xl pointer-events-auto whitespace-nowrap",
346
+ onClick: (e) => e.stopPropagation(),
347
+ children: [
348
+ /* @__PURE__ */ jsxs(
349
+ "button",
350
+ {
351
+ className: "flex items-center gap-1 px-2 py-1 rounded text-[11px] font-semibold font-inter text-move-highlight bg-move-highlight/10 hover:bg-move-highlight/20 transition-colors",
352
+ onClick: () => {
353
+ movePiece(selectedPieceId, { x, y });
354
+ setDisambiguateAt(null);
355
+ },
356
+ children: [
357
+ /* @__PURE__ */ jsx(Move, { size: 11, strokeWidth: 2 }),
358
+ t("move")
359
+ ]
360
+ }
361
+ ),
362
+ /* @__PURE__ */ jsxs(
363
+ "button",
364
+ {
365
+ className: "flex items-center gap-1 px-2 py-1 rounded text-[11px] font-semibold font-inter text-pass-highlight bg-pass-highlight/10 hover:bg-pass-highlight/20 transition-colors",
366
+ onClick: () => {
367
+ passBall({ x, y });
368
+ setDisambiguateAt(null);
369
+ },
370
+ children: [
371
+ /* @__PURE__ */ jsx("span", { className: "w-3 h-3 inline-flex", children: /* @__PURE__ */ jsx(Football, {}) }),
372
+ t("pass")
373
+ ]
374
+ }
375
+ )
376
+ ]
377
+ }
378
+ )
291
379
  ]
292
380
  },
293
381
  `${x}-${y}`
@@ -301,7 +389,11 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
301
389
  "div",
302
390
  {
303
391
  className: cn(
304
- "w-full max-w-[500px] mx-auto overflow-hidden rounded-xl shadow-2xl border-4 border-[#1a3317] bg-[#1a3317]",
392
+ // §6: Mobile edge-to-edge — thin 2px margin, no border, no radius
393
+ "w-full mx-0.5 bg-[#1a3317] overflow-hidden",
394
+ // §5+§6: Desktop — centered, rounded, bordered, height-based max-width
395
+ "md:mx-auto md:rounded-xl md:shadow-2xl md:border-4 md:border-[#1a3317]",
396
+ "md:max-w-[min(700px,calc((100dvh_-_150px)*0.75))]",
305
397
  keyboardNav && "focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-green focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary"
306
398
  ),
307
399
  ...keyboardNav ? {
@@ -336,12 +428,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
336
428
  /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 pointer-events-none", children: [
337
429
  /* @__PURE__ */ jsx(AnimatePresence, { mode: "sync", children: boardState.pieces.map((piece) => {
338
430
  const isPickupCarrier = pickupCarrierId === piece.id;
339
- const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : {
340
- type: "spring",
341
- stiffness: 200,
342
- damping: 25,
343
- mass: 1
344
- };
431
+ const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : { type: "spring", stiffness: 200, damping: 25, mass: 1 };
345
432
  return /* @__PURE__ */ jsx(
346
433
  motion.div,
347
434
  {
@@ -385,23 +472,10 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
385
472
  top: [`${prevY}%`, `${prevY}%`, `${prevY}%`, `${targetY}%`],
386
473
  scale: [1, 1, 1.35, 1]
387
474
  };
388
- transition = {
389
- duration: PICKUP_DURATION,
390
- times: [0, pulseStart, pickupProportion, 1],
391
- ease: "linear"
392
- };
475
+ transition = { duration: PICKUP_DURATION, times: [0, pulseStart, pickupProportion, 1], ease: "linear" };
393
476
  } else {
394
- animate = {
395
- left: `${targetX}%`,
396
- top: `${targetY}%`,
397
- scale: 1
398
- };
399
- transition = {
400
- type: "spring",
401
- stiffness: 300,
402
- damping: 30,
403
- mass: 0.5
404
- };
477
+ animate = { left: `${targetX}%`, top: `${targetY}%`, scale: 1 };
478
+ transition = { type: "spring", stiffness: 300, damping: 30, mass: 0.5 };
405
479
  }
406
480
  return /* @__PURE__ */ jsx(
407
481
  motion.div,
@@ -421,13 +495,17 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
421
495
  "ball"
422
496
  );
423
497
  })()
498
+ ] }),
499
+ isOffsideRisk && /* @__PURE__ */ jsxs("div", { className: "absolute bottom-2 left-1/2 -translate-x-1/2 z-30 flex items-center gap-1.5 px-3 py-1.5 bg-warning/20 border border-warning/50 rounded-full font-inter text-xs font-semibold text-warning pointer-events-none whitespace-nowrap backdrop-blur-sm", children: [
500
+ "\u26A0 ",
501
+ t("offsideWarning")
424
502
  ] })
425
503
  ] })
426
504
  }
427
505
  );
428
506
  if (!showCoordinates) return board;
429
507
  const ranksTopToBottom = [...RANK_LABELS].reverse();
430
- return /* @__PURE__ */ jsx("div", { className: "w-full max-w-[540px] mx-auto", "aria-hidden": "false", children: /* @__PURE__ */ jsxs(
508
+ return /* @__PURE__ */ jsx("div", { className: "w-full max-w-[540px] md:max-w-none mx-auto", "aria-hidden": "false", children: /* @__PURE__ */ jsxs(
431
509
  "div",
432
510
  {
433
511
  className: "grid gap-1 select-none",
@@ -450,26 +528,10 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
450
528
  ) });
451
529
  }
452
530
  function FileRow({ files }) {
453
- return /* @__PURE__ */ jsx("div", { className: "grid grid-cols-9 gap-[1px] px-[2px]", children: files.map((f) => /* @__PURE__ */ jsx(
454
- "span",
455
- {
456
- "aria-hidden": "true",
457
- className: "flex items-center justify-center font-mono text-[9px] text-fg-muted/80 uppercase tracking-[0.5px]",
458
- children: f
459
- },
460
- f
461
- )) });
531
+ return /* @__PURE__ */ jsx("div", { className: "grid grid-cols-9 gap-[1px] px-[2px]", children: files.map((f) => /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "flex items-center justify-center font-mono text-[9px] text-fg-muted/80 uppercase tracking-[0.5px]", children: f }, f)) });
462
532
  }
463
533
  function RankColumn({ ranks }) {
464
- return /* @__PURE__ */ jsx("div", { className: "grid grid-rows-12 gap-[1px] py-[2px]", children: ranks.map((r) => /* @__PURE__ */ jsx(
465
- "span",
466
- {
467
- "aria-hidden": "true",
468
- className: "flex items-center justify-center font-mono text-[9px] text-fg-muted/80 tabular-nums",
469
- children: r
470
- },
471
- r
472
- )) });
534
+ return /* @__PURE__ */ jsx("div", { className: "grid grid-rows-12 gap-[1px] py-[2px]", children: ranks.map((r) => /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "flex items-center justify-center font-mono text-[9px] text-fg-muted/80 tabular-nums", children: r }, r)) });
473
535
  }
474
536
  function Avatar({ className, children }) {
475
537
  return /* @__PURE__ */ jsx("div", { className: cn("relative w-9 h-9 rounded-full overflow-hidden shrink-0", className), children });
@@ -481,14 +543,6 @@ function AvatarImage({ src, alt, className }) {
481
543
  function AvatarFallback({ className, children }) {
482
544
  return /* @__PURE__ */ jsx("div", { className: cn("absolute inset-0 flex items-center justify-center text-xs font-semibold", className), children });
483
545
  }
484
- var fallback = (key) => key;
485
- var GameI18nContext = createContext(fallback);
486
- function GameI18nProvider({ t, children }) {
487
- return /* @__PURE__ */ jsx(GameI18nContext.Provider, { value: t, children });
488
- }
489
- function useGameT() {
490
- return useContext(GameI18nContext);
491
- }
492
546
  function ActionPoints({ total, remaining, size = 20, className, kingMustRelease }) {
493
547
  const t = useGameT();
494
548
  return /* @__PURE__ */ jsx("div", { className: cn("flex items-center gap-1.5", className), "aria-label": t("actionPointsAriaLabel", { remaining, total }), children: Array.from({ length: total }).map((_, i) => {
@@ -546,6 +600,7 @@ function Scoreboard({
546
600
  const isMyTurn = turn === userSide;
547
601
  const myKingMustRelease = isMyTurn && kingMustRelease === turn;
548
602
  const rivalKingMustRelease = !isMyTurn && kingMustRelease === turn;
603
+ const maxAP = boardState.maxActionPoints ?? 5;
549
604
  return /* @__PURE__ */ jsxs("div", { className: "w-full bg-bg-secondary flex items-center justify-between px-5 py-2.5", children: [
550
605
  /* @__PURE__ */ jsx(
551
606
  PlayerInfo,
@@ -555,6 +610,7 @@ function Scoreboard({
555
610
  avatarUrl: myAvatar,
556
611
  isActive: isMyTurn,
557
612
  actionPoints: isMyTurn ? actionPoints : 0,
613
+ maxActionPoints: maxAP,
558
614
  kingMustRelease: myKingMustRelease,
559
615
  align: "left"
560
616
  }
@@ -572,13 +628,14 @@ function Scoreboard({
572
628
  avatarUrl: rivalAvatar,
573
629
  isActive: !isMyTurn,
574
630
  actionPoints: !isMyTurn ? actionPoints : 0,
631
+ maxActionPoints: maxAP,
575
632
  kingMustRelease: rivalKingMustRelease,
576
633
  align: "right"
577
634
  }
578
635
  )
579
636
  ] });
580
637
  }
581
- function PlayerInfo({ name, team, avatarUrl, isActive, actionPoints, kingMustRelease, align }) {
638
+ function PlayerInfo({ name, team, avatarUrl, isActive, actionPoints, maxActionPoints, kingMustRelease, align }) {
582
639
  return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-2.5", align === "right" && "flex-row-reverse"), children: [
583
640
  /* @__PURE__ */ jsxs(Avatar, { className: cn(
584
641
  "w-9 h-9 border-2 transition-colors duration-300",
@@ -599,7 +656,7 @@ function PlayerInfo({ name, team, avatarUrl, isActive, actionPoints, kingMustRel
599
656
  /* @__PURE__ */ jsx(
600
657
  ActionPoints,
601
658
  {
602
- total: 5,
659
+ total: maxActionPoints,
603
660
  remaining: actionPoints,
604
661
  size: 12,
605
662
  kingMustRelease,
@@ -720,78 +777,36 @@ function ConfirmDialog({ open, title, description, confirmLabel, cancelLabel, on
720
777
  function GameControls({ isMyTurn }) {
721
778
  const {
722
779
  boardState,
723
- selectedPieceId,
724
- interactionMode,
725
- setInteractionMode,
726
780
  endTurn
727
781
  } = useGameStore();
728
782
  const t = useGameT();
729
783
  const [showEndTurnConfirm, setShowEndTurnConfirm] = useState(false);
730
784
  if (!boardState) return null;
731
785
  const actionPoints = boardState.actionPoints;
732
- const selectedPiece = boardState.pieces.find((p) => p.id === selectedPieceId);
733
- const hasBall = selectedPiece && boardState.ball.holderId === selectedPiece.id;
734
- const canPass = hasBall && actionPoints > 0;
735
- const canMove = actionPoints > 0 && !selectedPiece?.hasMovedThisTurn;
786
+ const handleEndTurn = () => {
787
+ if (actionPoints >= 2) {
788
+ setShowEndTurnConfirm(true);
789
+ } else {
790
+ endTurn();
791
+ }
792
+ };
736
793
  return /* @__PURE__ */ jsxs("div", { className: "w-full bg-bg-secondary flex flex-col gap-3 px-5 pt-3 pb-5", children: [
737
- /* @__PURE__ */ jsx(AnimatePresence, { children: isMyTurn && /* @__PURE__ */ jsxs(
738
- motion.div,
739
- {
740
- initial: { opacity: 0, y: 8 },
741
- animate: { opacity: 1, y: 0 },
742
- exit: { opacity: 0, y: 8 },
743
- className: "flex gap-2",
744
- children: [
745
- /* @__PURE__ */ jsxs(
746
- "button",
747
- {
748
- onClick: () => setInteractionMode("move"),
749
- disabled: !canMove,
750
- className: cn(
751
- "flex-1 flex items-center justify-center gap-2 h-11 rounded-md border font-inter text-sm font-semibold",
752
- "transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-move-highlight",
753
- "disabled:opacity-40 disabled:cursor-not-allowed",
754
- interactionMode === "move" ? "bg-move-highlight/10 border-move-highlight text-move-highlight" : "bg-bg-surface border-border-subtle text-move-highlight hover:bg-move-highlight/5"
755
- ),
756
- "aria-pressed": interactionMode === "move",
757
- children: [
758
- /* @__PURE__ */ jsx(Move, { size: 16, strokeWidth: 2 }),
759
- t("move")
760
- ]
761
- }
762
- ),
763
- /* @__PURE__ */ jsxs(
764
- "button",
765
- {
766
- onClick: () => setInteractionMode("pass"),
767
- disabled: !canPass,
768
- className: cn(
769
- "flex-1 flex items-center justify-center gap-2 h-11 rounded-md border font-inter text-sm font-semibold",
770
- "transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-pass-highlight",
771
- "disabled:opacity-40 disabled:cursor-not-allowed",
772
- interactionMode === "pass" ? "bg-pass-highlight/10 border-pass-highlight text-pass-highlight" : "bg-bg-surface border-border-subtle text-pass-highlight hover:bg-pass-highlight/5"
773
- ),
774
- "aria-pressed": interactionMode === "pass",
775
- children: [
776
- /* @__PURE__ */ jsx("span", { className: "w-4 h-4 inline-flex", children: /* @__PURE__ */ jsx(Football, {}) }),
777
- t("pass")
778
- ]
779
- }
780
- )
781
- ]
782
- },
783
- "action-btns"
784
- ) }),
785
794
  isMyTurn && /* @__PURE__ */ jsxs(
786
795
  Button,
787
796
  {
788
797
  variant: "primary",
789
798
  size: "default",
790
799
  className: "w-full gap-2 tracking-[1.5px]",
791
- onClick: () => setShowEndTurnConfirm(true),
800
+ onClick: handleEndTurn,
792
801
  children: [
793
802
  /* @__PURE__ */ jsx(Flag, { size: 16, strokeWidth: 2 }),
794
- t("endTurn")
803
+ t("endTurn"),
804
+ actionPoints >= 2 && /* @__PURE__ */ jsxs("span", { className: cn("ml-1 font-inter text-xs font-normal opacity-70 tracking-normal"), children: [
805
+ "\xB7 ",
806
+ actionPoints,
807
+ " ",
808
+ t("actionPointsShort")
809
+ ] })
795
810
  ]
796
811
  }
797
812
  ),
@@ -1082,7 +1097,194 @@ function StaticGameBoard() {
1082
1097
  )
1083
1098
  ] });
1084
1099
  }
1100
+ function TurnBanner({ isMyTurn, waitingLabel, className }) {
1101
+ const t = useGameT();
1102
+ const { boardState } = useGameStore();
1103
+ if (!boardState) return null;
1104
+ const { actionPoints, maxActionPoints } = boardState;
1105
+ return /* @__PURE__ */ jsxs(
1106
+ "div",
1107
+ {
1108
+ className: cn(
1109
+ "w-full h-9 flex items-center justify-between px-4 shrink-0 transition-colors duration-300",
1110
+ isMyTurn ? "bg-accent-green/15 border-b border-accent-green/30" : "bg-bg-surface border-b border-border-subtle",
1111
+ className
1112
+ ),
1113
+ "aria-live": "polite",
1114
+ "aria-atomic": "true",
1115
+ children: [
1116
+ /* @__PURE__ */ jsx(
1117
+ "span",
1118
+ {
1119
+ className: cn(
1120
+ "font-anton text-sm tracking-[1.5px] uppercase",
1121
+ isMyTurn ? "text-accent-green" : "text-fg-muted"
1122
+ ),
1123
+ children: isMyTurn ? t("yourTurn") : waitingLabel ?? t("waitingRival")
1124
+ }
1125
+ ),
1126
+ isMyTurn && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", "aria-label": t("actionPointsAriaLabel", { remaining: actionPoints, total: maxActionPoints }), children: [
1127
+ /* @__PURE__ */ jsx(Zap, { size: 12, className: "text-accent-green", "aria-hidden": "true" }),
1128
+ /* @__PURE__ */ jsxs("span", { className: "font-mono text-xs text-fg-secondary tabular-nums", children: [
1129
+ actionPoints,
1130
+ "/",
1131
+ maxActionPoints
1132
+ ] })
1133
+ ] })
1134
+ ]
1135
+ }
1136
+ );
1137
+ }
1138
+ var TOASTABLE = /* @__PURE__ */ new Set(["interception", "offside", "tackle"]);
1139
+ var TOAST_CONFIG = {
1140
+ interception: "bg-danger/20 border-danger/50 text-danger",
1141
+ offside: "bg-warning/20 border-warning/50 text-warning",
1142
+ tackle: "bg-warning/20 border-warning/50 text-warning"
1143
+ };
1144
+ function EventToast({ className }) {
1145
+ const t = useGameT();
1146
+ const { boardState } = useGameStore();
1147
+ const [toast, setToast] = useState(null);
1148
+ useEffect(() => {
1149
+ const lastMove = boardState?.lastMove;
1150
+ if (!lastMove) return;
1151
+ if (!TOASTABLE.has(lastMove.type)) return;
1152
+ setToast({ type: lastMove.type, key: String(lastMove.at) });
1153
+ }, [boardState?.lastMove?.at]);
1154
+ useEffect(() => {
1155
+ if (!toast) return;
1156
+ const timer = setTimeout(() => setToast(null), 2500);
1157
+ return () => clearTimeout(timer);
1158
+ }, [toast?.key]);
1159
+ return /* @__PURE__ */ jsx(
1160
+ "div",
1161
+ {
1162
+ role: "status",
1163
+ "aria-live": "polite",
1164
+ "aria-atomic": "true",
1165
+ className: cn("pointer-events-none flex justify-center", className),
1166
+ children: /* @__PURE__ */ jsx(AnimatePresence, { mode: "wait", children: toast && /* @__PURE__ */ jsx(
1167
+ motion.div,
1168
+ {
1169
+ initial: { opacity: 0, y: -8 },
1170
+ animate: { opacity: 1, y: 0 },
1171
+ exit: { opacity: 0, y: -8 },
1172
+ transition: { duration: 0.15 },
1173
+ className: cn(
1174
+ "px-4 py-2 rounded-full border font-inter text-sm font-semibold shadow-lg backdrop-blur-sm text-center",
1175
+ TOAST_CONFIG[toast.type]
1176
+ ),
1177
+ children: t(`eventToast.${toast.type}`)
1178
+ },
1179
+ toast.key
1180
+ ) })
1181
+ }
1182
+ );
1183
+ }
1184
+ function MobileHistory({ className }) {
1185
+ const { boardState } = useGameStore();
1186
+ const [open, setOpen] = useState(false);
1187
+ const lastMove = boardState?.moveHistory?.at(-1);
1188
+ if (!lastMove) return null;
1189
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1190
+ /* @__PURE__ */ jsx(HistoryChip, { lastMove, onClick: () => setOpen(true), className }),
1191
+ /* @__PURE__ */ jsx(AnimatePresence, { children: open && /* @__PURE__ */ jsxs(Fragment, { children: [
1192
+ /* @__PURE__ */ jsx(
1193
+ motion.div,
1194
+ {
1195
+ initial: { opacity: 0 },
1196
+ animate: { opacity: 1 },
1197
+ exit: { opacity: 0 },
1198
+ transition: { duration: 0.2 },
1199
+ className: "fixed inset-0 z-40 bg-black/60 backdrop-blur-sm",
1200
+ onClick: () => setOpen(false),
1201
+ "aria-hidden": "true"
1202
+ },
1203
+ "backdrop"
1204
+ ),
1205
+ /* @__PURE__ */ jsxs(
1206
+ motion.div,
1207
+ {
1208
+ initial: { y: "100%" },
1209
+ animate: { y: 0 },
1210
+ exit: { y: "100%" },
1211
+ transition: { type: "spring", stiffness: 300, damping: 32 },
1212
+ className: "fixed bottom-0 left-0 right-0 z-50 bg-bg-secondary rounded-t-2xl border-t border-border-subtle shadow-xl",
1213
+ role: "dialog",
1214
+ "aria-modal": "true",
1215
+ children: [
1216
+ /* @__PURE__ */ jsx(SheetHeader, { onClose: () => setOpen(false) }),
1217
+ /* @__PURE__ */ jsx("div", { className: "px-4 pb-6 max-h-[50dvh] overflow-y-auto", children: /* @__PURE__ */ jsx(MoveHistory, { scrollable: false }) })
1218
+ ]
1219
+ },
1220
+ "sheet"
1221
+ )
1222
+ ] }) })
1223
+ ] });
1224
+ }
1225
+ function SheetHeader({ onClose }) {
1226
+ const t = useGameT();
1227
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-5 pt-4 pb-3 border-b border-border-subtle", children: [
1228
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] tracking-[0.5px] uppercase text-fg-muted", children: t("history.title") }),
1229
+ /* @__PURE__ */ jsx(
1230
+ "button",
1231
+ {
1232
+ onClick: onClose,
1233
+ 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",
1234
+ "aria-label": "Close",
1235
+ children: /* @__PURE__ */ jsx(X, { size: 14 })
1236
+ }
1237
+ )
1238
+ ] });
1239
+ }
1240
+ function HistoryChip({ lastMove, onClick, className }) {
1241
+ const t = useGameT();
1242
+ const toSq = squareName(lastMove.to);
1243
+ let label;
1244
+ switch (lastMove.type) {
1245
+ case "pass":
1246
+ label = `\u2295 ${toSq}`;
1247
+ break;
1248
+ case "tackle":
1249
+ label = `\u2715 ${toSq}`;
1250
+ break;
1251
+ case "interception":
1252
+ label = `\u26D4 ${toSq}`;
1253
+ break;
1254
+ case "goal":
1255
+ label = `\u26BD GOL`;
1256
+ break;
1257
+ case "offside":
1258
+ label = `\u26A0 OFS`;
1259
+ break;
1260
+ default: {
1261
+ const pieceShort = t(`pieces.${SHORT_KEY[lastMove.pieceType]}`);
1262
+ const fromSq = lastMove.from ? squareName(lastMove.from) : "";
1263
+ label = fromSq ? `${pieceShort}${fromSq}\u2192${toSq}` : `${pieceShort}\u2192${toSq}`;
1264
+ }
1265
+ }
1266
+ return /* @__PURE__ */ jsxs(
1267
+ "button",
1268
+ {
1269
+ onClick,
1270
+ className: cn(
1271
+ "flex items-center gap-1.5 px-3 py-1.5 rounded-full",
1272
+ "bg-bg-surface border border-border-subtle",
1273
+ "font-mono text-[11px] text-fg-secondary",
1274
+ "hover:text-fg-primary hover:bg-bg-surface-elevated active:scale-95",
1275
+ "transition-all duration-150",
1276
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-green",
1277
+ className
1278
+ ),
1279
+ "aria-label": t("history.viewAll"),
1280
+ children: [
1281
+ /* @__PURE__ */ jsx("span", { children: label }),
1282
+ /* @__PURE__ */ jsx("span", { className: "text-fg-muted text-[10px]", "aria-hidden": "true", children: "\u25BE" })
1283
+ ]
1284
+ }
1285
+ );
1286
+ }
1085
1287
 
1086
- export { ActionPoints, Avatar, AvatarFallback, AvatarImage, Button, ConfirmDialog, Football, GameBoard, GameControls, GameI18nProvider, GamePiece, MiniScoreboard, MoveHistory, Scoreboard, SelectedPieceDetail, StaticGameBoard, cn, useGameT };
1288
+ export { ActionPoints, Avatar, AvatarFallback, AvatarImage, Button, ConfirmDialog, EventToast, Football, GameBoard, GameControls, GameI18nProvider, GamePiece, MiniScoreboard, MobileHistory, MoveHistory, Scoreboard, SelectedPieceDetail, StaticGameBoard, TurnBanner, cn, useGameT };
1087
1289
  //# sourceMappingURL=index.js.map
1088
1290
  //# sourceMappingURL=index.js.map