@scriptonita/chess-football-ui 0.6.0 → 0.7.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
@@ -74,26 +74,66 @@ var BLACK_PIECE_STYLE = {
74
74
  border: "1px solid rgba(0,0,0,0.55)",
75
75
  color: "#fafafa"
76
76
  };
77
+ var ON_BOARD_CHIP_STYLE = {
78
+ white: {
79
+ ...WHITE_PIECE_STYLE,
80
+ boxShadow: [
81
+ "inset 0 1.5px 1px rgba(255,255,255,0.95)",
82
+ "inset 0 -2px 3px rgba(0,0,0,0.18)",
83
+ "0 1px 1px rgba(0,0,0,0.35)"
84
+ ].join(", ")
85
+ },
86
+ black: {
87
+ ...BLACK_PIECE_STYLE,
88
+ boxShadow: [
89
+ "inset 0 1.5px 1px rgba(255,255,255,0.22)",
90
+ "inset 0 -2px 3px rgba(0,0,0,0.6)",
91
+ "0 1px 1px rgba(0,0,0,0.5)"
92
+ ].join(", ")
93
+ }
94
+ };
77
95
  var MOVED_STYLE = {
78
96
  filter: "saturate(0.15) brightness(0.75)"
79
97
  };
80
- function GamePiece({ piece, isSelected, hasBall, onClick }) {
81
- const baseStyle = piece.side === "white" ? WHITE_PIECE_STYLE : BLACK_PIECE_STYLE;
82
- const style = piece.hasMovedThisTurn ? { ...baseStyle, ...MOVED_STYLE } : baseStyle;
98
+ function GamePiece({ piece, isSelected, hasBall, onClick, isMoving = false }) {
99
+ const chipStyle = ON_BOARD_CHIP_STYLE[piece.side];
83
100
  return /* @__PURE__ */ jsxRuntime.jsxs(
84
101
  "div",
85
102
  {
86
103
  onClick,
87
- style,
104
+ style: piece.hasMovedThisTurn ? MOVED_STYLE : void 0,
88
105
  className: cn(
89
- "relative w-[88%] h-[88%] flex items-center justify-center rounded-full cursor-pointer z-10 transition-transform motion-reduce:transition-none",
106
+ "relative w-[88%] h-[88%] rounded-full cursor-pointer z-10 transition-transform duration-200 ease-out motion-reduce:transition-none",
90
107
  isSelected && "ring-4 ring-primary scale-110 z-30",
108
+ isMoving && "scale-[1.08] z-40",
91
109
  piece.hasMovedThisTurn && "opacity-70"
92
110
  ),
93
111
  children: [
94
- /* @__PURE__ */ jsxRuntime.jsx(PieceIcon, { type: piece.type, side: piece.side }),
95
- hasBall && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 rounded-full ring-2 ring-orange-400 pointer-events-none" }),
96
- isSelected && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 rounded-full animate-pulse motion-reduce:animate-none ring-2 ring-yellow-400 ring-offset-2 ring-offset-transparent" })
112
+ /* @__PURE__ */ jsxRuntime.jsx(
113
+ "div",
114
+ {
115
+ "aria-hidden": "true",
116
+ "data-testid": "piece-shadow",
117
+ "data-lifted": isMoving ? "true" : "false",
118
+ className: cn(
119
+ "absolute inset-0 rounded-full bg-black pointer-events-none transition-all duration-200 ease-out motion-reduce:transition-none",
120
+ isMoving ? "translate-x-[9%] translate-y-[24%] scale-[1.04] opacity-35 blur-[5px]" : "translate-x-[4%] translate-y-[9%] scale-95 opacity-50 blur-[3px]"
121
+ )
122
+ }
123
+ ),
124
+ /* @__PURE__ */ jsxRuntime.jsxs(
125
+ "div",
126
+ {
127
+ "data-testid": "piece-chip",
128
+ style: chipStyle,
129
+ className: "absolute inset-0 rounded-full flex items-center justify-center",
130
+ children: [
131
+ /* @__PURE__ */ jsxRuntime.jsx(PieceIcon, { type: piece.type, side: piece.side }),
132
+ hasBall && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 rounded-full ring-2 ring-orange-400 pointer-events-none" }),
133
+ isSelected && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 rounded-full animate-pulse motion-reduce:animate-none ring-2 ring-yellow-400 ring-offset-2 ring-offset-transparent" })
134
+ ]
135
+ }
136
+ )
97
137
  ]
98
138
  }
99
139
  );
@@ -205,10 +245,204 @@ function GameI18nProvider({ t, children }) {
205
245
  function useGameT() {
206
246
  return React.useContext(GameI18nContext);
207
247
  }
248
+ function BallHolderChip({ className }) {
249
+ const t = useGameT();
250
+ const boardState = chunkLTFNSTAQ_cjs.useGameStore((s) => s.boardState);
251
+ const holder = boardState?.pieces.find((p) => p.id === boardState.ball.holderId) ?? null;
252
+ if (!holder) return null;
253
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex items-center gap-1.5 min-h-[1.75rem]", className), "aria-live": "polite", children: [
254
+ /* @__PURE__ */ jsxRuntime.jsxs(
255
+ "div",
256
+ {
257
+ style: holder.side === "white" ? WHITE_PIECE_STYLE : BLACK_PIECE_STYLE,
258
+ className: "relative w-7 h-7 flex items-center justify-center rounded-full shrink-0",
259
+ children: [
260
+ /* @__PURE__ */ jsxRuntime.jsx(PieceIcon, { type: holder.type, side: holder.side }),
261
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute -bottom-1 -right-1 w-3.5 h-3.5 flex items-center justify-center rounded-full bg-bg-secondary border border-border-subtle", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2.5 h-2.5", children: /* @__PURE__ */ jsxRuntime.jsx(Football, {}) }) })
262
+ ]
263
+ }
264
+ ),
265
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-inter text-[11px] text-fg-secondary", children: [
266
+ t("ballHolder"),
267
+ ": ",
268
+ t(`pieces.${holder.type}`)
269
+ ] })
270
+ ] });
271
+ }
272
+ function KeyboardShortcutsList({ className }) {
273
+ const t = useGameT();
274
+ return /* @__PURE__ */ jsxRuntime.jsxs("ul", { className: cn("font-mono text-[10px] text-fg-secondary leading-relaxed", className), children: [
275
+ /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
276
+ /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "\u2191\u2193\u2190\u2192" }),
277
+ " ",
278
+ t("shortcuts.arrows")
279
+ ] }),
280
+ /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
281
+ /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "Enter" }),
282
+ " ",
283
+ t("shortcuts.select")
284
+ ] }),
285
+ /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
286
+ /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "M" }),
287
+ " ",
288
+ t("shortcuts.move")
289
+ ] }),
290
+ /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
291
+ /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "P" }),
292
+ " ",
293
+ t("shortcuts.pass")
294
+ ] }),
295
+ /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
296
+ /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "Esc" }),
297
+ " ",
298
+ t("shortcuts.cancel")
299
+ ] })
300
+ ] });
301
+ }
302
+ var PITCH_COLS = 9;
303
+ var PITCH_ROWS = 12;
304
+ var isGoalAreaSquare = (x, y) => x >= 2 && x <= 6 && (y >= 0 && y <= 1 || y >= PITCH_ROWS - 2 && y <= PITCH_ROWS - 1);
305
+ var GOAL_AREA_SHADE_CLASS = "shadow-[inset_0_0_0_100vmax_rgba(0,0,0,0.32)]";
306
+ function pitchSquareClass(x, y) {
307
+ const isEven = (x + y) % 2 === 0;
308
+ return cn(
309
+ "relative aspect-square w-full",
310
+ isEven ? "bg-field-green-1" : "bg-field-green-2",
311
+ isGoalAreaSquare(x, y) && GOAL_AREA_SHADE_CLASS
312
+ );
313
+ }
314
+ var GRAIN_TILE_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160"><filter id="g" x="0" y="0" width="100%" height="100%" color-interpolation-filters="sRGB"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="3" seed="7" stitchTiles="stitch"/><feColorMatrix type="saturate" values="0"/></filter><rect width="100%" height="100%" filter="url(#g)"/></svg>';
315
+ var GRAIN_TILE_URL = `url("data:image/svg+xml;utf8,${encodeURIComponent(GRAIN_TILE_SVG)}")`;
316
+ var VIGNETTE = "radial-gradient(120% 95% at 50% 38%, rgba(255,255,255,0.06) 0%, rgba(255,255,255,0) 42%, rgba(0,0,0,0.26) 100%)";
317
+ function GrassOverlay({ className }) {
318
+ return /* @__PURE__ */ jsxRuntime.jsxs(
319
+ "div",
320
+ {
321
+ "aria-hidden": "true",
322
+ "data-testid": "grass-overlay",
323
+ className: cn("absolute inset-0 pointer-events-none overflow-hidden", className),
324
+ children: [
325
+ /* @__PURE__ */ jsxRuntime.jsx(
326
+ "div",
327
+ {
328
+ "data-layer": "grain",
329
+ className: "absolute inset-0 mix-blend-overlay opacity-60",
330
+ style: { backgroundImage: GRAIN_TILE_URL, backgroundSize: "160px 160px" }
331
+ }
332
+ ),
333
+ /* @__PURE__ */ jsxRuntime.jsx("div", { "data-layer": "light", className: "absolute inset-0", style: { background: VIGNETTE } })
334
+ ]
335
+ }
336
+ );
337
+ }
338
+ var CHALK = "#f6f5ee";
339
+ function PitchMarkings({ className, children }) {
340
+ const id = React.useId();
341
+ const filterId = `${id}-chalk`;
342
+ return /* @__PURE__ */ jsxRuntime.jsxs(
343
+ "svg",
344
+ {
345
+ viewBox: "0 0 9 12",
346
+ preserveAspectRatio: "none",
347
+ className: cn("absolute inset-0 w-full h-full pointer-events-none", className),
348
+ "aria-hidden": "true",
349
+ "data-testid": "pitch-markings",
350
+ children: [
351
+ /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsxs(
352
+ "filter",
353
+ {
354
+ id: filterId,
355
+ filterUnits: "userSpaceOnUse",
356
+ x: "-0.5",
357
+ y: "-0.5",
358
+ width: "10",
359
+ height: "13",
360
+ colorInterpolationFilters: "sRGB",
361
+ children: [
362
+ /* @__PURE__ */ jsxRuntime.jsx("feTurbulence", { type: "fractalNoise", baseFrequency: "5.5", numOctaves: "2", seed: "3", result: "edgeNoise" }),
363
+ /* @__PURE__ */ jsxRuntime.jsx(
364
+ "feDisplacementMap",
365
+ {
366
+ in: "SourceGraphic",
367
+ in2: "edgeNoise",
368
+ scale: "0.045",
369
+ xChannelSelector: "R",
370
+ yChannelSelector: "G",
371
+ result: "rough"
372
+ }
373
+ ),
374
+ /* @__PURE__ */ jsxRuntime.jsx("feTurbulence", { type: "fractalNoise", baseFrequency: "14", numOctaves: "3", seed: "11", result: "dust" }),
375
+ /* @__PURE__ */ jsxRuntime.jsx(
376
+ "feColorMatrix",
377
+ {
378
+ in: "dust",
379
+ type: "matrix",
380
+ values: "0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0.75 0 0 0 0.55",
381
+ result: "dustMask"
382
+ }
383
+ ),
384
+ /* @__PURE__ */ jsxRuntime.jsx("feComposite", { in: "rough", in2: "dustMask", operator: "arithmetic", k1: "1", k2: "0", k3: "0", k4: "0" })
385
+ ]
386
+ }
387
+ ) }),
388
+ /* @__PURE__ */ jsxRuntime.jsx("g", { "data-layer": "halo", fill: "none", stroke: CHALK, strokeOpacity: "0.09", strokeWidth: "0.16", strokeLinecap: "round", children: /* @__PURE__ */ jsxRuntime.jsx(Markings, {}) }),
389
+ /* @__PURE__ */ jsxRuntime.jsx(
390
+ "g",
391
+ {
392
+ "data-layer": "chalk",
393
+ filter: `url(#${filterId})`,
394
+ fill: "none",
395
+ stroke: CHALK,
396
+ strokeOpacity: "0.82",
397
+ strokeWidth: "0.07",
398
+ strokeLinecap: "round",
399
+ strokeLinejoin: "round",
400
+ children: /* @__PURE__ */ jsxRuntime.jsx(Markings, {})
401
+ }
402
+ ),
403
+ children
404
+ ]
405
+ }
406
+ );
407
+ }
408
+ function Markings() {
409
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
410
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "0.06", y: "0.06", width: "8.88", height: "11.88" }),
411
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "0.06", width: "5", height: "1.94" }),
412
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "10", width: "5", height: "1.94" }),
413
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "0.06", y1: "6", x2: "8.94", y2: "6" }),
414
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "6", r: "1.5" }),
415
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "6", r: "0.09", fill: CHALK, fillOpacity: "0.9", stroke: "none" }),
416
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "1.5", r: "0.08", fill: CHALK, fillOpacity: "0.9", stroke: "none" }),
417
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "10.5", r: "0.08", fill: CHALK, fillOpacity: "0.9", stroke: "none" }),
418
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 3.086,2 A 1.5,1.5 0 0 0 5.914,2" }),
419
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 3.086,10 A 1.5,1.5 0 0 1 5.914,10" })
420
+ ] });
421
+ }
422
+ function PitchSurface({ className, squareClassName, children }) {
423
+ const squares = [];
424
+ for (let y = PITCH_ROWS - 1; y >= 0; y--) {
425
+ for (let x = 0; x < PITCH_COLS; x++) {
426
+ squares.push(
427
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(pitchSquareClass(x, y), squareClassName?.(x, y)) }, `${x}-${y}`)
428
+ );
429
+ }
430
+ }
431
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("relative", className), style: { containerType: "inline-size" }, children: [
432
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-9 gap-[1px]", "data-testid": "pitch-squares", children: squares }),
433
+ /* @__PURE__ */ jsxRuntime.jsx(GrassOverlay, {}),
434
+ /* @__PURE__ */ jsxRuntime.jsx(PitchMarkings, {}),
435
+ children
436
+ ] });
437
+ }
208
438
  var COLS = 9;
209
439
  var ROWS = 12;
210
440
  var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
211
- function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
441
+ var cellX = (x) => `${x * 100}%`;
442
+ var cellY = (y) => `${(ROWS - 1 - y) * 100}%`;
443
+ var PIECE_SLIDE = { type: "spring", stiffness: 190, damping: 28, mass: 1 };
444
+ var BALL_SLIDE = { type: "spring", stiffness: 300, damping: 26, mass: 0.6 };
445
+ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, toolbar = true }) {
212
446
  const t = useGameT();
213
447
  const prefersReducedMotion = framerMotion.useReducedMotion();
214
448
  const {
@@ -240,6 +474,14 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
240
474
  setInvalidClickAt({ x, y, nonce: invalidClickNonceRef.current });
241
475
  };
242
476
  const [hoveredPassAt, setHoveredPassAt] = React.useState(null);
477
+ const [movingIds, setMovingIds] = React.useState(() => /* @__PURE__ */ new Set());
478
+ const setMoving = (id, moving) => setMovingIds((prev) => {
479
+ if (prev.has(id) === moving) return prev;
480
+ const next = new Set(prev);
481
+ if (moving) next.add(id);
482
+ else next.delete(id);
483
+ return next;
484
+ });
243
485
  React.useEffect(() => {
244
486
  if (!invalidClickAt) return;
245
487
  const id = setTimeout(() => setInvalidClickAt(null), 100);
@@ -294,7 +536,6 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
294
536
  return [];
295
537
  }, [boardState, selectedPieceId]);
296
538
  if (!boardState) return null;
297
- const ballHolderPiece = boardState.pieces.find((p) => p.id === boardState.ball.holderId) ?? null;
298
539
  const lastMove = boardState.lastMove;
299
540
  const ball = boardState.ball;
300
541
  const ballMoved = !!prevBall && (prevBall.pos.x !== ball.pos.x || prevBall.pos.y !== ball.pos.y);
@@ -437,8 +678,6 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
437
678
  const squares = [];
438
679
  for (let y = ROWS - 1; y >= 0; y--) {
439
680
  for (let x = 0; x < COLS; x++) {
440
- const isGoalArea = x >= 2 && x <= 6 && (y >= 0 && y <= 1 || y >= 10 && y <= 11);
441
- const isEven = (x + y) % 2 === 0;
442
681
  const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
443
682
  const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
444
683
  const isAmbiguous = isValidMove && isValidPass;
@@ -460,9 +699,8 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
460
699
  },
461
700
  onMouseLeave: () => setHoveredPassAt(null),
462
701
  className: cn(
463
- "relative aspect-square w-full flex items-center justify-center cursor-pointer",
464
- isEven ? "bg-field-green-1" : "bg-field-green-2",
465
- isGoalArea && "bg-[#1a3a18] ring-1 ring-inset ring-emerald-500/30",
702
+ pitchSquareClass(x, y),
703
+ "flex items-center justify-center cursor-pointer",
466
704
  !isValidMove && !isValidPass && isLastMove && "bg-last-move-highlight/20",
467
705
  // §8: pulsing warning ring on enemy goal area
468
706
  isOffsideRisk && isEnemyGoalArea && !isValidMove && !isValidPass && "ring-2 ring-inset ring-warning/60",
@@ -549,25 +787,8 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
549
787
  onKeyDown: handleKeyDown
550
788
  } : {},
551
789
  children: [
552
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "hidden md:flex items-center justify-between gap-2 px-2.5 py-1", children: [
553
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-1.5 min-h-[1.75rem]", "aria-live": "polite", children: ballHolderPiece && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
554
- /* @__PURE__ */ jsxRuntime.jsxs(
555
- "div",
556
- {
557
- style: ballHolderPiece.side === "white" ? WHITE_PIECE_STYLE : BLACK_PIECE_STYLE,
558
- className: "relative w-7 h-7 flex items-center justify-center rounded-full shrink-0",
559
- children: [
560
- /* @__PURE__ */ jsxRuntime.jsx(PieceIcon, { type: ballHolderPiece.type, side: ballHolderPiece.side }),
561
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute -bottom-1 -right-1 w-3.5 h-3.5 flex items-center justify-center rounded-full bg-bg-secondary border border-border-subtle", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2.5 h-2.5", children: /* @__PURE__ */ jsxRuntime.jsx(Football, {}) }) })
562
- ]
563
- }
564
- ),
565
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-inter text-[11px] text-fg-secondary", children: [
566
- t("ballHolder"),
567
- ": ",
568
- t(`pieces.${ballHolderPiece.type}`)
569
- ] })
570
- ] }) }),
790
+ toolbar && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "hidden md:flex items-center justify-between gap-2 px-2.5 py-1", "data-testid": "board-toolbar", children: [
791
+ /* @__PURE__ */ jsxRuntime.jsx(BallHolderChip, {}),
571
792
  keyboardNav && /* @__PURE__ */ jsxRuntime.jsxs(
572
793
  "div",
573
794
  {
@@ -596,33 +817,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
596
817
  onClick: (e) => e.stopPropagation(),
597
818
  children: [
598
819
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-inter text-[11px] font-semibold text-fg-primary mb-1.5", children: t("shortcuts.title") }),
599
- /* @__PURE__ */ jsxRuntime.jsxs("ul", { className: "font-mono text-[10px] text-fg-secondary leading-relaxed", children: [
600
- /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
601
- /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "\u2191\u2193\u2190\u2192" }),
602
- " ",
603
- t("shortcuts.arrows")
604
- ] }),
605
- /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
606
- /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "Enter" }),
607
- " ",
608
- t("shortcuts.select")
609
- ] }),
610
- /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
611
- /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "M" }),
612
- " ",
613
- t("shortcuts.move")
614
- ] }),
615
- /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
616
- /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "P" }),
617
- " ",
618
- t("shortcuts.pass")
619
- ] }),
620
- /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
621
- /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "Esc" }),
622
- " ",
623
- t("shortcuts.cancel")
624
- ] })
625
- ] })
820
+ /* @__PURE__ */ jsxRuntime.jsx(KeyboardShortcutsList, {})
626
821
  ]
627
822
  }
628
823
  )
@@ -632,58 +827,51 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
632
827
  ] }),
633
828
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", style: { containerType: "inline-size" }, children: [
634
829
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-9 gap-[1px]", children: renderSquares() }),
635
- /* @__PURE__ */ jsxRuntime.jsxs(
830
+ /* @__PURE__ */ jsxRuntime.jsx(GrassOverlay, {}),
831
+ /* @__PURE__ */ jsxRuntime.jsx(PitchMarkings, {}),
832
+ /* @__PURE__ */ jsxRuntime.jsx(
636
833
  "svg",
637
834
  {
638
835
  viewBox: "0 0 9 12",
639
836
  preserveAspectRatio: "none",
640
837
  className: "absolute inset-0 w-full h-full pointer-events-none",
641
838
  "aria-hidden": "true",
642
- children: [
643
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "0", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
644
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "10", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
645
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "0", y1: "6", x2: "9", y2: "6", stroke: "white", strokeOpacity: "0.35", strokeWidth: "0.05" }),
646
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "6", r: "1.5", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
647
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "6", r: "0.12", fill: "white", fillOpacity: "0.5" }),
648
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "1.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
649
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "10.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
650
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 3.086,2 A 1.5,1.5 0 0 0 5.914,2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
651
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 3.086,10 A 1.5,1.5 0 0 1 5.914,10", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
652
- hoveredPassAt && passCarrier && /* @__PURE__ */ jsxRuntime.jsx(
653
- "line",
654
- {
655
- "data-testid": "pass-trajectory-line",
656
- x1: passCarrier.pos.x + 0.5,
657
- y1: ROWS - 1 - passCarrier.pos.y + 0.5,
658
- x2: hoveredPassAt.x + 0.5,
659
- y2: ROWS - 1 - hoveredPassAt.y + 0.5,
660
- stroke: trajectoryIntercepted ? "var(--danger)" : "var(--pass-highlight)",
661
- strokeWidth: "0.08",
662
- strokeDasharray: "0.15 0.1",
663
- strokeLinecap: "round"
664
- }
665
- )
666
- ]
839
+ children: hoveredPassAt && passCarrier && /* @__PURE__ */ jsxRuntime.jsx(
840
+ "line",
841
+ {
842
+ "data-testid": "pass-trajectory-line",
843
+ x1: passCarrier.pos.x + 0.5,
844
+ y1: ROWS - 1 - passCarrier.pos.y + 0.5,
845
+ x2: hoveredPassAt.x + 0.5,
846
+ y2: ROWS - 1 - hoveredPassAt.y + 0.5,
847
+ stroke: trajectoryIntercepted ? "var(--danger)" : "var(--pass-highlight)",
848
+ strokeWidth: "0.08",
849
+ strokeDasharray: "0.15 0.1",
850
+ strokeLinecap: "round"
851
+ }
852
+ )
667
853
  }
668
854
  ),
669
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 pointer-events-none", children: [
855
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 pointer-events-none", "data-testid": "pieces-layer", children: [
670
856
  /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { mode: "sync", children: boardState.pieces.map((piece) => {
671
857
  const isPickupCarrier = pickupCarrierId === piece.id;
672
- const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : { type: "spring", stiffness: 200, damping: 25, mass: 1 };
858
+ const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : PIECE_SLIDE;
673
859
  return /* @__PURE__ */ jsxRuntime.jsx(
674
860
  framerMotion.motion.div,
675
861
  {
676
- layout: true,
862
+ "data-piece-id": piece.id,
677
863
  initial: false,
678
- animate: {
679
- left: `${piece.pos.x / COLS * 100}%`,
680
- top: `${(ROWS - 1 - piece.pos.y) / ROWS * 100}%`
681
- },
864
+ animate: { x: cellX(piece.pos.x), y: cellY(piece.pos.y) },
682
865
  transition: pieceTransition,
866
+ onAnimationStart: () => setMoving(piece.id, true),
867
+ onAnimationComplete: () => setMoving(piece.id, false),
683
868
  style: {
684
869
  position: "absolute",
870
+ left: 0,
871
+ top: 0,
685
872
  width: `${100 / COLS}%`,
686
- height: `${100 / ROWS}%`
873
+ height: `${100 / ROWS}%`,
874
+ willChange: "transform"
687
875
  },
688
876
  className: "flex items-center justify-center pointer-events-auto",
689
877
  children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -692,6 +880,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
692
880
  piece,
693
881
  isSelected: selectedPieceId === piece.id,
694
882
  hasBall: boardState.ball.holderId === piece.id,
883
+ isMoving: movingIds.has(piece.id),
695
884
  onClick: (e) => handlePieceClick(piece.id, piece.pos.x, piece.pos.y, e)
696
885
  }
697
886
  )
@@ -700,23 +889,23 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
700
889
  );
701
890
  }) }),
702
891
  (() => {
703
- const targetX = ball.pos.x / COLS * 100;
704
- const targetY = (ROWS - 1 - ball.pos.y) / ROWS * 100;
892
+ const targetX = cellX(ball.pos.x);
893
+ const targetY = cellY(ball.pos.y);
705
894
  let animate;
706
895
  let transition;
707
896
  if (isPickupOnMove && prevBall) {
708
- const prevX = prevBall.pos.x / COLS * 100;
709
- const prevY = (ROWS - 1 - prevBall.pos.y) / ROWS * 100;
897
+ const prevX = cellX(prevBall.pos.x);
898
+ const prevY = cellY(prevBall.pos.y);
710
899
  const pulseStart = Math.max(0.05, pickupProportion - 0.06);
711
900
  animate = {
712
- left: [`${prevX}%`, `${prevX}%`, `${prevX}%`, `${targetX}%`],
713
- top: [`${prevY}%`, `${prevY}%`, `${prevY}%`, `${targetY}%`],
901
+ x: [prevX, prevX, prevX, targetX],
902
+ y: [prevY, prevY, prevY, targetY],
714
903
  scale: [1, 1, 1.35, 1]
715
904
  };
716
905
  transition = { duration: PICKUP_DURATION, times: [0, pulseStart, pickupProportion, 1], ease: "linear" };
717
906
  } else {
718
- animate = { left: `${targetX}%`, top: `${targetY}%`, scale: 1 };
719
- transition = { type: "spring", stiffness: 300, damping: 30, mass: 0.5 };
907
+ animate = { x: targetX, y: targetY, scale: 1 };
908
+ transition = ball.holderId ? PIECE_SLIDE : BALL_SLIDE;
720
909
  }
721
910
  return /* @__PURE__ */ jsxRuntime.jsx(
722
911
  framerMotion.motion.div,
@@ -726,9 +915,12 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
726
915
  transition,
727
916
  style: {
728
917
  position: "absolute",
918
+ left: 0,
919
+ top: 0,
729
920
  width: `${100 / COLS}%`,
730
921
  height: `${100 / ROWS}%`,
731
- zIndex: 50
922
+ zIndex: 50,
923
+ willChange: "transform"
732
924
  },
733
925
  className: "flex items-center justify-center pointer-events-none",
734
926
  children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-[60%] h-[60%]", children: /* @__PURE__ */ jsxRuntime.jsx(Football, {}) })
@@ -1289,77 +1481,31 @@ var COLS2 = 9;
1289
1481
  var ROWS2 = 12;
1290
1482
  function StaticGameBoard() {
1291
1483
  const boardState = React.useMemo(() => chunkLTFNSTAQ_cjs.getInitialBoardState("white"), []);
1292
- const renderSquares = () => {
1293
- const squares = [];
1294
- for (let y = ROWS2 - 1; y >= 0; y--) {
1295
- for (let x = 0; x < COLS2; x++) {
1296
- const isGoalArea = x >= 2 && x <= 6 && (y >= 0 && y <= 1 || y >= 10 && y <= 11);
1297
- const isEven = (x + y) % 2 === 0;
1298
- squares.push(
1299
- /* @__PURE__ */ jsxRuntime.jsx(
1300
- "div",
1301
- {
1302
- className: cn(
1303
- "relative aspect-square w-full",
1304
- isEven ? "bg-[#2d5a27]" : "bg-[#356a2d]",
1305
- isGoalArea && "bg-[#1a3a18] ring-1 ring-inset ring-emerald-500/30"
1306
- )
1307
- },
1308
- `${x}-${y}`
1309
- )
1310
- );
1311
- }
1312
- }
1313
- return squares;
1314
- };
1315
1484
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full overflow-hidden rounded-xl shadow-2xl border-4 border-[#1a3317] bg-[#1a3317]", children: [
1316
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", style: { containerType: "inline-size" }, children: [
1317
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-9 gap-[1px]", children: renderSquares() }),
1318
- /* @__PURE__ */ jsxRuntime.jsxs(
1319
- "svg",
1320
- {
1321
- viewBox: "0 0 9 12",
1322
- preserveAspectRatio: "none",
1323
- className: "absolute inset-0 w-full h-full pointer-events-none",
1324
- "aria-hidden": "true",
1325
- children: [
1326
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "0", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
1327
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "10", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
1328
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "0", y1: "6", x2: "9", y2: "6", stroke: "white", strokeOpacity: "0.35", strokeWidth: "0.05" }),
1329
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "6", r: "1.5", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
1330
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "6", r: "0.12", fill: "white", fillOpacity: "0.5" }),
1331
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "1.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
1332
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "10.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
1333
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 3.086,2 A 1.5,1.5 0 0 0 5.914,2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
1334
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 3.086,10 A 1.5,1.5 0 0 1 5.914,10", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" })
1335
- ]
1336
- }
1337
- ),
1338
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 pointer-events-none", children: boardState.pieces.map((piece) => /* @__PURE__ */ jsxRuntime.jsx(
1339
- "div",
1340
- {
1341
- style: {
1342
- position: "absolute",
1343
- left: `${piece.pos.x / COLS2 * 100}%`,
1344
- top: `${(ROWS2 - 1 - piece.pos.y) / ROWS2 * 100}%`,
1345
- width: `${100 / COLS2}%`,
1346
- height: `${100 / ROWS2}%`
1347
- },
1348
- className: "flex items-center justify-center",
1349
- children: /* @__PURE__ */ jsxRuntime.jsx(
1350
- GamePiece,
1351
- {
1352
- piece,
1353
- isSelected: false,
1354
- hasBall: boardState.ball.holderId === piece.id,
1355
- onClick: () => {
1356
- }
1357
- }
1358
- )
1485
+ /* @__PURE__ */ jsxRuntime.jsx(PitchSurface, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 pointer-events-none", children: boardState.pieces.map((piece) => /* @__PURE__ */ jsxRuntime.jsx(
1486
+ "div",
1487
+ {
1488
+ style: {
1489
+ position: "absolute",
1490
+ left: `${piece.pos.x / COLS2 * 100}%`,
1491
+ top: `${(ROWS2 - 1 - piece.pos.y) / ROWS2 * 100}%`,
1492
+ width: `${100 / COLS2}%`,
1493
+ height: `${100 / ROWS2}%`
1359
1494
  },
1360
- piece.id
1361
- )) })
1362
- ] }),
1495
+ className: "flex items-center justify-center",
1496
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1497
+ GamePiece,
1498
+ {
1499
+ piece,
1500
+ isSelected: false,
1501
+ hasBall: boardState.ball.holderId === piece.id,
1502
+ onClick: () => {
1503
+ }
1504
+ }
1505
+ )
1506
+ },
1507
+ piece.id
1508
+ )) }) }),
1363
1509
  /* @__PURE__ */ jsxRuntime.jsx(
1364
1510
  "div",
1365
1511
  {
@@ -1569,23 +1715,33 @@ exports.ActionPoints = ActionPoints;
1569
1715
  exports.Avatar = Avatar;
1570
1716
  exports.AvatarFallback = AvatarFallback;
1571
1717
  exports.AvatarImage = AvatarImage;
1718
+ exports.BallHolderChip = BallHolderChip;
1572
1719
  exports.Button = Button;
1573
1720
  exports.ConfirmDialog = ConfirmDialog;
1574
1721
  exports.EventToast = EventToast;
1575
1722
  exports.Football = Football;
1576
1723
  exports.GAME_I18N_KEYS = GAME_I18N_KEYS;
1724
+ exports.GOAL_AREA_SHADE_CLASS = GOAL_AREA_SHADE_CLASS;
1577
1725
  exports.GameBoard = GameBoard;
1578
1726
  exports.GameControls = GameControls;
1579
1727
  exports.GameI18nProvider = GameI18nProvider;
1580
1728
  exports.GamePiece = GamePiece;
1729
+ exports.GrassOverlay = GrassOverlay;
1730
+ exports.KeyboardShortcutsList = KeyboardShortcutsList;
1581
1731
  exports.MiniScoreboard = MiniScoreboard;
1582
1732
  exports.MobileHistory = MobileHistory;
1583
1733
  exports.MoveHistory = MoveHistory;
1734
+ exports.PITCH_COLS = PITCH_COLS;
1735
+ exports.PITCH_ROWS = PITCH_ROWS;
1736
+ exports.PitchMarkings = PitchMarkings;
1737
+ exports.PitchSurface = PitchSurface;
1584
1738
  exports.Scoreboard = Scoreboard;
1585
1739
  exports.SelectedPieceDetail = SelectedPieceDetail;
1586
1740
  exports.StaticGameBoard = StaticGameBoard;
1587
1741
  exports.TurnBanner = TurnBanner;
1588
1742
  exports.cn = cn;
1743
+ exports.isGoalAreaSquare = isGoalAreaSquare;
1744
+ exports.pitchSquareClass = pitchSquareClass;
1589
1745
  exports.useGameT = useGameT;
1590
1746
  //# sourceMappingURL=index.cjs.map
1591
1747
  //# sourceMappingURL=index.cjs.map