@dreamboard-games/ui-sdk 0.0.42 → 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.
Files changed (166) hide show
  1. package/dist/components/ActionButton.d.ts.map +1 -1
  2. package/dist/components/ActionButton.js +2 -1
  3. package/dist/components/Card.d.ts +1 -1
  4. package/dist/components/Card.d.ts.map +1 -1
  5. package/dist/components/DiceRoller.d.ts +3 -2
  6. package/dist/components/DiceRoller.d.ts.map +1 -1
  7. package/dist/components/DiceRoller.js +4 -13
  8. package/dist/components/ErrorBoundary.d.ts.map +1 -1
  9. package/dist/components/ErrorBoundary.js +94 -2
  10. package/dist/components/InteractionForm.d.ts +1 -1
  11. package/dist/components/InteractionForm.d.ts.map +1 -1
  12. package/dist/components/InteractionForm.js +29 -15
  13. package/dist/components/PrimaryActionButton.d.ts.map +1 -1
  14. package/dist/components/PrimaryActionButton.js +7 -6
  15. package/dist/components/ResourceCounter.d.ts +59 -25
  16. package/dist/components/ResourceCounter.d.ts.map +1 -1
  17. package/dist/components/ResourceCounter.js +106 -115
  18. package/dist/components/Toast.d.ts +13 -6
  19. package/dist/components/Toast.d.ts.map +1 -1
  20. package/dist/components/Toast.js +10 -5
  21. package/dist/components/board/HexGrid.js +6 -6
  22. package/dist/components/board/target-layer.d.ts +18 -2
  23. package/dist/components/board/target-layer.d.ts.map +1 -1
  24. package/dist/components/board/target-layer.js +20 -3
  25. package/dist/components/index.d.ts +3 -4
  26. package/dist/components/index.d.ts.map +1 -1
  27. package/dist/components/index.js +3 -4
  28. package/dist/components/surfaces/InboxSurface.d.ts.map +1 -1
  29. package/dist/components/surfaces/InboxSurface.js +2 -6
  30. package/dist/components/surfaces/PlayerCardsSurface.js +2 -2
  31. package/dist/components/surfaces/internal/CardZoneRoutedForm.d.ts +7 -0
  32. package/dist/components/surfaces/internal/CardZoneRoutedForm.d.ts.map +1 -0
  33. package/dist/components/surfaces/internal/CardZoneRoutedForm.js +9 -0
  34. package/dist/components/surfaces/internal/DefaultInteractionButton.d.ts.map +1 -1
  35. package/dist/components/surfaces/internal/DefaultInteractionButton.js +5 -8
  36. package/dist/components/surfaces/internal/useCardZoneInteractions.d.ts +2 -2
  37. package/dist/components/surfaces/internal/useCardZoneInteractions.d.ts.map +1 -1
  38. package/dist/components/surfaces/internal/useCardZoneInteractions.js +19 -43
  39. package/dist/context/InteractionDraftContext.d.ts +11 -2
  40. package/dist/context/InteractionDraftContext.d.ts.map +1 -1
  41. package/dist/context/InteractionDraftContext.js +41 -4
  42. package/dist/defaults/components.d.ts +0 -5
  43. package/dist/defaults/components.d.ts.map +1 -1
  44. package/dist/defaults/components.js +7 -11
  45. package/dist/hooks/useBoardInteractions.d.ts +35 -12
  46. package/dist/hooks/useBoardInteractions.d.ts.map +1 -1
  47. package/dist/hooks/useBoardInteractions.js +186 -82
  48. package/dist/hooks/useInteractionHandle.d.ts +1 -1
  49. package/dist/hooks/useInteractionHandle.d.ts.map +1 -1
  50. package/dist/hooks/useInteractionHandle.js +12 -27
  51. package/dist/index.d.ts +11 -17
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +5 -14
  54. package/dist/primitives/board.d.ts +53 -3
  55. package/dist/primitives/board.d.ts.map +1 -1
  56. package/dist/primitives/board.js +65 -41
  57. package/dist/primitives/dialog-lifecycle.d.ts +17 -0
  58. package/dist/primitives/dialog-lifecycle.d.ts.map +1 -0
  59. package/dist/primitives/dialog-lifecycle.js +24 -0
  60. package/dist/primitives/dice.d.ts +31 -0
  61. package/dist/primitives/dice.d.ts.map +1 -0
  62. package/dist/primitives/dice.js +33 -0
  63. package/dist/primitives/game.d.ts +55 -0
  64. package/dist/primitives/game.d.ts.map +1 -0
  65. package/dist/primitives/game.js +101 -0
  66. package/dist/primitives/index.d.ts +7 -4
  67. package/dist/primitives/index.d.ts.map +1 -1
  68. package/dist/primitives/index.js +7 -4
  69. package/dist/primitives/interaction-form-binding.d.ts +12 -0
  70. package/dist/primitives/interaction-form-binding.d.ts.map +1 -0
  71. package/dist/primitives/interaction-form-binding.js +14 -0
  72. package/dist/primitives/interaction-submit.d.ts +23 -0
  73. package/dist/primitives/interaction-submit.d.ts.map +1 -0
  74. package/dist/primitives/interaction-submit.js +41 -0
  75. package/dist/primitives/interaction.d.ts +76 -6
  76. package/dist/primitives/interaction.d.ts.map +1 -1
  77. package/dist/primitives/interaction.js +210 -26
  78. package/dist/primitives/player-roster.d.ts +2 -1
  79. package/dist/primitives/player-roster.d.ts.map +1 -1
  80. package/dist/primitives/prompt.d.ts +36 -11
  81. package/dist/primitives/prompt.d.ts.map +1 -1
  82. package/dist/primitives/prompt.js +29 -17
  83. package/dist/primitives/ui.d.ts +9 -0
  84. package/dist/primitives/ui.d.ts.map +1 -0
  85. package/dist/primitives/ui.js +7 -0
  86. package/dist/primitives/zone.d.ts +111 -5
  87. package/dist/primitives/zone.d.ts.map +1 -1
  88. package/dist/primitives/zone.js +349 -9
  89. package/dist/reducer.d.ts +2 -14
  90. package/dist/reducer.d.ts.map +1 -1
  91. package/dist/reducer.js +1 -14
  92. package/dist/runtime/createPluginRuntimeAPI.js +1 -1
  93. package/dist/types/hex-color.d.ts +7 -0
  94. package/dist/types/hex-color.d.ts.map +1 -0
  95. package/dist/types/hex-color.js +13 -0
  96. package/dist/types/player-state.d.ts +28 -14
  97. package/dist/types/player-state.d.ts.map +1 -1
  98. package/dist/types/plugin-state.d.ts +9 -3
  99. package/dist/types/plugin-state.d.ts.map +1 -1
  100. package/dist/ui-contract.d.ts +119 -14
  101. package/dist/ui-contract.d.ts.map +1 -1
  102. package/dist/ui-contract.js +4 -3
  103. package/dist/ui-sdk.d.ts +1637 -1245
  104. package/dist/utils/interaction-inputs.d.ts +8 -5
  105. package/dist/utils/interaction-inputs.d.ts.map +1 -1
  106. package/dist/utils/interaction-inputs.js +82 -14
  107. package/dist/utils/interaction-router.d.ts +31 -0
  108. package/dist/utils/interaction-router.d.ts.map +1 -0
  109. package/dist/utils/interaction-router.js +114 -0
  110. package/package.json +2 -2
  111. package/src/components/ActionButton.tsx +2 -1
  112. package/src/components/Card.tsx +1 -1
  113. package/src/components/DiceRoller.tsx +13 -22
  114. package/src/components/ErrorBoundary.test.tsx +19 -0
  115. package/src/components/ErrorBoundary.tsx +113 -24
  116. package/src/components/InteractionForm.test.tsx +24 -0
  117. package/src/components/InteractionForm.tsx +48 -23
  118. package/src/components/PrimaryActionButton.tsx +19 -5
  119. package/src/components/ResourceCounter.test.tsx +13 -13
  120. package/src/components/ResourceCounter.tsx +238 -244
  121. package/src/components/Toast.tsx +23 -10
  122. package/src/components/__fixtures__/ResourceCounter.fixture.tsx +70 -169
  123. package/src/components/board/HexGrid.tsx +6 -6
  124. package/src/components/board/target-layer.ts +44 -5
  125. package/src/components/index.ts +17 -10
  126. package/src/components/surfaces/InboxSurface.tsx +7 -5
  127. package/src/components/surfaces/PlayerCardsSurface.tsx +6 -6
  128. package/src/components/surfaces/internal/CardZoneRoutedForm.tsx +35 -0
  129. package/src/components/surfaces/internal/DefaultInteractionButton.tsx +17 -7
  130. package/src/components/surfaces/internal/useCardZoneInteractions.ts +25 -67
  131. package/src/context/InteractionDraftContext.tsx +51 -5
  132. package/src/defaults/components.tsx +12 -50
  133. package/src/defaults/defaults.test.tsx +1 -50
  134. package/src/hooks/useBoardInteractions.test.tsx +240 -17
  135. package/src/hooks/useBoardInteractions.ts +330 -105
  136. package/src/hooks/useInteractionHandle.ts +23 -28
  137. package/src/index.test.ts +60 -40
  138. package/src/index.ts +30 -36
  139. package/src/primitives/board.test.tsx +73 -0
  140. package/src/primitives/board.tsx +191 -40
  141. package/src/primitives/dialog-lifecycle.ts +58 -0
  142. package/src/primitives/dice.test.tsx +47 -0
  143. package/src/primitives/dice.tsx +79 -0
  144. package/src/primitives/game.test.tsx +98 -0
  145. package/src/primitives/game.tsx +213 -0
  146. package/src/primitives/index.ts +84 -0
  147. package/src/primitives/interaction-form-binding.tsx +56 -0
  148. package/src/primitives/interaction-submit.ts +90 -0
  149. package/src/primitives/interaction.test.tsx +396 -0
  150. package/src/primitives/interaction.tsx +451 -31
  151. package/src/primitives/player-roster.tsx +2 -1
  152. package/src/primitives/prompt.test.tsx +94 -3
  153. package/src/primitives/prompt.tsx +87 -48
  154. package/src/primitives/ui.test.tsx +131 -0
  155. package/src/primitives/ui.tsx +13 -0
  156. package/src/primitives/zone.test.tsx +305 -0
  157. package/src/primitives/zone.tsx +660 -12
  158. package/src/reducer.ts +7 -20
  159. package/src/runtime/createPluginRuntimeAPI.ts +1 -1
  160. package/src/types/hex-color.ts +20 -0
  161. package/src/types/player-state.ts +36 -18
  162. package/src/types/plugin-state.ts +10 -3
  163. package/src/ui-contract.ts +253 -21
  164. package/src/utils/interaction-inputs.test.ts +400 -0
  165. package/src/utils/interaction-inputs.ts +113 -11
  166. 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 { useInteractionHandle } from "../hooks/useInteractionHandle.js";
11
- import { useInteractionUiStore } from "../context/InteractionDraftContext.js";
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 { InteractionDescriptor } from "../types/plugin-state.js";
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 { useZoneCardContext } from "./zone.js";
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
- void handle?.submitDraft();
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
- value?: string;
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
- value,
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 cardId = value ?? card ?? zoneCard?.cardId;
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 || !zoneCard) return undefined;
314
- return state.gameplay.zones[zoneCard.zone]?.playableByCardId[cardId]?.find(
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 eligibleTargets =
336
- inputDescriptor?.domain.type === "target"
337
- ? inputDescriptor.domain.eligibleTargets
338
- : undefined;
339
- const isCardInput =
340
- inputDescriptor?.domain.type === "target" &&
341
- inputDescriptor.domain.targetKind === "card";
342
- const isEligible =
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
- !isEligible ||
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": zoneCard?.zone,
657
+ "data-zone": validationZone,
372
658
  "data-selected": isSelected || undefined,
373
- "data-eligible": isEligible,
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": isCandidateAvailable,
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
- handle.setInput(input, nextValue);
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?: string;
34
+ color?: HexColor;
34
35
  index: number;
35
36
  isActive: boolean;
36
37
  isCurrentPlayer: boolean;