@scriptonita/chess-football-ui 0.7.0 → 0.9.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/README.md +32 -0
- package/dist/{chunk-LTFNSTAQ.cjs → chunk-BSE4DUXM.cjs} +18 -5
- package/dist/chunk-BSE4DUXM.cjs.map +1 -0
- package/dist/{chunk-N24LGLLO.js → chunk-C5HCN7O5.js} +18 -6
- package/dist/chunk-C5HCN7O5.js.map +1 -0
- package/dist/index.cjs +382 -104
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -7
- package/dist/index.d.ts +77 -7
- package/dist/index.js +378 -103
- package/dist/index.js.map +1 -1
- package/dist/store.cjs +7 -3
- package/dist/store.d.cts +9 -1
- package/dist/store.d.ts +9 -1
- package/dist/store.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-LTFNSTAQ.cjs.map +0 -1
- package/dist/chunk-N24LGLLO.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useGameStore, getInitialBoardState } from './chunk-
|
|
1
|
+
import { useGameStore, DEFAULT_MAX_AP, getInitialBoardState } from './chunk-C5HCN7O5.js';
|
|
2
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
2
3
|
import { SHORT_KEY, LONG_KEY, getValidMoves, getValidPasses, isInEnemyArea, getPath, RANK_LABELS, FILE_LABELS, squareName } from '@scriptonita/chess-football-engine';
|
|
3
4
|
import { clsx } from 'clsx';
|
|
4
5
|
import { twMerge } from 'tailwind-merge';
|
|
@@ -83,7 +84,10 @@ function GamePiece({ piece, isSelected, hasBall, onClick, isMoving = false }) {
|
|
|
83
84
|
style: piece.hasMovedThisTurn ? MOVED_STYLE : void 0,
|
|
84
85
|
className: cn(
|
|
85
86
|
"relative w-[88%] h-[88%] rounded-full cursor-pointer z-10 transition-transform duration-200 ease-out motion-reduce:transition-none",
|
|
86
|
-
|
|
87
|
+
// `--primary` was the package's one remaining shadcn token, and
|
|
88
|
+
// CrazyGames had to define it by hand just to make the selection
|
|
89
|
+
// ring visible. It is part of the design system now.
|
|
90
|
+
isSelected && "ring-4 ring-accent-green scale-110 z-30",
|
|
87
91
|
isMoving && "scale-[1.08] z-40",
|
|
88
92
|
piece.hasMovedThisTurn && "opacity-70"
|
|
89
93
|
),
|
|
@@ -169,6 +173,7 @@ var STATIC_I18N_KEYS = [
|
|
|
169
173
|
"actionPointsAriaLabel",
|
|
170
174
|
"actionPointsShort",
|
|
171
175
|
"ballHolder",
|
|
176
|
+
"boardAriaLabel",
|
|
172
177
|
"endTurn",
|
|
173
178
|
"endTurnConfirm",
|
|
174
179
|
"endTurnConfirmDescription",
|
|
@@ -182,11 +187,13 @@ var STATIC_I18N_KEYS = [
|
|
|
182
187
|
"history.pass",
|
|
183
188
|
"history.tackle",
|
|
184
189
|
"history.title",
|
|
190
|
+
"history.close",
|
|
185
191
|
"history.turn",
|
|
186
192
|
"history.viewAll",
|
|
187
193
|
"move",
|
|
188
194
|
"offsideWarning",
|
|
189
195
|
"pass",
|
|
196
|
+
"scoreAriaLabel",
|
|
190
197
|
"selectedPiece.alreadyMoved",
|
|
191
198
|
"selectedPiece.atSquare",
|
|
192
199
|
"selectedPiece.empty",
|
|
@@ -198,6 +205,14 @@ var STATIC_I18N_KEYS = [
|
|
|
198
205
|
"shortcuts.pass",
|
|
199
206
|
"shortcuts.select",
|
|
200
207
|
"shortcuts.title",
|
|
208
|
+
"square.empty",
|
|
209
|
+
"square.emptyWithBall",
|
|
210
|
+
"square.moveOrPassTarget",
|
|
211
|
+
"square.moveTarget",
|
|
212
|
+
"square.passTarget",
|
|
213
|
+
"square.piece",
|
|
214
|
+
"square.pieceWithBall",
|
|
215
|
+
"square.selected",
|
|
201
216
|
"teamBlack",
|
|
202
217
|
"teamWhite",
|
|
203
218
|
"waitingRival",
|
|
@@ -207,6 +222,7 @@ var PIECE_I18N_KEYS = [...Object.values(SHORT_KEY), ...Object.values(LONG_KEY)].
|
|
|
207
222
|
(label) => `pieces.${label}`
|
|
208
223
|
);
|
|
209
224
|
var EVENT_TOAST_I18N_KEYS = [
|
|
225
|
+
"eventToast.goal",
|
|
210
226
|
"eventToast.interception",
|
|
211
227
|
"eventToast.offside",
|
|
212
228
|
"eventToast.tackle"
|
|
@@ -216,7 +232,13 @@ var GAME_I18N_KEYS = [
|
|
|
216
232
|
...PIECE_I18N_KEYS,
|
|
217
233
|
...EVENT_TOAST_I18N_KEYS
|
|
218
234
|
];
|
|
219
|
-
var
|
|
235
|
+
var nodeEnv = globalThis.process?.env?.NODE_ENV;
|
|
236
|
+
var fallback = (key) => {
|
|
237
|
+
if (nodeEnv && nodeEnv !== "production") {
|
|
238
|
+
console.warn(`[chess-football-ui] missing game i18n key: "${key}" (no GameI18nProvider?)`);
|
|
239
|
+
}
|
|
240
|
+
return key;
|
|
241
|
+
};
|
|
220
242
|
var GameI18nContext = createContext(fallback);
|
|
221
243
|
function GameI18nProvider({ t, children }) {
|
|
222
244
|
return /* @__PURE__ */ jsx(GameI18nContext.Provider, { value: t, children: /* @__PURE__ */ jsx(MotionConfig, { reducedMotion: "user", children }) });
|
|
@@ -421,17 +443,29 @@ var cellX = (x) => `${x * 100}%`;
|
|
|
421
443
|
var cellY = (y) => `${(ROWS - 1 - y) * 100}%`;
|
|
422
444
|
var PIECE_SLIDE = { type: "spring", stiffness: 190, damping: 28, mass: 1 };
|
|
423
445
|
var BALL_SLIDE = { type: "spring", stiffness: 300, damping: 26, mass: 0.6 };
|
|
424
|
-
function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, toolbar = true }) {
|
|
446
|
+
function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, toolbar = true, className }) {
|
|
425
447
|
const t = useGameT();
|
|
448
|
+
const boardId = useId();
|
|
426
449
|
const prefersReducedMotion = useReducedMotion();
|
|
427
450
|
const {
|
|
428
451
|
boardState,
|
|
429
452
|
selectedPieceId,
|
|
453
|
+
interactionMode,
|
|
430
454
|
setSelectedPieceId,
|
|
431
455
|
setInteractionMode,
|
|
432
456
|
movePiece,
|
|
433
457
|
passBall
|
|
434
|
-
} = useGameStore(
|
|
458
|
+
} = useGameStore(
|
|
459
|
+
useShallow((s) => ({
|
|
460
|
+
boardState: s.boardState,
|
|
461
|
+
selectedPieceId: s.selectedPieceId,
|
|
462
|
+
interactionMode: s.interactionMode,
|
|
463
|
+
setSelectedPieceId: s.setSelectedPieceId,
|
|
464
|
+
setInteractionMode: s.setInteractionMode,
|
|
465
|
+
movePiece: s.movePiece,
|
|
466
|
+
passBall: s.passBall
|
|
467
|
+
}))
|
|
468
|
+
);
|
|
435
469
|
const [cursor, setCursor] = useState(null);
|
|
436
470
|
const [disambiguateAt, setDisambiguateAt] = useState(null);
|
|
437
471
|
const [shortcutsOpen, setShortcutsOpen] = useState(false);
|
|
@@ -499,21 +533,21 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
499
533
|
return () => clearTimeout(id);
|
|
500
534
|
}, [replay]);
|
|
501
535
|
const validMoves = useMemo(() => {
|
|
502
|
-
if (!boardState) return [];
|
|
536
|
+
if (!boardState || interactionMode === "pass") return [];
|
|
503
537
|
const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
|
|
504
538
|
if (sp && !sp.hasMovedThisTurn) {
|
|
505
539
|
return getValidMoves(sp, boardState);
|
|
506
540
|
}
|
|
507
541
|
return [];
|
|
508
|
-
}, [boardState, selectedPieceId]);
|
|
542
|
+
}, [boardState, selectedPieceId, interactionMode]);
|
|
509
543
|
const validPasses = useMemo(() => {
|
|
510
|
-
if (!boardState) return [];
|
|
544
|
+
if (!boardState || interactionMode === "move") return [];
|
|
511
545
|
const sp = boardState.pieces.find((p) => p.id === selectedPieceId);
|
|
512
546
|
if (sp && boardState.ball.holderId === sp.id) {
|
|
513
547
|
return getValidPasses(sp, boardState);
|
|
514
548
|
}
|
|
515
549
|
return [];
|
|
516
|
-
}, [boardState, selectedPieceId]);
|
|
550
|
+
}, [boardState, selectedPieceId, interactionMode]);
|
|
517
551
|
if (!boardState) return null;
|
|
518
552
|
const lastMove = boardState.lastMove;
|
|
519
553
|
const ball = boardState.ball;
|
|
@@ -627,8 +661,11 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
627
661
|
activate(c.x, c.y);
|
|
628
662
|
break;
|
|
629
663
|
}
|
|
664
|
+
// Bare M / P only: Cmd+P is Print and Ctrl+M is a browser shortcut,
|
|
665
|
+
// and both used to resolve the popover behind the user's back.
|
|
630
666
|
case "m":
|
|
631
667
|
case "M":
|
|
668
|
+
if (e.ctrlKey || e.metaKey || e.altKey) break;
|
|
632
669
|
if (disambiguateAt && selectedPieceId) {
|
|
633
670
|
movePiece(selectedPieceId, disambiguateAt);
|
|
634
671
|
setDisambiguateAt(null);
|
|
@@ -636,6 +673,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
636
673
|
break;
|
|
637
674
|
case "p":
|
|
638
675
|
case "P":
|
|
676
|
+
if (e.ctrlKey || e.metaKey || e.altKey) break;
|
|
639
677
|
if (disambiguateAt) {
|
|
640
678
|
passBall(disambiguateAt);
|
|
641
679
|
setDisambiguateAt(null);
|
|
@@ -653,9 +691,28 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
653
691
|
break;
|
|
654
692
|
}
|
|
655
693
|
};
|
|
656
|
-
const
|
|
657
|
-
|
|
694
|
+
const cellId = (x, y) => `${boardId}-sq-${x}-${y}`;
|
|
695
|
+
const squareLabel = (x, y, pieceAt, isValidMove, isValidPass) => {
|
|
696
|
+
const square = squareName({ x, y });
|
|
697
|
+
const ballHere = ball.pos.x === x && ball.pos.y === y;
|
|
698
|
+
const parts = [];
|
|
699
|
+
if (pieceAt) {
|
|
700
|
+
const piece = t(`pieces.${LONG_KEY[pieceAt.type]}`);
|
|
701
|
+
const team = pieceAt.side === "white" ? t("teamWhite") : t("teamBlack");
|
|
702
|
+
parts.push(t(ballHere ? "square.pieceWithBall" : "square.piece", { square, piece, team }));
|
|
703
|
+
} else {
|
|
704
|
+
parts.push(t(ballHere ? "square.emptyWithBall" : "square.empty", { square }));
|
|
705
|
+
}
|
|
706
|
+
if (isValidMove && isValidPass) parts.push(t("square.moveOrPassTarget"));
|
|
707
|
+
else if (isValidMove) parts.push(t("square.moveTarget"));
|
|
708
|
+
else if (isValidPass) parts.push(t("square.passTarget"));
|
|
709
|
+
if (pieceAt && pieceAt.id === selectedPieceId) parts.push(t("square.selected"));
|
|
710
|
+
return parts.join(", ");
|
|
711
|
+
};
|
|
712
|
+
const renderRows = () => {
|
|
713
|
+
const rows = [];
|
|
658
714
|
for (let y = ROWS - 1; y >= 0; y--) {
|
|
715
|
+
const squares = [];
|
|
659
716
|
for (let x = 0; x < COLS; x++) {
|
|
660
717
|
const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
|
|
661
718
|
const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
|
|
@@ -672,27 +729,38 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
672
729
|
/* @__PURE__ */ jsxs(
|
|
673
730
|
"div",
|
|
674
731
|
{
|
|
732
|
+
id: cellId(x, y),
|
|
733
|
+
role: "gridcell",
|
|
734
|
+
"aria-label": squareLabel(x, y, pieceAt, isValidMove, isValidPass),
|
|
735
|
+
"aria-selected": !!pieceAt && pieceAt.id === selectedPieceId,
|
|
675
736
|
onClick: () => handleSquareClick(x, y),
|
|
676
737
|
onMouseEnter: () => {
|
|
677
738
|
if (isValidPass) setHoveredPassAt({ x, y });
|
|
678
739
|
},
|
|
679
740
|
onMouseLeave: () => setHoveredPassAt(null),
|
|
741
|
+
onTouchStart: () => {
|
|
742
|
+
if (isValidPass) setHoveredPassAt({ x, y });
|
|
743
|
+
},
|
|
680
744
|
className: cn(
|
|
681
745
|
pitchSquareClass(x, y),
|
|
682
746
|
"flex items-center justify-center cursor-pointer",
|
|
683
747
|
!isValidMove && !isValidPass && isLastMove && "bg-last-move-highlight/20",
|
|
684
748
|
// §8: pulsing warning ring on enemy goal area
|
|
685
749
|
isOffsideRisk && isEnemyGoalArea && !isValidMove && !isValidPass && "ring-2 ring-inset ring-warning/60",
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
750
|
+
// Move and pass used raw yellow-400/sky-400 here while
|
|
751
|
+
// the popover a few lines below used the
|
|
752
|
+
// move-highlight/pass-highlight tokens — two palettes
|
|
753
|
+
// for the same two concepts. One palette now: tokens.
|
|
754
|
+
isAmbiguous && "ring-[3px] ring-inset ring-move-highlight/90 bg-move-highlight/20",
|
|
755
|
+
!isAmbiguous && isValidMove && "ring-[3px] ring-inset ring-move-highlight/80 bg-move-highlight/15",
|
|
756
|
+
!isAmbiguous && isValidPass && "ring-[3px] ring-inset ring-pass-highlight/80 bg-pass-highlight/15"
|
|
689
757
|
),
|
|
690
758
|
children: [
|
|
691
|
-
!isAmbiguous && isValidMove && !pieceAt && /* @__PURE__ */ jsx("div", { className: "w-3 h-3 rounded-full bg-
|
|
692
|
-
!isAmbiguous && isValidPass && !pieceAt && /* @__PURE__ */ jsx("div", { className: "w-3 h-3 rounded-full
|
|
693
|
-
isAmbiguous && !pieceAt && /* @__PURE__ */ jsxs("div", { className: "flex gap-0.5", children: [
|
|
694
|
-
/* @__PURE__ */ jsx("div", { className: "w-2.5 h-2.5 rounded-full bg-
|
|
695
|
-
/* @__PURE__ */ jsx("div", { className: "w-2.5 h-2.5 rounded-full
|
|
759
|
+
!isAmbiguous && isValidMove && !pieceAt && /* @__PURE__ */ jsx("div", { "data-testid": "move-marker", className: "w-3 h-3 rounded-full bg-move-highlight/65" }),
|
|
760
|
+
!isAmbiguous && isValidPass && !pieceAt && /* @__PURE__ */ jsx("div", { "data-testid": "pass-marker", className: "w-3.5 h-3.5 rounded-full border-2 border-pass-highlight/80" }),
|
|
761
|
+
isAmbiguous && !pieceAt && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", children: [
|
|
762
|
+
/* @__PURE__ */ jsx("div", { "data-testid": "move-marker", className: "w-2.5 h-2.5 rounded-full bg-move-highlight/80" }),
|
|
763
|
+
/* @__PURE__ */ jsx("div", { "data-testid": "pass-marker", className: "w-2.5 h-2.5 rounded-full border-2 border-pass-highlight/90" })
|
|
696
764
|
] }),
|
|
697
765
|
isCursor && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 ring-2 ring-inset ring-white pointer-events-none z-20", "aria-hidden": "true" }),
|
|
698
766
|
isDisambiguateTarget && selectedPieceId && /* @__PURE__ */ jsxs(
|
|
@@ -745,26 +813,27 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
745
813
|
)
|
|
746
814
|
);
|
|
747
815
|
}
|
|
816
|
+
rows.push(
|
|
817
|
+
/* @__PURE__ */ jsx("div", { role: "row", className: "contents", children: squares }, `row-${y}`)
|
|
818
|
+
);
|
|
748
819
|
}
|
|
749
|
-
return
|
|
820
|
+
return rows;
|
|
750
821
|
};
|
|
751
822
|
const board = /* @__PURE__ */ jsxs(
|
|
752
823
|
"div",
|
|
753
824
|
{
|
|
825
|
+
"data-testid": "board-root",
|
|
754
826
|
className: cn(
|
|
755
827
|
// §6: Mobile edge-to-edge — thin 2px margin, no border, no radius
|
|
756
|
-
|
|
828
|
+
// The frame was hardcoded #1a3317 in three files, hand-tuned for
|
|
829
|
+
// the webapp's darker grass and visibly wrong against the
|
|
830
|
+
// CrazyGames pitch. It is a token now (see README).
|
|
831
|
+
"w-full mx-0.5 bg-field-frame overflow-hidden",
|
|
757
832
|
// §5+§6: Desktop — centered, rounded, bordered, height-based max-width
|
|
758
|
-
"md:mx-auto md:rounded-xl md:shadow-2xl md:border-4 md:border-
|
|
833
|
+
"md:mx-auto md:rounded-xl md:shadow-2xl md:border-4 md:border-field-frame",
|
|
759
834
|
"md:max-w-[min(700px,calc((100dvh_-_150px)*0.75))]",
|
|
760
|
-
|
|
835
|
+
!showCoordinates && className
|
|
761
836
|
),
|
|
762
|
-
...keyboardNav ? {
|
|
763
|
-
tabIndex: 0,
|
|
764
|
-
role: "application",
|
|
765
|
-
"aria-label": "Game board \u2014 arrow keys to move the cursor, Enter to act",
|
|
766
|
-
onKeyDown: handleKeyDown
|
|
767
|
-
} : {},
|
|
768
837
|
children: [
|
|
769
838
|
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
839
|
/* @__PURE__ */ jsx(BallHolderChip, {}),
|
|
@@ -773,7 +842,12 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
773
842
|
{
|
|
774
843
|
ref: shortcutsRef,
|
|
775
844
|
className: "relative",
|
|
776
|
-
onKeyDown: (e) =>
|
|
845
|
+
onKeyDown: (e) => {
|
|
846
|
+
if (e.key === "Escape" && shortcutsOpen) {
|
|
847
|
+
e.stopPropagation();
|
|
848
|
+
setShortcutsOpen(false);
|
|
849
|
+
}
|
|
850
|
+
},
|
|
777
851
|
children: [
|
|
778
852
|
/* @__PURE__ */ jsx(
|
|
779
853
|
"button",
|
|
@@ -805,7 +879,25 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
805
879
|
)
|
|
806
880
|
] }),
|
|
807
881
|
/* @__PURE__ */ jsxs("div", { className: "relative", style: { containerType: "inline-size" }, children: [
|
|
808
|
-
/* @__PURE__ */ jsx(
|
|
882
|
+
/* @__PURE__ */ jsx(
|
|
883
|
+
"div",
|
|
884
|
+
{
|
|
885
|
+
role: "grid",
|
|
886
|
+
"aria-label": t("boardAriaLabel"),
|
|
887
|
+
"aria-rowcount": ROWS,
|
|
888
|
+
"aria-colcount": COLS,
|
|
889
|
+
className: cn(
|
|
890
|
+
"grid grid-cols-9 gap-[1px]",
|
|
891
|
+
keyboardNav && "focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent-green"
|
|
892
|
+
),
|
|
893
|
+
...keyboardNav ? {
|
|
894
|
+
tabIndex: 0,
|
|
895
|
+
onKeyDown: handleKeyDown,
|
|
896
|
+
"aria-activedescendant": cursor ? cellId(cursor.x, cursor.y) : void 0
|
|
897
|
+
} : {},
|
|
898
|
+
children: renderRows()
|
|
899
|
+
}
|
|
900
|
+
),
|
|
809
901
|
/* @__PURE__ */ jsx(GrassOverlay, {}),
|
|
810
902
|
/* @__PURE__ */ jsx(PitchMarkings, {}),
|
|
811
903
|
/* @__PURE__ */ jsx(
|
|
@@ -831,7 +923,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
831
923
|
)
|
|
832
924
|
}
|
|
833
925
|
),
|
|
834
|
-
/* @__PURE__ */ jsxs("div", { className: "absolute inset-0 pointer-events-none", "data-testid": "pieces-layer", children: [
|
|
926
|
+
/* @__PURE__ */ jsxs("div", { className: "absolute inset-0 pointer-events-none", "data-testid": "pieces-layer", "aria-hidden": "true", children: [
|
|
835
927
|
/* @__PURE__ */ jsx(AnimatePresence, { mode: "sync", children: boardState.pieces.map((piece) => {
|
|
836
928
|
const isPickupCarrier = pickupCarrierId === piece.id;
|
|
837
929
|
const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : PIECE_SLIDE;
|
|
@@ -937,7 +1029,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
937
1029
|
);
|
|
938
1030
|
if (!showCoordinates) return board;
|
|
939
1031
|
const ranksTopToBottom = [...RANK_LABELS].reverse();
|
|
940
|
-
return /* @__PURE__ */ jsx("div", { className: "w-full max-w-[540px] md:max-w-none mx-auto",
|
|
1032
|
+
return /* @__PURE__ */ jsx("div", { className: cn("w-full max-w-[540px] md:max-w-none mx-auto", className), children: /* @__PURE__ */ jsxs(
|
|
941
1033
|
"div",
|
|
942
1034
|
{
|
|
943
1035
|
className: "grid gap-1 select-none",
|
|
@@ -969,8 +1061,20 @@ function Avatar({ className, children }) {
|
|
|
969
1061
|
return /* @__PURE__ */ jsx("div", { className: cn("relative w-9 h-9 rounded-full overflow-hidden shrink-0", className), children });
|
|
970
1062
|
}
|
|
971
1063
|
function AvatarImage({ src, alt, className }) {
|
|
972
|
-
|
|
973
|
-
|
|
1064
|
+
const [failed, setFailed] = useState(false);
|
|
1065
|
+
useEffect(() => {
|
|
1066
|
+
setFailed(false);
|
|
1067
|
+
}, [src]);
|
|
1068
|
+
if (!src || failed) return null;
|
|
1069
|
+
return /* @__PURE__ */ jsx(
|
|
1070
|
+
"img",
|
|
1071
|
+
{
|
|
1072
|
+
src,
|
|
1073
|
+
alt: alt ?? "",
|
|
1074
|
+
onError: () => setFailed(true),
|
|
1075
|
+
className: cn("w-full h-full object-cover", className)
|
|
1076
|
+
}
|
|
1077
|
+
);
|
|
974
1078
|
}
|
|
975
1079
|
function AvatarFallback({ className, children }) {
|
|
976
1080
|
return /* @__PURE__ */ jsx("div", { className: cn("absolute inset-0 flex items-center justify-center text-xs font-semibold", className), children });
|
|
@@ -1026,6 +1130,51 @@ function ActionPoints({ total, remaining, size = 20, className, kingMustRelease
|
|
|
1026
1130
|
);
|
|
1027
1131
|
}) });
|
|
1028
1132
|
}
|
|
1133
|
+
function PlayerIdentity({
|
|
1134
|
+
name,
|
|
1135
|
+
team,
|
|
1136
|
+
variant = "full",
|
|
1137
|
+
isActive = false,
|
|
1138
|
+
align = "left",
|
|
1139
|
+
adornment,
|
|
1140
|
+
className
|
|
1141
|
+
}) {
|
|
1142
|
+
if (variant === "compact") {
|
|
1143
|
+
return /* @__PURE__ */ jsxs(
|
|
1144
|
+
"span",
|
|
1145
|
+
{
|
|
1146
|
+
className: cn(
|
|
1147
|
+
"font-inter text-[11px] font-medium truncate max-w-[160px]",
|
|
1148
|
+
isActive ? "text-fg-secondary" : "text-fg-muted",
|
|
1149
|
+
className
|
|
1150
|
+
),
|
|
1151
|
+
children: [
|
|
1152
|
+
name,
|
|
1153
|
+
/* @__PURE__ */ jsxs("span", { className: "font-mono uppercase tracking-[0.5px] text-fg-muted", children: [
|
|
1154
|
+
" \xB7 ",
|
|
1155
|
+
team
|
|
1156
|
+
] })
|
|
1157
|
+
]
|
|
1158
|
+
}
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-1 min-w-0", align === "right" && "items-end", className), children: [
|
|
1162
|
+
/* @__PURE__ */ jsx(
|
|
1163
|
+
"span",
|
|
1164
|
+
{
|
|
1165
|
+
className: cn(
|
|
1166
|
+
"font-inter text-[13px] font-semibold leading-none truncate transition-colors duration-300",
|
|
1167
|
+
isActive ? "text-fg-primary" : "text-fg-muted"
|
|
1168
|
+
),
|
|
1169
|
+
children: name
|
|
1170
|
+
}
|
|
1171
|
+
),
|
|
1172
|
+
/* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-1", align === "right" && "flex-row-reverse"), children: [
|
|
1173
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] text-fg-muted uppercase tracking-[0.5px]", children: team }),
|
|
1174
|
+
adornment
|
|
1175
|
+
] })
|
|
1176
|
+
] });
|
|
1177
|
+
}
|
|
1029
1178
|
function initials(name) {
|
|
1030
1179
|
return (name?.split(" ").map((w) => w[0]).join("").toUpperCase() || "?").slice(0, 2);
|
|
1031
1180
|
}
|
|
@@ -1034,10 +1183,11 @@ function Scoreboard({
|
|
|
1034
1183
|
opponentUsername,
|
|
1035
1184
|
creatorAvatarUrl,
|
|
1036
1185
|
opponentAvatarUrl,
|
|
1037
|
-
userSide
|
|
1186
|
+
userSide,
|
|
1187
|
+
className
|
|
1038
1188
|
}) {
|
|
1039
1189
|
const t = useGameT();
|
|
1040
|
-
const
|
|
1190
|
+
const boardState = useGameStore((s) => s.boardState);
|
|
1041
1191
|
if (!boardState?.score) return null;
|
|
1042
1192
|
const { score, turn, actionPoints, kingMustRelease } = boardState;
|
|
1043
1193
|
const myScore = userSide === "white" ? score.white : score.black;
|
|
@@ -1051,8 +1201,8 @@ function Scoreboard({
|
|
|
1051
1201
|
const isMyTurn = turn === userSide;
|
|
1052
1202
|
const myKingMustRelease = isMyTurn && kingMustRelease === turn;
|
|
1053
1203
|
const rivalKingMustRelease = !isMyTurn && kingMustRelease === turn;
|
|
1054
|
-
const maxAP = boardState.maxActionPoints ??
|
|
1055
|
-
return /* @__PURE__ */ jsxs("div", { className: "w-full bg-bg-secondary flex items-center justify-between px-5 py-2.5", children: [
|
|
1204
|
+
const maxAP = boardState.maxActionPoints ?? DEFAULT_MAX_AP;
|
|
1205
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("w-full bg-bg-secondary flex items-center justify-between px-5 py-2.5", className), children: [
|
|
1056
1206
|
/* @__PURE__ */ jsx(
|
|
1057
1207
|
PlayerInfo,
|
|
1058
1208
|
{
|
|
@@ -1066,10 +1216,11 @@ function Scoreboard({
|
|
|
1066
1216
|
align: "left"
|
|
1067
1217
|
}
|
|
1068
1218
|
),
|
|
1069
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1070
|
-
/* @__PURE__ */ jsx("span", { className: "font-anton text-[32px] leading-none text-fg-primary tabular-nums", children: myScore }),
|
|
1071
|
-
/* @__PURE__ */ jsx("span", { className: "font-anton text-2xl text-fg-muted", children: "\u2014" }),
|
|
1072
|
-
/* @__PURE__ */ jsx("span", { className: "font-anton text-[32px] leading-none text-fg-primary tabular-nums", children: rivalScore })
|
|
1219
|
+
/* @__PURE__ */ jsxs("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: "flex items-center gap-3", children: [
|
|
1220
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "font-anton text-[32px] leading-none text-fg-primary tabular-nums", children: myScore }),
|
|
1221
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "font-anton text-2xl text-fg-muted", children: "\u2014" }),
|
|
1222
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "font-anton text-[32px] leading-none text-fg-primary tabular-nums", children: rivalScore }),
|
|
1223
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: t("scoreAriaLabel", { me: myName, myScore, rival: rivalName, rivalScore }) })
|
|
1073
1224
|
] }),
|
|
1074
1225
|
/* @__PURE__ */ jsx(
|
|
1075
1226
|
PlayerInfo,
|
|
@@ -1096,14 +1247,16 @@ function PlayerInfo({ name, team, avatarUrl, isActive, actionPoints, maxActionPo
|
|
|
1096
1247
|
/* @__PURE__ */ jsx(AvatarFallback, { className: "bg-bg-surface-elevated text-fg-secondary text-xs font-semibold", children: initials(name) })
|
|
1097
1248
|
] }),
|
|
1098
1249
|
/* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-1", align === "right" && "items-end"), children: [
|
|
1099
|
-
/* @__PURE__ */ jsx(
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1250
|
+
/* @__PURE__ */ jsx(
|
|
1251
|
+
PlayerIdentity,
|
|
1252
|
+
{
|
|
1253
|
+
name,
|
|
1254
|
+
team,
|
|
1255
|
+
isActive,
|
|
1256
|
+
align,
|
|
1257
|
+
adornment: isActive ? /* @__PURE__ */ jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-accent-green animate-pulse motion-reduce:animate-none", "aria-hidden": "true" }) : void 0
|
|
1258
|
+
}
|
|
1259
|
+
),
|
|
1107
1260
|
/* @__PURE__ */ jsx(
|
|
1108
1261
|
ActionPoints,
|
|
1109
1262
|
{
|
|
@@ -1124,7 +1277,7 @@ function MiniScoreboard({
|
|
|
1124
1277
|
className
|
|
1125
1278
|
}) {
|
|
1126
1279
|
const t = useGameT();
|
|
1127
|
-
const
|
|
1280
|
+
const boardState = useGameStore((s) => s.boardState);
|
|
1128
1281
|
if (!boardState?.score) return null;
|
|
1129
1282
|
const { score, turn } = boardState;
|
|
1130
1283
|
const myScore = userSide === "white" ? score.white : score.black;
|
|
@@ -1137,29 +1290,28 @@ function MiniScoreboard({
|
|
|
1137
1290
|
return /* @__PURE__ */ jsxs(
|
|
1138
1291
|
"div",
|
|
1139
1292
|
{
|
|
1293
|
+
role: "status",
|
|
1294
|
+
"aria-live": "polite",
|
|
1295
|
+
"aria-atomic": "true",
|
|
1140
1296
|
className: cn(
|
|
1141
1297
|
"w-full max-w-[540px] rounded-xl bg-bg-surface border border-border-subtle",
|
|
1142
1298
|
"flex items-center justify-between gap-4 px-5 py-3",
|
|
1143
1299
|
className
|
|
1144
1300
|
),
|
|
1145
1301
|
children: [
|
|
1146
|
-
/* @__PURE__ */
|
|
1147
|
-
|
|
1148
|
-
|
|
1302
|
+
/* @__PURE__ */ jsxs("div", { className: "contents", "aria-hidden": "true", children: [
|
|
1303
|
+
/* @__PURE__ */ jsx(SideCell, { name: myName, team: myTeam, score: myScore, isActive: isMyTurn, align: "left" }),
|
|
1304
|
+
/* @__PURE__ */ jsx("span", { className: "font-anton text-base text-fg-muted uppercase tracking-[2px]", children: "vs" }),
|
|
1305
|
+
/* @__PURE__ */ jsx(SideCell, { name: rivalName, team: rivalTeam, score: rivalScore, isActive: !isMyTurn, align: "right" })
|
|
1306
|
+
] }),
|
|
1307
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: t("scoreAriaLabel", { me: myName, myScore, rival: rivalName, rivalScore }) })
|
|
1149
1308
|
]
|
|
1150
1309
|
}
|
|
1151
1310
|
);
|
|
1152
1311
|
}
|
|
1153
1312
|
function SideCell({ name, team, score, isActive, align }) {
|
|
1154
1313
|
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col min-w-0", align === "right" ? "items-end" : "items-start"), children: [
|
|
1155
|
-
/* @__PURE__ */
|
|
1156
|
-
"font-mono text-[10px] uppercase tracking-[0.5px] truncate max-w-[160px]",
|
|
1157
|
-
isActive ? "text-fg-secondary" : "text-fg-muted"
|
|
1158
|
-
), children: [
|
|
1159
|
-
name,
|
|
1160
|
-
" \xB7 ",
|
|
1161
|
-
team
|
|
1162
|
-
] }),
|
|
1314
|
+
/* @__PURE__ */ jsx(PlayerIdentity, { name, team, variant: "compact", isActive }),
|
|
1163
1315
|
/* @__PURE__ */ jsx("span", { className: cn(
|
|
1164
1316
|
"font-anton text-[32px] leading-none tabular-nums",
|
|
1165
1317
|
isActive ? "text-fg-primary" : "text-fg-secondary"
|
|
@@ -1167,7 +1319,12 @@ function SideCell({ name, team, score, isActive, align }) {
|
|
|
1167
1319
|
] });
|
|
1168
1320
|
}
|
|
1169
1321
|
var VARIANT = {
|
|
1170
|
-
|
|
1322
|
+
// White on `--accent-green` is 4.27:1 — under WCAG AA for the Anton 400 the
|
|
1323
|
+
// primary CTA is set in. `--accent-green-dark` clears it (~6.5:1), so the
|
|
1324
|
+
// resting state is the dark green and interaction brightens it rather than
|
|
1325
|
+
// darkening it. Hover/active are transient; the resting state is the one a
|
|
1326
|
+
// player reads.
|
|
1327
|
+
primary: "bg-accent-green-dark text-fg-primary font-anton tracking-widest rounded-md hover:bg-accent-green active:bg-accent-green-light",
|
|
1171
1328
|
secondary: "bg-bg-surface text-fg-primary font-inter font-semibold rounded-md border border-border-subtle hover:bg-bg-surface-elevated active:bg-bg-secondary",
|
|
1172
1329
|
ghost: "bg-transparent text-fg-secondary font-inter font-medium rounded-md hover:text-fg-primary active:text-fg-muted",
|
|
1173
1330
|
destructive: "bg-danger/10 text-danger font-inter font-semibold rounded-md border border-danger hover:bg-danger/20 active:bg-danger/30"
|
|
@@ -1197,24 +1354,97 @@ var Button = React.forwardRef(
|
|
|
1197
1354
|
)
|
|
1198
1355
|
);
|
|
1199
1356
|
Button.displayName = "Button";
|
|
1357
|
+
var FOCUSABLE = [
|
|
1358
|
+
"a[href]",
|
|
1359
|
+
"button:not([disabled])",
|
|
1360
|
+
"input:not([disabled])",
|
|
1361
|
+
"select:not([disabled])",
|
|
1362
|
+
"textarea:not([disabled])",
|
|
1363
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
1364
|
+
].join(",");
|
|
1365
|
+
function focusableWithin(panel) {
|
|
1366
|
+
return Array.from(panel.querySelectorAll(FOCUSABLE)).filter(
|
|
1367
|
+
(el) => !el.closest("[hidden]") && !el.closest('[aria-hidden="true"]')
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
function useDialogA11y({ open, onClose }) {
|
|
1371
|
+
const panelRef = useRef(null);
|
|
1372
|
+
const restoreToRef = useRef(null);
|
|
1373
|
+
const onCloseRef = useRef(onClose);
|
|
1374
|
+
useEffect(() => {
|
|
1375
|
+
onCloseRef.current = onClose;
|
|
1376
|
+
});
|
|
1377
|
+
useEffect(() => {
|
|
1378
|
+
if (!open) return;
|
|
1379
|
+
const panel = panelRef.current;
|
|
1380
|
+
if (!panel) return;
|
|
1381
|
+
restoreToRef.current = document.activeElement;
|
|
1382
|
+
const initial = focusableWithin(panel)[0];
|
|
1383
|
+
if (initial) {
|
|
1384
|
+
initial.focus();
|
|
1385
|
+
} else {
|
|
1386
|
+
panel.tabIndex = -1;
|
|
1387
|
+
panel.focus();
|
|
1388
|
+
}
|
|
1389
|
+
const onKeyDown = (e) => {
|
|
1390
|
+
if (e.key === "Escape") {
|
|
1391
|
+
e.stopPropagation();
|
|
1392
|
+
onCloseRef.current();
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
if (e.key !== "Tab") return;
|
|
1396
|
+
const items = focusableWithin(panel);
|
|
1397
|
+
if (items.length === 0) {
|
|
1398
|
+
e.preventDefault();
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
const first = items[0];
|
|
1402
|
+
const last = items[items.length - 1];
|
|
1403
|
+
const active = document.activeElement;
|
|
1404
|
+
if (e.shiftKey && (active === first || !panel.contains(active))) {
|
|
1405
|
+
e.preventDefault();
|
|
1406
|
+
last.focus();
|
|
1407
|
+
} else if (!e.shiftKey && (active === last || !panel.contains(active))) {
|
|
1408
|
+
e.preventDefault();
|
|
1409
|
+
first.focus();
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
1413
|
+
return () => {
|
|
1414
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
1415
|
+
restoreToRef.current?.focus?.();
|
|
1416
|
+
};
|
|
1417
|
+
}, [open]);
|
|
1418
|
+
return panelRef;
|
|
1419
|
+
}
|
|
1200
1420
|
function ConfirmDialog({ open, title, description, confirmLabel, cancelLabel, onConfirm, onCancel }) {
|
|
1421
|
+
const baseId = useId();
|
|
1422
|
+
const titleId = `${baseId}-title`;
|
|
1423
|
+
const descId = `${baseId}-desc`;
|
|
1424
|
+
const panelRef = useDialogA11y({ open, onClose: onCancel });
|
|
1201
1425
|
if (!open) return null;
|
|
1202
1426
|
return /* @__PURE__ */ jsx(
|
|
1203
1427
|
"div",
|
|
1204
1428
|
{
|
|
1205
1429
|
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",
|
|
1206
|
-
|
|
1207
|
-
"aria-modal": "true",
|
|
1430
|
+
onClick: onCancel,
|
|
1208
1431
|
children: /* @__PURE__ */ jsxs(
|
|
1209
1432
|
"div",
|
|
1210
1433
|
{
|
|
1434
|
+
ref: panelRef,
|
|
1435
|
+
role: "dialog",
|
|
1436
|
+
"aria-modal": "true",
|
|
1437
|
+
"aria-labelledby": titleId,
|
|
1438
|
+
"aria-describedby": description ? descId : void 0,
|
|
1439
|
+
onClick: (e) => e.stopPropagation(),
|
|
1211
1440
|
className: cn(
|
|
1212
1441
|
"bg-bg-surface border border-border-subtle rounded-xl p-6 mx-4 max-w-sm w-full",
|
|
1213
|
-
"flex flex-col gap-4 shadow-2xl"
|
|
1442
|
+
"flex flex-col gap-4 shadow-2xl",
|
|
1443
|
+
"focus:outline-none"
|
|
1214
1444
|
),
|
|
1215
1445
|
children: [
|
|
1216
|
-
/* @__PURE__ */ jsx("h2", { className: "font-anton text-lg text-fg-primary uppercase tracking-wide", children: title }),
|
|
1217
|
-
description && /* @__PURE__ */ jsx("p", { className: "font-inter text-sm text-fg-secondary", children: description }),
|
|
1446
|
+
/* @__PURE__ */ jsx("h2", { id: titleId, className: "font-anton text-lg text-fg-primary uppercase tracking-wide", children: title }),
|
|
1447
|
+
description && /* @__PURE__ */ jsx("p", { id: descId, className: "font-inter text-sm text-fg-secondary", children: description }),
|
|
1218
1448
|
/* @__PURE__ */ jsxs("div", { className: "flex gap-2 pt-1", children: [
|
|
1219
1449
|
/* @__PURE__ */ jsx(Button, { variant: "secondary", className: "flex-1", onClick: onCancel, children: cancelLabel }),
|
|
1220
1450
|
/* @__PURE__ */ jsx(Button, { variant: "primary", className: "flex-1", onClick: onConfirm, children: confirmLabel })
|
|
@@ -1225,11 +1455,9 @@ function ConfirmDialog({ open, title, description, confirmLabel, cancelLabel, on
|
|
|
1225
1455
|
}
|
|
1226
1456
|
);
|
|
1227
1457
|
}
|
|
1228
|
-
function GameControls({ isMyTurn }) {
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1231
|
-
endTurn
|
|
1232
|
-
} = useGameStore();
|
|
1458
|
+
function GameControls({ isMyTurn, className }) {
|
|
1459
|
+
const boardState = useGameStore((s) => s.boardState);
|
|
1460
|
+
const endTurn = useGameStore((s) => s.endTurn);
|
|
1233
1461
|
const t = useGameT();
|
|
1234
1462
|
const [showEndTurnConfirm, setShowEndTurnConfirm] = useState(false);
|
|
1235
1463
|
if (!boardState) return null;
|
|
@@ -1241,7 +1469,7 @@ function GameControls({ isMyTurn }) {
|
|
|
1241
1469
|
endTurn();
|
|
1242
1470
|
}
|
|
1243
1471
|
};
|
|
1244
|
-
return /* @__PURE__ */ jsxs("div", { className: "w-full bg-bg-secondary flex flex-col gap-3 px-5 pt-3 pb-5", children: [
|
|
1472
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("w-full bg-bg-secondary flex flex-col gap-3 px-5 pt-3 pb-5", className), children: [
|
|
1245
1473
|
isMyTurn && /* @__PURE__ */ jsxs(
|
|
1246
1474
|
Button,
|
|
1247
1475
|
{
|
|
@@ -1261,7 +1489,15 @@ function GameControls({ isMyTurn }) {
|
|
|
1261
1489
|
]
|
|
1262
1490
|
}
|
|
1263
1491
|
),
|
|
1264
|
-
!isMyTurn && /* @__PURE__ */ jsx(
|
|
1492
|
+
!isMyTurn && /* @__PURE__ */ jsx(
|
|
1493
|
+
"div",
|
|
1494
|
+
{
|
|
1495
|
+
role: "status",
|
|
1496
|
+
"aria-live": "polite",
|
|
1497
|
+
className: "flex items-center justify-center h-11 rounded-md bg-bg-surface border border-border-subtle",
|
|
1498
|
+
children: /* @__PURE__ */ jsx("span", { className: "font-inter text-sm text-fg-muted", children: t("waitingRival") })
|
|
1499
|
+
}
|
|
1500
|
+
),
|
|
1265
1501
|
/* @__PURE__ */ jsx(
|
|
1266
1502
|
ConfirmDialog,
|
|
1267
1503
|
{
|
|
@@ -1288,7 +1524,9 @@ var PIECE_ICON2 = {
|
|
|
1288
1524
|
};
|
|
1289
1525
|
function SelectedPieceDetail({ isMyTurn, className }) {
|
|
1290
1526
|
const t = useGameT();
|
|
1291
|
-
const { boardState, selectedPieceId } = useGameStore(
|
|
1527
|
+
const { boardState, selectedPieceId } = useGameStore(
|
|
1528
|
+
useShallow((s) => ({ boardState: s.boardState, selectedPieceId: s.selectedPieceId }))
|
|
1529
|
+
);
|
|
1292
1530
|
if (!boardState) return null;
|
|
1293
1531
|
const piece = boardState.pieces.find((p) => p.id === selectedPieceId);
|
|
1294
1532
|
if (!piece) {
|
|
@@ -1322,7 +1560,6 @@ function SelectedPieceDetail({ isMyTurn, className }) {
|
|
|
1322
1560
|
),
|
|
1323
1561
|
role: "status",
|
|
1324
1562
|
"aria-live": "polite",
|
|
1325
|
-
"aria-label": `${longName} ${square}`,
|
|
1326
1563
|
children: [
|
|
1327
1564
|
/* @__PURE__ */ jsx(
|
|
1328
1565
|
"div",
|
|
@@ -1351,7 +1588,7 @@ function SelectedPieceDetail({ isMyTurn, className }) {
|
|
|
1351
1588
|
}
|
|
1352
1589
|
function MoveHistory({ scrollable = true, limit, className }) {
|
|
1353
1590
|
const t = useGameT();
|
|
1354
|
-
const
|
|
1591
|
+
const boardState = useGameStore((s) => s.boardState);
|
|
1355
1592
|
const scrollRef = useRef(null);
|
|
1356
1593
|
const history = boardState?.moveHistory ?? [];
|
|
1357
1594
|
const visible = limit ? history.slice(-limit) : history;
|
|
@@ -1458,9 +1695,9 @@ function HistoryRow({ entry, isLatest }) {
|
|
|
1458
1695
|
}
|
|
1459
1696
|
var COLS2 = 9;
|
|
1460
1697
|
var ROWS2 = 12;
|
|
1461
|
-
function StaticGameBoard() {
|
|
1698
|
+
function StaticGameBoard({ className } = {}) {
|
|
1462
1699
|
const boardState = useMemo(() => getInitialBoardState("white"), []);
|
|
1463
|
-
return /* @__PURE__ */ jsxs("div", { className: "relative w-full overflow-hidden rounded-xl shadow-2xl border-4 border-
|
|
1700
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("relative w-full overflow-hidden rounded-xl shadow-2xl border-4 border-field-frame bg-field-frame", className), children: [
|
|
1464
1701
|
/* @__PURE__ */ jsx(PitchSurface, { children: /* @__PURE__ */ jsx("div", { className: "absolute inset-0 pointer-events-none", children: boardState.pieces.map((piece) => /* @__PURE__ */ jsx(
|
|
1465
1702
|
"div",
|
|
1466
1703
|
{
|
|
@@ -1504,9 +1741,10 @@ function StaticGameBoard() {
|
|
|
1504
1741
|
}
|
|
1505
1742
|
function TurnBanner({ isMyTurn, waitingLabel, className }) {
|
|
1506
1743
|
const t = useGameT();
|
|
1507
|
-
const
|
|
1744
|
+
const boardState = useGameStore((s) => s.boardState);
|
|
1508
1745
|
if (!boardState) return null;
|
|
1509
|
-
const { actionPoints,
|
|
1746
|
+
const { actionPoints, turnNumber } = boardState;
|
|
1747
|
+
const maxActionPoints = boardState.maxActionPoints ?? DEFAULT_MAX_AP;
|
|
1510
1748
|
return /* @__PURE__ */ jsxs(
|
|
1511
1749
|
"div",
|
|
1512
1750
|
{
|
|
@@ -1518,16 +1756,19 @@ function TurnBanner({ isMyTurn, waitingLabel, className }) {
|
|
|
1518
1756
|
"aria-live": "polite",
|
|
1519
1757
|
"aria-atomic": "true",
|
|
1520
1758
|
children: [
|
|
1521
|
-
/* @__PURE__ */
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1759
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-2 min-w-0", children: [
|
|
1760
|
+
/* @__PURE__ */ jsx(
|
|
1761
|
+
"span",
|
|
1762
|
+
{
|
|
1763
|
+
className: cn(
|
|
1764
|
+
"font-anton text-sm tracking-[1.5px] uppercase truncate",
|
|
1765
|
+
isMyTurn ? "text-accent-green" : "text-fg-muted"
|
|
1766
|
+
),
|
|
1767
|
+
children: isMyTurn ? t("yourTurn") : waitingLabel ?? t("waitingRival")
|
|
1768
|
+
}
|
|
1769
|
+
),
|
|
1770
|
+
typeof turnNumber === "number" && /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] text-fg-muted uppercase tracking-[0.5px] shrink-0", children: t("history.turn", { n: turnNumber }) })
|
|
1771
|
+
] }),
|
|
1531
1772
|
isMyTurn && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", "aria-label": t("actionPointsAriaLabel", { remaining: actionPoints, total: maxActionPoints }), children: [
|
|
1532
1773
|
/* @__PURE__ */ jsx(Zap, { size: 12, className: "text-accent-green", "aria-hidden": "true" }),
|
|
1533
1774
|
/* @__PURE__ */ jsxs("span", { className: "font-mono text-xs text-fg-secondary tabular-nums", children: [
|
|
@@ -1540,15 +1781,16 @@ function TurnBanner({ isMyTurn, waitingLabel, className }) {
|
|
|
1540
1781
|
}
|
|
1541
1782
|
);
|
|
1542
1783
|
}
|
|
1543
|
-
var TOASTABLE = /* @__PURE__ */ new Set(["interception", "offside", "tackle"]);
|
|
1784
|
+
var TOASTABLE = /* @__PURE__ */ new Set(["goal", "interception", "offside", "tackle"]);
|
|
1544
1785
|
var TOAST_CONFIG = {
|
|
1786
|
+
goal: "bg-accent-green/20 border-accent-green/60 text-accent-green-light",
|
|
1545
1787
|
interception: "bg-danger/20 border-danger/50 text-danger",
|
|
1546
1788
|
offside: "bg-warning/20 border-warning/50 text-warning",
|
|
1547
1789
|
tackle: "bg-warning/20 border-warning/50 text-warning"
|
|
1548
1790
|
};
|
|
1549
1791
|
function EventToast({ className }) {
|
|
1550
1792
|
const t = useGameT();
|
|
1551
|
-
const
|
|
1793
|
+
const boardState = useGameStore((s) => s.boardState);
|
|
1552
1794
|
const [toast, setToast] = useState(null);
|
|
1553
1795
|
useEffect(() => {
|
|
1554
1796
|
const lastMove = boardState?.lastMove;
|
|
@@ -1587,8 +1829,10 @@ function EventToast({ className }) {
|
|
|
1587
1829
|
);
|
|
1588
1830
|
}
|
|
1589
1831
|
function MobileHistory({ className }) {
|
|
1590
|
-
const
|
|
1832
|
+
const boardState = useGameStore((s) => s.boardState);
|
|
1591
1833
|
const [open, setOpen] = useState(false);
|
|
1834
|
+
const titleId = `${useId()}-title`;
|
|
1835
|
+
const panelRef = useDialogA11y({ open, onClose: () => setOpen(false) });
|
|
1592
1836
|
const lastMove = boardState?.moveHistory?.at(-1);
|
|
1593
1837
|
if (!lastMove) return null;
|
|
1594
1838
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -1614,11 +1858,13 @@ function MobileHistory({ className }) {
|
|
|
1614
1858
|
animate: { y: 0 },
|
|
1615
1859
|
exit: { y: "100%" },
|
|
1616
1860
|
transition: { type: "spring", stiffness: 300, damping: 32 },
|
|
1617
|
-
|
|
1861
|
+
ref: panelRef,
|
|
1862
|
+
className: "fixed bottom-0 left-0 right-0 z-50 bg-bg-secondary rounded-t-2xl border-t border-border-subtle shadow-xl focus:outline-none",
|
|
1618
1863
|
role: "dialog",
|
|
1619
1864
|
"aria-modal": "true",
|
|
1865
|
+
"aria-labelledby": titleId,
|
|
1620
1866
|
children: [
|
|
1621
|
-
/* @__PURE__ */ jsx(SheetHeader, { onClose: () => setOpen(false) }),
|
|
1867
|
+
/* @__PURE__ */ jsx(SheetHeader, { titleId, onClose: () => setOpen(false) }),
|
|
1622
1868
|
/* @__PURE__ */ jsx("div", { className: "px-4 pb-6 max-h-[50dvh] overflow-y-auto", children: /* @__PURE__ */ jsx(MoveHistory, { scrollable: false }) })
|
|
1623
1869
|
]
|
|
1624
1870
|
},
|
|
@@ -1627,17 +1873,17 @@ function MobileHistory({ className }) {
|
|
|
1627
1873
|
] }) })
|
|
1628
1874
|
] });
|
|
1629
1875
|
}
|
|
1630
|
-
function SheetHeader({ onClose }) {
|
|
1876
|
+
function SheetHeader({ titleId, onClose }) {
|
|
1631
1877
|
const t = useGameT();
|
|
1632
1878
|
return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-5 pt-4 pb-3 border-b border-border-subtle", children: [
|
|
1633
|
-
/* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] tracking-[0.5px] uppercase text-fg-muted", children: t("history.title") }),
|
|
1879
|
+
/* @__PURE__ */ jsx("span", { id: titleId, className: "font-mono text-[10px] tracking-[0.5px] uppercase text-fg-muted", children: t("history.title") }),
|
|
1634
1880
|
/* @__PURE__ */ jsx(
|
|
1635
1881
|
"button",
|
|
1636
1882
|
{
|
|
1637
1883
|
onClick: onClose,
|
|
1638
1884
|
className: "w-8 h-8 flex items-center justify-center rounded-full bg-bg-surface hover:bg-bg-surface-elevated text-fg-muted hover:text-fg-primary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-green",
|
|
1639
|
-
"aria-label": "
|
|
1640
|
-
children: /* @__PURE__ */ jsx(X, { size: 14 })
|
|
1885
|
+
"aria-label": t("history.close"),
|
|
1886
|
+
children: /* @__PURE__ */ jsx(X, { size: 14, "aria-hidden": "true" })
|
|
1641
1887
|
}
|
|
1642
1888
|
)
|
|
1643
1889
|
] });
|
|
@@ -1690,6 +1936,35 @@ function HistoryChip({ lastMove, onClick, className }) {
|
|
|
1690
1936
|
);
|
|
1691
1937
|
}
|
|
1692
1938
|
|
|
1693
|
-
|
|
1939
|
+
// src/tokens.ts
|
|
1940
|
+
var REQUIRED_TOKENS = [
|
|
1941
|
+
// Surfaces
|
|
1942
|
+
"--bg-primary",
|
|
1943
|
+
"--bg-secondary",
|
|
1944
|
+
"--bg-surface",
|
|
1945
|
+
"--bg-surface-elevated",
|
|
1946
|
+
// Text
|
|
1947
|
+
"--fg-primary",
|
|
1948
|
+
"--fg-secondary",
|
|
1949
|
+
"--fg-muted",
|
|
1950
|
+
// Accent (the primary CTA rests on the dark step — see Button)
|
|
1951
|
+
"--accent-green",
|
|
1952
|
+
"--accent-green-light",
|
|
1953
|
+
"--accent-green-dark",
|
|
1954
|
+
// Borders
|
|
1955
|
+
"--border-subtle",
|
|
1956
|
+
// Semantic
|
|
1957
|
+
"--danger",
|
|
1958
|
+
"--warning",
|
|
1959
|
+
// Board
|
|
1960
|
+
"--field-green-1",
|
|
1961
|
+
"--field-green-2",
|
|
1962
|
+
"--field-frame",
|
|
1963
|
+
"--move-highlight",
|
|
1964
|
+
"--pass-highlight",
|
|
1965
|
+
"--last-move-highlight"
|
|
1966
|
+
];
|
|
1967
|
+
|
|
1968
|
+
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, PlayerIdentity, REQUIRED_TOKENS, Scoreboard, SelectedPieceDetail, StaticGameBoard, TurnBanner, cn, isGoalAreaSquare, pitchSquareClass, useDialogA11y, useGameT };
|
|
1694
1969
|
//# sourceMappingURL=index.js.map
|
|
1695
1970
|
//# sourceMappingURL=index.js.map
|