@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,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme-aware controlled primary CTA button — the dominant call-to-action on
|
|
3
|
+
* the screen at any given moment ("Roll dice", "End turn", "Confirm
|
|
4
|
+
* trade", "Place settlement").
|
|
5
|
+
*
|
|
6
|
+
* Visual contract (Laws of UX cross-references):
|
|
7
|
+
*
|
|
8
|
+
* - **Fitts** — defaults to `lg` size (min 56px tall, generous
|
|
9
|
+
* horizontal padding) so the dock target is easy to land on.
|
|
10
|
+
* Authors can opt down to `md`.
|
|
11
|
+
* - **Von Restorff (isolation)** — uses `intent.primary.solid` with
|
|
12
|
+
* `elevation.lifted` and an animated halo when `attention="auto"`
|
|
13
|
+
* and `available` is true, so the button outranks every other
|
|
14
|
+
* element in its peripheral neighbourhood.
|
|
15
|
+
* - **Peak-end** — when the action becomes available, the halo pulses for one breath cycle
|
|
16
|
+
* so the eye finds the change without re-scanning the screen.
|
|
17
|
+
* - **Doherty / responsiveness** — clicks set an internal `pending`
|
|
18
|
+
* flag the moment submit fires so the button visibly absorbs the
|
|
19
|
+
* tap, even on slow networks. Throwing submitters are swallowed
|
|
20
|
+
* here for the same reason `<DefaultInteractionButton>` does:
|
|
21
|
+
* descriptor availability is authoritative.
|
|
22
|
+
* - **Accessibility** — minimum 56×56 hit area satisfies WCAG 2.5.5.
|
|
23
|
+
* `prefers-reduced-motion` zeroes out the halo and press
|
|
24
|
+
* transitions through the theme's `motion.reducedMotion` token.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
useEffect,
|
|
29
|
+
useRef,
|
|
30
|
+
useState,
|
|
31
|
+
type CSSProperties,
|
|
32
|
+
type MouseEvent,
|
|
33
|
+
type ReactNode,
|
|
34
|
+
} from "react";
|
|
35
|
+
import { motion } from "framer-motion";
|
|
36
|
+
import { useTheme } from "../theme/ThemeProvider.js";
|
|
37
|
+
import {
|
|
38
|
+
intentForVariant,
|
|
39
|
+
type ButtonSize,
|
|
40
|
+
type ButtonVariant,
|
|
41
|
+
} from "../theme/derive.js";
|
|
42
|
+
import type { InteractionVisualState } from "../types/visual-state.js";
|
|
43
|
+
import { ThemedButton } from "./ThemedButton.js";
|
|
44
|
+
|
|
45
|
+
/** Attention-pulse policy for the trailing halo. */
|
|
46
|
+
export type PrimaryActionAttention = "auto" | "always" | "off";
|
|
47
|
+
|
|
48
|
+
export interface SubmittedActionConfig {
|
|
49
|
+
label?: ReactNode;
|
|
50
|
+
icon?: ReactNode;
|
|
51
|
+
variant?: ButtonVariant;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PrimaryActionButtonProps extends InteractionVisualState {
|
|
55
|
+
/**
|
|
56
|
+
* Override the visual variant. Defaults to `primary` (which maps
|
|
57
|
+
* to `intent.primary` regardless of the descriptor's `emphasis`
|
|
58
|
+
* hint — the shell's primary slot is, by definition, primary).
|
|
59
|
+
*/
|
|
60
|
+
variant?: ButtonVariant;
|
|
61
|
+
/**
|
|
62
|
+
* Sizing. Defaults to `lg` so the dock target is comfortable on
|
|
63
|
+
* touch and visually outranks panel buttons sized `md`.
|
|
64
|
+
*/
|
|
65
|
+
size?: ButtonSize;
|
|
66
|
+
/**
|
|
67
|
+
* Override the label inferred from `descriptor.label`. Use only
|
|
68
|
+
* when the descriptor's label needs phase-specific copy that the
|
|
69
|
+
* authoring layer can't express.
|
|
70
|
+
*/
|
|
71
|
+
label?: ReactNode;
|
|
72
|
+
/** Client-side draft readiness. Reducer availability remains authoritative. */
|
|
73
|
+
ready?: boolean;
|
|
74
|
+
/** Whether the action is currently available according to the caller. */
|
|
75
|
+
available?: boolean;
|
|
76
|
+
/** Optional reason rendered as the disabled tooltip. */
|
|
77
|
+
unavailableReason?: string;
|
|
78
|
+
/** External submission state. */
|
|
79
|
+
submitting?: boolean;
|
|
80
|
+
/** External submitted state. */
|
|
81
|
+
submitted?: boolean;
|
|
82
|
+
/** Copy and visual overrides once this interaction has been submitted. */
|
|
83
|
+
whenSubmitted?: SubmittedActionConfig;
|
|
84
|
+
/**
|
|
85
|
+
* Optional leading icon override. When omitted, falls back to
|
|
86
|
+
* `descriptor.icon` (an emoji glyph from the authoring spec).
|
|
87
|
+
*/
|
|
88
|
+
icon?: ReactNode;
|
|
89
|
+
/** Stable identifier for diagnostics and tests. */
|
|
90
|
+
actionId?: string;
|
|
91
|
+
/** Called when the controlled action is activated. */
|
|
92
|
+
onAction?: () => void | Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Attention-halo policy. `auto` (default) pulses the halo for one
|
|
95
|
+
* breath when the button transitions from disabled → enabled (so
|
|
96
|
+
* the user sees the moment the action becomes available), then
|
|
97
|
+
* settles into a slow ambient breath while the action remains
|
|
98
|
+
* available. `always` keeps the breath running unconditionally.
|
|
99
|
+
* `off` suppresses the halo entirely.
|
|
100
|
+
*
|
|
101
|
+
* Ignored when `theme.motion.reducedMotion === "true"`.
|
|
102
|
+
*/
|
|
103
|
+
attention?: PrimaryActionAttention;
|
|
104
|
+
/** Additional inline style merged after the resolved button style. */
|
|
105
|
+
style?: CSSProperties;
|
|
106
|
+
/** Optional className for downstream styling hooks. */
|
|
107
|
+
className?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @see PrimaryActionButtonProps
|
|
112
|
+
*/
|
|
113
|
+
export function PrimaryActionButton({
|
|
114
|
+
variant = "primary",
|
|
115
|
+
size = "lg",
|
|
116
|
+
label = "Action",
|
|
117
|
+
ready = true,
|
|
118
|
+
available: availableProp = true,
|
|
119
|
+
unavailableReason,
|
|
120
|
+
submitting: submittingProp = false,
|
|
121
|
+
submitted = false,
|
|
122
|
+
whenSubmitted,
|
|
123
|
+
icon,
|
|
124
|
+
actionId,
|
|
125
|
+
onAction,
|
|
126
|
+
attention = "auto",
|
|
127
|
+
style,
|
|
128
|
+
className,
|
|
129
|
+
}: PrimaryActionButtonProps) {
|
|
130
|
+
const theme = useTheme();
|
|
131
|
+
const reducedMotion = theme.motion.reducedMotion === "true";
|
|
132
|
+
const [pending, setPending] = useState(false);
|
|
133
|
+
|
|
134
|
+
const submitting = submittingProp || pending;
|
|
135
|
+
const available = availableProp && ready && !submitted && !submitting;
|
|
136
|
+
const resolvedVariant = submitted
|
|
137
|
+
? (whenSubmitted?.variant ?? "submitted")
|
|
138
|
+
: variant;
|
|
139
|
+
const disabled = !available;
|
|
140
|
+
const intent = intentForVariant(theme, resolvedVariant);
|
|
141
|
+
|
|
142
|
+
// Pulse the halo for one breath when availability flips on. After
|
|
143
|
+
// the breath we settle into the ambient cadence (or stop, when
|
|
144
|
+
// `attention` is `off`). Tracking the previous availability lets
|
|
145
|
+
// us catch the transition without re-mounting the component.
|
|
146
|
+
const previouslyAvailableRef = useRef(available);
|
|
147
|
+
const [pulseKey, setPulseKey] = useState(0);
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (!previouslyAvailableRef.current && available) {
|
|
150
|
+
setPulseKey((n) => n + 1);
|
|
151
|
+
}
|
|
152
|
+
previouslyAvailableRef.current = available;
|
|
153
|
+
}, [available]);
|
|
154
|
+
|
|
155
|
+
const haloEnabled =
|
|
156
|
+
!reducedMotion && available && attention !== "off" && !submitted;
|
|
157
|
+
|
|
158
|
+
const tooltip = available
|
|
159
|
+
? undefined
|
|
160
|
+
: formatUnavailableReason(unavailableReason);
|
|
161
|
+
|
|
162
|
+
const resolvedLabel: ReactNode = submitted
|
|
163
|
+
? (whenSubmitted?.label ?? label)
|
|
164
|
+
: label;
|
|
165
|
+
const resolvedIcon: ReactNode =
|
|
166
|
+
submitted && whenSubmitted?.icon ? (
|
|
167
|
+
<span aria-hidden style={{ fontSize: "1.15em" }}>
|
|
168
|
+
{whenSubmitted.icon}
|
|
169
|
+
</span>
|
|
170
|
+
) : (
|
|
171
|
+
(icon ?? null)
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<span
|
|
176
|
+
data-dreamboard-primary-action
|
|
177
|
+
data-available={available ? "true" : "false"}
|
|
178
|
+
data-pending={submitting ? "true" : undefined}
|
|
179
|
+
data-action-state={
|
|
180
|
+
submitted
|
|
181
|
+
? "submitted"
|
|
182
|
+
: submitting
|
|
183
|
+
? "submitting"
|
|
184
|
+
: available
|
|
185
|
+
? "available"
|
|
186
|
+
: "unavailable"
|
|
187
|
+
}
|
|
188
|
+
style={{
|
|
189
|
+
position: "relative",
|
|
190
|
+
display: "inline-flex",
|
|
191
|
+
alignItems: "center",
|
|
192
|
+
justifyContent: "center",
|
|
193
|
+
// The halo overflows the button bounds; the wrapper reserves
|
|
194
|
+
// a transparent buffer so it doesn't get clipped by the
|
|
195
|
+
// dock's safe-area frame.
|
|
196
|
+
padding: theme.space[1],
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
{haloEnabled ? (
|
|
200
|
+
<>
|
|
201
|
+
{/*
|
|
202
|
+
Ambient breath — slow, low-amplitude, runs as long as the
|
|
203
|
+
action is available. The outer `haloEnabled` already
|
|
204
|
+
short-circuits when `attention === "off"`, so this layer
|
|
205
|
+
is gated purely on the availability + reduced-motion
|
|
206
|
+
checks.
|
|
207
|
+
*/}
|
|
208
|
+
<motion.span
|
|
209
|
+
aria-hidden
|
|
210
|
+
style={{
|
|
211
|
+
position: "absolute",
|
|
212
|
+
inset: 0,
|
|
213
|
+
borderRadius: theme.radius.md,
|
|
214
|
+
background: intent.soft,
|
|
215
|
+
opacity: 0.55,
|
|
216
|
+
pointerEvents: "none",
|
|
217
|
+
}}
|
|
218
|
+
animate={{
|
|
219
|
+
scale: [1, 1.06, 1],
|
|
220
|
+
opacity: [0.45, 0.18, 0.45],
|
|
221
|
+
}}
|
|
222
|
+
transition={{
|
|
223
|
+
repeat: Infinity,
|
|
224
|
+
duration: 2.4,
|
|
225
|
+
ease: "easeInOut",
|
|
226
|
+
}}
|
|
227
|
+
/>
|
|
228
|
+
{/*
|
|
229
|
+
One-shot announce pulse keyed on `pulseKey` — re-mounts
|
|
230
|
+
(and thus re-runs) every time availability flips on so the
|
|
231
|
+
eye registers the change. We use a separate layer (rather
|
|
232
|
+
than retriggering the ambient breath) so the announce is
|
|
233
|
+
visibly louder than the steady-state cadence.
|
|
234
|
+
*/}
|
|
235
|
+
<motion.span
|
|
236
|
+
key={pulseKey}
|
|
237
|
+
aria-hidden
|
|
238
|
+
style={{
|
|
239
|
+
position: "absolute",
|
|
240
|
+
inset: 0,
|
|
241
|
+
borderRadius: theme.radius.md,
|
|
242
|
+
boxShadow: `0 0 0 0 ${intent.solid}`,
|
|
243
|
+
pointerEvents: "none",
|
|
244
|
+
}}
|
|
245
|
+
initial={{ opacity: 0.7 }}
|
|
246
|
+
animate={{
|
|
247
|
+
boxShadow: [
|
|
248
|
+
`0 0 0 0 ${withAlpha(intent.solid, 0.55)}`,
|
|
249
|
+
`0 0 0 14px ${withAlpha(intent.solid, 0)}`,
|
|
250
|
+
],
|
|
251
|
+
opacity: [0.7, 0],
|
|
252
|
+
}}
|
|
253
|
+
transition={{ duration: 0.9, ease: "easeOut" }}
|
|
254
|
+
/>
|
|
255
|
+
</>
|
|
256
|
+
) : null}
|
|
257
|
+
<ThemedButton
|
|
258
|
+
type="button"
|
|
259
|
+
variant={resolvedVariant}
|
|
260
|
+
size={size}
|
|
261
|
+
pressed={submitting}
|
|
262
|
+
className={className}
|
|
263
|
+
aria-label={
|
|
264
|
+
typeof resolvedLabel === "string" ? resolvedLabel : "Primary action"
|
|
265
|
+
}
|
|
266
|
+
aria-disabled={disabled || undefined}
|
|
267
|
+
data-interaction-id={actionId}
|
|
268
|
+
data-emphasis="primary"
|
|
269
|
+
title={tooltip}
|
|
270
|
+
disabled={disabled}
|
|
271
|
+
style={{
|
|
272
|
+
// Sit above the halo so clicks land on the button.
|
|
273
|
+
position: "relative",
|
|
274
|
+
zIndex: 1,
|
|
275
|
+
boxShadow: disabled || submitted ? undefined : theme.elevation.lifted,
|
|
276
|
+
...style,
|
|
277
|
+
}}
|
|
278
|
+
onClick={async (event: MouseEvent<HTMLButtonElement>) => {
|
|
279
|
+
event.preventDefault();
|
|
280
|
+
if (disabled) return;
|
|
281
|
+
setPending(true);
|
|
282
|
+
try {
|
|
283
|
+
await onAction?.();
|
|
284
|
+
} finally {
|
|
285
|
+
setPending(false);
|
|
286
|
+
}
|
|
287
|
+
}}
|
|
288
|
+
>
|
|
289
|
+
{resolvedIcon}
|
|
290
|
+
<span>{resolvedLabel}</span>
|
|
291
|
+
</ThemedButton>
|
|
292
|
+
</span>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function formatUnavailableReason(
|
|
297
|
+
reason: string | undefined,
|
|
298
|
+
): string | undefined {
|
|
299
|
+
if (reason === "INSUFFICIENT_RESOURCES") {
|
|
300
|
+
return "Insufficient resources";
|
|
301
|
+
}
|
|
302
|
+
return reason;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Add an alpha channel to a CSS colour string. Supports `#rgb`,
|
|
307
|
+
* `#rrggbb`, and any colour the browser can paint via a fallback to
|
|
308
|
+
* `color-mix` (modern Safari/Chrome/Firefox all support this; older
|
|
309
|
+
* runtimes get the original colour without alpha which is still
|
|
310
|
+
* visible — the halo is decorative).
|
|
311
|
+
*/
|
|
312
|
+
function withAlpha(color: string, alpha: number): string {
|
|
313
|
+
const trimmed = color.trim();
|
|
314
|
+
if (trimmed.startsWith("#")) {
|
|
315
|
+
const hex = trimmed.slice(1);
|
|
316
|
+
if (hex.length === 3) {
|
|
317
|
+
// `String.prototype.slice` always returns a string (possibly
|
|
318
|
+
// empty) — never `undefined` — so duplicating each digit is
|
|
319
|
+
// safe to feed to `parseInt` without further narrowing.
|
|
320
|
+
const r = parseInt(hex.slice(0, 1).repeat(2), 16);
|
|
321
|
+
const g = parseInt(hex.slice(1, 2).repeat(2), 16);
|
|
322
|
+
const b = parseInt(hex.slice(2, 3).repeat(2), 16);
|
|
323
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
324
|
+
}
|
|
325
|
+
if (hex.length === 6) {
|
|
326
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
327
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
328
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
329
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Best-effort fallback for non-hex colours (rgb/hsl/named): use
|
|
333
|
+
// `color-mix` with transparent. Safe to land in inline style — the
|
|
334
|
+
// halo is purely decorative and will gracefully degrade.
|
|
335
|
+
return `color-mix(in srgb, ${trimmed} ${Math.round(alpha * 100)}%, transparent)`;
|
|
336
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimally-styled primary action button.
|
|
3
|
+
*
|
|
4
|
+
* Visual styling now flows through {@link buttonStyle}: the background,
|
|
5
|
+
* border, foreground, radius, typography and elevation all derive from
|
|
6
|
+
* the active {@link useTheme}'s `intent.primary` slot. Override the
|
|
7
|
+
* variant when a non-primary call site needs a different emphasis (the
|
|
8
|
+
* underlying `<DefaultInteractionButton>` is the canonical button for
|
|
9
|
+
* interaction-bound submission).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ButtonHTMLAttributes } from "react";
|
|
13
|
+
import type { ButtonSize, ButtonVariant } from "../theme/derive.js";
|
|
14
|
+
import type { InteractionVisualState } from "../types/visual-state.js";
|
|
15
|
+
import { ThemedButton } from "./ThemedButton.js";
|
|
16
|
+
|
|
17
|
+
export interface PrimaryButtonProps
|
|
18
|
+
extends ButtonHTMLAttributes<HTMLButtonElement>, InteractionVisualState {
|
|
19
|
+
/** Intent slot — defaults to `primary`. */
|
|
20
|
+
variant?: ButtonVariant;
|
|
21
|
+
/** Sizing token — defaults to `md`. */
|
|
22
|
+
size?: ButtonSize;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function PrimaryButton({
|
|
26
|
+
children,
|
|
27
|
+
disabled,
|
|
28
|
+
style,
|
|
29
|
+
variant = "primary",
|
|
30
|
+
size = "md",
|
|
31
|
+
...rest
|
|
32
|
+
}: PrimaryButtonProps) {
|
|
33
|
+
return (
|
|
34
|
+
<ThemedButton
|
|
35
|
+
type="button"
|
|
36
|
+
disabled={disabled}
|
|
37
|
+
variant={variant}
|
|
38
|
+
size={size}
|
|
39
|
+
style={style}
|
|
40
|
+
{...rest}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</ThemedButton>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
createElement,
|
|
4
|
+
useContext,
|
|
5
|
+
useMemo,
|
|
6
|
+
type ComponentType,
|
|
7
|
+
type HTMLAttributes,
|
|
8
|
+
type ReactElement,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
11
|
+
import {
|
|
12
|
+
composeEventHandlers,
|
|
13
|
+
renderPrimitive,
|
|
14
|
+
type PrimitiveCommonProps,
|
|
15
|
+
} from "../primitives/primitive-props.js";
|
|
16
|
+
|
|
17
|
+
export type ResourceId = string;
|
|
18
|
+
|
|
19
|
+
export interface ResourceDisplayConfig<Resource extends string = ResourceId> {
|
|
20
|
+
type: Resource;
|
|
21
|
+
label: string;
|
|
22
|
+
icon:
|
|
23
|
+
| ReactNode
|
|
24
|
+
| ComponentType<{
|
|
25
|
+
className?: string;
|
|
26
|
+
strokeWidth?: number;
|
|
27
|
+
"aria-hidden"?: boolean | "true" | "false";
|
|
28
|
+
}>;
|
|
29
|
+
iconColor?: string;
|
|
30
|
+
bgColor?: string;
|
|
31
|
+
textColor?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ResourceCounterItemState<
|
|
35
|
+
Resource extends string = ResourceId,
|
|
36
|
+
> {
|
|
37
|
+
type: Resource;
|
|
38
|
+
label: string;
|
|
39
|
+
icon: ResourceDisplayConfig<Resource>["icon"];
|
|
40
|
+
iconColor?: string;
|
|
41
|
+
bgColor?: string;
|
|
42
|
+
textColor?: string;
|
|
43
|
+
count: number;
|
|
44
|
+
isZero: boolean;
|
|
45
|
+
interactive: boolean;
|
|
46
|
+
select: () => void;
|
|
47
|
+
renderIcon: (props?: ResourceIconProps) => ReactNode;
|
|
48
|
+
dataAttributes: {
|
|
49
|
+
"data-resource-id": Resource;
|
|
50
|
+
"data-resource-count": number;
|
|
51
|
+
"data-resource-zero": boolean | undefined;
|
|
52
|
+
"data-interactive": boolean | undefined;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ResourceIconProps {
|
|
57
|
+
className?: string;
|
|
58
|
+
strokeWidth?: number;
|
|
59
|
+
"aria-hidden"?: boolean | "true" | "false";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type ResourceCounterRootProps<Resource extends string = ResourceId> =
|
|
63
|
+
Omit<PrimitiveCommonProps, "children"> &
|
|
64
|
+
Omit<HTMLAttributes<HTMLElement>, "children"> & {
|
|
65
|
+
resources: ReadonlyArray<ResourceDisplayConfig<Resource>>;
|
|
66
|
+
counts: Partial<Record<Resource, number>>;
|
|
67
|
+
zero?: "show" | "hide";
|
|
68
|
+
onResourceClick?: (resourceType: Resource) => void;
|
|
69
|
+
children: ReactNode;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type BoundResourceCounterRootProps<
|
|
73
|
+
Resource extends string = ResourceId,
|
|
74
|
+
> = Omit<ResourceCounterRootProps<Resource>, "resources">;
|
|
75
|
+
|
|
76
|
+
export type ResourceCounterProps<Resource extends string = ResourceId> =
|
|
77
|
+
ResourceCounterRootProps<Resource>;
|
|
78
|
+
|
|
79
|
+
export type ResourceCounterPartProps<Resource extends string = ResourceId> =
|
|
80
|
+
Omit<PrimitiveCommonProps, "children"> &
|
|
81
|
+
Omit<HTMLAttributes<HTMLElement>, "children"> & {
|
|
82
|
+
children?:
|
|
83
|
+
| ReactNode
|
|
84
|
+
| ((resource: ResourceCounterItemState<Resource>) => ReactNode);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const ResourceCounterItemContext =
|
|
88
|
+
createContext<ResourceCounterItemState<string> | null>(null);
|
|
89
|
+
|
|
90
|
+
function useResourceCounterItemContext<Resource extends string>() {
|
|
91
|
+
const value = useContext(ResourceCounterItemContext);
|
|
92
|
+
if (!value) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
"ResourceCounter item primitives must be rendered inside <ResourceCounter.Item>.",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return value as ResourceCounterItemState<Resource>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function renderResourceIcon(
|
|
101
|
+
icon: ResourceDisplayConfig<string>["icon"],
|
|
102
|
+
props: ResourceIconProps = {},
|
|
103
|
+
) {
|
|
104
|
+
if (typeof icon === "function") {
|
|
105
|
+
return createElement(icon, {
|
|
106
|
+
"aria-hidden": true,
|
|
107
|
+
strokeWidth: 2.5,
|
|
108
|
+
...props,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const {
|
|
112
|
+
strokeWidth: _strokeWidth,
|
|
113
|
+
"aria-hidden": ariaHidden,
|
|
114
|
+
...spanProps
|
|
115
|
+
} = props;
|
|
116
|
+
return (
|
|
117
|
+
<span
|
|
118
|
+
aria-hidden={ariaHidden === undefined ? true : ariaHidden !== "false"}
|
|
119
|
+
{...spanProps}
|
|
120
|
+
>
|
|
121
|
+
{icon}
|
|
122
|
+
</span>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resolveResourceChildren<Resource extends string>(
|
|
127
|
+
children: ResourceCounterPartProps<Resource>["children"],
|
|
128
|
+
resource: ResourceCounterItemState<Resource>,
|
|
129
|
+
) {
|
|
130
|
+
return typeof children === "function" ? children(resource) : children;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function ResourceCounterRoot<Resource extends string = ResourceId>({
|
|
134
|
+
resources,
|
|
135
|
+
counts,
|
|
136
|
+
zero = "show",
|
|
137
|
+
onResourceClick,
|
|
138
|
+
children,
|
|
139
|
+
"aria-label": ariaLabel,
|
|
140
|
+
...props
|
|
141
|
+
}: ResourceCounterRootProps<Resource>) {
|
|
142
|
+
const items = useMemo(
|
|
143
|
+
() =>
|
|
144
|
+
resources
|
|
145
|
+
.map((resource) => {
|
|
146
|
+
const count = counts[resource.type] ?? 0;
|
|
147
|
+
return {
|
|
148
|
+
...resource,
|
|
149
|
+
count,
|
|
150
|
+
isZero: count === 0,
|
|
151
|
+
interactive: Boolean(onResourceClick),
|
|
152
|
+
select: () => onResourceClick?.(resource.type),
|
|
153
|
+
renderIcon: (iconProps) =>
|
|
154
|
+
renderResourceIcon(resource.icon, iconProps),
|
|
155
|
+
dataAttributes: {
|
|
156
|
+
"data-resource-id": resource.type,
|
|
157
|
+
"data-resource-count": count,
|
|
158
|
+
"data-resource-zero": count === 0 || undefined,
|
|
159
|
+
"data-interactive": onResourceClick ? true : undefined,
|
|
160
|
+
},
|
|
161
|
+
} satisfies ResourceCounterItemState<Resource>;
|
|
162
|
+
})
|
|
163
|
+
.filter((resource) => zero === "show" || !resource.isZero),
|
|
164
|
+
[counts, onResourceClick, resources, zero],
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
return renderPrimitive("div", {
|
|
168
|
+
role: "list",
|
|
169
|
+
"aria-label": ariaLabel ?? "Resource counts",
|
|
170
|
+
"data-dreamboard-resource-counter": "",
|
|
171
|
+
...props,
|
|
172
|
+
children: items.map((resource) => (
|
|
173
|
+
<ResourceCounterItemContext.Provider key={resource.type} value={resource}>
|
|
174
|
+
{children}
|
|
175
|
+
</ResourceCounterItemContext.Provider>
|
|
176
|
+
)),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function ResourceCounterItem<Resource extends string = ResourceId>({
|
|
181
|
+
children,
|
|
182
|
+
onClick,
|
|
183
|
+
"aria-label": ariaLabel,
|
|
184
|
+
...props
|
|
185
|
+
}: ResourceCounterPartProps<Resource>) {
|
|
186
|
+
const resource = useResourceCounterItemContext<Resource>();
|
|
187
|
+
return renderPrimitive("span", {
|
|
188
|
+
role: "listitem",
|
|
189
|
+
"aria-label": ariaLabel ?? `${resource.label}: ${resource.count}`,
|
|
190
|
+
...resource.dataAttributes,
|
|
191
|
+
...props,
|
|
192
|
+
onClick: composeEventHandlers(
|
|
193
|
+
onClick,
|
|
194
|
+
resource.interactive ? resource.select : undefined,
|
|
195
|
+
),
|
|
196
|
+
children: resolveResourceChildren(children, resource),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function ResourceCounterIcon<Resource extends string = ResourceId>({
|
|
201
|
+
className,
|
|
202
|
+
strokeWidth,
|
|
203
|
+
"aria-hidden": ariaHidden,
|
|
204
|
+
}: ResourceIconProps): ReactNode {
|
|
205
|
+
const resource = useResourceCounterItemContext<Resource>();
|
|
206
|
+
return resource.renderIcon({
|
|
207
|
+
className,
|
|
208
|
+
strokeWidth,
|
|
209
|
+
"aria-hidden": ariaHidden,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function ResourceCounterCount<Resource extends string = ResourceId>({
|
|
214
|
+
children,
|
|
215
|
+
...props
|
|
216
|
+
}: ResourceCounterPartProps<Resource>) {
|
|
217
|
+
const resource = useResourceCounterItemContext<Resource>();
|
|
218
|
+
return renderPrimitive("span", {
|
|
219
|
+
...props,
|
|
220
|
+
"data-dreamboard-resource-count": "",
|
|
221
|
+
children: resolveResourceChildren(children ?? resource.count, resource),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function ResourceCounterLabel<Resource extends string = ResourceId>({
|
|
226
|
+
children,
|
|
227
|
+
...props
|
|
228
|
+
}: ResourceCounterPartProps<Resource>) {
|
|
229
|
+
const resource = useResourceCounterItemContext<Resource>();
|
|
230
|
+
return renderPrimitive("span", {
|
|
231
|
+
...props,
|
|
232
|
+
"data-dreamboard-resource-label": "",
|
|
233
|
+
children: resolveResourceChildren(children ?? resource.label, resource),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface ResourceCounterComponents<
|
|
238
|
+
Resource extends string = ResourceId,
|
|
239
|
+
> {
|
|
240
|
+
Root(props: BoundResourceCounterRootProps<Resource>): ReactElement;
|
|
241
|
+
Item(props: ResourceCounterPartProps<Resource>): ReactElement;
|
|
242
|
+
Icon(props: ResourceIconProps): ReactNode;
|
|
243
|
+
Count(props: ResourceCounterPartProps<Resource>): ReactElement;
|
|
244
|
+
Label(props: ResourceCounterPartProps<Resource>): ReactElement;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function createResourceCounter<Resource extends string>(
|
|
248
|
+
resources: ReadonlyArray<ResourceDisplayConfig<Resource>>,
|
|
249
|
+
): ResourceCounterComponents<Resource> {
|
|
250
|
+
return {
|
|
251
|
+
Root(props) {
|
|
252
|
+
return createElement(ResourceCounterRoot<Resource>, {
|
|
253
|
+
...props,
|
|
254
|
+
resources,
|
|
255
|
+
});
|
|
256
|
+
},
|
|
257
|
+
Item: ResourceCounterItem,
|
|
258
|
+
Icon: ResourceCounterIcon,
|
|
259
|
+
Count: ResourceCounterCount,
|
|
260
|
+
Label: ResourceCounterLabel,
|
|
261
|
+
} satisfies ResourceCounterComponents<Resource>;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export const ResourceCounter = {
|
|
265
|
+
Root: ResourceCounterRoot,
|
|
266
|
+
Item: ResourceCounterItem,
|
|
267
|
+
Icon: ResourceCounterIcon,
|
|
268
|
+
Count: ResourceCounterCount,
|
|
269
|
+
Label: ResourceCounterLabel,
|
|
270
|
+
};
|