@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.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
  );
@@ -149,6 +189,7 @@ var Football = ({ className }) => {
149
189
  var STATIC_I18N_KEYS = [
150
190
  "actionPointsAriaLabel",
151
191
  "actionPointsShort",
192
+ "ballHolder",
152
193
  "endTurn",
153
194
  "endTurnConfirm",
154
195
  "endTurnConfirmDescription",
@@ -204,10 +245,204 @@ function GameI18nProvider({ t, children }) {
204
245
  function useGameT() {
205
246
  return React.useContext(GameI18nContext);
206
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
+ }
207
438
  var COLS = 9;
208
439
  var ROWS = 12;
209
440
  var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
210
- 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 }) {
211
446
  const t = useGameT();
212
447
  const prefersReducedMotion = framerMotion.useReducedMotion();
213
448
  const {
@@ -239,6 +474,14 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
239
474
  setInvalidClickAt({ x, y, nonce: invalidClickNonceRef.current });
240
475
  };
241
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
+ });
242
485
  React.useEffect(() => {
243
486
  if (!invalidClickAt) return;
244
487
  const id = setTimeout(() => setInvalidClickAt(null), 100);
@@ -435,8 +678,6 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
435
678
  const squares = [];
436
679
  for (let y = ROWS - 1; y >= 0; y--) {
437
680
  for (let x = 0; x < COLS; x++) {
438
- const isGoalArea = x >= 2 && x <= 6 && (y >= 0 && y <= 1 || y >= 10 && y <= 11);
439
- const isEven = (x + y) % 2 === 0;
440
681
  const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
441
682
  const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
442
683
  const isAmbiguous = isValidMove && isValidPass;
@@ -458,9 +699,8 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
458
699
  },
459
700
  onMouseLeave: () => setHoveredPassAt(null),
460
701
  className: cn(
461
- "relative aspect-square w-full flex items-center justify-center cursor-pointer",
462
- isEven ? "bg-field-green-1" : "bg-field-green-2",
463
- isGoalArea && "bg-[#1a3a18] ring-1 ring-inset ring-emerald-500/30",
702
+ pitchSquareClass(x, y),
703
+ "flex items-center justify-center cursor-pointer",
464
704
  !isValidMove && !isValidPass && isLastMove && "bg-last-move-highlight/20",
465
705
  // §8: pulsing warning ring on enemy goal area
466
706
  isOffsideRisk && isEnemyGoalArea && !isValidMove && !isValidPass && "ring-2 ring-inset ring-warning/60",
@@ -529,7 +769,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
529
769
  }
530
770
  return squares;
531
771
  };
532
- const board = /* @__PURE__ */ jsxRuntime.jsx(
772
+ const board = /* @__PURE__ */ jsxRuntime.jsxs(
533
773
  "div",
534
774
  {
535
775
  className: cn(
@@ -546,26 +786,57 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
546
786
  "aria-label": "Game board \u2014 arrow keys to move the cursor, Enter to act",
547
787
  onKeyDown: handleKeyDown
548
788
  } : {},
549
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", style: { containerType: "inline-size" }, children: [
550
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-9 gap-[1px]", children: renderSquares() }),
551
- /* @__PURE__ */ jsxRuntime.jsxs(
552
- "svg",
553
- {
554
- viewBox: "0 0 9 12",
555
- preserveAspectRatio: "none",
556
- className: "absolute inset-0 w-full h-full pointer-events-none",
557
- "aria-hidden": "true",
558
- children: [
559
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "0", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
560
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "10", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
561
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "0", y1: "6", x2: "9", y2: "6", stroke: "white", strokeOpacity: "0.35", strokeWidth: "0.05" }),
562
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "6", r: "1.5", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
563
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "6", r: "0.12", fill: "white", fillOpacity: "0.5" }),
564
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "1.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
565
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "10.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
566
- /* @__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" }),
567
- /* @__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" }),
568
- hoveredPassAt && passCarrier && /* @__PURE__ */ jsxRuntime.jsx(
789
+ children: [
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, {}),
792
+ keyboardNav && /* @__PURE__ */ jsxRuntime.jsxs(
793
+ "div",
794
+ {
795
+ ref: shortcutsRef,
796
+ className: "relative",
797
+ onKeyDown: (e) => e.stopPropagation(),
798
+ children: [
799
+ /* @__PURE__ */ jsxRuntime.jsx(
800
+ "button",
801
+ {
802
+ type: "button",
803
+ onClick: (e) => {
804
+ e.stopPropagation();
805
+ setShortcutsOpen((o) => !o);
806
+ },
807
+ "aria-label": t("shortcuts.buttonLabel"),
808
+ "aria-expanded": shortcutsOpen,
809
+ 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",
810
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.HelpCircle, { size: 16, strokeWidth: 2, "aria-hidden": "true" })
811
+ }
812
+ ),
813
+ shortcutsOpen && /* @__PURE__ */ jsxRuntime.jsxs(
814
+ "div",
815
+ {
816
+ 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",
817
+ onClick: (e) => e.stopPropagation(),
818
+ children: [
819
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-inter text-[11px] font-semibold text-fg-primary mb-1.5", children: t("shortcuts.title") }),
820
+ /* @__PURE__ */ jsxRuntime.jsx(KeyboardShortcutsList, {})
821
+ ]
822
+ }
823
+ )
824
+ ]
825
+ }
826
+ )
827
+ ] }),
828
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", style: { containerType: "inline-size" }, children: [
829
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-9 gap-[1px]", children: renderSquares() }),
830
+ /* @__PURE__ */ jsxRuntime.jsx(GrassOverlay, {}),
831
+ /* @__PURE__ */ jsxRuntime.jsx(PitchMarkings, {}),
832
+ /* @__PURE__ */ jsxRuntime.jsx(
833
+ "svg",
834
+ {
835
+ viewBox: "0 0 9 12",
836
+ preserveAspectRatio: "none",
837
+ className: "absolute inset-0 w-full h-full pointer-events-none",
838
+ "aria-hidden": "true",
839
+ children: hoveredPassAt && passCarrier && /* @__PURE__ */ jsxRuntime.jsx(
569
840
  "line",
570
841
  {
571
842
  "data-testid": "pass-trajectory-line",
@@ -579,165 +850,110 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true }) {
579
850
  strokeLinecap: "round"
580
851
  }
581
852
  )
582
- ]
583
- }
584
- ),
585
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 pointer-events-none", children: [
586
- /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { mode: "sync", children: boardState.pieces.map((piece) => {
587
- const isPickupCarrier = pickupCarrierId === piece.id;
588
- const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : { type: "spring", stiffness: 200, damping: 25, mass: 1 };
589
- return /* @__PURE__ */ jsxRuntime.jsx(
590
- framerMotion.motion.div,
591
- {
592
- layout: true,
593
- initial: false,
594
- animate: {
595
- left: `${piece.pos.x / COLS * 100}%`,
596
- top: `${(ROWS - 1 - piece.pos.y) / ROWS * 100}%`
853
+ }
854
+ ),
855
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 pointer-events-none", "data-testid": "pieces-layer", children: [
856
+ /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { mode: "sync", children: boardState.pieces.map((piece) => {
857
+ const isPickupCarrier = pickupCarrierId === piece.id;
858
+ const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : PIECE_SLIDE;
859
+ return /* @__PURE__ */ jsxRuntime.jsx(
860
+ framerMotion.motion.div,
861
+ {
862
+ "data-piece-id": piece.id,
863
+ initial: false,
864
+ animate: { x: cellX(piece.pos.x), y: cellY(piece.pos.y) },
865
+ transition: pieceTransition,
866
+ onAnimationStart: () => setMoving(piece.id, true),
867
+ onAnimationComplete: () => setMoving(piece.id, false),
868
+ style: {
869
+ position: "absolute",
870
+ left: 0,
871
+ top: 0,
872
+ width: `${100 / COLS}%`,
873
+ height: `${100 / ROWS}%`,
874
+ willChange: "transform"
875
+ },
876
+ className: "flex items-center justify-center pointer-events-auto",
877
+ children: /* @__PURE__ */ jsxRuntime.jsx(
878
+ GamePiece,
879
+ {
880
+ piece,
881
+ isSelected: selectedPieceId === piece.id,
882
+ hasBall: boardState.ball.holderId === piece.id,
883
+ isMoving: movingIds.has(piece.id),
884
+ onClick: (e) => handlePieceClick(piece.id, piece.pos.x, piece.pos.y, e)
885
+ }
886
+ )
597
887
  },
598
- transition: pieceTransition,
599
- style: {
600
- position: "absolute",
601
- width: `${100 / COLS}%`,
602
- height: `${100 / ROWS}%`
888
+ piece.id
889
+ );
890
+ }) }),
891
+ (() => {
892
+ const targetX = cellX(ball.pos.x);
893
+ const targetY = cellY(ball.pos.y);
894
+ let animate;
895
+ let transition;
896
+ if (isPickupOnMove && prevBall) {
897
+ const prevX = cellX(prevBall.pos.x);
898
+ const prevY = cellY(prevBall.pos.y);
899
+ const pulseStart = Math.max(0.05, pickupProportion - 0.06);
900
+ animate = {
901
+ x: [prevX, prevX, prevX, targetX],
902
+ y: [prevY, prevY, prevY, targetY],
903
+ scale: [1, 1, 1.35, 1]
904
+ };
905
+ transition = { duration: PICKUP_DURATION, times: [0, pulseStart, pickupProportion, 1], ease: "linear" };
906
+ } else {
907
+ animate = { x: targetX, y: targetY, scale: 1 };
908
+ transition = ball.holderId ? PIECE_SLIDE : BALL_SLIDE;
909
+ }
910
+ return /* @__PURE__ */ jsxRuntime.jsx(
911
+ framerMotion.motion.div,
912
+ {
913
+ initial: false,
914
+ animate,
915
+ transition,
916
+ style: {
917
+ position: "absolute",
918
+ left: 0,
919
+ top: 0,
920
+ width: `${100 / COLS}%`,
921
+ height: `${100 / ROWS}%`,
922
+ zIndex: 50,
923
+ willChange: "transform"
924
+ },
925
+ className: "flex items-center justify-center pointer-events-none",
926
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-[60%] h-[60%]", children: /* @__PURE__ */ jsxRuntime.jsx(Football, {}) })
603
927
  },
604
- className: "flex items-center justify-center pointer-events-auto",
605
- children: /* @__PURE__ */ jsxRuntime.jsx(
606
- GamePiece,
607
- {
608
- piece,
609
- isSelected: selectedPieceId === piece.id,
610
- hasBall: boardState.ball.holderId === piece.id,
611
- onClick: (e) => handlePieceClick(piece.id, piece.pos.x, piece.pos.y, e)
612
- }
613
- )
614
- },
615
- piece.id
616
- );
617
- }) }),
618
- (() => {
619
- const targetX = ball.pos.x / COLS * 100;
620
- const targetY = (ROWS - 1 - ball.pos.y) / ROWS * 100;
621
- let animate;
622
- let transition;
623
- if (isPickupOnMove && prevBall) {
624
- const prevX = prevBall.pos.x / COLS * 100;
625
- const prevY = (ROWS - 1 - prevBall.pos.y) / ROWS * 100;
626
- const pulseStart = Math.max(0.05, pickupProportion - 0.06);
627
- animate = {
628
- left: [`${prevX}%`, `${prevX}%`, `${prevX}%`, `${targetX}%`],
629
- top: [`${prevY}%`, `${prevY}%`, `${prevY}%`, `${targetY}%`],
630
- scale: [1, 1, 1.35, 1]
631
- };
632
- transition = { duration: PICKUP_DURATION, times: [0, pulseStart, pickupProportion, 1], ease: "linear" };
633
- } else {
634
- animate = { left: `${targetX}%`, top: `${targetY}%`, scale: 1 };
635
- transition = { type: "spring", stiffness: 300, damping: 30, mass: 0.5 };
636
- }
637
- return /* @__PURE__ */ jsxRuntime.jsx(
928
+ "ball"
929
+ );
930
+ })(),
931
+ invalidClickAt && /* @__PURE__ */ jsxRuntime.jsx(
638
932
  framerMotion.motion.div,
639
933
  {
934
+ "data-testid": "invalid-action-shake",
935
+ "aria-hidden": "true",
640
936
  initial: false,
641
- animate,
642
- transition,
937
+ animate: prefersReducedMotion ? { opacity: [0.9, 0] } : { x: [0, -3, 3, -3, 3, 0] },
938
+ transition: { duration: 0.08 },
643
939
  style: {
644
940
  position: "absolute",
941
+ left: `${invalidClickAt.x / COLS * 100}%`,
942
+ top: `${(ROWS - 1 - invalidClickAt.y) / ROWS * 100}%`,
645
943
  width: `${100 / COLS}%`,
646
- height: `${100 / ROWS}%`,
647
- zIndex: 50
944
+ height: `${100 / ROWS}%`
648
945
  },
649
- className: "flex items-center justify-center pointer-events-none",
650
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-[60%] h-[60%]", children: /* @__PURE__ */ jsxRuntime.jsx(Football, {}) })
651
- },
652
- "ball"
653
- );
654
- })(),
655
- invalidClickAt && /* @__PURE__ */ jsxRuntime.jsx(
656
- framerMotion.motion.div,
657
- {
658
- "data-testid": "invalid-action-shake",
659
- "aria-hidden": "true",
660
- initial: false,
661
- animate: prefersReducedMotion ? { opacity: [0.9, 0] } : { x: [0, -3, 3, -3, 3, 0] },
662
- transition: { duration: 0.08 },
663
- style: {
664
- position: "absolute",
665
- left: `${invalidClickAt.x / COLS * 100}%`,
666
- top: `${(ROWS - 1 - invalidClickAt.y) / ROWS * 100}%`,
667
- width: `${100 / COLS}%`,
668
- height: `${100 / ROWS}%`
946
+ className: "ring-2 ring-inset ring-danger/70 rounded-sm"
669
947
  },
670
- className: "ring-2 ring-inset ring-danger/70 rounded-sm"
671
- },
672
- invalidClickAt.nonce
673
- )
674
- ] }),
675
- isOffsideRisk && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute bottom-2 left-1/2 -translate-x-1/2 z-30 flex items-center gap-1.5 px-3 py-1.5 bg-warning/20 border border-warning/50 rounded-full font-inter text-xs font-semibold text-warning pointer-events-none whitespace-nowrap backdrop-blur-sm", children: [
676
- "\u26A0 ",
677
- t("offsideWarning")
678
- ] }),
679
- keyboardNav && /* @__PURE__ */ jsxRuntime.jsxs(
680
- "div",
681
- {
682
- ref: shortcutsRef,
683
- className: "hidden md:block absolute top-2 right-2 z-40",
684
- onKeyDown: (e) => e.stopPropagation(),
685
- children: [
686
- /* @__PURE__ */ jsxRuntime.jsx(
687
- "button",
688
- {
689
- type: "button",
690
- onClick: (e) => {
691
- e.stopPropagation();
692
- setShortcutsOpen((o) => !o);
693
- },
694
- "aria-label": t("shortcuts.buttonLabel"),
695
- "aria-expanded": shortcutsOpen,
696
- 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",
697
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.HelpCircle, { size: 16, strokeWidth: 2, "aria-hidden": "true" })
698
- }
699
- ),
700
- shortcutsOpen && /* @__PURE__ */ jsxRuntime.jsxs(
701
- "div",
702
- {
703
- 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",
704
- onClick: (e) => e.stopPropagation(),
705
- children: [
706
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-inter text-[11px] font-semibold text-fg-primary mb-1.5", children: t("shortcuts.title") }),
707
- /* @__PURE__ */ jsxRuntime.jsxs("ul", { className: "font-mono text-[10px] text-fg-secondary leading-relaxed", children: [
708
- /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
709
- /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "\u2191\u2193\u2190\u2192" }),
710
- " ",
711
- t("shortcuts.arrows")
712
- ] }),
713
- /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
714
- /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "Enter" }),
715
- " ",
716
- t("shortcuts.select")
717
- ] }),
718
- /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
719
- /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "M" }),
720
- " ",
721
- t("shortcuts.move")
722
- ] }),
723
- /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
724
- /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "P" }),
725
- " ",
726
- t("shortcuts.pass")
727
- ] }),
728
- /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
729
- /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-fg-primary", children: "Esc" }),
730
- " ",
731
- t("shortcuts.cancel")
732
- ] })
733
- ] })
734
- ]
735
- }
736
- )
737
- ]
738
- }
739
- )
740
- ] })
948
+ invalidClickAt.nonce
949
+ )
950
+ ] }),
951
+ isOffsideRisk && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute bottom-2 left-1/2 -translate-x-1/2 z-30 flex items-center gap-1.5 px-3 py-1.5 bg-warning/20 border border-warning/50 rounded-full font-inter text-xs font-semibold text-warning pointer-events-none whitespace-nowrap backdrop-blur-sm", children: [
952
+ "\u26A0 ",
953
+ t("offsideWarning")
954
+ ] })
955
+ ] })
956
+ ]
741
957
  }
742
958
  );
743
959
  if (!showCoordinates) return board;
@@ -1265,77 +1481,31 @@ var COLS2 = 9;
1265
1481
  var ROWS2 = 12;
1266
1482
  function StaticGameBoard() {
1267
1483
  const boardState = React.useMemo(() => chunkLTFNSTAQ_cjs.getInitialBoardState("white"), []);
1268
- const renderSquares = () => {
1269
- const squares = [];
1270
- for (let y = ROWS2 - 1; y >= 0; y--) {
1271
- for (let x = 0; x < COLS2; x++) {
1272
- const isGoalArea = x >= 2 && x <= 6 && (y >= 0 && y <= 1 || y >= 10 && y <= 11);
1273
- const isEven = (x + y) % 2 === 0;
1274
- squares.push(
1275
- /* @__PURE__ */ jsxRuntime.jsx(
1276
- "div",
1277
- {
1278
- className: cn(
1279
- "relative aspect-square w-full",
1280
- isEven ? "bg-[#2d5a27]" : "bg-[#356a2d]",
1281
- isGoalArea && "bg-[#1a3a18] ring-1 ring-inset ring-emerald-500/30"
1282
- )
1283
- },
1284
- `${x}-${y}`
1285
- )
1286
- );
1287
- }
1288
- }
1289
- return squares;
1290
- };
1291
1484
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full overflow-hidden rounded-xl shadow-2xl border-4 border-[#1a3317] bg-[#1a3317]", children: [
1292
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", style: { containerType: "inline-size" }, children: [
1293
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-9 gap-[1px]", children: renderSquares() }),
1294
- /* @__PURE__ */ jsxRuntime.jsxs(
1295
- "svg",
1296
- {
1297
- viewBox: "0 0 9 12",
1298
- preserveAspectRatio: "none",
1299
- className: "absolute inset-0 w-full h-full pointer-events-none",
1300
- "aria-hidden": "true",
1301
- children: [
1302
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "0", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
1303
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "10", width: "5", height: "2", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
1304
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "0", y1: "6", x2: "9", y2: "6", stroke: "white", strokeOpacity: "0.35", strokeWidth: "0.05" }),
1305
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "6", r: "1.5", fill: "none", stroke: "white", strokeOpacity: "0.3", strokeWidth: "0.05" }),
1306
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "6", r: "0.12", fill: "white", fillOpacity: "0.5" }),
1307
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "1.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
1308
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "10.5", r: "0.1", fill: "white", fillOpacity: "0.5" }),
1309
- /* @__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" }),
1310
- /* @__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" })
1311
- ]
1312
- }
1313
- ),
1314
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 pointer-events-none", children: boardState.pieces.map((piece) => /* @__PURE__ */ jsxRuntime.jsx(
1315
- "div",
1316
- {
1317
- style: {
1318
- position: "absolute",
1319
- left: `${piece.pos.x / COLS2 * 100}%`,
1320
- top: `${(ROWS2 - 1 - piece.pos.y) / ROWS2 * 100}%`,
1321
- width: `${100 / COLS2}%`,
1322
- height: `${100 / ROWS2}%`
1323
- },
1324
- className: "flex items-center justify-center",
1325
- children: /* @__PURE__ */ jsxRuntime.jsx(
1326
- GamePiece,
1327
- {
1328
- piece,
1329
- isSelected: false,
1330
- hasBall: boardState.ball.holderId === piece.id,
1331
- onClick: () => {
1332
- }
1333
- }
1334
- )
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}%`
1335
1494
  },
1336
- piece.id
1337
- )) })
1338
- ] }),
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
+ )) }) }),
1339
1509
  /* @__PURE__ */ jsxRuntime.jsx(
1340
1510
  "div",
1341
1511
  {
@@ -1545,23 +1715,33 @@ exports.ActionPoints = ActionPoints;
1545
1715
  exports.Avatar = Avatar;
1546
1716
  exports.AvatarFallback = AvatarFallback;
1547
1717
  exports.AvatarImage = AvatarImage;
1718
+ exports.BallHolderChip = BallHolderChip;
1548
1719
  exports.Button = Button;
1549
1720
  exports.ConfirmDialog = ConfirmDialog;
1550
1721
  exports.EventToast = EventToast;
1551
1722
  exports.Football = Football;
1552
1723
  exports.GAME_I18N_KEYS = GAME_I18N_KEYS;
1724
+ exports.GOAL_AREA_SHADE_CLASS = GOAL_AREA_SHADE_CLASS;
1553
1725
  exports.GameBoard = GameBoard;
1554
1726
  exports.GameControls = GameControls;
1555
1727
  exports.GameI18nProvider = GameI18nProvider;
1556
1728
  exports.GamePiece = GamePiece;
1729
+ exports.GrassOverlay = GrassOverlay;
1730
+ exports.KeyboardShortcutsList = KeyboardShortcutsList;
1557
1731
  exports.MiniScoreboard = MiniScoreboard;
1558
1732
  exports.MobileHistory = MobileHistory;
1559
1733
  exports.MoveHistory = MoveHistory;
1734
+ exports.PITCH_COLS = PITCH_COLS;
1735
+ exports.PITCH_ROWS = PITCH_ROWS;
1736
+ exports.PitchMarkings = PitchMarkings;
1737
+ exports.PitchSurface = PitchSurface;
1560
1738
  exports.Scoreboard = Scoreboard;
1561
1739
  exports.SelectedPieceDetail = SelectedPieceDetail;
1562
1740
  exports.StaticGameBoard = StaticGameBoard;
1563
1741
  exports.TurnBanner = TurnBanner;
1564
1742
  exports.cn = cn;
1743
+ exports.isGoalAreaSquare = isGoalAreaSquare;
1744
+ exports.pitchSquareClass = pitchSquareClass;
1565
1745
  exports.useGameT = useGameT;
1566
1746
  //# sourceMappingURL=index.cjs.map
1567
1747
  //# sourceMappingURL=index.cjs.map