@ankhorage/zora 21.1.4 → 22.0.1

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.
Files changed (109) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +4 -1
  3. package/dist/features/authoring/index.d.ts +0 -4
  4. package/dist/features/authoring/index.d.ts.map +1 -1
  5. package/dist/features/authoring/index.js +0 -3
  6. package/dist/features/authoring/index.js.map +1 -1
  7. package/dist/features/chess/adapters/inbound/ChessBoard.d.ts +1 -1
  8. package/dist/features/chess/adapters/inbound/ChessBoard.d.ts.map +1 -1
  9. package/dist/features/chess/adapters/inbound/ChessBoard.js +23 -7
  10. package/dist/features/chess/adapters/inbound/ChessBoard.js.map +1 -1
  11. package/dist/features/chess/meta.d.ts +68 -0
  12. package/dist/features/chess/meta.d.ts.map +1 -1
  13. package/dist/features/chess/meta.js +36 -1
  14. package/dist/features/chess/meta.js.map +1 -1
  15. package/dist/features/chess/public.d.ts +3 -1
  16. package/dist/features/chess/public.d.ts.map +1 -1
  17. package/dist/features/chess/public.js +2 -0
  18. package/dist/features/chess/public.js.map +1 -1
  19. package/dist/features/chess/utils/chessEngine.d.ts +8 -0
  20. package/dist/features/chess/utils/chessEngine.d.ts.map +1 -0
  21. package/dist/features/chess/utils/chessEngine.js +71 -0
  22. package/dist/features/chess/utils/chessEngine.js.map +1 -0
  23. package/dist/features/tabletop/meta.d.ts +32 -0
  24. package/dist/features/tabletop/meta.d.ts.map +1 -1
  25. package/dist/features/tabletop/meta.js +24 -0
  26. package/dist/features/tabletop/meta.js.map +1 -1
  27. package/dist/features/tabletop/public.d.ts +1 -0
  28. package/dist/features/tabletop/public.d.ts.map +1 -1
  29. package/dist/features/tabletop/public.js +1 -0
  30. package/dist/features/tabletop/public.js.map +1 -1
  31. package/dist/index.d.ts +4 -7
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +3 -5
  34. package/dist/index.js.map +1 -1
  35. package/dist/types/chess.d.ts +10 -0
  36. package/dist/types/chess.d.ts.map +1 -1
  37. package/dist/types/chess.js.map +1 -1
  38. package/package.json +5 -4
  39. package/src/features/authoring/featureManifest.test.ts +46 -10
  40. package/src/features/authoring/index.ts +0 -8
  41. package/src/features/chess/adapters/inbound/ChessBoard.tsx +34 -11
  42. package/src/features/chess/meta.ts +36 -1
  43. package/src/features/chess/public.test.tsx +83 -0
  44. package/src/features/chess/public.ts +3 -0
  45. package/src/features/chess/utils/chessEngine.test.ts +35 -0
  46. package/src/features/chess/utils/chessEngine.ts +83 -0
  47. package/src/features/chessTabletopOwnership.test.ts +8 -4
  48. package/src/features/tabletop/meta.ts +24 -0
  49. package/src/features/tabletop/public.test.tsx +12 -0
  50. package/src/features/tabletop/public.ts +1 -0
  51. package/src/index.ts +7 -15
  52. package/src/structureOwnership.test.ts +2 -7
  53. package/src/types/chess.ts +11 -0
  54. package/web-dist/chunks/Accordion-784f23tq.js +3813 -0
  55. package/web-dist/components/card-back/CardBack.js +1 -1
  56. package/web-dist/components/card-hand/CardHand.js +1 -1
  57. package/web-dist/components/chess-board/ChessBoard.js +1 -1
  58. package/web-dist/components/opening-book/OpeningBook.js +1 -1
  59. package/web-dist/components/playing-card/PlayingCard.js +1 -1
  60. package/web-dist/components/poker-training-table/PokerTrainingTable.js +1 -1
  61. package/web-dist/components/tabletop-table/TabletopTable.js +1 -1
  62. package/web-dist/manifest.json +12 -24
  63. package/dist/features/plugin/ZORA_CORE_PLUGIN.d.ts +0 -4
  64. package/dist/features/plugin/ZORA_CORE_PLUGIN.d.ts.map +0 -1
  65. package/dist/features/plugin/ZORA_CORE_PLUGIN.js +0 -8
  66. package/dist/features/plugin/ZORA_CORE_PLUGIN.js.map +0 -1
  67. package/dist/features/plugin/ZORA_CORE_PLUGIN_METADATA.d.ts +0 -3
  68. package/dist/features/plugin/ZORA_CORE_PLUGIN_METADATA.d.ts.map +0 -1
  69. package/dist/features/plugin/ZORA_CORE_PLUGIN_METADATA.js +0 -36
  70. package/dist/features/plugin/ZORA_CORE_PLUGIN_METADATA.js.map +0 -1
  71. package/dist/features/plugin/application/use-cases/composeZoraPluginMetadata.d.ts +0 -4
  72. package/dist/features/plugin/application/use-cases/composeZoraPluginMetadata.d.ts.map +0 -1
  73. package/dist/features/plugin/application/use-cases/composeZoraPluginMetadata.js +0 -106
  74. package/dist/features/plugin/application/use-cases/composeZoraPluginMetadata.js.map +0 -1
  75. package/dist/features/plugin/application/use-cases/composeZoraPlugins.d.ts +0 -4
  76. package/dist/features/plugin/application/use-cases/composeZoraPlugins.d.ts.map +0 -1
  77. package/dist/features/plugin/application/use-cases/composeZoraPlugins.js +0 -41
  78. package/dist/features/plugin/application/use-cases/composeZoraPlugins.js.map +0 -1
  79. package/dist/features/plugin/domain/ZoraPluginCompositionError.d.ts +0 -14
  80. package/dist/features/plugin/domain/ZoraPluginCompositionError.d.ts.map +0 -1
  81. package/dist/features/plugin/domain/ZoraPluginCompositionError.js +0 -14
  82. package/dist/features/plugin/domain/ZoraPluginCompositionError.js.map +0 -1
  83. package/dist/features/plugin/public.d.ts +0 -4
  84. package/dist/features/plugin/public.d.ts.map +0 -1
  85. package/dist/features/plugin/public.js +0 -4
  86. package/dist/features/plugin/public.js.map +0 -1
  87. package/dist/features/plugin/runtime.d.ts +0 -3
  88. package/dist/features/plugin/runtime.d.ts.map +0 -1
  89. package/dist/features/plugin/runtime.js +0 -3
  90. package/dist/features/plugin/runtime.js.map +0 -1
  91. package/dist/types/plugin.d.ts +0 -29
  92. package/dist/types/plugin.d.ts.map +0 -1
  93. package/dist/types/plugin.js +0 -2
  94. package/dist/types/plugin.js.map +0 -1
  95. package/src/features/plugin/ZORA_CORE_PLUGIN.ts +0 -9
  96. package/src/features/plugin/ZORA_CORE_PLUGIN_METADATA.ts +0 -49
  97. package/src/features/plugin/application/use-cases/composeZoraPluginMetadata.ts +0 -166
  98. package/src/features/plugin/application/use-cases/composeZoraPlugins.ts +0 -76
  99. package/src/features/plugin/domain/ZoraPluginCompositionError.ts +0 -21
  100. package/src/features/plugin/pluginComposition.test.ts +0 -139
  101. package/src/features/plugin/public.ts +0 -3
  102. package/src/features/plugin/runtime.ts +0 -2
  103. package/src/types/plugin.ts +0 -43
  104. package/web-dist/chunks/Accordion-86z2tvvz.js +0 -321
  105. package/web-dist/components/zora-plugin-composition-error/ZoraPluginCompositionError.d.ts +0 -3
  106. package/web-dist/components/zora-plugin-composition-error/ZoraPluginCompositionError.js +0 -17
  107. package/web-dist/components/zora-plugin-composition-error/index.d.ts +0 -1
  108. package/web-dist/components/zora-plugin-composition-error/index.js +0 -2
  109. /package/web-dist/chunks/{Accordion-57mn1j9e.js → Accordion-z585n0mr.js} +0 -0
@@ -1,9 +1,15 @@
1
1
  import React from 'react';
2
2
  import { Pressable, StyleSheet, Text, View } from 'react-native';
3
3
 
4
- import type { ChessBoardProps, ChessPieceState, ChessSquareId } from '../../../../types/chess';
4
+ import type {
5
+ ChessBoardProps,
6
+ ChessMoveAttempt,
7
+ ChessPieceState,
8
+ ChessSquareId,
9
+ } from '../../../../types/chess';
5
10
  import { withZoraThemeScope } from '../../../theme/adapters/inbound/withZoraThemeScope';
6
11
  import { useZoraTheme } from '../../../theme/composition/useZoraTheme';
12
+ import { getLegalTargets, readChessPieces, tryMove } from '../../utils/chessEngine';
7
13
  import { createBoardSquares } from '../../utils/createBoardSquares';
8
14
  import { createChessBoardColorScheme } from '../../utils/createChessBoardColorScheme';
9
15
  import { isLightSquare } from '../../utils/isLightSquare';
@@ -23,21 +29,25 @@ const symbols = new Map<string, string>([
23
29
  ['black:k', '♚'],
24
30
  ]);
25
31
 
26
- /*** Renders caller-owned chess presentation state without chess rule execution. */
32
+ /*** Renders caller-owned or FEN-backed chess state with optional legal-move validation. */
27
33
  export const ChessBoard = withZoraThemeScope(ChessBoardInner);
28
34
 
29
- /*** Renders the current presentation state without executing chess rules. */
35
+ /*** Renders the current presentation state and delegates FEN validation to the core chess engine. */
30
36
  function ChessBoardInner({
31
- pieces = [],
37
+ fen,
38
+ pieces,
32
39
  orientation = 'white',
33
40
  selectedSquare = null,
34
- legalTargets = [],
41
+ legalTargets,
35
42
  lastMove = null,
36
43
  disabled = false,
37
44
  showCoordinates = false,
45
+ validateMoves = true,
38
46
  colorScheme,
39
47
  onSquarePress,
40
48
  onMoveAttempt,
49
+ onLegalMove,
50
+ onInvalidMove,
41
51
  renderPiece,
42
52
  interactionPolicy,
43
53
  testID,
@@ -45,15 +55,28 @@ function ChessBoardInner({
45
55
  const { theme } = useZoraTheme();
46
56
  const colors = createChessBoardColorScheme(theme, colorScheme);
47
57
  const squares = createBoardSquares(orientation);
48
- const piecesBySquare = new Map<ChessSquareId, ChessPieceState>(
49
- pieces.map((piece) => [piece.square, piece]),
50
- );
51
- const legalTargetSet = new Set(legalTargets);
58
+ const piecesBySquare =
59
+ pieces === undefined
60
+ ? fen === undefined
61
+ ? new Map<ChessSquareId, ChessPieceState>()
62
+ : new Map(readChessPieces(fen))
63
+ : new Map<ChessSquareId, ChessPieceState>(pieces.map((piece) => [piece.square, piece]));
64
+ const resolvedLegalTargets =
65
+ legalTargets ??
66
+ (fen !== undefined && selectedSquare !== null ? getLegalTargets(fen, selectedSquare) : []);
67
+ const legalTargetSet = new Set(resolvedLegalTargets);
52
68
  const pressSquare = (square: ChessSquareId) => {
53
69
  if (disabled || interactionPolicy === 'passive') return;
54
70
  onSquarePress?.(square);
55
- if (selectedSquare !== null && selectedSquare !== square)
56
- onMoveAttempt?.({ from: selectedSquare, to: square });
71
+ if (selectedSquare === null || selectedSquare === square) return;
72
+
73
+ const attempt: ChessMoveAttempt = { from: selectedSquare, to: square };
74
+ onMoveAttempt?.(attempt);
75
+ if (!validateMoves || fen === undefined) return;
76
+
77
+ const result = tryMove(fen, attempt);
78
+ if (result) onLegalMove?.(result);
79
+ else onInvalidMove?.(attempt);
57
80
  };
58
81
  return (
59
82
  <View
@@ -9,7 +9,7 @@ export const chessBoardMeta = {
9
9
  allowedChildren: [],
10
10
  blueprint: {
11
11
  label: 'Chess board',
12
- defaultProps: { pieces: [], orientation: 'white', showCoordinates: true },
12
+ defaultProps: { pieces: [], orientation: 'white', showCoordinates: true, validateMoves: true },
13
13
  },
14
14
  events: {
15
15
  squarePress: {
@@ -23,6 +23,28 @@ export const chessBoardMeta = {
23
23
  payloadFields: [
24
24
  { path: 'from', type: 'string', label: 'From' },
25
25
  { path: 'to', type: 'string', label: 'To' },
26
+ { path: 'promotion', type: 'string', label: 'Promotion' },
27
+ ],
28
+ },
29
+ legalMove: {
30
+ label: 'Legal move',
31
+ eventType: 'chess.legalMove',
32
+ payloadFields: [
33
+ { path: 'from', type: 'string', label: 'From' },
34
+ { path: 'to', type: 'string', label: 'To' },
35
+ { path: 'fen', type: 'string', label: 'Position' },
36
+ { path: 'san', type: 'string', label: 'SAN' },
37
+ { path: 'lan', type: 'string', label: 'LAN' },
38
+ { path: 'promotion', type: 'string', label: 'Promotion' },
39
+ ],
40
+ },
41
+ invalidMove: {
42
+ label: 'Invalid move',
43
+ eventType: 'chess.invalidMove',
44
+ payloadFields: [
45
+ { path: 'from', type: 'string', label: 'From' },
46
+ { path: 'to', type: 'string', label: 'To' },
47
+ { path: 'promotion', type: 'string', label: 'Promotion' },
26
48
  ],
27
49
  },
28
50
  },
@@ -43,6 +65,12 @@ export const chessBoardMeta = {
43
65
  },
44
66
  },
45
67
  props: {
68
+ fen: {
69
+ type: 'string',
70
+ category: 'Position',
71
+ label: 'FEN',
72
+ authoring: { authority: 'instance' },
73
+ },
46
74
  pieces: {
47
75
  type: 'array',
48
76
  category: 'Board',
@@ -78,6 +106,13 @@ export const chessBoardMeta = {
78
106
  default: false,
79
107
  authoring: { authority: 'instance' },
80
108
  },
109
+ validateMoves: {
110
+ type: 'boolean',
111
+ category: 'Behavior',
112
+ label: 'Validate moves',
113
+ default: true,
114
+ authoring: { authority: 'instance' },
115
+ },
81
116
  disabled: {
82
117
  type: 'boolean',
83
118
  category: 'State',
@@ -7,6 +7,7 @@ import React, { act } from 'react';
7
7
  import { createRoot } from 'react-dom/client';
8
8
  import { renderToStaticMarkup } from 'react-dom/server';
9
9
 
10
+ import { chessBoardMeta } from './meta';
10
11
  import type {
11
12
  ChessBoard as ChessBoardComponent,
12
13
  OpeningBook as OpeningBookComponent,
@@ -30,6 +31,24 @@ describe('Chess presentation', () => {
30
31
  expect(typeof createChessBoardColorScheme).toBe('function');
31
32
  });
32
33
 
34
+ test('preserves FEN and validated-move authoring metadata', () => {
35
+ expect(chessBoardMeta.props.fen.type).toBe('string');
36
+ expect(chessBoardMeta.props.validateMoves.default).toBe(true);
37
+ expect(chessBoardMeta.events.legalMove.payloadFields.map((field) => field.path)).toEqual([
38
+ 'from',
39
+ 'to',
40
+ 'fen',
41
+ 'san',
42
+ 'lan',
43
+ 'promotion',
44
+ ]);
45
+ expect(chessBoardMeta.events.invalidMove.payloadFields.map((field) => field.path)).toEqual([
46
+ 'from',
47
+ 'to',
48
+ 'promotion',
49
+ ]);
50
+ });
51
+
33
52
  test('renders custom React nodes, square IDs, and opening metadata through independent artifacts', async () => {
34
53
  const { ChessBoard } = (await load('components/chess-board/index.js')) as {
35
54
  ChessBoard: typeof ChessBoardComponent;
@@ -88,6 +107,70 @@ describe('Chess presentation', () => {
88
107
  browser.close();
89
108
  });
90
109
 
110
+ test('preserves FEN-backed pieces, legal targets, and legal/invalid move callbacks', async () => {
111
+ const { ChessBoard } = (await load('components/chess-board/index.js')) as {
112
+ ChessBoard: typeof ChessBoardComponent;
113
+ };
114
+ const browser = new Window();
115
+ const keys = ['window', 'document', 'Node', 'navigator', 'IS_REACT_ACT_ENVIRONMENT'] as const;
116
+ const previous = keys.map(
117
+ (key) => [key, Object.getOwnPropertyDescriptor(globalThis, key)] as const,
118
+ );
119
+ Object.assign(globalThis, {
120
+ window: browser,
121
+ document: browser.document,
122
+ Node: browser.Node,
123
+ navigator: browser.navigator,
124
+ IS_REACT_ACT_ENVIRONMENT: true,
125
+ });
126
+ const host = document.createElement('div');
127
+ document.body.appendChild(host);
128
+ const legal: string[] = [];
129
+ const invalid: string[] = [];
130
+ const root = createRoot(host);
131
+
132
+ try {
133
+ act(() =>
134
+ root.render(
135
+ <ChessBoard
136
+ fen="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
137
+ onInvalidMove={(move) => invalid.push(`${move.from}-${move.to}`)}
138
+ onLegalMove={(move) => legal.push(`${move.lan}:${move.san}`)}
139
+ selectedSquare="e2"
140
+ showCoordinates
141
+ testID="fen-board"
142
+ />,
143
+ ),
144
+ );
145
+
146
+ expect(host.querySelector('[data-testid="fen-board-square-e1"]')?.textContent).toContain('♔');
147
+ expect(
148
+ host.querySelector('[data-testid="fen-board-square-e4"]')?.getAttribute('style'),
149
+ ).toContain('background-color');
150
+
151
+ act(() => {
152
+ host
153
+ .querySelector('[data-testid="fen-board-square-e4"]')
154
+ ?.dispatchEvent(new browser.MouseEvent('click', { bubbles: true }));
155
+ });
156
+ expect(legal).toEqual(['e2e4:e4']);
157
+
158
+ act(() => {
159
+ host
160
+ .querySelector('[data-testid="fen-board-square-e5"]')
161
+ ?.dispatchEvent(new browser.MouseEvent('click', { bubbles: true }));
162
+ });
163
+ expect(invalid).toEqual(['e2-e5']);
164
+ } finally {
165
+ act(() => root.unmount());
166
+ browser.close();
167
+ for (const [key, descriptor] of previous) {
168
+ if (descriptor) Object.defineProperty(globalThis, key, descriptor);
169
+ else Reflect.deleteProperty(globalThis, key);
170
+ }
171
+ }
172
+ });
173
+
91
174
  test('emits only caller-owned intents in active mode and suppresses them in passive mode', async () => {
92
175
  const { ChessBoard } = (await load('components/chess-board/index.js')) as {
93
176
  ChessBoard: typeof ChessBoardComponent;
@@ -4,6 +4,7 @@ export type {
4
4
  ChessBoardOrientation,
5
5
  ChessBoardProps,
6
6
  ChessMoveAttempt,
7
+ ChessMoveResult,
7
8
  ChessPieceCode,
8
9
  ChessPieceRenderContext,
9
10
  ChessPieceRenderer,
@@ -17,6 +18,8 @@ export type {
17
18
  } from '../../types/chess';
18
19
  export { ChessBoard } from './adapters/inbound/ChessBoard';
19
20
  export { OpeningBook } from './adapters/inbound/OpeningBook';
21
+ export { chessBoardMeta, openingBookMeta } from './meta';
22
+ export { getLegalTargets, readChessPieces, tryMove } from './utils/chessEngine';
20
23
  export { chessSquares } from './utils/chessSquares';
21
24
  export { createBoardSquares } from './utils/createBoardSquares';
22
25
  export type { ChessColorThemeShape } from './utils/createChessBoardColorScheme';
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { getLegalTargets, readChessPieces, tryMove } from './chessEngine';
4
+
5
+ const START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
6
+
7
+ describe('core chess engine', () => {
8
+ it('reads pieces from FEN and preserves piece codes', () => {
9
+ const pieces = readChessPieces(START_FEN);
10
+
11
+ expect(pieces.size).toBe(32);
12
+ expect(pieces.get('e1')?.piece).toBe('K');
13
+ expect(pieces.get('e8')?.piece).toBe('k');
14
+ });
15
+
16
+ it('returns an empty map for invalid FEN', () => {
17
+ expect(readChessPieces('invalid').size).toBe(0);
18
+ });
19
+
20
+ it('returns legal target squares', () => {
21
+ expect(getLegalTargets(START_FEN, 'e2')).toEqual(['e3', 'e4']);
22
+ });
23
+
24
+ it('returns resulting move details for legal moves', () => {
25
+ const result = tryMove(START_FEN, { from: 'e2', to: 'e4' });
26
+
27
+ expect(result?.san).toBe('e4');
28
+ expect(result?.lan).toBe('e2e4');
29
+ expect(result?.fen).toContain(' b ');
30
+ });
31
+
32
+ it('returns null for invalid moves', () => {
33
+ expect(tryMove(START_FEN, { from: 'e2', to: 'e5' })).toBeNull();
34
+ });
35
+ });
@@ -0,0 +1,83 @@
1
+ import { Chess, type Move } from 'chess.js';
2
+
3
+ import type {
4
+ ChessMoveAttempt,
5
+ ChessMoveResult,
6
+ ChessPieceCode,
7
+ ChessPieceState,
8
+ ChessPromotionPiece,
9
+ ChessSquareId,
10
+ } from '../../../types/chess';
11
+
12
+ /*** Create a chess.js position from FEN without leaking invalid-position exceptions. */
13
+ function createChess(fen: string): Chess | null {
14
+ try {
15
+ return new Chess(fen);
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ /*** Convert a chess.js piece into the public ZORA piece code. */
22
+ function toPieceCode(piece: { readonly color: 'b' | 'w'; readonly type: string }): ChessPieceCode {
23
+ return piece.color === 'w' ? piece.type.toUpperCase() : piece.type.toLowerCase();
24
+ }
25
+
26
+ /*** Normalize a chess.js promotion marker into the public promotion union. */
27
+ function toPromotionPiece(piece: string | undefined): ChessPromotionPiece | undefined {
28
+ return piece === 'q' || piece === 'r' || piece === 'b' || piece === 'n' ? piece : undefined;
29
+ }
30
+
31
+ /*** Convert one successful chess.js move into the public result contract. */
32
+ function toMoveResult(move: Move, fen: string): ChessMoveResult {
33
+ return {
34
+ fen,
35
+ from: move.from,
36
+ lan: move.lan,
37
+ promotion: toPromotionPiece(move.promotion),
38
+ san: move.san,
39
+ to: move.to,
40
+ };
41
+ }
42
+
43
+ /*** Read all pieces from a FEN-backed position. Invalid FEN produces an empty position. */
44
+ export function readChessPieces(fen: string): ReadonlyMap<ChessSquareId, ChessPieceState> {
45
+ const chess = createChess(fen);
46
+ const pieces = new Map<ChessSquareId, ChessPieceState>();
47
+ if (!chess) return pieces;
48
+
49
+ for (const rank of chess.board()) {
50
+ for (const piece of rank) {
51
+ if (!piece) continue;
52
+ pieces.set(piece.square, {
53
+ color: piece.color === 'w' ? 'white' : 'black',
54
+ piece: toPieceCode(piece),
55
+ square: piece.square,
56
+ });
57
+ }
58
+ }
59
+ return pieces;
60
+ }
61
+
62
+ /*** Resolve legal destination squares from one FEN position and source square. */
63
+ export function getLegalTargets(fen: string, from: ChessSquareId): readonly ChessSquareId[] {
64
+ const chess = createChess(fen);
65
+ return chess ? chess.moves({ square: from, verbose: true }).map((move) => move.to) : [];
66
+ }
67
+
68
+ /*** Attempt one legal chess move against FEN and return the resulting position/move notation. */
69
+ export function tryMove(fen: string, attempt: ChessMoveAttempt): ChessMoveResult | null {
70
+ const chess = createChess(fen);
71
+ if (!chess) return null;
72
+
73
+ try {
74
+ const move = chess.move({
75
+ from: attempt.from,
76
+ promotion: attempt.promotion,
77
+ to: attempt.to,
78
+ });
79
+ return toMoveResult(move, chess.fen());
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
@@ -10,7 +10,7 @@ test('Chess and Tabletop implementation modules own one matching runtime export
10
10
  for (const featureRoot of featureRoots) {
11
11
  for (const directory of [join(featureRoot, 'adapters/inbound'), join(featureRoot, 'utils')]) {
12
12
  for (const name of readdirSync(directory)) {
13
- if (!['.ts', '.tsx'].includes(extname(name))) continue;
13
+ if (!['.ts', '.tsx'].includes(extname(name)) || name.includes('.test.')) continue;
14
14
  const source = ts.createSourceFile(
15
15
  name,
16
16
  readFileSync(join(directory, name), 'utf8'),
@@ -32,13 +32,17 @@ test('Chess and Tabletop implementation modules own one matching runtime export
32
32
  );
33
33
  return [];
34
34
  });
35
- expect(runtimeExports, join(directory, name)).toEqual([parse(name).name]);
35
+ const expectedExports =
36
+ name === 'chessEngine.ts'
37
+ ? ['getLegalTargets', 'readChessPieces', 'tryMove']
38
+ : [parse(name).name];
39
+ expect(runtimeExports.sort(), join(directory, name)).toEqual(expectedExports.sort());
36
40
  }
37
41
  }
38
42
  }
39
43
  });
40
44
 
41
- test('Chess and Tabletop source graphs contain no standalone package or game-service imports', () => {
45
+ test('Chess and Tabletop source graphs depend only on their intentional runtime owners', () => {
42
46
  const sourcePaths = featureRoots.flatMap((featureRoot) => {
43
47
  const visit = (directory: string): string[] =>
44
48
  readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
@@ -63,7 +67,7 @@ test('Chess and Tabletop source graphs contain no standalone package or game-ser
63
67
  for (const specifier of imports) {
64
68
  if (specifier.startsWith('.')) continue;
65
69
  expect(
66
- ['react', 'react-native', '@ankhorage/color-theory'],
70
+ ['react', 'react-native', 'chess.js', '@ankhorage/color-theory'],
67
71
  `${path}: ${specifier}`,
68
72
  ).toContain(specifier);
69
73
  }
@@ -116,6 +116,24 @@ export const tabletopTableMeta = {
116
116
  acceptsFallback: true,
117
117
  acceptsTransforms: true,
118
118
  },
119
+ centerLabel: {
120
+ label: 'Center label',
121
+ value: { type: 'string' },
122
+ acceptsFallback: true,
123
+ acceptsTransforms: true,
124
+ },
125
+ centerSublabel: {
126
+ label: 'Center sublabel',
127
+ value: { type: 'string' },
128
+ acceptsFallback: true,
129
+ acceptsTransforms: true,
130
+ },
131
+ disabled: {
132
+ label: 'Disabled',
133
+ value: { type: 'boolean' },
134
+ acceptsFallback: true,
135
+ acceptsTransforms: true,
136
+ },
119
137
  },
120
138
  },
121
139
  props: {
@@ -211,6 +229,12 @@ export const pokerTrainingTableMeta = {
211
229
  },
212
230
  acceptsFallback: true,
213
231
  },
232
+ disabled: {
233
+ label: 'Disabled',
234
+ value: { type: 'boolean' },
235
+ acceptsFallback: true,
236
+ acceptsTransforms: true,
237
+ },
214
238
  },
215
239
  },
216
240
  props: {
@@ -6,6 +6,7 @@ import { Window } from 'happy-dom';
6
6
  import React from 'react';
7
7
  import { renderToStaticMarkup } from 'react-dom/server';
8
8
 
9
+ import { pokerTrainingTableMeta, tabletopTableMeta } from './meta';
9
10
  import type {
10
11
  CardHand as CardHandComponent,
11
12
  PokerTrainingTable as PokerTrainingTableComponent,
@@ -65,6 +66,17 @@ describe('Tabletop presentation', () => {
65
66
  ]);
66
67
  });
67
68
 
69
+ test('preserves the standalone Tabletop authoring bindings in core metadata', () => {
70
+ expect(Object.keys(tabletopTableMeta.bindings.props)).toEqual([
71
+ 'seats',
72
+ 'centerCards',
73
+ 'centerLabel',
74
+ 'centerSublabel',
75
+ 'disabled',
76
+ ]);
77
+ expect(pokerTrainingTableMeta.bindings.props.disabled.value.type).toBe('boolean');
78
+ });
79
+
68
80
  test('renders card sizing, per-card IDs, seat accessibility, and table shapes', async () => {
69
81
  const { CardHand } = (await load('components/card-hand/index.js')) as {
70
82
  CardHand: typeof CardHandComponent;
@@ -27,6 +27,7 @@ export { CardHand } from './adapters/inbound/CardHand';
27
27
  export { PlayingCard } from './adapters/inbound/PlayingCard';
28
28
  export { PokerTrainingTable } from './adapters/inbound/PokerTrainingTable';
29
29
  export { TabletopTable } from './adapters/inbound/TabletopTable';
30
+ export { pokerTrainingTableMeta, tabletopTableMeta } from './meta';
30
31
  export { createPokerTrainingTableState } from './utils/createPokerTrainingTableState';
31
32
  export { createTabletopColorScheme } from './utils/createTabletopColorScheme';
32
33
  export { createTabletopGameSeats } from './utils/createTabletopGameSeats';
package/src/index.ts CHANGED
@@ -131,6 +131,7 @@ export type {
131
131
  ChessBoardOrientation,
132
132
  ChessBoardProps,
133
133
  ChessMoveAttempt,
134
+ ChessMoveResult,
134
135
  ChessPieceCode,
135
136
  ChessPieceRenderContext,
136
137
  ChessPieceRenderer,
@@ -143,15 +144,18 @@ export type {
143
144
  OpeningBookProps,
144
145
  } from './features/chess/public';
145
146
  export type { ChessColorThemeShape } from './features/chess/public';
146
- export { ChessBoard, OpeningBook } from './features/chess/public';
147
+ export { ChessBoard, chessBoardMeta, OpeningBook, openingBookMeta } from './features/chess/public';
147
148
  export {
148
149
  chessSquares,
149
150
  createBoardSquares,
150
151
  createChessBoardColorScheme,
151
152
  createOpeningBookColorScheme,
153
+ getLegalTargets,
152
154
  getSquareFile,
153
155
  getSquareRank,
154
156
  isLightSquare,
157
+ readChessPieces,
158
+ tryMove,
155
159
  } from './features/chess/public';
156
160
  export type { ChipProps } from './features/chip/public';
157
161
  export type { ChipGroupItem, ChipGroupProps } from './features/chip/public';
@@ -319,12 +323,6 @@ export { MissingElement, type MissingElementProps } from './features/missing-ele
319
323
  export { Pagination, type PaginationProps } from './features/pagination/public';
320
324
  export type { PaletteItemProps } from './features/palette-item/public';
321
325
  export { PaletteItem } from './features/palette-item/public';
322
- export {
323
- composeZoraPluginMetadata,
324
- ZORA_CORE_PLUGIN_METADATA,
325
- ZoraPluginCompositionError,
326
- } from './features/plugin/public';
327
- export { composeZoraPlugins, ZORA_CORE_PLUGIN } from './features/plugin/runtime';
328
326
  export {
329
327
  PopoverMenu,
330
328
  type PopoverMenuAction,
@@ -423,7 +421,9 @@ export {
423
421
  createTabletopGameSeats,
424
422
  PlayingCard,
425
423
  PokerTrainingTable,
424
+ pokerTrainingTableMeta,
426
425
  TabletopTable,
426
+ tabletopTableMeta,
427
427
  } from './features/tabletop/public';
428
428
  export {
429
429
  Tab,
@@ -495,12 +495,4 @@ export type {
495
495
  ValidateUploadAssetInput,
496
496
  } from './features/uploader/public';
497
497
  export { Uploader, validateUploadAsset } from './features/uploader/public';
498
- export type {
499
- ComposedZoraPluginCatalog,
500
- ComposedZoraPluginMetadataCatalog,
501
- ZoraPluginCompositionErrorCode,
502
- ZoraPluginDescriptor,
503
- ZoraPluginMetadata,
504
- ZoraPluginPlacement,
505
- } from './types/plugin';
506
498
  export type { ZoraComponentRegistry } from './types/registry';
@@ -26,10 +26,9 @@ describe('src ownership', () => {
26
26
  }
27
27
  });
28
28
 
29
- test('keeps reusable authoring, plugin, registry, and theme types centralized', () => {
29
+ test('keeps reusable authoring, registry, and theme types centralized', () => {
30
30
  for (const path of [
31
31
  'src/types/authoring.ts',
32
- 'src/types/plugin.ts',
33
32
  'src/types/registry.ts',
34
33
  'src/types/theme.ts',
35
34
  'src/types/theme-recipe.ts',
@@ -49,8 +48,6 @@ describe('src ownership', () => {
49
48
 
50
49
  test('keeps deliberate feature facades instead of legacy catch-all implementation files', () => {
51
50
  for (const path of [
52
- 'src/features/plugin/public.ts',
53
- 'src/features/plugin/runtime.ts',
54
51
  'src/features/registry/public.ts',
55
52
  'src/features/theme/public.ts',
56
53
  'src/features/theme/runtime.ts',
@@ -59,9 +56,7 @@ describe('src ownership', () => {
59
56
  }
60
57
 
61
58
  for (const path of [
62
- 'src/features/plugin/pluginComposition.ts',
63
- 'src/features/plugin/corePlugin.ts',
64
- 'src/features/plugin/corePluginMetadata.ts',
59
+ 'src/features/plugin',
65
60
  'src/features/registry/registry.ts',
66
61
  'src/features/theme/index.ts',
67
62
  ]) {
@@ -14,6 +14,13 @@ export interface ChessMoveAttempt {
14
14
  readonly promotion?: ChessPromotionPiece;
15
15
  }
16
16
 
17
+ export interface ChessMoveResult extends Omit<ChessMoveAttempt, 'promotion'> {
18
+ readonly fen: string;
19
+ readonly lan: string;
20
+ readonly promotion?: ChessPromotionPiece;
21
+ readonly san: string;
22
+ }
23
+
17
24
  export interface ChessPieceState {
18
25
  readonly color: 'black' | 'white';
19
26
  readonly piece: ChessPieceCode;
@@ -58,6 +65,7 @@ export interface OpeningBookColorScheme {
58
65
  export type OpeningBookColorOverrides = Partial<OpeningBookColorScheme>;
59
66
 
60
67
  export interface ChessBoardProps extends ZoraBaseProps {
68
+ readonly fen?: string;
61
69
  readonly pieces?: readonly ChessPieceState[];
62
70
  readonly orientation?: ChessBoardOrientation;
63
71
  readonly selectedSquare?: ChessSquareId | null;
@@ -65,9 +73,12 @@ export interface ChessBoardProps extends ZoraBaseProps {
65
73
  readonly lastMove?: ChessMoveAttempt | null;
66
74
  readonly disabled?: boolean;
67
75
  readonly showCoordinates?: boolean;
76
+ readonly validateMoves?: boolean;
68
77
  readonly colorScheme?: ChessBoardColorOverrides;
69
78
  readonly onSquarePress?: (square: ChessSquareId) => void;
70
79
  readonly onMoveAttempt?: (move: ChessMoveAttempt) => void;
80
+ readonly onLegalMove?: (move: ChessMoveResult) => void;
81
+ readonly onInvalidMove?: (move: ChessMoveAttempt) => void;
71
82
  readonly renderPiece?: ChessPieceRenderer;
72
83
  }
73
84