@scriptonita/chess-football-ui 0.5.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
  );
@@ -128,6 +168,7 @@ var Football = ({ className }) => {
128
168
  var STATIC_I18N_KEYS = [
129
169
  "actionPointsAriaLabel",
130
170
  "actionPointsShort",
171
+ "ballHolder",
131
172
  "endTurn",
132
173
  "endTurnConfirm",
133
174
  "endTurnConfirmDescription",
@@ -183,10 +224,204 @@ function GameI18nProvider({ t, children }) {
183
224
  function useGameT() {
184
225
  return useContext(GameI18nContext);
185
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
+ }
186
417
  var COLS = 9;
187
418
  var ROWS = 12;
188
419
  var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
189
- 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 }) {
190
425
  const t = useGameT();
191
426
  const prefersReducedMotion = useReducedMotion();
192
427
  const {
@@ -218,6 +453,14 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
218
453
  setInvalidClickAt({ x, y, nonce: invalidClickNonceRef.current });
219
454
  };
220
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
+ });
221
464
  useEffect(() => {
222
465
  if (!invalidClickAt) return;
223
466
  const id = setTimeout(() => setInvalidClickAt(null), 100);
@@ -414,8 +657,6 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
414
657
  const squares = [];
415
658
  for (let y = ROWS - 1; y >= 0; y--) {
416
659
  for (let x = 0; x < COLS; x++) {
417
- const isGoalArea = x >= 2 && x <= 6 && (y >= 0 && y <= 1 || y >= 10 && y <= 11);
418
- const isEven = (x + y) % 2 === 0;
419
660
  const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
420
661
  const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
421
662
  const isAmbiguous = isValidMove && isValidPass;
@@ -437,9 +678,8 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
437
678
  },
438
679
  onMouseLeave: () => setHoveredPassAt(null),
439
680
  className: cn(
440
- "relative aspect-square w-full flex items-center justify-center cursor-pointer",
441
- isEven ? "bg-field-green-1" : "bg-field-green-2",
442
- isGoalArea && "bg-[#1a3a18] ring-1 ring-inset ring-emerald-500/30",
681
+ pitchSquareClass(x, y),
682
+ "flex items-center justify-center cursor-pointer",
443
683
  !isValidMove && !isValidPass && isLastMove && "bg-last-move-highlight/20",
444
684
  // §8: pulsing warning ring on enemy goal area
445
685
  isOffsideRisk && isEnemyGoalArea && !isValidMove && !isValidPass && "ring-2 ring-inset ring-warning/60",
@@ -508,7 +748,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
508
748
  }
509
749
  return squares;
510
750
  };
511
- const board = /* @__PURE__ */ jsx(
751
+ const board = /* @__PURE__ */ jsxs(
512
752
  "div",
513
753
  {
514
754
  className: cn(
@@ -525,26 +765,57 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
525
765
  "aria-label": "Game board \u2014 arrow keys to move the cursor, Enter to act",
526
766
  onKeyDown: handleKeyDown
527
767
  } : {},
528
- children: /* @__PURE__ */ jsxs("div", { className: "relative", style: { containerType: "inline-size" }, children: [
529
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-9 gap-[1px]", children: renderSquares() }),
530
- /* @__PURE__ */ jsxs(
531
- "svg",
532
- {
533
- viewBox: "0 0 9 12",
534
- preserveAspectRatio: "none",
535
- className: "absolute inset-0 w-full h-full pointer-events-none",
536
- "aria-hidden": "true",
537
- children: [
538
- /* @__PURE__ */ jsx("rect", { x: "2", y: "0", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
539
- /* @__PURE__ */ jsx("rect", { x: "2", y: "10", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
540
- /* @__PURE__ */ jsx("line", { x1: "0", y1: "6", x2: "9", y2: "6", stroke: "white", strokeOpacity: "0.35", strokeWidth: "0.05" }),
541
- /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "6", r: "1.5", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
542
- /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "6", r: "0.12", fill: "white", fillOpacity: "0.5" }),
543
- /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "1.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
544
- /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "10.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
545
- /* @__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" }),
546
- /* @__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" }),
547
- hoveredPassAt && passCarrier && /* @__PURE__ */ jsx(
768
+ children: [
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, {}),
771
+ keyboardNav && /* @__PURE__ */ jsxs(
772
+ "div",
773
+ {
774
+ ref: shortcutsRef,
775
+ className: "relative",
776
+ onKeyDown: (e) => e.stopPropagation(),
777
+ children: [
778
+ /* @__PURE__ */ jsx(
779
+ "button",
780
+ {
781
+ type: "button",
782
+ onClick: (e) => {
783
+ e.stopPropagation();
784
+ setShortcutsOpen((o) => !o);
785
+ },
786
+ "aria-label": t("shortcuts.buttonLabel"),
787
+ "aria-expanded": shortcutsOpen,
788
+ className: "w-8 h-8 flex items-center justify-center rounded-full bg-bg-secondary/80 border border-border-subtle text-fg-muted hover:text-fg-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-green",
789
+ children: /* @__PURE__ */ jsx(HelpCircle, { size: 16, strokeWidth: 2, "aria-hidden": "true" })
790
+ }
791
+ ),
792
+ shortcutsOpen && /* @__PURE__ */ jsxs(
793
+ "div",
794
+ {
795
+ className: "absolute top-full right-0 mt-1 z-50 w-max max-w-[220px] bg-bg-secondary border border-border-subtle rounded-md shadow-xl p-2.5 pointer-events-auto",
796
+ onClick: (e) => e.stopPropagation(),
797
+ children: [
798
+ /* @__PURE__ */ jsx("p", { className: "font-inter text-[11px] font-semibold text-fg-primary mb-1.5", children: t("shortcuts.title") }),
799
+ /* @__PURE__ */ jsx(KeyboardShortcutsList, {})
800
+ ]
801
+ }
802
+ )
803
+ ]
804
+ }
805
+ )
806
+ ] }),
807
+ /* @__PURE__ */ jsxs("div", { className: "relative", style: { containerType: "inline-size" }, children: [
808
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-9 gap-[1px]", children: renderSquares() }),
809
+ /* @__PURE__ */ jsx(GrassOverlay, {}),
810
+ /* @__PURE__ */ jsx(PitchMarkings, {}),
811
+ /* @__PURE__ */ jsx(
812
+ "svg",
813
+ {
814
+ viewBox: "0 0 9 12",
815
+ preserveAspectRatio: "none",
816
+ className: "absolute inset-0 w-full h-full pointer-events-none",
817
+ "aria-hidden": "true",
818
+ children: hoveredPassAt && passCarrier && /* @__PURE__ */ jsx(
548
819
  "line",
549
820
  {
550
821
  "data-testid": "pass-trajectory-line",
@@ -558,165 +829,110 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
558
829
  strokeLinecap: "round"
559
830
  }
560
831
  )
561
- ]
562
- }
563
- ),
564
- /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 pointer-events-none", children: [
565
- /* @__PURE__ */ jsx(AnimatePresence, { mode: "sync", children: boardState.pieces.map((piece) => {
566
- const isPickupCarrier = pickupCarrierId === piece.id;
567
- const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : { type: "spring", stiffness: 200, damping: 25, mass: 1 };
568
- return /* @__PURE__ */ jsx(
569
- motion.div,
570
- {
571
- layout: true,
572
- initial: false,
573
- animate: {
574
- left: `${piece.pos.x / COLS * 100}%`,
575
- top: `${(ROWS - 1 - piece.pos.y) / ROWS * 100}%`
832
+ }
833
+ ),
834
+ /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 pointer-events-none", "data-testid": "pieces-layer", children: [
835
+ /* @__PURE__ */ jsx(AnimatePresence, { mode: "sync", children: boardState.pieces.map((piece) => {
836
+ const isPickupCarrier = pickupCarrierId === piece.id;
837
+ const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : PIECE_SLIDE;
838
+ return /* @__PURE__ */ jsx(
839
+ motion.div,
840
+ {
841
+ "data-piece-id": piece.id,
842
+ initial: false,
843
+ animate: { x: cellX(piece.pos.x), y: cellY(piece.pos.y) },
844
+ transition: pieceTransition,
845
+ onAnimationStart: () => setMoving(piece.id, true),
846
+ onAnimationComplete: () => setMoving(piece.id, false),
847
+ style: {
848
+ position: "absolute",
849
+ left: 0,
850
+ top: 0,
851
+ width: `${100 / COLS}%`,
852
+ height: `${100 / ROWS}%`,
853
+ willChange: "transform"
854
+ },
855
+ className: "flex items-center justify-center pointer-events-auto",
856
+ children: /* @__PURE__ */ jsx(
857
+ GamePiece,
858
+ {
859
+ piece,
860
+ isSelected: selectedPieceId === piece.id,
861
+ hasBall: boardState.ball.holderId === piece.id,
862
+ isMoving: movingIds.has(piece.id),
863
+ onClick: (e) => handlePieceClick(piece.id, piece.pos.x, piece.pos.y, e)
864
+ }
865
+ )
576
866
  },
577
- transition: pieceTransition,
578
- style: {
579
- position: "absolute",
580
- width: `${100 / COLS}%`,
581
- height: `${100 / ROWS}%`
867
+ piece.id
868
+ );
869
+ }) }),
870
+ (() => {
871
+ const targetX = cellX(ball.pos.x);
872
+ const targetY = cellY(ball.pos.y);
873
+ let animate;
874
+ let transition;
875
+ if (isPickupOnMove && prevBall) {
876
+ const prevX = cellX(prevBall.pos.x);
877
+ const prevY = cellY(prevBall.pos.y);
878
+ const pulseStart = Math.max(0.05, pickupProportion - 0.06);
879
+ animate = {
880
+ x: [prevX, prevX, prevX, targetX],
881
+ y: [prevY, prevY, prevY, targetY],
882
+ scale: [1, 1, 1.35, 1]
883
+ };
884
+ transition = { duration: PICKUP_DURATION, times: [0, pulseStart, pickupProportion, 1], ease: "linear" };
885
+ } else {
886
+ animate = { x: targetX, y: targetY, scale: 1 };
887
+ transition = ball.holderId ? PIECE_SLIDE : BALL_SLIDE;
888
+ }
889
+ return /* @__PURE__ */ jsx(
890
+ motion.div,
891
+ {
892
+ initial: false,
893
+ animate,
894
+ transition,
895
+ style: {
896
+ position: "absolute",
897
+ left: 0,
898
+ top: 0,
899
+ width: `${100 / COLS}%`,
900
+ height: `${100 / ROWS}%`,
901
+ zIndex: 50,
902
+ willChange: "transform"
903
+ },
904
+ className: "flex items-center justify-center pointer-events-none",
905
+ children: /* @__PURE__ */ jsx("div", { className: "w-[60%] h-[60%]", children: /* @__PURE__ */ jsx(Football, {}) })
582
906
  },
583
- className: "flex items-center justify-center pointer-events-auto",
584
- children: /* @__PURE__ */ jsx(
585
- GamePiece,
586
- {
587
- piece,
588
- isSelected: selectedPieceId === piece.id,
589
- hasBall: boardState.ball.holderId === piece.id,
590
- onClick: (e) => handlePieceClick(piece.id, piece.pos.x, piece.pos.y, e)
591
- }
592
- )
593
- },
594
- piece.id
595
- );
596
- }) }),
597
- (() => {
598
- const targetX = ball.pos.x / COLS * 100;
599
- const targetY = (ROWS - 1 - ball.pos.y) / ROWS * 100;
600
- let animate;
601
- let transition;
602
- if (isPickupOnMove && prevBall) {
603
- const prevX = prevBall.pos.x / COLS * 100;
604
- const prevY = (ROWS - 1 - prevBall.pos.y) / ROWS * 100;
605
- const pulseStart = Math.max(0.05, pickupProportion - 0.06);
606
- animate = {
607
- left: [`${prevX}%`, `${prevX}%`, `${prevX}%`, `${targetX}%`],
608
- top: [`${prevY}%`, `${prevY}%`, `${prevY}%`, `${targetY}%`],
609
- scale: [1, 1, 1.35, 1]
610
- };
611
- transition = { duration: PICKUP_DURATION, times: [0, pulseStart, pickupProportion, 1], ease: "linear" };
612
- } else {
613
- animate = { left: `${targetX}%`, top: `${targetY}%`, scale: 1 };
614
- transition = { type: "spring", stiffness: 300, damping: 30, mass: 0.5 };
615
- }
616
- return /* @__PURE__ */ jsx(
907
+ "ball"
908
+ );
909
+ })(),
910
+ invalidClickAt && /* @__PURE__ */ jsx(
617
911
  motion.div,
618
912
  {
913
+ "data-testid": "invalid-action-shake",
914
+ "aria-hidden": "true",
619
915
  initial: false,
620
- animate,
621
- transition,
916
+ animate: prefersReducedMotion ? { opacity: [0.9, 0] } : { x: [0, -3, 3, -3, 3, 0] },
917
+ transition: { duration: 0.08 },
622
918
  style: {
623
919
  position: "absolute",
920
+ left: `${invalidClickAt.x / COLS * 100}%`,
921
+ top: `${(ROWS - 1 - invalidClickAt.y) / ROWS * 100}%`,
624
922
  width: `${100 / COLS}%`,
625
- height: `${100 / ROWS}%`,
626
- zIndex: 50
923
+ height: `${100 / ROWS}%`
627
924
  },
628
- className: "flex items-center justify-center pointer-events-none",
629
- children: /* @__PURE__ */ jsx("div", { className: "w-[60%] h-[60%]", children: /* @__PURE__ */ jsx(Football, {}) })
630
- },
631
- "ball"
632
- );
633
- })(),
634
- invalidClickAt && /* @__PURE__ */ jsx(
635
- motion.div,
636
- {
637
- "data-testid": "invalid-action-shake",
638
- "aria-hidden": "true",
639
- initial: false,
640
- animate: prefersReducedMotion ? { opacity: [0.9, 0] } : { x: [0, -3, 3, -3, 3, 0] },
641
- transition: { duration: 0.08 },
642
- style: {
643
- position: "absolute",
644
- left: `${invalidClickAt.x / COLS * 100}%`,
645
- top: `${(ROWS - 1 - invalidClickAt.y) / ROWS * 100}%`,
646
- width: `${100 / COLS}%`,
647
- height: `${100 / ROWS}%`
925
+ className: "ring-2 ring-inset ring-danger/70 rounded-sm"
648
926
  },
649
- className: "ring-2 ring-inset ring-danger/70 rounded-sm"
650
- },
651
- invalidClickAt.nonce
652
- )
653
- ] }),
654
- isOffsideRisk && /* @__PURE__ */ jsxs("div", { className: "absolute bottom-2 left-1/2 -translate-x-1/2 z-30 flex items-center gap-1.5 px-3 py-1.5 bg-warning/20 border border-warning/50 rounded-full font-inter text-xs font-semibold text-warning pointer-events-none whitespace-nowrap backdrop-blur-sm", children: [
655
- "\u26A0 ",
656
- t("offsideWarning")
657
- ] }),
658
- keyboardNav && /* @__PURE__ */ jsxs(
659
- "div",
660
- {
661
- ref: shortcutsRef,
662
- className: "hidden md:block absolute top-2 right-2 z-40",
663
- onKeyDown: (e) => e.stopPropagation(),
664
- children: [
665
- /* @__PURE__ */ jsx(
666
- "button",
667
- {
668
- type: "button",
669
- onClick: (e) => {
670
- e.stopPropagation();
671
- setShortcutsOpen((o) => !o);
672
- },
673
- "aria-label": t("shortcuts.buttonLabel"),
674
- "aria-expanded": shortcutsOpen,
675
- className: "w-11 h-11 flex items-center justify-center rounded-full bg-bg-secondary/80 border border-border-subtle text-fg-muted hover:text-fg-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-green backdrop-blur-sm",
676
- children: /* @__PURE__ */ jsx(HelpCircle, { size: 16, strokeWidth: 2, "aria-hidden": "true" })
677
- }
678
- ),
679
- shortcutsOpen && /* @__PURE__ */ jsxs(
680
- "div",
681
- {
682
- className: "absolute top-full right-0 mt-1 z-50 w-max max-w-[220px] bg-bg-secondary border border-border-subtle rounded-md shadow-xl p-2.5 pointer-events-auto",
683
- onClick: (e) => e.stopPropagation(),
684
- children: [
685
- /* @__PURE__ */ jsx("p", { className: "font-inter text-[11px] font-semibold text-fg-primary mb-1.5", children: t("shortcuts.title") }),
686
- /* @__PURE__ */ jsxs("ul", { className: "font-mono text-[10px] text-fg-secondary leading-relaxed", children: [
687
- /* @__PURE__ */ jsxs("li", { children: [
688
- /* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "\u2191\u2193\u2190\u2192" }),
689
- " ",
690
- t("shortcuts.arrows")
691
- ] }),
692
- /* @__PURE__ */ jsxs("li", { children: [
693
- /* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "Enter" }),
694
- " ",
695
- t("shortcuts.select")
696
- ] }),
697
- /* @__PURE__ */ jsxs("li", { children: [
698
- /* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "M" }),
699
- " ",
700
- t("shortcuts.move")
701
- ] }),
702
- /* @__PURE__ */ jsxs("li", { children: [
703
- /* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "P" }),
704
- " ",
705
- t("shortcuts.pass")
706
- ] }),
707
- /* @__PURE__ */ jsxs("li", { children: [
708
- /* @__PURE__ */ jsx("kbd", { className: "text-fg-primary", children: "Esc" }),
709
- " ",
710
- t("shortcuts.cancel")
711
- ] })
712
- ] })
713
- ]
714
- }
715
- )
716
- ]
717
- }
718
- )
719
- ] })
927
+ invalidClickAt.nonce
928
+ )
929
+ ] }),
930
+ isOffsideRisk && /* @__PURE__ */ jsxs("div", { className: "absolute bottom-2 left-1/2 -translate-x-1/2 z-30 flex items-center gap-1.5 px-3 py-1.5 bg-warning/20 border border-warning/50 rounded-full font-inter text-xs font-semibold text-warning pointer-events-none whitespace-nowrap backdrop-blur-sm", children: [
931
+ "\u26A0 ",
932
+ t("offsideWarning")
933
+ ] })
934
+ ] })
935
+ ]
720
936
  }
721
937
  );
722
938
  if (!showCoordinates) return board;
@@ -1244,77 +1460,31 @@ var COLS2 = 9;
1244
1460
  var ROWS2 = 12;
1245
1461
  function StaticGameBoard() {
1246
1462
  const boardState = useMemo(() => getInitialBoardState("white"), []);
1247
- const renderSquares = () => {
1248
- const squares = [];
1249
- for (let y = ROWS2 - 1; y >= 0; y--) {
1250
- for (let x = 0; x < COLS2; x++) {
1251
- const isGoalArea = x >= 2 && x <= 6 && (y >= 0 && y <= 1 || y >= 10 && y <= 11);
1252
- const isEven = (x + y) % 2 === 0;
1253
- squares.push(
1254
- /* @__PURE__ */ jsx(
1255
- "div",
1256
- {
1257
- className: cn(
1258
- "relative aspect-square w-full",
1259
- isEven ? "bg-[#2d5a27]" : "bg-[#356a2d]",
1260
- isGoalArea && "bg-[#1a3a18] ring-1 ring-inset ring-emerald-500/30"
1261
- )
1262
- },
1263
- `${x}-${y}`
1264
- )
1265
- );
1266
- }
1267
- }
1268
- return squares;
1269
- };
1270
1463
  return /* @__PURE__ */ jsxs("div", { className: "relative w-full overflow-hidden rounded-xl shadow-2xl border-4 border-[#1a3317] bg-[#1a3317]", children: [
1271
- /* @__PURE__ */ jsxs("div", { className: "relative", style: { containerType: "inline-size" }, children: [
1272
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-9 gap-[1px]", children: renderSquares() }),
1273
- /* @__PURE__ */ jsxs(
1274
- "svg",
1275
- {
1276
- viewBox: "0 0 9 12",
1277
- preserveAspectRatio: "none",
1278
- className: "absolute inset-0 w-full h-full pointer-events-none",
1279
- "aria-hidden": "true",
1280
- children: [
1281
- /* @__PURE__ */ jsx("rect", { x: "2", y: "0", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
1282
- /* @__PURE__ */ jsx("rect", { x: "2", y: "10", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
1283
- /* @__PURE__ */ jsx("line", { x1: "0", y1: "6", x2: "9", y2: "6", stroke: "white", strokeOpacity: "0.35", strokeWidth: "0.05" }),
1284
- /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "6", r: "1.5", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
1285
- /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "6", r: "0.12", fill: "white", fillOpacity: "0.5" }),
1286
- /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "1.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
1287
- /* @__PURE__ */ jsx("circle", { cx: "4.5", cy: "10.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
1288
- /* @__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" }),
1289
- /* @__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" })
1290
- ]
1291
- }
1292
- ),
1293
- /* @__PURE__ */ jsx("div", { className: "absolute inset-0 pointer-events-none", children: boardState.pieces.map((piece) => /* @__PURE__ */ jsx(
1294
- "div",
1295
- {
1296
- style: {
1297
- position: "absolute",
1298
- left: `${piece.pos.x / COLS2 * 100}%`,
1299
- top: `${(ROWS2 - 1 - piece.pos.y) / ROWS2 * 100}%`,
1300
- width: `${100 / COLS2}%`,
1301
- height: `${100 / ROWS2}%`
1302
- },
1303
- className: "flex items-center justify-center",
1304
- children: /* @__PURE__ */ jsx(
1305
- GamePiece,
1306
- {
1307
- piece,
1308
- isSelected: false,
1309
- hasBall: boardState.ball.holderId === piece.id,
1310
- onClick: () => {
1311
- }
1312
- }
1313
- )
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}%`
1314
1473
  },
1315
- piece.id
1316
- )) })
1317
- ] }),
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
+ )) }) }),
1318
1488
  /* @__PURE__ */ jsx(
1319
1489
  "div",
1320
1490
  {
@@ -1520,6 +1690,6 @@ function HistoryChip({ lastMove, onClick, className }) {
1520
1690
  );
1521
1691
  }
1522
1692
 
1523
- 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 };
1524
1694
  //# sourceMappingURL=index.js.map
1525
1695
  //# sourceMappingURL=index.js.map