@dreamboard-games/sdk 0.2.0 → 0.2.1-alpha.1

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 (90) hide show
  1. package/dist/{ThemeProvider-fy0_QzgO.d.ts → ThemeProvider-BBMVT3KG.d.ts} +1 -1
  2. package/dist/attributes-BeRyboMS.d.ts +279 -0
  3. package/dist/browser-interaction.d.ts +708 -0
  4. package/dist/browser-interaction.js +106 -0
  5. package/dist/browser-interaction.js.map +1 -0
  6. package/dist/{bundle-TIZcw8LB.d.ts → bundle-CDd5FKeD.d.ts} +3 -1
  7. package/dist/{chunk-U5C6BONG.js → chunk-326PGVAA.js} +2 -2
  8. package/dist/{chunk-VFTAA4WO.js → chunk-MKXPVOUT.js} +4 -2
  9. package/dist/chunk-MKXPVOUT.js.map +1 -0
  10. package/dist/{chunk-GKKBPPSW.js → chunk-MZNVHMJ5.js} +4 -4
  11. package/dist/{chunk-KAELH4KC.js → chunk-NKCRKGR2.js} +2 -2
  12. package/dist/{chunk-WN74KVNY.js → chunk-PEI3FIL2.js} +2 -2
  13. package/dist/chunk-PEI3FIL2.js.map +1 -0
  14. package/dist/chunk-QLG6VEMW.js +1691 -0
  15. package/dist/chunk-QLG6VEMW.js.map +1 -0
  16. package/dist/{chunk-WYPQ3GG5.js → chunk-WG4JQL3S.js} +4 -1
  17. package/dist/{chunk-WYPQ3GG5.js.map → chunk-WG4JQL3S.js.map} +1 -1
  18. package/dist/{chunk-7YAHLYBR.js → chunk-XV6D3ET4.js} +8 -4
  19. package/dist/{chunk-7YAHLYBR.js.map → chunk-XV6D3ET4.js.map} +1 -1
  20. package/dist/{chunk-TDSWKVZ4.js → chunk-ZABVH7AO.js} +1236 -17
  21. package/dist/chunk-ZABVH7AO.js.map +1 -0
  22. package/dist/{components-D5ZRE2Hl.d.ts → components-BoiVSYqx.d.ts} +1 -1
  23. package/dist/generated/runtime/primitives.d.ts +5 -4
  24. package/dist/generated/runtime/primitives.js +4 -3
  25. package/dist/generated/runtime-api.d.ts +1 -1
  26. package/dist/generated/runtime.d.ts +5 -4
  27. package/dist/generated/runtime.js +7 -6
  28. package/dist/generated/workspace-contract.d.ts +5 -4
  29. package/dist/generated/workspace-contract.js +6 -5
  30. package/dist/{hex-board-view-D_07hO6O.d.ts → hex-board-view-1iAyJRFn.d.ts} +1 -0
  31. package/dist/index.js +1 -1
  32. package/dist/infrastructure/reducer-bundle-abi.d.ts +113 -113
  33. package/dist/infrastructure/reducer-bundle-abi.js +1 -1
  34. package/dist/package-set.d.ts +2 -2
  35. package/dist/package-set.js +1 -1
  36. package/dist/reducer.d.ts +1 -1
  37. package/dist/reducer.js +305 -12
  38. package/dist/reducer.js.map +1 -1
  39. package/dist/runtime/primitives.d.ts +6 -5
  40. package/dist/runtime/primitives.js +4 -3
  41. package/dist/runtime/workspace-contract.d.ts +6 -5
  42. package/dist/runtime/workspace-contract.js +6 -5
  43. package/dist/{runtime-api-DWxvTr-O.d.ts → runtime-api-CPLm_XDG.d.ts} +6 -0
  44. package/dist/runtime.d.ts +5 -4
  45. package/dist/runtime.js +6 -5
  46. package/dist/testing.d.ts +2 -2
  47. package/dist/ui/components.d.ts +2 -2
  48. package/dist/ui/components.js +1 -1
  49. package/dist/{ui-contract-iQfTtUSL.d.ts → ui-contract-rzKBwOLC.d.ts} +5 -3
  50. package/dist/ui.d.ts +5 -5
  51. package/dist/ui.js +2 -2
  52. package/package.json +15 -9
  53. package/src/browser-interaction/attributes.ts +211 -0
  54. package/src/browser-interaction/canonical.ts +77 -0
  55. package/src/browser-interaction/constants.ts +77 -0
  56. package/src/browser-interaction/effects.ts +176 -0
  57. package/src/browser-interaction/index.ts +111 -0
  58. package/src/browser-interaction/normalize.ts +997 -0
  59. package/src/browser-interaction/registry.ts +70 -0
  60. package/src/browser-interaction/resolve.ts +596 -0
  61. package/src/browser-interaction/schemas.ts +152 -0
  62. package/src/browser-interaction/types.ts +304 -0
  63. package/src/browser-interaction.ts +1 -0
  64. package/src/generated/reducer-contract/wire.ts +1 -1
  65. package/src/generated/reducer-contract/zod.ts +3 -1
  66. package/src/package-set.ts +1 -1
  67. package/src/reducer/bundle/ingress-bundle.ts +1 -1
  68. package/src/reducer/bundle/trusted/interaction-types.ts +3 -0
  69. package/src/reducer/bundle/trusted/projection-builder.ts +337 -13
  70. package/src/reducer/ingress/input-codec.ts +1 -1
  71. package/src/reducer/ingress/session-codec.ts +1 -1
  72. package/src/runtime-internal/components/InteractionForm.tsx +345 -7
  73. package/src/runtime-internal/components/PluginRuntime.tsx +2 -0
  74. package/src/runtime-internal/components/board/target-layer.ts +2 -0
  75. package/src/runtime-internal/context/PluginStateContext.tsx +41 -0
  76. package/src/runtime-internal/hooks/useBoardInteractions.ts +73 -11
  77. package/src/runtime-internal/primitives/board.tsx +71 -0
  78. package/src/runtime-internal/primitives/interaction.tsx +160 -1
  79. package/src/runtime-internal/types/plugin-state.ts +6 -0
  80. package/src/runtime-internal/utils/browser-interaction-effects.ts +240 -0
  81. package/src/runtime-internal/utils/interaction-draft-digest.ts +252 -0
  82. package/src/runtime-internal/utils/semantic-projection-digest.ts +407 -0
  83. package/src/ui/components/board/HexGrid.tsx +3 -0
  84. package/src/ui/components/board/target-layer.ts +1 -0
  85. package/dist/chunk-TDSWKVZ4.js.map +0 -1
  86. package/dist/chunk-VFTAA4WO.js.map +0 -1
  87. package/dist/chunk-WN74KVNY.js.map +0 -1
  88. /package/dist/{chunk-U5C6BONG.js.map → chunk-326PGVAA.js.map} +0 -0
  89. /package/dist/{chunk-GKKBPPSW.js.map → chunk-MZNVHMJ5.js.map} +0 -0
  90. /package/dist/{chunk-KAELH4KC.js.map → chunk-NKCRKGR2.js.map} +0 -0
@@ -20,6 +20,14 @@ import {
20
20
  useTheme,
21
21
  useThemeCssVars,
22
22
  } from "../../ui.js";
23
+ import {
24
+ createGameplayActuatorAttributes,
25
+ createGameplayInteractionRootAttributes,
26
+ type GameplayActuatorAttributesInput,
27
+ type BrowserInteractionAttributeMap,
28
+ type BrowserInteractionCandidateState,
29
+ type GameplayBrowserInteractionIntent,
30
+ } from "../../browser-interaction/index.js";
23
31
  import type {
24
32
  DraftValidation,
25
33
  InteractionHandle,
@@ -40,7 +48,16 @@ import {
40
48
  resolveInteractionInputs,
41
49
  toggleManyValue,
42
50
  } from "../utils/interaction-inputs.js";
51
+ import { interactionDraftDigestForValues } from "../utils/interaction-draft-digest.js";
43
52
  import { isInteractionAvailable } from "../utils/interaction-status.js";
53
+ import {
54
+ gameplayCandidateMetadata,
55
+ gameplayPreparationPatternsForDescriptor,
56
+ gameplayResourceMetadata,
57
+ gameplayScalarFillMetadata,
58
+ gameplayScalarStepMetadata,
59
+ gameplaySubmitMetadata,
60
+ } from "../utils/browser-interaction-effects.js";
44
61
  import { useChromeSuppression, ThemedButton } from "../../ui.js";
45
62
 
46
63
  export interface InteractionFieldRenderProps<
@@ -163,6 +180,92 @@ export interface InteractionFormProps<
163
180
  }
164
181
 
165
182
  const EMPTY_FIELD_ERRORS: readonly string[] = [];
183
+ const GAMEPLAY_BROWSER_SCOPE_ID = "runtime";
184
+
185
+ function gameplayInteractionRootAttributes({
186
+ descriptor,
187
+ draftValues,
188
+ ready,
189
+ available,
190
+ }: {
191
+ descriptor: InteractionDescriptor;
192
+ draftValues?: Readonly<Record<string, unknown>>;
193
+ ready: boolean;
194
+ available: boolean;
195
+ }): BrowserInteractionAttributeMap {
196
+ return createGameplayInteractionRootAttributes({
197
+ scopeId: GAMEPLAY_BROWSER_SCOPE_ID,
198
+ interactionKey: descriptor.interactionKey,
199
+ interactionId: descriptor.interactionId,
200
+ ...(descriptor.descriptorDigest !== undefined
201
+ ? { descriptorDigest: descriptor.descriptorDigest }
202
+ : {}),
203
+ ...(descriptor.draftDigest !== undefined
204
+ ? {
205
+ draftDigest: interactionDraftDigestForValues(
206
+ descriptor,
207
+ draftValues ?? {},
208
+ ),
209
+ }
210
+ : {}),
211
+ readiness: available ? (ready ? "ready" : "blocked") : "unavailable",
212
+ });
213
+ }
214
+
215
+ function gameplayActuatorAttributes({
216
+ descriptor,
217
+ inputKey,
218
+ intent,
219
+ candidateValue,
220
+ candidateState,
221
+ semanticEffects,
222
+ acceptedEffectPatterns,
223
+ preparationPatterns,
224
+ draftValues,
225
+ enabled,
226
+ actuatorKind,
227
+ actuatorId,
228
+ }: {
229
+ descriptor: InteractionDescriptor;
230
+ inputKey?: string;
231
+ intent: GameplayBrowserInteractionIntent;
232
+ candidateValue?: unknown;
233
+ candidateState?: BrowserInteractionCandidateState;
234
+ semanticEffects?: GameplayActuatorAttributesInput["semanticEffects"];
235
+ acceptedEffectPatterns?: GameplayActuatorAttributesInput["acceptedEffectPatterns"];
236
+ preparationPatterns?: GameplayActuatorAttributesInput["preparationPatterns"];
237
+ draftValues?: Readonly<Record<string, unknown>>;
238
+ enabled: boolean;
239
+ actuatorKind: GameplayActuatorAttributesInput["actuatorKind"];
240
+ actuatorId: string;
241
+ }): BrowserInteractionAttributeMap {
242
+ return createGameplayActuatorAttributes({
243
+ scopeId: GAMEPLAY_BROWSER_SCOPE_ID,
244
+ interactionKey: descriptor.interactionKey,
245
+ interactionId: descriptor.interactionId,
246
+ intent,
247
+ enabled,
248
+ actuatorKind,
249
+ actuatorId,
250
+ ...(descriptor.descriptorDigest !== undefined
251
+ ? { descriptorDigest: descriptor.descriptorDigest }
252
+ : {}),
253
+ ...(descriptor.draftDigest !== undefined
254
+ ? {
255
+ draftDigest: interactionDraftDigestForValues(
256
+ descriptor,
257
+ draftValues ?? {},
258
+ ),
259
+ }
260
+ : {}),
261
+ ...(inputKey !== undefined ? { inputKey } : {}),
262
+ ...(candidateValue !== undefined ? { candidateValue } : {}),
263
+ ...(candidateState !== undefined ? { candidateState } : {}),
264
+ ...(semanticEffects !== undefined ? { semanticEffects } : {}),
265
+ ...(acceptedEffectPatterns !== undefined ? { acceptedEffectPatterns } : {}),
266
+ ...(preparationPatterns !== undefined ? { preparationPatterns } : {}),
267
+ });
268
+ }
166
269
 
167
270
  export function InteractionForm<
168
271
  Params extends InteractionParamsShape = InteractionParamsShape,
@@ -227,6 +330,34 @@ export function InteractionForm<
227
330
  ];
228
331
  const isDisabled = disabled || pending || !isInteractionAvailable(descriptor);
229
332
  const useAccordion = accordion && visibleInputs.length > 0;
333
+ const rootBrowserAttributes = gameplayInteractionRootAttributes({
334
+ descriptor,
335
+ draftValues: handle.values as Readonly<Record<string, unknown>>,
336
+ ready: handle.isReady,
337
+ available: !isDisabled,
338
+ });
339
+ const armBrowserAttributes = gameplayActuatorAttributes({
340
+ descriptor,
341
+ draftValues: handle.values as Readonly<Record<string, unknown>>,
342
+ intent: "arm",
343
+ enabled: !isDisabled,
344
+ actuatorKind: "click",
345
+ actuatorId: "arm",
346
+ preparationPatterns: gameplayPreparationPatternsForDescriptor(
347
+ descriptor,
348
+ handle.values as Readonly<Record<string, unknown>>,
349
+ ),
350
+ });
351
+ const submitMetadata = gameplaySubmitMetadata({ descriptor });
352
+ const submitBrowserAttributes = gameplayActuatorAttributes({
353
+ descriptor,
354
+ draftValues: handle.values as Readonly<Record<string, unknown>>,
355
+ intent: submitMetadata.intent,
356
+ enabled: !isDisabled && handle.isReady,
357
+ actuatorKind: "click",
358
+ actuatorId: "submit",
359
+ semanticEffects: submitMetadata.semanticEffects,
360
+ });
230
361
 
231
362
  useEffect(() => {
232
363
  setAccordionOpen(defaultOpen);
@@ -391,6 +522,7 @@ export function InteractionForm<
391
522
  size="sm"
392
523
  disabled={isDisabled}
393
524
  className="h-9 px-3 text-sm"
525
+ {...submitBrowserAttributes}
394
526
  {...buttonProps}
395
527
  >
396
528
  {pending
@@ -408,6 +540,7 @@ export function InteractionForm<
408
540
  size="sm"
409
541
  disabled={isDisabled}
410
542
  className="h-9 px-3 text-sm"
543
+ {...submitBrowserAttributes}
411
544
  >
412
545
  {pending ? "Submitting..." : (submitLabel ?? fallbackLabel)}
413
546
  </ThemedButton>
@@ -429,6 +562,7 @@ export function InteractionForm<
429
562
  data-interaction-id={descriptor.interactionId}
430
563
  onSubmit={(event) => void submit(event)}
431
564
  style={containerStyle}
565
+ {...rootBrowserAttributes}
432
566
  >
433
567
  {useAccordion ? (
434
568
  <AccordionPrimitive.Root
@@ -442,6 +576,7 @@ export function InteractionForm<
442
576
  <AccordionPrimitive.Item value="fields">
443
577
  <AccordionPrimitive.Header style={{ margin: 0 }}>
444
578
  <AccordionPrimitive.Trigger
579
+ {...armBrowserAttributes}
445
580
  style={{
446
581
  alignItems: "center",
447
582
  appearance: "none",
@@ -543,6 +678,24 @@ function createInteractionInputSlot<
543
678
  kind === "card"
544
679
  ? { "data-dreamboard-interaction-card-slot": "" }
545
680
  : { "data-dreamboard-interaction-target-slot": "" };
681
+ const browserAttributes = gameplayActuatorAttributes({
682
+ descriptor,
683
+ draftValues: handle.values as Readonly<Record<string, unknown>>,
684
+ inputKey: input.key,
685
+ intent: selection?.mode === "many" ? "toggle" : "select",
686
+ candidateValue: targetValue,
687
+ candidateState: selected ? "selected" : "unselected",
688
+ enabled: !isDisabled,
689
+ actuatorKind: "click",
690
+ actuatorId: `${kind}:${input.key}:${targetValue}`,
691
+ semanticEffects: gameplayCandidateMetadata({
692
+ descriptor,
693
+ draftValues: handle.values as Readonly<Record<string, unknown>>,
694
+ inputKey: input.key,
695
+ candidateValue: targetValue,
696
+ intent: selection?.mode === "many" ? "toggle" : "select",
697
+ }).semanticEffects,
698
+ });
546
699
  return (
547
700
  <button
548
701
  type="button"
@@ -556,6 +709,7 @@ function createInteractionInputSlot<
556
709
  data-selected={selected || undefined}
557
710
  data-disabled={isDisabled || undefined}
558
711
  {...dataAttribute}
712
+ {...browserAttributes}
559
713
  {...buttonProps}
560
714
  onClick={() => {
561
715
  if (isDisabled) return;
@@ -588,6 +742,26 @@ function createInteractionInputSlot<
588
742
  Default: ({ children }) => {
589
743
  const hasDefault = "defaultValue" in input;
590
744
  const isDisabled = disabled || !hasDefault;
745
+ const browserAttributes = gameplayActuatorAttributes({
746
+ descriptor,
747
+ draftValues: handle.values as Readonly<Record<string, unknown>>,
748
+ inputKey: input.key,
749
+ intent: "select",
750
+ candidateValue: input.defaultValue,
751
+ candidateState: "unselected",
752
+ enabled: !isDisabled,
753
+ actuatorKind: "click",
754
+ actuatorId: `default:${input.key}`,
755
+ semanticEffects: hasDefault
756
+ ? gameplayCandidateMetadata({
757
+ descriptor,
758
+ draftValues: handle.values as Readonly<Record<string, unknown>>,
759
+ inputKey: input.key,
760
+ candidateValue: input.defaultValue,
761
+ intent: "select",
762
+ }).semanticEffects
763
+ : undefined,
764
+ });
591
765
  return (
592
766
  <button
593
767
  type="button"
@@ -596,6 +770,7 @@ function createInteractionInputSlot<
596
770
  data-dreamboard-interaction-default-slot=""
597
771
  data-input-name={input.key}
598
772
  data-disabled={isDisabled || undefined}
773
+ {...browserAttributes}
599
774
  onClick={() => {
600
775
  if (isDisabled) return;
601
776
  handle.setInput(input.key, input.defaultValue as Params[Key]);
@@ -869,6 +1044,8 @@ function ChoiceField<
869
1044
  Params extends InteractionParamsShape,
870
1045
  Key extends keyof Params & string,
871
1046
  >({
1047
+ descriptor,
1048
+ handle,
872
1049
  input,
873
1050
  value,
874
1051
  setValue,
@@ -903,15 +1080,38 @@ function ChoiceField<
903
1080
  >
904
1081
  {choices.map((choice) => {
905
1082
  const selected = value === choice.value;
1083
+ const isDisabled = disabled || choice.disabled;
906
1084
  return (
907
1085
  <ThemedButton
908
1086
  key={choiceRenderKey(choice)}
909
1087
  type="button"
910
1088
  variant={selected ? "primary" : "secondary"}
911
1089
  size="sm"
912
- disabled={disabled || choice.disabled}
1090
+ disabled={isDisabled}
913
1091
  aria-pressed={selected}
914
1092
  title={choice.disabledReason ?? choice.description}
1093
+ {...gameplayActuatorAttributes({
1094
+ descriptor,
1095
+ draftValues: handle.values as Readonly<
1096
+ Record<string, unknown>
1097
+ >,
1098
+ inputKey: input.key,
1099
+ intent: "select",
1100
+ candidateValue: choice.value,
1101
+ candidateState: selected ? "selected" : "unselected",
1102
+ enabled: !isDisabled,
1103
+ actuatorKind: "click",
1104
+ actuatorId: `choice:${input.key}:${choiceRenderKey(choice)}`,
1105
+ semanticEffects: gameplayCandidateMetadata({
1106
+ descriptor,
1107
+ draftValues: handle.values as Readonly<
1108
+ Record<string, unknown>
1109
+ >,
1110
+ inputKey: input.key,
1111
+ candidateValue: choice.value,
1112
+ intent: "select",
1113
+ }).semanticEffects,
1114
+ })}
915
1115
  onClick={() => setValue(choice.value as Params[Key])}
916
1116
  className="h-8 px-3 text-sm"
917
1117
  >
@@ -937,7 +1137,24 @@ function ChoiceField<
937
1137
  setValue(decodeChoiceSelectValue(next) as Params[Key])
938
1138
  }
939
1139
  >
940
- <SelectTrigger id={controlId} size="sm" className="w-full bg-white">
1140
+ <SelectTrigger
1141
+ id={controlId}
1142
+ size="sm"
1143
+ className="w-full bg-white"
1144
+ {...gameplayActuatorAttributes({
1145
+ descriptor,
1146
+ draftValues: handle.values as Readonly<Record<string, unknown>>,
1147
+ inputKey: input.key,
1148
+ intent: "reveal",
1149
+ enabled: !disabled,
1150
+ actuatorKind: "click",
1151
+ actuatorId: `choice-reveal:${input.key}`,
1152
+ preparationPatterns: gameplayPreparationPatternsForDescriptor(
1153
+ { inputs: [input] },
1154
+ handle.values as Readonly<Record<string, unknown>>,
1155
+ ),
1156
+ })}
1157
+ >
941
1158
  <span data-slot="select-value">
942
1159
  {selectedChoice ? (
943
1160
  <ChoiceOptionLabel choice={selectedChoice} />
@@ -960,6 +1177,27 @@ function ChoiceField<
960
1177
  value={choiceRenderKey(choice)}
961
1178
  textValue={choice.label}
962
1179
  disabled={choice.disabled}
1180
+ {...gameplayActuatorAttributes({
1181
+ descriptor,
1182
+ draftValues: handle.values as Readonly<Record<string, unknown>>,
1183
+ inputKey: input.key,
1184
+ intent: "select",
1185
+ candidateValue: choice.value,
1186
+ candidateState:
1187
+ value === choice.value ? "selected" : "unselected",
1188
+ enabled: !disabled && !choice.disabled,
1189
+ actuatorKind: "click",
1190
+ actuatorId: `choice:${input.key}:${choiceRenderKey(choice)}`,
1191
+ semanticEffects: gameplayCandidateMetadata({
1192
+ descriptor,
1193
+ draftValues: handle.values as Readonly<
1194
+ Record<string, unknown>
1195
+ >,
1196
+ inputKey: input.key,
1197
+ candidateValue: choice.value,
1198
+ intent: "select",
1199
+ }).semanticEffects,
1200
+ })}
963
1201
  >
964
1202
  <ChoiceOptionLabel choice={choice} />
965
1203
  </SelectItem>
@@ -975,6 +1213,8 @@ function ChoiceListField<
975
1213
  Params extends InteractionParamsShape,
976
1214
  Key extends keyof Params & string,
977
1215
  >({
1216
+ descriptor,
1217
+ handle,
978
1218
  input,
979
1219
  value,
980
1220
  setValue,
@@ -1028,19 +1268,37 @@ function ChoiceListField<
1028
1268
  {(domain.choices ?? []).map((choice) => {
1029
1269
  const value = choice.value as string;
1030
1270
  const checked = selected.has(value);
1271
+ const isDisabled =
1272
+ disabled || choice.disabled || (!checked && selected.size >= max);
1031
1273
  return (
1032
1274
  <ThemedButton
1033
1275
  key={value}
1034
1276
  type="button"
1035
1277
  variant={checked ? "primary" : "secondary"}
1036
1278
  size="sm"
1037
- disabled={
1038
- disabled ||
1039
- choice.disabled ||
1040
- (!checked && selected.size >= max)
1041
- }
1279
+ disabled={isDisabled}
1042
1280
  aria-pressed={checked}
1043
1281
  title={choice.disabledReason ?? choice.description}
1282
+ {...gameplayActuatorAttributes({
1283
+ descriptor,
1284
+ draftValues: handle.values as Readonly<Record<string, unknown>>,
1285
+ inputKey: input.key,
1286
+ intent: "toggle",
1287
+ candidateValue: value,
1288
+ candidateState: checked ? "selected" : "unselected",
1289
+ enabled: !isDisabled,
1290
+ actuatorKind: "click",
1291
+ actuatorId: `choice-list:${input.key}:${value}`,
1292
+ semanticEffects: gameplayCandidateMetadata({
1293
+ descriptor,
1294
+ draftValues: handle.values as Readonly<
1295
+ Record<string, unknown>
1296
+ >,
1297
+ inputKey: input.key,
1298
+ candidateValue: value,
1299
+ intent: "toggle",
1300
+ }).semanticEffects,
1301
+ })}
1044
1302
  onClick={() => toggle(value)}
1045
1303
  className="h-8 px-3 text-sm"
1046
1304
  >
@@ -1057,6 +1315,8 @@ function ResourceMapField<
1057
1315
  Params extends InteractionParamsShape,
1058
1316
  Key extends keyof Params & string,
1059
1317
  >({
1318
+ descriptor,
1319
+ handle,
1060
1320
  input,
1061
1321
  value,
1062
1322
  setValue,
@@ -1123,6 +1383,23 @@ function ResourceMapField<
1123
1383
  <StepperButton
1124
1384
  label={`Decrease ${resource.label ?? resource.resourceId}`}
1125
1385
  disabled={disabled || amount <= min}
1386
+ browserAttributes={gameplayActuatorAttributes({
1387
+ descriptor,
1388
+ draftValues: handle.values as Readonly<
1389
+ Record<string, unknown>
1390
+ >,
1391
+ inputKey: input.key,
1392
+ intent: "decrement",
1393
+ candidateValue: resource.resourceId,
1394
+ enabled: !(disabled || amount <= min),
1395
+ actuatorKind: "click",
1396
+ actuatorId: `resource-decrement:${input.key}:${resource.resourceId}`,
1397
+ semanticEffects: gameplayResourceMetadata({
1398
+ inputKey: input.key,
1399
+ resourceKey: resource.resourceId,
1400
+ delta: -1,
1401
+ }).semanticEffects,
1402
+ })}
1126
1403
  onClick={() => update(resource.resourceId, -1, min, max)}
1127
1404
  >
1128
1405
  -
@@ -1139,6 +1416,23 @@ function ResourceMapField<
1139
1416
  <StepperButton
1140
1417
  label={`Increase ${resource.label ?? resource.resourceId}`}
1141
1418
  disabled={disabled || amount >= max}
1419
+ browserAttributes={gameplayActuatorAttributes({
1420
+ descriptor,
1421
+ draftValues: handle.values as Readonly<
1422
+ Record<string, unknown>
1423
+ >,
1424
+ inputKey: input.key,
1425
+ intent: "increment",
1426
+ candidateValue: resource.resourceId,
1427
+ enabled: !(disabled || amount >= max),
1428
+ actuatorKind: "click",
1429
+ actuatorId: `resource-increment:${input.key}:${resource.resourceId}`,
1430
+ semanticEffects: gameplayResourceMetadata({
1431
+ inputKey: input.key,
1432
+ resourceKey: resource.resourceId,
1433
+ delta: 1,
1434
+ }).semanticEffects,
1435
+ })}
1142
1436
  onClick={() => update(resource.resourceId, 1, min, max)}
1143
1437
  >
1144
1438
  +
@@ -1155,6 +1449,8 @@ function BoundedNumberField<
1155
1449
  Params extends InteractionParamsShape,
1156
1450
  Key extends keyof Params & string,
1157
1451
  >({
1452
+ descriptor,
1453
+ handle,
1158
1454
  input,
1159
1455
  value,
1160
1456
  setValue,
@@ -1190,6 +1486,19 @@ function BoundedNumberField<
1190
1486
  <StepperButton
1191
1487
  label={`Decrease ${input.key}`}
1192
1488
  disabled={disabled || current <= min}
1489
+ browserAttributes={gameplayActuatorAttributes({
1490
+ descriptor,
1491
+ draftValues: handle.values as Readonly<Record<string, unknown>>,
1492
+ inputKey: input.key,
1493
+ intent: "decrement",
1494
+ enabled: !(disabled || current <= min),
1495
+ actuatorKind: "click",
1496
+ actuatorId: `bounded-decrement:${input.key}`,
1497
+ semanticEffects: gameplayScalarStepMetadata({
1498
+ inputKey: input.key,
1499
+ value: Math.max(min, Math.min(max, current - step)),
1500
+ }).semanticEffects,
1501
+ })}
1193
1502
  onClick={() => update(current - step)}
1194
1503
  >
1195
1504
  -
@@ -1202,12 +1511,38 @@ function BoundedNumberField<
1202
1511
  step={step}
1203
1512
  value={current}
1204
1513
  disabled={disabled}
1514
+ {...gameplayActuatorAttributes({
1515
+ descriptor,
1516
+ draftValues: handle.values as Readonly<Record<string, unknown>>,
1517
+ inputKey: input.key,
1518
+ intent: "fill",
1519
+ enabled: !disabled,
1520
+ actuatorKind: "fill",
1521
+ actuatorId: `bounded-fill:${input.key}`,
1522
+ acceptedEffectPatterns: gameplayScalarFillMetadata({
1523
+ inputKey: input.key,
1524
+ domain,
1525
+ }).acceptedEffectPatterns,
1526
+ })}
1205
1527
  onChange={(event) => update(Number(event.target.value))}
1206
1528
  className="h-9 w-[8ch] px-2 text-center text-sm md:text-sm"
1207
1529
  />
1208
1530
  <StepperButton
1209
1531
  label={`Increase ${input.key}`}
1210
1532
  disabled={disabled || current >= max}
1533
+ browserAttributes={gameplayActuatorAttributes({
1534
+ descriptor,
1535
+ draftValues: handle.values as Readonly<Record<string, unknown>>,
1536
+ inputKey: input.key,
1537
+ intent: "increment",
1538
+ enabled: !(disabled || current >= max),
1539
+ actuatorKind: "click",
1540
+ actuatorId: `bounded-increment:${input.key}`,
1541
+ semanticEffects: gameplayScalarStepMetadata({
1542
+ inputKey: input.key,
1543
+ value: Math.max(min, Math.min(max, current + step)),
1544
+ }).semanticEffects,
1545
+ })}
1211
1546
  onClick={() => update(current + step)}
1212
1547
  >
1213
1548
  +
@@ -1259,11 +1594,13 @@ function targetSelectionLabel(domain: InputDomain): string {
1259
1594
  function StepperButton({
1260
1595
  label,
1261
1596
  disabled,
1597
+ browserAttributes,
1262
1598
  onClick,
1263
1599
  children,
1264
1600
  }: {
1265
1601
  label: string;
1266
1602
  disabled: boolean;
1603
+ browserAttributes?: BrowserInteractionAttributeMap;
1267
1604
  onClick: () => void;
1268
1605
  children: ReactNode;
1269
1606
  }) {
@@ -1274,6 +1611,7 @@ function StepperButton({
1274
1611
  size="sm"
1275
1612
  aria-label={label}
1276
1613
  disabled={disabled}
1614
+ {...browserAttributes}
1277
1615
  onClick={onClick}
1278
1616
  className="h-8 w-8 text-sm"
1279
1617
  >
@@ -2,6 +2,7 @@ import React from "react";
2
2
  import { InteractionUiProvider } from "../context/InteractionDraftContext.js";
3
3
  import { RuntimeProvider } from "../context/RuntimeContext.js";
4
4
  import { usePluginSession } from "../context/PluginSessionContext.js";
5
+ import { RuntimeSemanticProjectionMarker } from "../context/PluginStateContext.js";
5
6
  import { usePluginRuntime } from "../hooks/usePluginRuntime.js";
6
7
  import { GameSkeleton } from "../../ui.js";
7
8
 
@@ -97,6 +98,7 @@ function SessionScopedInteractionUiProvider({
97
98
  const { controllingPlayerId } = usePluginSession();
98
99
  return (
99
100
  <InteractionUiProvider key={controllingPlayerId ?? "__no_player__"}>
101
+ <RuntimeSemanticProjectionMarker />
100
102
  {children}
101
103
  </InteractionUiProvider>
102
104
  );
@@ -1,4 +1,5 @@
1
1
  import type { BoardTargetKind } from "../../utils/interaction-inputs.js";
2
+ import type { BrowserInteractionAttributeMap } from "../../../browser-interaction/index.js";
2
3
 
3
4
  export interface InteractiveTargetState {
4
5
  kind?: BoardTargetKind;
@@ -13,6 +14,7 @@ export interface InteractiveTargetState {
13
14
  conflict: boolean;
14
15
  conflictInteractionKeys?: readonly string[];
15
16
  unavailableReason?: string;
17
+ browserAttributes?: BrowserInteractionAttributeMap;
16
18
  select?: () => unknown | Promise<unknown>;
17
19
  }
18
20
 
@@ -10,6 +10,12 @@ import {
10
10
  import type { PluginStateSnapshot } from "../types/plugin-state.js";
11
11
  import { useRuntimeContext } from "./RuntimeContext.js";
12
12
  import type { PluginRuntimeAPI } from "../runtime/createPluginRuntimeAPI.js";
13
+ import {
14
+ BROWSER_INTERACTION_ATTRIBUTES,
15
+ DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION,
16
+ GAMEPLAY_BROWSER_INTERACTION_SURFACE,
17
+ } from "../../browser-interaction/index.js";
18
+ import { semanticProjectionDigestForState } from "../utils/semantic-projection-digest.js";
13
19
 
14
20
  /**
15
21
  * React Context for providing plugin state from state-sync messages.
@@ -121,11 +127,46 @@ export function PluginStateProvider({
121
127
 
122
128
  return (
123
129
  <PluginStateContext.Provider value={state}>
130
+ <SemanticProjectionMarker state={state} />
124
131
  {children}
125
132
  </PluginStateContext.Provider>
126
133
  );
127
134
  }
128
135
 
136
+ const GAMEPLAY_BROWSER_SCOPE_ID = "runtime";
137
+ const BROWSER_PROJECTION_DIGEST_ATTRIBUTE = "data-dreamboard-projection-digest";
138
+
139
+ export function RuntimeSemanticProjectionMarker() {
140
+ const state = usePluginState((snapshot) => snapshot);
141
+ return <SemanticProjectionMarker state={state} />;
142
+ }
143
+
144
+ export function SemanticProjectionMarker({
145
+ state,
146
+ }: {
147
+ state: PluginStateSnapshot;
148
+ }) {
149
+ const digest = semanticProjectionDigestForState(state);
150
+ if (!digest) {
151
+ return null;
152
+ }
153
+ return (
154
+ <span
155
+ aria-hidden="true"
156
+ style={{ display: "none" }}
157
+ {...{
158
+ [BROWSER_INTERACTION_ATTRIBUTES.protocol]:
159
+ DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION,
160
+ [BROWSER_INTERACTION_ATTRIBUTES.surface]:
161
+ GAMEPLAY_BROWSER_INTERACTION_SURFACE,
162
+ [BROWSER_INTERACTION_ATTRIBUTES.scope]: GAMEPLAY_BROWSER_SCOPE_ID,
163
+ [BROWSER_INTERACTION_ATTRIBUTES.role]: "projection",
164
+ [BROWSER_PROJECTION_DIGEST_ATTRIBUTE]: digest,
165
+ }}
166
+ />
167
+ );
168
+ }
169
+
129
170
  /**
130
171
  * Hook to access the full plugin state snapshot.
131
172
  *