@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.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,8 +91,16 @@ 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" }),
96
+ piece.hasMovedThisTurn && /* @__PURE__ */ jsxRuntime.jsx(
97
+ "span",
98
+ {
99
+ "aria-hidden": "true",
100
+ 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",
101
+ children: "\u2713"
102
+ }
103
+ ),
92
104
  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
105
  ]
94
106
  }
@@ -142,20 +154,29 @@ var Football = ({ className }) => {
142
154
  }
143
155
  );
144
156
  };
157
+ var fallback = (key) => key;
158
+ var GameI18nContext = React.createContext(fallback);
159
+ function GameI18nProvider({ t, children }) {
160
+ return /* @__PURE__ */ jsxRuntime.jsx(GameI18nContext.Provider, { value: t, children: /* @__PURE__ */ jsxRuntime.jsx(framerMotion.MotionConfig, { reducedMotion: "user", children }) });
161
+ }
162
+ function useGameT() {
163
+ return React.useContext(GameI18nContext);
164
+ }
145
165
  var COLS = 9;
146
166
  var ROWS = 12;
147
167
  var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
148
- function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
168
+ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
169
+ const t = useGameT();
149
170
  const {
150
171
  boardState,
151
172
  selectedPieceId,
152
173
  setSelectedPieceId,
153
- interactionMode,
154
174
  setInteractionMode,
155
175
  movePiece,
156
176
  passBall
157
177
  } = chunkJ2TBQPPC_cjs.useGameStore();
158
178
  const [cursor, setCursor] = React.useState(null);
179
+ const [disambiguateAt, setDisambiguateAt] = React.useState(null);
159
180
  const prevBallRef = React.useRef(boardState?.ball);
160
181
  const prevBall = prevBallRef.current;
161
182
  React.useEffect(() => {
@@ -164,19 +185,19 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
164
185
  const validMoves = React.useMemo(() => {
165
186
  if (!boardState) return [];
166
187
  const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
167
- if (sp && interactionMode === "move" && !sp.hasMovedThisTurn) {
188
+ if (sp && !sp.hasMovedThisTurn) {
168
189
  return chessFootballEngine.getValidMoves(sp, boardState);
169
190
  }
170
191
  return [];
171
- }, [boardState, selectedPieceId, interactionMode]);
192
+ }, [boardState, selectedPieceId]);
172
193
  const validPasses = React.useMemo(() => {
173
194
  if (!boardState) return [];
174
195
  const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
175
- if (sp && interactionMode === "pass") {
196
+ if (sp && boardState.ball.holderId === sp.id) {
176
197
  return chessFootballEngine.getValidPasses(sp, boardState);
177
198
  }
178
199
  return [];
179
- }, [boardState, selectedPieceId, interactionMode]);
200
+ }, [boardState, selectedPieceId]);
180
201
  if (!boardState) return null;
181
202
  const lastMove = boardState.lastMove;
182
203
  const ball = boardState.ball;
@@ -196,33 +217,40 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
196
217
  );
197
218
  pickupProportion = totalSteps > 0 ? Math.min(0.85, Math.max(0.15, stepsToBall / totalSteps)) : 0.5;
198
219
  }
220
+ const ballCarrier = ball.holderId ? boardState.pieces.find((p) => p.id === ball.holderId) ?? null : null;
221
+ const isOffsideRisk = !!ballCarrier && ballCarrier.type !== "king" && userSide !== null && ballCarrier.side === userSide && boardState.turn === userSide && chessFootballEngine.isInEnemyArea(ballCarrier.pos, ballCarrier.side);
199
222
  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
- }
223
+ const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
224
+ const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
225
+ if (isValidMove && isValidPass) {
226
+ setDisambiguateAt({ x, y });
227
+ return;
228
+ }
229
+ if (isValidMove && selectedPieceId) {
230
+ movePiece(selectedPieceId, { x, y });
231
+ setDisambiguateAt(null);
232
+ } else if (isValidPass) {
233
+ passBall({ x, y });
234
+ setDisambiguateAt(null);
235
+ } else {
236
+ setDisambiguateAt(null);
208
237
  }
209
238
  };
210
239
  const handlePieceClick = (pieceId, x, y, e) => {
211
240
  e.stopPropagation();
212
241
  const pieceAt = boardState.pieces.find((p) => p.id === pieceId);
213
242
  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) {
243
+ const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
244
+ const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
245
+ if (isValidMove || isValidPass) {
216
246
  handleSquareClick(x, y);
217
247
  } else if (boardState.turn === pieceAt.side && userSide === pieceAt.side) {
218
248
  if (selectedPieceId === pieceAt.id) {
219
- if (boardState.ball.holderId === pieceAt.id) {
220
- setInteractionMode(interactionMode === "move" ? "pass" : "move");
221
- }
249
+ setSelectedPieceId(null);
222
250
  } else {
223
251
  setSelectedPieceId(pieceAt.id);
224
- setInteractionMode("move");
225
252
  }
253
+ setDisambiguateAt(null);
226
254
  }
227
255
  };
228
256
  const defaultCursor = () => {
@@ -251,7 +279,6 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
251
279
  case "ArrowUp":
252
280
  step(0, 1);
253
281
  break;
254
- // visually up = higher rank (y grows toward the top)
255
282
  case "ArrowDown":
256
283
  step(0, -1);
257
284
  break;
@@ -271,15 +298,25 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
271
298
  }
272
299
  case "m":
273
300
  case "M":
274
- setInteractionMode("move");
301
+ if (disambiguateAt && selectedPieceId) {
302
+ movePiece(selectedPieceId, disambiguateAt);
303
+ setDisambiguateAt(null);
304
+ }
275
305
  break;
276
306
  case "p":
277
307
  case "P":
278
- setInteractionMode("pass");
308
+ if (disambiguateAt) {
309
+ passBall(disambiguateAt);
310
+ setDisambiguateAt(null);
311
+ }
279
312
  break;
280
313
  case "Escape":
281
- setSelectedPieceId(null);
282
- setInteractionMode(null);
314
+ if (disambiguateAt) {
315
+ setDisambiguateAt(null);
316
+ } else {
317
+ setSelectedPieceId(null);
318
+ setInteractionMode(null);
319
+ }
283
320
  break;
284
321
  }
285
322
  };
@@ -289,9 +326,16 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
289
326
  for (let x = 0; x < COLS; x++) {
290
327
  const isGoalArea = x >= 2 && x <= 6 && (y >= 0 && y <= 1 || y >= 10 && y <= 11);
291
328
  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);
329
+ const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
330
+ const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
331
+ const isAmbiguous = isValidMove && isValidPass;
332
+ const isLastMoveOrigin = lastMove?.from && lastMove.from.x === x && lastMove.from.y === y;
333
+ const isLastMoveDest = lastMove?.to && lastMove.to.x === x && lastMove.to.y === y;
334
+ const isLastMove = isLastMoveOrigin || isLastMoveDest;
335
+ const isEnemyGoalArea = userSide !== null && (x >= 2 && x <= 6) && (userSide === "white" ? y >= 10 && y <= 11 : y >= 0 && y <= 1);
293
336
  const pieceAt = boardState.pieces.find((p) => p.pos.x === x && p.pos.y === y);
294
337
  const isCursor = keyboardNav && cursor?.x === x && cursor?.y === y;
338
+ const isDisambiguateTarget = disambiguateAt?.x === x && disambiguateAt?.y === y;
295
339
  squares.push(
296
340
  /* @__PURE__ */ jsxRuntime.jsxs(
297
341
  "div",
@@ -299,16 +343,60 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
299
343
  onClick: () => handleSquareClick(x, y),
300
344
  className: cn(
301
345
  "relative aspect-square w-full flex items-center justify-center cursor-pointer",
302
- isEven ? "bg-[#2d5a27]" : "bg-[#356a2d]",
346
+ isEven ? "bg-field-green-1" : "bg-field-green-2",
303
347
  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")
348
+ !isValidMove && !isValidPass && isLastMove && "bg-last-move-highlight/20",
349
+ // §8: pulsing warning ring on enemy goal area
350
+ isOffsideRisk && isEnemyGoalArea && !isValidMove && !isValidPass && "ring-2 ring-inset ring-warning/60",
351
+ isAmbiguous && "ring-[3px] ring-inset ring-yellow-400/90 bg-yellow-400/20",
352
+ !isAmbiguous && isValidMove && "ring-[3px] ring-inset ring-yellow-400/80 bg-yellow-400/15",
353
+ !isAmbiguous && isValidPass && "ring-[3px] ring-inset ring-sky-400/80 bg-sky-400/15"
305
354
  ),
306
355
  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" })
356
+ !isAmbiguous && isValidMove && !pieceAt && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-full bg-yellow-400/65" }),
357
+ !isAmbiguous && isValidPass && !pieceAt && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-full bg-sky-400/65" }),
358
+ isAmbiguous && !pieceAt && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-0.5", children: [
359
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2.5 h-2.5 rounded-full bg-yellow-400/80" }),
360
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2.5 h-2.5 rounded-full bg-sky-400/80" })
361
+ ] }),
362
+ isCursor && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 ring-2 ring-inset ring-white pointer-events-none z-20", "aria-hidden": "true" }),
363
+ isDisambiguateTarget && selectedPieceId && /* @__PURE__ */ jsxRuntime.jsxs(
364
+ "div",
365
+ {
366
+ 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",
367
+ onClick: (e) => e.stopPropagation(),
368
+ children: [
369
+ /* @__PURE__ */ jsxRuntime.jsxs(
370
+ "button",
371
+ {
372
+ 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",
373
+ onClick: () => {
374
+ movePiece(selectedPieceId, { x, y });
375
+ setDisambiguateAt(null);
376
+ },
377
+ children: [
378
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Move, { size: 11, strokeWidth: 2 }),
379
+ t("move")
380
+ ]
381
+ }
382
+ ),
383
+ /* @__PURE__ */ jsxRuntime.jsxs(
384
+ "button",
385
+ {
386
+ 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",
387
+ onClick: () => {
388
+ passBall({ x, y });
389
+ setDisambiguateAt(null);
390
+ },
391
+ children: [
392
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-3 h-3 inline-flex", children: /* @__PURE__ */ jsxRuntime.jsx(Football, {}) }),
393
+ t("pass")
394
+ ]
395
+ }
396
+ )
397
+ ]
398
+ }
399
+ )
312
400
  ]
313
401
  },
314
402
  `${x}-${y}`
@@ -322,7 +410,11 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
322
410
  "div",
323
411
  {
324
412
  className: cn(
325
- "w-full max-w-[500px] mx-auto overflow-hidden rounded-xl shadow-2xl border-4 border-[#1a3317] bg-[#1a3317]",
413
+ // §6: Mobile edge-to-edge thin 2px margin, no border, no radius
414
+ "w-full mx-0.5 bg-[#1a3317] overflow-hidden",
415
+ // §5+§6: Desktop — centered, rounded, bordered, height-based max-width
416
+ "md:mx-auto md:rounded-xl md:shadow-2xl md:border-4 md:border-[#1a3317]",
417
+ "md:max-w-[min(700px,calc((100dvh_-_150px)*0.75))]",
326
418
  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
419
  ),
328
420
  ...keyboardNav ? {
@@ -357,12 +449,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
357
449
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 pointer-events-none", children: [
358
450
  /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { mode: "sync", children: boardState.pieces.map((piece) => {
359
451
  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
- };
452
+ const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : { type: "spring", stiffness: 200, damping: 25, mass: 1 };
366
453
  return /* @__PURE__ */ jsxRuntime.jsx(
367
454
  framerMotion.motion.div,
368
455
  {
@@ -406,23 +493,10 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
406
493
  top: [`${prevY}%`, `${prevY}%`, `${prevY}%`, `${targetY}%`],
407
494
  scale: [1, 1, 1.35, 1]
408
495
  };
409
- transition = {
410
- duration: PICKUP_DURATION,
411
- times: [0, pulseStart, pickupProportion, 1],
412
- ease: "linear"
413
- };
496
+ transition = { duration: PICKUP_DURATION, times: [0, pulseStart, pickupProportion, 1], ease: "linear" };
414
497
  } 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
- };
498
+ animate = { left: `${targetX}%`, top: `${targetY}%`, scale: 1 };
499
+ transition = { type: "spring", stiffness: 300, damping: 30, mass: 0.5 };
426
500
  }
427
501
  return /* @__PURE__ */ jsxRuntime.jsx(
428
502
  framerMotion.motion.div,
@@ -442,13 +516,17 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
442
516
  "ball"
443
517
  );
444
518
  })()
519
+ ] }),
520
+ 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: [
521
+ "\u26A0 ",
522
+ t("offsideWarning")
445
523
  ] })
446
524
  ] })
447
525
  }
448
526
  );
449
527
  if (!showCoordinates) return board;
450
528
  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(
529
+ 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
530
  "div",
453
531
  {
454
532
  className: "grid gap-1 select-none",
@@ -471,26 +549,10 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = false }) {
471
549
  ) });
472
550
  }
473
551
  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
- )) });
552
+ 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
553
  }
484
554
  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
- )) });
555
+ 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
556
  }
495
557
  function Avatar({ className, children }) {
496
558
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("relative w-9 h-9 rounded-full overflow-hidden shrink-0", className), children });
@@ -502,14 +564,6 @@ function AvatarImage({ src, alt, className }) {
502
564
  function AvatarFallback({ className, children }) {
503
565
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("absolute inset-0 flex items-center justify-center text-xs font-semibold", className), children });
504
566
  }
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
567
  function ActionPoints({ total, remaining, size = 20, className, kingMustRelease }) {
514
568
  const t = useGameT();
515
569
  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 +621,7 @@ function Scoreboard({
567
621
  const isMyTurn = turn === userSide;
568
622
  const myKingMustRelease = isMyTurn && kingMustRelease === turn;
569
623
  const rivalKingMustRelease = !isMyTurn && kingMustRelease === turn;
624
+ const maxAP = boardState.maxActionPoints ?? 5;
570
625
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full bg-bg-secondary flex items-center justify-between px-5 py-2.5", children: [
571
626
  /* @__PURE__ */ jsxRuntime.jsx(
572
627
  PlayerInfo,
@@ -576,6 +631,7 @@ function Scoreboard({
576
631
  avatarUrl: myAvatar,
577
632
  isActive: isMyTurn,
578
633
  actionPoints: isMyTurn ? actionPoints : 0,
634
+ maxActionPoints: maxAP,
579
635
  kingMustRelease: myKingMustRelease,
580
636
  align: "left"
581
637
  }
@@ -593,13 +649,14 @@ function Scoreboard({
593
649
  avatarUrl: rivalAvatar,
594
650
  isActive: !isMyTurn,
595
651
  actionPoints: !isMyTurn ? actionPoints : 0,
652
+ maxActionPoints: maxAP,
596
653
  kingMustRelease: rivalKingMustRelease,
597
654
  align: "right"
598
655
  }
599
656
  )
600
657
  ] });
601
658
  }
602
- function PlayerInfo({ name, team, avatarUrl, isActive, actionPoints, kingMustRelease, align }) {
659
+ function PlayerInfo({ name, team, avatarUrl, isActive, actionPoints, maxActionPoints, kingMustRelease, align }) {
603
660
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex items-center gap-2.5", align === "right" && "flex-row-reverse"), children: [
604
661
  /* @__PURE__ */ jsxRuntime.jsxs(Avatar, { className: cn(
605
662
  "w-9 h-9 border-2 transition-colors duration-300",
@@ -620,7 +677,7 @@ function PlayerInfo({ name, team, avatarUrl, isActive, actionPoints, kingMustRel
620
677
  /* @__PURE__ */ jsxRuntime.jsx(
621
678
  ActionPoints,
622
679
  {
623
- total: 5,
680
+ total: maxActionPoints,
624
681
  remaining: actionPoints,
625
682
  size: 12,
626
683
  kingMustRelease,
@@ -741,78 +798,36 @@ function ConfirmDialog({ open, title, description, confirmLabel, cancelLabel, on
741
798
  function GameControls({ isMyTurn }) {
742
799
  const {
743
800
  boardState,
744
- selectedPieceId,
745
- interactionMode,
746
- setInteractionMode,
747
801
  endTurn
748
802
  } = chunkJ2TBQPPC_cjs.useGameStore();
749
803
  const t = useGameT();
750
804
  const [showEndTurnConfirm, setShowEndTurnConfirm] = React.useState(false);
751
805
  if (!boardState) return null;
752
806
  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;
807
+ const handleEndTurn = () => {
808
+ if (actionPoints >= 2) {
809
+ setShowEndTurnConfirm(true);
810
+ } else {
811
+ endTurn();
812
+ }
813
+ };
757
814
  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
815
  isMyTurn && /* @__PURE__ */ jsxRuntime.jsxs(
807
816
  Button,
808
817
  {
809
818
  variant: "primary",
810
819
  size: "default",
811
820
  className: "w-full gap-2 tracking-[1.5px]",
812
- onClick: () => setShowEndTurnConfirm(true),
821
+ onClick: handleEndTurn,
813
822
  children: [
814
823
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Flag, { size: 16, strokeWidth: 2 }),
815
- t("endTurn")
824
+ t("endTurn"),
825
+ actionPoints >= 2 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: cn("ml-1 font-inter text-xs font-normal opacity-70 tracking-normal"), children: [
826
+ "\xB7 ",
827
+ actionPoints,
828
+ " ",
829
+ t("actionPointsShort")
830
+ ] })
816
831
  ]
817
832
  }
818
833
  ),
@@ -1103,6 +1118,193 @@ function StaticGameBoard() {
1103
1118
  )
1104
1119
  ] });
1105
1120
  }
1121
+ function TurnBanner({ isMyTurn, waitingLabel, className }) {
1122
+ const t = useGameT();
1123
+ const { boardState } = chunkJ2TBQPPC_cjs.useGameStore();
1124
+ if (!boardState) return null;
1125
+ const { actionPoints, maxActionPoints } = boardState;
1126
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1127
+ "div",
1128
+ {
1129
+ className: cn(
1130
+ "w-full h-9 flex items-center justify-between px-4 shrink-0 transition-colors duration-300",
1131
+ isMyTurn ? "bg-accent-green/15 border-b border-accent-green/30" : "bg-bg-surface border-b border-border-subtle",
1132
+ className
1133
+ ),
1134
+ "aria-live": "polite",
1135
+ "aria-atomic": "true",
1136
+ children: [
1137
+ /* @__PURE__ */ jsxRuntime.jsx(
1138
+ "span",
1139
+ {
1140
+ className: cn(
1141
+ "font-anton text-sm tracking-[1.5px] uppercase",
1142
+ isMyTurn ? "text-accent-green" : "text-fg-muted"
1143
+ ),
1144
+ children: isMyTurn ? t("yourTurn") : waitingLabel ?? t("waitingRival")
1145
+ }
1146
+ ),
1147
+ isMyTurn && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", "aria-label": t("actionPointsAriaLabel", { remaining: actionPoints, total: maxActionPoints }), children: [
1148
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Zap, { size: 12, className: "text-accent-green", "aria-hidden": "true" }),
1149
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-mono text-xs text-fg-secondary tabular-nums", children: [
1150
+ actionPoints,
1151
+ "/",
1152
+ maxActionPoints
1153
+ ] })
1154
+ ] })
1155
+ ]
1156
+ }
1157
+ );
1158
+ }
1159
+ var TOASTABLE = /* @__PURE__ */ new Set(["interception", "offside", "tackle"]);
1160
+ var TOAST_CONFIG = {
1161
+ interception: "bg-danger/20 border-danger/50 text-danger",
1162
+ offside: "bg-warning/20 border-warning/50 text-warning",
1163
+ tackle: "bg-warning/20 border-warning/50 text-warning"
1164
+ };
1165
+ function EventToast({ className }) {
1166
+ const t = useGameT();
1167
+ const { boardState } = chunkJ2TBQPPC_cjs.useGameStore();
1168
+ const [toast, setToast] = React.useState(null);
1169
+ React.useEffect(() => {
1170
+ const lastMove = boardState?.lastMove;
1171
+ if (!lastMove) return;
1172
+ if (!TOASTABLE.has(lastMove.type)) return;
1173
+ setToast({ type: lastMove.type, key: String(lastMove.at) });
1174
+ }, [boardState?.lastMove?.at]);
1175
+ React.useEffect(() => {
1176
+ if (!toast) return;
1177
+ const timer = setTimeout(() => setToast(null), 2500);
1178
+ return () => clearTimeout(timer);
1179
+ }, [toast?.key]);
1180
+ return /* @__PURE__ */ jsxRuntime.jsx(
1181
+ "div",
1182
+ {
1183
+ role: "status",
1184
+ "aria-live": "polite",
1185
+ "aria-atomic": "true",
1186
+ className: cn("pointer-events-none flex justify-center", className),
1187
+ children: /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { mode: "wait", children: toast && /* @__PURE__ */ jsxRuntime.jsx(
1188
+ framerMotion.motion.div,
1189
+ {
1190
+ initial: { opacity: 0, y: -8 },
1191
+ animate: { opacity: 1, y: 0 },
1192
+ exit: { opacity: 0, y: -8 },
1193
+ transition: { duration: 0.15 },
1194
+ className: cn(
1195
+ "px-4 py-2 rounded-full border font-inter text-sm font-semibold shadow-lg backdrop-blur-sm text-center",
1196
+ TOAST_CONFIG[toast.type]
1197
+ ),
1198
+ children: t(`eventToast.${toast.type}`)
1199
+ },
1200
+ toast.key
1201
+ ) })
1202
+ }
1203
+ );
1204
+ }
1205
+ function MobileHistory({ className }) {
1206
+ const { boardState } = chunkJ2TBQPPC_cjs.useGameStore();
1207
+ const [open, setOpen] = React.useState(false);
1208
+ const lastMove = boardState?.moveHistory?.at(-1);
1209
+ if (!lastMove) return null;
1210
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1211
+ /* @__PURE__ */ jsxRuntime.jsx(HistoryChip, { lastMove, onClick: () => setOpen(true), className }),
1212
+ /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: open && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1213
+ /* @__PURE__ */ jsxRuntime.jsx(
1214
+ framerMotion.motion.div,
1215
+ {
1216
+ initial: { opacity: 0 },
1217
+ animate: { opacity: 1 },
1218
+ exit: { opacity: 0 },
1219
+ transition: { duration: 0.2 },
1220
+ className: "fixed inset-0 z-40 bg-black/60 backdrop-blur-sm",
1221
+ onClick: () => setOpen(false),
1222
+ "aria-hidden": "true"
1223
+ },
1224
+ "backdrop"
1225
+ ),
1226
+ /* @__PURE__ */ jsxRuntime.jsxs(
1227
+ framerMotion.motion.div,
1228
+ {
1229
+ initial: { y: "100%" },
1230
+ animate: { y: 0 },
1231
+ exit: { y: "100%" },
1232
+ transition: { type: "spring", stiffness: 300, damping: 32 },
1233
+ className: "fixed bottom-0 left-0 right-0 z-50 bg-bg-secondary rounded-t-2xl border-t border-border-subtle shadow-xl",
1234
+ role: "dialog",
1235
+ "aria-modal": "true",
1236
+ children: [
1237
+ /* @__PURE__ */ jsxRuntime.jsx(SheetHeader, { onClose: () => setOpen(false) }),
1238
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 pb-6 max-h-[50dvh] overflow-y-auto", children: /* @__PURE__ */ jsxRuntime.jsx(MoveHistory, { scrollable: false }) })
1239
+ ]
1240
+ },
1241
+ "sheet"
1242
+ )
1243
+ ] }) })
1244
+ ] });
1245
+ }
1246
+ function SheetHeader({ onClose }) {
1247
+ const t = useGameT();
1248
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-5 pt-4 pb-3 border-b border-border-subtle", children: [
1249
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[10px] tracking-[0.5px] uppercase text-fg-muted", children: t("history.title") }),
1250
+ /* @__PURE__ */ jsxRuntime.jsx(
1251
+ "button",
1252
+ {
1253
+ onClick: onClose,
1254
+ 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",
1255
+ "aria-label": "Close",
1256
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 14 })
1257
+ }
1258
+ )
1259
+ ] });
1260
+ }
1261
+ function HistoryChip({ lastMove, onClick, className }) {
1262
+ const t = useGameT();
1263
+ const toSq = chessFootballEngine.squareName(lastMove.to);
1264
+ let label;
1265
+ switch (lastMove.type) {
1266
+ case "pass":
1267
+ label = `\u2295 ${toSq}`;
1268
+ break;
1269
+ case "tackle":
1270
+ label = `\u2715 ${toSq}`;
1271
+ break;
1272
+ case "interception":
1273
+ label = `\u26D4 ${toSq}`;
1274
+ break;
1275
+ case "goal":
1276
+ label = `\u26BD GOL`;
1277
+ break;
1278
+ case "offside":
1279
+ label = `\u26A0 OFS`;
1280
+ break;
1281
+ default: {
1282
+ const pieceShort = t(`pieces.${chessFootballEngine.SHORT_KEY[lastMove.pieceType]}`);
1283
+ const fromSq = lastMove.from ? chessFootballEngine.squareName(lastMove.from) : "";
1284
+ label = fromSq ? `${pieceShort}${fromSq}\u2192${toSq}` : `${pieceShort}\u2192${toSq}`;
1285
+ }
1286
+ }
1287
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1288
+ "button",
1289
+ {
1290
+ onClick,
1291
+ className: cn(
1292
+ "flex items-center gap-1.5 px-3 py-1.5 rounded-full",
1293
+ "bg-bg-surface border border-border-subtle",
1294
+ "font-mono text-[11px] text-fg-secondary",
1295
+ "hover:text-fg-primary hover:bg-bg-surface-elevated active:scale-95",
1296
+ "transition-all duration-150",
1297
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-green",
1298
+ className
1299
+ ),
1300
+ "aria-label": t("history.viewAll"),
1301
+ children: [
1302
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: label }),
1303
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-fg-muted text-[10px]", "aria-hidden": "true", children: "\u25BE" })
1304
+ ]
1305
+ }
1306
+ );
1307
+ }
1106
1308
 
1107
1309
  exports.ActionPoints = ActionPoints;
1108
1310
  exports.Avatar = Avatar;
@@ -1110,16 +1312,19 @@ exports.AvatarFallback = AvatarFallback;
1110
1312
  exports.AvatarImage = AvatarImage;
1111
1313
  exports.Button = Button;
1112
1314
  exports.ConfirmDialog = ConfirmDialog;
1315
+ exports.EventToast = EventToast;
1113
1316
  exports.Football = Football;
1114
1317
  exports.GameBoard = GameBoard;
1115
1318
  exports.GameControls = GameControls;
1116
1319
  exports.GameI18nProvider = GameI18nProvider;
1117
1320
  exports.GamePiece = GamePiece;
1118
1321
  exports.MiniScoreboard = MiniScoreboard;
1322
+ exports.MobileHistory = MobileHistory;
1119
1323
  exports.MoveHistory = MoveHistory;
1120
1324
  exports.Scoreboard = Scoreboard;
1121
1325
  exports.SelectedPieceDetail = SelectedPieceDetail;
1122
1326
  exports.StaticGameBoard = StaticGameBoard;
1327
+ exports.TurnBanner = TurnBanner;
1123
1328
  exports.cn = cn;
1124
1329
  exports.useGameT = useGameT;
1125
1330
  //# sourceMappingURL=index.cjs.map