@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,1076 @@
1
+ /**
2
+ * Controlled drag-to-target surface for the SDK hand and drop-target views.
3
+ *
4
+ * `CardDragSurface` is the single owner of:
5
+ *
6
+ * - the drag-lifecycle phase (`idle`/`inspecting`/`dragging`/`settling`/
7
+ * `returning`)
8
+ * - the registry of drop targets, including their eligibility
9
+ * - all committed `CardIntent` emission (`activate`, `previewStart`,
10
+ * `previewEnd`, and `drop`)
11
+ * - the lifted-card overlay, settle/snap-back animation and live
12
+ * announcement
13
+ *
14
+ * `CardDropTargetView` is a generic controlled drop-target wrapper. It
15
+ * registers the underlying DOM element (and its eligible/disabled state) so
16
+ * the lifted pointer can be matched without exposing geometry to the
17
+ * caller. Its registration is stable: only the `targetId` triggers register/
18
+ * unregister; eligibility and label changes flow through `updateTarget`.
19
+ *
20
+ * `HandView` (and the hook it uses) drives this surface through the
21
+ * controller exposed by `useCardDragSurface()`. Pointer events come from
22
+ * `HandPointerEngine`'s lift callbacks. Keyboard pickup, target traversal
23
+ * and Escape are handled here so that the drag-lifecycle has exactly one
24
+ * authoritative owner.
25
+ */
26
+
27
+ import {
28
+ createContext,
29
+ useCallback,
30
+ useContext,
31
+ useEffect,
32
+ useId,
33
+ useMemo,
34
+ useRef,
35
+ useState,
36
+ type CSSProperties,
37
+ type KeyboardEvent,
38
+ type ReactNode,
39
+ } from "react";
40
+ import { createPortal } from "react-dom";
41
+ import { clsx } from "clsx";
42
+ import { AnimatePresence, motion, type Transition } from "framer-motion";
43
+ import { useTheme } from "../theme/ThemeProvider.js";
44
+ import {
45
+ dropTargetVisualStateDataAttributes,
46
+ type CardDropTargetVisualState,
47
+ type CardIntent,
48
+ } from "../types/visual-state.js";
49
+
50
+ interface RegisteredDropTarget {
51
+ targetId: string;
52
+ disabled: boolean;
53
+ eligible: boolean;
54
+ element: HTMLElement;
55
+ /** Plain-text label used for the live a11y announcement. */
56
+ label: string | null;
57
+ /** Order hint for keyboard target traversal (lower numbers focus first). */
58
+ order: number;
59
+ }
60
+
61
+ export type DragPhase =
62
+ | "idle"
63
+ | "inspecting"
64
+ | "dragging"
65
+ | "settling"
66
+ | "returning";
67
+
68
+ interface ActiveDragState {
69
+ cardId: string;
70
+ cardLabel: string | null;
71
+ source: "pointer" | "keyboard";
72
+ pointerId: number | null;
73
+ pointerX: number;
74
+ pointerY: number;
75
+ grabOffsetX: number;
76
+ grabOffsetY: number;
77
+ /**
78
+ * Source rectangle captured at lift time, used as the snap-back/origin
79
+ * geometry for animated returns.
80
+ */
81
+ sourceRect: { left: number; top: number; width: number; height: number };
82
+ content: ReactNode;
83
+ overTargetId: string | null;
84
+ keyboardFocusedTargetId: string | null;
85
+ /** DOM node we should focus when the lifecycle ends. */
86
+ sourceFocus: HTMLElement | null;
87
+ }
88
+
89
+ interface SettlingState {
90
+ cardId: string;
91
+ source: "pointer" | "keyboard";
92
+ pointerX: number;
93
+ pointerY: number;
94
+ grabOffsetX: number;
95
+ grabOffsetY: number;
96
+ sourceRect: { left: number; top: number; width: number; height: number };
97
+ targetRect: { left: number; top: number; width: number; height: number };
98
+ content: ReactNode;
99
+ }
100
+
101
+ interface ReturningState {
102
+ cardId: string;
103
+ source: "pointer" | "keyboard";
104
+ pointerX: number;
105
+ pointerY: number;
106
+ grabOffsetX: number;
107
+ grabOffsetY: number;
108
+ sourceRect: { left: number; top: number; width: number; height: number };
109
+ content: ReactNode;
110
+ }
111
+
112
+ export interface CardDragSurfaceController {
113
+ /** Identity of the card currently in the drag-lifecycle, if any. */
114
+ activeCardId: string | null;
115
+ /** Source of the active drag, if any. */
116
+ activeSource: "pointer" | "keyboard" | null;
117
+ /** Drag-lifecycle phase. */
118
+ phase: DragPhase;
119
+ /** Currently highlighted drop target id, if any. */
120
+ overTargetId: string | null;
121
+ /** Currently keyboard-focused target id, if any. */
122
+ keyboardFocusedTargetId: string | null;
123
+ /**
124
+ * Begin a pointer drag session. Returns `true` if the session started.
125
+ */
126
+ startPointerDrag: (input: PointerDragInput) => boolean;
127
+ /** Update the pointer coordinates of an active pointer drag. */
128
+ updatePointer: (point: { x: number; y: number }) => void;
129
+ /**
130
+ * Commit the active pointer drag at the supplied release position. Emits
131
+ * a `drop` intent if the pointer is over an eligible target, or schedules
132
+ * a snap back otherwise.
133
+ */
134
+ releasePointer: (point: { x: number; y: number }) => void;
135
+ /** Cancel the active drag (pointer or keyboard) without committing. */
136
+ cancelDrag: () => void;
137
+ /**
138
+ * Begin a keyboard drag session. The first eligible registered target is
139
+ * focused automatically.
140
+ */
141
+ startKeyboardDrag: (input: KeyboardDragInput) => boolean;
142
+ /** Move keyboard focus across registered eligible targets. */
143
+ moveKeyboardFocus: (direction: "next" | "prev") => void;
144
+ /** Commit the active keyboard drag on the focused target. */
145
+ commitKeyboardDrop: () => void;
146
+ /**
147
+ * Record a tap that did not produce a lift. The surface holds the
148
+ * `inspecting` phase until another lift, drop, or external dismissal.
149
+ */
150
+ recordTap: (input: TapInput) => void;
151
+ /**
152
+ * Record a `previewStart` intent. Surface owns canonical intent emission
153
+ * so consumers subscribe in one place.
154
+ */
155
+ recordPreviewStart: (cardId: string) => void;
156
+ /** Record a `previewEnd` intent. */
157
+ recordPreviewEnd: (cardId: string) => void;
158
+ /**
159
+ * Record a desktop/keyboard `activate` intent under the `direct-activate`
160
+ * policy. Drag-to-target policy never calls this; it is centralized here
161
+ * so that the surface remains the only ingress for `CardIntent`.
162
+ */
163
+ recordActivate: (cardId: string, source: "tap" | "keyboard") => void;
164
+ /** Clear the `inspecting` phase. */
165
+ clearInspect: () => void;
166
+ /** Total number of currently registered eligible targets. */
167
+ eligibleTargetCount: number;
168
+ }
169
+
170
+ interface TapInput {
171
+ cardId: string;
172
+ cardEligible: boolean;
173
+ cardDisabled: boolean;
174
+ sourceFocus?: HTMLElement | null;
175
+ }
176
+
177
+ interface PointerDragInput {
178
+ cardId: string;
179
+ cardLabel?: string | null;
180
+ pointerId: number;
181
+ startX: number;
182
+ startY: number;
183
+ pointerX: number;
184
+ pointerY: number;
185
+ grabOffsetX: number;
186
+ grabOffsetY: number;
187
+ sourceRect: { left: number; top: number; width: number; height: number };
188
+ content: ReactNode;
189
+ sourceFocus?: HTMLElement | null;
190
+ }
191
+
192
+ interface KeyboardDragInput {
193
+ cardId: string;
194
+ cardLabel?: string | null;
195
+ cardEligible: boolean;
196
+ sourceRect: { left: number; top: number; width: number; height: number };
197
+ content: ReactNode;
198
+ sourceFocus?: HTMLElement | null;
199
+ }
200
+
201
+ export interface CardDragSurfaceContextValue {
202
+ registerTarget: (target: RegisteredDropTarget) => () => void;
203
+ updateTarget: (
204
+ targetId: string,
205
+ patch: Partial<Omit<RegisteredDropTarget, "targetId" | "element">>,
206
+ ) => void;
207
+ controller: CardDragSurfaceController;
208
+ /** Expose the most recent active card id for visual-state computation. */
209
+ activeCardId: string | null;
210
+ /** Expose the highlighted target id for visual-state computation. */
211
+ overTargetId: string | null;
212
+ /** Expose whether any drag is in progress. */
213
+ dragActive: boolean;
214
+ keyboardFocusedTargetId: string | null;
215
+ }
216
+
217
+ const CardDragSurfaceContext =
218
+ createContext<CardDragSurfaceContextValue | null>(null);
219
+
220
+ export function useCardDragSurface(): CardDragSurfaceContextValue | null {
221
+ return useContext(CardDragSurfaceContext);
222
+ }
223
+
224
+ export interface CardDragSurfaceProps {
225
+ onCardIntent?: (intent: CardIntent) => void;
226
+ /**
227
+ * Approximate inset (px) used for the deterministic hit test. Defaults to
228
+ * `8`. Lowering this lets edges register more aggressively; raising it
229
+ * makes overlapping targets less ambiguous.
230
+ */
231
+ hitTestInsetPx?: number;
232
+ /** Suppress the live a11y announcement (for environments providing their own). */
233
+ suppressLiveAnnouncement?: boolean;
234
+ /** Animation transition tunable for settle/return. */
235
+ motionTransition?: Transition;
236
+ className?: string;
237
+ style?: CSSProperties;
238
+ children: ReactNode;
239
+ }
240
+
241
+ const SETTLE_TRANSITION: Transition = {
242
+ type: "spring",
243
+ stiffness: 380,
244
+ damping: 32,
245
+ mass: 0.9,
246
+ };
247
+ const RETURN_TRANSITION: Transition = {
248
+ type: "spring",
249
+ stiffness: 320,
250
+ damping: 26,
251
+ mass: 0.8,
252
+ };
253
+ const REDUCED_TRANSITION: Transition = { duration: 0 };
254
+
255
+ export function CardDragSurface({
256
+ onCardIntent,
257
+ hitTestInsetPx = 8,
258
+ suppressLiveAnnouncement = false,
259
+ motionTransition,
260
+ className,
261
+ style,
262
+ children,
263
+ }: CardDragSurfaceProps) {
264
+ const theme = useTheme();
265
+ const reducedMotion = theme.motion.reducedMotion === "true";
266
+ const liveRegionId = useId();
267
+ const targetsRef = useRef(new Map<string, RegisteredDropTarget>());
268
+ const orderCounterRef = useRef(0);
269
+ const activeDragRef = useRef<ActiveDragState | null>(null);
270
+ const inspectingRef = useRef<string | null>(null);
271
+ const [activeDrag, setActiveDrag] = useState<ActiveDragState | null>(null);
272
+ const [inspectingCardId, setInspectingCardId] = useState<string | null>(null);
273
+ const [settlingState, setSettlingState] = useState<SettlingState | null>(
274
+ null,
275
+ );
276
+ const [returningState, setReturningState] = useState<ReturningState | null>(
277
+ null,
278
+ );
279
+ const onIntentRef = useRef(onCardIntent);
280
+ onIntentRef.current = onCardIntent;
281
+
282
+ const settleTransition = motionTransition ?? SETTLE_TRANSITION;
283
+ const returnTransition = motionTransition ?? RETURN_TRANSITION;
284
+
285
+ const setActive = useCallback((next: ActiveDragState | null) => {
286
+ activeDragRef.current = next;
287
+ setActiveDrag(next);
288
+ }, []);
289
+
290
+ const setInspecting = useCallback((next: string | null) => {
291
+ inspectingRef.current = next;
292
+ setInspectingCardId(next);
293
+ }, []);
294
+
295
+ const finalizeInteraction = useCallback((sourceFocus: HTMLElement | null) => {
296
+ if (sourceFocus) {
297
+ // Defer focus restoration so animation can complete first frame.
298
+ queueMicrotask(() => {
299
+ try {
300
+ sourceFocus.focus({ preventScroll: true });
301
+ } catch {
302
+ // Source may have been unmounted; ignore.
303
+ }
304
+ });
305
+ }
306
+ }, []);
307
+
308
+ const registerTarget = useCallback(
309
+ (target: RegisteredDropTarget) => {
310
+ targetsRef.current.set(target.targetId, {
311
+ ...target,
312
+ order: target.order || ++orderCounterRef.current,
313
+ });
314
+ return () => {
315
+ targetsRef.current.delete(target.targetId);
316
+ const active = activeDragRef.current;
317
+ if (active && active.keyboardFocusedTargetId === target.targetId) {
318
+ setActive({ ...active, keyboardFocusedTargetId: null });
319
+ }
320
+ };
321
+ },
322
+ [setActive],
323
+ );
324
+
325
+ const updateTarget = useCallback(
326
+ (
327
+ targetId: string,
328
+ patch: Partial<Omit<RegisteredDropTarget, "targetId" | "element">>,
329
+ ) => {
330
+ const existing = targetsRef.current.get(targetId);
331
+ if (!existing) return;
332
+ targetsRef.current.set(targetId, { ...existing, ...patch });
333
+ },
334
+ [],
335
+ );
336
+
337
+ const isTargetUsable = useCallback((target: RegisteredDropTarget) => {
338
+ return !target.disabled && target.eligible !== false;
339
+ }, []);
340
+
341
+ const resolveDropTarget = useCallback(
342
+ (point: { x: number; y: number }): string | null => {
343
+ for (const target of targetsRef.current.values()) {
344
+ if (!isTargetUsable(target)) continue;
345
+ const rect = target.element.getBoundingClientRect();
346
+ if (
347
+ point.x >= rect.left + hitTestInsetPx &&
348
+ point.x <= rect.right - hitTestInsetPx &&
349
+ point.y >= rect.top + hitTestInsetPx &&
350
+ point.y <= rect.bottom - hitTestInsetPx
351
+ ) {
352
+ return target.targetId;
353
+ }
354
+ }
355
+ return null;
356
+ },
357
+ [hitTestInsetPx, isTargetUsable],
358
+ );
359
+
360
+ const sortedUsableTargetIds = useCallback((): string[] => {
361
+ const entries = Array.from(targetsRef.current.values()).filter((t) =>
362
+ isTargetUsable(t),
363
+ );
364
+ entries.sort((a, b) => a.order - b.order);
365
+ return entries.map((t) => t.targetId);
366
+ }, [isTargetUsable]);
367
+
368
+ const recordTap = useCallback(
369
+ (input: TapInput) => {
370
+ if (input.cardDisabled || !input.cardEligible) return;
371
+ // Tap is non-committing in drag-to-target mode regardless of whether
372
+ // any usable target is currently registered. A missing target is a
373
+ // composition/availability problem, not a reason to silently fall
374
+ // back to tap-to-play. Hold the card in `inspecting` so the user can
375
+ // see what they tapped while the runtime decides what to do next.
376
+ setInspecting(input.cardId);
377
+ },
378
+ [setInspecting],
379
+ );
380
+
381
+ const recordPreviewStart = useCallback((cardId: string) => {
382
+ onIntentRef.current?.({ type: "previewStart", cardId });
383
+ }, []);
384
+
385
+ const recordPreviewEnd = useCallback((cardId: string) => {
386
+ onIntentRef.current?.({ type: "previewEnd", cardId });
387
+ }, []);
388
+
389
+ const recordActivate = useCallback(
390
+ (cardId: string, source: "tap" | "keyboard") => {
391
+ onIntentRef.current?.({ type: "activate", cardId, source });
392
+ },
393
+ [],
394
+ );
395
+
396
+ const clearInspect = useCallback(() => {
397
+ if (inspectingRef.current !== null) setInspecting(null);
398
+ }, [setInspecting]);
399
+
400
+ const startPointerDrag = useCallback(
401
+ (input: PointerDragInput): boolean => {
402
+ if (activeDragRef.current) return false;
403
+ if (inspectingRef.current) setInspecting(null);
404
+ const overTargetId = resolveDropTarget({
405
+ x: input.pointerX,
406
+ y: input.pointerY,
407
+ });
408
+ const next: ActiveDragState = {
409
+ cardId: input.cardId,
410
+ cardLabel: input.cardLabel ?? null,
411
+ source: "pointer",
412
+ pointerId: input.pointerId,
413
+ pointerX: input.pointerX,
414
+ pointerY: input.pointerY,
415
+ grabOffsetX: input.grabOffsetX,
416
+ grabOffsetY: input.grabOffsetY,
417
+ sourceRect: input.sourceRect,
418
+ content: input.content,
419
+ overTargetId,
420
+ keyboardFocusedTargetId: null,
421
+ sourceFocus: input.sourceFocus ?? null,
422
+ };
423
+ setActive(next);
424
+ return true;
425
+ },
426
+ [resolveDropTarget, setActive, setInspecting],
427
+ );
428
+
429
+ const updatePointer = useCallback(
430
+ (point: { x: number; y: number }) => {
431
+ const active = activeDragRef.current;
432
+ if (!active || active.source !== "pointer") return;
433
+ const overTargetId = resolveDropTarget(point);
434
+ setActive({
435
+ ...active,
436
+ pointerX: point.x,
437
+ pointerY: point.y,
438
+ overTargetId,
439
+ });
440
+ },
441
+ [resolveDropTarget, setActive],
442
+ );
443
+
444
+ const completeWithDrop = useCallback(
445
+ (
446
+ active: ActiveDragState,
447
+ targetId: string,
448
+ releaseX: number,
449
+ releaseY: number,
450
+ ) => {
451
+ const target = targetsRef.current.get(targetId);
452
+ const targetRect = target?.element.getBoundingClientRect();
453
+ const settling: SettlingState = {
454
+ cardId: active.cardId,
455
+ source: active.source,
456
+ pointerX: releaseX,
457
+ pointerY: releaseY,
458
+ grabOffsetX: active.grabOffsetX,
459
+ grabOffsetY: active.grabOffsetY,
460
+ sourceRect: active.sourceRect,
461
+ targetRect: targetRect
462
+ ? {
463
+ left: targetRect.left,
464
+ top: targetRect.top,
465
+ width: targetRect.width,
466
+ height: targetRect.height,
467
+ }
468
+ : active.sourceRect,
469
+ content: active.content,
470
+ };
471
+ onIntentRef.current?.({
472
+ type: "drop",
473
+ cardId: active.cardId,
474
+ targetId,
475
+ source: active.source === "keyboard" ? "keyboard" : "pointer",
476
+ });
477
+ setSettlingState(settling);
478
+ setReturningState(null);
479
+ setActive(null);
480
+ finalizeInteraction(active.sourceFocus);
481
+ },
482
+ [finalizeInteraction, setActive],
483
+ );
484
+
485
+ const completeWithReturn = useCallback(
486
+ (active: ActiveDragState, releaseX: number, releaseY: number) => {
487
+ const returning: ReturningState = {
488
+ cardId: active.cardId,
489
+ source: active.source,
490
+ pointerX: releaseX,
491
+ pointerY: releaseY,
492
+ grabOffsetX: active.grabOffsetX,
493
+ grabOffsetY: active.grabOffsetY,
494
+ sourceRect: active.sourceRect,
495
+ content: active.content,
496
+ };
497
+ setReturningState(returning);
498
+ setSettlingState(null);
499
+ setActive(null);
500
+ finalizeInteraction(active.sourceFocus);
501
+ },
502
+ [finalizeInteraction, setActive],
503
+ );
504
+
505
+ const releasePointer = useCallback(
506
+ (point: { x: number; y: number }) => {
507
+ const active = activeDragRef.current;
508
+ if (!active || active.source !== "pointer") return;
509
+ const overTargetId = resolveDropTarget(point);
510
+ if (overTargetId) {
511
+ completeWithDrop(active, overTargetId, point.x, point.y);
512
+ return;
513
+ }
514
+ completeWithReturn(active, point.x, point.y);
515
+ },
516
+ [completeWithDrop, completeWithReturn, resolveDropTarget],
517
+ );
518
+
519
+ const cancelDrag = useCallback(() => {
520
+ const active = activeDragRef.current;
521
+ if (!active) return;
522
+ completeWithReturn(active, active.pointerX, active.pointerY);
523
+ }, [completeWithReturn]);
524
+
525
+ const startKeyboardDrag = useCallback(
526
+ (input: KeyboardDragInput): boolean => {
527
+ if (activeDragRef.current) return false;
528
+ if (!input.cardEligible) return false;
529
+ if (inspectingRef.current) setInspecting(null);
530
+ const ids = sortedUsableTargetIds();
531
+ if (ids.length === 0) return false;
532
+ const firstFocus = ids[0]!;
533
+ const firstTarget = targetsRef.current.get(firstFocus);
534
+ const firstRect = firstTarget?.element.getBoundingClientRect();
535
+ const next: ActiveDragState = {
536
+ cardId: input.cardId,
537
+ cardLabel: input.cardLabel ?? null,
538
+ source: "keyboard",
539
+ pointerId: null,
540
+ pointerX: firstRect ? firstRect.left + firstRect.width / 2 : 0,
541
+ pointerY: firstRect ? firstRect.top + firstRect.height / 2 : 0,
542
+ grabOffsetX: 0,
543
+ grabOffsetY: 0,
544
+ sourceRect: input.sourceRect,
545
+ content: input.content,
546
+ overTargetId: firstFocus,
547
+ keyboardFocusedTargetId: firstFocus,
548
+ sourceFocus: input.sourceFocus ?? null,
549
+ };
550
+ setActive(next);
551
+ // Focus is moved by `CardDropTargetView` in an effect that watches
552
+ // `keyboardFocusedTargetId`, ensuring focus transfer happens after
553
+ // React commits the render that promotes this target.
554
+ return true;
555
+ },
556
+ [setActive, setInspecting, sortedUsableTargetIds],
557
+ );
558
+
559
+ const moveKeyboardFocus = useCallback(
560
+ (direction: "next" | "prev") => {
561
+ const active = activeDragRef.current;
562
+ if (!active || active.source !== "keyboard") return;
563
+ const ids = sortedUsableTargetIds();
564
+ if (ids.length === 0) return;
565
+ const currentIdx = active.keyboardFocusedTargetId
566
+ ? ids.indexOf(active.keyboardFocusedTargetId)
567
+ : -1;
568
+ const nextIdx =
569
+ direction === "next"
570
+ ? (currentIdx + 1) % ids.length
571
+ : (currentIdx - 1 + ids.length) % ids.length;
572
+ const nextId = ids[nextIdx]!;
573
+ const target = targetsRef.current.get(nextId);
574
+ const rect = target?.element.getBoundingClientRect();
575
+ setActive({
576
+ ...active,
577
+ keyboardFocusedTargetId: nextId,
578
+ overTargetId: nextId,
579
+ pointerX: rect ? rect.left + rect.width / 2 : active.pointerX,
580
+ pointerY: rect ? rect.top + rect.height / 2 : active.pointerY,
581
+ });
582
+ // Focus moved by the target's keyboardFocused effect.
583
+ },
584
+ [setActive, sortedUsableTargetIds],
585
+ );
586
+
587
+ const commitKeyboardDrop = useCallback(() => {
588
+ const active = activeDragRef.current;
589
+ if (!active || active.source !== "keyboard") return;
590
+ const targetId = active.keyboardFocusedTargetId;
591
+ if (!targetId) return;
592
+ const target = targetsRef.current.get(targetId);
593
+ if (!target || !isTargetUsable(target)) return;
594
+ const rect = target.element.getBoundingClientRect();
595
+ completeWithDrop(
596
+ active,
597
+ targetId,
598
+ rect.left + rect.width / 2,
599
+ rect.top + rect.height / 2,
600
+ );
601
+ }, [completeWithDrop, isTargetUsable]);
602
+
603
+ const phase: DragPhase = useMemo(() => {
604
+ if (activeDrag) return "dragging";
605
+ if (settlingState) return "settling";
606
+ if (returningState) return "returning";
607
+ if (inspectingCardId) return "inspecting";
608
+ return "idle";
609
+ }, [activeDrag, inspectingCardId, returningState, settlingState]);
610
+
611
+ const eligibleTargetCount = useMemo(
612
+ () => sortedUsableTargetIds().length,
613
+ // Recompute whenever any target patch changes — `activeDrag` is a cheap
614
+ // proxy: target updates also bump the surface re-render via parent state.
615
+ // eslint-disable-next-line react-hooks/exhaustive-deps
616
+ [activeDrag, sortedUsableTargetIds],
617
+ );
618
+
619
+ const controller: CardDragSurfaceController = useMemo(
620
+ () => ({
621
+ activeCardId:
622
+ activeDrag?.cardId ??
623
+ settlingState?.cardId ??
624
+ returningState?.cardId ??
625
+ (inspectingCardId ? inspectingCardId : null),
626
+ activeSource: activeDrag?.source ?? null,
627
+ phase,
628
+ overTargetId: activeDrag?.overTargetId ?? null,
629
+ keyboardFocusedTargetId: activeDrag?.keyboardFocusedTargetId ?? null,
630
+ startPointerDrag,
631
+ updatePointer,
632
+ releasePointer,
633
+ cancelDrag,
634
+ startKeyboardDrag,
635
+ moveKeyboardFocus,
636
+ commitKeyboardDrop,
637
+ recordTap,
638
+ recordPreviewStart,
639
+ recordPreviewEnd,
640
+ recordActivate,
641
+ clearInspect,
642
+ eligibleTargetCount,
643
+ }),
644
+ [
645
+ activeDrag,
646
+ cancelDrag,
647
+ clearInspect,
648
+ commitKeyboardDrop,
649
+ eligibleTargetCount,
650
+ inspectingCardId,
651
+ moveKeyboardFocus,
652
+ phase,
653
+ recordActivate,
654
+ recordPreviewEnd,
655
+ recordPreviewStart,
656
+ recordTap,
657
+ releasePointer,
658
+ returningState,
659
+ settlingState,
660
+ startKeyboardDrag,
661
+ startPointerDrag,
662
+ updatePointer,
663
+ ],
664
+ );
665
+
666
+ const contextValue: CardDragSurfaceContextValue = useMemo(
667
+ () => ({
668
+ registerTarget,
669
+ updateTarget,
670
+ controller,
671
+ activeCardId: controller.activeCardId,
672
+ overTargetId: activeDrag?.overTargetId ?? null,
673
+ dragActive: activeDrag !== null,
674
+ keyboardFocusedTargetId: activeDrag?.keyboardFocusedTargetId ?? null,
675
+ }),
676
+ [activeDrag, controller, registerTarget, updateTarget],
677
+ );
678
+
679
+ useEffect(() => {
680
+ if (!activeDrag) return;
681
+ function onKeyDown(event: globalThis.KeyboardEvent) {
682
+ if (event.key === "Escape") {
683
+ event.preventDefault();
684
+ cancelDrag();
685
+ }
686
+ }
687
+ window.addEventListener("keydown", onKeyDown);
688
+ return () => window.removeEventListener("keydown", onKeyDown);
689
+ }, [activeDrag, cancelDrag]);
690
+
691
+ const announcement = useMemo(() => {
692
+ if (!activeDrag) return null;
693
+ const overTarget = activeDrag.overTargetId
694
+ ? targetsRef.current.get(activeDrag.overTargetId)
695
+ : null;
696
+ const overLabel = overTarget?.label ?? null;
697
+ const cardLabel = activeDrag.cardLabel ?? "Card";
698
+ if (overLabel) {
699
+ return `${cardLabel} over ${overLabel}. Press Enter to drop or Escape to cancel.`;
700
+ }
701
+ return `${cardLabel} picked up. Move to a target or press Escape to cancel.`;
702
+ }, [activeDrag]);
703
+
704
+ const overlayContent = activeDrag ? (
705
+ <DragOverlay session={activeDrag} reducedMotion={reducedMotion} />
706
+ ) : null;
707
+
708
+ const settleOverlay = settlingState ? (
709
+ <SettleOverlay
710
+ key={`settle-${settlingState.cardId}`}
711
+ session={settlingState}
712
+ transition={reducedMotion ? REDUCED_TRANSITION : settleTransition}
713
+ onDone={() => setSettlingState(null)}
714
+ />
715
+ ) : null;
716
+
717
+ const returnOverlay = returningState ? (
718
+ <ReturnOverlay
719
+ key={`return-${returningState.cardId}`}
720
+ session={returningState}
721
+ transition={reducedMotion ? REDUCED_TRANSITION : returnTransition}
722
+ onDone={() => setReturningState(null)}
723
+ />
724
+ ) : null;
725
+
726
+ const portalRoot = typeof document !== "undefined" ? document.body : null;
727
+
728
+ return (
729
+ <CardDragSurfaceContext.Provider value={contextValue}>
730
+ <div
731
+ data-dreamboard-card-drag-surface=""
732
+ data-drag-active={activeDrag ? "true" : undefined}
733
+ data-drag-source={activeDrag?.source}
734
+ data-drag-phase={phase}
735
+ className={clsx("relative", className)}
736
+ style={style}
737
+ >
738
+ {children}
739
+ </div>
740
+ {portalRoot
741
+ ? createPortal(
742
+ <AnimatePresence initial={false}>
743
+ {overlayContent}
744
+ {settleOverlay}
745
+ {returnOverlay}
746
+ </AnimatePresence>,
747
+ portalRoot,
748
+ )
749
+ : null}
750
+ {!suppressLiveAnnouncement ? (
751
+ <div
752
+ id={liveRegionId}
753
+ role="status"
754
+ aria-live="polite"
755
+ className="sr-only"
756
+ data-dreamboard-card-drag-announcement=""
757
+ style={{
758
+ position: "absolute",
759
+ width: 1,
760
+ height: 1,
761
+ padding: 0,
762
+ margin: -1,
763
+ overflow: "hidden",
764
+ clip: "rect(0,0,0,0)",
765
+ whiteSpace: "nowrap",
766
+ border: 0,
767
+ }}
768
+ >
769
+ {announcement ?? ""}
770
+ </div>
771
+ ) : null}
772
+ </CardDragSurfaceContext.Provider>
773
+ );
774
+ }
775
+
776
+ interface DragOverlayProps {
777
+ session: ActiveDragState;
778
+ reducedMotion: boolean;
779
+ }
780
+
781
+ function DragOverlay({ session, reducedMotion }: DragOverlayProps) {
782
+ if (session.source === "keyboard") {
783
+ return (
784
+ <motion.div
785
+ key="overlay-keyboard"
786
+ data-dreamboard-card-drag-overlay=""
787
+ data-source="keyboard"
788
+ initial={{ opacity: 0 }}
789
+ animate={{ opacity: 1 }}
790
+ exit={{ opacity: 0 }}
791
+ transition={reducedMotion ? REDUCED_TRANSITION : { duration: 0.12 }}
792
+ style={{
793
+ position: "fixed",
794
+ left: session.sourceRect.left,
795
+ top: session.sourceRect.top,
796
+ width: session.sourceRect.width,
797
+ height: session.sourceRect.height,
798
+ pointerEvents: "none",
799
+ zIndex: 1000,
800
+ boxShadow: reducedMotion ? "none" : "0 12px 32px rgba(0,0,0,0.18)",
801
+ borderRadius: 12,
802
+ }}
803
+ >
804
+ {session.content}
805
+ </motion.div>
806
+ );
807
+ }
808
+
809
+ const liftedAnimate = reducedMotion
810
+ ? {
811
+ scale: 1,
812
+ rotate: 0,
813
+ filter: "drop-shadow(0 0 0 rgba(0,0,0,0))",
814
+ }
815
+ : {
816
+ scale: [1, 1.42, 1.35],
817
+ rotate: [0, -3, 3, -1.8, 1.8, 0],
818
+ filter: [
819
+ "drop-shadow(0 4px 8px rgba(0,0,0,0.18))",
820
+ "drop-shadow(0 24px 36px rgba(0,0,0,0.36))",
821
+ ],
822
+ };
823
+ const liftedTransition: Transition = reducedMotion
824
+ ? REDUCED_TRANSITION
825
+ : {
826
+ scale: { type: "spring", stiffness: 420, damping: 22, mass: 0.7 },
827
+ rotate: { duration: 0.55, ease: "easeInOut" },
828
+ filter: { duration: 0.18, ease: "easeOut" },
829
+ };
830
+ return (
831
+ <motion.div
832
+ key="overlay-pointer"
833
+ data-dreamboard-card-drag-overlay=""
834
+ data-source="pointer"
835
+ initial={{
836
+ scale: 1,
837
+ rotate: 0,
838
+ opacity: 1,
839
+ filter: "drop-shadow(0 2px 6px rgba(0,0,0,0.15))",
840
+ }}
841
+ animate={liftedAnimate}
842
+ exit={{ opacity: 0 }}
843
+ transition={liftedTransition}
844
+ style={{
845
+ position: "fixed",
846
+ left: session.pointerX - session.sourceRect.width / 2,
847
+ top: session.pointerY - session.sourceRect.height / 2,
848
+ width: session.sourceRect.width,
849
+ height: session.sourceRect.height,
850
+ zIndex: 1000,
851
+ pointerEvents: "none",
852
+ touchAction: "none",
853
+ transformOrigin: "center center",
854
+ willChange: "transform, filter",
855
+ }}
856
+ >
857
+ {session.content}
858
+ </motion.div>
859
+ );
860
+ }
861
+
862
+ interface SettleOverlayProps {
863
+ session: SettlingState;
864
+ transition: Transition;
865
+ onDone: () => void;
866
+ }
867
+
868
+ function SettleOverlay({ session, transition, onDone }: SettleOverlayProps) {
869
+ const startLeft = session.pointerX - session.sourceRect.width / 2;
870
+ const startTop = session.pointerY - session.sourceRect.height / 2;
871
+ const endLeft =
872
+ session.targetRect.left +
873
+ session.targetRect.width / 2 -
874
+ session.sourceRect.width / 2;
875
+ const endTop =
876
+ session.targetRect.top +
877
+ session.targetRect.height / 2 -
878
+ session.sourceRect.height / 2;
879
+ return (
880
+ <motion.div
881
+ data-dreamboard-card-drag-overlay=""
882
+ data-source={session.source}
883
+ data-drag-phase="settling"
884
+ initial={{ left: startLeft, top: startTop, scale: 1.06, opacity: 1 }}
885
+ animate={{ left: endLeft, top: endTop, scale: 0.92, opacity: 0 }}
886
+ transition={transition}
887
+ onAnimationComplete={onDone}
888
+ style={{
889
+ position: "fixed",
890
+ width: session.sourceRect.width,
891
+ height: session.sourceRect.height,
892
+ zIndex: 1000,
893
+ pointerEvents: "none",
894
+ }}
895
+ >
896
+ {session.content}
897
+ </motion.div>
898
+ );
899
+ }
900
+
901
+ interface ReturnOverlayProps {
902
+ session: ReturningState;
903
+ transition: Transition;
904
+ onDone: () => void;
905
+ }
906
+
907
+ function ReturnOverlay({ session, transition, onDone }: ReturnOverlayProps) {
908
+ const startLeft = session.pointerX - session.sourceRect.width / 2;
909
+ const startTop = session.pointerY - session.sourceRect.height / 2;
910
+ return (
911
+ <motion.div
912
+ data-dreamboard-card-drag-overlay=""
913
+ data-source={session.source}
914
+ data-drag-phase="returning"
915
+ initial={{ left: startLeft, top: startTop, scale: 1.06, opacity: 1 }}
916
+ animate={{
917
+ left: session.sourceRect.left,
918
+ top: session.sourceRect.top,
919
+ scale: 1,
920
+ opacity: 1,
921
+ }}
922
+ exit={{ opacity: 0 }}
923
+ transition={transition}
924
+ onAnimationComplete={onDone}
925
+ style={{
926
+ position: "fixed",
927
+ width: session.sourceRect.width,
928
+ height: session.sourceRect.height,
929
+ zIndex: 1000,
930
+ pointerEvents: "none",
931
+ }}
932
+ >
933
+ {session.content}
934
+ </motion.div>
935
+ );
936
+ }
937
+
938
+ export interface CardDropTargetViewProps {
939
+ targetId: string;
940
+ state?: CardDropTargetVisualState;
941
+ /** Plain-text label used in live announcements ("Selected cards", etc.). */
942
+ label?: string;
943
+ renderTarget: (state: CardDropTargetVisualState) => ReactNode;
944
+ className?: string;
945
+ style?: CSSProperties;
946
+ /** Tab order hint (lower numbers focus first). */
947
+ order?: number;
948
+ /** ARIA role override; defaults to `button`. */
949
+ role?: string;
950
+ }
951
+
952
+ export function CardDropTargetView({
953
+ targetId,
954
+ state,
955
+ label,
956
+ renderTarget,
957
+ className,
958
+ style,
959
+ order,
960
+ role = "button",
961
+ }: CardDropTargetViewProps) {
962
+ const surface = useCardDragSurface();
963
+ const ref = useRef<HTMLDivElement | null>(null);
964
+ const disabled = state?.disabled ?? false;
965
+ const baseEligible = state?.eligible ?? true;
966
+ const baseLabel = label ?? null;
967
+ const orderProp = order ?? 0;
968
+
969
+ const registerTargetRef = useRef(surface?.registerTarget);
970
+ registerTargetRef.current = surface?.registerTarget;
971
+ const updateTargetRef = useRef(surface?.updateTarget);
972
+ updateTargetRef.current = surface?.updateTarget;
973
+
974
+ // Stable register/unregister keyed only on `targetId`. Eligibility, label
975
+ // and disabled flow through `updateTarget` so changing surface context
976
+ // values cannot tear down the registration mid-drag.
977
+ useEffect(() => {
978
+ const element = ref.current;
979
+ const register = registerTargetRef.current;
980
+ if (!register || !element) return;
981
+ const unregister = register({
982
+ targetId,
983
+ disabled,
984
+ eligible: baseEligible,
985
+ element,
986
+ label: baseLabel,
987
+ order: orderProp,
988
+ });
989
+ return unregister;
990
+ // eslint-disable-next-line react-hooks/exhaustive-deps
991
+ }, [targetId]);
992
+
993
+ useEffect(() => {
994
+ updateTargetRef.current?.(targetId, {
995
+ disabled,
996
+ eligible: baseEligible,
997
+ label: baseLabel,
998
+ order: orderProp,
999
+ });
1000
+ }, [targetId, disabled, baseEligible, baseLabel, orderProp]);
1001
+
1002
+ const dragActive = surface?.dragActive ?? false;
1003
+ const overTargetId = surface?.overTargetId ?? null;
1004
+ const keyboardFocused = surface?.keyboardFocusedTargetId === targetId;
1005
+
1006
+ // Move focus into this target whenever the surface promotes it to the
1007
+ // keyboard-focused id. Doing it here, after React commits, is more
1008
+ // reliable than firing focus() from inside the surface's `setActive`
1009
+ // callback (where React batching can race the source card's commit).
1010
+ useEffect(() => {
1011
+ if (!keyboardFocused) return;
1012
+ const el = ref.current;
1013
+ if (!el) return;
1014
+ if (document.activeElement === el) return;
1015
+ try {
1016
+ el.focus({ preventScroll: true });
1017
+ } catch {
1018
+ // Element may have unmounted; ignore.
1019
+ }
1020
+ }, [keyboardFocused, targetId]);
1021
+
1022
+ const computedState: CardDropTargetVisualState = {
1023
+ ...state,
1024
+ eligible: baseEligible,
1025
+ active: dragActive ? true : state?.active,
1026
+ over:
1027
+ overTargetId === targetId && baseEligible ? true : (state?.over ?? false),
1028
+ };
1029
+
1030
+ const handleKeyDown = useCallback(
1031
+ (event: KeyboardEvent<HTMLDivElement>) => {
1032
+ if (!surface) return;
1033
+ const controller = surface.controller;
1034
+ if (controller.activeSource !== "keyboard") return;
1035
+ if (event.key === "Enter" || event.key === " ") {
1036
+ event.preventDefault();
1037
+ controller.commitKeyboardDrop();
1038
+ return;
1039
+ }
1040
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") {
1041
+ event.preventDefault();
1042
+ controller.moveKeyboardFocus("next");
1043
+ return;
1044
+ }
1045
+ if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
1046
+ event.preventDefault();
1047
+ controller.moveKeyboardFocus("prev");
1048
+ return;
1049
+ }
1050
+ if (event.key === "Escape") {
1051
+ event.preventDefault();
1052
+ controller.cancelDrag();
1053
+ }
1054
+ },
1055
+ [surface],
1056
+ );
1057
+
1058
+ return (
1059
+ <div
1060
+ ref={ref}
1061
+ data-dreamboard-card-drop-target=""
1062
+ data-target-id={targetId}
1063
+ data-keyboard-focused={keyboardFocused ? "true" : undefined}
1064
+ role={role}
1065
+ tabIndex={disabled || !baseEligible ? -1 : 0}
1066
+ aria-disabled={disabled || !baseEligible || undefined}
1067
+ aria-label={label}
1068
+ onKeyDown={handleKeyDown}
1069
+ className={className}
1070
+ style={style}
1071
+ {...dropTargetVisualStateDataAttributes(computedState)}
1072
+ >
1073
+ {renderTarget(computedState)}
1074
+ </div>
1075
+ );
1076
+ }