@scriptonita/chess-football-ui 0.7.0 → 0.8.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 +21 -0
- package/dist/index.cjs +188 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -3
- package/dist/index.d.ts +22 -3
- package/dist/index.js +188 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,6 +35,27 @@ npm install @scriptonita/chess-football-ui
|
|
|
35
35
|
(`bg-bg-surface`, `text-accent-green`, …). Each app must define those tokens and include this
|
|
36
36
|
package's files in its Tailwind `content`/`@source` scan so the classes are generated.
|
|
37
37
|
|
|
38
|
+
## Host app contract
|
|
39
|
+
|
|
40
|
+
Two things the package cannot enforce at build time and every consumer must check:
|
|
41
|
+
|
|
42
|
+
**1. Translation coverage.** `GAME_I18N_KEYS` is the manifest of every `game`-namespace key
|
|
43
|
+
the components read. Assert it in each app's test suite — without this, a key added here
|
|
44
|
+
surfaces as raw text on the pitch (`⚠ game.offsideWarning`) instead of failing CI:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { GAME_I18N_KEYS } from '@scriptonita/chess-football-ui'
|
|
48
|
+
import es from './messages/es.json'
|
|
49
|
+
|
|
50
|
+
const missing = GAME_I18N_KEYS.filter(k => !k.split('.').reduce<any>((o, p) => o?.[p], es.game))
|
|
51
|
+
expect(missing).toEqual([])
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**2. Design tokens.** Components emit Tailwind classes bound to tokens the app defines. A
|
|
55
|
+
token that is missing fails silently — the rule simply does not apply. The board's
|
|
56
|
+
`--last-move-highlight`, `--move-highlight`, `--pass-highlight`, `--accent-green`,
|
|
57
|
+
`--accent-green-light` and `--accent-green-dark` are all required.
|
|
58
|
+
|
|
38
59
|
## Peer dependencies
|
|
39
60
|
|
|
40
61
|
`react`, `react-dom`, `framer-motion`, `lucide-react`.
|
package/dist/index.cjs
CHANGED
|
@@ -190,6 +190,7 @@ var STATIC_I18N_KEYS = [
|
|
|
190
190
|
"actionPointsAriaLabel",
|
|
191
191
|
"actionPointsShort",
|
|
192
192
|
"ballHolder",
|
|
193
|
+
"boardAriaLabel",
|
|
193
194
|
"endTurn",
|
|
194
195
|
"endTurnConfirm",
|
|
195
196
|
"endTurnConfirmDescription",
|
|
@@ -203,11 +204,13 @@ var STATIC_I18N_KEYS = [
|
|
|
203
204
|
"history.pass",
|
|
204
205
|
"history.tackle",
|
|
205
206
|
"history.title",
|
|
207
|
+
"history.close",
|
|
206
208
|
"history.turn",
|
|
207
209
|
"history.viewAll",
|
|
208
210
|
"move",
|
|
209
211
|
"offsideWarning",
|
|
210
212
|
"pass",
|
|
213
|
+
"scoreAriaLabel",
|
|
211
214
|
"selectedPiece.alreadyMoved",
|
|
212
215
|
"selectedPiece.atSquare",
|
|
213
216
|
"selectedPiece.empty",
|
|
@@ -219,6 +222,14 @@ var STATIC_I18N_KEYS = [
|
|
|
219
222
|
"shortcuts.pass",
|
|
220
223
|
"shortcuts.select",
|
|
221
224
|
"shortcuts.title",
|
|
225
|
+
"square.empty",
|
|
226
|
+
"square.emptyWithBall",
|
|
227
|
+
"square.moveOrPassTarget",
|
|
228
|
+
"square.moveTarget",
|
|
229
|
+
"square.passTarget",
|
|
230
|
+
"square.piece",
|
|
231
|
+
"square.pieceWithBall",
|
|
232
|
+
"square.selected",
|
|
222
233
|
"teamBlack",
|
|
223
234
|
"teamWhite",
|
|
224
235
|
"waitingRival",
|
|
@@ -228,6 +239,7 @@ var PIECE_I18N_KEYS = [...Object.values(chessFootballEngine.SHORT_KEY), ...Objec
|
|
|
228
239
|
(label) => `pieces.${label}`
|
|
229
240
|
);
|
|
230
241
|
var EVENT_TOAST_I18N_KEYS = [
|
|
242
|
+
"eventToast.goal",
|
|
231
243
|
"eventToast.interception",
|
|
232
244
|
"eventToast.offside",
|
|
233
245
|
"eventToast.tackle"
|
|
@@ -237,7 +249,13 @@ var GAME_I18N_KEYS = [
|
|
|
237
249
|
...PIECE_I18N_KEYS,
|
|
238
250
|
...EVENT_TOAST_I18N_KEYS
|
|
239
251
|
];
|
|
240
|
-
var
|
|
252
|
+
var nodeEnv = globalThis.process?.env?.NODE_ENV;
|
|
253
|
+
var fallback = (key) => {
|
|
254
|
+
if (nodeEnv && nodeEnv !== "production") {
|
|
255
|
+
console.warn(`[chess-football-ui] missing game i18n key: "${key}" (no GameI18nProvider?)`);
|
|
256
|
+
}
|
|
257
|
+
return key;
|
|
258
|
+
};
|
|
241
259
|
var GameI18nContext = React.createContext(fallback);
|
|
242
260
|
function GameI18nProvider({ t, children }) {
|
|
243
261
|
return /* @__PURE__ */ jsxRuntime.jsx(GameI18nContext.Provider, { value: t, children: /* @__PURE__ */ jsxRuntime.jsx(framerMotion.MotionConfig, { reducedMotion: "user", children }) });
|
|
@@ -444,6 +462,7 @@ var PIECE_SLIDE = { type: "spring", stiffness: 190, damping: 28, mass: 1 };
|
|
|
444
462
|
var BALL_SLIDE = { type: "spring", stiffness: 300, damping: 26, mass: 0.6 };
|
|
445
463
|
function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, toolbar = true }) {
|
|
446
464
|
const t = useGameT();
|
|
465
|
+
const boardId = React.useId();
|
|
447
466
|
const prefersReducedMotion = framerMotion.useReducedMotion();
|
|
448
467
|
const {
|
|
449
468
|
boardState,
|
|
@@ -674,9 +693,28 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
674
693
|
break;
|
|
675
694
|
}
|
|
676
695
|
};
|
|
677
|
-
const
|
|
678
|
-
|
|
696
|
+
const cellId = (x, y) => `${boardId}-sq-${x}-${y}`;
|
|
697
|
+
const squareLabel = (x, y, pieceAt, isValidMove, isValidPass) => {
|
|
698
|
+
const square = chessFootballEngine.squareName({ x, y });
|
|
699
|
+
const ballHere = ball.pos.x === x && ball.pos.y === y;
|
|
700
|
+
const parts = [];
|
|
701
|
+
if (pieceAt) {
|
|
702
|
+
const piece = t(`pieces.${chessFootballEngine.LONG_KEY[pieceAt.type]}`);
|
|
703
|
+
const team = pieceAt.side === "white" ? t("teamWhite") : t("teamBlack");
|
|
704
|
+
parts.push(t(ballHere ? "square.pieceWithBall" : "square.piece", { square, piece, team }));
|
|
705
|
+
} else {
|
|
706
|
+
parts.push(t(ballHere ? "square.emptyWithBall" : "square.empty", { square }));
|
|
707
|
+
}
|
|
708
|
+
if (isValidMove && isValidPass) parts.push(t("square.moveOrPassTarget"));
|
|
709
|
+
else if (isValidMove) parts.push(t("square.moveTarget"));
|
|
710
|
+
else if (isValidPass) parts.push(t("square.passTarget"));
|
|
711
|
+
if (pieceAt && pieceAt.id === selectedPieceId) parts.push(t("square.selected"));
|
|
712
|
+
return parts.join(", ");
|
|
713
|
+
};
|
|
714
|
+
const renderRows = () => {
|
|
715
|
+
const rows = [];
|
|
679
716
|
for (let y = ROWS - 1; y >= 0; y--) {
|
|
717
|
+
const squares = [];
|
|
680
718
|
for (let x = 0; x < COLS; x++) {
|
|
681
719
|
const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
|
|
682
720
|
const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
|
|
@@ -693,6 +731,10 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
693
731
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
694
732
|
"div",
|
|
695
733
|
{
|
|
734
|
+
id: cellId(x, y),
|
|
735
|
+
role: "gridcell",
|
|
736
|
+
"aria-label": squareLabel(x, y, pieceAt, isValidMove, isValidPass),
|
|
737
|
+
"aria-selected": !!pieceAt && pieceAt.id === selectedPieceId,
|
|
696
738
|
onClick: () => handleSquareClick(x, y),
|
|
697
739
|
onMouseEnter: () => {
|
|
698
740
|
if (isValidPass) setHoveredPassAt({ x, y });
|
|
@@ -766,26 +808,23 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
766
808
|
)
|
|
767
809
|
);
|
|
768
810
|
}
|
|
811
|
+
rows.push(
|
|
812
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { role: "row", className: "contents", children: squares }, `row-${y}`)
|
|
813
|
+
);
|
|
769
814
|
}
|
|
770
|
-
return
|
|
815
|
+
return rows;
|
|
771
816
|
};
|
|
772
817
|
const board = /* @__PURE__ */ jsxRuntime.jsxs(
|
|
773
818
|
"div",
|
|
774
819
|
{
|
|
820
|
+
"data-testid": "board-root",
|
|
775
821
|
className: cn(
|
|
776
822
|
// §6: Mobile edge-to-edge — thin 2px margin, no border, no radius
|
|
777
823
|
"w-full mx-0.5 bg-[#1a3317] overflow-hidden",
|
|
778
824
|
// §5+§6: Desktop — centered, rounded, bordered, height-based max-width
|
|
779
825
|
"md:mx-auto md:rounded-xl md:shadow-2xl md:border-4 md:border-[#1a3317]",
|
|
780
|
-
"md:max-w-[min(700px,calc((100dvh_-_150px)*0.75))]"
|
|
781
|
-
keyboardNav && "focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-green focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary"
|
|
826
|
+
"md:max-w-[min(700px,calc((100dvh_-_150px)*0.75))]"
|
|
782
827
|
),
|
|
783
|
-
...keyboardNav ? {
|
|
784
|
-
tabIndex: 0,
|
|
785
|
-
role: "application",
|
|
786
|
-
"aria-label": "Game board \u2014 arrow keys to move the cursor, Enter to act",
|
|
787
|
-
onKeyDown: handleKeyDown
|
|
788
|
-
} : {},
|
|
789
828
|
children: [
|
|
790
829
|
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
830
|
/* @__PURE__ */ jsxRuntime.jsx(BallHolderChip, {}),
|
|
@@ -794,7 +833,12 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
794
833
|
{
|
|
795
834
|
ref: shortcutsRef,
|
|
796
835
|
className: "relative",
|
|
797
|
-
onKeyDown: (e) =>
|
|
836
|
+
onKeyDown: (e) => {
|
|
837
|
+
if (e.key === "Escape" && shortcutsOpen) {
|
|
838
|
+
e.stopPropagation();
|
|
839
|
+
setShortcutsOpen(false);
|
|
840
|
+
}
|
|
841
|
+
},
|
|
798
842
|
children: [
|
|
799
843
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
800
844
|
"button",
|
|
@@ -826,7 +870,25 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
826
870
|
)
|
|
827
871
|
] }),
|
|
828
872
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", style: { containerType: "inline-size" }, children: [
|
|
829
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
873
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
874
|
+
"div",
|
|
875
|
+
{
|
|
876
|
+
role: "grid",
|
|
877
|
+
"aria-label": t("boardAriaLabel"),
|
|
878
|
+
"aria-rowcount": ROWS,
|
|
879
|
+
"aria-colcount": COLS,
|
|
880
|
+
className: cn(
|
|
881
|
+
"grid grid-cols-9 gap-[1px]",
|
|
882
|
+
keyboardNav && "focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent-green"
|
|
883
|
+
),
|
|
884
|
+
...keyboardNav ? {
|
|
885
|
+
tabIndex: 0,
|
|
886
|
+
onKeyDown: handleKeyDown,
|
|
887
|
+
"aria-activedescendant": cursor ? cellId(cursor.x, cursor.y) : void 0
|
|
888
|
+
} : {},
|
|
889
|
+
children: renderRows()
|
|
890
|
+
}
|
|
891
|
+
),
|
|
830
892
|
/* @__PURE__ */ jsxRuntime.jsx(GrassOverlay, {}),
|
|
831
893
|
/* @__PURE__ */ jsxRuntime.jsx(PitchMarkings, {}),
|
|
832
894
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -852,7 +914,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
|
|
|
852
914
|
)
|
|
853
915
|
}
|
|
854
916
|
),
|
|
855
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 pointer-events-none", "data-testid": "pieces-layer", children: [
|
|
917
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 pointer-events-none", "data-testid": "pieces-layer", "aria-hidden": "true", children: [
|
|
856
918
|
/* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { mode: "sync", children: boardState.pieces.map((piece) => {
|
|
857
919
|
const isPickupCarrier = pickupCarrierId === piece.id;
|
|
858
920
|
const pieceTransition = isPickupCarrier ? { duration: PICKUP_DURATION, ease: "linear" } : PIECE_SLIDE;
|
|
@@ -1087,10 +1149,11 @@ function Scoreboard({
|
|
|
1087
1149
|
align: "left"
|
|
1088
1150
|
}
|
|
1089
1151
|
),
|
|
1090
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1091
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-anton text-[32px] leading-none text-fg-primary tabular-nums", children: myScore }),
|
|
1092
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-anton text-2xl text-fg-muted", children: "\u2014" }),
|
|
1093
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-anton text-[32px] leading-none text-fg-primary tabular-nums", children: rivalScore })
|
|
1152
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: "flex items-center gap-3", children: [
|
|
1153
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: "font-anton text-[32px] leading-none text-fg-primary tabular-nums", children: myScore }),
|
|
1154
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: "font-anton text-2xl text-fg-muted", children: "\u2014" }),
|
|
1155
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: "font-anton text-[32px] leading-none text-fg-primary tabular-nums", children: rivalScore }),
|
|
1156
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: t("scoreAriaLabel", { me: myName, myScore, rival: rivalName, rivalScore }) })
|
|
1094
1157
|
] }),
|
|
1095
1158
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1096
1159
|
PlayerInfo,
|
|
@@ -1158,15 +1221,21 @@ function MiniScoreboard({
|
|
|
1158
1221
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1159
1222
|
"div",
|
|
1160
1223
|
{
|
|
1224
|
+
role: "status",
|
|
1225
|
+
"aria-live": "polite",
|
|
1226
|
+
"aria-atomic": "true",
|
|
1161
1227
|
className: cn(
|
|
1162
1228
|
"w-full max-w-[540px] rounded-xl bg-bg-surface border border-border-subtle",
|
|
1163
1229
|
"flex items-center justify-between gap-4 px-5 py-3",
|
|
1164
1230
|
className
|
|
1165
1231
|
),
|
|
1166
1232
|
children: [
|
|
1167
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1168
|
-
|
|
1169
|
-
|
|
1233
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "contents", "aria-hidden": "true", children: [
|
|
1234
|
+
/* @__PURE__ */ jsxRuntime.jsx(SideCell, { name: myName, team: myTeam, score: myScore, isActive: isMyTurn, align: "left" }),
|
|
1235
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-anton text-base text-fg-muted uppercase tracking-[2px]", children: "vs" }),
|
|
1236
|
+
/* @__PURE__ */ jsxRuntime.jsx(SideCell, { name: rivalName, team: rivalTeam, score: rivalScore, isActive: !isMyTurn, align: "right" })
|
|
1237
|
+
] }),
|
|
1238
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: t("scoreAriaLabel", { me: myName, myScore, rival: rivalName, rivalScore }) })
|
|
1170
1239
|
]
|
|
1171
1240
|
}
|
|
1172
1241
|
);
|
|
@@ -1188,7 +1257,12 @@ function SideCell({ name, team, score, isActive, align }) {
|
|
|
1188
1257
|
] });
|
|
1189
1258
|
}
|
|
1190
1259
|
var VARIANT = {
|
|
1191
|
-
|
|
1260
|
+
// White on `--accent-green` is 4.27:1 — under WCAG AA for the Anton 400 the
|
|
1261
|
+
// primary CTA is set in. `--accent-green-dark` clears it (~6.5:1), so the
|
|
1262
|
+
// resting state is the dark green and interaction brightens it rather than
|
|
1263
|
+
// darkening it. Hover/active are transient; the resting state is the one a
|
|
1264
|
+
// player reads.
|
|
1265
|
+
primary: "bg-accent-green-dark text-fg-primary font-anton tracking-widest rounded-md hover:bg-accent-green active:bg-accent-green-light",
|
|
1192
1266
|
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",
|
|
1193
1267
|
ghost: "bg-transparent text-fg-secondary font-inter font-medium rounded-md hover:text-fg-primary active:text-fg-muted",
|
|
1194
1268
|
destructive: "bg-danger/10 text-danger font-inter font-semibold rounded-md border border-danger hover:bg-danger/20 active:bg-danger/30"
|
|
@@ -1218,24 +1292,97 @@ var Button = React__namespace.forwardRef(
|
|
|
1218
1292
|
)
|
|
1219
1293
|
);
|
|
1220
1294
|
Button.displayName = "Button";
|
|
1295
|
+
var FOCUSABLE = [
|
|
1296
|
+
"a[href]",
|
|
1297
|
+
"button:not([disabled])",
|
|
1298
|
+
"input:not([disabled])",
|
|
1299
|
+
"select:not([disabled])",
|
|
1300
|
+
"textarea:not([disabled])",
|
|
1301
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
1302
|
+
].join(",");
|
|
1303
|
+
function focusableWithin(panel) {
|
|
1304
|
+
return Array.from(panel.querySelectorAll(FOCUSABLE)).filter(
|
|
1305
|
+
(el) => !el.closest("[hidden]") && !el.closest('[aria-hidden="true"]')
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
function useDialogA11y({ open, onClose }) {
|
|
1309
|
+
const panelRef = React.useRef(null);
|
|
1310
|
+
const restoreToRef = React.useRef(null);
|
|
1311
|
+
const onCloseRef = React.useRef(onClose);
|
|
1312
|
+
React.useEffect(() => {
|
|
1313
|
+
onCloseRef.current = onClose;
|
|
1314
|
+
});
|
|
1315
|
+
React.useEffect(() => {
|
|
1316
|
+
if (!open) return;
|
|
1317
|
+
const panel = panelRef.current;
|
|
1318
|
+
if (!panel) return;
|
|
1319
|
+
restoreToRef.current = document.activeElement;
|
|
1320
|
+
const initial = focusableWithin(panel)[0];
|
|
1321
|
+
if (initial) {
|
|
1322
|
+
initial.focus();
|
|
1323
|
+
} else {
|
|
1324
|
+
panel.tabIndex = -1;
|
|
1325
|
+
panel.focus();
|
|
1326
|
+
}
|
|
1327
|
+
const onKeyDown = (e) => {
|
|
1328
|
+
if (e.key === "Escape") {
|
|
1329
|
+
e.stopPropagation();
|
|
1330
|
+
onCloseRef.current();
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
if (e.key !== "Tab") return;
|
|
1334
|
+
const items = focusableWithin(panel);
|
|
1335
|
+
if (items.length === 0) {
|
|
1336
|
+
e.preventDefault();
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
const first = items[0];
|
|
1340
|
+
const last = items[items.length - 1];
|
|
1341
|
+
const active = document.activeElement;
|
|
1342
|
+
if (e.shiftKey && (active === first || !panel.contains(active))) {
|
|
1343
|
+
e.preventDefault();
|
|
1344
|
+
last.focus();
|
|
1345
|
+
} else if (!e.shiftKey && (active === last || !panel.contains(active))) {
|
|
1346
|
+
e.preventDefault();
|
|
1347
|
+
first.focus();
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
1351
|
+
return () => {
|
|
1352
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
1353
|
+
restoreToRef.current?.focus?.();
|
|
1354
|
+
};
|
|
1355
|
+
}, [open]);
|
|
1356
|
+
return panelRef;
|
|
1357
|
+
}
|
|
1221
1358
|
function ConfirmDialog({ open, title, description, confirmLabel, cancelLabel, onConfirm, onCancel }) {
|
|
1359
|
+
const baseId = React.useId();
|
|
1360
|
+
const titleId = `${baseId}-title`;
|
|
1361
|
+
const descId = `${baseId}-desc`;
|
|
1362
|
+
const panelRef = useDialogA11y({ open, onClose: onCancel });
|
|
1222
1363
|
if (!open) return null;
|
|
1223
1364
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1224
1365
|
"div",
|
|
1225
1366
|
{
|
|
1226
1367
|
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",
|
|
1227
|
-
|
|
1228
|
-
"aria-modal": "true",
|
|
1368
|
+
onClick: onCancel,
|
|
1229
1369
|
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1230
1370
|
"div",
|
|
1231
1371
|
{
|
|
1372
|
+
ref: panelRef,
|
|
1373
|
+
role: "dialog",
|
|
1374
|
+
"aria-modal": "true",
|
|
1375
|
+
"aria-labelledby": titleId,
|
|
1376
|
+
"aria-describedby": description ? descId : void 0,
|
|
1377
|
+
onClick: (e) => e.stopPropagation(),
|
|
1232
1378
|
className: cn(
|
|
1233
1379
|
"bg-bg-surface border border-border-subtle rounded-xl p-6 mx-4 max-w-sm w-full",
|
|
1234
|
-
"flex flex-col gap-4 shadow-2xl"
|
|
1380
|
+
"flex flex-col gap-4 shadow-2xl",
|
|
1381
|
+
"focus:outline-none"
|
|
1235
1382
|
),
|
|
1236
1383
|
children: [
|
|
1237
|
-
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "font-anton text-lg text-fg-primary uppercase tracking-wide", children: title }),
|
|
1238
|
-
description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-inter text-sm text-fg-secondary", children: description }),
|
|
1384
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { id: titleId, className: "font-anton text-lg text-fg-primary uppercase tracking-wide", children: title }),
|
|
1385
|
+
description && /* @__PURE__ */ jsxRuntime.jsx("p", { id: descId, className: "font-inter text-sm text-fg-secondary", children: description }),
|
|
1239
1386
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2 pt-1", children: [
|
|
1240
1387
|
/* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "secondary", className: "flex-1", onClick: onCancel, children: cancelLabel }),
|
|
1241
1388
|
/* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "primary", className: "flex-1", onClick: onConfirm, children: confirmLabel })
|
|
@@ -1561,8 +1708,9 @@ function TurnBanner({ isMyTurn, waitingLabel, className }) {
|
|
|
1561
1708
|
}
|
|
1562
1709
|
);
|
|
1563
1710
|
}
|
|
1564
|
-
var TOASTABLE = /* @__PURE__ */ new Set(["interception", "offside", "tackle"]);
|
|
1711
|
+
var TOASTABLE = /* @__PURE__ */ new Set(["goal", "interception", "offside", "tackle"]);
|
|
1565
1712
|
var TOAST_CONFIG = {
|
|
1713
|
+
goal: "bg-accent-green/20 border-accent-green/60 text-accent-green-light",
|
|
1566
1714
|
interception: "bg-danger/20 border-danger/50 text-danger",
|
|
1567
1715
|
offside: "bg-warning/20 border-warning/50 text-warning",
|
|
1568
1716
|
tackle: "bg-warning/20 border-warning/50 text-warning"
|
|
@@ -1610,6 +1758,8 @@ function EventToast({ className }) {
|
|
|
1610
1758
|
function MobileHistory({ className }) {
|
|
1611
1759
|
const { boardState } = chunkLTFNSTAQ_cjs.useGameStore();
|
|
1612
1760
|
const [open, setOpen] = React.useState(false);
|
|
1761
|
+
const titleId = `${React.useId()}-title`;
|
|
1762
|
+
const panelRef = useDialogA11y({ open, onClose: () => setOpen(false) });
|
|
1613
1763
|
const lastMove = boardState?.moveHistory?.at(-1);
|
|
1614
1764
|
if (!lastMove) return null;
|
|
1615
1765
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
@@ -1635,11 +1785,13 @@ function MobileHistory({ className }) {
|
|
|
1635
1785
|
animate: { y: 0 },
|
|
1636
1786
|
exit: { y: "100%" },
|
|
1637
1787
|
transition: { type: "spring", stiffness: 300, damping: 32 },
|
|
1638
|
-
|
|
1788
|
+
ref: panelRef,
|
|
1789
|
+
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",
|
|
1639
1790
|
role: "dialog",
|
|
1640
1791
|
"aria-modal": "true",
|
|
1792
|
+
"aria-labelledby": titleId,
|
|
1641
1793
|
children: [
|
|
1642
|
-
/* @__PURE__ */ jsxRuntime.jsx(SheetHeader, { onClose: () => setOpen(false) }),
|
|
1794
|
+
/* @__PURE__ */ jsxRuntime.jsx(SheetHeader, { titleId, onClose: () => setOpen(false) }),
|
|
1643
1795
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 pb-6 max-h-[50dvh] overflow-y-auto", children: /* @__PURE__ */ jsxRuntime.jsx(MoveHistory, { scrollable: false }) })
|
|
1644
1796
|
]
|
|
1645
1797
|
},
|
|
@@ -1648,17 +1800,17 @@ function MobileHistory({ className }) {
|
|
|
1648
1800
|
] }) })
|
|
1649
1801
|
] });
|
|
1650
1802
|
}
|
|
1651
|
-
function SheetHeader({ onClose }) {
|
|
1803
|
+
function SheetHeader({ titleId, onClose }) {
|
|
1652
1804
|
const t = useGameT();
|
|
1653
1805
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-5 pt-4 pb-3 border-b border-border-subtle", children: [
|
|
1654
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[10px] tracking-[0.5px] uppercase text-fg-muted", children: t("history.title") }),
|
|
1806
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { id: titleId, className: "font-mono text-[10px] tracking-[0.5px] uppercase text-fg-muted", children: t("history.title") }),
|
|
1655
1807
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1656
1808
|
"button",
|
|
1657
1809
|
{
|
|
1658
1810
|
onClick: onClose,
|
|
1659
1811
|
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",
|
|
1660
|
-
"aria-label": "
|
|
1661
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 14 })
|
|
1812
|
+
"aria-label": t("history.close"),
|
|
1813
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 14, "aria-hidden": "true" })
|
|
1662
1814
|
}
|
|
1663
1815
|
)
|
|
1664
1816
|
] });
|
|
@@ -1742,6 +1894,7 @@ exports.TurnBanner = TurnBanner;
|
|
|
1742
1894
|
exports.cn = cn;
|
|
1743
1895
|
exports.isGoalAreaSquare = isGoalAreaSquare;
|
|
1744
1896
|
exports.pitchSquareClass = pitchSquareClass;
|
|
1897
|
+
exports.useDialogA11y = useDialogA11y;
|
|
1745
1898
|
exports.useGameT = useGameT;
|
|
1746
1899
|
//# sourceMappingURL=index.cjs.map
|
|
1747
1900
|
//# sourceMappingURL=index.cjs.map
|