@dreamboard-games/ui-sdk 0.0.43 → 0.0.45
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/dist/components/ActionButton.d.ts.map +1 -1
- package/dist/components/ActionButton.js +2 -1
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.d.ts.map +1 -1
- package/dist/components/DiceRoller.d.ts +3 -2
- package/dist/components/DiceRoller.d.ts.map +1 -1
- package/dist/components/DiceRoller.js +4 -13
- package/dist/components/ErrorBoundary.d.ts.map +1 -1
- package/dist/components/ErrorBoundary.js +94 -2
- package/dist/components/InteractionForm.d.ts +1 -1
- package/dist/components/InteractionForm.d.ts.map +1 -1
- package/dist/components/InteractionForm.js +29 -15
- package/dist/components/PrimaryActionButton.d.ts.map +1 -1
- package/dist/components/PrimaryActionButton.js +7 -6
- package/dist/components/ResourceCounter.d.ts +59 -25
- package/dist/components/ResourceCounter.d.ts.map +1 -1
- package/dist/components/ResourceCounter.js +106 -115
- package/dist/components/Toast.d.ts +13 -6
- package/dist/components/Toast.d.ts.map +1 -1
- package/dist/components/Toast.js +10 -5
- package/dist/components/board/HexGrid.js +6 -6
- package/dist/components/board/target-layer.d.ts +18 -2
- package/dist/components/board/target-layer.d.ts.map +1 -1
- package/dist/components/board/target-layer.js +20 -3
- package/dist/components/index.d.ts +3 -4
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +3 -4
- package/dist/components/surfaces/InboxSurface.d.ts.map +1 -1
- package/dist/components/surfaces/InboxSurface.js +2 -6
- package/dist/components/surfaces/PlayerCardsSurface.js +2 -2
- package/dist/components/surfaces/internal/CardZoneRoutedForm.d.ts +7 -0
- package/dist/components/surfaces/internal/CardZoneRoutedForm.d.ts.map +1 -0
- package/dist/components/surfaces/internal/CardZoneRoutedForm.js +9 -0
- package/dist/components/surfaces/internal/DefaultInteractionButton.d.ts.map +1 -1
- package/dist/components/surfaces/internal/DefaultInteractionButton.js +5 -8
- package/dist/components/surfaces/internal/useCardZoneInteractions.d.ts +2 -2
- package/dist/components/surfaces/internal/useCardZoneInteractions.d.ts.map +1 -1
- package/dist/components/surfaces/internal/useCardZoneInteractions.js +19 -43
- package/dist/context/InteractionDraftContext.d.ts +11 -2
- package/dist/context/InteractionDraftContext.d.ts.map +1 -1
- package/dist/context/InteractionDraftContext.js +41 -4
- package/dist/defaults/components.d.ts +0 -5
- package/dist/defaults/components.d.ts.map +1 -1
- package/dist/defaults/components.js +7 -11
- package/dist/hooks/useBoardInteractions.d.ts +35 -12
- package/dist/hooks/useBoardInteractions.d.ts.map +1 -1
- package/dist/hooks/useBoardInteractions.js +186 -82
- package/dist/hooks/useInteractionHandle.d.ts +1 -1
- package/dist/hooks/useInteractionHandle.d.ts.map +1 -1
- package/dist/hooks/useInteractionHandle.js +12 -27
- package/dist/index.d.ts +11 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -14
- package/dist/primitives/board.d.ts +53 -3
- package/dist/primitives/board.d.ts.map +1 -1
- package/dist/primitives/board.js +65 -41
- package/dist/primitives/dialog-lifecycle.d.ts +17 -0
- package/dist/primitives/dialog-lifecycle.d.ts.map +1 -0
- package/dist/primitives/dialog-lifecycle.js +24 -0
- package/dist/primitives/dice.d.ts +31 -0
- package/dist/primitives/dice.d.ts.map +1 -0
- package/dist/primitives/dice.js +33 -0
- package/dist/primitives/game.d.ts +55 -0
- package/dist/primitives/game.d.ts.map +1 -0
- package/dist/primitives/game.js +101 -0
- package/dist/primitives/index.d.ts +7 -4
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/primitives/index.js +7 -4
- package/dist/primitives/interaction-form-binding.d.ts +12 -0
- package/dist/primitives/interaction-form-binding.d.ts.map +1 -0
- package/dist/primitives/interaction-form-binding.js +14 -0
- package/dist/primitives/interaction-submit.d.ts +23 -0
- package/dist/primitives/interaction-submit.d.ts.map +1 -0
- package/dist/primitives/interaction-submit.js +41 -0
- package/dist/primitives/interaction.d.ts +76 -6
- package/dist/primitives/interaction.d.ts.map +1 -1
- package/dist/primitives/interaction.js +210 -26
- package/dist/primitives/player-roster.d.ts +2 -1
- package/dist/primitives/player-roster.d.ts.map +1 -1
- package/dist/primitives/prompt.d.ts +36 -11
- package/dist/primitives/prompt.d.ts.map +1 -1
- package/dist/primitives/prompt.js +29 -17
- package/dist/primitives/ui.d.ts +9 -0
- package/dist/primitives/ui.d.ts.map +1 -0
- package/dist/primitives/ui.js +7 -0
- package/dist/primitives/zone.d.ts +111 -5
- package/dist/primitives/zone.d.ts.map +1 -1
- package/dist/primitives/zone.js +349 -9
- package/dist/reducer.d.ts +2 -14
- package/dist/reducer.d.ts.map +1 -1
- package/dist/reducer.js +1 -14
- package/dist/runtime/createPluginRuntimeAPI.js +1 -1
- package/dist/types/hex-color.d.ts +7 -0
- package/dist/types/hex-color.d.ts.map +1 -0
- package/dist/types/hex-color.js +13 -0
- package/dist/types/player-state.d.ts +28 -14
- package/dist/types/player-state.d.ts.map +1 -1
- package/dist/types/plugin-state.d.ts +9 -3
- package/dist/types/plugin-state.d.ts.map +1 -1
- package/dist/ui-contract.d.ts +119 -14
- package/dist/ui-contract.d.ts.map +1 -1
- package/dist/ui-contract.js +4 -3
- package/dist/ui-sdk.d.ts +1637 -1245
- package/dist/utils/interaction-inputs.d.ts +8 -5
- package/dist/utils/interaction-inputs.d.ts.map +1 -1
- package/dist/utils/interaction-inputs.js +82 -14
- package/dist/utils/interaction-router.d.ts +31 -0
- package/dist/utils/interaction-router.d.ts.map +1 -0
- package/dist/utils/interaction-router.js +114 -0
- package/package.json +1 -1
- package/src/components/ActionButton.tsx +2 -1
- package/src/components/Card.tsx +1 -1
- package/src/components/DiceRoller.tsx +13 -22
- package/src/components/ErrorBoundary.test.tsx +19 -0
- package/src/components/ErrorBoundary.tsx +113 -24
- package/src/components/InteractionForm.test.tsx +24 -0
- package/src/components/InteractionForm.tsx +48 -23
- package/src/components/PrimaryActionButton.tsx +19 -5
- package/src/components/ResourceCounter.test.tsx +13 -13
- package/src/components/ResourceCounter.tsx +238 -244
- package/src/components/Toast.tsx +23 -10
- package/src/components/__fixtures__/ResourceCounter.fixture.tsx +70 -169
- package/src/components/board/HexGrid.tsx +6 -6
- package/src/components/board/target-layer.ts +44 -5
- package/src/components/index.ts +17 -10
- package/src/components/surfaces/InboxSurface.tsx +7 -5
- package/src/components/surfaces/PlayerCardsSurface.tsx +6 -6
- package/src/components/surfaces/internal/CardZoneRoutedForm.tsx +35 -0
- package/src/components/surfaces/internal/DefaultInteractionButton.tsx +17 -7
- package/src/components/surfaces/internal/useCardZoneInteractions.ts +25 -67
- package/src/context/InteractionDraftContext.tsx +51 -5
- package/src/defaults/components.tsx +12 -50
- package/src/defaults/defaults.test.tsx +1 -50
- package/src/hooks/useBoardInteractions.test.tsx +240 -17
- package/src/hooks/useBoardInteractions.ts +330 -105
- package/src/hooks/useInteractionHandle.ts +23 -28
- package/src/index.test.ts +60 -40
- package/src/index.ts +30 -36
- package/src/primitives/board.test.tsx +73 -0
- package/src/primitives/board.tsx +191 -40
- package/src/primitives/dialog-lifecycle.ts +58 -0
- package/src/primitives/dice.test.tsx +47 -0
- package/src/primitives/dice.tsx +79 -0
- package/src/primitives/game.test.tsx +98 -0
- package/src/primitives/game.tsx +213 -0
- package/src/primitives/index.ts +84 -0
- package/src/primitives/interaction-form-binding.tsx +56 -0
- package/src/primitives/interaction-submit.ts +90 -0
- package/src/primitives/interaction.test.tsx +396 -0
- package/src/primitives/interaction.tsx +451 -31
- package/src/primitives/player-roster.tsx +2 -1
- package/src/primitives/prompt.test.tsx +94 -3
- package/src/primitives/prompt.tsx +87 -48
- package/src/primitives/ui.test.tsx +131 -0
- package/src/primitives/ui.tsx +13 -0
- package/src/primitives/zone.test.tsx +305 -0
- package/src/primitives/zone.tsx +660 -12
- package/src/reducer.ts +7 -20
- package/src/runtime/createPluginRuntimeAPI.ts +1 -1
- package/src/types/hex-color.ts +20 -0
- package/src/types/player-state.ts +36 -18
- package/src/types/plugin-state.ts +10 -3
- package/src/ui-contract.ts +253 -21
- package/src/utils/interaction-inputs.test.ts +400 -0
- package/src/utils/interaction-inputs.ts +113 -11
- package/src/utils/interaction-router.ts +200 -0
|
@@ -7,23 +7,61 @@ import {
|
|
|
7
7
|
type InputHTMLAttributes,
|
|
8
8
|
type ReactNode,
|
|
9
9
|
} from "react";
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
useInteractionHandle,
|
|
12
|
+
type InteractionHandle,
|
|
13
|
+
type InteractionParamsShape,
|
|
14
|
+
} from "../hooks/useInteractionHandle.js";
|
|
15
|
+
import {
|
|
16
|
+
useInteractionUiStore,
|
|
17
|
+
usePendingInteractionKey,
|
|
18
|
+
} from "../context/InteractionDraftContext.js";
|
|
12
19
|
import { usePluginState } from "../context/PluginStateContext.js";
|
|
13
20
|
import type { InteractionInputKey, InteractionKey } from "../ui-contract.js";
|
|
14
|
-
import type {
|
|
21
|
+
import type {
|
|
22
|
+
InteractionDescriptor,
|
|
23
|
+
InteractionInputDescriptor,
|
|
24
|
+
ZoneHandlesSnapshot,
|
|
25
|
+
} from "../types/plugin-state.js";
|
|
15
26
|
import {
|
|
27
|
+
hasInteractionFieldErrors,
|
|
16
28
|
inputByKey,
|
|
17
29
|
isManyTargetSelectable,
|
|
18
30
|
toggleManyValue,
|
|
19
31
|
} from "../utils/interaction-inputs.js";
|
|
32
|
+
import {
|
|
33
|
+
applyInteractionDraftMutation,
|
|
34
|
+
getInteractionDraftReadiness,
|
|
35
|
+
markInteractionPending,
|
|
36
|
+
shouldRouteInteractionPending,
|
|
37
|
+
} from "../utils/interaction-router.js";
|
|
20
38
|
import { interactionLabel } from "../utils/interaction-labels.js";
|
|
21
39
|
import {
|
|
22
40
|
composeEventHandlers,
|
|
23
41
|
renderPrimitive,
|
|
24
42
|
type PrimitiveCommonProps,
|
|
25
43
|
} from "./primitive-props.js";
|
|
26
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
BoundInteractionForm,
|
|
46
|
+
castInteractionDraft,
|
|
47
|
+
castInteractionHandle,
|
|
48
|
+
type BoundInteractionFormProps,
|
|
49
|
+
} from "./interaction-form-binding.js";
|
|
50
|
+
import {
|
|
51
|
+
InteractionField as BaseInteractionField,
|
|
52
|
+
type InteractionFieldProps as BaseInteractionFieldProps,
|
|
53
|
+
} from "../components/InteractionForm.js";
|
|
54
|
+
import {
|
|
55
|
+
submitInteractionDraft,
|
|
56
|
+
submitInteractionParams,
|
|
57
|
+
type InteractionSubmitCallbacks,
|
|
58
|
+
} from "./interaction-submit.js";
|
|
59
|
+
import {
|
|
60
|
+
useDialogLifecycle,
|
|
61
|
+
type DialogLifecycleState,
|
|
62
|
+
} from "./dialog-lifecycle.js";
|
|
63
|
+
import { useGameActionError } from "./game.js";
|
|
64
|
+
import { useOptionalZonePrimitiveContext, useZoneCardContext } from "./zone.js";
|
|
27
65
|
|
|
28
66
|
interface InteractionContextValue {
|
|
29
67
|
interaction: string;
|
|
@@ -123,6 +161,98 @@ export function InteractionRoot<Interaction extends string = InteractionKey>({
|
|
|
123
161
|
);
|
|
124
162
|
}
|
|
125
163
|
|
|
164
|
+
export type InteractionDialogState = DialogLifecycleState;
|
|
165
|
+
|
|
166
|
+
export interface InteractionDialogRenderState<
|
|
167
|
+
Interaction extends string = InteractionKey,
|
|
168
|
+
> {
|
|
169
|
+
interaction: Interaction;
|
|
170
|
+
state: InteractionDialogState;
|
|
171
|
+
open: boolean;
|
|
172
|
+
minimized: boolean;
|
|
173
|
+
dismissed: boolean;
|
|
174
|
+
setOpen: (open: boolean) => void;
|
|
175
|
+
restore: () => void;
|
|
176
|
+
minimize: () => void;
|
|
177
|
+
dismiss: () => void;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface InteractionDialogProps<
|
|
181
|
+
Interaction extends string = InteractionKey,
|
|
182
|
+
> {
|
|
183
|
+
defaultOpen?: boolean;
|
|
184
|
+
onStateChange?: (state: InteractionDialogState) => void;
|
|
185
|
+
children: (state: InteractionDialogRenderState<Interaction>) => ReactNode;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function InteractionDialog<Interaction extends string = InteractionKey>({
|
|
189
|
+
defaultOpen = true,
|
|
190
|
+
onStateChange,
|
|
191
|
+
children,
|
|
192
|
+
}: InteractionDialogProps<Interaction>) {
|
|
193
|
+
const { interaction } = useInteractionPrimitiveContext();
|
|
194
|
+
const lifecycle = useDialogLifecycle({ defaultOpen, onStateChange });
|
|
195
|
+
const renderState = useMemo<InteractionDialogRenderState<Interaction>>(
|
|
196
|
+
() => ({
|
|
197
|
+
interaction: interaction as Interaction,
|
|
198
|
+
...lifecycle,
|
|
199
|
+
}),
|
|
200
|
+
[interaction, lifecycle],
|
|
201
|
+
);
|
|
202
|
+
return <>{children(renderState)}</>;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface InteractionSwitchRenderState<
|
|
206
|
+
Interaction extends string = InteractionKey,
|
|
207
|
+
> {
|
|
208
|
+
interaction: Interaction;
|
|
209
|
+
descriptor: InteractionDescriptor<Interaction>;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export type InteractionRouteMap<Interaction extends string = InteractionKey> = {
|
|
213
|
+
[Key in Interaction]?: (
|
|
214
|
+
state: InteractionSwitchRenderState<Key>,
|
|
215
|
+
) => ReactNode;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export interface InteractionSwitchProps<
|
|
219
|
+
Interaction extends string = InteractionKey,
|
|
220
|
+
> {
|
|
221
|
+
interaction?: Interaction;
|
|
222
|
+
routes: InteractionRouteMap<Interaction>;
|
|
223
|
+
fallback?: ReactNode;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function InteractionSwitch<Interaction extends string = InteractionKey>({
|
|
227
|
+
interaction,
|
|
228
|
+
routes,
|
|
229
|
+
fallback = null,
|
|
230
|
+
}: InteractionSwitchProps<Interaction>) {
|
|
231
|
+
const pendingInteractionKey = usePendingInteractionKey();
|
|
232
|
+
const descriptors = usePluginState(
|
|
233
|
+
(state) => state.gameplay.availableInteractions,
|
|
234
|
+
);
|
|
235
|
+
const routedInteraction = interaction ?? pendingInteractionKey;
|
|
236
|
+
const descriptor = routedInteraction
|
|
237
|
+
? descriptors.find(
|
|
238
|
+
(candidate) => candidate.interactionKey === routedInteraction,
|
|
239
|
+
)
|
|
240
|
+
: undefined;
|
|
241
|
+
if (!descriptor) return <>{fallback}</>;
|
|
242
|
+
const route =
|
|
243
|
+
routes[descriptor.interactionKey as keyof typeof routes] ?? null;
|
|
244
|
+
if (!route) return <>{fallback}</>;
|
|
245
|
+
const typedInteraction = descriptor.interactionKey as Interaction;
|
|
246
|
+
return (
|
|
247
|
+
<InteractionRoot interaction={typedInteraction}>
|
|
248
|
+
{route({
|
|
249
|
+
interaction: typedInteraction,
|
|
250
|
+
descriptor: descriptor as InteractionDescriptor<Interaction>,
|
|
251
|
+
})}
|
|
252
|
+
</InteractionRoot>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
126
256
|
export type InteractionPartProps = PrimitiveCommonProps &
|
|
127
257
|
HTMLAttributes<HTMLElement>;
|
|
128
258
|
|
|
@@ -215,20 +345,144 @@ export function InteractionTrigger({
|
|
|
215
345
|
});
|
|
216
346
|
}
|
|
217
347
|
|
|
348
|
+
export interface InteractionStateSnapshot<
|
|
349
|
+
Params extends InteractionParamsShape = InteractionParamsShape,
|
|
350
|
+
DefaultedKeys extends keyof Params & string = never,
|
|
351
|
+
> {
|
|
352
|
+
interaction: string;
|
|
353
|
+
descriptor: InteractionDescriptor;
|
|
354
|
+
handle: InteractionHandle<Params, DefaultedKeys>;
|
|
355
|
+
draft: InteractionHandle<Params, DefaultedKeys>["draft"];
|
|
356
|
+
values: InteractionHandle<Params, DefaultedKeys>["values"];
|
|
357
|
+
status: InteractionHandle<Params, DefaultedKeys>["status"];
|
|
358
|
+
available: boolean;
|
|
359
|
+
isReady: boolean;
|
|
360
|
+
isArmed: boolean;
|
|
361
|
+
inputKeys: readonly string[];
|
|
362
|
+
missingInputs: readonly string[];
|
|
363
|
+
readyFrontier: readonly string[];
|
|
364
|
+
blockedInputs: readonly string[];
|
|
365
|
+
hasInputs: boolean;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export interface InteractionStateProps<
|
|
369
|
+
Params extends InteractionParamsShape = InteractionParamsShape,
|
|
370
|
+
DefaultedKeys extends keyof Params & string = never,
|
|
371
|
+
> {
|
|
372
|
+
unavailable: ReactNode;
|
|
373
|
+
children: (
|
|
374
|
+
state: InteractionStateSnapshot<Params, DefaultedKeys>,
|
|
375
|
+
) => ReactNode;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function InteractionState<
|
|
379
|
+
Params extends InteractionParamsShape = InteractionParamsShape,
|
|
380
|
+
DefaultedKeys extends keyof Params & string = never,
|
|
381
|
+
>({ children, unavailable }: InteractionStateProps<Params, DefaultedKeys>) {
|
|
382
|
+
const { interaction, descriptor, handle } = useInteractionPrimitiveContext();
|
|
383
|
+
const store = useInteractionUiStore();
|
|
384
|
+
if (!descriptor || !handle) {
|
|
385
|
+
return <>{unavailable}</>;
|
|
386
|
+
}
|
|
387
|
+
const typedHandle = castInteractionHandle<Params, DefaultedKeys>(handle);
|
|
388
|
+
const liveDraft = castInteractionDraft<Params, DefaultedKeys>(
|
|
389
|
+
store.getDraft(descriptor.interactionKey),
|
|
390
|
+
);
|
|
391
|
+
const inputKeys = descriptor.inputs.map((input) => input.key);
|
|
392
|
+
const readiness = getInteractionDraftReadiness(descriptor, liveDraft);
|
|
393
|
+
return (
|
|
394
|
+
<>
|
|
395
|
+
{children({
|
|
396
|
+
interaction,
|
|
397
|
+
descriptor,
|
|
398
|
+
handle: typedHandle,
|
|
399
|
+
draft: liveDraft,
|
|
400
|
+
values: typedHandle.values,
|
|
401
|
+
status: typedHandle.status,
|
|
402
|
+
available: typedHandle.available,
|
|
403
|
+
isReady: readiness.ready,
|
|
404
|
+
isArmed: typedHandle.isArmed,
|
|
405
|
+
inputKeys,
|
|
406
|
+
missingInputs: readiness.missingInputs,
|
|
407
|
+
readyFrontier: readiness.readyFrontier,
|
|
408
|
+
blockedInputs: readiness.blockedInputs,
|
|
409
|
+
hasInputs: inputKeys.length > 0,
|
|
410
|
+
})}
|
|
411
|
+
</>
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export type InteractionFormPrimitiveProps<
|
|
416
|
+
Params extends InteractionParamsShape = InteractionParamsShape,
|
|
417
|
+
DefaultedKeys extends keyof Params & string = never,
|
|
418
|
+
> = BoundInteractionFormProps<Params, DefaultedKeys>;
|
|
419
|
+
|
|
420
|
+
export function InteractionFormPrimitive<
|
|
421
|
+
Params extends InteractionParamsShape = InteractionParamsShape,
|
|
422
|
+
DefaultedKeys extends keyof Params & string = never,
|
|
423
|
+
>(props: InteractionFormPrimitiveProps<Params, DefaultedKeys>) {
|
|
424
|
+
const { descriptor, handle } = useInteractionPrimitiveContext();
|
|
425
|
+
if (!descriptor || !handle) return null;
|
|
426
|
+
return (
|
|
427
|
+
<BoundInteractionForm<Params, DefaultedKeys>
|
|
428
|
+
descriptor={descriptor}
|
|
429
|
+
handle={handle}
|
|
430
|
+
{...props}
|
|
431
|
+
/>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export type InteractionFieldPrimitiveProps<
|
|
436
|
+
Params extends InteractionParamsShape = InteractionParamsShape,
|
|
437
|
+
Input extends keyof Params & string = keyof Params & string,
|
|
438
|
+
> = Omit<
|
|
439
|
+
BaseInteractionFieldProps<Params, Input>,
|
|
440
|
+
"descriptor" | "handle" | "inputKey"
|
|
441
|
+
> & {
|
|
442
|
+
input: Input;
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
export function InteractionFieldPrimitive<
|
|
446
|
+
Params extends InteractionParamsShape = InteractionParamsShape,
|
|
447
|
+
Input extends keyof Params & string = keyof Params & string,
|
|
448
|
+
>({ input, ...props }: InteractionFieldPrimitiveProps<Params, Input>) {
|
|
449
|
+
const { descriptor, handle } = useInteractionPrimitiveContext();
|
|
450
|
+
if (!descriptor || !handle) return null;
|
|
451
|
+
return (
|
|
452
|
+
<BaseInteractionField<Params, Input>
|
|
453
|
+
descriptor={descriptor}
|
|
454
|
+
handle={castInteractionHandle<Params>(handle)}
|
|
455
|
+
inputKey={input}
|
|
456
|
+
{...props}
|
|
457
|
+
/>
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
218
461
|
export type InteractionSubmitProps = PrimitiveCommonProps &
|
|
219
|
-
ButtonHTMLAttributes<HTMLButtonElement
|
|
462
|
+
ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
463
|
+
params?:
|
|
464
|
+
| InteractionParamsShape
|
|
465
|
+
| (() => InteractionParamsShape | null | undefined);
|
|
466
|
+
onSubmitSuccess?: InteractionSubmitCallbacks["onSubmitSuccess"];
|
|
467
|
+
onSubmitError?: InteractionSubmitCallbacks["onSubmitError"];
|
|
468
|
+
};
|
|
220
469
|
|
|
221
470
|
export function InteractionSubmit({
|
|
222
471
|
disabled,
|
|
223
472
|
onClick,
|
|
473
|
+
params,
|
|
474
|
+
onSubmitSuccess,
|
|
475
|
+
onSubmitError,
|
|
224
476
|
...props
|
|
225
477
|
}: InteractionSubmitProps) {
|
|
226
478
|
const { descriptor, handle } = useInteractionPrimitiveContext();
|
|
479
|
+
const gameActionError = useGameActionError();
|
|
227
480
|
const isSubmitting = handle?.status === "submitting";
|
|
481
|
+
const hasExplicitParams = params !== undefined;
|
|
228
482
|
const isDisabled =
|
|
229
483
|
disabled === true ||
|
|
230
484
|
!descriptor?.available ||
|
|
231
|
-
!handle?.isReady ||
|
|
485
|
+
(!hasExplicitParams && !handle?.isReady) ||
|
|
232
486
|
isSubmitting;
|
|
233
487
|
return renderPrimitive("button", {
|
|
234
488
|
type: "button",
|
|
@@ -241,10 +495,24 @@ export function InteractionSubmit({
|
|
|
241
495
|
"data-available": descriptor?.available ?? false,
|
|
242
496
|
"data-disabled": isDisabled || undefined,
|
|
243
497
|
"data-ready": handle?.isReady ?? false,
|
|
498
|
+
"data-has-inputs": descriptor ? descriptor.inputs.length > 0 : undefined,
|
|
499
|
+
"data-input-count": descriptor?.inputs.length,
|
|
244
500
|
"data-submitting": isSubmitting || undefined,
|
|
245
501
|
"data-state": handle?.status ?? "unavailable",
|
|
246
502
|
onClick: composeEventHandlers(onClick, () => {
|
|
247
|
-
|
|
503
|
+
if (isDisabled || !handle) return;
|
|
504
|
+
const resolvedParams = typeof params === "function" ? params() : params;
|
|
505
|
+
if (resolvedParams === null || resolvedParams === undefined) {
|
|
506
|
+
void submitInteractionDraft(handle, {
|
|
507
|
+
onSubmitSuccess,
|
|
508
|
+
onSubmitError: onSubmitError ?? gameActionError ?? undefined,
|
|
509
|
+
});
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
void submitInteractionParams(handle, resolvedParams, {
|
|
513
|
+
onSubmitSuccess,
|
|
514
|
+
onSubmitError: onSubmitError ?? gameActionError ?? undefined,
|
|
515
|
+
});
|
|
248
516
|
}),
|
|
249
517
|
});
|
|
250
518
|
}
|
|
@@ -286,8 +554,7 @@ export type InteractionCardInputProps<
|
|
|
286
554
|
> = PrimitiveCommonProps &
|
|
287
555
|
ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
288
556
|
input: Input;
|
|
289
|
-
|
|
290
|
-
card?: string;
|
|
557
|
+
unsafeCardId?: string;
|
|
291
558
|
selected?: boolean;
|
|
292
559
|
onSelectedChange?: (selected: boolean) => void;
|
|
293
560
|
};
|
|
@@ -296,8 +563,7 @@ export function InteractionCardInput<
|
|
|
296
563
|
Input extends string = InteractionInputKey,
|
|
297
564
|
>({
|
|
298
565
|
input,
|
|
299
|
-
|
|
300
|
-
card,
|
|
566
|
+
unsafeCardId,
|
|
301
567
|
selected,
|
|
302
568
|
onSelectedChange,
|
|
303
569
|
onClick,
|
|
@@ -308,10 +574,15 @@ export function InteractionCardInput<
|
|
|
308
574
|
const { descriptor, handle } = useInteractionPrimitiveContext();
|
|
309
575
|
const store = useInteractionUiStore();
|
|
310
576
|
const zoneCard = useZoneCardContext();
|
|
311
|
-
const
|
|
577
|
+
const zoneContext = useOptionalZonePrimitiveContext();
|
|
578
|
+
const cardId = zoneCard?.cardId ?? unsafeCardId;
|
|
579
|
+
const validationZone = zoneCard?.zone ?? zoneContext?.zone;
|
|
580
|
+
const zoneSnapshot = usePluginState((state) =>
|
|
581
|
+
validationZone ? state.gameplay.zones[validationZone] : undefined,
|
|
582
|
+
);
|
|
312
583
|
const cardDescriptor = usePluginState((state) => {
|
|
313
|
-
if (!cardId || !
|
|
314
|
-
return state.gameplay.zones[
|
|
584
|
+
if (!cardId || !validationZone) return undefined;
|
|
585
|
+
return state.gameplay.zones[validationZone]?.playableByCardId[cardId]?.find(
|
|
315
586
|
(candidate) =>
|
|
316
587
|
candidate.interactionKey === descriptor?.interactionKey &&
|
|
317
588
|
candidate.inputs.some((candidateInput) => candidateInput.key === input),
|
|
@@ -320,6 +591,23 @@ export function InteractionCardInput<
|
|
|
320
591
|
const inputDescriptor = descriptor
|
|
321
592
|
? inputByKey(descriptor, input)
|
|
322
593
|
: undefined;
|
|
594
|
+
const targetInvalidReason = cardTargetInvalidReason({
|
|
595
|
+
cardDescriptor,
|
|
596
|
+
cardId,
|
|
597
|
+
inputDescriptor,
|
|
598
|
+
unsafeCardId,
|
|
599
|
+
validationZone,
|
|
600
|
+
zoneCard,
|
|
601
|
+
zoneSnapshot,
|
|
602
|
+
});
|
|
603
|
+
const isTargetValid = targetInvalidReason === undefined;
|
|
604
|
+
throwCardInputDevMismatch({
|
|
605
|
+
cardId,
|
|
606
|
+
targetInvalidReason,
|
|
607
|
+
unsafeCardId,
|
|
608
|
+
validationZone,
|
|
609
|
+
zoneCard,
|
|
610
|
+
});
|
|
323
611
|
const liveDraft = descriptor ? store.getDraft(descriptor.interactionKey) : {};
|
|
324
612
|
const currentValue =
|
|
325
613
|
liveDraft[input] ?? handle?.draft[input] ?? handle?.values[input];
|
|
@@ -332,22 +620,18 @@ export function InteractionCardInput<
|
|
|
332
620
|
cardId !== undefined &&
|
|
333
621
|
String(currentValue) === String(cardId);
|
|
334
622
|
const isSelected = selected ?? selectedByDraft;
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
cardId !== undefined &&
|
|
344
|
-
isCardInput &&
|
|
345
|
-
(eligibleTargets === undefined || eligibleTargets.includes(cardId));
|
|
623
|
+
const isCardAvailable =
|
|
624
|
+
cardDescriptor?.available ??
|
|
625
|
+
(isTargetValid && (descriptor?.available ?? false));
|
|
626
|
+
const cardUnavailableReason =
|
|
627
|
+
cardDescriptor?.unavailableReason ??
|
|
628
|
+
(!descriptor?.available
|
|
629
|
+
? (descriptor?.unavailableReason ?? "interaction-unavailable")
|
|
630
|
+
: undefined);
|
|
346
631
|
const isSelectable =
|
|
347
632
|
cardId !== undefined &&
|
|
348
633
|
inputDescriptor !== undefined &&
|
|
349
634
|
isManyTargetSelectable(inputDescriptor, currentValue, cardId);
|
|
350
|
-
const isCandidateAvailable = cardDescriptor?.available ?? true;
|
|
351
635
|
const validation = handle?.validateDraft();
|
|
352
636
|
const fieldErrors = validation?.fieldErrors[input] ?? [];
|
|
353
637
|
const isInvalid = fieldErrors.length > 0;
|
|
@@ -355,7 +639,9 @@ export function InteractionCardInput<
|
|
|
355
639
|
disabled === true ||
|
|
356
640
|
!descriptor?.available ||
|
|
357
641
|
!cardId ||
|
|
358
|
-
!
|
|
642
|
+
!isTargetValid ||
|
|
643
|
+
!isCardAvailable ||
|
|
644
|
+
!isSelectable ||
|
|
359
645
|
!handle;
|
|
360
646
|
|
|
361
647
|
return renderPrimitive("button", {
|
|
@@ -368,11 +654,14 @@ export function InteractionCardInput<
|
|
|
368
654
|
"data-dreamboard-interaction-card-input": "",
|
|
369
655
|
"data-input-name": input,
|
|
370
656
|
"data-card-id": cardId,
|
|
371
|
-
"data-zone":
|
|
657
|
+
"data-zone": validationZone,
|
|
372
658
|
"data-selected": isSelected || undefined,
|
|
373
|
-
"data-eligible":
|
|
659
|
+
"data-eligible": isTargetValid && isCardAvailable,
|
|
660
|
+
"data-target-valid": isTargetValid,
|
|
661
|
+
"data-target-invalid-reason": targetInvalidReason,
|
|
374
662
|
"data-selectable": isSelectable,
|
|
375
|
-
"data-card-available":
|
|
663
|
+
"data-card-available": isCardAvailable,
|
|
664
|
+
"data-card-unavailable-reason": cardUnavailableReason,
|
|
376
665
|
"data-invalid": isInvalid || undefined,
|
|
377
666
|
"data-disabled": isDisabled || undefined,
|
|
378
667
|
"data-missing-resource": cardId ? undefined : true,
|
|
@@ -382,7 +671,35 @@ export function InteractionCardInput<
|
|
|
382
671
|
selection?.mode === "many"
|
|
383
672
|
? toggleManyValue(currentValue, cardId, selection)
|
|
384
673
|
: cardId;
|
|
385
|
-
|
|
674
|
+
const nextDraft = applyInteractionDraftMutation(store, descriptor, [
|
|
675
|
+
{ key: input, value: nextValue },
|
|
676
|
+
]);
|
|
677
|
+
const readiness = getInteractionDraftReadiness(descriptor, nextDraft);
|
|
678
|
+
const hasMissingSurfaceTarget = readiness.readyFrontier.some((key) => {
|
|
679
|
+
const candidate = inputByKey(descriptor, key);
|
|
680
|
+
return (
|
|
681
|
+
candidate?.domain.type === "target" &&
|
|
682
|
+
candidate.domain.targetKind !== "card" &&
|
|
683
|
+
candidate.domain.selection?.mode !== "many"
|
|
684
|
+
);
|
|
685
|
+
});
|
|
686
|
+
const hasMissingFormInput = readiness.readyFrontier.some((key) => {
|
|
687
|
+
const candidate = inputByKey(descriptor, key);
|
|
688
|
+
return (
|
|
689
|
+
candidate !== undefined &&
|
|
690
|
+
(candidate.domain.type !== "target" ||
|
|
691
|
+
candidate.domain.selection?.mode === "many")
|
|
692
|
+
);
|
|
693
|
+
});
|
|
694
|
+
const hasFieldErrors = hasInteractionFieldErrors(readiness.fieldErrors);
|
|
695
|
+
if (shouldRouteInteractionPending(descriptor, readiness)) {
|
|
696
|
+
markInteractionPending(store, descriptor);
|
|
697
|
+
store.setPendingInteraction(
|
|
698
|
+
!hasMissingSurfaceTarget && (hasMissingFormInput || hasFieldErrors)
|
|
699
|
+
? descriptor.interactionKey
|
|
700
|
+
: null,
|
|
701
|
+
);
|
|
702
|
+
}
|
|
386
703
|
onSelectedChange?.(
|
|
387
704
|
selection?.mode === "many"
|
|
388
705
|
? Array.isArray(nextValue) && nextValue.includes(cardId)
|
|
@@ -392,14 +709,117 @@ export function InteractionCardInput<
|
|
|
392
709
|
});
|
|
393
710
|
}
|
|
394
711
|
|
|
712
|
+
type CardTargetInvalidReason =
|
|
713
|
+
| "missing-card"
|
|
714
|
+
| "wrong-zone"
|
|
715
|
+
| "not-in-zone"
|
|
716
|
+
| "not-top-card"
|
|
717
|
+
| "not-in-domain";
|
|
718
|
+
|
|
719
|
+
function cardTargetInvalidReason({
|
|
720
|
+
cardDescriptor,
|
|
721
|
+
cardId,
|
|
722
|
+
inputDescriptor,
|
|
723
|
+
unsafeCardId,
|
|
724
|
+
validationZone,
|
|
725
|
+
zoneCard,
|
|
726
|
+
zoneSnapshot,
|
|
727
|
+
}: {
|
|
728
|
+
cardDescriptor: InteractionDescriptor | undefined;
|
|
729
|
+
cardId: string | undefined;
|
|
730
|
+
inputDescriptor: InteractionInputDescriptor | undefined;
|
|
731
|
+
unsafeCardId: string | undefined;
|
|
732
|
+
validationZone: string | undefined;
|
|
733
|
+
zoneCard: ReturnType<typeof useZoneCardContext>;
|
|
734
|
+
zoneSnapshot: ZoneHandlesSnapshot | undefined;
|
|
735
|
+
}): CardTargetInvalidReason | undefined {
|
|
736
|
+
if (!cardId) return "missing-card";
|
|
737
|
+
if (
|
|
738
|
+
inputDescriptor?.domain.type !== "target" ||
|
|
739
|
+
inputDescriptor.domain.targetKind !== "card"
|
|
740
|
+
) {
|
|
741
|
+
return "not-in-domain";
|
|
742
|
+
}
|
|
743
|
+
if (zoneCard && unsafeCardId && unsafeCardId !== zoneCard.cardId) {
|
|
744
|
+
return "wrong-zone";
|
|
745
|
+
}
|
|
746
|
+
if (
|
|
747
|
+
validationZone &&
|
|
748
|
+
zoneSnapshot &&
|
|
749
|
+
!zoneSnapshot.cardIds.includes(cardId)
|
|
750
|
+
) {
|
|
751
|
+
return "not-in-zone";
|
|
752
|
+
}
|
|
753
|
+
if (
|
|
754
|
+
validationZone &&
|
|
755
|
+
inputDescriptor.domain.zoneIds &&
|
|
756
|
+
!inputDescriptor.domain.zoneIds.includes(validationZone)
|
|
757
|
+
) {
|
|
758
|
+
return "wrong-zone";
|
|
759
|
+
}
|
|
760
|
+
if (validationZone && !cardDescriptor) {
|
|
761
|
+
return zoneSnapshot?.cardIds[0] !== cardId
|
|
762
|
+
? "not-top-card"
|
|
763
|
+
: "not-in-domain";
|
|
764
|
+
}
|
|
765
|
+
if (
|
|
766
|
+
!zoneCard &&
|
|
767
|
+
inputDescriptor.domain.eligibleTargets &&
|
|
768
|
+
!inputDescriptor.domain.eligibleTargets.includes(cardId)
|
|
769
|
+
) {
|
|
770
|
+
return "not-in-domain";
|
|
771
|
+
}
|
|
772
|
+
return undefined;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function throwCardInputDevMismatch({
|
|
776
|
+
cardId,
|
|
777
|
+
targetInvalidReason,
|
|
778
|
+
unsafeCardId,
|
|
779
|
+
validationZone,
|
|
780
|
+
zoneCard,
|
|
781
|
+
}: {
|
|
782
|
+
cardId: string | undefined;
|
|
783
|
+
targetInvalidReason: CardTargetInvalidReason | undefined;
|
|
784
|
+
unsafeCardId: string | undefined;
|
|
785
|
+
validationZone: string | undefined;
|
|
786
|
+
zoneCard: ReturnType<typeof useZoneCardContext>;
|
|
787
|
+
}) {
|
|
788
|
+
if (!isDevelopmentRuntime() || !validationZone) return;
|
|
789
|
+
if (zoneCard && unsafeCardId && unsafeCardId !== zoneCard.cardId) {
|
|
790
|
+
throw new Error(
|
|
791
|
+
`Interaction.CardInput unsafeCardId '${unsafeCardId}' does not match surrounding Zone.Item card '${zoneCard.cardId}'.`,
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
if (targetInvalidReason === "not-in-zone" && cardId) {
|
|
795
|
+
throw new Error(
|
|
796
|
+
`Interaction.CardInput card '${cardId}' is not present in surrounding zone '${validationZone}'.`,
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function isDevelopmentRuntime(): boolean {
|
|
802
|
+
const processLike = (
|
|
803
|
+
globalThis as {
|
|
804
|
+
process?: { env?: { NODE_ENV?: string } };
|
|
805
|
+
}
|
|
806
|
+
).process;
|
|
807
|
+
return processLike?.env?.NODE_ENV !== "production";
|
|
808
|
+
}
|
|
809
|
+
|
|
395
810
|
export const Interaction = {
|
|
396
811
|
Root: InteractionRoot,
|
|
812
|
+
State: InteractionState,
|
|
813
|
+
Switch: InteractionSwitch,
|
|
814
|
+
Dialog: InteractionDialog,
|
|
397
815
|
Trigger: InteractionTrigger,
|
|
398
816
|
Label: InteractionLabel,
|
|
399
817
|
Description: InteractionDescription,
|
|
400
818
|
UnavailableMessage: InteractionUnavailableMessage,
|
|
401
819
|
ValidationMessage: InteractionValidationMessage,
|
|
402
820
|
Input: InteractionInput,
|
|
821
|
+
Field: InteractionFieldPrimitive,
|
|
403
822
|
CardInput: InteractionCardInput,
|
|
823
|
+
Form: InteractionFormPrimitive,
|
|
404
824
|
Submit: InteractionSubmit,
|
|
405
825
|
};
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
renderPrimitive,
|
|
20
20
|
type PrimitiveCommonProps,
|
|
21
21
|
} from "./primitive-props.js";
|
|
22
|
+
import type { HexColor } from "../types/hex-color.js";
|
|
22
23
|
|
|
23
24
|
export interface PlayerRosterBadge {
|
|
24
25
|
key: string;
|
|
@@ -30,7 +31,7 @@ export interface PlayerRosterBadge {
|
|
|
30
31
|
export interface PlayerRosterEntry {
|
|
31
32
|
playerId: PlayerId;
|
|
32
33
|
name: string;
|
|
33
|
-
color?:
|
|
34
|
+
color?: HexColor;
|
|
34
35
|
index: number;
|
|
35
36
|
isActive: boolean;
|
|
36
37
|
isCurrentPlayer: boolean;
|