@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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/zora",
3
3
  "type": "module",
4
- "version": "25.11.1",
4
+ "version": "26.0.1",
5
5
  "description": "Opinionated React Native and React Native Web UI kit built on @ankhorage/surface.",
6
6
  "homepage": "https://github.com/ankhorage/zora#readme",
7
7
  "bugs": {
@@ -51,7 +51,6 @@
51
51
  "@ankhorage/runtime": "^5.2.4",
52
52
  "@ankhorage/surface": "^9.4.15",
53
53
  "@ankhorage/utility": "^1.10.0",
54
- "chess.js": "^1.4.0",
55
54
  "expo-document-picker": "~57.0.3",
56
55
  "expo-image-picker": "~57.0.20"
57
56
  },
@@ -155,6 +154,7 @@
155
154
  "format:check": "ankhorage-prettier --check .",
156
155
  "prepack": "bun run build",
157
156
  "test": "bun run build && bun test src examples/grid-workspaces/time examples/grid-workspaces/plugins --isolate",
157
+ "test:graph-view:chromium": "bun scripts/test-graph-view-chromium.ts",
158
158
  "typecheck": "bun x tsc --noEmit -p tsconfig.json && bun x tsc --noEmit -p tsconfig.scripts.json && bun x tsc -p tsconfig.typecheck.json",
159
159
  "version-packages": "ankhorage-changeset version"
160
160
  },
@@ -171,8 +171,8 @@
171
171
  "@types/react": "^19.3.0",
172
172
  "@types/react-dom": "^19.3.0",
173
173
  "@types/react-native-web": "^0.19.3",
174
- "cytoscape": "^3.34.4",
175
- "cytoscape-elk": "^2.3.0",
174
+ "cytoscape": "^3.35.0",
175
+ "cytoscape-elk": "^2.4.0",
176
176
  "happy-dom": "^21.0.4",
177
177
  "react": "19.2.3",
178
178
  "react-dom": "19.2.3",
@@ -529,7 +529,7 @@
529
529
  }
530
530
  },
531
531
  {
532
- "id": "chess.invalidMove",
532
+ "id": "chess.cellPress",
533
533
  "owner": "@ankhorage/zora",
534
534
  "access": [
535
535
  "emit"
@@ -540,67 +540,18 @@
540
540
  "source"
541
541
  ]
542
542
  },
543
- "label": "Invalid move",
543
+ "label": "Cell press",
544
544
  "output": {
545
545
  "schema": {
546
546
  "type": "object",
547
547
  "properties": {
548
- "from": {
549
- "type": "string",
550
- "title": "From"
551
- },
552
- "to": {
553
- "type": "string",
554
- "title": "To"
555
- },
556
- "promotion": {
557
- "type": "string",
558
- "title": "Promotion"
559
- }
560
- }
561
- }
562
- }
563
- },
564
- {
565
- "id": "chess.legalMove",
566
- "owner": "@ankhorage/zora",
567
- "access": [
568
- "emit"
569
- ],
570
- "binding": {
571
- "kind": "event",
572
- "bindableAs": [
573
- "source"
574
- ]
575
- },
576
- "label": "Legal move",
577
- "output": {
578
- "schema": {
579
- "type": "object",
580
- "properties": {
581
- "from": {
582
- "type": "string",
583
- "title": "From"
584
- },
585
- "to": {
586
- "type": "string",
587
- "title": "To"
588
- },
589
- "fen": {
590
- "type": "string",
591
- "title": "Position"
592
- },
593
- "san": {
594
- "type": "string",
595
- "title": "SAN"
596
- },
597
- "lan": {
598
- "type": "string",
599
- "title": "LAN"
548
+ "column": {
549
+ "type": "number",
550
+ "title": "Column"
600
551
  },
601
- "promotion": {
602
- "type": "string",
603
- "title": "Promotion"
552
+ "row": {
553
+ "type": "number",
554
+ "title": "Row"
604
555
  }
605
556
  }
606
557
  }
@@ -624,16 +575,30 @@
624
575
  "type": "object",
625
576
  "properties": {
626
577
  "from": {
627
- "type": "string",
628
- "title": "From"
578
+ "type": "object",
579
+ "properties": {
580
+ "column": {
581
+ "type": "number",
582
+ "title": "From column"
583
+ },
584
+ "row": {
585
+ "type": "number",
586
+ "title": "From row"
587
+ }
588
+ }
629
589
  },
630
590
  "to": {
631
- "type": "string",
632
- "title": "To"
633
- },
634
- "promotion": {
635
- "type": "string",
636
- "title": "Promotion"
591
+ "type": "object",
592
+ "properties": {
593
+ "column": {
594
+ "type": "number",
595
+ "title": "To column"
596
+ },
597
+ "row": {
598
+ "type": "number",
599
+ "title": "To row"
600
+ }
601
+ }
637
602
  }
638
603
  }
639
604
  }
@@ -672,31 +637,6 @@
672
637
  }
673
638
  }
674
639
  },
675
- {
676
- "id": "chess.squarePress",
677
- "owner": "@ankhorage/zora",
678
- "access": [
679
- "emit"
680
- ],
681
- "binding": {
682
- "kind": "event",
683
- "bindableAs": [
684
- "source"
685
- ]
686
- },
687
- "label": "Square press",
688
- "output": {
689
- "schema": {
690
- "type": "object",
691
- "properties": {
692
- "square": {
693
- "type": "string",
694
- "title": "Square"
695
- }
696
- }
697
- }
698
- }
699
- },
700
640
  {
701
641
  "id": "chip.press",
702
642
  "owner": "@ankhorage/zora",
@@ -2,17 +2,18 @@ import React from 'react';
2
2
  import { Pressable, StyleSheet, Text, View } from 'react-native';
3
3
 
4
4
  import type {
5
+ ChessBoardCell,
6
+ ChessBoardDimensions,
5
7
  ChessBoardProps,
6
8
  ChessMoveAttempt,
7
9
  ChessPieceState,
8
- ChessSquareId,
9
10
  } from '../../../../types/chess';
11
+ import { GridView } from '../../../grid-view/public';
10
12
  import { withZoraThemeScope } from '../../../theme/adapters/inbound/withZoraThemeScope';
11
13
  import { useZoraTheme } from '../../../theme/composition/useZoraTheme';
12
- import { getLegalTargets, readChessPieces, tryMove } from '../../utils/chessEngine';
13
- import { createBoardSquares } from '../../utils/createBoardSquares';
14
+ import { createChessBoardCellPressHandler } from '../../utils/createChessBoardCellPressHandler';
15
+ import { createChessBoardCells } from '../../utils/createChessBoardCells';
14
16
  import { createChessBoardColorScheme } from '../../utils/createChessBoardColorScheme';
15
- import { isLightSquare } from '../../utils/isLightSquare';
16
17
 
17
18
  const symbols = new Map<string, string>([
18
19
  ['white:p', '♙'],
@@ -28,138 +29,293 @@ const symbols = new Map<string, string>([
28
29
  ['black:q', '♛'],
29
30
  ['black:k', '♚'],
30
31
  ]);
32
+ const minimumDimension = 2;
33
+ const maximumDimension = 24;
31
34
 
32
- /*** Renders caller-owned or FEN-backed chess state with optional legal-move validation. */
35
+ /*** Renders a configurable caller-owned chess presentation through the shared GridView surface. */
33
36
  export const ChessBoard = withZoraThemeScope(ChessBoardInner);
34
37
 
35
- /*** Renders the current presentation state and delegates FEN validation to the core chess engine. */
38
+ /*** Renders board state and emits caller-owned cell and move intents without applying chess rules. */
36
39
  function ChessBoardInner({
37
- fen,
40
+ columns = 8,
41
+ rows = 8,
38
42
  pieces,
39
43
  orientation = 'white',
40
- selectedSquare = null,
41
- legalTargets,
44
+ selectedCell = null,
45
+ legalTargets = [],
42
46
  lastMove = null,
43
47
  disabled = false,
44
48
  showCoordinates = false,
45
- validateMoves = true,
49
+ resizable = false,
46
50
  colorScheme,
47
- onSquarePress,
51
+ onCellPress,
48
52
  onMoveAttempt,
49
- onLegalMove,
50
- onInvalidMove,
53
+ onDimensionsChange,
51
54
  renderPiece,
52
55
  interactionPolicy,
53
56
  testID,
54
57
  }: ChessBoardProps) {
55
58
  const { theme } = useZoraTheme();
56
59
  const colors = createChessBoardColorScheme(theme, colorScheme);
57
- const squares = createBoardSquares(orientation);
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);
68
- const pressSquare = (square: ChessSquareId) => {
69
- if (disabled || interactionPolicy === 'passive') return;
70
- onSquarePress?.(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;
60
+ const dimensions = React.useMemo(() => ({ columns, rows }), [columns, rows]);
61
+ const [width, setWidth] = React.useState(0);
62
+ const cells = React.useMemo(
63
+ () => createChessBoardCells(columns, rows, orientation),
64
+ [columns, orientation, rows],
65
+ );
66
+ const piecesByCell = React.useMemo(
67
+ () => new Map((pieces ?? []).map((piece) => [getCellKey(piece.cell), piece])),
68
+ [pieces],
69
+ );
70
+ const cellById = React.useMemo(() => new Map(cells.map((cell) => [cell.id, cell])), [cells]);
71
+ const legalTargetSet = React.useMemo(() => new Set(legalTargets.map(getCellKey)), [legalTargets]);
72
+ const pressCell = createChessBoardCellPressHandler({
73
+ disabled,
74
+ interactionPolicy,
75
+ onCellPress,
76
+ onMoveAttempt,
77
+ selectedCell,
78
+ });
79
+ const canResize =
80
+ resizable && onDimensionsChange !== undefined && !disabled && interactionPolicy !== 'passive';
81
+ const height = width * (dimensions.rows / dimensions.columns);
82
+ const cellSize = width / dimensions.columns;
76
83
 
77
- const result = tryMove(fen, attempt);
78
- if (result) onLegalMove?.(result);
79
- else onInvalidMove?.(attempt);
80
- };
81
84
  return (
82
85
  <View
83
86
  accessibilityLabel="Chess board"
84
- style={[styles.board, { borderColor: colors.border, opacity: disabled ? 0.56 : 1 }]}
87
+ onLayout={(event) => setWidth(event.nativeEvent.layout.width)}
88
+ style={[
89
+ styles.board,
90
+ {
91
+ aspectRatio: dimensions.columns / dimensions.rows,
92
+ borderColor: colors.border,
93
+ opacity: disabled ? 0.56 : 1,
94
+ },
95
+ ]}
85
96
  testID={testID}
86
97
  >
87
- {squares.map((square) => {
88
- const piece = piecesBySquare.get(square);
89
- const light = isLightSquare(square);
90
- const isTarget = legalTargetSet.has(square);
91
- const backgroundColor =
92
- selectedSquare === square
93
- ? colors.selectedSquare
94
- : isTarget
95
- ? colors.legalTarget
96
- : lastMove?.from === square
97
- ? colors.lastMoveFrom
98
- : lastMove?.to === square
99
- ? colors.lastMoveTo
100
- : light
101
- ? colors.lightSquare
102
- : colors.darkSquare;
103
- const pieceContent =
104
- piece === undefined
105
- ? null
106
- : renderPiece
107
- ? renderPiece({
98
+ {width > 0 ? (
99
+ <GridView
100
+ contentHeight={dimensions.rows}
101
+ contentWidth={dimensions.columns}
102
+ defaultViewport={{ pixelsPerUnitX: cellSize, pixelsPerUnitY: cellSize }}
103
+ height={height}
104
+ interactionPolicy="passive"
105
+ items={cells}
106
+ overscanPixels={0}
107
+ renderItem={(item) => {
108
+ const cell = cellById.get(item.id);
109
+ return cell === undefined
110
+ ? null
111
+ : renderChessBoardCell({
112
+ cell,
113
+ cellSize,
114
+ colors,
115
+ disabled: disabled || interactionPolicy === 'passive',
116
+ lastMove,
117
+ legalTargetSet,
118
+ onPress: pressCell,
119
+ piece: piecesByCell.get(item.id),
120
+ renderPiece,
121
+ selectedCell,
122
+ showCoordinates,
123
+ testID,
124
+ });
125
+ }}
126
+ width={width}
127
+ />
128
+ ) : null}
129
+ {canResize ? renderChessBoardResizeActions(dimensions, onDimensionsChange, testID) : null}
130
+ </View>
131
+ );
132
+ }
133
+
134
+ /*** Render one accessible, caller-controlled rectangular board cell. */
135
+ function renderChessBoardCell({
136
+ cell,
137
+ cellSize,
138
+ colors,
139
+ disabled,
140
+ lastMove,
141
+ legalTargetSet,
142
+ onPress,
143
+ piece,
144
+ renderPiece,
145
+ selectedCell,
146
+ showCoordinates,
147
+ testID,
148
+ }: {
149
+ readonly cell: ChessBoardCell & { readonly id: string };
150
+ readonly cellSize: number;
151
+ readonly colors: ReturnType<typeof createChessBoardColorScheme>;
152
+ readonly disabled: boolean;
153
+ readonly lastMove: ChessMoveAttempt | null;
154
+ readonly legalTargetSet: ReadonlySet<string>;
155
+ readonly onPress: (cell: ChessBoardCell) => void;
156
+ readonly piece: ChessPieceState | undefined;
157
+ readonly renderPiece: ChessBoardProps['renderPiece'];
158
+ readonly selectedCell: ChessBoardCell | null;
159
+ readonly showCoordinates: boolean;
160
+ readonly testID: string | undefined;
161
+ }) {
162
+ const light = (cell.column + cell.row) % 2 === 1;
163
+ const key = getCellKey(cell);
164
+ const backgroundColor = cellsEqual(selectedCell, cell)
165
+ ? colors.selectedSquare
166
+ : legalTargetSet.has(key)
167
+ ? colors.legalTarget
168
+ : cellsEqual(lastMove?.from, cell)
169
+ ? colors.lastMoveFrom
170
+ : cellsEqual(lastMove?.to, cell)
171
+ ? colors.lastMoveTo
172
+ : light
173
+ ? colors.lightSquare
174
+ : colors.darkSquare;
175
+ const pieceContent =
176
+ piece === undefined
177
+ ? null
178
+ : renderPiece
179
+ ? renderPiece({
180
+ cell,
181
+ color: piece.color === 'white' ? colors.lightPiece : colors.darkPiece,
182
+ piece: piece.piece,
183
+ size: cellSize,
184
+ })
185
+ : (symbols.get(`${piece.color}:${piece.piece.toLowerCase()}`) ?? piece.piece);
186
+ const coordinate = formatCellCoordinate(cell);
187
+
188
+ return (
189
+ <Pressable
190
+ accessibilityLabel={coordinate}
191
+ accessibilityRole="button"
192
+ accessibilityState={{ disabled }}
193
+ disabled={disabled}
194
+ key={key}
195
+ onPress={() => onPress(cell)}
196
+ style={[styles.square, { backgroundColor }]}
197
+ testID={testID === undefined ? undefined : `${testID}-cell-${coordinate}`}
198
+ >
199
+ {showCoordinates ? (
200
+ <Text
201
+ style={[
202
+ styles.coordinate,
203
+ { color: light ? colors.lightSquareText : colors.darkSquareText },
204
+ ]}
205
+ >
206
+ {coordinate}
207
+ </Text>
208
+ ) : null}
209
+ {piece === undefined ? null : (
210
+ <View pointerEvents="none" style={styles.pieceContainer}>
211
+ {renderPiece ? (
212
+ pieceContent
213
+ ) : (
214
+ <Text
215
+ style={[
216
+ styles.piece,
217
+ {
108
218
  color: piece.color === 'white' ? colors.lightPiece : colors.darkPiece,
109
- piece: piece.piece,
110
- square,
111
- })
112
- : (symbols.get(`${piece.color}:${piece.piece.toLowerCase()}`) ?? piece.piece);
113
- return (
114
- <Pressable
115
- accessibilityLabel={square}
116
- accessibilityRole="button"
117
- disabled={disabled || interactionPolicy === 'passive'}
118
- key={square}
119
- onPress={() => pressSquare(square)}
120
- style={[styles.square, { backgroundColor }]}
121
- testID={testID ? `${testID}-square-${square}` : undefined}
122
- >
123
- {showCoordinates ? (
124
- <Text
125
- style={[
126
- styles.coordinate,
127
- { color: light ? colors.lightSquareText : colors.darkSquareText },
128
- ]}
129
- >
130
- {square}
131
- </Text>
132
- ) : null}
133
- {piece === undefined ? null : (
134
- <View pointerEvents="none" style={styles.pieceContainer}>
135
- {renderPiece ? (
136
- pieceContent
137
- ) : (
138
- <Text
139
- style={[
140
- styles.piece,
141
- { color: piece.color === 'white' ? colors.lightPiece : colors.darkPiece },
142
- ]}
143
- >
144
- {pieceContent}
145
- </Text>
146
- )}
147
- </View>
148
- )}
149
- </Pressable>
150
- );
151
- })}
219
+ fontSize: cellSize * 0.58,
220
+ lineHeight: cellSize * 0.7,
221
+ },
222
+ ]}
223
+ >
224
+ {pieceContent}
225
+ </Text>
226
+ )}
227
+ </View>
228
+ )}
229
+ </Pressable>
230
+ );
231
+ }
232
+
233
+ /*** Render bounded editor actions only when the caller explicitly enables dimension editing. */
234
+ function renderChessBoardResizeActions(
235
+ dimensions: ChessBoardDimensions,
236
+ onDimensionsChange: (dimensions: ChessBoardDimensions) => void,
237
+ testID: string | undefined,
238
+ ) {
239
+ return (
240
+ <View accessibilityLabel="Chess board dimensions" style={styles.resizeActions}>
241
+ {renderResizeAction(
242
+ 'Add row',
243
+ { ...dimensions, rows: dimensions.rows + 1 },
244
+ onDimensionsChange,
245
+ dimensions.rows < maximumDimension,
246
+ testID,
247
+ )}
248
+ {renderResizeAction(
249
+ 'Remove row',
250
+ { ...dimensions, rows: dimensions.rows - 1 },
251
+ onDimensionsChange,
252
+ dimensions.rows > minimumDimension,
253
+ testID,
254
+ )}
255
+ {renderResizeAction(
256
+ 'Add column',
257
+ { ...dimensions, columns: dimensions.columns + 1 },
258
+ onDimensionsChange,
259
+ dimensions.columns < maximumDimension,
260
+ testID,
261
+ )}
262
+ {renderResizeAction(
263
+ 'Remove column',
264
+ { ...dimensions, columns: dimensions.columns - 1 },
265
+ onDimensionsChange,
266
+ dimensions.columns > minimumDimension,
267
+ testID,
268
+ )}
152
269
  </View>
153
270
  );
154
271
  }
155
272
 
273
+ /*** Render one caller-controlled dimension-change action. */
274
+ function renderResizeAction(
275
+ label: string,
276
+ dimensions: ChessBoardDimensions,
277
+ onDimensionsChange: (dimensions: ChessBoardDimensions) => void,
278
+ enabled: boolean,
279
+ testID: string | undefined,
280
+ ) {
281
+ return (
282
+ <Pressable
283
+ accessibilityLabel={label}
284
+ accessibilityRole="button"
285
+ disabled={!enabled}
286
+ onPress={() => onDimensionsChange(dimensions)}
287
+ testID={
288
+ testID === undefined ? undefined : `${testID}-${label.toLowerCase().replace(' ', '-')}`
289
+ }
290
+ >
291
+ <Text>{label}</Text>
292
+ </Pressable>
293
+ );
294
+ }
295
+
296
+ /*** Compares optional cell coordinates without attaching chess-rule meaning to them. */
297
+ function cellsEqual(
298
+ left: ChessBoardCell | undefined | null,
299
+ right: ChessBoardCell | undefined | null,
300
+ ): boolean {
301
+ if (!left || !right) return false;
302
+ return left.column === right.column && left.row === right.row;
303
+ }
304
+
305
+ /*** Format a stable display label for a bounded board cell. */
306
+ function formatCellCoordinate(cell: ChessBoardCell): string {
307
+ return `${String.fromCharCode(96 + cell.column)}${cell.row}`;
308
+ }
309
+
310
+ /*** Create a stable generic identity from caller-owned row and column values. */
311
+ function getCellKey(cell: ChessBoardCell): string {
312
+ return `${cell.column}:${cell.row}`;
313
+ }
314
+
156
315
  const styles = StyleSheet.create({
157
316
  board: {
158
- aspectRatio: 1,
159
317
  borderRadius: 12,
160
318
  borderWidth: StyleSheet.hairlineWidth,
161
- flexDirection: 'row',
162
- flexWrap: 'wrap',
163
319
  overflow: 'hidden',
164
320
  width: '100%',
165
321
  },
@@ -172,7 +328,7 @@ const styles = StyleSheet.create({
172
328
  top: 2,
173
329
  zIndex: 2,
174
330
  },
175
- piece: { fontSize: 32, fontWeight: '600', lineHeight: 38 },
331
+ piece: { fontWeight: '600' },
176
332
  pieceContainer: {
177
333
  alignItems: 'center',
178
334
  bottom: 0,
@@ -182,11 +338,6 @@ const styles = StyleSheet.create({
182
338
  right: 0,
183
339
  top: 0,
184
340
  },
185
- square: {
186
- alignItems: 'center',
187
- aspectRatio: 1,
188
- width: '12.5%',
189
- justifyContent: 'center',
190
- position: 'relative',
191
- },
341
+ resizeActions: { flexDirection: 'row', gap: 8, padding: 8 },
342
+ square: { alignItems: 'center', justifyContent: 'center', position: 'relative' },
192
343
  });