@scriptonita/chess-football-ui 0.1.1 → 0.2.1

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
@@ -39,14 +39,18 @@ var PIECE_ICON = {
39
39
  bishop: lucideReact.ChessBishop,
40
40
  knight: lucideReact.ChessKnight
41
41
  };
42
- var PieceIcon = ({ type }) => {
42
+ var EMBOSS_FILTER = {
43
+ 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
+ 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
+ var PieceIcon = ({ type, side }) => {
43
47
  const Icon = PIECE_ICON[type] ?? lucideReact.ShieldQuestion;
44
48
  const style = {
45
49
  width: "clamp(16px, 7.4cqw, 44px)",
46
50
  height: "clamp(16px, 7.4cqw, 44px)",
47
- filter: "drop-shadow(0 1px 1px rgba(0,0,0,0.35))"
51
+ filter: EMBOSS_FILTER[side]
48
52
  };
49
- return /* @__PURE__ */ jsxRuntime.jsx(Icon, { style, strokeWidth: 1.75, "aria-hidden": "true" });
53
+ return /* @__PURE__ */ jsxRuntime.jsx(Icon, { style, strokeWidth: 2, "aria-hidden": "true" });
50
54
  };
51
55
  var WHITE_PIECE_STYLE = {
52
56
  background: "radial-gradient(circle at 30% 25%, #ffffff 0%, #f4f4f5 55%, #d4d4d8 100%)",
@@ -87,7 +91,7 @@ function GamePiece({ piece, isSelected, hasBall, onClick }) {
87
91
  piece.hasMovedThisTurn && "opacity-70"
88
92
  ),
89
93
  children: [
90
- /* @__PURE__ */ jsxRuntime.jsx(PieceIcon, { type: piece.type }),
94
+ /* @__PURE__ */ jsxRuntime.jsx(PieceIcon, { type: piece.type, side: piece.side }),
91
95
  hasBall && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 rounded-full ring-2 ring-orange-400 pointer-events-none" }),
92
96
  isSelected && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 rounded-full animate-pulse ring-2 ring-yellow-400 ring-offset-2 ring-offset-transparent" })
93
97
  ]
@@ -142,20 +146,29 @@ var Football = ({ className }) => {
142
146
  }
143
147
  );
144
148
  };
149
+ var fallback = (key) => key;
150
+ var GameI18nContext = React.createContext(fallback);
151
+ function GameI18nProvider({ t, children }) {
152
+ return /* @__PURE__ */ jsxRuntime.jsx(GameI18nContext.Provider, { value: t, children: /* @__PURE__ */ jsxRuntime.jsx(framerMotion.MotionConfig, { reducedMotion: "user", children }) });
153
+ }
154
+ function useGameT() {
155
+ return React.useContext(GameI18nContext);
156
+ }
145
157
  var COLS = 9;
146
158
  var ROWS = 12;
147
159
  var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
148
- function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
160
+ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
161
+ const t = useGameT();
149
162
  const {
150
163
  boardState,
151
164
  selectedPieceId,
152
165
  setSelectedPieceId,
153
- interactionMode,
154
166
  setInteractionMode,
155
167
  movePiece,
156
168
  passBall
157
169
  } = chunkJ2TBQPPC_cjs.useGameStore();
158
170
  const [cursor, setCursor] = React.useState(null);
171
+ const [disambiguateAt, setDisambiguateAt] = React.useState(null);
159
172
  const prevBallRef = React.useRef(boardState?.ball);
160
173
  const prevBall = prevBallRef.current;
161
174
  React.useEffect(() => {
@@ -164,19 +177,19 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
164
177
  const validMoves = React.useMemo(() => {
165
178
  if (!boardState) return [];
166
179
  const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
167
- if (sp && interactionMode === "move" && !sp.hasMovedThisTurn) {
180
+ if (sp && !sp.hasMovedThisTurn) {
168
181
  return chessFootballEngine.getValidMoves(sp, boardState);
169
182
  }
170
183
  return [];
171
- }, [boardState, selectedPieceId, interactionMode]);
184
+ }, [boardState, selectedPieceId]);
172
185
  const validPasses = React.useMemo(() => {
173
186
  if (!boardState) return [];
174
187
  const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
175
- if (sp && interactionMode === "pass") {
188
+ if (sp && boardState.ball.holderId === sp.id) {
176
189
  return chessFootballEngine.getValidPasses(sp, boardState);
177
190
  }
178
191
  return [];
179
- }, [boardState, selectedPieceId, interactionMode]);
192
+ }, [boardState, selectedPieceId]);
180
193
  if (!boardState) return null;
181
194
  const lastMove = boardState.lastMove;
182
195
  const ball = boardState.ball;
@@ -196,33 +209,41 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
196
209
  );
197
210
  pickupProportion = totalSteps > 0 ? Math.min(0.85, Math.max(0.15, stepsToBall / totalSteps)) : 0.5;
198
211
  }
212
+ const ballCarrier = ball.holderId ? boardState.pieces.find((p) => p.id === ball.holderId) ?? null : null;
213
+ const isOffsideRisk = !!ballCarrier && ballCarrier.type !== "king" && userSide !== null && ballCarrier.side === userSide && boardState.turn === userSide && chessFootballEngine.isInEnemyArea(ballCarrier.pos, ballCarrier.side);
199
214
  const handleSquareClick = (x, y) => {
200
- if (interactionMode === "move" && selectedPieceId) {
201
- if (validMoves.some((m) => m.x === x && m.y === y)) {
202
- movePiece(selectedPieceId, { x, y });
203
- }
204
- } else if (interactionMode === "pass" && selectedPieceId) {
205
- if (validPasses.some((p) => p.x === x && p.y === y)) {
206
- passBall({ x, y });
207
- }
215
+ setCursor(null);
216
+ const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
217
+ const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
218
+ if (isValidMove && isValidPass) {
219
+ setDisambiguateAt({ x, y });
220
+ return;
221
+ }
222
+ if (isValidMove && selectedPieceId) {
223
+ movePiece(selectedPieceId, { x, y });
224
+ setDisambiguateAt(null);
225
+ } else if (isValidPass) {
226
+ passBall({ x, y });
227
+ setDisambiguateAt(null);
228
+ } else {
229
+ setDisambiguateAt(null);
208
230
  }
209
231
  };
210
232
  const handlePieceClick = (pieceId, x, y, e) => {
211
233
  e.stopPropagation();
212
234
  const pieceAt = boardState.pieces.find((p) => p.id === pieceId);
213
235
  if (!pieceAt) return;
214
- const isValidDest = validMoves.some((m) => m.x === x && m.y === y) || validPasses.some((p) => p.x === x && p.y === y);
215
- if (isValidDest) {
236
+ const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
237
+ const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
238
+ if (isValidMove || isValidPass) {
216
239
  handleSquareClick(x, y);
217
240
  } else if (boardState.turn === pieceAt.side && userSide === pieceAt.side) {
218
241
  if (selectedPieceId === pieceAt.id) {
219
- if (boardState.ball.holderId === pieceAt.id) {
220
- setInteractionMode(interactionMode === "move" ? "pass" : "move");
221
- }
242
+ setSelectedPieceId(null);
222
243
  } else {
223
244
  setSelectedPieceId(pieceAt.id);
224
- setInteractionMode("move");
225
245
  }
246
+ setDisambiguateAt(null);
226
247
  }
227
248
  };
228
249
  const defaultCursor = () => {
@@ -251,7 +272,6 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
251
272
  case "ArrowUp":
252
273
  step(0, 1);
253
274
  break;
254
- // visually up = higher rank (y grows toward the top)
255
275
  case "ArrowDown":
256
276
  step(0, -1);
257
277
  break;
@@ -271,15 +291,25 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
271
291
  }
272
292
  case "m":
273
293
  case "M":
274
- setInteractionMode("move");
294
+ if (disambiguateAt && selectedPieceId) {
295
+ movePiece(selectedPieceId, disambiguateAt);
296
+ setDisambiguateAt(null);
297
+ }
275
298
  break;
276
299
  case "p":
277
300
  case "P":
278
- setInteractionMode("pass");
301
+ if (disambiguateAt) {
302
+ passBall(disambiguateAt);
303
+ setDisambiguateAt(null);
304
+ }
279
305
  break;
280
306
  case "Escape":
281
- setSelectedPieceId(null);
282
- setInteractionMode(null);
307
+ if (disambiguateAt) {
308
+ setDisambiguateAt(null);
309
+ } else {
310
+ setSelectedPieceId(null);
311
+ setInteractionMode(null);
312
+ }
283
313
  break;
284
314
  }
285
315
  };
@@ -289,9 +319,16 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
289
319
  for (let x = 0; x < COLS; x++) {
290
320
  const isGoalArea = x >= 2 && x <= 6 && (y >= 0 && y <= 1 || y >= 10 && y <= 11);
291
321
  const isEven = (x + y) % 2 === 0;
292
- const isValidDest = validMoves.some((m) => m.x === x && m.y === y) || validPasses.some((p) => p.x === x && p.y === y);
322
+ const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
323
+ const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
324
+ const isAmbiguous = isValidMove && isValidPass;
325
+ const isLastMoveOrigin = lastMove?.from && lastMove.from.x === x && lastMove.from.y === y;
326
+ const isLastMoveDest = lastMove?.to && lastMove.to.x === x && lastMove.to.y === y;
327
+ const isLastMove = isLastMoveOrigin || isLastMoveDest;
328
+ const isEnemyGoalArea = userSide !== null && (x >= 2 && x <= 6) && (userSide === "white" ? y >= 10 && y <= 11 : y >= 0 && y <= 1);
293
329
  const pieceAt = boardState.pieces.find((p) => p.pos.x === x && p.pos.y === y);
294
330
  const isCursor = keyboardNav && cursor?.x === x && cursor?.y === y;
331
+ const isDisambiguateTarget = disambiguateAt?.x === x && disambiguateAt?.y === y;
295
332
  squares.push(
296
333
  /* @__PURE__ */ jsxRuntime.jsxs(
297
334
  "div",
@@ -299,16 +336,60 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
299
336
  onClick: () => handleSquareClick(x, y),
300
337
  className: cn(
301
338
  "relative aspect-square w-full flex items-center justify-center cursor-pointer",
302
- isEven ? "bg-[#2d5a27]" : "bg-[#356a2d]",
339
+ isEven ? "bg-field-green-1" : "bg-field-green-2",
303
340
  isGoalArea && "bg-[#1a3a18] ring-1 ring-inset ring-emerald-500/30",
304
- 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")
341
+ !isValidMove && !isValidPass && isLastMove && "bg-last-move-highlight/20",
342
+ // §8: pulsing warning ring on enemy goal area
343
+ isOffsideRisk && isEnemyGoalArea && !isValidMove && !isValidPass && "ring-2 ring-inset ring-warning/60",
344
+ isAmbiguous && "ring-[3px] ring-inset ring-yellow-400/90 bg-yellow-400/20",
345
+ !isAmbiguous && isValidMove && "ring-[3px] ring-inset ring-yellow-400/80 bg-yellow-400/15",
346
+ !isAmbiguous && isValidPass && "ring-[3px] ring-inset ring-sky-400/80 bg-sky-400/15"
305
347
  ),
306
348
  children: [
307
- isValidDest && !pieceAt && /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(
308
- "w-3 h-3 rounded-full",
309
- interactionMode === "move" ? "bg-yellow-400/40" : "bg-sky-400/40"
310
- ) }),
311
- isCursor && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 ring-2 ring-inset ring-white pointer-events-none z-20", "aria-hidden": "true" })
349
+ !isAmbiguous && isValidMove && !pieceAt && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-full bg-yellow-400/65" }),
350
+ !isAmbiguous && isValidPass && !pieceAt && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-full bg-sky-400/65" }),
351
+ isAmbiguous && !pieceAt && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-0.5", children: [
352
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2.5 h-2.5 rounded-full bg-yellow-400/80" }),
353
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2.5 h-2.5 rounded-full bg-sky-400/80" })
354
+ ] }),
355
+ isCursor && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 ring-2 ring-inset ring-white pointer-events-none z-20", "aria-hidden": "true" }),
356
+ isDisambiguateTarget && selectedPieceId && /* @__PURE__ */ jsxRuntime.jsxs(
357
+ "div",
358
+ {
359
+ 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",
360
+ onClick: (e) => e.stopPropagation(),
361
+ children: [
362
+ /* @__PURE__ */ jsxRuntime.jsxs(
363
+ "button",
364
+ {
365
+ 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",
366
+ onClick: () => {
367
+ movePiece(selectedPieceId, { x, y });
368
+ setDisambiguateAt(null);
369
+ },
370
+ children: [
371
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Move, { size: 11, strokeWidth: 2 }),
372
+ t("move")
373
+ ]
374
+ }
375
+ ),
376
+ /* @__PURE__ */ jsxRuntime.jsxs(
377
+ "button",
378
+ {
379
+ 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",
380
+ onClick: () => {
381
+ passBall({ x, y });
382
+ setDisambiguateAt(null);
383
+ },
384
+ children: [
385
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-3 h-3 inline-flex", children: /* @__PURE__ */ jsxRuntime.jsx(Football, {}) }),
386
+ t("pass")
387
+ ]
388
+ }
389
+ )
390
+ ]
391
+ }
392
+ )
312
393
  ]
313
394
  },
314
395
  `${x}-${y}`
@@ -322,15 +403,18 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
322
403
  "div",
323
404
  {
324
405
  className: cn(
325
- "w-full max-w-[500px] mx-auto overflow-hidden rounded-xl shadow-2xl border-4 border-[#1a3317] bg-[#1a3317]",
406
+ // §6: Mobile edge-to-edge thin 2px margin, no border, no radius
407
+ "w-full mx-0.5 bg-[#1a3317] overflow-hidden",
408
+ // §5+§6: Desktop — centered, rounded, bordered, height-based max-width
409
+ "md:mx-auto md:rounded-xl md:shadow-2xl md:border-4 md:border-[#1a3317]",
410
+ "md:max-w-[min(700px,calc((100dvh_-_150px)*0.75))]",
326
411
  keyboardNav && "focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-green focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary"
327
412
  ),
328
413
  ...keyboardNav ? {
329
414
  tabIndex: 0,
330
415
  role: "application",
331
416
  "aria-label": "Game board \u2014 arrow keys to move the cursor, Enter to act",
332
- onKeyDown: handleKeyDown,
333
- onFocus: () => setCursor((c) => c ?? defaultCursor())
417
+ onKeyDown: handleKeyDown
334
418
  } : {},
335
419
  children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", style: { containerType: "inline-size" }, children: [
336
420
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-9 gap-[1px]", children: renderSquares() }),
@@ -357,12 +441,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
357
441
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 pointer-events-none", children: [
358
442
  /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { mode: "sync", children: boardState.pieces.map((piece) => {
359
443
  const isPickupCarrier = pickupCarrierId === piece.id;
360
- const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : {
361
- type: "spring",
362
- stiffness: 200,
363
- damping: 25,
364
- mass: 1
365
- };
444
+ const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : { type: "spring", stiffness: 200, damping: 25, mass: 1 };
366
445
  return /* @__PURE__ */ jsxRuntime.jsx(
367
446
  framerMotion.motion.div,
368
447
  {
@@ -406,23 +485,10 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
406
485
  top: [`${prevY}%`, `${prevY}%`, `${prevY}%`, `${targetY}%`],
407
486
  scale: [1, 1, 1.35, 1]
408
487
  };
409
- transition = {
410
- duration: PICKUP_DURATION,
411
- times: [0, pulseStart, pickupProportion, 1],
412
- ease: "linear"
413
- };
488
+ transition = { duration: PICKUP_DURATION, times: [0, pulseStart, pickupProportion, 1], ease: "linear" };
414
489
  } else {
415
- animate = {
416
- left: `${targetX}%`,
417
- top: `${targetY}%`,
418
- scale: 1
419
- };
420
- transition = {
421
- type: "spring",
422
- stiffness: 300,
423
- damping: 30,
424
- mass: 0.5
425
- };
490
+ animate = { left: `${targetX}%`, top: `${targetY}%`, scale: 1 };
491
+ transition = { type: "spring", stiffness: 300, damping: 30, mass: 0.5 };
426
492
  }
427
493
  return /* @__PURE__ */ jsxRuntime.jsx(
428
494
  framerMotion.motion.div,
@@ -442,13 +508,17 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
442
508
  "ball"
443
509
  );
444
510
  })()
511
+ ] }),
512
+ isOffsideRisk && /* @__PURE__ */ jsxRuntime.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: [
513
+ "\u26A0 ",
514
+ t("offsideWarning")
445
515
  ] })
446
516
  ] })
447
517
  }
448
518
  );
449
519
  if (!showCoordinates) return board;
450
520
  const ranksTopToBottom = [...chessFootballEngine.RANK_LABELS].reverse();
451
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full max-w-[540px] mx-auto", "aria-hidden": "false", children: /* @__PURE__ */ jsxRuntime.jsxs(
521
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full max-w-[540px] md:max-w-none mx-auto", "aria-hidden": "false", children: /* @__PURE__ */ jsxRuntime.jsxs(
452
522
  "div",
453
523
  {
454
524
  className: "grid gap-1 select-none",
@@ -471,26 +541,10 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
471
541
  ) });
472
542
  }
473
543
  function FileRow({ files }) {
474
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-9 gap-[1px] px-[2px]", children: files.map((f) => /* @__PURE__ */ jsxRuntime.jsx(
475
- "span",
476
- {
477
- "aria-hidden": "true",
478
- className: "flex items-center justify-center font-mono text-[9px] text-fg-muted/80 uppercase tracking-[0.5px]",
479
- children: f
480
- },
481
- f
482
- )) });
544
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-9 gap-[1px] px-[2px]", children: files.map((f) => /* @__PURE__ */ jsxRuntime.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)) });
483
545
  }
484
546
  function RankColumn({ ranks }) {
485
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-rows-12 gap-[1px] py-[2px]", children: ranks.map((r) => /* @__PURE__ */ jsxRuntime.jsx(
486
- "span",
487
- {
488
- "aria-hidden": "true",
489
- className: "flex items-center justify-center font-mono text-[9px] text-fg-muted/80 tabular-nums",
490
- children: r
491
- },
492
- r
493
- )) });
547
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-rows-12 gap-[1px] py-[2px]", children: ranks.map((r) => /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: "flex items-center justify-center font-mono text-[9px] text-fg-muted/80 tabular-nums", children: r }, r)) });
494
548
  }
495
549
  function Avatar({ className, children }) {
496
550
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("relative w-9 h-9 rounded-full overflow-hidden shrink-0", className), children });
@@ -502,14 +556,6 @@ function AvatarImage({ src, alt, className }) {
502
556
  function AvatarFallback({ className, children }) {
503
557
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("absolute inset-0 flex items-center justify-center text-xs font-semibold", className), children });
504
558
  }
505
- var fallback = (key) => key;
506
- var GameI18nContext = React.createContext(fallback);
507
- function GameI18nProvider({ t, children }) {
508
- return /* @__PURE__ */ jsxRuntime.jsx(GameI18nContext.Provider, { value: t, children });
509
- }
510
- function useGameT() {
511
- return React.useContext(GameI18nContext);
512
- }
513
559
  function ActionPoints({ total, remaining, size = 20, className, kingMustRelease }) {
514
560
  const t = useGameT();
515
561
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex items-center gap-1.5", className), "aria-label": t("actionPointsAriaLabel", { remaining, total }), children: Array.from({ length: total }).map((_, i) => {
@@ -567,6 +613,7 @@ function Scoreboard({
567
613
  const isMyTurn = turn === userSide;
568
614
  const myKingMustRelease = isMyTurn && kingMustRelease === turn;
569
615
  const rivalKingMustRelease = !isMyTurn && kingMustRelease === turn;
616
+ const maxAP = boardState.maxActionPoints ?? 5;
570
617
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full bg-bg-secondary flex items-center justify-between px-5 py-2.5", children: [
571
618
  /* @__PURE__ */ jsxRuntime.jsx(
572
619
  PlayerInfo,
@@ -576,6 +623,7 @@ function Scoreboard({
576
623
  avatarUrl: myAvatar,
577
624
  isActive: isMyTurn,
578
625
  actionPoints: isMyTurn ? actionPoints : 0,
626
+ maxActionPoints: maxAP,
579
627
  kingMustRelease: myKingMustRelease,
580
628
  align: "left"
581
629
  }
@@ -593,13 +641,14 @@ function Scoreboard({
593
641
  avatarUrl: rivalAvatar,
594
642
  isActive: !isMyTurn,
595
643
  actionPoints: !isMyTurn ? actionPoints : 0,
644
+ maxActionPoints: maxAP,
596
645
  kingMustRelease: rivalKingMustRelease,
597
646
  align: "right"
598
647
  }
599
648
  )
600
649
  ] });
601
650
  }
602
- function PlayerInfo({ name, team, avatarUrl, isActive, actionPoints, kingMustRelease, align }) {
651
+ function PlayerInfo({ name, team, avatarUrl, isActive, actionPoints, maxActionPoints, kingMustRelease, align }) {
603
652
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex items-center gap-2.5", align === "right" && "flex-row-reverse"), children: [
604
653
  /* @__PURE__ */ jsxRuntime.jsxs(Avatar, { className: cn(
605
654
  "w-9 h-9 border-2 transition-colors duration-300",
@@ -620,7 +669,7 @@ function PlayerInfo({ name, team, avatarUrl, isActive, actionPoints, kingMustRel
620
669
  /* @__PURE__ */ jsxRuntime.jsx(
621
670
  ActionPoints,
622
671
  {
623
- total: 5,
672
+ total: maxActionPoints,
624
673
  remaining: actionPoints,
625
674
  size: 12,
626
675
  kingMustRelease,
@@ -741,78 +790,36 @@ function ConfirmDialog({ open, title, description, confirmLabel, cancelLabel, on
741
790
  function GameControls({ isMyTurn }) {
742
791
  const {
743
792
  boardState,
744
- selectedPieceId,
745
- interactionMode,
746
- setInteractionMode,
747
793
  endTurn
748
794
  } = chunkJ2TBQPPC_cjs.useGameStore();
749
795
  const t = useGameT();
750
796
  const [showEndTurnConfirm, setShowEndTurnConfirm] = React.useState(false);
751
797
  if (!boardState) return null;
752
798
  const actionPoints = boardState.actionPoints;
753
- const selectedPiece = boardState.pieces.find((p) => p.id === selectedPieceId);
754
- const hasBall = selectedPiece && boardState.ball.holderId === selectedPiece.id;
755
- const canPass = hasBall && actionPoints > 0;
756
- const canMove = actionPoints > 0 && !selectedPiece?.hasMovedThisTurn;
799
+ const handleEndTurn = () => {
800
+ if (actionPoints >= 2) {
801
+ setShowEndTurnConfirm(true);
802
+ } else {
803
+ endTurn();
804
+ }
805
+ };
757
806
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full bg-bg-secondary flex flex-col gap-3 px-5 pt-3 pb-5", children: [
758
- /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: isMyTurn && /* @__PURE__ */ jsxRuntime.jsxs(
759
- framerMotion.motion.div,
760
- {
761
- initial: { opacity: 0, y: 8 },
762
- animate: { opacity: 1, y: 0 },
763
- exit: { opacity: 0, y: 8 },
764
- className: "flex gap-2",
765
- children: [
766
- /* @__PURE__ */ jsxRuntime.jsxs(
767
- "button",
768
- {
769
- onClick: () => setInteractionMode("move"),
770
- disabled: !canMove,
771
- className: cn(
772
- "flex-1 flex items-center justify-center gap-2 h-11 rounded-md border font-inter text-sm font-semibold",
773
- "transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-move-highlight",
774
- "disabled:opacity-40 disabled:cursor-not-allowed",
775
- 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"
776
- ),
777
- "aria-pressed": interactionMode === "move",
778
- children: [
779
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Move, { size: 16, strokeWidth: 2 }),
780
- t("move")
781
- ]
782
- }
783
- ),
784
- /* @__PURE__ */ jsxRuntime.jsxs(
785
- "button",
786
- {
787
- onClick: () => setInteractionMode("pass"),
788
- disabled: !canPass,
789
- className: cn(
790
- "flex-1 flex items-center justify-center gap-2 h-11 rounded-md border font-inter text-sm font-semibold",
791
- "transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-pass-highlight",
792
- "disabled:opacity-40 disabled:cursor-not-allowed",
793
- 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"
794
- ),
795
- "aria-pressed": interactionMode === "pass",
796
- children: [
797
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-4 h-4 inline-flex", children: /* @__PURE__ */ jsxRuntime.jsx(Football, {}) }),
798
- t("pass")
799
- ]
800
- }
801
- )
802
- ]
803
- },
804
- "action-btns"
805
- ) }),
806
807
  isMyTurn && /* @__PURE__ */ jsxRuntime.jsxs(
807
808
  Button,
808
809
  {
809
810
  variant: "primary",
810
811
  size: "default",
811
812
  className: "w-full gap-2 tracking-[1.5px]",
812
- onClick: () => setShowEndTurnConfirm(true),
813
+ onClick: handleEndTurn,
813
814
  children: [
814
815
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Flag, { size: 16, strokeWidth: 2 }),
815
- t("endTurn")
816
+ t("endTurn"),
817
+ actionPoints >= 2 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: cn("ml-1 font-inter text-xs font-normal opacity-70 tracking-normal"), children: [
818
+ "\xB7 ",
819
+ actionPoints,
820
+ " ",
821
+ t("actionPointsShort")
822
+ ] })
816
823
  ]
817
824
  }
818
825
  ),
@@ -1103,6 +1110,193 @@ function StaticGameBoard() {
1103
1110
  )
1104
1111
  ] });
1105
1112
  }
1113
+ function TurnBanner({ isMyTurn, waitingLabel, className }) {
1114
+ const t = useGameT();
1115
+ const { boardState } = chunkJ2TBQPPC_cjs.useGameStore();
1116
+ if (!boardState) return null;
1117
+ const { actionPoints, maxActionPoints } = boardState;
1118
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1119
+ "div",
1120
+ {
1121
+ className: cn(
1122
+ "w-full h-9 flex items-center justify-between px-4 shrink-0 transition-colors duration-300",
1123
+ isMyTurn ? "bg-accent-green/15 border-b border-accent-green/30" : "bg-bg-surface border-b border-border-subtle",
1124
+ className
1125
+ ),
1126
+ "aria-live": "polite",
1127
+ "aria-atomic": "true",
1128
+ children: [
1129
+ /* @__PURE__ */ jsxRuntime.jsx(
1130
+ "span",
1131
+ {
1132
+ className: cn(
1133
+ "font-anton text-sm tracking-[1.5px] uppercase",
1134
+ isMyTurn ? "text-accent-green" : "text-fg-muted"
1135
+ ),
1136
+ children: isMyTurn ? t("yourTurn") : waitingLabel ?? t("waitingRival")
1137
+ }
1138
+ ),
1139
+ isMyTurn && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", "aria-label": t("actionPointsAriaLabel", { remaining: actionPoints, total: maxActionPoints }), children: [
1140
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Zap, { size: 12, className: "text-accent-green", "aria-hidden": "true" }),
1141
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-mono text-xs text-fg-secondary tabular-nums", children: [
1142
+ actionPoints,
1143
+ "/",
1144
+ maxActionPoints
1145
+ ] })
1146
+ ] })
1147
+ ]
1148
+ }
1149
+ );
1150
+ }
1151
+ var TOASTABLE = /* @__PURE__ */ new Set(["interception", "offside", "tackle"]);
1152
+ var TOAST_CONFIG = {
1153
+ interception: "bg-danger/20 border-danger/50 text-danger",
1154
+ offside: "bg-warning/20 border-warning/50 text-warning",
1155
+ tackle: "bg-warning/20 border-warning/50 text-warning"
1156
+ };
1157
+ function EventToast({ className }) {
1158
+ const t = useGameT();
1159
+ const { boardState } = chunkJ2TBQPPC_cjs.useGameStore();
1160
+ const [toast, setToast] = React.useState(null);
1161
+ React.useEffect(() => {
1162
+ const lastMove = boardState?.lastMove;
1163
+ if (!lastMove) return;
1164
+ if (!TOASTABLE.has(lastMove.type)) return;
1165
+ setToast({ type: lastMove.type, key: String(lastMove.at) });
1166
+ }, [boardState?.lastMove?.at]);
1167
+ React.useEffect(() => {
1168
+ if (!toast) return;
1169
+ const timer = setTimeout(() => setToast(null), 2500);
1170
+ return () => clearTimeout(timer);
1171
+ }, [toast?.key]);
1172
+ return /* @__PURE__ */ jsxRuntime.jsx(
1173
+ "div",
1174
+ {
1175
+ role: "status",
1176
+ "aria-live": "polite",
1177
+ "aria-atomic": "true",
1178
+ className: cn("pointer-events-none flex justify-center", className),
1179
+ children: /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { mode: "wait", children: toast && /* @__PURE__ */ jsxRuntime.jsx(
1180
+ framerMotion.motion.div,
1181
+ {
1182
+ initial: { opacity: 0, y: -8 },
1183
+ animate: { opacity: 1, y: 0 },
1184
+ exit: { opacity: 0, y: -8 },
1185
+ transition: { duration: 0.15 },
1186
+ className: cn(
1187
+ "px-4 py-2 rounded-full border font-inter text-sm font-semibold shadow-lg backdrop-blur-sm text-center",
1188
+ TOAST_CONFIG[toast.type]
1189
+ ),
1190
+ children: t(`eventToast.${toast.type}`)
1191
+ },
1192
+ toast.key
1193
+ ) })
1194
+ }
1195
+ );
1196
+ }
1197
+ function MobileHistory({ className }) {
1198
+ const { boardState } = chunkJ2TBQPPC_cjs.useGameStore();
1199
+ const [open, setOpen] = React.useState(false);
1200
+ const lastMove = boardState?.moveHistory?.at(-1);
1201
+ if (!lastMove) return null;
1202
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1203
+ /* @__PURE__ */ jsxRuntime.jsx(HistoryChip, { lastMove, onClick: () => setOpen(true), className }),
1204
+ /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: open && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1205
+ /* @__PURE__ */ jsxRuntime.jsx(
1206
+ framerMotion.motion.div,
1207
+ {
1208
+ initial: { opacity: 0 },
1209
+ animate: { opacity: 1 },
1210
+ exit: { opacity: 0 },
1211
+ transition: { duration: 0.2 },
1212
+ className: "fixed inset-0 z-40 bg-black/60 backdrop-blur-sm",
1213
+ onClick: () => setOpen(false),
1214
+ "aria-hidden": "true"
1215
+ },
1216
+ "backdrop"
1217
+ ),
1218
+ /* @__PURE__ */ jsxRuntime.jsxs(
1219
+ framerMotion.motion.div,
1220
+ {
1221
+ initial: { y: "100%" },
1222
+ animate: { y: 0 },
1223
+ exit: { y: "100%" },
1224
+ transition: { type: "spring", stiffness: 300, damping: 32 },
1225
+ className: "fixed bottom-0 left-0 right-0 z-50 bg-bg-secondary rounded-t-2xl border-t border-border-subtle shadow-xl",
1226
+ role: "dialog",
1227
+ "aria-modal": "true",
1228
+ children: [
1229
+ /* @__PURE__ */ jsxRuntime.jsx(SheetHeader, { onClose: () => setOpen(false) }),
1230
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 pb-6 max-h-[50dvh] overflow-y-auto", children: /* @__PURE__ */ jsxRuntime.jsx(MoveHistory, { scrollable: false }) })
1231
+ ]
1232
+ },
1233
+ "sheet"
1234
+ )
1235
+ ] }) })
1236
+ ] });
1237
+ }
1238
+ function SheetHeader({ onClose }) {
1239
+ const t = useGameT();
1240
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-5 pt-4 pb-3 border-b border-border-subtle", children: [
1241
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[10px] tracking-[0.5px] uppercase text-fg-muted", children: t("history.title") }),
1242
+ /* @__PURE__ */ jsxRuntime.jsx(
1243
+ "button",
1244
+ {
1245
+ onClick: onClose,
1246
+ 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",
1247
+ "aria-label": "Close",
1248
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 14 })
1249
+ }
1250
+ )
1251
+ ] });
1252
+ }
1253
+ function HistoryChip({ lastMove, onClick, className }) {
1254
+ const t = useGameT();
1255
+ const toSq = chessFootballEngine.squareName(lastMove.to);
1256
+ let label;
1257
+ switch (lastMove.type) {
1258
+ case "pass":
1259
+ label = `\u2295 ${toSq}`;
1260
+ break;
1261
+ case "tackle":
1262
+ label = `\u2715 ${toSq}`;
1263
+ break;
1264
+ case "interception":
1265
+ label = `\u26D4 ${toSq}`;
1266
+ break;
1267
+ case "goal":
1268
+ label = `\u26BD GOL`;
1269
+ break;
1270
+ case "offside":
1271
+ label = `\u26A0 OFS`;
1272
+ break;
1273
+ default: {
1274
+ const pieceShort = t(`pieces.${chessFootballEngine.SHORT_KEY[lastMove.pieceType]}`);
1275
+ const fromSq = lastMove.from ? chessFootballEngine.squareName(lastMove.from) : "";
1276
+ label = fromSq ? `${pieceShort}${fromSq}\u2192${toSq}` : `${pieceShort}\u2192${toSq}`;
1277
+ }
1278
+ }
1279
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1280
+ "button",
1281
+ {
1282
+ onClick,
1283
+ className: cn(
1284
+ "flex items-center gap-1.5 px-3 py-1.5 rounded-full",
1285
+ "bg-bg-surface border border-border-subtle",
1286
+ "font-mono text-[11px] text-fg-secondary",
1287
+ "hover:text-fg-primary hover:bg-bg-surface-elevated active:scale-95",
1288
+ "transition-all duration-150",
1289
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-green",
1290
+ className
1291
+ ),
1292
+ "aria-label": t("history.viewAll"),
1293
+ children: [
1294
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: label }),
1295
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-fg-muted text-[10px]", "aria-hidden": "true", children: "\u25BE" })
1296
+ ]
1297
+ }
1298
+ );
1299
+ }
1106
1300
 
1107
1301
  exports.ActionPoints = ActionPoints;
1108
1302
  exports.Avatar = Avatar;
@@ -1110,16 +1304,19 @@ exports.AvatarFallback = AvatarFallback;
1110
1304
  exports.AvatarImage = AvatarImage;
1111
1305
  exports.Button = Button;
1112
1306
  exports.ConfirmDialog = ConfirmDialog;
1307
+ exports.EventToast = EventToast;
1113
1308
  exports.Football = Football;
1114
1309
  exports.GameBoard = GameBoard;
1115
1310
  exports.GameControls = GameControls;
1116
1311
  exports.GameI18nProvider = GameI18nProvider;
1117
1312
  exports.GamePiece = GamePiece;
1118
1313
  exports.MiniScoreboard = MiniScoreboard;
1314
+ exports.MobileHistory = MobileHistory;
1119
1315
  exports.MoveHistory = MoveHistory;
1120
1316
  exports.Scoreboard = Scoreboard;
1121
1317
  exports.SelectedPieceDetail = SelectedPieceDetail;
1122
1318
  exports.StaticGameBoard = StaticGameBoard;
1319
+ exports.TurnBanner = TurnBanner;
1123
1320
  exports.cn = cn;
1124
1321
  exports.useGameT = useGameT;
1125
1322
  //# sourceMappingURL=index.cjs.map