@dreamboard-games/sdk 0.2.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.
Files changed (369) hide show
  1. package/LICENSE.md +96 -0
  2. package/README.md +12 -0
  3. package/dist/HandView-ncJIVLhN.d.ts +193 -0
  4. package/dist/ResourceCounter-CTREyF73.d.ts +102 -0
  5. package/dist/ThemeProvider-fy0_QzgO.d.ts +99 -0
  6. package/dist/bundle-TIZcw8LB.d.ts +281 -0
  7. package/dist/cards-Sl3b40Mv.d.ts +13 -0
  8. package/dist/chunk-7YAHLYBR.js +481 -0
  9. package/dist/chunk-7YAHLYBR.js.map +1 -0
  10. package/dist/chunk-FDNZTDD6.js +8085 -0
  11. package/dist/chunk-FDNZTDD6.js.map +1 -0
  12. package/dist/chunk-GKKBPPSW.js +598 -0
  13. package/dist/chunk-GKKBPPSW.js.map +1 -0
  14. package/dist/chunk-I46YJSOD.js +1 -0
  15. package/dist/chunk-I46YJSOD.js.map +1 -0
  16. package/dist/chunk-KAELH4KC.js +104 -0
  17. package/dist/chunk-KAELH4KC.js.map +1 -0
  18. package/dist/chunk-PZ5AY32C.js +10 -0
  19. package/dist/chunk-PZ5AY32C.js.map +1 -0
  20. package/dist/chunk-T3ZKNUZ7.js +1 -0
  21. package/dist/chunk-T3ZKNUZ7.js.map +1 -0
  22. package/dist/chunk-T52J5RMF.js +1 -0
  23. package/dist/chunk-T52J5RMF.js.map +1 -0
  24. package/dist/chunk-TDSWKVZ4.js +5401 -0
  25. package/dist/chunk-TDSWKVZ4.js.map +1 -0
  26. package/dist/chunk-U5C6BONG.js +34 -0
  27. package/dist/chunk-U5C6BONG.js.map +1 -0
  28. package/dist/chunk-VDXOF4FW.js +69 -0
  29. package/dist/chunk-VDXOF4FW.js.map +1 -0
  30. package/dist/chunk-VFTAA4WO.js +115 -0
  31. package/dist/chunk-VFTAA4WO.js.map +1 -0
  32. package/dist/chunk-WN74KVNY.js +17 -0
  33. package/dist/chunk-WN74KVNY.js.map +1 -0
  34. package/dist/chunk-WYPQ3GG5.js +10990 -0
  35. package/dist/chunk-WYPQ3GG5.js.map +1 -0
  36. package/dist/components-D5ZRE2Hl.d.ts +1451 -0
  37. package/dist/generated/runtime/primitives.d.ts +12 -0
  38. package/dist/generated/runtime/primitives.js +180 -0
  39. package/dist/generated/runtime/primitives.js.map +1 -0
  40. package/dist/generated/runtime-api.d.ts +3 -0
  41. package/dist/generated/runtime-api.js +2 -0
  42. package/dist/generated/runtime-api.js.map +1 -0
  43. package/dist/generated/runtime.d.ts +14 -0
  44. package/dist/generated/runtime.js +18 -0
  45. package/dist/generated/runtime.js.map +1 -0
  46. package/dist/generated/workspace-contract.d.ts +14 -0
  47. package/dist/generated/workspace-contract.js +14 -0
  48. package/dist/generated/workspace-contract.js.map +1 -0
  49. package/dist/hex-board-view-D_07hO6O.d.ts +933 -0
  50. package/dist/hex-color-MhOyuY-o.d.ts +8 -0
  51. package/dist/index-BwqPQtBu.d.ts +1433 -0
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.js +12 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/infrastructure/reducer-bundle-abi.d.ts +1083 -0
  56. package/dist/infrastructure/reducer-bundle-abi.js +14 -0
  57. package/dist/infrastructure/reducer-bundle-abi.js.map +1 -0
  58. package/dist/infrastructure/workspace-codegen.d.ts +53 -0
  59. package/dist/infrastructure/workspace-codegen.js +44 -0
  60. package/dist/infrastructure/workspace-codegen.js.map +1 -0
  61. package/dist/manifest-contract-BNHVGFtU.d.ts +9 -0
  62. package/dist/package-set.d.ts +13 -0
  63. package/dist/package-set.js +12 -0
  64. package/dist/package-set.js.map +1 -0
  65. package/dist/primitive-props-DpKs-GCr.d.ts +11 -0
  66. package/dist/reducer.d.ts +3786 -0
  67. package/dist/reducer.js +8131 -0
  68. package/dist/reducer.js.map +1 -0
  69. package/dist/runtime/primitives.d.ts +226 -0
  70. package/dist/runtime/primitives.js +180 -0
  71. package/dist/runtime/primitives.js.map +1 -0
  72. package/dist/runtime/types/runtime-api.d.ts +1 -0
  73. package/dist/runtime/types/runtime-api.js +2 -0
  74. package/dist/runtime/types/runtime-api.js.map +1 -0
  75. package/dist/runtime/workspace-contract.d.ts +172 -0
  76. package/dist/runtime/workspace-contract.js +14 -0
  77. package/dist/runtime/workspace-contract.js.map +1 -0
  78. package/dist/runtime-api-3dshj6kK.d.ts +101 -0
  79. package/dist/runtime-api-DWxvTr-O.d.ts +379 -0
  80. package/dist/runtime.d.ts +58 -0
  81. package/dist/runtime.js +13 -0
  82. package/dist/runtime.js.map +1 -0
  83. package/dist/slots-1GPGihk8.d.ts +8 -0
  84. package/dist/testing.d.ts +149 -0
  85. package/dist/testing.js +513 -0
  86. package/dist/testing.js.map +1 -0
  87. package/dist/types.d.ts +496 -0
  88. package/dist/types.js +28 -0
  89. package/dist/types.js.map +1 -0
  90. package/dist/ui/components.d.ts +16 -0
  91. package/dist/ui/components.js +192 -0
  92. package/dist/ui/components.js.map +1 -0
  93. package/dist/ui/defaults.d.ts +19 -0
  94. package/dist/ui/defaults.js +104 -0
  95. package/dist/ui/defaults.js.map +1 -0
  96. package/dist/ui/plugin-styles.css +250 -0
  97. package/dist/ui/types/player-state.d.ts +365 -0
  98. package/dist/ui/types/player-state.js +1 -0
  99. package/dist/ui/types/player-state.js.map +1 -0
  100. package/dist/ui-contract-iQfTtUSL.d.ts +1161 -0
  101. package/dist/ui.d.ts +320 -0
  102. package/dist/ui.js +253 -0
  103. package/dist/ui.js.map +1 -0
  104. package/package.json +199 -0
  105. package/src/generated/reducer-contract/builders.ts +90 -0
  106. package/src/generated/reducer-contract/version.ts +9 -0
  107. package/src/generated/reducer-contract/wire.ts +100 -0
  108. package/src/generated/reducer-contract/zod.ts +101 -0
  109. package/src/generated/runtime/primitives.ts +2 -0
  110. package/src/generated/runtime-api.ts +5 -0
  111. package/src/generated/runtime.ts +35 -0
  112. package/src/generated/workspace-contract.ts +2 -0
  113. package/src/index.ts +7 -0
  114. package/src/infrastructure/reducer-bundle-abi.ts +8 -0
  115. package/src/infrastructure/reducer-contract/bundle.ts +37 -0
  116. package/src/infrastructure/workspace-codegen/hex-geometry.ts +69 -0
  117. package/src/infrastructure/workspace-codegen/index.ts +64 -0
  118. package/src/infrastructure/workspace-codegen/manifest-contract.ts +6632 -0
  119. package/src/infrastructure/workspace-codegen/manifest-validation.ts +795 -0
  120. package/src/infrastructure/workspace-codegen/ownership.ts +131 -0
  121. package/src/infrastructure/workspace-codegen/preset-card-sets.ts +169 -0
  122. package/src/infrastructure/workspace-codegen/seeds.ts +1705 -0
  123. package/src/infrastructure/workspace-codegen.ts +1 -0
  124. package/src/package-set.ts +19 -0
  125. package/src/reducer/authoring/contract.ts +157 -0
  126. package/src/reducer/authoring/effect.ts +224 -0
  127. package/src/reducer/authoring/game.ts +23 -0
  128. package/src/reducer/authoring/interaction.ts +98 -0
  129. package/src/reducer/authoring/phase.ts +300 -0
  130. package/src/reducer/authoring/types.ts +70 -0
  131. package/src/reducer/authoring/validation.ts +382 -0
  132. package/src/reducer/authoring/view-stage.ts +68 -0
  133. package/src/reducer/authoring.ts +29 -0
  134. package/src/reducer/bundle/ingress-bundle.ts +491 -0
  135. package/src/reducer/bundle/trusted/engine-instruction-resolver.ts +254 -0
  136. package/src/reducer/bundle/trusted/flow-instruction-resolver.ts +73 -0
  137. package/src/reducer/bundle/trusted/instruction-runner.ts +414 -0
  138. package/src/reducer/bundle/trusted/interaction-authorization.ts +137 -0
  139. package/src/reducer/bundle/trusted/interaction-collectors.ts +859 -0
  140. package/src/reducer/bundle/trusted/interaction-decision.ts +747 -0
  141. package/src/reducer/bundle/trusted/interaction-resolver.ts +95 -0
  142. package/src/reducer/bundle/trusted/interaction-types.ts +171 -0
  143. package/src/reducer/bundle/trusted/lifecycle-runner.ts +427 -0
  144. package/src/reducer/bundle/trusted/projection-builder.ts +356 -0
  145. package/src/reducer/bundle/trusted/projection-context.ts +39 -0
  146. package/src/reducer/bundle/trusted/rng-sampler.ts +150 -0
  147. package/src/reducer/bundle/trusted/runtime-registry.ts +120 -0
  148. package/src/reducer/bundle/trusted/runtime-scope.ts +336 -0
  149. package/src/reducer/bundle/trusted/simultaneous-player.ts +97 -0
  150. package/src/reducer/bundle/trusted/stage-resolver.ts +87 -0
  151. package/src/reducer/bundle/trusted/static-projection.ts +116 -0
  152. package/src/reducer/bundle/trusted/trusted-runtime-args.ts +97 -0
  153. package/src/reducer/bundle/trusted/trusted-runtime-result.ts +39 -0
  154. package/src/reducer/bundle/trusted/trusted-setup-profiles.ts +43 -0
  155. package/src/reducer/bundle/trusted/trusted-state-codec.ts +48 -0
  156. package/src/reducer/bundle/trusted-bundle.ts +97 -0
  157. package/src/reducer/bundle/types.ts +171 -0
  158. package/src/reducer/bundle.ts +2 -0
  159. package/src/reducer/client-param-schemas.ts +57 -0
  160. package/src/reducer/compose.ts +34 -0
  161. package/src/reducer/core/runtime-input.ts +30 -0
  162. package/src/reducer/core/runtime-instruction.ts +59 -0
  163. package/src/reducer/core/types.ts +62 -0
  164. package/src/reducer/definition-index.ts +277 -0
  165. package/src/reducer/derived.ts +106 -0
  166. package/src/reducer/effects.ts +92 -0
  167. package/src/reducer/engine/runtime-instruction-engine.ts +155 -0
  168. package/src/reducer/ingress/decode-runtime-input.ts +7 -0
  169. package/src/reducer/ingress/decode-session-state.ts +9 -0
  170. package/src/reducer/ingress/encode-session-state.ts +6 -0
  171. package/src/reducer/ingress/input-codec.ts +18 -0
  172. package/src/reducer/ingress/phase-schemas.ts +62 -0
  173. package/src/reducer/ingress/raw-types.ts +107 -0
  174. package/src/reducer/ingress/runtime-codec.ts +14 -0
  175. package/src/reducer/ingress/runtime-payload.ts +13 -0
  176. package/src/reducer/ingress/session-codec.ts +392 -0
  177. package/src/reducer/ingress/types.ts +6 -0
  178. package/src/reducer/inputs/boardInput.ts +217 -0
  179. package/src/reducer/inputs/boardTarget.ts +190 -0
  180. package/src/reducer/inputs/cardInput.ts +86 -0
  181. package/src/reducer/inputs/cardTarget.ts +101 -0
  182. package/src/reducer/inputs/choiceTarget.ts +104 -0
  183. package/src/reducer/inputs/defineInputs.ts +71 -0
  184. package/src/reducer/inputs/formInput.ts +809 -0
  185. package/src/reducer/inputs/many.ts +120 -0
  186. package/src/reducer/inputs/promptInput.ts +87 -0
  187. package/src/reducer/inputs/rngInput.ts +58 -0
  188. package/src/reducer/inputs/targetRule.ts +123 -0
  189. package/src/reducer/inputs.ts +41 -0
  190. package/src/reducer/model/definition.ts +1072 -0
  191. package/src/reducer/model/extract.ts +745 -0
  192. package/src/reducer/model/manifest.ts +570 -0
  193. package/src/reducer/model/queries.ts +641 -0
  194. package/src/reducer/model/runtime.ts +264 -0
  195. package/src/reducer/model/spec.ts +1386 -0
  196. package/src/reducer/model/table.ts +260 -0
  197. package/src/reducer/model.ts +7 -0
  198. package/src/reducer/ops.ts +1034 -0
  199. package/src/reducer/parse-utils.ts +28 -0
  200. package/src/reducer/per-player.ts +422 -0
  201. package/src/reducer/rng.ts +69 -0
  202. package/src/reducer/schema-helpers.ts +185 -0
  203. package/src/reducer/setup-bootstrap-helpers.ts +171 -0
  204. package/src/reducer/setup-bootstrap.ts +481 -0
  205. package/src/reducer/table-ops.ts +2671 -0
  206. package/src/reducer/table-queries.ts +372 -0
  207. package/src/reducer/transaction.ts +120 -0
  208. package/src/reducer.ts +314 -0
  209. package/src/runtime/primitives.ts +1 -0
  210. package/src/runtime/types/runtime-api.ts +1 -0
  211. package/src/runtime/workspace-contract.ts +32 -0
  212. package/src/runtime-internal/components/InteractionForm.tsx +1309 -0
  213. package/src/runtime-internal/components/PluginRuntime.tsx +103 -0
  214. package/src/runtime-internal/components/board/target-layer.ts +70 -0
  215. package/src/runtime-internal/context/ClientParamSchemaContext.tsx +44 -0
  216. package/src/runtime-internal/context/InteractionDraftContext.tsx +279 -0
  217. package/src/runtime-internal/context/PluginSessionContext.tsx +47 -0
  218. package/src/runtime-internal/context/PluginStateContext.tsx +262 -0
  219. package/src/runtime-internal/context/RuntimeContext.tsx +96 -0
  220. package/src/runtime-internal/defaults/components.tsx +409 -0
  221. package/src/runtime-internal/defaults/index.ts +11 -0
  222. package/src/runtime-internal/errors/ValidationError.ts +29 -0
  223. package/src/runtime-internal/hooks/useActivePlayers.ts +33 -0
  224. package/src/runtime-internal/hooks/useBoardInteractions.ts +665 -0
  225. package/src/runtime-internal/hooks/useGameSelector.ts +105 -0
  226. package/src/runtime-internal/hooks/useGameView.ts +9 -0
  227. package/src/runtime-internal/hooks/useInteractionByKey.ts +354 -0
  228. package/src/runtime-internal/hooks/useInteractionHandle.ts +438 -0
  229. package/src/runtime-internal/hooks/useIsMyTurn.ts +20 -0
  230. package/src/runtime-internal/hooks/useLobby.ts +76 -0
  231. package/src/runtime-internal/hooks/useMe.ts +48 -0
  232. package/src/runtime-internal/hooks/usePlayerInfo.ts +28 -0
  233. package/src/runtime-internal/hooks/usePlayerTurnOrder.ts +23 -0
  234. package/src/runtime-internal/hooks/usePluginRuntime.ts +147 -0
  235. package/src/runtime-internal/hooks/useSeatInbox.ts +61 -0
  236. package/src/runtime-internal/hooks/useSimultaneousPhase.ts +10 -0
  237. package/src/runtime-internal/index.ts +42 -0
  238. package/src/runtime-internal/internal.ts +43 -0
  239. package/src/runtime-internal/plugin-styles.css +250 -0
  240. package/src/runtime-internal/primitives/board.tsx +459 -0
  241. package/src/runtime-internal/primitives/dialog-lifecycle.ts +58 -0
  242. package/src/runtime-internal/primitives/dice.tsx +79 -0
  243. package/src/runtime-internal/primitives/game-ui-provider.tsx +35 -0
  244. package/src/runtime-internal/primitives/game.tsx +387 -0
  245. package/src/runtime-internal/primitives/hand-intent-adapter.ts +147 -0
  246. package/src/runtime-internal/primitives/hand-surface.tsx +594 -0
  247. package/src/runtime-internal/primitives/index.ts +196 -0
  248. package/src/runtime-internal/primitives/interaction-form-binding.tsx +56 -0
  249. package/src/runtime-internal/primitives/interaction-submit.ts +90 -0
  250. package/src/runtime-internal/primitives/interaction.tsx +987 -0
  251. package/src/runtime-internal/primitives/phase.tsx +43 -0
  252. package/src/runtime-internal/primitives/player-roster.tsx +302 -0
  253. package/src/runtime-internal/primitives/primitive-props.tsx +101 -0
  254. package/src/runtime-internal/primitives/prompt.tsx +255 -0
  255. package/src/runtime-internal/primitives/ui.tsx +60 -0
  256. package/src/runtime-internal/primitives/zone.tsx +791 -0
  257. package/src/runtime-internal/reducer.ts +30 -0
  258. package/src/runtime-internal/runtime/createPluginRuntimeAPI.ts +605 -0
  259. package/src/runtime-internal/types/plugin-state.ts +508 -0
  260. package/src/runtime-internal/types/reducer-state.ts +24 -0
  261. package/src/runtime-internal/types/runtime-api.ts +114 -0
  262. package/src/runtime-internal/ui-contract.ts +519 -0
  263. package/src/runtime-internal/utils/card-intent-adapter.ts +546 -0
  264. package/src/runtime-internal/utils/interaction-inputs.ts +492 -0
  265. package/src/runtime-internal/utils/interaction-labels.ts +23 -0
  266. package/src/runtime-internal/utils/interaction-router.ts +273 -0
  267. package/src/runtime-internal/utils/interaction-status.ts +74 -0
  268. package/src/runtime-internal/workspace-contract.ts +1170 -0
  269. package/src/runtime.ts +34 -0
  270. package/src/testing/create-expect-api.ts +352 -0
  271. package/src/testing/create-test-runtime.ts +381 -0
  272. package/src/testing/definitions.ts +127 -0
  273. package/src/testing/index.ts +3 -0
  274. package/src/testing.ts +1 -0
  275. package/src/type-stubs/manifest-contract.d.ts +42 -0
  276. package/src/type-stubs/manifest-contract.js +72 -0
  277. package/src/type-stubs/ui-contract.d.ts +5 -0
  278. package/src/type-stubs/ui-contract.js +1 -0
  279. package/src/types/authoring-card-properties.type-test.ts +266 -0
  280. package/src/types/authoring.ts +1282 -0
  281. package/src/types/cards.ts +19 -0
  282. package/src/types/contracts.ts +1550 -0
  283. package/src/types/generated-helpers.ts +35 -0
  284. package/src/types/index.ts +147 -0
  285. package/src/types/slots.ts +11 -0
  286. package/src/types.ts +1 -0
  287. package/src/ui/components/ActionButton.tsx +97 -0
  288. package/src/ui/components/ActionPanel.tsx +315 -0
  289. package/src/ui/components/Card.tsx +378 -0
  290. package/src/ui/components/CardDragSurface.tsx +1076 -0
  291. package/src/ui/components/ChromeSuppressionContext.tsx +70 -0
  292. package/src/ui/components/CostDisplay.tsx +145 -0
  293. package/src/ui/components/DiceRoller.tsx +581 -0
  294. package/src/ui/components/Drawer.tsx +180 -0
  295. package/src/ui/components/ErrorBoundary.tsx +275 -0
  296. package/src/ui/components/GameEndDisplay.tsx +398 -0
  297. package/src/ui/components/GameSkeleton.tsx +260 -0
  298. package/src/ui/components/Hand.tsx +468 -0
  299. package/src/ui/components/HandDock.tsx +299 -0
  300. package/src/ui/components/HandView.tsx +441 -0
  301. package/src/ui/components/MobileHandTray.tsx +381 -0
  302. package/src/ui/components/MoreActions.tsx +143 -0
  303. package/src/ui/components/PhaseIndicator.tsx +341 -0
  304. package/src/ui/components/PlayArea.tsx +146 -0
  305. package/src/ui/components/PrimaryActionButton.tsx +336 -0
  306. package/src/ui/components/PrimaryButton.tsx +45 -0
  307. package/src/ui/components/ResourceCounter.tsx +270 -0
  308. package/src/ui/components/StagingZone.tsx +134 -0
  309. package/src/ui/components/ThemedButton.tsx +113 -0
  310. package/src/ui/components/Toast.tsx +264 -0
  311. package/src/ui/components/board/HexGrid.tsx +1294 -0
  312. package/src/ui/components/board/NetworkGraph.tsx +476 -0
  313. package/src/ui/components/board/SlotSystem.tsx +388 -0
  314. package/src/ui/components/board/SquareGrid.tsx +1165 -0
  315. package/src/ui/components/board/TrackBoard.tsx +496 -0
  316. package/src/ui/components/board/ZoneMap.tsx +448 -0
  317. package/src/ui/components/board/hex-board-view.ts +123 -0
  318. package/src/ui/components/board/index.ts +142 -0
  319. package/src/ui/components/board/interaction-accessibility.ts +21 -0
  320. package/src/ui/components/board/target-layer.ts +66 -0
  321. package/src/ui/components/card-render-content.type-test.ts +27 -0
  322. package/src/ui/components/hand-layout-math.ts +163 -0
  323. package/src/ui/components/hand-pointer-engine.ts +413 -0
  324. package/src/ui/components/index.ts +245 -0
  325. package/src/ui/components.ts +1 -0
  326. package/src/ui/defaults/components.tsx +106 -0
  327. package/src/ui/defaults/index.ts +8 -0
  328. package/src/ui/defaults.ts +1 -0
  329. package/src/ui/errors/ValidationError.ts +29 -0
  330. package/src/ui/helpers/cards.ts +19 -0
  331. package/src/ui/helpers/track-board.ts +211 -0
  332. package/src/ui/hooks/useBoardTopology.ts +316 -0
  333. package/src/ui/hooks/useCards.ts +10 -0
  334. package/src/ui/hooks/useHandCardPointer.ts +381 -0
  335. package/src/ui/hooks/useHandLayout.ts +378 -0
  336. package/src/ui/hooks/useHandPresentation.ts +121 -0
  337. package/src/ui/hooks/useHexBoard.ts +74 -0
  338. package/src/ui/hooks/useHexGrid.ts +185 -0
  339. package/src/ui/hooks/useIsMobile.ts +35 -0
  340. package/src/ui/hooks/usePanZoom.ts +278 -0
  341. package/src/ui/hooks/useSquareBoard.ts +124 -0
  342. package/src/ui/hooks/useSquareGrid.ts +328 -0
  343. package/src/ui/index.ts +98 -0
  344. package/src/ui/internal/ui/alert.tsx +51 -0
  345. package/src/ui/internal/ui/button.tsx +58 -0
  346. package/src/ui/internal/ui/dialog.tsx +134 -0
  347. package/src/ui/internal/ui/input.tsx +21 -0
  348. package/src/ui/internal/ui/label.tsx +21 -0
  349. package/src/ui/internal/ui/select.tsx +129 -0
  350. package/src/ui/internal/ui/tooltip.tsx +54 -0
  351. package/src/ui/internal/ui/utils.ts +5 -0
  352. package/src/ui/plugin-styles.css +250 -0
  353. package/src/ui/primitives/dialog-lifecycle.ts +58 -0
  354. package/src/ui/primitives/dice.tsx +79 -0
  355. package/src/ui/primitives/primitive-props.tsx +101 -0
  356. package/src/ui/theme/ThemeProvider.tsx +252 -0
  357. package/src/ui/theme/board.ts +61 -0
  358. package/src/ui/theme/css-vars.ts +105 -0
  359. package/src/ui/theme/derive.ts +240 -0
  360. package/src/ui/theme/index.ts +61 -0
  361. package/src/ui/theme/presets/arcade.ts +261 -0
  362. package/src/ui/theme/presets/studio.ts +261 -0
  363. package/src/ui/theme/presets/tabletop.ts +266 -0
  364. package/src/ui/theme/tokens.ts +392 -0
  365. package/src/ui/types/hex-color.ts +20 -0
  366. package/src/ui/types/player-state.ts +463 -0
  367. package/src/ui/types/tiled-board.ts +785 -0
  368. package/src/ui/types/visual-state.ts +137 -0
  369. package/src/ui.ts +1 -0
@@ -0,0 +1,1165 @@
1
+ /**
2
+ * SVG-based square grid for grid-based games (Chess, Checkers, Go, Scrabble, Battleship).
3
+ * All rendering controlled by parent via required render functions.
4
+ */
5
+
6
+ import { useMemo, useState, type ReactNode } from "react";
7
+ import { clsx } from "clsx";
8
+ import { usePanZoom } from "../../hooks/usePanZoom.js";
9
+ import { handleKeyboardActivation } from "./interaction-accessibility.js";
10
+ import {
11
+ interactiveTargetRenderState,
12
+ isInteractiveTargetSelectable,
13
+ type InteractiveTargetLayer,
14
+ type InteractiveTargetRenderState,
15
+ } from "./target-layer.js";
16
+ import type { SquarePieceState } from "../../types/player-state.js";
17
+ import {
18
+ type AuthoredSquareBoardInput,
19
+ type AnySquareBoardInput,
20
+ type GeneratedSquareBoardInput,
21
+ type NormalizedSquareBoard,
22
+ type NormalizedSquareCellOf,
23
+ type NormalizedSquareEdgeOf,
24
+ type NormalizedSquarePieceOf,
25
+ type NormalizedSquareVertexOf,
26
+ normalizeSquareBoardInput,
27
+ } from "../../types/tiled-board.js";
28
+
29
+ export type {
30
+ InteractiveTargetLayer,
31
+ InteractiveTargetRenderState,
32
+ } from "./target-layer.js";
33
+
34
+ // ============================================================================
35
+ // Types
36
+ // ============================================================================
37
+
38
+ interface SquareCellWithId {
39
+ id: string;
40
+ row: number;
41
+ col: number;
42
+ }
43
+
44
+ export interface SquareEdgePosition {
45
+ x1: number;
46
+ y1: number;
47
+ x2: number;
48
+ y2: number;
49
+ midX: number;
50
+ midY: number;
51
+ angle: number;
52
+ }
53
+
54
+ export interface SquareVertexPosition {
55
+ x: number;
56
+ y: number;
57
+ }
58
+
59
+ export interface InteractiveSquareEdge<
60
+ TBoard extends AnySquareBoardInput = AnySquareBoardInput,
61
+ > extends NormalizedSquareEdgeOf<TBoard> {
62
+ position: SquareEdgePosition;
63
+ }
64
+
65
+ export interface InteractiveSquareVertex<
66
+ TBoard extends AnySquareBoardInput = AnySquareBoardInput,
67
+ > extends NormalizedSquareVertexOf<TBoard> {
68
+ position: SquareVertexPosition;
69
+ }
70
+
71
+ export type InteractiveSquareSpace<
72
+ TBoard extends AnySquareBoardInput = AnySquareBoardInput,
73
+ > = NormalizedSquareCellOf<TBoard>;
74
+
75
+ interface SquareGeneratedGridInputProps {
76
+ id?: string;
77
+ layout?: "square";
78
+ spaces: Extract<AnySquareBoardInput, { spaces: unknown }>["spaces"];
79
+ pieces?: AnySquareBoardInput["pieces"];
80
+ edges?: AnySquareBoardInput["edges"];
81
+ vertices?: AnySquareBoardInput["vertices"];
82
+ }
83
+
84
+ interface SquareAuthoredGridInputProps {
85
+ id?: string;
86
+ layout?: "square";
87
+ rows?: number;
88
+ cols?: number;
89
+ cells: Extract<AnySquareBoardInput, { cells: unknown }>["cells"];
90
+ pieces?: readonly SquarePieceState[];
91
+ edges?: AnySquareBoardInput["edges"];
92
+ vertices?: AnySquareBoardInput["vertices"];
93
+ }
94
+
95
+ type SquareGridInputProps =
96
+ | SquareGeneratedGridInputProps
97
+ | SquareAuthoredGridInputProps;
98
+
99
+ type ResolvedSquareArrayProp<Value> =
100
+ Exclude<Value, undefined> extends readonly unknown[]
101
+ ? Exclude<Value, undefined>
102
+ : readonly [];
103
+
104
+ type SquareBoardLikeOfProps<TProps extends SquareGridInputProps> =
105
+ TProps extends {
106
+ id?: infer Id;
107
+ layout?: infer Layout;
108
+ spaces: infer Spaces;
109
+ pieces?: infer Pieces;
110
+ edges?: infer Edges;
111
+ vertices?: infer Vertices;
112
+ }
113
+ ? {
114
+ id: Extract<Id, string> extends never ? string : Extract<Id, string>;
115
+ layout?: Extract<Layout, "square">;
116
+ spaces: Spaces;
117
+ pieces: ResolvedSquareArrayProp<Pieces>;
118
+ edges: ResolvedSquareArrayProp<Edges>;
119
+ vertices: ResolvedSquareArrayProp<Vertices>;
120
+ } & GeneratedSquareBoardInput
121
+ : TProps extends {
122
+ id?: infer Id;
123
+ layout?: infer Layout;
124
+ rows?: infer Rows;
125
+ cols?: infer Cols;
126
+ cells: infer Cells;
127
+ pieces?: infer Pieces;
128
+ edges?: infer Edges;
129
+ vertices?: infer Vertices;
130
+ }
131
+ ? {
132
+ id: Extract<Id, string> extends never ? string : Extract<Id, string>;
133
+ layout?: Extract<Layout, "square">;
134
+ rows: Extract<Rows, number> extends never
135
+ ? number
136
+ : Extract<Rows, number>;
137
+ cols: Extract<Cols, number> extends never
138
+ ? number
139
+ : Extract<Cols, number>;
140
+ cells: Cells;
141
+ pieces: ResolvedSquareArrayProp<Pieces>;
142
+ edges: ResolvedSquareArrayProp<Edges>;
143
+ vertices: ResolvedSquareArrayProp<Vertices>;
144
+ } & AuthoredSquareBoardInput
145
+ : never;
146
+
147
+ export type SquareGridProps<
148
+ TProps extends SquareGridInputProps = SquareGridInputProps,
149
+ > = TProps & {
150
+ cellSize?: number;
151
+ /** Receives row/col with transform centered at cell position */
152
+ renderCell: (row: number, col: number) => ReactNode;
153
+ /** Receives piece with transform centered at cell center */
154
+ renderPiece: (
155
+ piece: NormalizedSquarePieceOf<NoInfer<SquareBoardLikeOfProps<TProps>>>,
156
+ ) => ReactNode;
157
+ renderEdge?: (
158
+ edge: NormalizedSquareEdgeOf<NoInfer<SquareBoardLikeOfProps<TProps>>>,
159
+ position: SquareEdgePosition,
160
+ ) => ReactNode;
161
+ renderVertex?: (
162
+ vertex: NormalizedSquareVertexOf<NoInfer<SquareBoardLikeOfProps<TProps>>>,
163
+ position: SquareVertexPosition,
164
+ ) => ReactNode;
165
+ showCoordinates?: boolean;
166
+ coordinateStyle?: "algebraic" | "numeric" | "none";
167
+ width?: number | string;
168
+ height?: number | string;
169
+ enablePanZoom?: boolean;
170
+ initialZoom?: number;
171
+ minZoom?: number;
172
+ maxZoom?: number;
173
+ className?: string;
174
+ interactiveSpaces?: InteractiveTargetLayer;
175
+ interactiveEdges?: InteractiveTargetLayer;
176
+ interactiveVertices?: InteractiveTargetLayer;
177
+ renderInteractiveSpace?: (
178
+ space: InteractiveSquareSpace<NoInfer<SquareBoardLikeOfProps<TProps>>>,
179
+ state: InteractiveTargetRenderState,
180
+ ) => ReactNode;
181
+ renderInteractiveEdge?: (
182
+ edge: InteractiveSquareEdge<NoInfer<SquareBoardLikeOfProps<TProps>>>,
183
+ position: SquareEdgePosition,
184
+ state: InteractiveTargetRenderState,
185
+ ) => ReactNode;
186
+ renderInteractiveVertex?: (
187
+ vertex: InteractiveSquareVertex<NoInfer<SquareBoardLikeOfProps<TProps>>>,
188
+ position: SquareVertexPosition,
189
+ state: InteractiveTargetRenderState,
190
+ ) => ReactNode;
191
+ };
192
+
193
+ export interface SquareGridBoardProps<
194
+ TBoard extends AnySquareBoardInput = AnySquareBoardInput,
195
+ > {
196
+ board: TBoard;
197
+ cellSize?: number;
198
+ renderCell: (row: number, col: number) => ReactNode;
199
+ renderPiece: (piece: NormalizedSquarePieceOf<NoInfer<TBoard>>) => ReactNode;
200
+ renderEdge?: (
201
+ edge: NormalizedSquareEdgeOf<NoInfer<TBoard>>,
202
+ position: SquareEdgePosition,
203
+ ) => ReactNode;
204
+ renderVertex?: (
205
+ vertex: NormalizedSquareVertexOf<NoInfer<TBoard>>,
206
+ position: SquareVertexPosition,
207
+ ) => ReactNode;
208
+ showCoordinates?: boolean;
209
+ coordinateStyle?: "algebraic" | "numeric" | "none";
210
+ width?: number | string;
211
+ height?: number | string;
212
+ enablePanZoom?: boolean;
213
+ initialZoom?: number;
214
+ minZoom?: number;
215
+ maxZoom?: number;
216
+ className?: string;
217
+ interactiveSpaces?: InteractiveTargetLayer;
218
+ interactiveEdges?: InteractiveTargetLayer;
219
+ interactiveVertices?: InteractiveTargetLayer;
220
+ renderInteractiveSpace?: (
221
+ space: InteractiveSquareSpace<NoInfer<TBoard>>,
222
+ state: InteractiveTargetRenderState,
223
+ ) => ReactNode;
224
+ renderInteractiveEdge?: (
225
+ edge: InteractiveSquareEdge<NoInfer<TBoard>>,
226
+ position: SquareEdgePosition,
227
+ state: InteractiveTargetRenderState,
228
+ ) => ReactNode;
229
+ renderInteractiveVertex?: (
230
+ vertex: InteractiveSquareVertex<NoInfer<TBoard>>,
231
+ position: SquareVertexPosition,
232
+ state: InteractiveTargetRenderState,
233
+ ) => ReactNode;
234
+ }
235
+
236
+ // ============================================================================
237
+ // Pre-built Helper Components
238
+ // ============================================================================
239
+
240
+ export interface DefaultGridCellProps {
241
+ size: number;
242
+ isLight?: boolean;
243
+ lightColor?: string;
244
+ darkColor?: string;
245
+ isHighlighted?: boolean;
246
+ highlightColor?: string;
247
+ isSelected?: boolean;
248
+ selectedColor?: string;
249
+ isValidMove?: boolean;
250
+ isCapture?: boolean;
251
+ onClick?: () => void;
252
+ onPointerEnter?: () => void;
253
+ onPointerLeave?: () => void;
254
+ className?: string;
255
+ }
256
+
257
+ /** Pre-built grid cell component for use in `renderCell`. */
258
+ export function DefaultGridCell({
259
+ size,
260
+ isLight = true,
261
+ lightColor = "#f0d9b5",
262
+ darkColor = "#b58863",
263
+ isHighlighted = false,
264
+ highlightColor = "rgba(250, 204, 21, 0.4)",
265
+ isSelected = false,
266
+ selectedColor = "rgba(59, 130, 246, 0.5)",
267
+ isValidMove = false,
268
+ isCapture = false,
269
+ onClick,
270
+ onPointerEnter,
271
+ onPointerLeave,
272
+ className,
273
+ }: DefaultGridCellProps) {
274
+ const baseColor = isLight ? lightColor : darkColor;
275
+
276
+ return (
277
+ <g
278
+ onClick={onClick}
279
+ onPointerEnter={onPointerEnter}
280
+ onPointerLeave={onPointerLeave}
281
+ onKeyDown={(event) => handleKeyboardActivation(event, onClick)}
282
+ className={clsx(
283
+ "transition-colors duration-100",
284
+ onClick && "cursor-pointer",
285
+ className,
286
+ )}
287
+ role={onClick ? "button" : undefined}
288
+ tabIndex={onClick ? 0 : undefined}
289
+ aria-label={onClick ? "Grid cell" : undefined}
290
+ >
291
+ {/* Base cell */}
292
+ <rect width={size} height={size} fill={baseColor} />
293
+
294
+ {/* Selected overlay */}
295
+ {isSelected && (
296
+ <rect
297
+ width={size}
298
+ height={size}
299
+ fill={selectedColor}
300
+ pointerEvents="none"
301
+ />
302
+ )}
303
+
304
+ {/* Highlight overlay */}
305
+ {isHighlighted && !isSelected && (
306
+ <rect
307
+ width={size}
308
+ height={size}
309
+ fill={highlightColor}
310
+ pointerEvents="none"
311
+ />
312
+ )}
313
+
314
+ {/* Valid move indicator (dot) */}
315
+ {isValidMove && !isCapture && (
316
+ <circle
317
+ cx={size / 2}
318
+ cy={size / 2}
319
+ r={size * 0.15}
320
+ fill="rgba(34, 197, 94, 0.6)"
321
+ pointerEvents="none"
322
+ />
323
+ )}
324
+
325
+ {/* Capture indicator (ring) */}
326
+ {isCapture && (
327
+ <circle
328
+ cx={size / 2}
329
+ cy={size / 2}
330
+ r={size * 0.42}
331
+ fill="none"
332
+ stroke="rgba(239, 68, 68, 0.8)"
333
+ strokeWidth={size * 0.08}
334
+ pointerEvents="none"
335
+ />
336
+ )}
337
+ </g>
338
+ );
339
+ }
340
+
341
+ export interface DefaultGridPieceProps {
342
+ size: number;
343
+ color?: string;
344
+ strokeColor?: string;
345
+ label?: string;
346
+ isDragging?: boolean;
347
+ onClick?: () => void;
348
+ onPointerDown?: (e: React.PointerEvent) => void;
349
+ className?: string;
350
+ }
351
+
352
+ /** Pre-built grid piece component for use in `renderPiece`. */
353
+ export function DefaultGridPiece({
354
+ size,
355
+ color = "#94a3b8",
356
+ strokeColor,
357
+ label,
358
+ isDragging = false,
359
+ onClick,
360
+ onPointerDown,
361
+ className,
362
+ }: DefaultGridPieceProps) {
363
+ const radius = size * 0.38;
364
+ const effectiveStroke =
365
+ strokeColor ??
366
+ (color === "#f8fafc" || color === "#ffffff" ? "#1e293b" : "#f8fafc");
367
+
368
+ return (
369
+ <g
370
+ onClick={onClick}
371
+ onPointerDown={onPointerDown}
372
+ onKeyDown={(event) => handleKeyboardActivation(event, onClick)}
373
+ className={clsx(
374
+ "transition-transform duration-150",
375
+ (onClick || onPointerDown) && "cursor-pointer hover:scale-105",
376
+ className,
377
+ )}
378
+ opacity={isDragging ? 0.8 : 1}
379
+ role={onClick ? "button" : undefined}
380
+ tabIndex={onClick ? 0 : undefined}
381
+ aria-label={onClick ? (label ?? "Grid piece") : undefined}
382
+ >
383
+ <circle
384
+ r={isDragging ? radius * 1.1 : radius}
385
+ fill={color}
386
+ stroke={effectiveStroke}
387
+ strokeWidth={2}
388
+ style={{ filter: "drop-shadow(1px 2px 2px rgba(0,0,0,0.4))" }}
389
+ />
390
+ {label && (
391
+ <text
392
+ y={4}
393
+ textAnchor="middle"
394
+ fill={effectiveStroke}
395
+ fontSize={size * 0.35}
396
+ fontWeight="bold"
397
+ pointerEvents="none"
398
+ >
399
+ {label}
400
+ </text>
401
+ )}
402
+ </g>
403
+ );
404
+ }
405
+
406
+ export interface DefaultChessPieceProps {
407
+ size: number;
408
+ type: string;
409
+ owner: "white" | "black";
410
+ onClick?: () => void;
411
+ onPointerDown?: (e: React.PointerEvent) => void;
412
+ className?: string;
413
+ }
414
+
415
+ const CHESS_SYMBOLS: Record<string, Record<string, string>> = {
416
+ white: {
417
+ king: "♔",
418
+ queen: "♕",
419
+ rook: "♖",
420
+ bishop: "♗",
421
+ knight: "♘",
422
+ pawn: "♙",
423
+ },
424
+ black: {
425
+ king: "♚",
426
+ queen: "♛",
427
+ rook: "♜",
428
+ bishop: "♝",
429
+ knight: "♞",
430
+ pawn: "♟",
431
+ },
432
+ };
433
+
434
+ /** Pre-built chess piece component using Unicode symbols. */
435
+ export function DefaultChessPiece({
436
+ size,
437
+ type,
438
+ owner,
439
+ onClick,
440
+ onPointerDown,
441
+ className,
442
+ }: DefaultChessPieceProps) {
443
+ const symbol = CHESS_SYMBOLS[owner]?.[type] ?? "?";
444
+ const textColor = owner === "white" ? "#f8fafc" : "#1e293b";
445
+ const shadowFilter =
446
+ owner === "white"
447
+ ? "drop-shadow(1px 1px 1px rgba(0,0,0,0.5))"
448
+ : "drop-shadow(1px 1px 1px rgba(255,255,255,0.3))";
449
+
450
+ return (
451
+ <g
452
+ onClick={onClick}
453
+ onPointerDown={onPointerDown}
454
+ onKeyDown={(event) => handleKeyboardActivation(event, onClick)}
455
+ className={clsx(
456
+ (onClick || onPointerDown) && "cursor-pointer",
457
+ className,
458
+ )}
459
+ role={onClick ? "button" : undefined}
460
+ tabIndex={onClick ? 0 : undefined}
461
+ aria-label={onClick ? `${owner} ${type}` : undefined}
462
+ >
463
+ <text
464
+ textAnchor="middle"
465
+ dominantBaseline="middle"
466
+ fontSize={size * 0.7}
467
+ fill={textColor}
468
+ style={{ filter: shadowFilter }}
469
+ >
470
+ {symbol}
471
+ </text>
472
+ </g>
473
+ );
474
+ }
475
+
476
+ // ============================================================================
477
+ // Utilities
478
+ // ============================================================================
479
+
480
+ /**
481
+ * Convert row/col to algebraic notation (a1, b2, etc.)
482
+ */
483
+ export function toAlgebraic(
484
+ row: number,
485
+ col: number,
486
+ totalRows: number,
487
+ ): string {
488
+ const file = String.fromCharCode(97 + col); // a, b, c, ...
489
+ const rank = totalRows - row; // 8, 7, 6, ... (bottom to top)
490
+ return `${file}${rank}`;
491
+ }
492
+
493
+ /**
494
+ * Convert row/col to numeric notation (1,1, 2,3, etc.)
495
+ */
496
+ export function toNumeric(row: number, col: number): string {
497
+ return `${row + 1},${col + 1}`;
498
+ }
499
+
500
+ function getCellId(cell: { id: string }): string {
501
+ return cell.id;
502
+ }
503
+
504
+ function edgePositionForCells(
505
+ firstCell: { row: number; col: number },
506
+ secondCell: { row: number; col: number },
507
+ cellSize: number,
508
+ labelMargin: number,
509
+ ): SquareEdgePosition | null {
510
+ if (
511
+ Math.abs(firstCell.row - secondCell.row) +
512
+ Math.abs(firstCell.col - secondCell.col) !==
513
+ 1
514
+ ) {
515
+ return null;
516
+ }
517
+
518
+ const minRow = Math.min(firstCell.row, secondCell.row);
519
+ const minCol = Math.min(firstCell.col, secondCell.col);
520
+
521
+ if (firstCell.row === secondCell.row) {
522
+ const x = labelMargin + (minCol + 1) * cellSize;
523
+ const y1 = minRow * cellSize;
524
+ const y2 = y1 + cellSize;
525
+ return {
526
+ x1: x,
527
+ y1,
528
+ x2: x,
529
+ y2,
530
+ midX: x,
531
+ midY: (y1 + y2) / 2,
532
+ angle: 90,
533
+ };
534
+ }
535
+
536
+ const y = (minRow + 1) * cellSize;
537
+ const x1 = labelMargin + minCol * cellSize;
538
+ const x2 = x1 + cellSize;
539
+ return {
540
+ x1,
541
+ y1: y,
542
+ x2,
543
+ y2: y,
544
+ midX: (x1 + x2) / 2,
545
+ midY: y,
546
+ angle: 0,
547
+ };
548
+ }
549
+
550
+ function cornerKeysForCell(cell: {
551
+ row: number;
552
+ col: number;
553
+ }): Record<string, SquareVertexPosition> {
554
+ return {
555
+ [`${cell.col},${cell.row}`]: { x: cell.col, y: cell.row },
556
+ [`${cell.col + 1},${cell.row}`]: { x: cell.col + 1, y: cell.row },
557
+ [`${cell.col + 1},${cell.row + 1}`]: {
558
+ x: cell.col + 1,
559
+ y: cell.row + 1,
560
+ },
561
+ [`${cell.col},${cell.row + 1}`]: { x: cell.col, y: cell.row + 1 },
562
+ };
563
+ }
564
+
565
+ function vertexPositionForCells(
566
+ cells: readonly SquareCellWithId[],
567
+ cellSize: number,
568
+ labelMargin: number,
569
+ ): SquareVertexPosition | null {
570
+ if (cells.length === 0) {
571
+ return null;
572
+ }
573
+
574
+ const candidateKeys = cells.map(
575
+ (cell) => new Set(Object.keys(cornerKeysForCell(cell))),
576
+ );
577
+ const firstKeySet = candidateKeys[0];
578
+ if (firstKeySet === undefined) {
579
+ return null;
580
+ }
581
+ const sharedKeys = [...firstKeySet].filter((key) =>
582
+ candidateKeys.every((keySet) => keySet.has(key)),
583
+ );
584
+ if (sharedKeys.length !== 1) {
585
+ return null;
586
+ }
587
+
588
+ const sharedKey = sharedKeys[0];
589
+ if (!sharedKey) {
590
+ return null;
591
+ }
592
+ const [colToken, rowToken] = sharedKey.split(",");
593
+ if (colToken === undefined || rowToken === undefined) {
594
+ return null;
595
+ }
596
+ const col = Number(colToken);
597
+ const row = Number(rowToken);
598
+ if (!Number.isFinite(col) || !Number.isFinite(row)) {
599
+ return null;
600
+ }
601
+
602
+ return {
603
+ x: labelMargin + col * cellSize,
604
+ y: row * cellSize,
605
+ };
606
+ }
607
+
608
+ // ============================================================================
609
+ // Component
610
+ // ============================================================================
611
+
612
+ export interface SquareGridComponent {
613
+ <const TBoard extends AnySquareBoardInput>(
614
+ props: SquareGridBoardProps<TBoard>,
615
+ ): ReactNode;
616
+ <const TProps extends SquareGeneratedGridInputProps>(
617
+ props: SquareGridProps<TProps>,
618
+ ): ReactNode;
619
+ <const TProps extends SquareAuthoredGridInputProps>(
620
+ props: SquareGridProps<TProps>,
621
+ ): ReactNode;
622
+ }
623
+
624
+ function SquareGridImpl(
625
+ props:
626
+ | SquareGridBoardProps<AnySquareBoardInput>
627
+ | SquareGridProps<SquareGridInputProps>,
628
+ ) {
629
+ const {
630
+ cellSize = 60,
631
+ renderCell,
632
+ renderPiece,
633
+ renderEdge,
634
+ renderVertex,
635
+ showCoordinates = true,
636
+ coordinateStyle = "algebraic",
637
+ width,
638
+ height,
639
+ enablePanZoom = false,
640
+ initialZoom = 1,
641
+ minZoom = 0.5,
642
+ maxZoom = 3,
643
+ className,
644
+ interactiveSpaces,
645
+ interactiveEdges,
646
+ interactiveVertices,
647
+ renderInteractiveSpace,
648
+ renderInteractiveEdge,
649
+ renderInteractiveVertex,
650
+ } = props;
651
+ const board =
652
+ "board" in props
653
+ ? props.board
654
+ : (("spaces" in props
655
+ ? {
656
+ id: "__square-grid__",
657
+ spaces: props.spaces,
658
+ pieces: props.pieces ?? [],
659
+ edges: props.edges ?? [],
660
+ vertices: props.vertices ?? [],
661
+ }
662
+ : {
663
+ id: "__square-grid__",
664
+ rows: props.rows ?? 0,
665
+ cols: props.cols ?? 0,
666
+ cells: props.cells,
667
+ pieces: props.pieces ?? [],
668
+ edges: props.edges ?? [],
669
+ vertices: props.vertices ?? [],
670
+ }) satisfies AnySquareBoardInput);
671
+ const [hoveredInteractiveSpaceId, setHoveredInteractiveSpaceId] = useState<
672
+ string | null
673
+ >(null);
674
+ const [hoveredInteractiveEdgeId, setHoveredInteractiveEdgeId] = useState<
675
+ string | null
676
+ >(null);
677
+ const [hoveredInteractiveVertexId, setHoveredInteractiveVertexId] = useState<
678
+ string | null
679
+ >(null);
680
+
681
+ // Use the unified pan/zoom hook
682
+ const {
683
+ transform,
684
+ bind,
685
+ isDragging: isPanning,
686
+ } = usePanZoom({
687
+ enabled: enablePanZoom,
688
+ initialZoom,
689
+ minZoom,
690
+ maxZoom,
691
+ mode: "viewbox",
692
+ });
693
+
694
+ // Coordinate label margin
695
+ const labelMargin = showCoordinates && coordinateStyle !== "none" ? 24 : 0;
696
+
697
+ const normalizedBoard = useMemo<NormalizedSquareBoard<AnySquareBoardInput>>(
698
+ () => normalizeSquareBoardInput(board),
699
+ [board],
700
+ );
701
+
702
+ const rows = normalizedBoard.rows;
703
+ const cols = normalizedBoard.cols;
704
+ const resolvedEdges = normalizedBoard.edges as Array<
705
+ NormalizedSquareEdgeOf<AnySquareBoardInput>
706
+ >;
707
+ const resolvedVertices = normalizedBoard.vertices as Array<
708
+ NormalizedSquareVertexOf<AnySquareBoardInput>
709
+ >;
710
+ const resolvedPieces = normalizedBoard.pieces as Array<
711
+ NormalizedSquarePieceOf<AnySquareBoardInput>
712
+ >;
713
+
714
+ // Calculate grid dimensions
715
+ const gridWidth = cols * cellSize;
716
+ const gridHeight = rows * cellSize;
717
+ const totalWidth = gridWidth + labelMargin;
718
+ const totalHeight = gridHeight + labelMargin;
719
+
720
+ const renderableCells = useMemo(() => {
721
+ const result: Array<{ row: number; col: number }> = [];
722
+ for (let row = 0; row < rows; row++) {
723
+ for (let col = 0; col < cols; col++) {
724
+ result.push({ row, col });
725
+ }
726
+ }
727
+ return result;
728
+ }, [rows, cols]);
729
+
730
+ const resolvedCells = useMemo<SquareCellWithId[]>(() => {
731
+ if (normalizedBoard.cells.length > 0) {
732
+ return normalizedBoard.cells.map((cell) => ({
733
+ ...cell,
734
+ id: getCellId(cell),
735
+ }));
736
+ }
737
+
738
+ return renderableCells.map(({ row, col }) => ({
739
+ id: `${row},${col}`,
740
+ row,
741
+ col,
742
+ }));
743
+ }, [normalizedBoard.cells, renderableCells]);
744
+
745
+ const cellsById = useMemo(
746
+ () => new Map(resolvedCells.map((cell) => [cell.id, cell] as const)),
747
+ [resolvedCells],
748
+ );
749
+
750
+ const resolvedEdgePositions = useMemo(
751
+ () =>
752
+ resolvedEdges.flatMap((edge) => {
753
+ if (edge.spaceIds.length < 2) {
754
+ return [];
755
+ }
756
+ const firstCell = cellsById.get(edge.spaceIds[0] ?? "");
757
+ const secondCell = cellsById.get(edge.spaceIds[1] ?? "");
758
+ if (!firstCell || !secondCell) {
759
+ return [];
760
+ }
761
+ const position = edgePositionForCells(
762
+ firstCell,
763
+ secondCell,
764
+ cellSize,
765
+ labelMargin,
766
+ );
767
+ return position
768
+ ? [
769
+ {
770
+ edge,
771
+ interactiveEdge: {
772
+ ...edge,
773
+ position,
774
+ } as InteractiveSquareEdge<AnySquareBoardInput>,
775
+ },
776
+ ]
777
+ : [];
778
+ }),
779
+ [cellSize, cellsById, resolvedEdges, labelMargin],
780
+ );
781
+
782
+ const resolvedVertexPositions = useMemo(
783
+ () =>
784
+ resolvedVertices.flatMap((vertex) => {
785
+ const vertexCells = vertex.spaceIds.flatMap((spaceId) => {
786
+ const cell = cellsById.get(spaceId);
787
+ return cell ? [cell] : [];
788
+ });
789
+ const position = vertexPositionForCells(
790
+ vertexCells,
791
+ cellSize,
792
+ labelMargin,
793
+ );
794
+ return position
795
+ ? [
796
+ {
797
+ vertex,
798
+ interactiveVertex: {
799
+ ...vertex,
800
+ position,
801
+ } as InteractiveSquareVertex<AnySquareBoardInput>,
802
+ },
803
+ ]
804
+ : [];
805
+ }),
806
+ [cellSize, cellsById, labelMargin, resolvedVertices],
807
+ );
808
+
809
+ // Calculate viewBox for pan/zoom
810
+ const viewBoxWidth = totalWidth / transform.zoom;
811
+ const viewBoxHeight = totalHeight / transform.zoom;
812
+ const viewBoxX = (totalWidth - viewBoxWidth) / 2 - transform.pan.x;
813
+ const viewBoxY = (totalHeight - viewBoxHeight) / 2 - transform.pan.y;
814
+
815
+ // Determine SVG dimensions
816
+ const svgWidth = width ?? totalWidth;
817
+ const svgHeight = height ?? totalHeight;
818
+
819
+ return (
820
+ <svg
821
+ width={svgWidth}
822
+ height={svgHeight}
823
+ viewBox={
824
+ enablePanZoom
825
+ ? `${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}`
826
+ : `0 0 ${totalWidth} ${totalHeight}`
827
+ }
828
+ className={clsx(
829
+ "square-grid",
830
+ enablePanZoom && "touch-none",
831
+ isPanning && "cursor-grabbing",
832
+ enablePanZoom && !isPanning && "cursor-grab",
833
+ className,
834
+ )}
835
+ {...bind()}
836
+ role="img"
837
+ aria-label={`${rows}x${cols} game grid`}
838
+ >
839
+ <defs>
840
+ {/* Drop shadow for pieces */}
841
+ <filter id="pieceShadow" x="-20%" y="-20%" width="140%" height="140%">
842
+ <feDropShadow dx="1" dy="2" stdDeviation="2" floodOpacity="0.4" />
843
+ </filter>
844
+ </defs>
845
+
846
+ {/* Cells layer */}
847
+ <g className="cells" role="list" aria-label="Grid cells">
848
+ {renderableCells.map(({ row, col }) => {
849
+ const x = labelMargin + col * cellSize;
850
+ const y = row * cellSize;
851
+
852
+ return (
853
+ <g
854
+ key={`${row}-${col}`}
855
+ transform={`translate(${x}, ${y})`}
856
+ role="listitem"
857
+ aria-label={
858
+ coordinateStyle === "algebraic"
859
+ ? toAlgebraic(row, col, rows)
860
+ : toNumeric(row, col)
861
+ }
862
+ >
863
+ {renderCell(row, col)}
864
+ </g>
865
+ );
866
+ })}
867
+ </g>
868
+
869
+ {interactiveSpaces && (
870
+ <g className="interactive-spaces" aria-label="Interactive spaces">
871
+ {resolvedCells.map((space) => {
872
+ const state = interactiveTargetRenderState(
873
+ interactiveSpaces,
874
+ space.id,
875
+ hoveredInteractiveSpaceId === space.id,
876
+ );
877
+ const isSelectable = isInteractiveTargetSelectable(
878
+ interactiveSpaces,
879
+ state,
880
+ );
881
+ const x = labelMargin + space.col * cellSize;
882
+ const y = space.row * cellSize;
883
+ return (
884
+ <g
885
+ key={space.id}
886
+ transform={`translate(${x}, ${y})`}
887
+ onClick={
888
+ isSelectable
889
+ ? () => {
890
+ void interactiveSpaces.selectTargetId?.(space.id);
891
+ }
892
+ : undefined
893
+ }
894
+ onKeyDown={(event) =>
895
+ handleKeyboardActivation(
896
+ event,
897
+ isSelectable
898
+ ? () => {
899
+ void interactiveSpaces.selectTargetId?.(space.id);
900
+ }
901
+ : undefined,
902
+ )
903
+ }
904
+ onPointerEnter={() => setHoveredInteractiveSpaceId(space.id)}
905
+ onPointerLeave={() =>
906
+ setHoveredInteractiveSpaceId((currentId) =>
907
+ currentId === space.id ? null : currentId,
908
+ )
909
+ }
910
+ className={clsx(isSelectable && "cursor-pointer")}
911
+ role={isSelectable ? "button" : undefined}
912
+ tabIndex={isSelectable ? 0 : undefined}
913
+ aria-label={
914
+ isSelectable ? `Select space ${space.id}` : undefined
915
+ }
916
+ >
917
+ {isSelectable && (
918
+ <rect
919
+ x={0}
920
+ y={0}
921
+ width={cellSize}
922
+ height={cellSize}
923
+ fill="rgba(255,255,255,0.001)"
924
+ pointerEvents="all"
925
+ />
926
+ )}
927
+ {renderInteractiveSpace
928
+ ? renderInteractiveSpace(space, state)
929
+ : null}
930
+ </g>
931
+ );
932
+ })}
933
+ </g>
934
+ )}
935
+
936
+ {renderEdge && resolvedEdgePositions.length > 0 && (
937
+ <g className="edges" aria-label="Board edges">
938
+ {resolvedEdgePositions.map(({ edge, interactiveEdge }) => (
939
+ <g key={edge.id}>{renderEdge(edge, interactiveEdge.position)}</g>
940
+ ))}
941
+ </g>
942
+ )}
943
+
944
+ {interactiveEdges && (
945
+ <g className="interactive-edges" aria-label="Interactive edges">
946
+ {resolvedEdgePositions.map(({ interactiveEdge: edge }) => {
947
+ const state = interactiveTargetRenderState(
948
+ interactiveEdges,
949
+ edge.id,
950
+ hoveredInteractiveEdgeId === edge.id,
951
+ );
952
+ const isSelectable = isInteractiveTargetSelectable(
953
+ interactiveEdges,
954
+ state,
955
+ );
956
+ return (
957
+ <g
958
+ key={edge.id}
959
+ onClick={
960
+ isSelectable
961
+ ? () => {
962
+ void interactiveEdges.selectTargetId?.(edge.id);
963
+ }
964
+ : undefined
965
+ }
966
+ onKeyDown={(event) =>
967
+ handleKeyboardActivation(
968
+ event,
969
+ isSelectable
970
+ ? () => {
971
+ void interactiveEdges.selectTargetId?.(edge.id);
972
+ }
973
+ : undefined,
974
+ )
975
+ }
976
+ onPointerEnter={() => setHoveredInteractiveEdgeId(edge.id)}
977
+ onPointerLeave={() =>
978
+ setHoveredInteractiveEdgeId((currentId) =>
979
+ currentId === edge.id ? null : currentId,
980
+ )
981
+ }
982
+ className={clsx(isSelectable && "cursor-pointer")}
983
+ role={isSelectable ? "button" : undefined}
984
+ tabIndex={isSelectable ? 0 : undefined}
985
+ aria-label={isSelectable ? `Select edge ${edge.id}` : undefined}
986
+ >
987
+ {renderInteractiveEdge ? (
988
+ renderInteractiveEdge(edge, edge.position, state)
989
+ ) : state.isEnabled && state.isEligible ? (
990
+ <line
991
+ x1={edge.position.x1}
992
+ y1={edge.position.y1}
993
+ x2={edge.position.x2}
994
+ y2={edge.position.y2}
995
+ stroke="rgba(255,255,255,0.001)"
996
+ strokeWidth={Math.max(12, cellSize * 0.18)}
997
+ pointerEvents="stroke"
998
+ />
999
+ ) : null}
1000
+ </g>
1001
+ );
1002
+ })}
1003
+ </g>
1004
+ )}
1005
+
1006
+ {renderVertex && resolvedVertexPositions.length > 0 && (
1007
+ <g className="vertices" aria-label="Board vertices">
1008
+ {resolvedVertexPositions.map(({ vertex, interactiveVertex }) => (
1009
+ <g key={vertex.id}>
1010
+ {renderVertex(vertex, interactiveVertex.position)}
1011
+ </g>
1012
+ ))}
1013
+ </g>
1014
+ )}
1015
+
1016
+ {interactiveVertices && (
1017
+ <g className="interactive-vertices" aria-label="Interactive vertices">
1018
+ {resolvedVertexPositions.map(({ interactiveVertex: vertex }) => {
1019
+ const state = interactiveTargetRenderState(
1020
+ interactiveVertices,
1021
+ vertex.id,
1022
+ hoveredInteractiveVertexId === vertex.id,
1023
+ );
1024
+ const isSelectable = isInteractiveTargetSelectable(
1025
+ interactiveVertices,
1026
+ state,
1027
+ );
1028
+ return (
1029
+ <g
1030
+ key={vertex.id}
1031
+ onClick={
1032
+ isSelectable
1033
+ ? () => {
1034
+ void interactiveVertices.selectTargetId?.(vertex.id);
1035
+ }
1036
+ : undefined
1037
+ }
1038
+ onKeyDown={(event) =>
1039
+ handleKeyboardActivation(
1040
+ event,
1041
+ isSelectable
1042
+ ? () => {
1043
+ void interactiveVertices.selectTargetId?.(vertex.id);
1044
+ }
1045
+ : undefined,
1046
+ )
1047
+ }
1048
+ onPointerEnter={() => setHoveredInteractiveVertexId(vertex.id)}
1049
+ onPointerLeave={() =>
1050
+ setHoveredInteractiveVertexId((currentId) =>
1051
+ currentId === vertex.id ? null : currentId,
1052
+ )
1053
+ }
1054
+ className={clsx(isSelectable && "cursor-pointer")}
1055
+ role={isSelectable ? "button" : undefined}
1056
+ tabIndex={isSelectable ? 0 : undefined}
1057
+ aria-label={
1058
+ isSelectable ? `Select vertex ${vertex.id}` : undefined
1059
+ }
1060
+ >
1061
+ {renderInteractiveVertex ? (
1062
+ renderInteractiveVertex(vertex, vertex.position, state)
1063
+ ) : state.isEnabled && state.isEligible ? (
1064
+ <circle
1065
+ cx={vertex.position.x}
1066
+ cy={vertex.position.y}
1067
+ r={Math.max(8, cellSize * 0.12)}
1068
+ fill="rgba(255,255,255,0.001)"
1069
+ pointerEvents="all"
1070
+ />
1071
+ ) : null}
1072
+ </g>
1073
+ );
1074
+ })}
1075
+ </g>
1076
+ )}
1077
+
1078
+ {/* Coordinate labels */}
1079
+ {showCoordinates && coordinateStyle !== "none" && (
1080
+ <g className="coordinates" aria-hidden="true">
1081
+ {/* File labels (a-h) - bottom */}
1082
+ {Array.from({ length: cols }).map((_, col) => {
1083
+ const label =
1084
+ coordinateStyle === "algebraic"
1085
+ ? String.fromCharCode(97 + col)
1086
+ : String(col + 1);
1087
+ return (
1088
+ <text
1089
+ key={`file-${col}`}
1090
+ x={labelMargin + col * cellSize + cellSize / 2}
1091
+ y={gridHeight + 16}
1092
+ textAnchor="middle"
1093
+ fill="#64748b"
1094
+ fontSize={12}
1095
+ fontWeight="500"
1096
+ >
1097
+ {label}
1098
+ </text>
1099
+ );
1100
+ })}
1101
+ {/* Rank labels (1-8) - left */}
1102
+ {Array.from({ length: rows }).map((_, row) => {
1103
+ const label =
1104
+ coordinateStyle === "algebraic"
1105
+ ? String(rows - row)
1106
+ : String(row + 1);
1107
+ return (
1108
+ <text
1109
+ key={`rank-${row}`}
1110
+ x={10}
1111
+ y={row * cellSize + cellSize / 2 + 4}
1112
+ textAnchor="middle"
1113
+ fill="#64748b"
1114
+ fontSize={12}
1115
+ fontWeight="500"
1116
+ >
1117
+ {label}
1118
+ </text>
1119
+ );
1120
+ })}
1121
+ </g>
1122
+ )}
1123
+
1124
+ {/* Pieces layer */}
1125
+ <g className="pieces" role="list" aria-label="Game pieces">
1126
+ {resolvedPieces.map((piece) => {
1127
+ const x = labelMargin + piece.col * cellSize + cellSize / 2;
1128
+ const y = piece.row * cellSize + cellSize / 2;
1129
+
1130
+ return (
1131
+ <g
1132
+ key={piece.id}
1133
+ transform={`translate(${x}, ${y})`}
1134
+ role="listitem"
1135
+ aria-label={`${piece.owner ?? ""} ${piece.typeId}`}
1136
+ >
1137
+ {renderPiece(piece)}
1138
+ </g>
1139
+ );
1140
+ })}
1141
+ </g>
1142
+
1143
+ {/* Zoom indicator */}
1144
+ {enablePanZoom && transform.zoom !== 1 && (
1145
+ <g
1146
+ transform={`translate(${viewBoxX + 10}, ${viewBoxY + viewBoxHeight - 30})`}
1147
+ >
1148
+ <rect
1149
+ x={0}
1150
+ y={0}
1151
+ width={60}
1152
+ height={20}
1153
+ rx={4}
1154
+ fill="rgba(0,0,0,0.6)"
1155
+ />
1156
+ <text x={30} y={14} textAnchor="middle" fill="white" fontSize={12}>
1157
+ {Math.round(transform.zoom * 100)}%
1158
+ </text>
1159
+ </g>
1160
+ )}
1161
+ </svg>
1162
+ );
1163
+ }
1164
+
1165
+ export const SquareGrid = SquareGridImpl as SquareGridComponent;