@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.js CHANGED
@@ -53,26 +53,66 @@ var BLACK_PIECE_STYLE = {
53
53
  border: "1px solid rgba(0,0,0,0.55)",
54
54
  color: "#fafafa"
55
55
  };
56
+ var ON_BOARD_CHIP_STYLE = {
57
+ white: {
58
+ ...WHITE_PIECE_STYLE,
59
+ boxShadow: [
60
+ "inset 0 1.5px 1px rgba(255,255,255,0.95)",
61
+ "inset 0 -2px 3px rgba(0,0,0,0.18)",
62
+ "0 1px 1px rgba(0,0,0,0.35)"
63
+ ].join(", ")
64
+ },
65
+ black: {
66
+ ...BLACK_PIECE_STYLE,
67
+ boxShadow: [
68
+ "inset 0 1.5px 1px rgba(255,255,255,0.22)",
69
+ "inset 0 -2px 3px rgba(0,0,0,0.6)",
70
+ "0 1px 1px rgba(0,0,0,0.5)"
71
+ ].join(", ")
72
+ }
73
+ };
56
74
  var MOVED_STYLE = {
57
75
  filter: "saturate(0.15) brightness(0.75)"
58
76
  };
59
- function GamePiece({ piece, isSelected, hasBall, onClick }) {
60
- const baseStyle = piece.side === "white" ? WHITE_PIECE_STYLE : BLACK_PIECE_STYLE;
61
- const style = piece.hasMovedThisTurn ? { ...baseStyle, ...MOVED_STYLE } : baseStyle;
77
+ function GamePiece({ piece, isSelected, hasBall, onClick, isMoving = false }) {
78
+ const chipStyle = ON_BOARD_CHIP_STYLE[piece.side];
62
79
  return /* @__PURE__ */ jsxs(
63
80
  "div",
64
81
  {
65
82
  onClick,
66
- style,
83
+ style: piece.hasMovedThisTurn ? MOVED_STYLE : void 0,
67
84
  className: cn(
68
- "relative w-[88%] h-[88%] flex items-center justify-center rounded-full cursor-pointer z-10 transition-transform motion-reduce:transition-none",
85
+ "relative w-[88%] h-[88%] rounded-full cursor-pointer z-10 transition-transform duration-200 ease-out motion-reduce:transition-none",
69
86
  isSelected && "ring-4 ring-primary scale-110 z-30",
87
+ isMoving && "scale-[1.08] z-40",
70
88
  piece.hasMovedThisTurn && "opacity-70"
71
89
  ),
72
90
  children: [
73
- /* @__PURE__ */ jsx(PieceIcon, { type: piece.type, side: piece.side }),
74
- hasBall && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 rounded-full ring-2 ring-orange-400 pointer-events-none" }),
75
- isSelected && /* @__PURE__ */ 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" })
91
+ /* @__PURE__ */ jsx(
92
+ "div",
93
+ {
94
+ "aria-hidden": "true",
95
+ "data-testid": "piece-shadow",
96
+ "data-lifted": isMoving ? "true" : "false",
97
+ className: cn(
98
+ "absolute inset-0 rounded-full bg-black pointer-events-none transition-all duration-200 ease-out motion-reduce:transition-none",
99
+ 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]"
100
+ )
101
+ }
102
+ ),
103
+ /* @__PURE__ */ jsxs(
104
+ "div",
105
+ {
106
+ "data-testid": "piece-chip",
107
+ style: chipStyle,
108
+ className: "absolute inset-0 rounded-full flex items-center justify-center",
109
+ children: [
110
+ /* @__PURE__ */ jsx(PieceIcon, { type: piece.type, side: piece.side }),
111
+ hasBall && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 rounded-full ring-2 ring-orange-400 pointer-events-none" }),
112
+ isSelected && /* @__PURE__ */ 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" })
113
+ ]
114
+ }
115
+ )
76
116
  ]
77
117
  }
78
118
  );
@@ -184,10 +224,204 @@ function GameI18nProvider({ t, children }) {
184
224
  function useGameT() {
185
225
  return useContext(GameI18nContext);
186
226
  }
227
+ function BallHolderChip({ className }) {
228
+ const t = useGameT();
229
+ const boardState = useGameStore((s) => s.boardState);
230
+ const holder = boardState?.pieces.find((p) => p.id === boardState.ball.holderId) ?? null;
231
+ if (!holder) return null;
232
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-1.5 min-h-[1.75rem]", className), "aria-live": "polite", children: [
233
+ /* @__PURE__ */ jsxs(
234
+ "div",
235
+ {
236
+ style: holder.side === "white" ? WHITE_PIECE_STYLE : BLACK_PIECE_STYLE,
237
+ className: "relative w-7 h-7 flex items-center justify-center rounded-full shrink-0",
238
+ children: [
239
+ /* @__PURE__ */ jsx(PieceIcon, { type: holder.type, side: holder.side }),
240
+ /* @__PURE__ */ 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__ */ jsx("div", { className: "w-2.5 h-2.5", children: /* @__PURE__ */ jsx(Football, {}) }) })
241
+ ]
242
+ }
243
+ ),
244
+ /* @__PURE__ */ jsxs("span", { className: "font-inter text-[11px] text-fg-secondary", children: [
245
+ t("ballHolder"),
246
+ ": ",
247
+ t(`pieces.${holder.type}`)
248
+ ] })
249
+ ] });
250
+ }
251
+ function KeyboardShortcutsList({ className }) {
252
+ const t = useGameT();
253
+ return /* @__PURE__ */ jsxs("ul", { className: cn("font-mono text-[10px] text-fg-secondary leading-relaxed", className), children: [
254
+ /* @__PURE__ */ jsxs("li", { children: [
255
+ /* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "\u2191\u2193\u2190\u2192" }),
256
+ " ",
257
+ t("shortcuts.arrows")
258
+ ] }),
259
+ /* @__PURE__ */ jsxs("li", { children: [
260
+ /* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "Enter" }),
261
+ " ",
262
+ t("shortcuts.select")
263
+ ] }),
264
+ /* @__PURE__ */ jsxs("li", { children: [
265
+ /* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "M" }),
266
+ " ",
267
+ t("shortcuts.move")
268
+ ] }),
269
+ /* @__PURE__ */ jsxs("li", { children: [
270
+ /* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "P" }),
271
+ " ",
272
+ t("shortcuts.pass")
273
+ ] }),
274
+ /* @__PURE__ */ jsxs("li", { children: [
275
+ /* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "Esc" }),
276
+ " ",
277
+ t("shortcuts.cancel")
278
+ ] })
279
+ ] });
280
+ }
281
+ var PITCH_COLS = 9;
282
+ var PITCH_ROWS = 12;
283
+ var isGoalAreaSquare = (x, y) => x >= 2 && x <= 6 && (y >= 0 && y <= 1 || y >= PITCH_ROWS - 2 && y <= PITCH_ROWS - 1);
284
+ var GOAL_AREA_SHADE_CLASS = "shadow-[inset_0_0_0_100vmax_rgba(0,0,0,0.32)]";
285
+ function pitchSquareClass(x, y) {
286
+ const isEven = (x + y) % 2 === 0;
287
+ return cn(
288
+ "relative aspect-square w-full",
289
+ isEven ? "bg-field-green-1" : "bg-field-green-2",
290
+ isGoalAreaSquare(x, y) && GOAL_AREA_SHADE_CLASS
291
+ );
292
+ }
293
+ 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>';
294
+ var GRAIN_TILE_URL = `url("data:image/svg+xml;utf8,${encodeURIComponent(GRAIN_TILE_SVG)}")`;
295
+ 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%)";
296
+ function GrassOverlay({ className }) {
297
+ return /* @__PURE__ */ jsxs(
298
+ "div",
299
+ {
300
+ "aria-hidden": "true",
301
+ "data-testid": "grass-overlay",
302
+ className: cn("absolute inset-0 pointer-events-none overflow-hidden", className),
303
+ children: [
304
+ /* @__PURE__ */ jsx(
305
+ "div",
306
+ {
307
+ "data-layer": "grain",
308
+ className: "absolute inset-0 mix-blend-overlay opacity-60",
309
+ style: { backgroundImage: GRAIN_TILE_URL, backgroundSize: "160px 160px" }
310
+ }
311
+ ),
312
+ /* @__PURE__ */ jsx("div", { "data-layer": "light", className: "absolute inset-0", style: { background: VIGNETTE } })
313
+ ]
314
+ }
315
+ );
316
+ }
317
+ var CHALK = "#f6f5ee";
318
+ function PitchMarkings({ className, children }) {
319
+ const id = useId();
320
+ const filterId = `${id}-chalk`;
321
+ return /* @__PURE__ */ jsxs(
322
+ "svg",
323
+ {
324
+ viewBox: "0 0 9 12",
325
+ preserveAspectRatio: "none",
326
+ className: cn("absolute inset-0 w-full h-full pointer-events-none", className),
327
+ "aria-hidden": "true",
328
+ "data-testid": "pitch-markings",
329
+ children: [
330
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs(
331
+ "filter",
332
+ {
333
+ id: filterId,
334
+ filterUnits: "userSpaceOnUse",
335
+ x: "-0.5",
336
+ y: "-0.5",
337
+ width: "10",
338
+ height: "13",
339
+ colorInterpolationFilters: "sRGB",
340
+ children: [
341
+ /* @__PURE__ */ jsx("feTurbulence", { type: "fractalNoise", baseFrequency: "5.5", numOctaves: "2", seed: "3", result: "edgeNoise" }),
342
+ /* @__PURE__ */ jsx(
343
+ "feDisplacementMap",
344
+ {
345
+ in: "SourceGraphic",
346
+ in2: "edgeNoise",
347
+ scale: "0.045",
348
+ xChannelSelector: "R",
349
+ yChannelSelector: "G",
350
+ result: "rough"
351
+ }
352
+ ),
353
+ /* @__PURE__ */ jsx("feTurbulence", { type: "fractalNoise", baseFrequency: "14", numOctaves: "3", seed: "11", result: "dust" }),
354
+ /* @__PURE__ */ jsx(
355
+ "feColorMatrix",
356
+ {
357
+ in: "dust",
358
+ type: "matrix",
359
+ values: "0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0.75 0 0 0 0.55",
360
+ result: "dustMask"
361
+ }
362
+ ),
363
+ /* @__PURE__ */ jsx("feComposite", { in: "rough", in2: "dustMask", operator: "arithmetic", k1: "1", k2: "0", k3: "0", k4: "0" })
364
+ ]
365
+ }
366
+ ) }),
367
+ /* @__PURE__ */ jsx("g", { "data-layer": "halo", fill: "none", stroke: CHALK, strokeOpacity: "0.09", strokeWidth: "0.16", strokeLinecap: "round", children: /* @__PURE__ */ jsx(Markings, {}) }),
368
+ /* @__PURE__ */ jsx(
369
+ "g",
370
+ {
371
+ "data-layer": "chalk",
372
+ filter: `url(#${filterId})`,
373
+ fill: "none",
374
+ stroke: CHALK,
375
+ strokeOpacity: "0.82",
376
+ strokeWidth: "0.07",
377
+ strokeLinecap: "round",
378
+ strokeLinejoin: "round",
379
+ children: /* @__PURE__ */ jsx(Markings, {})
380
+ }
381
+ ),
382
+ children
383
+ ]
384
+ }
385
+ );
386
+ }
387
+ function Markings() {
388
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
389
+ /* @__PURE__ */ jsx("rect", { x: "0.06", y: "0.06", width: "8.88", height: "11.88" }),
390
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "0.06", width: "5", height: "1.94" }),
391
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "10", width: "5", height: "1.94" }),
392
+ /* @__PURE__ */ jsx("line", { x1: "0.06", y1: "6", x2: "8.94", y2: "6" }),
393
+ /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "6", r: "1.5" }),
394
+ /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "6", r: "0.09", fill: CHALK, fillOpacity: "0.9", stroke: "none" }),
395
+ /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "1.5", r: "0.08", fill: CHALK, fillOpacity: "0.9", stroke: "none" }),
396
+ /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "10.5", r: "0.08", fill: CHALK, fillOpacity: "0.9", stroke: "none" }),
397
+ /* @__PURE__ */ jsx("path", { d: "M 3.086,2 A 1.5,1.5 0 0 0 5.914,2" }),
398
+ /* @__PURE__ */ jsx("path", { d: "M 3.086,10 A 1.5,1.5 0 0 1 5.914,10" })
399
+ ] });
400
+ }
401
+ function PitchSurface({ className, squareClassName, children }) {
402
+ const squares = [];
403
+ for (let y = PITCH_ROWS - 1; y >= 0; y--) {
404
+ for (let x = 0; x < PITCH_COLS; x++) {
405
+ squares.push(
406
+ /* @__PURE__ */ jsx("div", { className: cn(pitchSquareClass(x, y), squareClassName?.(x, y)) }, `${x}-${y}`)
407
+ );
408
+ }
409
+ }
410
+ return /* @__PURE__ */ jsxs("div", { className: cn("relative", className), style: { containerType: "inline-size" }, children: [
411
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-9 gap-[1px]", "data-testid": "pitch-squares", children: squares }),
412
+ /* @__PURE__ */ jsx(GrassOverlay, {}),
413
+ /* @__PURE__ */ jsx(PitchMarkings, {}),
414
+ children
415
+ ] });
416
+ }
187
417
  var COLS = 9;
188
418
  var ROWS = 12;
189
419
  var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
190
- function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
420
+ var cellX = (x) => `${x * 100}%`;
421
+ var cellY = (y) => `${(ROWS - 1 - y) * 100}%`;
422
+ var PIECE_SLIDE = { type: "spring", stiffness: 190, damping: 28, mass: 1 };
423
+ var BALL_SLIDE = { type: "spring", stiffness: 300, damping: 26, mass: 0.6 };
424
+ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, toolbar = true }) {
191
425
  const t = useGameT();
192
426
  const prefersReducedMotion = useReducedMotion();
193
427
  const {
@@ -219,6 +453,14 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
219
453
  setInvalidClickAt({ x, y, nonce: invalidClickNonceRef.current });
220
454
  };
221
455
  const [hoveredPassAt, setHoveredPassAt] = useState(null);
456
+ const [movingIds, setMovingIds] = useState(() => /* @__PURE__ */ new Set());
457
+ const setMoving = (id, moving) => setMovingIds((prev) => {
458
+ if (prev.has(id) === moving) return prev;
459
+ const next = new Set(prev);
460
+ if (moving) next.add(id);
461
+ else next.delete(id);
462
+ return next;
463
+ });
222
464
  useEffect(() => {
223
465
  if (!invalidClickAt) return;
224
466
  const id = setTimeout(() => setInvalidClickAt(null), 100);
@@ -273,7 +515,6 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
273
515
  return [];
274
516
  }, [boardState, selectedPieceId]);
275
517
  if (!boardState) return null;
276
- const ballHolderPiece = boardState.pieces.find((p) => p.id === boardState.ball.holderId) ?? null;
277
518
  const lastMove = boardState.lastMove;
278
519
  const ball = boardState.ball;
279
520
  const ballMoved = !!prevBall && (prevBall.pos.x !== ball.pos.x || prevBall.pos.y !== ball.pos.y);
@@ -416,8 +657,6 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
416
657
  const squares = [];
417
658
  for (let y = ROWS - 1; y >= 0; y--) {
418
659
  for (let x = 0; x < COLS; x++) {
419
- const isGoalArea = x >= 2 && x <= 6 && (y >= 0 && y <= 1 || y >= 10 && y <= 11);
420
- const isEven = (x + y) % 2 === 0;
421
660
  const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
422
661
  const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
423
662
  const isAmbiguous = isValidMove && isValidPass;
@@ -439,9 +678,8 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
439
678
  },
440
679
  onMouseLeave: () => setHoveredPassAt(null),
441
680
  className: cn(
442
- "relative aspect-square w-full flex items-center justify-center cursor-pointer",
443
- isEven ? "bg-field-green-1" : "bg-field-green-2",
444
- isGoalArea && "bg-[#1a3a18] ring-1 ring-inset ring-emerald-500/30",
681
+ pitchSquareClass(x, y),
682
+ "flex items-center justify-center cursor-pointer",
445
683
  !isValidMove && !isValidPass && isLastMove && "bg-last-move-highlight/20",
446
684
  // §8: pulsing warning ring on enemy goal area
447
685
  isOffsideRisk && isEnemyGoalArea && !isValidMove && !isValidPass && "ring-2 ring-inset ring-warning/60",
@@ -528,25 +766,8 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
528
766
  onKeyDown: handleKeyDown
529
767
  } : {},
530
768
  children: [
531
- /* @__PURE__ */ jsxs("div", { className: "hidden md:flex items-center justify-between gap-2 px-2.5 py-1", children: [
532
- /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1.5 min-h-[1.75rem]", "aria-live": "polite", children: ballHolderPiece && /* @__PURE__ */ jsxs(Fragment, { children: [
533
- /* @__PURE__ */ jsxs(
534
- "div",
535
- {
536
- style: ballHolderPiece.side === "white" ? WHITE_PIECE_STYLE : BLACK_PIECE_STYLE,
537
- className: "relative w-7 h-7 flex items-center justify-center rounded-full shrink-0",
538
- children: [
539
- /* @__PURE__ */ jsx(PieceIcon, { type: ballHolderPiece.type, side: ballHolderPiece.side }),
540
- /* @__PURE__ */ 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__ */ jsx("div", { className: "w-2.5 h-2.5", children: /* @__PURE__ */ jsx(Football, {}) }) })
541
- ]
542
- }
543
- ),
544
- /* @__PURE__ */ jsxs("span", { className: "font-inter text-[11px] text-fg-secondary", children: [
545
- t("ballHolder"),
546
- ": ",
547
- t(`pieces.${ballHolderPiece.type}`)
548
- ] })
549
- ] }) }),
769
+ toolbar && /* @__PURE__ */ jsxs("div", { className: "hidden md:flex items-center justify-between gap-2 px-2.5 py-1", "data-testid": "board-toolbar", children: [
770
+ /* @__PURE__ */ jsx(BallHolderChip, {}),
550
771
  keyboardNav && /* @__PURE__ */ jsxs(
551
772
  "div",
552
773
  {
@@ -575,33 +796,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
575
796
  onClick: (e) => e.stopPropagation(),
576
797
  children: [
577
798
  /* @__PURE__ */ jsx("p", { className: "font-inter text-[11px] font-semibold text-fg-primary mb-1.5", children: t("shortcuts.title") }),
578
- /* @__PURE__ */ jsxs("ul", { className: "font-mono text-[10px] text-fg-secondary leading-relaxed", children: [
579
- /* @__PURE__ */ jsxs("li", { children: [
580
- /* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "\u2191\u2193\u2190\u2192" }),
581
- " ",
582
- t("shortcuts.arrows")
583
- ] }),
584
- /* @__PURE__ */ jsxs("li", { children: [
585
- /* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "Enter" }),
586
- " ",
587
- t("shortcuts.select")
588
- ] }),
589
- /* @__PURE__ */ jsxs("li", { children: [
590
- /* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "M" }),
591
- " ",
592
- t("shortcuts.move")
593
- ] }),
594
- /* @__PURE__ */ jsxs("li", { children: [
595
- /* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "P" }),
596
- " ",
597
- t("shortcuts.pass")
598
- ] }),
599
- /* @__PURE__ */ jsxs("li", { children: [
600
- /* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "Esc" }),
601
- " ",
602
- t("shortcuts.cancel")
603
- ] })
604
- ] })
799
+ /* @__PURE__ */ jsx(KeyboardShortcutsList, {})
605
800
  ]
606
801
  }
607
802
  )
@@ -611,58 +806,51 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
611
806
  ] }),
612
807
  /* @__PURE__ */ jsxs("div", { className: "relative", style: { containerType: "inline-size" }, children: [
613
808
  /* @__PURE__ */ jsx("div", { className: "grid grid-cols-9 gap-[1px]", children: renderSquares() }),
614
- /* @__PURE__ */ jsxs(
809
+ /* @__PURE__ */ jsx(GrassOverlay, {}),
810
+ /* @__PURE__ */ jsx(PitchMarkings, {}),
811
+ /* @__PURE__ */ jsx(
615
812
  "svg",
616
813
  {
617
814
  viewBox: "0 0 9 12",
618
815
  preserveAspectRatio: "none",
619
816
  className: "absolute inset-0 w-full h-full pointer-events-none",
620
817
  "aria-hidden": "true",
621
- children: [
622
- /* @__PURE__ */ jsx("rect", { x: "2", y: "0", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
623
- /* @__PURE__ */ jsx("rect", { x: "2", y: "10", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
624
- /* @__PURE__ */ jsx("line", { x1: "0", y1: "6", x2: "9", y2: "6", stroke: "white", strokeOpacity: "0.35", strokeWidth: "0.05" }),
625
- /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "6", r: "1.5", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
626
- /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "6", r: "0.12", fill: "white", fillOpacity: "0.5" }),
627
- /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "1.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
628
- /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "10.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
629
- /* @__PURE__ */ 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" }),
630
- /* @__PURE__ */ 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" }),
631
- hoveredPassAt && passCarrier && /* @__PURE__ */ jsx(
632
- "line",
633
- {
634
- "data-testid": "pass-trajectory-line",
635
- x1: passCarrier.pos.x + 0.5,
636
- y1: ROWS - 1 - passCarrier.pos.y + 0.5,
637
- x2: hoveredPassAt.x + 0.5,
638
- y2: ROWS - 1 - hoveredPassAt.y + 0.5,
639
- stroke: trajectoryIntercepted ? "var(--danger)" : "var(--pass-highlight)",
640
- strokeWidth: "0.08",
641
- strokeDasharray: "0.15 0.1",
642
- strokeLinecap: "round"
643
- }
644
- )
645
- ]
818
+ children: hoveredPassAt && passCarrier && /* @__PURE__ */ jsx(
819
+ "line",
820
+ {
821
+ "data-testid": "pass-trajectory-line",
822
+ x1: passCarrier.pos.x + 0.5,
823
+ y1: ROWS - 1 - passCarrier.pos.y + 0.5,
824
+ x2: hoveredPassAt.x + 0.5,
825
+ y2: ROWS - 1 - hoveredPassAt.y + 0.5,
826
+ stroke: trajectoryIntercepted ? "var(--danger)" : "var(--pass-highlight)",
827
+ strokeWidth: "0.08",
828
+ strokeDasharray: "0.15 0.1",
829
+ strokeLinecap: "round"
830
+ }
831
+ )
646
832
  }
647
833
  ),
648
- /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 pointer-events-none", children: [
834
+ /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 pointer-events-none", "data-testid": "pieces-layer", children: [
649
835
  /* @__PURE__ */ jsx(AnimatePresence, { mode: "sync", children: boardState.pieces.map((piece) => {
650
836
  const isPickupCarrier = pickupCarrierId === piece.id;
651
- const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : { type: "spring", stiffness: 200, damping: 25, mass: 1 };
837
+ const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : PIECE_SLIDE;
652
838
  return /* @__PURE__ */ jsx(
653
839
  motion.div,
654
840
  {
655
- layout: true,
841
+ "data-piece-id": piece.id,
656
842
  initial: false,
657
- animate: {
658
- left: `${piece.pos.x / COLS * 100}%`,
659
- top: `${(ROWS - 1 - piece.pos.y) / ROWS * 100}%`
660
- },
843
+ animate: { x: cellX(piece.pos.x), y: cellY(piece.pos.y) },
661
844
  transition: pieceTransition,
845
+ onAnimationStart: () => setMoving(piece.id, true),
846
+ onAnimationComplete: () => setMoving(piece.id, false),
662
847
  style: {
663
848
  position: "absolute",
849
+ left: 0,
850
+ top: 0,
664
851
  width: `${100 / COLS}%`,
665
- height: `${100 / ROWS}%`
852
+ height: `${100 / ROWS}%`,
853
+ willChange: "transform"
666
854
  },
667
855
  className: "flex items-center justify-center pointer-events-auto",
668
856
  children: /* @__PURE__ */ jsx(
@@ -671,6 +859,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
671
859
  piece,
672
860
  isSelected: selectedPieceId === piece.id,
673
861
  hasBall: boardState.ball.holderId === piece.id,
862
+ isMoving: movingIds.has(piece.id),
674
863
  onClick: (e) => handlePieceClick(piece.id, piece.pos.x, piece.pos.y, e)
675
864
  }
676
865
  )
@@ -679,23 +868,23 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
679
868
  );
680
869
  }) }),
681
870
  (() => {
682
- const targetX = ball.pos.x / COLS * 100;
683
- const targetY = (ROWS - 1 - ball.pos.y) / ROWS * 100;
871
+ const targetX = cellX(ball.pos.x);
872
+ const targetY = cellY(ball.pos.y);
684
873
  let animate;
685
874
  let transition;
686
875
  if (isPickupOnMove && prevBall) {
687
- const prevX = prevBall.pos.x / COLS * 100;
688
- const prevY = (ROWS - 1 - prevBall.pos.y) / ROWS * 100;
876
+ const prevX = cellX(prevBall.pos.x);
877
+ const prevY = cellY(prevBall.pos.y);
689
878
  const pulseStart = Math.max(0.05, pickupProportion - 0.06);
690
879
  animate = {
691
- left: [`${prevX}%`, `${prevX}%`, `${prevX}%`, `${targetX}%`],
692
- top: [`${prevY}%`, `${prevY}%`, `${prevY}%`, `${targetY}%`],
880
+ x: [prevX, prevX, prevX, targetX],
881
+ y: [prevY, prevY, prevY, targetY],
693
882
  scale: [1, 1, 1.35, 1]
694
883
  };
695
884
  transition = { duration: PICKUP_DURATION, times: [0, pulseStart, pickupProportion, 1], ease: "linear" };
696
885
  } else {
697
- animate = { left: `${targetX}%`, top: `${targetY}%`, scale: 1 };
698
- transition = { type: "spring", stiffness: 300, damping: 30, mass: 0.5 };
886
+ animate = { x: targetX, y: targetY, scale: 1 };
887
+ transition = ball.holderId ? PIECE_SLIDE : BALL_SLIDE;
699
888
  }
700
889
  return /* @__PURE__ */ jsx(
701
890
  motion.div,
@@ -705,9 +894,12 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
705
894
  transition,
706
895
  style: {
707
896
  position: "absolute",
897
+ left: 0,
898
+ top: 0,
708
899
  width: `${100 / COLS}%`,
709
900
  height: `${100 / ROWS}%`,
710
- zIndex: 50
901
+ zIndex: 50,
902
+ willChange: "transform"
711
903
  },
712
904
  className: "flex items-center justify-center pointer-events-none",
713
905
  children: /* @__PURE__ */ jsx("div", { className: "w-[60%] h-[60%]", children: /* @__PURE__ */ jsx(Football, {}) })
@@ -1268,77 +1460,31 @@ var COLS2 = 9;
1268
1460
  var ROWS2 = 12;
1269
1461
  function StaticGameBoard() {
1270
1462
  const boardState = useMemo(() => getInitialBoardState("white"), []);
1271
- const renderSquares = () => {
1272
- const squares = [];
1273
- for (let y = ROWS2 - 1; y >= 0; y--) {
1274
- for (let x = 0; x < COLS2; x++) {
1275
- const isGoalArea = x >= 2 && x <= 6 && (y >= 0 && y <= 1 || y >= 10 && y <= 11);
1276
- const isEven = (x + y) % 2 === 0;
1277
- squares.push(
1278
- /* @__PURE__ */ jsx(
1279
- "div",
1280
- {
1281
- className: cn(
1282
- "relative aspect-square w-full",
1283
- isEven ? "bg-[#2d5a27]" : "bg-[#356a2d]",
1284
- isGoalArea && "bg-[#1a3a18] ring-1 ring-inset ring-emerald-500/30"
1285
- )
1286
- },
1287
- `${x}-${y}`
1288
- )
1289
- );
1290
- }
1291
- }
1292
- return squares;
1293
- };
1294
1463
  return /* @__PURE__ */ jsxs("div", { className: "relative w-full overflow-hidden rounded-xl shadow-2xl border-4 border-[#1a3317] bg-[#1a3317]", children: [
1295
- /* @__PURE__ */ jsxs("div", { className: "relative", style: { containerType: "inline-size" }, children: [
1296
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-9 gap-[1px]", children: renderSquares() }),
1297
- /* @__PURE__ */ jsxs(
1298
- "svg",
1299
- {
1300
- viewBox: "0 0 9 12",
1301
- preserveAspectRatio: "none",
1302
- className: "absolute inset-0 w-full h-full pointer-events-none",
1303
- "aria-hidden": "true",
1304
- children: [
1305
- /* @__PURE__ */ jsx("rect", { x: "2", y: "0", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
1306
- /* @__PURE__ */ jsx("rect", { x: "2", y: "10", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
1307
- /* @__PURE__ */ jsx("line", { x1: "0", y1: "6", x2: "9", y2: "6", stroke: "white", strokeOpacity: "0.35", strokeWidth: "0.05" }),
1308
- /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "6", r: "1.5", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
1309
- /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "6", r: "0.12", fill: "white", fillOpacity: "0.5" }),
1310
- /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "1.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
1311
- /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "10.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
1312
- /* @__PURE__ */ 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" }),
1313
- /* @__PURE__ */ 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" })
1314
- ]
1315
- }
1316
- ),
1317
- /* @__PURE__ */ jsx("div", { className: "absolute inset-0 pointer-events-none", children: boardState.pieces.map((piece) => /* @__PURE__ */ jsx(
1318
- "div",
1319
- {
1320
- style: {
1321
- position: "absolute",
1322
- left: `${piece.pos.x / COLS2 * 100}%`,
1323
- top: `${(ROWS2 - 1 - piece.pos.y) / ROWS2 * 100}%`,
1324
- width: `${100 / COLS2}%`,
1325
- height: `${100 / ROWS2}%`
1326
- },
1327
- className: "flex items-center justify-center",
1328
- children: /* @__PURE__ */ jsx(
1329
- GamePiece,
1330
- {
1331
- piece,
1332
- isSelected: false,
1333
- hasBall: boardState.ball.holderId === piece.id,
1334
- onClick: () => {
1335
- }
1336
- }
1337
- )
1464
+ /* @__PURE__ */ jsx(PitchSurface, { children: /* @__PURE__ */ jsx("div", { className: "absolute inset-0 pointer-events-none", children: boardState.pieces.map((piece) => /* @__PURE__ */ jsx(
1465
+ "div",
1466
+ {
1467
+ style: {
1468
+ position: "absolute",
1469
+ left: `${piece.pos.x / COLS2 * 100}%`,
1470
+ top: `${(ROWS2 - 1 - piece.pos.y) / ROWS2 * 100}%`,
1471
+ width: `${100 / COLS2}%`,
1472
+ height: `${100 / ROWS2}%`
1338
1473
  },
1339
- piece.id
1340
- )) })
1341
- ] }),
1474
+ className: "flex items-center justify-center",
1475
+ children: /* @__PURE__ */ jsx(
1476
+ GamePiece,
1477
+ {
1478
+ piece,
1479
+ isSelected: false,
1480
+ hasBall: boardState.ball.holderId === piece.id,
1481
+ onClick: () => {
1482
+ }
1483
+ }
1484
+ )
1485
+ },
1486
+ piece.id
1487
+ )) }) }),
1342
1488
  /* @__PURE__ */ jsx(
1343
1489
  "div",
1344
1490
  {
@@ -1544,6 +1690,6 @@ function HistoryChip({ lastMove, onClick, className }) {
1544
1690
  );
1545
1691
  }
1546
1692
 
1547
- export { ActionPoints, Avatar, AvatarFallback, AvatarImage, Button, ConfirmDialog, EventToast, Football, GAME_I18N_KEYS, GameBoard, GameControls, GameI18nProvider, GamePiece, MiniScoreboard, MobileHistory, MoveHistory, Scoreboard, SelectedPieceDetail, StaticGameBoard, TurnBanner, cn, useGameT };
1693
+ export { ActionPoints, Avatar, AvatarFallback, AvatarImage, BallHolderChip, Button, ConfirmDialog, EventToast, Football, GAME_I18N_KEYS, GOAL_AREA_SHADE_CLASS, GameBoard, GameControls, GameI18nProvider, GamePiece, GrassOverlay, KeyboardShortcutsList, MiniScoreboard, MobileHistory, MoveHistory, PITCH_COLS, PITCH_ROWS, PitchMarkings, PitchSurface, Scoreboard, SelectedPieceDetail, StaticGameBoard, TurnBanner, cn, isGoalAreaSquare, pitchSquareClass, useGameT };
1548
1694
  //# sourceMappingURL=index.js.map
1549
1695
  //# sourceMappingURL=index.js.map