@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
@@ -9,14 +9,17 @@ import type {
9
9
  ZoneHandlesSnapshot,
10
10
  } from "../../../types/plugin-state.js";
11
11
  import {
12
- hasInteractionFieldErrors,
13
12
  inputByTarget,
14
- interactionArmScope,
15
13
  interactionInputKeys,
16
- isInputValueReady,
17
14
  toggleManyValue,
18
- validateInteractionInputDomains,
19
15
  } from "../../../utils/interaction-inputs.js";
16
+ import {
17
+ applyInteractionDraftMutation,
18
+ clearInteractionRoute,
19
+ getInteractionDraftReadiness,
20
+ markInteractionPending,
21
+ shouldRouteInteractionPending,
22
+ } from "../../../utils/interaction-router.js";
20
23
  import type {
21
24
  InteractionActionState,
22
25
  InteractionDisabledReason,
@@ -42,9 +45,8 @@ export function useCardZoneInteractions<I extends string = string>(
42
45
  const draftStore = useInteractionUiStore();
43
46
  const draftSnapshot = useStore(draftStore, (state) => state.drafts);
44
47
  const submittingSnapshot = useStore(draftStore, (state) => state.submitting);
45
- const [followUp, setFollowUp] = useState<InteractionDescriptor<I> | null>(
46
- null,
47
- );
48
+ const [routedInteraction, setRoutedInteraction] =
49
+ useState<InteractionDescriptor<I> | null>(null);
48
50
  const zone = usePluginState((s) => s.gameplay.zones?.[zoneId]) as
49
51
  | ZoneHandlesSnapshot<I>
50
52
  | undefined;
@@ -79,11 +81,7 @@ export function useCardZoneInteractions<I extends string = string>(
79
81
  descriptor.interactionId,
80
82
  params,
81
83
  );
82
- draftStore.clearInput(descriptor.interactionKey);
83
- const armScope = interactionArmScope(descriptor);
84
- if (draftStore.getArmed(armScope) === descriptor.interactionKey) {
85
- draftStore.arm(armScope, null);
86
- }
84
+ clearInteractionRoute(draftStore, descriptor);
87
85
  } finally {
88
86
  draftStore.setSubmitting(descriptor.interactionKey, false);
89
87
  }
@@ -111,37 +109,20 @@ export function useCardZoneInteractions<I extends string = string>(
111
109
  cardInput.domain.selection,
112
110
  )
113
111
  : card.id;
114
- const nextDraft: Record<string, unknown> = {
115
- ...currentDraft,
116
- ...(params ?? {}),
117
- [cardInputKey]: nextCardValue,
118
- };
119
- for (const [key, value] of Object.entries(params ?? {})) {
120
- draftStore.setInput(descriptor.interactionKey, key, value);
121
- }
122
- draftStore.setInput(
123
- descriptor.interactionKey,
124
- cardInputKey,
125
- nextCardValue,
126
- );
112
+ const nextDraft = applyInteractionDraftMutation(draftStore, descriptor, [
113
+ ...Object.entries(params ?? {}).map(([key, value]) => ({ key, value })),
114
+ { key: cardInputKey, value: nextCardValue },
115
+ ]);
116
+ const readiness = getInteractionDraftReadiness(descriptor, nextDraft);
127
117
  if (
128
118
  cardInput.domain.type === "target" &&
129
119
  cardInput.domain.selection?.mode === "many"
130
120
  ) {
131
- if (
132
- descriptor.commit.mode === "autoWhenReady" &&
133
- isDraftReady(descriptor, nextDraft)
134
- ) {
121
+ if (descriptor.commit.mode === "autoWhenReady" && readiness.ready) {
135
122
  await submitInteractionDraft(descriptor, nextDraft);
136
123
  }
137
124
  return;
138
125
  }
139
- const remaining = descriptor.inputs.filter((input) => {
140
- const value =
141
- nextDraft[input.key] ??
142
- ("defaultValue" in input ? input.defaultValue : undefined);
143
- return input.key !== cardInputKey && value === undefined;
144
- });
145
126
  const needsBoardTarget = descriptor.inputs.some((input) => {
146
127
  const value =
147
128
  nextDraft[input.key] ??
@@ -153,26 +134,18 @@ export function useCardZoneInteractions<I extends string = string>(
153
134
  );
154
135
  });
155
136
  if (needsBoardTarget) {
156
- setFollowUp(null);
157
- draftStore.arm(
158
- interactionArmScope(descriptor),
159
- descriptor.interactionKey,
160
- );
137
+ setRoutedInteraction(null);
138
+ markInteractionPending(draftStore, descriptor);
139
+ draftStore.setPendingInteraction(null);
161
140
  return;
162
141
  }
163
- if (remaining.length > 0) {
164
- setFollowUp(descriptor);
142
+ if (readiness.missingInputs.length > 0) {
143
+ setRoutedInteraction(descriptor);
165
144
  return;
166
145
  }
167
- if (
168
- !isDraftReady(descriptor, nextDraft) ||
169
- descriptor.commit.mode !== "autoWhenReady"
170
- ) {
171
- setFollowUp(null);
172
- draftStore.arm(
173
- interactionArmScope(descriptor),
174
- descriptor.interactionKey,
175
- );
146
+ if (shouldRouteInteractionPending(descriptor, readiness)) {
147
+ setRoutedInteraction(null);
148
+ markInteractionPending(draftStore, descriptor);
176
149
  return;
177
150
  }
178
151
  await submitInteractionDraft(descriptor, nextDraft);
@@ -252,7 +225,7 @@ export function useCardZoneInteractions<I extends string = string>(
252
225
  zone,
253
226
  ]);
254
227
 
255
- return { cards, contexts, followUp, setFollowUp };
228
+ return { cards, contexts, routedInteraction, setRoutedInteraction };
256
229
  }
257
230
 
258
231
  function isCardSelected(
@@ -283,21 +256,6 @@ function isCardInteractionSelectable(
283
256
  return selection.max === undefined || current.length < selection.max;
284
257
  }
285
258
 
286
- function isDraftReady(
287
- descriptor: InteractionDescriptor,
288
- draft: Readonly<Record<string, unknown>>,
289
- ): boolean {
290
- const fieldErrors = validateInteractionInputDomains(descriptor, draft);
291
- if (hasInteractionFieldErrors(fieldErrors)) return false;
292
- return interactionInputKeys(descriptor).every((key) => {
293
- const input = descriptor.inputs.find((candidate) => candidate.key === key);
294
- const value =
295
- draft[key] ??
296
- (input && "defaultValue" in input ? input.defaultValue : undefined);
297
- return input ? isInputValueReady(input, value) : value !== undefined;
298
- });
299
- }
300
-
301
259
  function resolveCardInput(
302
260
  descriptor: InteractionDescriptor,
303
261
  cardId: string,
@@ -11,10 +11,11 @@ interface DraftState {
11
11
  drafts: Readonly<Record<string, Draft>>;
12
12
  arms: Readonly<Record<string, string>>;
13
13
  submitting: Readonly<Record<string, true>>;
14
+ pendingInteractionKey: string | null;
14
15
  }
15
16
 
16
17
  /**
17
- * Imperative API exposed to `useInteractionHandle` and `<BoardSurface>`.
18
+ * Imperative API exposed to interaction primitives.
18
19
  * Intentionally small; the vanilla zustand store underneath powers
19
20
  * fine-grained subscriptions via {@link useDraft} and {@link useArmed}.
20
21
  */
@@ -31,8 +32,14 @@ export interface InteractionUiStore {
31
32
  getArmed(surface: string): string | null;
32
33
  /** Arm a specific interaction on a surface. Pass `null` to disarm. */
33
34
  arm(surface: string, interactionId: string | null): void;
35
+ /** Which interaction draft currently needs route-owned remaining input UI. */
36
+ getPendingInteraction(): string | null;
37
+ /** Mark the interaction draft currently waiting for remaining input. */
38
+ setPendingInteraction(interactionId: string | null): void;
34
39
  /** True while a local submission is in flight before the host echo arrives. */
35
40
  isSubmitting(interactionId: string): boolean;
41
+ /** Atomically mark a submission in flight. Returns false if already busy. */
42
+ claimSubmitting(interactionId: string): boolean;
36
43
  /** Mark or clear a local submission in flight. */
37
44
  setSubmitting(interactionId: string, submitting: boolean): void;
38
45
  }
@@ -45,6 +52,7 @@ export function createInteractionUiStore(): InteractionUiStoreApi {
45
52
  drafts: {},
46
53
  arms: {},
47
54
  submitting: {},
55
+ pendingInteractionKey: null,
48
56
  }));
49
57
 
50
58
  const api: InteractionUiStore = {
@@ -89,11 +97,17 @@ export function createInteractionUiStore(): InteractionUiStoreApi {
89
97
  if (
90
98
  Object.keys(prev.drafts).length === 0 &&
91
99
  Object.keys(prev.arms).length === 0 &&
92
- Object.keys(prev.submitting).length === 0
100
+ Object.keys(prev.submitting).length === 0 &&
101
+ prev.pendingInteractionKey === null
93
102
  ) {
94
103
  return prev;
95
104
  }
96
- return { drafts: {}, arms: {}, submitting: {} };
105
+ return {
106
+ drafts: {},
107
+ arms: {},
108
+ submitting: {},
109
+ pendingInteractionKey: null,
110
+ };
97
111
  });
98
112
  },
99
113
  getArmed(surface) {
@@ -110,9 +124,30 @@ export function createInteractionUiStore(): InteractionUiStoreApi {
110
124
  return { ...prev, arms: { ...prev.arms, [surface]: interactionId } };
111
125
  });
112
126
  },
127
+ getPendingInteraction() {
128
+ return store.getState().pendingInteractionKey;
129
+ },
130
+ setPendingInteraction(interactionId) {
131
+ store.setState((prev) => {
132
+ if (prev.pendingInteractionKey === interactionId) return prev;
133
+ return { ...prev, pendingInteractionKey: interactionId };
134
+ });
135
+ },
113
136
  isSubmitting(interactionId) {
114
137
  return store.getState().submitting[interactionId] === true;
115
138
  },
139
+ claimSubmitting(interactionId) {
140
+ let claimed = false;
141
+ store.setState((prev) => {
142
+ if (prev.submitting[interactionId] === true) return prev;
143
+ claimed = true;
144
+ return {
145
+ ...prev,
146
+ submitting: { ...prev.submitting, [interactionId]: true },
147
+ };
148
+ });
149
+ return claimed;
150
+ },
116
151
  setSubmitting(interactionId, submitting) {
117
152
  store.setState((prev) => {
118
153
  const current = prev.submitting[interactionId] === true;
@@ -143,7 +178,7 @@ const InteractionUiCtx = createContext<InteractionUiStoreApi | null>(null);
143
178
  * ```tsx
144
179
  * <InteractionUiProvider>
145
180
  * <PanelSurface />
146
- * <BoardSurface>{(ctx) => <Board ctx={ctx} />}</BoardSurface>
181
+ * <Board.Root>{...}</Board.Root>
147
182
  * </InteractionUiProvider>
148
183
  * ```
149
184
  */
@@ -180,12 +215,13 @@ export function useInteractionUiStore(): InteractionUiStoreApi {
180
215
  */
181
216
  export function useInteractionDraft(interactionId: string): Draft {
182
217
  const store = useInteractionUiStore();
183
- return useStore(
218
+ useStore(
184
219
  store,
185
220
  useShallow(
186
221
  (state: DraftState) => state.drafts[interactionId] ?? EMPTY_DRAFT,
187
222
  ),
188
223
  );
224
+ return store.getDraft(interactionId);
189
225
  }
190
226
 
191
227
  /** Subscribe to the armed interaction id on a given surface. */
@@ -194,6 +230,16 @@ export function useArmedInteraction(surface: string): string | null {
194
230
  return useStore(store, (state: DraftState) => state.arms[surface] ?? null);
195
231
  }
196
232
 
233
+ /** Subscribe to the interaction draft currently waiting for pending input. */
234
+ export function usePendingInteractionKey(): string | null {
235
+ const store = useInteractionUiStore();
236
+ const subscribed = useStore(
237
+ store,
238
+ (state: DraftState) => state.pendingInteractionKey ?? null,
239
+ );
240
+ return store.getPendingInteraction() ?? subscribed;
241
+ }
242
+
197
243
  /** Subscribe to local submitting status for a single interaction key. */
198
244
  export function useInteractionSubmitting(interactionId: string): boolean {
199
245
  const store = useInteractionUiStore();
@@ -6,8 +6,6 @@ import type {
6
6
  } from "react";
7
7
  import {
8
8
  Interaction,
9
- Prompt,
10
- PromptInbox,
11
9
  Zone,
12
10
  renderPrimitive,
13
11
  useInteractionPrimitiveContext,
@@ -127,49 +125,6 @@ export const GameLayout = {
127
125
  Bottom: GameLayoutBottom,
128
126
  };
129
127
 
130
- export interface DefaultPromptInboxProps extends PrimitiveCommonProps {
131
- empty?: ReactNode;
132
- renderPrompt?: (prompt: InteractionDescriptor) => ReactNode;
133
- }
134
-
135
- export function DefaultPromptInbox({
136
- empty = "No available prompts.",
137
- renderPrompt,
138
- ...props
139
- }: DefaultPromptInboxProps) {
140
- return (
141
- <PromptInbox.Root>
142
- {renderPrimitive("section", {
143
- ...props,
144
- "data-dreamboard-default-prompt-inbox": "",
145
- children: (
146
- <>
147
- <PromptInbox.Empty>{empty}</PromptInbox.Empty>
148
- <PromptInbox.Items>
149
- {(prompt) =>
150
- renderPrompt ? (
151
- renderPrompt(prompt)
152
- ) : (
153
- <Prompt.Root
154
- key={prompt.interactionKey}
155
- interaction={prompt.interactionKey}
156
- >
157
- <Prompt.Title />
158
- <Prompt.Message />
159
- <div data-dreamboard-default-prompt-options="">
160
- <Prompt.Options />
161
- </div>
162
- </Prompt.Root>
163
- )
164
- }
165
- </PromptInbox.Items>
166
- </>
167
- ),
168
- })}
169
- </PromptInbox.Root>
170
- );
171
- }
172
-
173
128
  export interface DefaultInteractionListProps extends PrimitiveCommonProps {
174
129
  empty?: ReactNode;
175
130
  includeUnavailable?: boolean;
@@ -298,23 +253,30 @@ function DefaultInteractionField({
298
253
  function DefaultChoiceField({ input }: { input: InteractionInputDescriptor }) {
299
254
  const { handle } = useInteractionPrimitiveContext();
300
255
  const value = handle?.draft[input.key] ?? handle?.values[input.key];
256
+ const encodeValue = (candidate: unknown) =>
257
+ candidate === null ? "__dreamboard_null_choice__" : String(candidate);
301
258
  return (
302
259
  <label data-dreamboard-default-interaction-field="">
303
260
  {input.key}
304
261
  <select
305
262
  name={input.key}
306
- value={value === undefined ? "" : String(value)}
263
+ value={value === undefined ? "" : encodeValue(value)}
307
264
  data-dreamboard-default-choice-input=""
308
265
  disabled={!handle?.available}
309
266
  onChange={(event) =>
310
- handle?.setInput(input.key, event.currentTarget.value)
267
+ handle?.setInput(
268
+ input.key,
269
+ event.currentTarget.value === "__dreamboard_null_choice__"
270
+ ? null
271
+ : event.currentTarget.value,
272
+ )
311
273
  }
312
274
  >
313
275
  <option value="" />
314
276
  {(input.domain.choices ?? []).map((choice) => (
315
277
  <option
316
- key={choice.value}
317
- value={choice.value}
278
+ key={encodeValue(choice.value)}
279
+ value={encodeValue(choice.value)}
318
280
  disabled={choice.disabled}
319
281
  >
320
282
  {choice.label}
@@ -399,7 +361,7 @@ function DefaultZoneList({
399
361
  return (
400
362
  <Zone.Item key={cardId} card={cardId}>
401
363
  {input ? (
402
- <Interaction.CardInput input={input} card={cardId}>
364
+ <Interaction.CardInput input={input}>
403
365
  {body}
404
366
  </Interaction.CardInput>
405
367
  ) : (
@@ -7,12 +7,7 @@ import type {
7
7
  } from "../types/plugin-state.js";
8
8
  import type { PluginSessionState, RuntimeAPI } from "../types/runtime-api.js";
9
9
  import { GameUIProvider } from "../primitives/game-ui-provider.js";
10
- import {
11
- DefaultInteractionList,
12
- DefaultPromptInbox,
13
- DefaultZone,
14
- GameLayout,
15
- } from "./index.js";
10
+ import { DefaultInteractionList, DefaultZone, GameLayout } from "./index.js";
16
11
 
17
12
  function createSessionState(): PluginSessionState {
18
13
  return {
@@ -66,50 +61,6 @@ function renderWithRuntime(
66
61
  );
67
62
  }
68
63
 
69
- test("DefaultPromptInbox composes PromptInbox and Prompt primitives", () => {
70
- const prompt: InteractionDescriptor = {
71
- phaseName: "trading",
72
- interactionKey: "trading.respond",
73
- interactionId: "respond",
74
- kind: "prompt",
75
- inputs: [
76
- {
77
- key: "response",
78
- kind: "choice",
79
- domain: {
80
- type: "choice",
81
- choices: [
82
- { value: "accept", label: "Accept" },
83
- { value: "decline", label: "Decline" },
84
- ],
85
- },
86
- },
87
- ],
88
- commit: { mode: "manual" },
89
- available: true,
90
- context: {
91
- to: "player-1",
92
- title: "Trade offer",
93
- payload: { message: "Two wheat for one ore?" },
94
- options: [
95
- { id: "accept", label: "Accept" },
96
- { id: "decline", label: "Decline" },
97
- ],
98
- },
99
- };
100
-
101
- const html = renderWithRuntime(
102
- { availableInteractions: [prompt] },
103
- <DefaultPromptInbox />,
104
- );
105
-
106
- expect(html).toContain('data-dreamboard-default-prompt-inbox=""');
107
- expect(html).toContain("Trade offer");
108
- expect(html).toContain("Two wheat for one ore?");
109
- expect(html).toContain('data-dreamboard-prompt-option=""');
110
- expect(html).toContain('data-option-value="accept"');
111
- });
112
-
113
64
  test("DefaultInteractionList renders action items without descriptor style metadata", () => {
114
65
  const ready: InteractionDescriptor = {
115
66
  phaseName: "setup",