@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
@@ -4,7 +4,12 @@
4
4
  * Catches React errors and displays a fallback UI
5
5
  */
6
6
 
7
- import { Component, type ReactNode, type ErrorInfo } from "react";
7
+ import {
8
+ Component,
9
+ type CSSProperties,
10
+ type ReactNode,
11
+ type ErrorInfo,
12
+ } from "react";
8
13
  import { AlertTriangle, RefreshCw } from "lucide-react";
9
14
 
10
15
  export interface ErrorBoundaryProps {
@@ -66,35 +71,123 @@ function DefaultErrorFallback({
66
71
  error: Error;
67
72
  onReset: () => void;
68
73
  }) {
74
+ const styles = {
75
+ shell: {
76
+ alignItems: "center",
77
+ background: "#fdfbf7",
78
+ boxSizing: "border-box",
79
+ color: "#0f172a",
80
+ display: "flex",
81
+ justifyContent: "center",
82
+ minHeight: "100vh",
83
+ padding: "24px",
84
+ },
85
+ panel: {
86
+ background: "#fff",
87
+ border: "2px solid #0f172a",
88
+ borderRadius: "12px",
89
+ boxShadow: "6px 6px 0 #111827",
90
+ boxSizing: "border-box",
91
+ maxWidth: "520px",
92
+ padding: "28px",
93
+ width: "100%",
94
+ },
95
+ iconWrap: {
96
+ alignItems: "center",
97
+ background: "#fee2e2",
98
+ border: "2px solid #991b1b",
99
+ borderRadius: "999px",
100
+ display: "flex",
101
+ height: "56px",
102
+ justifyContent: "center",
103
+ marginBottom: "18px",
104
+ width: "56px",
105
+ },
106
+ eyebrow: {
107
+ color: "#991b1b",
108
+ fontSize: "12px",
109
+ fontWeight: 800,
110
+ letterSpacing: "0.08em",
111
+ margin: "0 0 8px",
112
+ textTransform: "uppercase",
113
+ },
114
+ title: {
115
+ fontSize: "28px",
116
+ fontWeight: 900,
117
+ lineHeight: 1.1,
118
+ margin: "0 0 12px",
119
+ },
120
+ body: {
121
+ color: "#475569",
122
+ fontSize: "15px",
123
+ lineHeight: 1.5,
124
+ margin: "0 0 20px",
125
+ },
126
+ details: {
127
+ marginBottom: "22px",
128
+ },
129
+ summary: {
130
+ color: "#334155",
131
+ cursor: "pointer",
132
+ fontSize: "14px",
133
+ fontWeight: 700,
134
+ marginBottom: "8px",
135
+ },
136
+ pre: {
137
+ background: "#f8fafc",
138
+ border: "1px solid #cbd5e1",
139
+ borderRadius: "8px",
140
+ color: "#334155",
141
+ fontSize: "12px",
142
+ lineHeight: 1.45,
143
+ margin: 0,
144
+ maxHeight: "180px",
145
+ overflow: "auto",
146
+ padding: "12px",
147
+ whiteSpace: "pre-wrap",
148
+ },
149
+ button: {
150
+ alignItems: "center",
151
+ background: "#0f172a",
152
+ border: "2px solid #0f172a",
153
+ borderRadius: "8px",
154
+ boxShadow: "3px 3px 0 #111827",
155
+ color: "#fff",
156
+ cursor: "pointer",
157
+ display: "inline-flex",
158
+ fontSize: "15px",
159
+ fontWeight: 800,
160
+ gap: "8px",
161
+ justifyContent: "center",
162
+ padding: "12px 16px",
163
+ width: "100%",
164
+ },
165
+ } satisfies Record<string, CSSProperties>;
166
+
69
167
  return (
70
- <div
71
- className="min-h-screen flex items-center justify-center p-4 sm:p-8 bg-gradient-to-br from-red-50 to-orange-50"
72
- role="alert"
73
- aria-live="assertive"
74
- >
75
- <div className="max-w-md w-full bg-white rounded-2xl shadow-xl p-6 sm:p-8">
76
- <div className="flex items-center justify-center w-16 h-16 mx-auto mb-4 bg-red-100 rounded-full">
168
+ <div style={styles.shell} role="alert" aria-live="assertive">
169
+ <div style={styles.panel}>
170
+ <div style={styles.iconWrap}>
77
171
  <AlertTriangle
78
- size={32}
79
- className="text-red-600"
172
+ size={28}
173
+ color="#991b1b"
174
+ strokeWidth={2.5}
80
175
  aria-hidden="true"
81
176
  />
82
177
  </div>
83
178
 
84
- <h1 className="text-xl sm:text-2xl font-bold text-center text-slate-900 mb-2">
85
- Something went wrong
86
- </h1>
179
+ <p style={styles.eyebrow}>Runtime error</p>
180
+
181
+ <h1 style={styles.title}>Game failed to start</h1>
87
182
 
88
- <p className="text-sm sm:text-base text-slate-600 text-center mb-6">
183
+ <p style={styles.body}>
89
184
  The game encountered an error and couldn&apos;t continue. You can try
90
185
  reloading to start fresh.
91
186
  </p>
92
187
 
93
- <details className="mb-6">
94
- <summary className="cursor-pointer text-sm text-slate-500 hover:text-slate-700 mb-2">
95
- Technical details
96
- </summary>
97
- <pre className="text-xs bg-slate-50 text-slate-700 p-3 rounded-lg overflow-auto max-h-40 border border-slate-200">
188
+ <details style={styles.details}>
189
+ <summary style={styles.summary}>Technical details</summary>
190
+ <pre style={styles.pre}>
98
191
  {error.message}
99
192
  {error.stack && (
100
193
  <>
@@ -105,11 +198,7 @@ function DefaultErrorFallback({
105
198
  </pre>
106
199
  </details>
107
200
 
108
- <button
109
- type="button"
110
- onClick={onReset}
111
- className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white font-medium rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
112
- >
201
+ <button type="button" onClick={onReset} style={styles.button}>
113
202
  <RefreshCw size={18} aria-hidden="true" />
114
203
  Try again
115
204
  </button>
@@ -66,6 +66,7 @@ function renderForm(
66
66
  <InteractionForm
67
67
  descriptor={descriptor}
68
68
  handle={createHandle(descriptor, values)}
69
+ accordion={false}
69
70
  />
70
71
  </ThemeProvider>,
71
72
  );
@@ -117,6 +118,29 @@ test("InteractionForm renders choice select values with icons", () => {
117
118
  expect(html).toContain("Wood");
118
119
  });
119
120
 
121
+ test("InteractionForm renders explicit null choice defaults", () => {
122
+ const descriptor = createDescriptor([
123
+ {
124
+ key: "bonus",
125
+ kind: "form",
126
+ defaultValue: null,
127
+ domain: {
128
+ type: "choice",
129
+ choices: [
130
+ { value: null, label: "No bonus" },
131
+ { value: "coin", label: "Coin" },
132
+ { value: "card", label: "Card" },
133
+ { value: "point", label: "Point" },
134
+ ],
135
+ },
136
+ },
137
+ ]);
138
+
139
+ const html = renderForm(descriptor, { bonus: null });
140
+
141
+ expect(html).toContain("No bonus");
142
+ });
143
+
120
144
  test("InteractionForm renders choice badges and selected descriptions", () => {
121
145
  const descriptor = createDescriptor([
122
146
  {
@@ -30,6 +30,10 @@ import type {
30
30
  InputDomain,
31
31
  } from "../types/plugin-state.js";
32
32
  import { interactionLabel } from "../utils/interaction-labels.js";
33
+ import {
34
+ resolveInputDomain,
35
+ resolveInteractionInputs,
36
+ } from "../utils/interaction-inputs.js";
33
37
  import { useChromeSuppression } from "./ChromeSuppressionContext.js";
34
38
  import { ThemedButton } from "./ThemedButton.js";
35
39
 
@@ -125,12 +129,15 @@ export function InteractionForm<
125
129
  const hidden = useMemo(() => new Set(hiddenFields ?? []), [hiddenFields]);
126
130
  const visibleInputs = useMemo(() => {
127
131
  const allowed = fields ? new Set(fields) : null;
128
- return defaultFormInputs(descriptor).filter((input) => {
132
+ return defaultFormInputs(
133
+ descriptor,
134
+ handle.values as Readonly<Record<string, unknown>>,
135
+ ).filter((input) => {
129
136
  const key = input.key as keyof Params & string;
130
137
  if (allowed && !allowed.has(key)) return false;
131
138
  return !hidden.has(key);
132
139
  });
133
- }, [descriptor, fields, hidden]);
140
+ }, [descriptor, fields, hidden, handle.values]);
134
141
 
135
142
  const currentValidation = validation;
136
143
  const fieldErrors = (currentValidation?.fieldErrors ?? {}) as Partial<
@@ -236,17 +243,13 @@ export function InteractionForm<
236
243
  );
237
244
  })}
238
245
  </div>
239
- ) : (
240
- <span
241
- style={{
242
- fontSize: theme.typography.fontSize.sm,
243
- color: theme.semantic.text.muted,
244
- }}
245
- >
246
- No visible fields are required. Board or card selection may complete
247
- this interaction.
248
- </span>
246
+ ) : null;
247
+
248
+ if (visibleInputs.length === 0 && !handle.isReady) {
249
+ throw new Error(
250
+ `InteractionForm '${descriptor.interactionKey}' has required inputs that cannot be rendered by the default form. Provide renderFields, select on a surface, or use a default-renderable input domain.`,
249
251
  );
252
+ }
250
253
 
251
254
  const formErrorContent =
252
255
  formErrors.length > 0 ? (
@@ -396,7 +399,10 @@ export function InteractionField<
396
399
  (candidate) => candidate.key === inputKey,
397
400
  );
398
401
  if (!input) return null;
399
- const typedInput = input as InteractionInputDescriptor & { key: Key };
402
+ const typedInput = resolveInputDomain(
403
+ input,
404
+ handle.values as Readonly<Record<string, unknown>>,
405
+ ) as InteractionInputDescriptor & { key: Key };
400
406
  const value = handle.values[inputKey] as Params[Key] | undefined;
401
407
  const props: InteractionFieldRenderProps<Params, Key> = {
402
408
  descriptor,
@@ -421,8 +427,9 @@ export function hasDefaultInteractionFormFields(
421
427
 
422
428
  export function defaultFormInputs(
423
429
  descriptor: Pick<InteractionDescriptor, "inputs">,
430
+ values: Readonly<Record<string, unknown>> = {},
424
431
  ): InteractionInputDescriptor[] {
425
- return descriptor.inputs.filter((input) => {
432
+ return resolveInteractionInputs(descriptor, values).filter((input) => {
426
433
  switch (input.domain.type) {
427
434
  case "choice":
428
435
  case "choiceList":
@@ -589,6 +596,21 @@ function ChoiceDescription({ choice }: { choice?: InteractionChoiceOption }) {
589
596
  );
590
597
  }
591
598
 
599
+ const NULL_CHOICE_SELECT_VALUE = "__dreamboard_null_choice__";
600
+
601
+ function choiceRenderKey(choice: InteractionChoiceOption): string {
602
+ return choice.value === null ? NULL_CHOICE_SELECT_VALUE : choice.value;
603
+ }
604
+
605
+ function encodeChoiceSelectValue(value: unknown): string | undefined {
606
+ if (value === null) return NULL_CHOICE_SELECT_VALUE;
607
+ return typeof value === "string" ? value : undefined;
608
+ }
609
+
610
+ function decodeChoiceSelectValue(value: string): string | null {
611
+ return value === NULL_CHOICE_SELECT_VALUE ? null : value;
612
+ }
613
+
592
614
  function ChoiceField<
593
615
  Params extends InteractionParamsShape,
594
616
  Key extends keyof Params & string,
@@ -608,7 +630,7 @@ function ChoiceField<
608
630
  const choices = domain.choices ?? [];
609
631
  const controlId = useId();
610
632
  const selectedChoice =
611
- typeof value === "string"
633
+ typeof value === "string" || value === null
612
634
  ? choices.find((choice) => choice.value === value)
613
635
  : undefined;
614
636
  if (choices.length > 0 && choices.length <= 3) {
@@ -629,7 +651,7 @@ function ChoiceField<
629
651
  const selected = value === choice.value;
630
652
  return (
631
653
  <ThemedButton
632
- key={choice.value}
654
+ key={choiceRenderKey(choice)}
633
655
  type="button"
634
656
  variant={selected ? "primary" : "secondary"}
635
657
  size="sm"
@@ -656,8 +678,10 @@ function ChoiceField<
656
678
  >
657
679
  <Select
658
680
  disabled={disabled}
659
- value={typeof value === "string" ? value : undefined}
660
- onValueChange={(next: string) => setValue(next as Params[Key])}
681
+ value={encodeChoiceSelectValue(value)}
682
+ onValueChange={(next: string) =>
683
+ setValue(decodeChoiceSelectValue(next) as Params[Key])
684
+ }
661
685
  >
662
686
  <SelectTrigger id={controlId} size="sm" className="w-full bg-white">
663
687
  <span data-slot="select-value">
@@ -678,8 +702,8 @@ function ChoiceField<
678
702
  >
679
703
  {choices.map((choice) => (
680
704
  <SelectItem
681
- key={choice.value}
682
- value={choice.value}
705
+ key={choiceRenderKey(choice)}
706
+ value={choiceRenderKey(choice)}
683
707
  textValue={choice.label}
684
708
  disabled={choice.disabled}
685
709
  >
@@ -748,10 +772,11 @@ function ChoiceListField<
748
772
  >
749
773
  <span style={{ display: "flex", flexWrap: "wrap", gap: theme.space[1] }}>
750
774
  {(domain.choices ?? []).map((choice) => {
751
- const checked = selected.has(choice.value);
775
+ const value = choice.value as string;
776
+ const checked = selected.has(value);
752
777
  return (
753
778
  <ThemedButton
754
- key={choice.value}
779
+ key={value}
755
780
  type="button"
756
781
  variant={checked ? "primary" : "secondary"}
757
782
  size="sm"
@@ -762,7 +787,7 @@ function ChoiceListField<
762
787
  }
763
788
  aria-pressed={checked}
764
789
  title={choice.disabledReason ?? choice.description}
765
- onClick={() => toggle(choice.value)}
790
+ onClick={() => toggle(value)}
766
791
  className="h-8 px-3 text-sm"
767
792
  >
768
793
  <ChoiceOptionLabel choice={choice} />
@@ -50,6 +50,10 @@ import type {
50
50
  InteractionParamsShape,
51
51
  } from "../hooks/useInteractionHandle.js";
52
52
  import { interactionLabel } from "../utils/interaction-labels.js";
53
+ import {
54
+ submitInteractionDraft,
55
+ submitInteractionParams,
56
+ } from "../primitives/interaction-submit.js";
53
57
  import { ThemedButton } from "./ThemedButton.js";
54
58
  import type { SubmittedActionConfig } from "./surfaces/internal/DefaultInteractionButton.js";
55
59
 
@@ -289,13 +293,23 @@ export function PrimaryActionButton<
289
293
  setPending(true);
290
294
  try {
291
295
  if (params !== undefined) {
292
- await handle.submit(params as Params);
296
+ await submitInteractionParams(
297
+ handle,
298
+ params as Params,
299
+ {},
300
+ {
301
+ unhandledError: "ignore",
302
+ },
303
+ );
293
304
  } else {
294
- await handle.submitDraft();
305
+ await submitInteractionDraft(
306
+ handle,
307
+ {},
308
+ {
309
+ unhandledError: "ignore",
310
+ },
311
+ );
295
312
  }
296
- } catch {
297
- // Descriptor availability is authoritative; submit errors
298
- // surface via the runtime's error channel.
299
313
  } finally {
300
314
  setPending(false);
301
315
  }
@@ -1,6 +1,5 @@
1
1
  import { expect, test } from "bun:test";
2
2
  import { renderToString } from "react-dom/server";
3
- import { ThemeProvider } from "../theme/ThemeProvider.js";
4
3
  import {
5
4
  ResourceCounter,
6
5
  type ResourceDisplayConfig,
@@ -10,20 +9,21 @@ const resources: ResourceDisplayConfig[] = [
10
9
  { type: "gold", label: "Gold", icon: "🪙" },
11
10
  ];
12
11
 
13
- test("ResourceCounter renders tooltip triggers for each resource chip", () => {
12
+ test("ResourceCounter provides headless resource state to author markup", () => {
14
13
  const html = renderToString(
15
- <ThemeProvider>
16
- <ResourceCounter
17
- resources={resources}
18
- counts={{ gold: 3 }}
19
- layout="row"
20
- size="md"
21
- showZero
22
- />
23
- </ThemeProvider>,
14
+ <ResourceCounter.Root resources={resources} counts={{ gold: 3 }}>
15
+ <ResourceCounter.Item className="author-chip">
16
+ <ResourceCounter.Icon className="author-icon" />
17
+ <ResourceCounter.Count />
18
+ </ResourceCounter.Item>
19
+ </ResourceCounter.Root>,
24
20
  );
25
21
 
26
- expect(html).toContain('data-slot="tooltip-trigger"');
22
+ expect(html).toContain('data-dreamboard-resource-counter=""');
23
+ expect(html).toContain('class="author-chip"');
24
+ expect(html).toContain('class="author-icon"');
25
+ expect(html).toContain('data-resource-id="gold"');
26
+ expect(html).toContain('data-resource-count="3"');
27
27
  expect(html).toContain('aria-label="Gold: 3"');
28
- expect(html).not.toContain("title=");
28
+ expect(html).not.toContain("tooltip");
29
29
  });