@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.
- package/LICENSE.md +96 -0
- package/README.md +12 -0
- package/dist/HandView-ncJIVLhN.d.ts +193 -0
- package/dist/ResourceCounter-CTREyF73.d.ts +102 -0
- package/dist/ThemeProvider-fy0_QzgO.d.ts +99 -0
- package/dist/bundle-TIZcw8LB.d.ts +281 -0
- package/dist/cards-Sl3b40Mv.d.ts +13 -0
- package/dist/chunk-7YAHLYBR.js +481 -0
- package/dist/chunk-7YAHLYBR.js.map +1 -0
- package/dist/chunk-FDNZTDD6.js +8085 -0
- package/dist/chunk-FDNZTDD6.js.map +1 -0
- package/dist/chunk-GKKBPPSW.js +598 -0
- package/dist/chunk-GKKBPPSW.js.map +1 -0
- package/dist/chunk-I46YJSOD.js +1 -0
- package/dist/chunk-I46YJSOD.js.map +1 -0
- package/dist/chunk-KAELH4KC.js +104 -0
- package/dist/chunk-KAELH4KC.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-T3ZKNUZ7.js +1 -0
- package/dist/chunk-T3ZKNUZ7.js.map +1 -0
- package/dist/chunk-T52J5RMF.js +1 -0
- package/dist/chunk-T52J5RMF.js.map +1 -0
- package/dist/chunk-TDSWKVZ4.js +5401 -0
- package/dist/chunk-TDSWKVZ4.js.map +1 -0
- package/dist/chunk-U5C6BONG.js +34 -0
- package/dist/chunk-U5C6BONG.js.map +1 -0
- package/dist/chunk-VDXOF4FW.js +69 -0
- package/dist/chunk-VDXOF4FW.js.map +1 -0
- package/dist/chunk-VFTAA4WO.js +115 -0
- package/dist/chunk-VFTAA4WO.js.map +1 -0
- package/dist/chunk-WN74KVNY.js +17 -0
- package/dist/chunk-WN74KVNY.js.map +1 -0
- package/dist/chunk-WYPQ3GG5.js +10990 -0
- package/dist/chunk-WYPQ3GG5.js.map +1 -0
- package/dist/components-D5ZRE2Hl.d.ts +1451 -0
- package/dist/generated/runtime/primitives.d.ts +12 -0
- package/dist/generated/runtime/primitives.js +180 -0
- package/dist/generated/runtime/primitives.js.map +1 -0
- package/dist/generated/runtime-api.d.ts +3 -0
- package/dist/generated/runtime-api.js +2 -0
- package/dist/generated/runtime-api.js.map +1 -0
- package/dist/generated/runtime.d.ts +14 -0
- package/dist/generated/runtime.js +18 -0
- package/dist/generated/runtime.js.map +1 -0
- package/dist/generated/workspace-contract.d.ts +14 -0
- package/dist/generated/workspace-contract.js +14 -0
- package/dist/generated/workspace-contract.js.map +1 -0
- package/dist/hex-board-view-D_07hO6O.d.ts +933 -0
- package/dist/hex-color-MhOyuY-o.d.ts +8 -0
- package/dist/index-BwqPQtBu.d.ts +1433 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/infrastructure/reducer-bundle-abi.d.ts +1083 -0
- package/dist/infrastructure/reducer-bundle-abi.js +14 -0
- package/dist/infrastructure/reducer-bundle-abi.js.map +1 -0
- package/dist/infrastructure/workspace-codegen.d.ts +53 -0
- package/dist/infrastructure/workspace-codegen.js +44 -0
- package/dist/infrastructure/workspace-codegen.js.map +1 -0
- package/dist/manifest-contract-BNHVGFtU.d.ts +9 -0
- package/dist/package-set.d.ts +13 -0
- package/dist/package-set.js +12 -0
- package/dist/package-set.js.map +1 -0
- package/dist/primitive-props-DpKs-GCr.d.ts +11 -0
- package/dist/reducer.d.ts +3786 -0
- package/dist/reducer.js +8131 -0
- package/dist/reducer.js.map +1 -0
- package/dist/runtime/primitives.d.ts +226 -0
- package/dist/runtime/primitives.js +180 -0
- package/dist/runtime/primitives.js.map +1 -0
- package/dist/runtime/types/runtime-api.d.ts +1 -0
- package/dist/runtime/types/runtime-api.js +2 -0
- package/dist/runtime/types/runtime-api.js.map +1 -0
- package/dist/runtime/workspace-contract.d.ts +172 -0
- package/dist/runtime/workspace-contract.js +14 -0
- package/dist/runtime/workspace-contract.js.map +1 -0
- package/dist/runtime-api-3dshj6kK.d.ts +101 -0
- package/dist/runtime-api-DWxvTr-O.d.ts +379 -0
- package/dist/runtime.d.ts +58 -0
- package/dist/runtime.js +13 -0
- package/dist/runtime.js.map +1 -0
- package/dist/slots-1GPGihk8.d.ts +8 -0
- package/dist/testing.d.ts +149 -0
- package/dist/testing.js +513 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +496 -0
- package/dist/types.js +28 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/components.d.ts +16 -0
- package/dist/ui/components.js +192 -0
- package/dist/ui/components.js.map +1 -0
- package/dist/ui/defaults.d.ts +19 -0
- package/dist/ui/defaults.js +104 -0
- package/dist/ui/defaults.js.map +1 -0
- package/dist/ui/plugin-styles.css +250 -0
- package/dist/ui/types/player-state.d.ts +365 -0
- package/dist/ui/types/player-state.js +1 -0
- package/dist/ui/types/player-state.js.map +1 -0
- package/dist/ui-contract-iQfTtUSL.d.ts +1161 -0
- package/dist/ui.d.ts +320 -0
- package/dist/ui.js +253 -0
- package/dist/ui.js.map +1 -0
- package/package.json +199 -0
- package/src/generated/reducer-contract/builders.ts +90 -0
- package/src/generated/reducer-contract/version.ts +9 -0
- package/src/generated/reducer-contract/wire.ts +100 -0
- package/src/generated/reducer-contract/zod.ts +101 -0
- package/src/generated/runtime/primitives.ts +2 -0
- package/src/generated/runtime-api.ts +5 -0
- package/src/generated/runtime.ts +35 -0
- package/src/generated/workspace-contract.ts +2 -0
- package/src/index.ts +7 -0
- package/src/infrastructure/reducer-bundle-abi.ts +8 -0
- package/src/infrastructure/reducer-contract/bundle.ts +37 -0
- package/src/infrastructure/workspace-codegen/hex-geometry.ts +69 -0
- package/src/infrastructure/workspace-codegen/index.ts +64 -0
- package/src/infrastructure/workspace-codegen/manifest-contract.ts +6632 -0
- package/src/infrastructure/workspace-codegen/manifest-validation.ts +795 -0
- package/src/infrastructure/workspace-codegen/ownership.ts +131 -0
- package/src/infrastructure/workspace-codegen/preset-card-sets.ts +169 -0
- package/src/infrastructure/workspace-codegen/seeds.ts +1705 -0
- package/src/infrastructure/workspace-codegen.ts +1 -0
- package/src/package-set.ts +19 -0
- package/src/reducer/authoring/contract.ts +157 -0
- package/src/reducer/authoring/effect.ts +224 -0
- package/src/reducer/authoring/game.ts +23 -0
- package/src/reducer/authoring/interaction.ts +98 -0
- package/src/reducer/authoring/phase.ts +300 -0
- package/src/reducer/authoring/types.ts +70 -0
- package/src/reducer/authoring/validation.ts +382 -0
- package/src/reducer/authoring/view-stage.ts +68 -0
- package/src/reducer/authoring.ts +29 -0
- package/src/reducer/bundle/ingress-bundle.ts +491 -0
- package/src/reducer/bundle/trusted/engine-instruction-resolver.ts +254 -0
- package/src/reducer/bundle/trusted/flow-instruction-resolver.ts +73 -0
- package/src/reducer/bundle/trusted/instruction-runner.ts +414 -0
- package/src/reducer/bundle/trusted/interaction-authorization.ts +137 -0
- package/src/reducer/bundle/trusted/interaction-collectors.ts +859 -0
- package/src/reducer/bundle/trusted/interaction-decision.ts +747 -0
- package/src/reducer/bundle/trusted/interaction-resolver.ts +95 -0
- package/src/reducer/bundle/trusted/interaction-types.ts +171 -0
- package/src/reducer/bundle/trusted/lifecycle-runner.ts +427 -0
- package/src/reducer/bundle/trusted/projection-builder.ts +356 -0
- package/src/reducer/bundle/trusted/projection-context.ts +39 -0
- package/src/reducer/bundle/trusted/rng-sampler.ts +150 -0
- package/src/reducer/bundle/trusted/runtime-registry.ts +120 -0
- package/src/reducer/bundle/trusted/runtime-scope.ts +336 -0
- package/src/reducer/bundle/trusted/simultaneous-player.ts +97 -0
- package/src/reducer/bundle/trusted/stage-resolver.ts +87 -0
- package/src/reducer/bundle/trusted/static-projection.ts +116 -0
- package/src/reducer/bundle/trusted/trusted-runtime-args.ts +97 -0
- package/src/reducer/bundle/trusted/trusted-runtime-result.ts +39 -0
- package/src/reducer/bundle/trusted/trusted-setup-profiles.ts +43 -0
- package/src/reducer/bundle/trusted/trusted-state-codec.ts +48 -0
- package/src/reducer/bundle/trusted-bundle.ts +97 -0
- package/src/reducer/bundle/types.ts +171 -0
- package/src/reducer/bundle.ts +2 -0
- package/src/reducer/client-param-schemas.ts +57 -0
- package/src/reducer/compose.ts +34 -0
- package/src/reducer/core/runtime-input.ts +30 -0
- package/src/reducer/core/runtime-instruction.ts +59 -0
- package/src/reducer/core/types.ts +62 -0
- package/src/reducer/definition-index.ts +277 -0
- package/src/reducer/derived.ts +106 -0
- package/src/reducer/effects.ts +92 -0
- package/src/reducer/engine/runtime-instruction-engine.ts +155 -0
- package/src/reducer/ingress/decode-runtime-input.ts +7 -0
- package/src/reducer/ingress/decode-session-state.ts +9 -0
- package/src/reducer/ingress/encode-session-state.ts +6 -0
- package/src/reducer/ingress/input-codec.ts +18 -0
- package/src/reducer/ingress/phase-schemas.ts +62 -0
- package/src/reducer/ingress/raw-types.ts +107 -0
- package/src/reducer/ingress/runtime-codec.ts +14 -0
- package/src/reducer/ingress/runtime-payload.ts +13 -0
- package/src/reducer/ingress/session-codec.ts +392 -0
- package/src/reducer/ingress/types.ts +6 -0
- package/src/reducer/inputs/boardInput.ts +217 -0
- package/src/reducer/inputs/boardTarget.ts +190 -0
- package/src/reducer/inputs/cardInput.ts +86 -0
- package/src/reducer/inputs/cardTarget.ts +101 -0
- package/src/reducer/inputs/choiceTarget.ts +104 -0
- package/src/reducer/inputs/defineInputs.ts +71 -0
- package/src/reducer/inputs/formInput.ts +809 -0
- package/src/reducer/inputs/many.ts +120 -0
- package/src/reducer/inputs/promptInput.ts +87 -0
- package/src/reducer/inputs/rngInput.ts +58 -0
- package/src/reducer/inputs/targetRule.ts +123 -0
- package/src/reducer/inputs.ts +41 -0
- package/src/reducer/model/definition.ts +1072 -0
- package/src/reducer/model/extract.ts +745 -0
- package/src/reducer/model/manifest.ts +570 -0
- package/src/reducer/model/queries.ts +641 -0
- package/src/reducer/model/runtime.ts +264 -0
- package/src/reducer/model/spec.ts +1386 -0
- package/src/reducer/model/table.ts +260 -0
- package/src/reducer/model.ts +7 -0
- package/src/reducer/ops.ts +1034 -0
- package/src/reducer/parse-utils.ts +28 -0
- package/src/reducer/per-player.ts +422 -0
- package/src/reducer/rng.ts +69 -0
- package/src/reducer/schema-helpers.ts +185 -0
- package/src/reducer/setup-bootstrap-helpers.ts +171 -0
- package/src/reducer/setup-bootstrap.ts +481 -0
- package/src/reducer/table-ops.ts +2671 -0
- package/src/reducer/table-queries.ts +372 -0
- package/src/reducer/transaction.ts +120 -0
- package/src/reducer.ts +314 -0
- package/src/runtime/primitives.ts +1 -0
- package/src/runtime/types/runtime-api.ts +1 -0
- package/src/runtime/workspace-contract.ts +32 -0
- package/src/runtime-internal/components/InteractionForm.tsx +1309 -0
- package/src/runtime-internal/components/PluginRuntime.tsx +103 -0
- package/src/runtime-internal/components/board/target-layer.ts +70 -0
- package/src/runtime-internal/context/ClientParamSchemaContext.tsx +44 -0
- package/src/runtime-internal/context/InteractionDraftContext.tsx +279 -0
- package/src/runtime-internal/context/PluginSessionContext.tsx +47 -0
- package/src/runtime-internal/context/PluginStateContext.tsx +262 -0
- package/src/runtime-internal/context/RuntimeContext.tsx +96 -0
- package/src/runtime-internal/defaults/components.tsx +409 -0
- package/src/runtime-internal/defaults/index.ts +11 -0
- package/src/runtime-internal/errors/ValidationError.ts +29 -0
- package/src/runtime-internal/hooks/useActivePlayers.ts +33 -0
- package/src/runtime-internal/hooks/useBoardInteractions.ts +665 -0
- package/src/runtime-internal/hooks/useGameSelector.ts +105 -0
- package/src/runtime-internal/hooks/useGameView.ts +9 -0
- package/src/runtime-internal/hooks/useInteractionByKey.ts +354 -0
- package/src/runtime-internal/hooks/useInteractionHandle.ts +438 -0
- package/src/runtime-internal/hooks/useIsMyTurn.ts +20 -0
- package/src/runtime-internal/hooks/useLobby.ts +76 -0
- package/src/runtime-internal/hooks/useMe.ts +48 -0
- package/src/runtime-internal/hooks/usePlayerInfo.ts +28 -0
- package/src/runtime-internal/hooks/usePlayerTurnOrder.ts +23 -0
- package/src/runtime-internal/hooks/usePluginRuntime.ts +147 -0
- package/src/runtime-internal/hooks/useSeatInbox.ts +61 -0
- package/src/runtime-internal/hooks/useSimultaneousPhase.ts +10 -0
- package/src/runtime-internal/index.ts +42 -0
- package/src/runtime-internal/internal.ts +43 -0
- package/src/runtime-internal/plugin-styles.css +250 -0
- package/src/runtime-internal/primitives/board.tsx +459 -0
- package/src/runtime-internal/primitives/dialog-lifecycle.ts +58 -0
- package/src/runtime-internal/primitives/dice.tsx +79 -0
- package/src/runtime-internal/primitives/game-ui-provider.tsx +35 -0
- package/src/runtime-internal/primitives/game.tsx +387 -0
- package/src/runtime-internal/primitives/hand-intent-adapter.ts +147 -0
- package/src/runtime-internal/primitives/hand-surface.tsx +594 -0
- package/src/runtime-internal/primitives/index.ts +196 -0
- package/src/runtime-internal/primitives/interaction-form-binding.tsx +56 -0
- package/src/runtime-internal/primitives/interaction-submit.ts +90 -0
- package/src/runtime-internal/primitives/interaction.tsx +987 -0
- package/src/runtime-internal/primitives/phase.tsx +43 -0
- package/src/runtime-internal/primitives/player-roster.tsx +302 -0
- package/src/runtime-internal/primitives/primitive-props.tsx +101 -0
- package/src/runtime-internal/primitives/prompt.tsx +255 -0
- package/src/runtime-internal/primitives/ui.tsx +60 -0
- package/src/runtime-internal/primitives/zone.tsx +791 -0
- package/src/runtime-internal/reducer.ts +30 -0
- package/src/runtime-internal/runtime/createPluginRuntimeAPI.ts +605 -0
- package/src/runtime-internal/types/plugin-state.ts +508 -0
- package/src/runtime-internal/types/reducer-state.ts +24 -0
- package/src/runtime-internal/types/runtime-api.ts +114 -0
- package/src/runtime-internal/ui-contract.ts +519 -0
- package/src/runtime-internal/utils/card-intent-adapter.ts +546 -0
- package/src/runtime-internal/utils/interaction-inputs.ts +492 -0
- package/src/runtime-internal/utils/interaction-labels.ts +23 -0
- package/src/runtime-internal/utils/interaction-router.ts +273 -0
- package/src/runtime-internal/utils/interaction-status.ts +74 -0
- package/src/runtime-internal/workspace-contract.ts +1170 -0
- package/src/runtime.ts +34 -0
- package/src/testing/create-expect-api.ts +352 -0
- package/src/testing/create-test-runtime.ts +381 -0
- package/src/testing/definitions.ts +127 -0
- package/src/testing/index.ts +3 -0
- package/src/testing.ts +1 -0
- package/src/type-stubs/manifest-contract.d.ts +42 -0
- package/src/type-stubs/manifest-contract.js +72 -0
- package/src/type-stubs/ui-contract.d.ts +5 -0
- package/src/type-stubs/ui-contract.js +1 -0
- package/src/types/authoring-card-properties.type-test.ts +266 -0
- package/src/types/authoring.ts +1282 -0
- package/src/types/cards.ts +19 -0
- package/src/types/contracts.ts +1550 -0
- package/src/types/generated-helpers.ts +35 -0
- package/src/types/index.ts +147 -0
- package/src/types/slots.ts +11 -0
- package/src/types.ts +1 -0
- package/src/ui/components/ActionButton.tsx +97 -0
- package/src/ui/components/ActionPanel.tsx +315 -0
- package/src/ui/components/Card.tsx +378 -0
- package/src/ui/components/CardDragSurface.tsx +1076 -0
- package/src/ui/components/ChromeSuppressionContext.tsx +70 -0
- package/src/ui/components/CostDisplay.tsx +145 -0
- package/src/ui/components/DiceRoller.tsx +581 -0
- package/src/ui/components/Drawer.tsx +180 -0
- package/src/ui/components/ErrorBoundary.tsx +275 -0
- package/src/ui/components/GameEndDisplay.tsx +398 -0
- package/src/ui/components/GameSkeleton.tsx +260 -0
- package/src/ui/components/Hand.tsx +468 -0
- package/src/ui/components/HandDock.tsx +299 -0
- package/src/ui/components/HandView.tsx +441 -0
- package/src/ui/components/MobileHandTray.tsx +381 -0
- package/src/ui/components/MoreActions.tsx +143 -0
- package/src/ui/components/PhaseIndicator.tsx +341 -0
- package/src/ui/components/PlayArea.tsx +146 -0
- package/src/ui/components/PrimaryActionButton.tsx +336 -0
- package/src/ui/components/PrimaryButton.tsx +45 -0
- package/src/ui/components/ResourceCounter.tsx +270 -0
- package/src/ui/components/StagingZone.tsx +134 -0
- package/src/ui/components/ThemedButton.tsx +113 -0
- package/src/ui/components/Toast.tsx +264 -0
- package/src/ui/components/board/HexGrid.tsx +1294 -0
- package/src/ui/components/board/NetworkGraph.tsx +476 -0
- package/src/ui/components/board/SlotSystem.tsx +388 -0
- package/src/ui/components/board/SquareGrid.tsx +1165 -0
- package/src/ui/components/board/TrackBoard.tsx +496 -0
- package/src/ui/components/board/ZoneMap.tsx +448 -0
- package/src/ui/components/board/hex-board-view.ts +123 -0
- package/src/ui/components/board/index.ts +142 -0
- package/src/ui/components/board/interaction-accessibility.ts +21 -0
- package/src/ui/components/board/target-layer.ts +66 -0
- package/src/ui/components/card-render-content.type-test.ts +27 -0
- package/src/ui/components/hand-layout-math.ts +163 -0
- package/src/ui/components/hand-pointer-engine.ts +413 -0
- package/src/ui/components/index.ts +245 -0
- package/src/ui/components.ts +1 -0
- package/src/ui/defaults/components.tsx +106 -0
- package/src/ui/defaults/index.ts +8 -0
- package/src/ui/defaults.ts +1 -0
- package/src/ui/errors/ValidationError.ts +29 -0
- package/src/ui/helpers/cards.ts +19 -0
- package/src/ui/helpers/track-board.ts +211 -0
- package/src/ui/hooks/useBoardTopology.ts +316 -0
- package/src/ui/hooks/useCards.ts +10 -0
- package/src/ui/hooks/useHandCardPointer.ts +381 -0
- package/src/ui/hooks/useHandLayout.ts +378 -0
- package/src/ui/hooks/useHandPresentation.ts +121 -0
- package/src/ui/hooks/useHexBoard.ts +74 -0
- package/src/ui/hooks/useHexGrid.ts +185 -0
- package/src/ui/hooks/useIsMobile.ts +35 -0
- package/src/ui/hooks/usePanZoom.ts +278 -0
- package/src/ui/hooks/useSquareBoard.ts +124 -0
- package/src/ui/hooks/useSquareGrid.ts +328 -0
- package/src/ui/index.ts +98 -0
- package/src/ui/internal/ui/alert.tsx +51 -0
- package/src/ui/internal/ui/button.tsx +58 -0
- package/src/ui/internal/ui/dialog.tsx +134 -0
- package/src/ui/internal/ui/input.tsx +21 -0
- package/src/ui/internal/ui/label.tsx +21 -0
- package/src/ui/internal/ui/select.tsx +129 -0
- package/src/ui/internal/ui/tooltip.tsx +54 -0
- package/src/ui/internal/ui/utils.ts +5 -0
- package/src/ui/plugin-styles.css +250 -0
- package/src/ui/primitives/dialog-lifecycle.ts +58 -0
- package/src/ui/primitives/dice.tsx +79 -0
- package/src/ui/primitives/primitive-props.tsx +101 -0
- package/src/ui/theme/ThemeProvider.tsx +252 -0
- package/src/ui/theme/board.ts +61 -0
- package/src/ui/theme/css-vars.ts +105 -0
- package/src/ui/theme/derive.ts +240 -0
- package/src/ui/theme/index.ts +61 -0
- package/src/ui/theme/presets/arcade.ts +261 -0
- package/src/ui/theme/presets/studio.ts +261 -0
- package/src/ui/theme/presets/tabletop.ts +266 -0
- package/src/ui/theme/tokens.ts +392 -0
- package/src/ui/types/hex-color.ts +20 -0
- package/src/ui/types/player-state.ts +463 -0
- package/src/ui/types/tiled-board.ts +785 -0
- package/src/ui/types/visual-state.ts +137 -0
- package/src/ui.ts +1 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
AnyHexBoardInput,
|
|
4
|
+
AnySquareBoardInput,
|
|
5
|
+
BoardEdgeIdOf,
|
|
6
|
+
BoardSpaceIdOf,
|
|
7
|
+
BoardVertexIdOf,
|
|
8
|
+
NormalizedHexBoard,
|
|
9
|
+
NormalizedHexEdgeOf,
|
|
10
|
+
NormalizedHexTileOf,
|
|
11
|
+
NormalizedHexVertexOf,
|
|
12
|
+
NormalizedSquareBoard,
|
|
13
|
+
NormalizedSquareCellOf,
|
|
14
|
+
NormalizedSquareEdgeOf,
|
|
15
|
+
NormalizedSquareVertexOf,
|
|
16
|
+
} from "../types/tiled-board.js";
|
|
17
|
+
import {
|
|
18
|
+
isGeneratedHexBoardInput,
|
|
19
|
+
normalizeHexBoardInput,
|
|
20
|
+
normalizeSquareBoardInput,
|
|
21
|
+
} from "../types/tiled-board.js";
|
|
22
|
+
|
|
23
|
+
type BoardTopologyData = AnyHexBoardInput | AnySquareBoardInput;
|
|
24
|
+
|
|
25
|
+
type BoardSpaceOf<TBoard extends BoardTopologyData> =
|
|
26
|
+
TBoard extends AnyHexBoardInput
|
|
27
|
+
? NormalizedHexTileOf<TBoard>
|
|
28
|
+
: TBoard extends AnySquareBoardInput
|
|
29
|
+
? NormalizedSquareCellOf<TBoard>
|
|
30
|
+
: never;
|
|
31
|
+
|
|
32
|
+
type BoardEdgeOf<TBoard extends BoardTopologyData> =
|
|
33
|
+
TBoard extends AnyHexBoardInput
|
|
34
|
+
? NormalizedHexEdgeOf<TBoard>
|
|
35
|
+
: TBoard extends AnySquareBoardInput
|
|
36
|
+
? NormalizedSquareEdgeOf<TBoard>
|
|
37
|
+
: never;
|
|
38
|
+
|
|
39
|
+
type BoardVertexOf<TBoard extends BoardTopologyData> =
|
|
40
|
+
TBoard extends AnyHexBoardInput
|
|
41
|
+
? NormalizedHexVertexOf<TBoard>
|
|
42
|
+
: TBoard extends AnySquareBoardInput
|
|
43
|
+
? NormalizedSquareVertexOf<TBoard>
|
|
44
|
+
: never;
|
|
45
|
+
|
|
46
|
+
type NormalizedBoardOf<TBoard extends BoardTopologyData> =
|
|
47
|
+
TBoard extends AnyHexBoardInput
|
|
48
|
+
? NormalizedHexBoard<TBoard>
|
|
49
|
+
: TBoard extends AnySquareBoardInput
|
|
50
|
+
? NormalizedSquareBoard<TBoard>
|
|
51
|
+
: never;
|
|
52
|
+
|
|
53
|
+
type BoardLayoutOf<TBoard extends BoardTopologyData> =
|
|
54
|
+
TBoard extends AnyHexBoardInput ? "hex" : "square";
|
|
55
|
+
|
|
56
|
+
function toSpaceIds<TBoard extends BoardTopologyData>(
|
|
57
|
+
edge: BoardEdgeOf<TBoard>,
|
|
58
|
+
): ReadonlyArray<BoardSpaceIdOf<TBoard>> {
|
|
59
|
+
if ("spaceIds" in edge) {
|
|
60
|
+
return edge.spaceIds as ReadonlyArray<BoardSpaceIdOf<TBoard>>;
|
|
61
|
+
}
|
|
62
|
+
return [edge.hex1, edge.hex2] as unknown as ReadonlyArray<
|
|
63
|
+
BoardSpaceIdOf<TBoard>
|
|
64
|
+
>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toVertexSpaceIds<TBoard extends BoardTopologyData>(
|
|
68
|
+
vertex: BoardVertexOf<TBoard>,
|
|
69
|
+
): ReadonlyArray<BoardSpaceIdOf<TBoard>> {
|
|
70
|
+
if ("spaceIds" in vertex) {
|
|
71
|
+
return vertex.spaceIds as ReadonlyArray<BoardSpaceIdOf<TBoard>>;
|
|
72
|
+
}
|
|
73
|
+
return vertex.hexes as ReadonlyArray<BoardSpaceIdOf<TBoard>>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface UseBoardTopologyReturn<TBoard extends BoardTopologyData> {
|
|
77
|
+
layout: BoardLayoutOf<TBoard>;
|
|
78
|
+
board: NormalizedBoardOf<TBoard>;
|
|
79
|
+
getSpace: (
|
|
80
|
+
spaceId: BoardSpaceIdOf<TBoard>,
|
|
81
|
+
) => BoardSpaceOf<TBoard> | undefined;
|
|
82
|
+
getEdge: (edgeId: BoardEdgeIdOf<TBoard>) => BoardEdgeOf<TBoard> | undefined;
|
|
83
|
+
getVertex: (
|
|
84
|
+
vertexId: BoardVertexIdOf<TBoard>,
|
|
85
|
+
) => BoardVertexOf<TBoard> | undefined;
|
|
86
|
+
getAdjacentSpaces: (
|
|
87
|
+
spaceId: BoardSpaceIdOf<TBoard>,
|
|
88
|
+
) => Array<BoardSpaceOf<TBoard>>;
|
|
89
|
+
getDistance: (
|
|
90
|
+
fromSpaceId: BoardSpaceIdOf<TBoard>,
|
|
91
|
+
toSpaceId: BoardSpaceIdOf<TBoard>,
|
|
92
|
+
) => number;
|
|
93
|
+
getSpaceEdges: (
|
|
94
|
+
spaceId: BoardSpaceIdOf<TBoard>,
|
|
95
|
+
) => Array<BoardEdgeOf<TBoard>>;
|
|
96
|
+
getSpaceVertices: (
|
|
97
|
+
spaceId: BoardSpaceIdOf<TBoard>,
|
|
98
|
+
) => Array<BoardVertexOf<TBoard>>;
|
|
99
|
+
getIncidentEdges: (
|
|
100
|
+
vertexId: BoardVertexIdOf<TBoard>,
|
|
101
|
+
) => Array<BoardEdgeOf<TBoard>>;
|
|
102
|
+
getIncidentVertices: (
|
|
103
|
+
edgeId: BoardEdgeIdOf<TBoard>,
|
|
104
|
+
) => Array<BoardVertexOf<TBoard>>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function useBoardTopology<const TBoard extends BoardTopologyData>(
|
|
108
|
+
board: TBoard,
|
|
109
|
+
): UseBoardTopologyReturn<TBoard> {
|
|
110
|
+
const normalizedBoard = useMemo<NormalizedBoardOf<TBoard>>(() => {
|
|
111
|
+
const candidateBoard = board as BoardTopologyData;
|
|
112
|
+
|
|
113
|
+
if (isGeneratedHexBoardInput(candidateBoard) || "tiles" in candidateBoard) {
|
|
114
|
+
return normalizeHexBoardInput(
|
|
115
|
+
candidateBoard as AnyHexBoardInput,
|
|
116
|
+
) as NormalizedBoardOf<TBoard>;
|
|
117
|
+
}
|
|
118
|
+
return normalizeSquareBoardInput(
|
|
119
|
+
candidateBoard as AnySquareBoardInput,
|
|
120
|
+
) as NormalizedBoardOf<TBoard>;
|
|
121
|
+
}, [board]);
|
|
122
|
+
|
|
123
|
+
const layout = (
|
|
124
|
+
"tiles" in normalizedBoard ? "hex" : "square"
|
|
125
|
+
) as BoardLayoutOf<TBoard>;
|
|
126
|
+
const spaces = useMemo(
|
|
127
|
+
() =>
|
|
128
|
+
("tiles" in normalizedBoard
|
|
129
|
+
? normalizedBoard.tiles
|
|
130
|
+
: normalizedBoard.cells) as Array<BoardSpaceOf<TBoard>>,
|
|
131
|
+
[normalizedBoard],
|
|
132
|
+
);
|
|
133
|
+
const edges = useMemo(
|
|
134
|
+
() => normalizedBoard.edges as Array<BoardEdgeOf<TBoard>>,
|
|
135
|
+
[normalizedBoard.edges],
|
|
136
|
+
);
|
|
137
|
+
const vertices = useMemo(
|
|
138
|
+
() => normalizedBoard.vertices as Array<BoardVertexOf<TBoard>>,
|
|
139
|
+
[normalizedBoard.vertices],
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const spaceById = useMemo(
|
|
143
|
+
() =>
|
|
144
|
+
new Map<BoardSpaceIdOf<TBoard>, BoardSpaceOf<TBoard>>(
|
|
145
|
+
spaces.map((space) => [space.id as BoardSpaceIdOf<TBoard>, space]),
|
|
146
|
+
),
|
|
147
|
+
[spaces],
|
|
148
|
+
);
|
|
149
|
+
const edgeById = useMemo(
|
|
150
|
+
() =>
|
|
151
|
+
new Map<BoardEdgeIdOf<TBoard>, BoardEdgeOf<TBoard>>(
|
|
152
|
+
edges.map((edge) => [edge.id as BoardEdgeIdOf<TBoard>, edge]),
|
|
153
|
+
),
|
|
154
|
+
[edges],
|
|
155
|
+
);
|
|
156
|
+
const vertexById = useMemo(
|
|
157
|
+
() =>
|
|
158
|
+
new Map<BoardVertexIdOf<TBoard>, BoardVertexOf<TBoard>>(
|
|
159
|
+
vertices.map((vertex) => [
|
|
160
|
+
vertex.id as BoardVertexIdOf<TBoard>,
|
|
161
|
+
vertex,
|
|
162
|
+
]),
|
|
163
|
+
),
|
|
164
|
+
[vertices],
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const adjacentSpaceIdsById = useMemo(() => {
|
|
168
|
+
const adjacency = new Map<
|
|
169
|
+
BoardSpaceIdOf<TBoard>,
|
|
170
|
+
Array<BoardSpaceIdOf<TBoard>>
|
|
171
|
+
>();
|
|
172
|
+
for (const space of spaces) {
|
|
173
|
+
adjacency.set(space.id as BoardSpaceIdOf<TBoard>, []);
|
|
174
|
+
}
|
|
175
|
+
for (const edge of edges) {
|
|
176
|
+
const spaceIds = toSpaceIds(edge);
|
|
177
|
+
if (spaceIds.length !== 2) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const [leftId, rightId] = spaceIds;
|
|
181
|
+
if (!leftId || !rightId) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
adjacency.set(leftId, [...(adjacency.get(leftId) ?? []), rightId]);
|
|
185
|
+
adjacency.set(rightId, [...(adjacency.get(rightId) ?? []), leftId]);
|
|
186
|
+
}
|
|
187
|
+
return adjacency;
|
|
188
|
+
}, [edges, spaces]);
|
|
189
|
+
|
|
190
|
+
const getSpace = useCallback(
|
|
191
|
+
(spaceId: BoardSpaceIdOf<TBoard>) => {
|
|
192
|
+
return spaceById.get(spaceId);
|
|
193
|
+
},
|
|
194
|
+
[spaceById],
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const getEdge = useCallback(
|
|
198
|
+
(edgeId: BoardEdgeIdOf<TBoard>) => {
|
|
199
|
+
return edgeById.get(edgeId);
|
|
200
|
+
},
|
|
201
|
+
[edgeById],
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const getVertex = useCallback(
|
|
205
|
+
(vertexId: BoardVertexIdOf<TBoard>) => {
|
|
206
|
+
return vertexById.get(vertexId);
|
|
207
|
+
},
|
|
208
|
+
[vertexById],
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const getAdjacentSpaces = useCallback(
|
|
212
|
+
(spaceId: BoardSpaceIdOf<TBoard>) => {
|
|
213
|
+
return (adjacentSpaceIdsById.get(spaceId) ?? [])
|
|
214
|
+
.map((adjacentSpaceId) => spaceById.get(adjacentSpaceId))
|
|
215
|
+
.filter((space): space is BoardSpaceOf<TBoard> => space !== undefined);
|
|
216
|
+
},
|
|
217
|
+
[adjacentSpaceIdsById, spaceById],
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const getDistance = useCallback(
|
|
221
|
+
(
|
|
222
|
+
fromSpaceId: BoardSpaceIdOf<TBoard>,
|
|
223
|
+
toSpaceId: BoardSpaceIdOf<TBoard>,
|
|
224
|
+
) => {
|
|
225
|
+
if (fromSpaceId === toSpaceId) {
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const visited = new Set<BoardSpaceIdOf<TBoard>>([fromSpaceId]);
|
|
230
|
+
let frontier = [fromSpaceId];
|
|
231
|
+
let distance = 0;
|
|
232
|
+
|
|
233
|
+
while (frontier.length > 0) {
|
|
234
|
+
distance += 1;
|
|
235
|
+
const nextFrontier: Array<BoardSpaceIdOf<TBoard>> = [];
|
|
236
|
+
for (const currentSpaceId of frontier) {
|
|
237
|
+
for (const adjacentSpaceId of adjacentSpaceIdsById.get(
|
|
238
|
+
currentSpaceId,
|
|
239
|
+
) ?? []) {
|
|
240
|
+
if (adjacentSpaceId === toSpaceId) {
|
|
241
|
+
return distance;
|
|
242
|
+
}
|
|
243
|
+
if (!visited.has(adjacentSpaceId)) {
|
|
244
|
+
visited.add(adjacentSpaceId);
|
|
245
|
+
nextFrontier.push(adjacentSpaceId);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
frontier = nextFrontier;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return Number.POSITIVE_INFINITY;
|
|
253
|
+
},
|
|
254
|
+
[adjacentSpaceIdsById],
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const getSpaceEdges = useCallback(
|
|
258
|
+
(spaceId: BoardSpaceIdOf<TBoard>) => {
|
|
259
|
+
return edges.filter((edge) => toSpaceIds(edge).includes(spaceId));
|
|
260
|
+
},
|
|
261
|
+
[edges],
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const getSpaceVertices = useCallback(
|
|
265
|
+
(spaceId: BoardSpaceIdOf<TBoard>) => {
|
|
266
|
+
return vertices.filter((vertex) =>
|
|
267
|
+
toVertexSpaceIds(vertex).includes(spaceId),
|
|
268
|
+
);
|
|
269
|
+
},
|
|
270
|
+
[vertices],
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const getIncidentEdges = useCallback(
|
|
274
|
+
(vertexId: BoardVertexIdOf<TBoard>) => {
|
|
275
|
+
const vertex = vertexById.get(vertexId);
|
|
276
|
+
if (!vertex) {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
const vertexSpaceIds = new Set(toVertexSpaceIds(vertex));
|
|
280
|
+
return edges.filter((edge) =>
|
|
281
|
+
toSpaceIds(edge).every((spaceId) => vertexSpaceIds.has(spaceId)),
|
|
282
|
+
);
|
|
283
|
+
},
|
|
284
|
+
[edges, vertexById],
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
const getIncidentVertices = useCallback(
|
|
288
|
+
(edgeId: BoardEdgeIdOf<TBoard>) => {
|
|
289
|
+
const edge = edgeById.get(edgeId);
|
|
290
|
+
if (!edge) {
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
const edgeSpaceIds = new Set(toSpaceIds(edge));
|
|
294
|
+
return vertices.filter((vertex) =>
|
|
295
|
+
Array.from(edgeSpaceIds).every((spaceId) =>
|
|
296
|
+
toVertexSpaceIds(vertex).includes(spaceId),
|
|
297
|
+
),
|
|
298
|
+
);
|
|
299
|
+
},
|
|
300
|
+
[edgeById, vertices],
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
layout,
|
|
305
|
+
board: normalizedBoard,
|
|
306
|
+
getSpace,
|
|
307
|
+
getEdge,
|
|
308
|
+
getVertex,
|
|
309
|
+
getAdjacentSpaces,
|
|
310
|
+
getDistance,
|
|
311
|
+
getSpaceEdges,
|
|
312
|
+
getSpaceVertices,
|
|
313
|
+
getIncidentEdges,
|
|
314
|
+
getIncidentVertices,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { CardCollection, ViewCard } from "../../types/index.js";
|
|
3
|
+
import { materializeCards } from "../helpers/cards.js";
|
|
4
|
+
|
|
5
|
+
export function useCards<
|
|
6
|
+
CardIdValue extends string,
|
|
7
|
+
CardValue extends ViewCard<CardIdValue>,
|
|
8
|
+
>(collection: CardCollection<CardIdValue, CardValue>) {
|
|
9
|
+
return useMemo(() => materializeCards(collection), [collection]);
|
|
10
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React binding for the {@link HandPointerEngine}.
|
|
3
|
+
*
|
|
4
|
+
* The hook recognizes pointer/keyboard activity on a card and forwards it to
|
|
5
|
+
* the surrounding `CardDragSurface` controller. All `CardIntent` emission
|
|
6
|
+
* (`activate`, `previewStart`, `previewEnd`, `drop`) flows through the
|
|
7
|
+
* surface so that the surface is the single source of truth for the
|
|
8
|
+
* drag-lifecycle.
|
|
9
|
+
*
|
|
10
|
+
* Two interaction policies:
|
|
11
|
+
*
|
|
12
|
+
* - `direct-activate` — desktop tap/click and keyboard Enter/Space emit one
|
|
13
|
+
* `activate` (the hook calls `onIntent` directly because no drag-lifecycle
|
|
14
|
+
* exists in this mode).
|
|
15
|
+
* - `drag-to-target` — pointer lift starts a surface drag session; release
|
|
16
|
+
* commits/returns through the surface; tap/keyboard Enter trigger the
|
|
17
|
+
* surface's `recordTap`/`startKeyboardDrag` so it owns the lifecycle.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
21
|
+
import {
|
|
22
|
+
DEFAULT_POINTER_THRESHOLDS,
|
|
23
|
+
HandPointerEngine,
|
|
24
|
+
type CardPointerState,
|
|
25
|
+
type HandInteractionPolicy,
|
|
26
|
+
type PointerEngineCardSnapshot,
|
|
27
|
+
type PointerEngineThresholds,
|
|
28
|
+
} from "../components/hand-pointer-engine.js";
|
|
29
|
+
import type { CardDragSurfaceController } from "../components/CardDragSurface.js";
|
|
30
|
+
import type { CardIntent } from "../types/visual-state.js";
|
|
31
|
+
import type { ReactNode } from "react";
|
|
32
|
+
|
|
33
|
+
export interface UseHandCardPointerOptions {
|
|
34
|
+
onIntent?: (intent: CardIntent) => void;
|
|
35
|
+
thresholds?: PointerEngineThresholds;
|
|
36
|
+
/**
|
|
37
|
+
* Active interaction policy. Defaults to `direct-activate`.
|
|
38
|
+
*/
|
|
39
|
+
policy?: HandInteractionPolicy;
|
|
40
|
+
/**
|
|
41
|
+
* Drag-surface controller for `drag-to-target` mode. The hook forwards
|
|
42
|
+
* pointer/keyboard lifts to the controller; the controller emits intents.
|
|
43
|
+
*/
|
|
44
|
+
surface?: CardDragSurfaceController | null;
|
|
45
|
+
/**
|
|
46
|
+
* Render the lifted card payload. Required when a `surface` is supplied;
|
|
47
|
+
* the hook calls this to produce the overlay content for the active card.
|
|
48
|
+
*/
|
|
49
|
+
renderLiftedCard?: (cardId: string) => ReactNode;
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the human-readable label of the card being dragged for live
|
|
52
|
+
* announcements.
|
|
53
|
+
*/
|
|
54
|
+
resolveCardLabel?: (cardId: string) => string | null | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface HandCardPointerBindings {
|
|
58
|
+
/** Identity of the card the engine is currently tracking, if any. */
|
|
59
|
+
activeCardId: string | null;
|
|
60
|
+
/** Pointer recognition state (used for previewing/scroll-lock visuals). */
|
|
61
|
+
recognitionState: CardPointerState;
|
|
62
|
+
/** Whether the host should suppress vertical scrolling. */
|
|
63
|
+
scrollLocked: boolean;
|
|
64
|
+
/** Returns the props bag for a single card. */
|
|
65
|
+
bindCard: (card: PointerEngineCardSnapshot) => HandCardPointerProps;
|
|
66
|
+
/** Programmatic activation, for keyboard and synthetic clicks. */
|
|
67
|
+
activateKeyboard: (card: PointerEngineCardSnapshot) => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface HandCardPointerProps {
|
|
71
|
+
ref: (element: HTMLElement | null) => void;
|
|
72
|
+
onPointerDown: (event: React.PointerEvent<HTMLElement>) => void;
|
|
73
|
+
onPointerMove: (event: React.PointerEvent<HTMLElement>) => void;
|
|
74
|
+
onPointerUp: (event: React.PointerEvent<HTMLElement>) => void;
|
|
75
|
+
onPointerCancel: (event: React.PointerEvent<HTMLElement>) => void;
|
|
76
|
+
onLostPointerCapture: (event: React.PointerEvent<HTMLElement>) => void;
|
|
77
|
+
onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => void;
|
|
78
|
+
onClick: (event: React.MouseEvent<HTMLElement>) => void;
|
|
79
|
+
style: React.CSSProperties;
|
|
80
|
+
"data-pointer-state": CardPointerState["kind"];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function useHandCardPointer({
|
|
84
|
+
onIntent,
|
|
85
|
+
thresholds = DEFAULT_POINTER_THRESHOLDS,
|
|
86
|
+
policy = "direct-activate",
|
|
87
|
+
surface = null,
|
|
88
|
+
renderLiftedCard,
|
|
89
|
+
resolveCardLabel,
|
|
90
|
+
}: UseHandCardPointerOptions = {}): HandCardPointerBindings {
|
|
91
|
+
const intentRef = useRef(onIntent);
|
|
92
|
+
intentRef.current = onIntent;
|
|
93
|
+
const policyRef = useRef(policy);
|
|
94
|
+
policyRef.current = policy;
|
|
95
|
+
const surfaceRef = useRef<CardDragSurfaceController | null>(surface);
|
|
96
|
+
surfaceRef.current = surface;
|
|
97
|
+
const renderLiftedCardRef = useRef(renderLiftedCard);
|
|
98
|
+
renderLiftedCardRef.current = renderLiftedCard;
|
|
99
|
+
const resolveCardLabelRef = useRef(resolveCardLabel);
|
|
100
|
+
resolveCardLabelRef.current = resolveCardLabel;
|
|
101
|
+
|
|
102
|
+
const cardElementsRef = useRef(new Map<string, HTMLElement>());
|
|
103
|
+
const cardRefsRef = useRef(
|
|
104
|
+
new Map<string, (element: HTMLElement | null) => void>(),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const getCardRef = useCallback((cardId: string) => {
|
|
108
|
+
let stable = cardRefsRef.current.get(cardId);
|
|
109
|
+
if (!stable) {
|
|
110
|
+
stable = (element: HTMLElement | null) => {
|
|
111
|
+
if (element) {
|
|
112
|
+
cardElementsRef.current.set(cardId, element);
|
|
113
|
+
} else {
|
|
114
|
+
cardElementsRef.current.delete(cardId);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
cardRefsRef.current.set(cardId, stable);
|
|
118
|
+
}
|
|
119
|
+
return stable;
|
|
120
|
+
}, []);
|
|
121
|
+
|
|
122
|
+
const [activeCardId, setActiveCardId] = useState<string | null>(null);
|
|
123
|
+
const [recognitionState, setRecognitionState] = useState<CardPointerState>({
|
|
124
|
+
kind: "idle",
|
|
125
|
+
});
|
|
126
|
+
const [scrollLocked, setScrollLocked] = useState(false);
|
|
127
|
+
|
|
128
|
+
const captureSourceRect = useCallback((cardId: string) => {
|
|
129
|
+
const el = cardElementsRef.current.get(cardId);
|
|
130
|
+
if (!el) return { left: 0, top: 0, width: 0, height: 0 };
|
|
131
|
+
const rect = el.getBoundingClientRect();
|
|
132
|
+
return {
|
|
133
|
+
left: rect.left,
|
|
134
|
+
top: rect.top,
|
|
135
|
+
width: rect.width,
|
|
136
|
+
height: rect.height,
|
|
137
|
+
};
|
|
138
|
+
}, []);
|
|
139
|
+
|
|
140
|
+
const engine = useMemo(
|
|
141
|
+
() =>
|
|
142
|
+
new HandPointerEngine(
|
|
143
|
+
{
|
|
144
|
+
onTap: (cardId) => {
|
|
145
|
+
const cardEl = cardElementsRef.current.get(cardId);
|
|
146
|
+
if (policyRef.current === "drag-to-target" && surfaceRef.current) {
|
|
147
|
+
surfaceRef.current.recordTap({
|
|
148
|
+
cardId,
|
|
149
|
+
cardEligible: true,
|
|
150
|
+
cardDisabled: false,
|
|
151
|
+
sourceFocus: cardEl ?? null,
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (surfaceRef.current) {
|
|
156
|
+
surfaceRef.current.recordActivate(cardId, "tap");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
intentRef.current?.({
|
|
160
|
+
type: "activate",
|
|
161
|
+
cardId,
|
|
162
|
+
source: "tap",
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
onPreviewStart: (cardId) => {
|
|
166
|
+
// Surface is the canonical owner of all `CardIntent` emission
|
|
167
|
+
// when present. Without a surface (legacy direct-activate-only
|
|
168
|
+
// mounts), fall back to the local `onIntent` callback.
|
|
169
|
+
if (surfaceRef.current) {
|
|
170
|
+
surfaceRef.current.recordPreviewStart(cardId);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
intentRef.current?.({ type: "previewStart", cardId });
|
|
174
|
+
},
|
|
175
|
+
onPreviewEnd: (cardId) => {
|
|
176
|
+
if (surfaceRef.current) {
|
|
177
|
+
surfaceRef.current.recordPreviewEnd(cardId);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
intentRef.current?.({ type: "previewEnd", cardId });
|
|
181
|
+
},
|
|
182
|
+
onLiftStart: (event) => {
|
|
183
|
+
if (policyRef.current !== "drag-to-target") return;
|
|
184
|
+
const surfaceController = surfaceRef.current;
|
|
185
|
+
if (!surfaceController) return;
|
|
186
|
+
const content = renderLiftedCardRef.current?.(event.cardId) ?? null;
|
|
187
|
+
const label = resolveCardLabelRef.current?.(event.cardId) ?? null;
|
|
188
|
+
const sourceRect = captureSourceRect(event.cardId);
|
|
189
|
+
const cardEl = cardElementsRef.current.get(event.cardId) ?? null;
|
|
190
|
+
surfaceController.startPointerDrag({
|
|
191
|
+
cardId: event.cardId,
|
|
192
|
+
cardLabel: label,
|
|
193
|
+
pointerId: event.pointerId,
|
|
194
|
+
startX: event.startX,
|
|
195
|
+
startY: event.startY,
|
|
196
|
+
pointerX: event.pointerX,
|
|
197
|
+
pointerY: event.pointerY,
|
|
198
|
+
grabOffsetX: event.grabOffsetX,
|
|
199
|
+
grabOffsetY: event.grabOffsetY,
|
|
200
|
+
sourceRect,
|
|
201
|
+
content,
|
|
202
|
+
sourceFocus: cardEl,
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
onLiftMove: (event) => {
|
|
206
|
+
surfaceRef.current?.updatePointer({
|
|
207
|
+
x: event.pointerX,
|
|
208
|
+
y: event.pointerY,
|
|
209
|
+
});
|
|
210
|
+
},
|
|
211
|
+
onLiftEnd: (event) => {
|
|
212
|
+
surfaceRef.current?.releasePointer({
|
|
213
|
+
x: event.pointerX,
|
|
214
|
+
y: event.pointerY,
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
onLiftCancel: () => {
|
|
218
|
+
surfaceRef.current?.cancelDrag();
|
|
219
|
+
},
|
|
220
|
+
onLockScroll: setScrollLocked,
|
|
221
|
+
onStateChange: (next) => {
|
|
222
|
+
setRecognitionState(next);
|
|
223
|
+
if (next.kind === "idle") {
|
|
224
|
+
setActiveCardId(null);
|
|
225
|
+
} else {
|
|
226
|
+
setActiveCardId(next.cardId);
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
thresholds,
|
|
231
|
+
),
|
|
232
|
+
[captureSourceRect, thresholds],
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
useEffect(() => () => engine.dispose(), [engine]);
|
|
236
|
+
|
|
237
|
+
const bindCard = useCallback(
|
|
238
|
+
(card: PointerEngineCardSnapshot): HandCardPointerProps => {
|
|
239
|
+
const isActive = activeCardId === card.cardId;
|
|
240
|
+
const pointerStateKind: CardPointerState["kind"] = isActive
|
|
241
|
+
? recognitionState.kind
|
|
242
|
+
: "idle";
|
|
243
|
+
return {
|
|
244
|
+
ref: getCardRef(card.cardId),
|
|
245
|
+
style: {
|
|
246
|
+
touchAction: isActive && scrollLocked ? "none" : "pan-x",
|
|
247
|
+
userSelect: "none",
|
|
248
|
+
WebkitUserSelect: "none",
|
|
249
|
+
},
|
|
250
|
+
"data-pointer-state": pointerStateKind,
|
|
251
|
+
onPointerDown: (event) => {
|
|
252
|
+
if (event.button !== undefined && event.button !== 0) return;
|
|
253
|
+
if (card.disabled) return;
|
|
254
|
+
try {
|
|
255
|
+
(event.currentTarget as HTMLElement).setPointerCapture?.(
|
|
256
|
+
event.pointerId,
|
|
257
|
+
);
|
|
258
|
+
} catch {
|
|
259
|
+
// Test environments without an OS-backed pointer (jsdom-like
|
|
260
|
+
// headless Chromium with synthesized PointerEvents) may throw
|
|
261
|
+
// here. Capture is a UX nicety, not required for correctness.
|
|
262
|
+
}
|
|
263
|
+
engine.handlePointerDown(card, {
|
|
264
|
+
pointerId: event.pointerId,
|
|
265
|
+
clientX: event.clientX,
|
|
266
|
+
clientY: event.clientY,
|
|
267
|
+
});
|
|
268
|
+
},
|
|
269
|
+
onPointerMove: (event) => {
|
|
270
|
+
engine.handlePointerMove(card, {
|
|
271
|
+
pointerId: event.pointerId,
|
|
272
|
+
clientX: event.clientX,
|
|
273
|
+
clientY: event.clientY,
|
|
274
|
+
});
|
|
275
|
+
},
|
|
276
|
+
onPointerUp: (event) => {
|
|
277
|
+
try {
|
|
278
|
+
(event.currentTarget as HTMLElement).releasePointerCapture?.(
|
|
279
|
+
event.pointerId,
|
|
280
|
+
);
|
|
281
|
+
} catch {
|
|
282
|
+
// Pair with the synthesized-pointer try in `onPointerDown`.
|
|
283
|
+
}
|
|
284
|
+
engine.handlePointerUp(card, {
|
|
285
|
+
pointerId: event.pointerId,
|
|
286
|
+
clientX: event.clientX,
|
|
287
|
+
clientY: event.clientY,
|
|
288
|
+
});
|
|
289
|
+
},
|
|
290
|
+
onPointerCancel: () => {
|
|
291
|
+
engine.handlePointerCancel(card);
|
|
292
|
+
},
|
|
293
|
+
onLostPointerCapture: () => {
|
|
294
|
+
engine.handlePointerCancel(card);
|
|
295
|
+
},
|
|
296
|
+
onKeyDown: (event) => {
|
|
297
|
+
if (event.key === "Escape") {
|
|
298
|
+
event.preventDefault();
|
|
299
|
+
surfaceRef.current?.cancelDrag();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
303
|
+
event.preventDefault();
|
|
304
|
+
if (card.disabled || !card.eligible) return;
|
|
305
|
+
if (policyRef.current === "drag-to-target") {
|
|
306
|
+
const surfaceController = surfaceRef.current;
|
|
307
|
+
if (!surfaceController) return;
|
|
308
|
+
const cardEl = cardElementsRef.current.get(card.cardId) ?? null;
|
|
309
|
+
const content =
|
|
310
|
+
renderLiftedCardRef.current?.(card.cardId) ?? null;
|
|
311
|
+
const label = resolveCardLabelRef.current?.(card.cardId) ?? null;
|
|
312
|
+
const sourceRect = captureSourceRect(card.cardId);
|
|
313
|
+
const started = surfaceController.startKeyboardDrag({
|
|
314
|
+
cardId: card.cardId,
|
|
315
|
+
cardLabel: label,
|
|
316
|
+
cardEligible: card.eligible,
|
|
317
|
+
sourceRect,
|
|
318
|
+
content,
|
|
319
|
+
sourceFocus: cardEl,
|
|
320
|
+
});
|
|
321
|
+
if (!started) {
|
|
322
|
+
// No eligible target — record an inspection through the
|
|
323
|
+
// surface so the visual treatment matches a tap. We never
|
|
324
|
+
// silently revert to direct activate under drag-to-target.
|
|
325
|
+
surfaceController.recordTap({
|
|
326
|
+
cardId: card.cardId,
|
|
327
|
+
cardEligible: card.eligible,
|
|
328
|
+
cardDisabled: card.disabled,
|
|
329
|
+
sourceFocus: cardEl,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (surfaceRef.current) {
|
|
335
|
+
surfaceRef.current.recordActivate(card.cardId, "keyboard");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
intentRef.current?.({
|
|
339
|
+
type: "activate",
|
|
340
|
+
cardId: card.cardId,
|
|
341
|
+
source: "keyboard",
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
onClick: () => {
|
|
346
|
+
// Pointerup paths already emit; suppress the synthetic click.
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
},
|
|
350
|
+
[
|
|
351
|
+
activeCardId,
|
|
352
|
+
captureSourceRect,
|
|
353
|
+
engine,
|
|
354
|
+
getCardRef,
|
|
355
|
+
recognitionState.kind,
|
|
356
|
+
scrollLocked,
|
|
357
|
+
],
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const activateKeyboard = useCallback((card: PointerEngineCardSnapshot) => {
|
|
361
|
+
if (card.disabled || !card.eligible) return;
|
|
362
|
+
if (policyRef.current === "drag-to-target") return;
|
|
363
|
+
if (surfaceRef.current) {
|
|
364
|
+
surfaceRef.current.recordActivate(card.cardId, "keyboard");
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
intentRef.current?.({
|
|
368
|
+
type: "activate",
|
|
369
|
+
cardId: card.cardId,
|
|
370
|
+
source: "keyboard",
|
|
371
|
+
});
|
|
372
|
+
}, []);
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
activeCardId,
|
|
376
|
+
recognitionState,
|
|
377
|
+
scrollLocked,
|
|
378
|
+
bindCard,
|
|
379
|
+
activateKeyboard,
|
|
380
|
+
};
|
|
381
|
+
}
|