@ankhorage/zora 25.11.1 → 26.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 (97) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -4
  3. package/dist/capabilities/index.js +65 -41
  4. package/dist/capabilities/index.js.map +3 -3
  5. package/dist/features/chess/adapters/inbound/ChessBoard.d.ts +1 -1
  6. package/dist/features/chess/adapters/inbound/ChessBoard.d.ts.map +1 -1
  7. package/dist/features/chess/adapters/inbound/ChessBoard.js +130 -83
  8. package/dist/features/chess/adapters/inbound/ChessBoard.js.map +1 -1
  9. package/dist/features/chess/meta.d.ts +97 -72
  10. package/dist/features/chess/meta.d.ts.map +1 -1
  11. package/dist/features/chess/meta.js +64 -40
  12. package/dist/features/chess/meta.js.map +1 -1
  13. package/dist/features/chess/public.d.ts +1 -7
  14. package/dist/features/chess/public.d.ts.map +1 -1
  15. package/dist/features/chess/public.js +0 -6
  16. package/dist/features/chess/public.js.map +1 -1
  17. package/dist/features/chess/utils/createChessBoardCellPressHandler.d.ts +6 -0
  18. package/dist/features/chess/utils/createChessBoardCellPressHandler.d.ts.map +1 -0
  19. package/dist/features/chess/utils/createChessBoardCellPressHandler.js +17 -0
  20. package/dist/features/chess/utils/createChessBoardCellPressHandler.js.map +1 -0
  21. package/dist/features/chess/utils/createChessBoardCells.d.ts +12 -0
  22. package/dist/features/chess/utils/createChessBoardCells.d.ts.map +1 -0
  23. package/dist/features/chess/utils/createChessBoardCells.js +28 -0
  24. package/dist/features/chess/utils/createChessBoardCells.js.map +1 -0
  25. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.js +2 -3
  26. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.js.map +1 -1
  27. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.js +3 -1
  28. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.js.map +1 -1
  29. package/dist/features/graph-view/adapters/inbound/web-artifact/getGraphGeometryKey.d.ts +5 -5
  30. package/dist/features/graph-view/adapters/inbound/web-artifact/getGraphGeometryKey.d.ts.map +1 -1
  31. package/dist/features/graph-view/adapters/inbound/web-artifact/getGraphGeometryKey.js +13 -7
  32. package/dist/features/graph-view/adapters/inbound/web-artifact/getGraphGeometryKey.js.map +1 -1
  33. package/dist/readerFree.d.ts +2 -2
  34. package/dist/readerFree.d.ts.map +1 -1
  35. package/dist/readerFree.js +1 -1
  36. package/dist/readerFree.js.map +1 -1
  37. package/dist/types/authoring.d.ts +2 -1
  38. package/dist/types/authoring.d.ts.map +1 -1
  39. package/dist/types/authoring.js.map +1 -1
  40. package/dist/types/chess.d.ts +27 -20
  41. package/dist/types/chess.d.ts.map +1 -1
  42. package/dist/types/chess.js.map +1 -1
  43. package/package.json +34 -94
  44. package/src/features/chess/adapters/inbound/ChessBoard.tsx +263 -112
  45. package/src/features/chess/meta.ts +64 -40
  46. package/src/features/chess/public.test.tsx +77 -228
  47. package/src/features/chess/public.ts +2 -9
  48. package/src/features/chess/utils/createChessBoardCellPressHandler.ts +30 -0
  49. package/src/features/chess/utils/createChessBoardCells.ts +54 -0
  50. package/src/features/chessTabletopOwnership.test.ts +3 -7
  51. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphController.test.ts +1 -2
  52. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphController.ts +2 -3
  53. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.test.ts +45 -0
  54. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.ts +3 -1
  55. package/src/features/graph-view/adapters/inbound/web-artifact/getGraphGeometryKey.test.ts +65 -3
  56. package/src/features/graph-view/adapters/inbound/web-artifact/getGraphGeometryKey.ts +13 -7
  57. package/src/features/registry/componentMeta.test.ts +12 -1
  58. package/src/readerFree.ts +3 -15
  59. package/src/types/authoring.ts +2 -1
  60. package/src/types/chess.ts +28 -21
  61. package/web-dist/chunks/Accordion-91sjb3zh.js +453 -0
  62. package/web-dist/components/chess-board/ChessBoard.js +2 -1
  63. package/web-dist/components/graph-view/GraphView.js +878 -317
  64. package/web-dist/components/opening-book/OpeningBook.js +2 -1
  65. package/web-dist/manifest.json +8 -4
  66. package/dist/features/chess/utils/chessEngine.d.ts +0 -8
  67. package/dist/features/chess/utils/chessEngine.d.ts.map +0 -1
  68. package/dist/features/chess/utils/chessEngine.js +0 -71
  69. package/dist/features/chess/utils/chessEngine.js.map +0 -1
  70. package/dist/features/chess/utils/chessSquares.d.ts +0 -4
  71. package/dist/features/chess/utils/chessSquares.d.ts.map +0 -1
  72. package/dist/features/chess/utils/chessSquares.js +0 -3
  73. package/dist/features/chess/utils/chessSquares.js.map +0 -1
  74. package/dist/features/chess/utils/createBoardSquares.d.ts +0 -4
  75. package/dist/features/chess/utils/createBoardSquares.d.ts.map +0 -1
  76. package/dist/features/chess/utils/createBoardSquares.js +0 -9
  77. package/dist/features/chess/utils/createBoardSquares.js.map +0 -1
  78. package/dist/features/chess/utils/getSquareFile.d.ts +0 -4
  79. package/dist/features/chess/utils/getSquareFile.d.ts.map +0 -1
  80. package/dist/features/chess/utils/getSquareFile.js +0 -5
  81. package/dist/features/chess/utils/getSquareFile.js.map +0 -1
  82. package/dist/features/chess/utils/getSquareRank.d.ts +0 -4
  83. package/dist/features/chess/utils/getSquareRank.d.ts.map +0 -1
  84. package/dist/features/chess/utils/getSquareRank.js +0 -5
  85. package/dist/features/chess/utils/getSquareRank.js.map +0 -1
  86. package/dist/features/chess/utils/isLightSquare.d.ts +0 -4
  87. package/dist/features/chess/utils/isLightSquare.d.ts.map +0 -1
  88. package/dist/features/chess/utils/isLightSquare.js +0 -6
  89. package/dist/features/chess/utils/isLightSquare.js.map +0 -1
  90. package/src/features/chess/utils/chessEngine.test.ts +0 -35
  91. package/src/features/chess/utils/chessEngine.ts +0 -83
  92. package/src/features/chess/utils/chessSquares.ts +0 -6
  93. package/src/features/chess/utils/createBoardSquares.ts +0 -13
  94. package/src/features/chess/utils/getSquareFile.ts +0 -6
  95. package/src/features/chess/utils/getSquareRank.ts +0 -6
  96. package/src/features/chess/utils/isLightSquare.ts +0 -8
  97. package/web-dist/chunks/Accordion-xcm1qc39.js +0 -3813
@@ -9,42 +9,31 @@ export const chessBoardMeta = {
9
9
  allowedChildren: [],
10
10
  blueprint: {
11
11
  label: 'Chess board',
12
- defaultProps: { pieces: [], orientation: 'white', showCoordinates: true, validateMoves: true },
12
+ defaultProps: {
13
+ columns: 8,
14
+ pieces: [],
15
+ orientation: 'white',
16
+ rows: 8,
17
+ showCoordinates: true,
18
+ },
13
19
  },
14
20
  events: {
15
- squarePress: {
16
- label: 'Square press',
17
- eventType: 'chess.squarePress',
18
- payloadFields: [{ path: 'square', type: 'string', label: 'Square' }],
21
+ cellPress: {
22
+ label: 'Cell press',
23
+ eventType: 'chess.cellPress',
24
+ payloadFields: [
25
+ { path: 'column', type: 'number', label: 'Column' },
26
+ { path: 'row', type: 'number', label: 'Row' },
27
+ ],
19
28
  },
20
29
  moveAttempt: {
21
30
  label: 'Move attempt',
22
31
  eventType: 'chess.moveAttempt',
23
32
  payloadFields: [
24
- { path: 'from', type: 'string', label: 'From' },
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' },
33
+ { path: 'from.column', type: 'number', label: 'From column' },
34
+ { path: 'from.row', type: 'number', label: 'From row' },
35
+ { path: 'to.column', type: 'number', label: 'To column' },
36
+ { path: 'to.row', type: 'number', label: 'To row' },
48
37
  ],
49
38
  },
50
39
  },
@@ -56,19 +45,46 @@ export const chessBoardMeta = {
56
45
  acceptsFallback: true,
57
46
  acceptsTransforms: true,
58
47
  },
48
+ selectedCell: {
49
+ label: 'Selected cell',
50
+ value: {
51
+ type: 'object',
52
+ fields: [
53
+ { path: 'column', type: 'number', label: 'Column' },
54
+ { path: 'row', type: 'number', label: 'Row' },
55
+ ],
56
+ },
57
+ acceptsFallback: true,
58
+ acceptsTransforms: true,
59
+ },
59
60
  legalTargets: {
60
61
  label: 'Legal targets',
61
- value: { type: 'array', itemType: 'string' },
62
+ value: {
63
+ type: 'array',
64
+ itemType: 'object',
65
+ fields: [
66
+ { path: '[].column', type: 'number', label: 'Column' },
67
+ { path: '[].row', type: 'number', label: 'Row' },
68
+ ],
69
+ },
62
70
  acceptsFallback: true,
63
71
  acceptsTransforms: true,
64
72
  },
65
73
  },
66
74
  },
67
75
  props: {
68
- fen: {
69
- type: 'string',
70
- category: 'Position',
71
- label: 'FEN',
76
+ columns: {
77
+ type: 'number',
78
+ category: 'Board',
79
+ label: 'Columns',
80
+ default: 8,
81
+ authoring: { authority: 'instance' },
82
+ },
83
+ rows: {
84
+ type: 'number',
85
+ category: 'Board',
86
+ label: 'Rows',
87
+ default: 8,
72
88
  authoring: { authority: 'instance' },
73
89
  },
74
90
  pieces: {
@@ -86,10 +102,14 @@ export const chessBoardMeta = {
86
102
  default: 'white',
87
103
  authoring: { authority: 'instance' },
88
104
  },
89
- selectedSquare: {
90
- type: 'string',
105
+ selectedCell: {
106
+ type: 'object',
91
107
  category: 'Board',
92
- label: 'Selected square',
108
+ label: 'Selected cell',
109
+ itemSchema: [
110
+ { key: 'column', schema: { type: 'number', category: 'Cell' } },
111
+ { key: 'row', schema: { type: 'number', category: 'Cell' } },
112
+ ],
93
113
  authoring: { authority: 'instance' },
94
114
  },
95
115
  legalTargets: {
@@ -97,6 +117,10 @@ export const chessBoardMeta = {
97
117
  category: 'Board',
98
118
  label: 'Legal targets',
99
119
  default: [],
120
+ itemSchema: [
121
+ { key: 'column', schema: { type: 'number', category: 'Cell' } },
122
+ { key: 'row', schema: { type: 'number', category: 'Cell' } },
123
+ ],
100
124
  authoring: { authority: 'instance' },
101
125
  },
102
126
  showCoordinates: {
@@ -106,11 +130,11 @@ export const chessBoardMeta = {
106
130
  default: false,
107
131
  authoring: { authority: 'instance' },
108
132
  },
109
- validateMoves: {
133
+ resizable: {
110
134
  type: 'boolean',
111
135
  category: 'Behavior',
112
- label: 'Validate moves',
113
- default: true,
136
+ label: 'Resizable',
137
+ default: false,
114
138
  authoring: { authority: 'instance' },
115
139
  },
116
140
  disabled: {
@@ -1,248 +1,97 @@
1
- import { join } from 'node:path';
2
- import { pathToFileURL } from 'node:url';
3
-
4
1
  import { describe, expect, test } from 'bun:test';
5
- import { Window } from 'happy-dom';
6
- import React, { act } from 'react';
7
- import { createRoot } from 'react-dom/client';
8
- import { renderToStaticMarkup } from 'react-dom/server';
9
2
 
10
3
  import { chessBoardMeta } from './meta';
11
- import type {
12
- ChessBoard as ChessBoardComponent,
13
- OpeningBook as OpeningBookComponent,
14
- } from './public';
15
- import { createBoardSquares } from './utils/createBoardSquares';
16
- import { createChessBoardColorScheme } from './utils/createChessBoardColorScheme';
17
- import { getSquareFile } from './utils/getSquareFile';
18
- import { getSquareRank } from './utils/getSquareRank';
19
- import { isLightSquare } from './utils/isLightSquare';
20
-
21
- const webDistRoot = join(import.meta.dir, '../../../web-dist');
22
- const load = (path: string) => import(pathToFileURL(join(webDistRoot, path)).href);
4
+ import { createChessBoardCellPressHandler } from './utils/createChessBoardCellPressHandler';
5
+ import { createChessBoardCells } from './utils/createChessBoardCells';
23
6
 
24
7
  describe('Chess presentation', () => {
25
- test('keeps square order and theme-derived presentation independent of chess rules', () => {
26
- expect(createBoardSquares('white').at(0)).toBe('a8');
27
- expect(createBoardSquares('black').at(0)).toBe('h1');
28
- expect(getSquareFile('e4')).toBe('e');
29
- expect(getSquareRank('e4')).toBe('4');
30
- expect(isLightSquare('a1')).toBe(false);
31
- expect(typeof createChessBoardColorScheme).toBe('function');
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',
8
+ test('declares caller-owned dimensions, cells, and move intents without rule-engine events', () => {
9
+ expect(chessBoardMeta.blueprint.defaultProps).toMatchObject({ columns: 8, rows: 8 });
10
+ expect(chessBoardMeta.props.columns.default).toBe(8);
11
+ expect(chessBoardMeta.props.rows.default).toBe(8);
12
+ expect(chessBoardMeta.events.cellPress.payloadFields).toEqual([
13
+ { path: 'column', type: 'number', label: 'Column' },
14
+ { path: 'row', type: 'number', label: 'Row' },
49
15
  ]);
16
+ expect(chessBoardMeta.events).not.toHaveProperty('legalMove');
17
+ expect(chessBoardMeta.events).not.toHaveProperty('invalidMove');
50
18
  });
51
19
 
52
- test('renders custom React nodes, square IDs, and opening metadata through independent artifacts', async () => {
53
- const { ChessBoard } = (await load('components/chess-board/index.js')) as {
54
- ChessBoard: typeof ChessBoardComponent;
55
- };
56
- const { OpeningBook } = (await load('components/opening-book/index.js')) as {
57
- OpeningBook: typeof OpeningBookComponent;
58
- };
59
- const markup = renderToStaticMarkup(
60
- <>
61
- <ChessBoard
62
- colorScheme={{ selectedSquare: '#123456', legalTarget: '#234567' }}
63
- legalTargets={['e4']}
64
- pieces={[{ color: 'white', piece: 'p', square: 'e2' }]}
65
- renderPiece={() => <div data-testid="custom-piece" />}
66
- selectedSquare="e2"
67
- showCoordinates
68
- testID="board"
69
- />
70
- <OpeningBook
71
- moves={[
72
- {
73
- san: 'Nf3',
74
- eco: 'A04',
75
- name: 'Reti Opening',
76
- games: 42,
77
- whiteWinRate: 0.52,
78
- drawRate: 0.2,
79
- blackWinRate: 0.28,
80
- },
81
- ]}
82
- testID="book"
83
- />
84
- </>,
85
- );
86
- const browser = new Window();
87
- browser.document.body.innerHTML = markup;
88
- expect(browser.document.querySelectorAll('[data-testid^="board-square-"]')).toHaveLength(64);
89
- expect(
90
- browser.document.querySelector('[data-testid="board-square-e2"]')?.textContent,
91
- ).toContain('e2');
92
- expect(
93
- browser.document.querySelector('[data-testid="custom-piece"]')?.parentElement?.className,
94
- ).toContain('css-view');
95
- expect(
96
- browser.document.querySelector('[data-testid="board-square-e2"]')?.getAttribute('style'),
97
- ).toContain('background-color:rgba(18,52,86,1.00)');
98
- expect(
99
- browser.document.querySelector('[data-testid="board-square-e4"]')?.getAttribute('style'),
100
- ).toContain('background-color:rgba(35,69,103,1.00)');
101
- expect(browser.document.querySelector('[data-testid="book-move-Nf3"]')?.textContent).toContain(
102
- 'A04 · Reti Opening · 42 games',
103
- );
104
- expect(browser.document.querySelector('[data-testid="book-move-Nf3"]')?.textContent).toContain(
105
- 'W 52% D 20% B 28%',
106
- );
107
- browser.close();
20
+ test('uses the shared GridView presentation boundary rather than a chess rules engine', async () => {
21
+ const source = await Bun.file('src/features/chess/adapters/inbound/ChessBoard.tsx').text();
22
+
23
+ expect(source).toContain("import { GridView } from '../../../grid-view/public';");
24
+ expect(source).not.toContain('chess.js');
25
+ expect(source).not.toContain('fen');
108
26
  });
109
27
 
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,
28
+ test('maps non-square boards into opposite GridView coordinates for white and black orientation', () => {
29
+ const white = createChessBoardCells(3, 2, 'white');
30
+ const black = createChessBoardCells(3, 2, 'black');
31
+
32
+ expect(white.find((cell) => cell.column === 1 && cell.row === 2)).toMatchObject({
33
+ x: 0,
34
+ y: 0,
35
+ });
36
+ expect(white.find((cell) => cell.column === 3 && cell.row === 1)).toMatchObject({
37
+ x: 2,
38
+ y: 1,
125
39
  });
126
- const host = document.createElement('div');
127
- document.body.appendChild(host);
128
- const legal: string[] = [];
129
- const invalid: string[] = [];
130
- const root = createRoot(host);
40
+ expect(black.find((cell) => cell.column === 3 && cell.row === 1)).toMatchObject({
41
+ x: 0,
42
+ y: 0,
43
+ });
44
+ expect(black.find((cell) => cell.column === 1 && cell.row === 2)).toMatchObject({
45
+ x: 2,
46
+ y: 1,
47
+ });
48
+ });
131
49
 
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
- );
50
+ test('accepts only the documented 2 through 24 presentation dimensions', () => {
51
+ expect(createChessBoardCells(2, 24, 'white')).toHaveLength(48);
52
+ expect(() => createChessBoardCells(1, 8, 'white')).toThrow(RangeError);
53
+ expect(() => createChessBoardCells(8, 25, 'black')).toThrow(RangeError);
54
+ expect(() => createChessBoardCells(2.5, 8, 'white')).toThrow(RangeError);
55
+ });
145
56
 
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');
57
+ test('emits caller-owned press and move intents without acting in passive mode', () => {
58
+ const presses: unknown[] = [];
59
+ const attempts: unknown[] = [];
60
+ const active = createChessBoardCellPressHandler({
61
+ onCellPress: (cell) => presses.push(cell),
62
+ onMoveAttempt: (attempt) => attempts.push(attempt),
63
+ selectedCell: { column: 2, row: 2 },
64
+ });
150
65
 
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']);
66
+ active({ column: 3, row: 4 });
67
+ createChessBoardCellPressHandler({
68
+ interactionPolicy: 'passive',
69
+ onCellPress: (cell) => presses.push(cell),
70
+ onMoveAttempt: (attempt) => attempts.push(attempt),
71
+ selectedCell: { column: 2, row: 2 },
72
+ })({ column: 4, row: 4 });
157
73
 
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
- }
74
+ expect(presses).toEqual([{ column: 3, row: 4 }]);
75
+ expect(attempts).toEqual([{ from: { column: 2, row: 2 }, to: { column: 3, row: 4 } }]);
172
76
  });
173
77
 
174
- test('emits only caller-owned intents in active mode and suppresses them in passive mode', async () => {
175
- const { ChessBoard } = (await load('components/chess-board/index.js')) as {
176
- ChessBoard: typeof ChessBoardComponent;
177
- };
178
- const { OpeningBook } = (await load('components/opening-book/index.js')) as {
179
- OpeningBook: typeof OpeningBookComponent;
180
- };
181
- const browser = new Window();
182
- const keys = ['window', 'document', 'Node', 'navigator', 'IS_REACT_ACT_ENVIRONMENT'] as const;
183
- const previous = keys.map(
184
- (key) => [key, Object.getOwnPropertyDescriptor(globalThis, key)] as const,
185
- );
186
- Object.assign(globalThis, {
187
- window: browser,
188
- document: browser.document,
189
- Node: browser.Node,
190
- navigator: browser.navigator,
191
- IS_REACT_ACT_ENVIRONMENT: true,
78
+ test('describes selected cells and legal targets as cell objects in manifest metadata', () => {
79
+ expect(chessBoardMeta.props.selectedCell).toMatchObject({
80
+ type: 'object',
81
+ itemSchema: [{ key: 'column' }, { key: 'row' }],
82
+ });
83
+ expect(chessBoardMeta.props.legalTargets).toMatchObject({
84
+ type: 'array',
85
+ itemSchema: [{ key: 'column' }, { key: 'row' }],
86
+ });
87
+ expect(chessBoardMeta.bindings.props.selectedCell.value).toMatchObject({
88
+ type: 'object',
89
+ fields: [{ path: 'column' }, { path: 'row' }],
90
+ });
91
+ expect(chessBoardMeta.bindings.props.legalTargets.value).toMatchObject({
92
+ type: 'array',
93
+ itemType: 'object',
94
+ fields: [{ path: '[].column' }, { path: '[].row' }],
192
95
  });
193
- const host = document.createElement('div');
194
- document.body.appendChild(host);
195
- const squares: string[] = [];
196
- const moves: string[] = [];
197
- const book: string[] = [];
198
- const root = createRoot(host);
199
- const render = (interactionPolicy: 'active' | 'passive') => (
200
- <>
201
- <ChessBoard
202
- interactionPolicy={interactionPolicy}
203
- selectedSquare="e2"
204
- testID="board"
205
- onSquarePress={(square) => squares.push(square)}
206
- onMoveAttempt={(move) => moves.push(`${move.from}-${move.to}`)}
207
- />
208
- <OpeningBook
209
- interactionPolicy={interactionPolicy}
210
- moves={[{ san: 'e4' }]}
211
- onMovePress={(move) => book.push(move.san)}
212
- testID="book"
213
- />
214
- </>
215
- );
216
- try {
217
- act(() => root.render(render('passive')));
218
- act(() => {
219
- host
220
- .querySelector('[data-testid="board-square-e4"]')
221
- ?.dispatchEvent(new browser.MouseEvent('click', { bubbles: true }));
222
- host
223
- .querySelector('[data-testid="book-move-e4"]')
224
- ?.dispatchEvent(new browser.MouseEvent('click', { bubbles: true }));
225
- });
226
- expect([squares, moves, book]).toEqual([[], [], []]);
227
- act(() => root.render(render('active')));
228
- act(() => {
229
- host
230
- .querySelector('[data-testid="board-square-e4"]')
231
- ?.dispatchEvent(new browser.MouseEvent('click', { bubbles: true }));
232
- host
233
- .querySelector('[data-testid="book-move-e4"]')
234
- ?.dispatchEvent(new browser.MouseEvent('click', { bubbles: true }));
235
- });
236
- expect(squares).toEqual(['e4']);
237
- expect(moves).toEqual(['e2-e4']);
238
- expect(book).toEqual(['e4']);
239
- } finally {
240
- act(() => root.unmount());
241
- browser.close();
242
- for (const [key, descriptor] of previous) {
243
- if (descriptor) Object.defineProperty(globalThis, key, descriptor);
244
- else Reflect.deleteProperty(globalThis, key);
245
- }
246
- }
247
96
  });
248
97
  });
@@ -1,16 +1,15 @@
1
1
  export type {
2
+ ChessBoardCell,
2
3
  ChessBoardColorOverrides,
3
4
  ChessBoardColorScheme,
5
+ ChessBoardDimensions,
4
6
  ChessBoardOrientation,
5
7
  ChessBoardProps,
6
8
  ChessMoveAttempt,
7
- ChessMoveResult,
8
9
  ChessPieceCode,
9
10
  ChessPieceRenderContext,
10
11
  ChessPieceRenderer,
11
12
  ChessPieceState,
12
- ChessPromotionPiece,
13
- ChessSquareId,
14
13
  OpeningBookColorOverrides,
15
14
  OpeningBookColorScheme,
16
15
  OpeningBookMove,
@@ -19,12 +18,6 @@ export type {
19
18
  export { ChessBoard } from './adapters/inbound/ChessBoard';
20
19
  export { OpeningBook } from './adapters/inbound/OpeningBook';
21
20
  export { chessBoardMeta, openingBookMeta } from './meta';
22
- export { getLegalTargets, readChessPieces, tryMove } from './utils/chessEngine';
23
- export { chessSquares } from './utils/chessSquares';
24
- export { createBoardSquares } from './utils/createBoardSquares';
25
21
  export type { ChessColorThemeShape } from './utils/createChessBoardColorScheme';
26
22
  export { createChessBoardColorScheme } from './utils/createChessBoardColorScheme';
27
23
  export { createOpeningBookColorScheme } from './utils/createOpeningBookColorScheme';
28
- export { getSquareFile } from './utils/getSquareFile';
29
- export { getSquareRank } from './utils/getSquareRank';
30
- export { isLightSquare } from './utils/isLightSquare';
@@ -0,0 +1,30 @@
1
+ import type { ChessBoardCell, ChessBoardProps } from '../../../types/chess';
2
+
3
+ type ChessBoardCellPressHandlerOptions = Pick<
4
+ ChessBoardProps,
5
+ 'disabled' | 'interactionPolicy' | 'onCellPress' | 'onMoveAttempt' | 'selectedCell'
6
+ >;
7
+
8
+ /*** Creates the caller-owned cell and move intent boundary for an interactive chess presentation. */
9
+ export function createChessBoardCellPressHandler({
10
+ disabled,
11
+ interactionPolicy,
12
+ onCellPress,
13
+ onMoveAttempt,
14
+ selectedCell,
15
+ }: ChessBoardCellPressHandlerOptions): (cell: ChessBoardCell) => void {
16
+ return (cell) => {
17
+ if (disabled || interactionPolicy === 'passive') return;
18
+
19
+ onCellPress?.(cell);
20
+ if (selectedCell === null || selectedCell === undefined || cellsEqual(selectedCell, cell)) {
21
+ return;
22
+ }
23
+ onMoveAttempt?.({ from: selectedCell, to: cell });
24
+ };
25
+ }
26
+
27
+ /*** Compares caller-owned coordinates without adding chess-rule semantics. */
28
+ function cellsEqual(left: ChessBoardCell, right: ChessBoardCell): boolean {
29
+ return left.column === right.column && left.row === right.row;
30
+ }
@@ -0,0 +1,54 @@
1
+ import type { ChessBoardCell, ChessBoardOrientation } from '../../../types/chess';
2
+
3
+ const minimumDimension = 2;
4
+ const maximumDimension = 24;
5
+
6
+ type ChessBoardGridCell = ChessBoardCell & {
7
+ readonly height: 1;
8
+ readonly id: string;
9
+ readonly width: 1;
10
+ readonly x: number;
11
+ readonly y: number;
12
+ };
13
+
14
+ /*** Creates bounded GridView cells whose coordinates represent the requested chess orientation. */
15
+ export function createChessBoardCells(
16
+ columns: number,
17
+ rows: number,
18
+ orientation: ChessBoardOrientation,
19
+ ): readonly ChessBoardGridCell[] {
20
+ validateChessBoardDimensions(columns, rows);
21
+
22
+ return Array.from({ length: rows }, (_, rowIndex) =>
23
+ Array.from({ length: columns }, (_, columnIndex) => {
24
+ const column = columnIndex + 1;
25
+ const row = rowIndex + 1;
26
+
27
+ return {
28
+ column,
29
+ height: 1 as const,
30
+ id: `${column}:${row}`,
31
+ row,
32
+ width: 1 as const,
33
+ x: orientation === 'white' ? columnIndex : columns - column,
34
+ y: orientation === 'white' ? rows - row : rowIndex,
35
+ };
36
+ }),
37
+ ).flat();
38
+ }
39
+
40
+ /*** Rejects dimensions outside the presentation contract before GridView receives them. */
41
+ function validateChessBoardDimensions(columns: number, rows: number): void {
42
+ if (
43
+ ![columns, rows].every(
44
+ (dimension) =>
45
+ Number.isInteger(dimension) &&
46
+ dimension >= minimumDimension &&
47
+ dimension <= maximumDimension,
48
+ )
49
+ ) {
50
+ throw new RangeError(
51
+ `ChessBoard dimensions must be whole numbers from ${minimumDimension} through ${maximumDimension}.`,
52
+ );
53
+ }
54
+ }
@@ -32,17 +32,13 @@ test('Chess and Tabletop implementation modules own one matching runtime export
32
32
  );
33
33
  return [];
34
34
  });
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());
35
+ expect(runtimeExports.sort(), join(directory, name)).toEqual([parse(name).name]);
40
36
  }
41
37
  }
42
38
  }
43
39
  });
44
40
 
45
- test('Chess and Tabletop source graphs depend only on their intentional runtime owners', () => {
41
+ test('Chess and Tabletop source graphs depend only on their intentional presentation owners', () => {
46
42
  const sourcePaths = featureRoots.flatMap((featureRoot) => {
47
43
  const visit = (directory: string): string[] =>
48
44
  readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
@@ -67,7 +63,7 @@ test('Chess and Tabletop source graphs depend only on their intentional runtime
67
63
  for (const specifier of imports) {
68
64
  if (specifier.startsWith('.')) continue;
69
65
  expect(
70
- ['react', 'react-native', 'chess.js', '@ankhorage/color-theory'],
66
+ ['react', 'react-native', '@ankhorage/color-theory'],
71
67
  `${path}: ${specifier}`,
72
68
  ).toContain(specifier);
73
69
  }
@@ -243,7 +243,6 @@ test('measures requested rendered nodes without exposing Cytoscape', () => {
243
243
  try {
244
244
  const measurement = controller.measureNodes?.(['a', 'missing']);
245
245
  const node = cy.getElementById('a');
246
- const bounds = node.renderedBoundingBox();
247
246
 
248
247
  expect(measurement).toEqual({
249
248
  viewport: { width: 800, height: 600 },
@@ -251,7 +250,7 @@ test('measures requested rendered nodes without exposing Cytoscape', () => {
251
250
  {
252
251
  id: 'a',
253
252
  position: node.renderedPosition(),
254
- size: { width: bounds.w, height: bounds.h },
253
+ size: { width: node.renderedOuterWidth(), height: node.renderedOuterHeight() },
255
254
  },
256
255
  ],
257
256
  });