@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
@@ -0,0 +1,407 @@
1
+ import type {
2
+ GameplaySnapshot,
3
+ InteractionDescriptor,
4
+ InteractionInputDescriptor,
5
+ PluginStateSnapshot,
6
+ } from "../types/plugin-state.js";
7
+
8
+ type JsonValue =
9
+ | null
10
+ | boolean
11
+ | number
12
+ | string
13
+ | JsonValue[]
14
+ | { [key: string]: JsonValue };
15
+
16
+ type CanonicalJson = JsonValue;
17
+
18
+ export const AUTHORIZED_SEAT_PROJECTION_DIGEST_VERSION =
19
+ "authorized-seat-projection@2";
20
+
21
+ export function semanticProjectionDigestForState(
22
+ state: PluginStateSnapshot,
23
+ ): string | null {
24
+ const actorPlayerId = state.session.controllingPlayerId;
25
+ if (!actorPlayerId) {
26
+ return null;
27
+ }
28
+ const seatOrder = state.lobby?.seats.map((seat) => seat.playerId) ?? [];
29
+ if (!seatOrder.includes(actorPlayerId)) {
30
+ return null;
31
+ }
32
+ return hashJson(semanticSeatProjection(state, state.gameplay, seatOrder));
33
+ }
34
+
35
+ function semanticSeatProjection(
36
+ state: PluginStateSnapshot,
37
+ gameplay: GameplaySnapshot,
38
+ seatOrder: readonly string[],
39
+ ): CanonicalJson {
40
+ const playerToSeat = new Map(
41
+ seatOrder.map((playerId, index) => [playerId, index] as const),
42
+ );
43
+ const actorPlayerId = state.session.controllingPlayerId;
44
+ const actorSeat = actorPlayerId ? playerToSeat.get(actorPlayerId) : undefined;
45
+ if (actorSeat === undefined) {
46
+ throw new Error(
47
+ `authorized projection player ${actorPlayerId ?? "__none__"} is not present in seat order`,
48
+ );
49
+ }
50
+
51
+ return {
52
+ digestVersion: AUTHORIZED_SEAT_PROJECTION_DIGEST_VERSION,
53
+ actorSeat,
54
+ currentStage: gameplay.currentStage ?? null,
55
+ stageSeats: gameplay.activePlayers.map((playerId) =>
56
+ canonicalizeSeatReference(playerId, playerToSeat),
57
+ ),
58
+ view: canonicalizeSemanticProjectionValue(state.view, playerToSeat),
59
+ zones: canonicalizeSemanticProjectionValue(gameplay.zones ?? {}, playerToSeat),
60
+ availableInteractions: gameplay.availableInteractions.map((descriptor) =>
61
+ semanticInteractionDescriptor(descriptor, playerToSeat),
62
+ ),
63
+ };
64
+ }
65
+
66
+ function semanticInteractionDescriptor(
67
+ descriptor: InteractionDescriptor,
68
+ playerToSeat: ReadonlyMap<string, number>,
69
+ ): CanonicalJson {
70
+ const inputs = descriptor.inputs.map((input) =>
71
+ semanticInteractionInput(input, playerToSeat),
72
+ );
73
+ const { defaults, defaultProvenance } = collectDefaults(descriptor.inputs);
74
+ return {
75
+ interactionKey: descriptor.interactionKey,
76
+ interactionId: descriptor.interactionId,
77
+ stableIdentity: canonicalizeSemanticProjectionValue(
78
+ `${descriptor.interactionKey}:${descriptor.interactionId}`,
79
+ playerToSeat,
80
+ ),
81
+ kind: descriptor.kind,
82
+ surface:
83
+ descriptor.zoneId ??
84
+ descriptor.zoneIds?.find((zoneId) => typeof zoneId === "string") ??
85
+ null,
86
+ commitMode:
87
+ descriptor.commit.mode === "autoWhenReady" ? "autoWhenReady" : "manual",
88
+ status: descriptor.availability.status ?? null,
89
+ available: descriptor.availability.status === "available",
90
+ inputs,
91
+ defaults: canonicalizeSemanticProjectionValue(defaults, playerToSeat),
92
+ defaultProvenance,
93
+ };
94
+ }
95
+
96
+ function semanticInteractionInput(
97
+ input: InteractionInputDescriptor,
98
+ playerToSeat: ReadonlyMap<string, number>,
99
+ ): CanonicalJson {
100
+ const domain = asRecord(input.domain);
101
+ const selection = asRecord(domain.selection);
102
+ return {
103
+ key: input.key,
104
+ kind: input.kind,
105
+ domain: canonicalizeSemanticProjectionValue(input.domain, playerToSeat),
106
+ defaultValue:
107
+ input.defaultValue === undefined
108
+ ? null
109
+ : canonicalizeSemanticProjectionValue(input.defaultValue, playerToSeat),
110
+ defaultProvenance: input.defaultValue === undefined ? null : "input",
111
+ selectionMode: stringField(selection, "mode"),
112
+ min: numberField(domain),
113
+ max: numberField(domain, "max"),
114
+ };
115
+ }
116
+
117
+ function collectDefaults(inputs: readonly InteractionInputDescriptor[]): {
118
+ defaults: Record<string, JsonValue>;
119
+ defaultProvenance: Record<string, "input">;
120
+ } {
121
+ const defaults: Record<string, JsonValue> = {};
122
+ const defaultProvenance: Record<string, "input"> = {};
123
+ for (const input of inputs) {
124
+ if (input.defaultValue !== undefined) {
125
+ defaults[input.key] = toCanonicalJson(input.defaultValue) as JsonValue;
126
+ defaultProvenance[input.key] = "input";
127
+ }
128
+ }
129
+ return { defaults, defaultProvenance };
130
+ }
131
+
132
+ function canonicalizeSemanticProjectionValue(
133
+ value: unknown,
134
+ playerToSeat: ReadonlyMap<string, number>,
135
+ ): CanonicalJson {
136
+ if (
137
+ value === null ||
138
+ typeof value === "boolean" ||
139
+ typeof value === "number"
140
+ ) {
141
+ return toCanonicalJson(value);
142
+ }
143
+ if (typeof value === "string") {
144
+ return canonicalizeSeatReference(value, playerToSeat);
145
+ }
146
+ if (Array.isArray(value)) {
147
+ return value.map((item) =>
148
+ canonicalizeSemanticProjectionValue(item, playerToSeat),
149
+ );
150
+ }
151
+ if (isRecord(value)) {
152
+ return Object.fromEntries(
153
+ Object.entries(value)
154
+ .filter(
155
+ ([key, item]) =>
156
+ item !== undefined &&
157
+ !SEMANTIC_PROJECTION_TRANSPORT_FIELDS.has(key),
158
+ )
159
+ .map(([key, item]) => [
160
+ canonicalizeObjectKey(key, playerToSeat),
161
+ canonicalizeSemanticProjectionProperty(key, item, playerToSeat),
162
+ ]),
163
+ );
164
+ }
165
+ return null;
166
+ }
167
+
168
+ function canonicalizeSemanticProjectionProperty(
169
+ key: string,
170
+ value: unknown,
171
+ playerToSeat: ReadonlyMap<string, number>,
172
+ ): CanonicalJson {
173
+ const canonicalValue = canonicalizeSemanticProjectionValue(value, playerToSeat);
174
+ if (
175
+ (key === "eligibleTargets" || key === "dependentCases") &&
176
+ Array.isArray(canonicalValue)
177
+ ) {
178
+ return [...canonicalValue].sort((left, right) =>
179
+ compareJson(canonicalJson(left), canonicalJson(right)),
180
+ );
181
+ }
182
+ return canonicalValue;
183
+ }
184
+
185
+ function canonicalizeSeatReference(
186
+ value: string,
187
+ playerToSeat: ReadonlyMap<string, number>,
188
+ ): CanonicalJson {
189
+ const seat = playerToSeat.get(value);
190
+ return seat === undefined ? value : { $seat: seat };
191
+ }
192
+
193
+ function canonicalizeObjectKey(
194
+ key: string,
195
+ playerToSeat: ReadonlyMap<string, number>,
196
+ ): string {
197
+ const seat = playerToSeat.get(key);
198
+ return seat === undefined ? key : `$seat:${seat}`;
199
+ }
200
+
201
+ const SEMANTIC_PROJECTION_TRANSPORT_FIELDS = new Set([
202
+ "availableInteractionRefs",
203
+ "board",
204
+ "cursor",
205
+ "etag",
206
+ "hydratedAt",
207
+ "hydrationCursor",
208
+ "hydrationSequence",
209
+ "interactionRef",
210
+ "interactionRefs",
211
+ "lastEventId",
212
+ "receivedAt",
213
+ "sequence",
214
+ "serverTime",
215
+ "snapshotVersion",
216
+ "transportSequence",
217
+ "transportVersion",
218
+ "updatedAt",
219
+ ]);
220
+
221
+ function toCanonicalJson(value: unknown): CanonicalJson {
222
+ if (
223
+ value === null ||
224
+ typeof value === "boolean" ||
225
+ typeof value === "string"
226
+ ) {
227
+ return value;
228
+ }
229
+ if (typeof value === "number") {
230
+ return value;
231
+ }
232
+ if (Array.isArray(value)) {
233
+ return value.map((item) => toCanonicalJson(item));
234
+ }
235
+ if (isRecord(value)) {
236
+ return Object.fromEntries(
237
+ Object.entries(value)
238
+ .filter(([, item]) => item !== undefined)
239
+ .map(([key, item]) => [key, toCanonicalJson(item)]),
240
+ );
241
+ }
242
+ return null;
243
+ }
244
+
245
+ function hashJson(value: CanonicalJson): string {
246
+ return `sha256:${sha256Hex(canonicalJson(value))}`;
247
+ }
248
+
249
+ function canonicalJson(value: CanonicalJson): string {
250
+ return JSON.stringify(canonicalizeJson(value));
251
+ }
252
+
253
+ function canonicalizeJson(value: CanonicalJson): CanonicalJson {
254
+ if (
255
+ value === null ||
256
+ typeof value === "boolean" ||
257
+ typeof value === "string"
258
+ ) {
259
+ return value;
260
+ }
261
+ if (typeof value === "number") {
262
+ if (!Number.isFinite(value)) {
263
+ throw new Error("canonical JSON contains a non-finite number");
264
+ }
265
+ return value;
266
+ }
267
+ if (Array.isArray(value)) {
268
+ return value.map((item) => canonicalizeJson(item));
269
+ }
270
+ return Object.fromEntries(
271
+ Object.entries(value)
272
+ .sort(([left], [right]) => compareJson(left, right))
273
+ .map(([key, item]) => [key, canonicalizeJson(item)]),
274
+ );
275
+ }
276
+
277
+ function compareJson(left: string, right: string): number {
278
+ return left < right ? -1 : left > right ? 1 : 0;
279
+ }
280
+
281
+ function isRecord(value: unknown): value is Record<string, unknown> {
282
+ return !!value && typeof value === "object" && !Array.isArray(value);
283
+ }
284
+
285
+ function asRecord(value: unknown): Record<string, unknown> {
286
+ return isRecord(value) ? value : {};
287
+ }
288
+
289
+ function stringField(record: Record<string, unknown>, key: string): string | null {
290
+ const value = record[key];
291
+ return typeof value === "string" && value.trim() ? value : null;
292
+ }
293
+
294
+ function numberField(
295
+ record: Record<string, unknown>,
296
+ key = "min",
297
+ ): number | null {
298
+ const value = record[key];
299
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
300
+ }
301
+
302
+ const SHA256_K = [
303
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
304
+ 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
305
+ 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
306
+ 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
307
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
308
+ 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
309
+ 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
310
+ 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
311
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
312
+ 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
313
+ 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
314
+ ];
315
+
316
+ function sha256Hex(input: string): string {
317
+ const bytes = utf8Bytes(input);
318
+ const bitLength = bytes.length * 8;
319
+ bytes.push(0x80);
320
+ while (bytes.length % 64 !== 56) {
321
+ bytes.push(0);
322
+ }
323
+ for (let shift = 56; shift >= 0; shift -= 8) {
324
+ bytes.push(Math.floor(bitLength / 2 ** shift) & 0xff);
325
+ }
326
+
327
+ let h0 = 0x6a09e667;
328
+ let h1 = 0xbb67ae85;
329
+ let h2 = 0x3c6ef372;
330
+ let h3 = 0xa54ff53a;
331
+ let h4 = 0x510e527f;
332
+ let h5 = 0x9b05688c;
333
+ let h6 = 0x1f83d9ab;
334
+ let h7 = 0x5be0cd19;
335
+ const words = new Array<number>(64);
336
+
337
+ for (let offset = 0; offset < bytes.length; offset += 64) {
338
+ for (let index = 0; index < 16; index += 1) {
339
+ const base = offset + index * 4;
340
+ words[index] =
341
+ ((bytes[base]! << 24) |
342
+ (bytes[base + 1]! << 16) |
343
+ (bytes[base + 2]! << 8) |
344
+ bytes[base + 3]!) >>>
345
+ 0;
346
+ }
347
+ for (let index = 16; index < 64; index += 1) {
348
+ const s0 =
349
+ rotateRight(words[index - 15]!, 7) ^
350
+ rotateRight(words[index - 15]!, 18) ^
351
+ (words[index - 15]! >>> 3);
352
+ const s1 =
353
+ rotateRight(words[index - 2]!, 17) ^
354
+ rotateRight(words[index - 2]!, 19) ^
355
+ (words[index - 2]! >>> 10);
356
+ words[index] =
357
+ (words[index - 16]! + s0 + words[index - 7]! + s1) >>> 0;
358
+ }
359
+
360
+ let a = h0;
361
+ let b = h1;
362
+ let c = h2;
363
+ let d = h3;
364
+ let e = h4;
365
+ let f = h5;
366
+ let g = h6;
367
+ let h = h7;
368
+
369
+ for (let index = 0; index < 64; index += 1) {
370
+ const s1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
371
+ const ch = (e & f) ^ (~e & g);
372
+ const temp1 = (h + s1 + ch + SHA256_K[index]! + words[index]!) >>> 0;
373
+ const s0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
374
+ const maj = (a & b) ^ (a & c) ^ (b & c);
375
+ const temp2 = (s0 + maj) >>> 0;
376
+ h = g;
377
+ g = f;
378
+ f = e;
379
+ e = (d + temp1) >>> 0;
380
+ d = c;
381
+ c = b;
382
+ b = a;
383
+ a = (temp1 + temp2) >>> 0;
384
+ }
385
+
386
+ h0 = (h0 + a) >>> 0;
387
+ h1 = (h1 + b) >>> 0;
388
+ h2 = (h2 + c) >>> 0;
389
+ h3 = (h3 + d) >>> 0;
390
+ h4 = (h4 + e) >>> 0;
391
+ h5 = (h5 + f) >>> 0;
392
+ h6 = (h6 + g) >>> 0;
393
+ h7 = (h7 + h) >>> 0;
394
+ }
395
+
396
+ return [h0, h1, h2, h3, h4, h5, h6, h7]
397
+ .map((value) => value.toString(16).padStart(8, "0"))
398
+ .join("");
399
+ }
400
+
401
+ function rotateRight(value: number, bits: number): number {
402
+ return (value >>> bits) | (value << (32 - bits));
403
+ }
404
+
405
+ function utf8Bytes(input: string): number[] {
406
+ return Array.from(new TextEncoder().encode(input));
407
+ }
@@ -1041,6 +1041,7 @@ function HexGridImpl(
1041
1041
  return (
1042
1042
  <g
1043
1043
  key={space.id}
1044
+ {...state.browserAttributes}
1044
1045
  transform={`translate(${pos.x}, ${pos.y})`}
1045
1046
  role={isSelectable ? "button" : undefined}
1046
1047
  className={clsx(isSelectable && "cursor-pointer")}
@@ -1140,6 +1141,7 @@ function HexGridImpl(
1140
1141
  return (
1141
1142
  <g
1142
1143
  key={edge.id}
1144
+ {...state.browserAttributes}
1143
1145
  role={isSelectable ? "button" : undefined}
1144
1146
  className={clsx(isSelectable && "cursor-pointer")}
1145
1147
  onPointerEnter={() => setHoveredEdgeId(edge.id)}
@@ -1215,6 +1217,7 @@ function HexGridImpl(
1215
1217
  return (
1216
1218
  <g
1217
1219
  key={vertex.id}
1220
+ {...state.browserAttributes}
1218
1221
  role={isSelectable ? "button" : undefined}
1219
1222
  className={clsx(isSelectable && "cursor-pointer")}
1220
1223
  onPointerEnter={() => setHoveredVertexId(vertex.id)}
@@ -9,6 +9,7 @@ export interface InteractiveTargetState {
9
9
  pending: boolean;
10
10
  conflict: boolean;
11
11
  unavailableReason?: string;
12
+ browserAttributes?: Record<string, string | boolean>;
12
13
  select?: () => unknown | Promise<unknown>;
13
14
  }
14
15