@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.
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 +1 -1
  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
@@ -0,0 +1,305 @@
1
+ import { expect, test } from "bun:test";
2
+ import { renderToString } from "react-dom/server";
3
+ import type {
4
+ InteractionDescriptor,
5
+ PluginStateSnapshot,
6
+ } from "../types/plugin-state.js";
7
+ import type { RuntimeAPI, PluginSessionState } from "../types/runtime-api.js";
8
+ import { GameUIProvider } from "./game-ui-provider.js";
9
+ import { Zone } from "./zone.js";
10
+
11
+ function createSessionState(): PluginSessionState {
12
+ return {
13
+ status: "ready",
14
+ sessionId: "session-1",
15
+ controllablePlayerIds: ["player-1"],
16
+ controllingPlayerId: "player-1",
17
+ userId: "user-1",
18
+ };
19
+ }
20
+
21
+ function createSnapshot(): PluginStateSnapshot {
22
+ return {
23
+ view: {},
24
+ gameplay: {
25
+ currentPhase: "setup",
26
+ currentStage: null,
27
+ activePlayers: ["player-1"],
28
+ zones: {
29
+ discard: {
30
+ cardIds: ["card-1", "card-2"],
31
+ cardsById: {
32
+ "card-1": '{"id":"card-1","cardType":"tech","properties":{}}',
33
+ "card-2": '{"id":"card-2","cardType":"tech","properties":{}}',
34
+ },
35
+ playableByCardId: {
36
+ "card-1": [],
37
+ "card-2": [],
38
+ },
39
+ },
40
+ },
41
+ availableInteractions: [],
42
+ },
43
+ lobby: null,
44
+ notifications: [],
45
+ session: createSessionState(),
46
+ history: null,
47
+ syncId: 1,
48
+ };
49
+ }
50
+
51
+ function createCardDescriptor(
52
+ interactionId: string,
53
+ options: Partial<InteractionDescriptor> = {},
54
+ ): InteractionDescriptor {
55
+ return {
56
+ phaseName: "play",
57
+ interactionKey: `play.${interactionId}`,
58
+ interactionId,
59
+ surface: "hand",
60
+ kind: "action",
61
+ label: interactionId,
62
+ zoneId: "discard",
63
+ inputs: [
64
+ {
65
+ key: "cardId",
66
+ kind: "card",
67
+ domain: {
68
+ type: "target",
69
+ targetKind: "card",
70
+ eligibleTargets: ["card-1"],
71
+ },
72
+ },
73
+ ],
74
+ commit: { mode: "manual" },
75
+ available: true,
76
+ ...options,
77
+ };
78
+ }
79
+
80
+ function createRuntime(snapshot: PluginStateSnapshot): RuntimeAPI {
81
+ return {
82
+ validateInteraction: async () => ({ valid: true }),
83
+ submitInteraction: async () => undefined,
84
+ getSessionState: () => snapshot.session,
85
+ disconnect: () => undefined,
86
+ getSnapshot: () => snapshot,
87
+ subscribeToState: () => () => undefined,
88
+ } as RuntimeAPI;
89
+ }
90
+
91
+ test("Zone pile primitives expose projected card state headlessly", () => {
92
+ const html = renderToString(
93
+ <GameUIProvider runtime={createRuntime(createSnapshot())}>
94
+ <Zone.PileRoot zone="discard" label="Discard">
95
+ <Zone.PileLabel />
96
+ <Zone.PileTrigger>
97
+ <Zone.PileCount />
98
+ </Zone.PileTrigger>
99
+ <Zone.PileDescription />
100
+ <Zone.PileCards renderCard={(card) => <span>{card.id}</span>} />
101
+ </Zone.PileRoot>
102
+ </GameUIProvider>,
103
+ );
104
+
105
+ expect(html).toContain('data-dreamboard-zone-root=""');
106
+ expect(html).toContain('data-dreamboard-zone-pile-trigger=""');
107
+ expect(html).toContain('data-card-count="2"');
108
+ expect(html).toContain("Discard");
109
+ expect(html).toContain("2 cards");
110
+ expect(html).not.toContain('data-dreamboard-zone-pile-description=""');
111
+ expect(html).toContain('data-dreamboard-zone-item=""');
112
+ expect(html).toContain('data-card-id="card-1"');
113
+ expect(html).toContain('data-card-id="card-2"');
114
+ });
115
+
116
+ test("Zone.TopCard and Zone.CardAt render positional cards with zone context", () => {
117
+ const html = renderToString(
118
+ <GameUIProvider runtime={createRuntime(createSnapshot())}>
119
+ <Zone.TopCard zone="discard">
120
+ {(card) => <span>top:{card.id}</span>}
121
+ </Zone.TopCard>
122
+ <Zone.Root zone="discard">
123
+ <Zone.CardAt index={0}>
124
+ {(card) => <span>first:{card.id}</span>}
125
+ </Zone.CardAt>
126
+ <Zone.CardAt index={-1}>
127
+ {(card) => <span>bottom:{card.id}</span>}
128
+ </Zone.CardAt>
129
+ </Zone.Root>
130
+ </GameUIProvider>,
131
+ );
132
+
133
+ expect(html).toContain("top:");
134
+ expect(html).toContain("first:");
135
+ expect(html).toContain("bottom:");
136
+ expect(html).toContain('data-card-id="card-1"');
137
+ expect(html).toContain('data-card-id="card-2"');
138
+ });
139
+
140
+ test("Zone.CardAt renders empty for hidden, empty, and out-of-range positions", () => {
141
+ const snapshot = createSnapshot();
142
+ snapshot.gameplay.zones.empty = {
143
+ cardIds: [],
144
+ cardsById: {},
145
+ playableByCardId: {},
146
+ };
147
+
148
+ const html = renderToString(
149
+ <GameUIProvider runtime={createRuntime(snapshot)}>
150
+ <Zone.CardAt zone="empty" index={0} empty={<span>empty-zone</span>} />
151
+ <Zone.CardAt zone="discard" index={3} empty={<span>out-of-range</span>} />
152
+ <Zone.CardAt zone="missing" index={0} empty={<span>hidden-zone</span>} />
153
+ </GameUIProvider>,
154
+ );
155
+
156
+ expect(html).toContain("empty-zone");
157
+ expect(html).toContain("out-of-range");
158
+ expect(html).toContain("hidden-zone");
159
+ });
160
+
161
+ test("Zone.CardAction routes card actions and exposes ambiguity escape hatch", () => {
162
+ const snapshot = createSnapshot();
163
+ snapshot.gameplay.zones.discard.playableByCardId = {
164
+ "card-1": [
165
+ createCardDescriptor("playCard"),
166
+ createCardDescriptor("discardCard"),
167
+ ],
168
+ };
169
+ const runtime = createRuntime(snapshot);
170
+
171
+ const ambiguousHtml = renderToString(
172
+ <GameUIProvider runtime={runtime}>
173
+ <Zone.Root zone="discard">
174
+ <Zone.Item card="card-1">
175
+ <Zone.CardAction>Play</Zone.CardAction>
176
+ </Zone.Item>
177
+ </Zone.Root>
178
+ </GameUIProvider>,
179
+ );
180
+ const explicitHtml = renderToString(
181
+ <GameUIProvider runtime={runtime}>
182
+ <Zone.Root zone="discard">
183
+ <Zone.Item card="card-1">
184
+ <Zone.CardAction interaction="play.playCard">Play</Zone.CardAction>
185
+ </Zone.Item>
186
+ </Zone.Root>
187
+ </GameUIProvider>,
188
+ );
189
+
190
+ expect(ambiguousHtml).toContain('data-ambiguous="true"');
191
+ expect(ambiguousHtml).toContain("disabled=");
192
+ expect(explicitHtml).toContain('data-dreamboard-zone-card-action=""');
193
+ expect(explicitHtml).toContain('data-card-id="card-1"');
194
+ expect(explicitHtml).toContain('data-interaction-key="play.playCard"');
195
+ expect(explicitHtml).toContain('data-input-name="cardId"');
196
+ expect(explicitHtml).not.toContain('data-ambiguous="true"');
197
+ });
198
+
199
+ test("Zone.CardAction explicit input still requires an eligible card target", () => {
200
+ const snapshot = createSnapshot();
201
+ snapshot.gameplay.zones.discard.playableByCardId = {
202
+ "card-1": [
203
+ createCardDescriptor("playCard", {
204
+ inputs: [
205
+ {
206
+ key: "otherCardId",
207
+ kind: "card",
208
+ domain: {
209
+ type: "target",
210
+ targetKind: "card",
211
+ eligibleTargets: ["card-2"],
212
+ },
213
+ },
214
+ ],
215
+ }),
216
+ ],
217
+ };
218
+
219
+ const html = renderToString(
220
+ <GameUIProvider runtime={createRuntime(snapshot)}>
221
+ <Zone.Root zone="discard">
222
+ <Zone.Item card="card-1">
223
+ <Zone.CardAction interaction="play.playCard" input="otherCardId">
224
+ Play
225
+ </Zone.CardAction>
226
+ </Zone.Item>
227
+ </Zone.Root>
228
+ </GameUIProvider>,
229
+ );
230
+
231
+ expect(html).toContain('data-interaction-key="play.playCard"');
232
+ expect(html).toContain('data-input-name="otherCardId"');
233
+ expect(html).toContain('data-eligible="false"');
234
+ expect(html).toContain("disabled=");
235
+ });
236
+
237
+ test("Zone pile primitives hide piles whose zone is not in projection scope", () => {
238
+ // The `deck` zone has no entry in the snapshot — it isn't in the current
239
+ // phase's projection scope. The pile renders with hidden semantics and no
240
+ // PileCards iteration, instead of fabricating items.
241
+ const html = renderToString(
242
+ <GameUIProvider runtime={createRuntime(createSnapshot())}>
243
+ <Zone.PileRoot zone="deck" label="Deck" hiddenDescription="Hidden deck.">
244
+ <Zone.PileTrigger>
245
+ <Zone.PileCount />
246
+ </Zone.PileTrigger>
247
+ <Zone.PileDescription />
248
+ <Zone.PileCards renderCard={(card) => <span>{card.id}</span>} />
249
+ </Zone.PileRoot>
250
+ </GameUIProvider>,
251
+ );
252
+
253
+ expect(html).toContain('data-card-count="0"');
254
+ expect(html).toContain("0 cards");
255
+ expect(html).toContain("Hidden deck.");
256
+ expect(html).not.toContain('data-dreamboard-zone-list=""');
257
+ expect(html).not.toContain('data-dreamboard-zone-item=""');
258
+ });
259
+
260
+ test("Zone pile primitives surface card ids the snapshot withholds as hidden items", () => {
261
+ // Snapshot exposes cardIds but no cardsById entries — e.g. a future
262
+ // visibility mode that surfaces a count and ids without contents. The
263
+ // primitive must not fabricate a `cardType: "unknown"` ViewCard; it should
264
+ // discriminate via `card.hidden === true`.
265
+ const snapshot = createSnapshot();
266
+ snapshot.gameplay.zones.opaque = {
267
+ cardIds: ["card-x"],
268
+ cardsById: {},
269
+ playableByCardId: {},
270
+ };
271
+
272
+ const html = renderToString(
273
+ <GameUIProvider runtime={createRuntime(snapshot)}>
274
+ <Zone.PileRoot zone="opaque" label="Opaque">
275
+ <Zone.PileCards
276
+ renderCard={(card) =>
277
+ card.hidden ? <span>hidden:{card.id}</span> : <span>{card.id}</span>
278
+ }
279
+ />
280
+ </Zone.PileRoot>
281
+ </GameUIProvider>,
282
+ );
283
+
284
+ expect(html).toContain("card-x");
285
+ expect(html).toContain("hidden:");
286
+ expect(html).toContain('data-card-hidden="true"');
287
+ expect(html).not.toContain('data-card-type="unknown"');
288
+ });
289
+
290
+ test("Zone pile descriptions render when configured", () => {
291
+ const html = renderToString(
292
+ <GameUIProvider runtime={createRuntime(createSnapshot())}>
293
+ <Zone.PileRoot
294
+ zone="discard"
295
+ label="Discard"
296
+ visibleDescription={(count) => `${count} visible cards.`}
297
+ >
298
+ <Zone.PileDescription />
299
+ </Zone.PileRoot>
300
+ </GameUIProvider>,
301
+ );
302
+
303
+ expect(html).toContain('data-dreamboard-zone-pile-description=""');
304
+ expect(html).toContain("2 visible cards.");
305
+ });