@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 CHANGED
@@ -35,6 +35,38 @@ 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 — Tailwind emits the rule, the variable resolves to
56
+ nothing, and the declaration is dropped. That is how `--last-move-highlight` went missing
57
+ in the CrazyGames SPA: the last-move highlight and the bot's turn replay never rendered,
58
+ with no error anywhere. `REQUIRED_TOKENS` is the manifest; assert it the same way:
59
+
60
+ ```ts
61
+ import { REQUIRED_TOKENS } from '@scriptonita/chess-football-ui'
62
+
63
+ const css = readFileSync('src/index.css', 'utf8')
64
+ expect(REQUIRED_TOKENS.filter(t => !css.includes(`${t}:`))).toEqual([])
65
+ ```
66
+
67
+ `--field-frame` is new in 0.9.0: the board frame was hardcoded `#1a3317` in three places,
68
+ hand-tuned for the webapp's grass and visibly wrong against the CrazyGames pitch.
69
+
38
70
  ## Peer dependencies
39
71
 
40
72
  `react`, `react-dom`, `framer-motion`, `lucide-react`.
@@ -4,19 +4,31 @@ var zustand = require('zustand');
4
4
  var chessFootballEngine = require('@scriptonita/chess-football-engine');
5
5
 
6
6
  // src/store/use-game-store.ts
7
- var useGameStore = zustand.create((set) => ({
7
+ var DEFAULT_MAX_AP = 5;
8
+ var INITIAL = {
8
9
  boardState: null,
9
10
  selectedPieceId: null,
10
- interactionMode: null,
11
+ interactionMode: null
12
+ };
13
+ var useGameStore = zustand.create((set) => ({
14
+ ...INITIAL,
11
15
  setBoardState: (state) => set({ boardState: state }),
12
16
  setSelectedPieceId: (id) => set({ selectedPieceId: id }),
13
17
  setInteractionMode: (mode) => set({ interactionMode: mode }),
18
+ /**
19
+ * The store is a module-global singleton, so a new match opens on the
20
+ * previous match's final board — which ended on a goal, and was therefore
21
+ * re-detected as a fresh goal (the bug behind webapp PR #42). Apps were
22
+ * each reaching for `useGameStore.setState({...})` by hand to work around
23
+ * it; this makes the intent explicit and keeps the shape in one place.
24
+ */
25
+ reset: () => set({ ...INITIAL }),
14
26
  resetTurn: () => set((state) => {
15
27
  if (!state.boardState) return state;
16
28
  return {
17
29
  boardState: {
18
30
  ...state.boardState,
19
- actionPoints: state.boardState.maxActionPoints ?? 5,
31
+ actionPoints: state.boardState.maxActionPoints ?? DEFAULT_MAX_AP,
20
32
  pieces: state.boardState.pieces.map((p) => ({ ...p, hasMovedThisTurn: false }))
21
33
  }
22
34
  };
@@ -50,6 +62,7 @@ Object.defineProperty(exports, "getInitialBoardState", {
50
62
  enumerable: true,
51
63
  get: function () { return chessFootballEngine.getInitialBoardState; }
52
64
  });
65
+ exports.DEFAULT_MAX_AP = DEFAULT_MAX_AP;
53
66
  exports.useGameStore = useGameStore;
54
- //# sourceMappingURL=chunk-LTFNSTAQ.cjs.map
55
- //# sourceMappingURL=chunk-LTFNSTAQ.cjs.map
67
+ //# sourceMappingURL=chunk-BSE4DUXM.cjs.map
68
+ //# sourceMappingURL=chunk-BSE4DUXM.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/store/use-game-store.ts"],"names":["create","applyMove","applyPass","applyEndTurn"],"mappings":"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","file":"chunk-BSE4DUXM.cjs","sourcesContent":["import { create } from 'zustand'\nimport type { BoardState, Position } from '@scriptonita/chess-football-engine'\nimport { applyMove, applyPass, applyEndTurn } from '@scriptonita/chess-football-engine'\n\n// The canonical kickoff position lives in the engine (rules-adjacent). Re-exported\n// here so app imports from `@scriptonita/chess-football-ui/store` keep working.\nexport { getInitialBoardState } from '@scriptonita/chess-football-engine'\n\n/**\n * Fallback when a board predates configurable action points. `maxActionPoints`\n * is optional on `BoardState`, and every consumer was inventing its own `?? 5`\n * — except `TurnBanner`, which had none and rendered \"3/\" on an old match.\n */\nexport const DEFAULT_MAX_AP = 5\n\ninterface GameStore {\n boardState: BoardState | null\n selectedPieceId: string | null\n interactionMode: 'move' | 'pass' | null\n setBoardState: (state: BoardState) => void\n setSelectedPieceId: (id: string | null) => void\n setInteractionMode: (mode: 'move' | 'pass' | null) => void\n resetTurn: () => void\n /** Return the store to its initial state. */\n reset: () => void\n movePiece: (pieceId: string, to: Position) => void\n passBall: (to: Position) => void\n endTurn: () => void\n}\n\nconst INITIAL = {\n boardState: null,\n selectedPieceId: null,\n interactionMode: null,\n} satisfies Pick<GameStore, 'boardState' | 'selectedPieceId' | 'interactionMode'>\n\nexport const useGameStore = create<GameStore>((set) => ({\n ...INITIAL,\n\n setBoardState: (state) => set({ boardState: state }),\n setSelectedPieceId: (id) => set({ selectedPieceId: id }),\n setInteractionMode: (mode) => set({ interactionMode: mode }),\n\n /**\n * The store is a module-global singleton, so a new match opens on the\n * previous match's final board — which ended on a goal, and was therefore\n * re-detected as a fresh goal (the bug behind webapp PR #42). Apps were\n * each reaching for `useGameStore.setState({...})` by hand to work around\n * it; this makes the intent explicit and keeps the shape in one place.\n */\n reset: () => set({ ...INITIAL }),\n\n resetTurn: () => set((state) => {\n if (!state.boardState) return state\n return {\n boardState: {\n ...state.boardState,\n actionPoints: state.boardState.maxActionPoints ?? DEFAULT_MAX_AP,\n pieces: state.boardState.pieces.map(p => ({ ...p, hasMovedThisTurn: false })),\n },\n }\n }),\n\n movePiece: (pieceId, to) => set((state) => {\n if (!state.boardState || state.boardState.actionPoints <= 0) return state\n const piece = state.boardState.pieces.find(p => p.id === pieceId)\n if (!piece || piece.hasMovedThisTurn) return state\n\n const { boardState } = applyMove(state.boardState, pieceId, to)\n return { boardState, selectedPieceId: null, interactionMode: null }\n }),\n\n passBall: (to) => set((state) => {\n if (!state.boardState || state.boardState.actionPoints <= 0 || !state.boardState.ball.holderId) return state\n const holder = state.boardState.pieces.find(p => p.id === state.boardState!.ball.holderId)\n if (!holder) return state\n\n const { boardState, forcedTurnEnd } = applyPass(state.boardState, to)\n const keepSelection = !forcedTurnEnd && boardState.ball.holderId\n return {\n boardState,\n selectedPieceId: keepSelection ? boardState.ball.holderId : null,\n interactionMode: keepSelection ? 'pass' : null,\n }\n }),\n\n endTurn: () => set((state) => {\n if (!state.boardState) return state\n return { boardState: applyEndTurn(state.boardState) }\n }),\n}))\n"]}
@@ -3,19 +3,31 @@ import { applyEndTurn, applyPass, applyMove } from '@scriptonita/chess-football-
3
3
  export { getInitialBoardState } from '@scriptonita/chess-football-engine';
4
4
 
5
5
  // src/store/use-game-store.ts
6
- var useGameStore = create((set) => ({
6
+ var DEFAULT_MAX_AP = 5;
7
+ var INITIAL = {
7
8
  boardState: null,
8
9
  selectedPieceId: null,
9
- interactionMode: null,
10
+ interactionMode: null
11
+ };
12
+ var useGameStore = create((set) => ({
13
+ ...INITIAL,
10
14
  setBoardState: (state) => set({ boardState: state }),
11
15
  setSelectedPieceId: (id) => set({ selectedPieceId: id }),
12
16
  setInteractionMode: (mode) => set({ interactionMode: mode }),
17
+ /**
18
+ * The store is a module-global singleton, so a new match opens on the
19
+ * previous match's final board — which ended on a goal, and was therefore
20
+ * re-detected as a fresh goal (the bug behind webapp PR #42). Apps were
21
+ * each reaching for `useGameStore.setState({...})` by hand to work around
22
+ * it; this makes the intent explicit and keeps the shape in one place.
23
+ */
24
+ reset: () => set({ ...INITIAL }),
13
25
  resetTurn: () => set((state) => {
14
26
  if (!state.boardState) return state;
15
27
  return {
16
28
  boardState: {
17
29
  ...state.boardState,
18
- actionPoints: state.boardState.maxActionPoints ?? 5,
30
+ actionPoints: state.boardState.maxActionPoints ?? DEFAULT_MAX_AP,
19
31
  pieces: state.boardState.pieces.map((p) => ({ ...p, hasMovedThisTurn: false }))
20
32
  }
21
33
  };
@@ -45,6 +57,6 @@ var useGameStore = create((set) => ({
45
57
  })
46
58
  }));
47
59
 
48
- export { useGameStore };
49
- //# sourceMappingURL=chunk-N24LGLLO.js.map
50
- //# sourceMappingURL=chunk-N24LGLLO.js.map
60
+ export { DEFAULT_MAX_AP, useGameStore };
61
+ //# sourceMappingURL=chunk-C5HCN7O5.js.map
62
+ //# sourceMappingURL=chunk-C5HCN7O5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/store/use-game-store.ts"],"names":[],"mappings":"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","file":"chunk-C5HCN7O5.js","sourcesContent":["import { create } from 'zustand'\nimport type { BoardState, Position } from '@scriptonita/chess-football-engine'\nimport { applyMove, applyPass, applyEndTurn } from '@scriptonita/chess-football-engine'\n\n// The canonical kickoff position lives in the engine (rules-adjacent). Re-exported\n// here so app imports from `@scriptonita/chess-football-ui/store` keep working.\nexport { getInitialBoardState } from '@scriptonita/chess-football-engine'\n\n/**\n * Fallback when a board predates configurable action points. `maxActionPoints`\n * is optional on `BoardState`, and every consumer was inventing its own `?? 5`\n * — except `TurnBanner`, which had none and rendered \"3/\" on an old match.\n */\nexport const DEFAULT_MAX_AP = 5\n\ninterface GameStore {\n boardState: BoardState | null\n selectedPieceId: string | null\n interactionMode: 'move' | 'pass' | null\n setBoardState: (state: BoardState) => void\n setSelectedPieceId: (id: string | null) => void\n setInteractionMode: (mode: 'move' | 'pass' | null) => void\n resetTurn: () => void\n /** Return the store to its initial state. */\n reset: () => void\n movePiece: (pieceId: string, to: Position) => void\n passBall: (to: Position) => void\n endTurn: () => void\n}\n\nconst INITIAL = {\n boardState: null,\n selectedPieceId: null,\n interactionMode: null,\n} satisfies Pick<GameStore, 'boardState' | 'selectedPieceId' | 'interactionMode'>\n\nexport const useGameStore = create<GameStore>((set) => ({\n ...INITIAL,\n\n setBoardState: (state) => set({ boardState: state }),\n setSelectedPieceId: (id) => set({ selectedPieceId: id }),\n setInteractionMode: (mode) => set({ interactionMode: mode }),\n\n /**\n * The store is a module-global singleton, so a new match opens on the\n * previous match's final board — which ended on a goal, and was therefore\n * re-detected as a fresh goal (the bug behind webapp PR #42). Apps were\n * each reaching for `useGameStore.setState({...})` by hand to work around\n * it; this makes the intent explicit and keeps the shape in one place.\n */\n reset: () => set({ ...INITIAL }),\n\n resetTurn: () => set((state) => {\n if (!state.boardState) return state\n return {\n boardState: {\n ...state.boardState,\n actionPoints: state.boardState.maxActionPoints ?? DEFAULT_MAX_AP,\n pieces: state.boardState.pieces.map(p => ({ ...p, hasMovedThisTurn: false })),\n },\n }\n }),\n\n movePiece: (pieceId, to) => set((state) => {\n if (!state.boardState || state.boardState.actionPoints <= 0) return state\n const piece = state.boardState.pieces.find(p => p.id === pieceId)\n if (!piece || piece.hasMovedThisTurn) return state\n\n const { boardState } = applyMove(state.boardState, pieceId, to)\n return { boardState, selectedPieceId: null, interactionMode: null }\n }),\n\n passBall: (to) => set((state) => {\n if (!state.boardState || state.boardState.actionPoints <= 0 || !state.boardState.ball.holderId) return state\n const holder = state.boardState.pieces.find(p => p.id === state.boardState!.ball.holderId)\n if (!holder) return state\n\n const { boardState, forcedTurnEnd } = applyPass(state.boardState, to)\n const keepSelection = !forcedTurnEnd && boardState.ball.holderId\n return {\n boardState,\n selectedPieceId: keepSelection ? boardState.ball.holderId : null,\n interactionMode: keepSelection ? 'pass' : null,\n }\n }),\n\n endTurn: () => set((state) => {\n if (!state.boardState) return state\n return { boardState: applyEndTurn(state.boardState) }\n }),\n}))\n"]}