@dreamboard-games/sdk 0.5.0-alpha.11 → 0.5.0-alpha.13

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.
package/dist/react.d.ts CHANGED
@@ -1,23 +1,23 @@
1
1
  import * as react from 'react';
2
- import { PointerEvent, MouseEvent, DragEvent, CSSProperties, ReactNode } from 'react';
3
- import { a4 as TargetOptions, a1 as SeatCardId, q as Features, E as InstanceOptions, n as CoreInstance, p as FeatureContext, G as GameInstance, r as GameSnapshot, o as DropTarget } from './model-BeslAXVA.js';
2
+ import { PointerEvent, FocusEvent, MouseEvent, DragEvent, CSSProperties, ReactNode } from 'react';
3
+ import { aa as TargetOptions, a1 as SeatCardId, q as Features, E as InstanceOptions, n as CoreInstance, p as FeatureContext, G as GameInstance, r as GameSnapshot, a5 as ShortcutOptions, a6 as ShortcutTarget, a4 as ShortcutHint, o as DropTarget } from './shortcuts-Chlx8o7p.js';
4
4
  import { G as GameSource } from './types-Xg_RxV1n.js';
5
+ import '@tanstack/store';
5
6
  import './extract-DgzrJ5el.js';
6
7
  import 'zod';
7
8
  import 'honeycomb-grid';
8
- import '@tanstack/store';
9
9
 
10
10
  interface CardGestureProps {
11
11
  onPointerDown(event: PointerEvent<Element>): void;
12
- onPointerEnter(event: PointerEvent<Element>): void;
13
- onPointerLeave(event: PointerEvent<Element>): void;
14
- onFocus(): void;
12
+ onFocus(event: FocusEvent<Element>): void;
15
13
  onBlur(): void;
16
14
  onContextMenu(event: MouseEvent<Element>): void;
17
15
  onDragStart(event: DragEvent<Element>): void;
18
16
  readonly style: CSSProperties;
19
17
  readonly "data-dragging"?: true;
20
18
  readonly "data-inspecting"?: "hold" | "hover";
19
+ readonly "data-card-gesture": string;
20
+ readonly "data-gesture-session": string;
21
21
  }
22
22
 
23
23
  interface SelectionOptions<Value> {
@@ -36,6 +36,7 @@ interface CardGesture {
36
36
  readonly props: CardGestureProps;
37
37
  readonly canDrag: boolean;
38
38
  readonly isDragging: boolean;
39
+ readonly isActive: boolean;
39
40
  /** Held on touch, or rested on with a mouse. */
40
41
  readonly inspecting: "hold" | "hover" | null;
41
42
  }
@@ -73,6 +74,21 @@ declare function createGameHook<Game, Source extends GameSource = GameSource>():
73
74
  children: (value: Value) => ReactNode;
74
75
  }) => ReactNode;
75
76
  useCardGesture: (cardId: SeatCardId<Game>, options: CardGestureOptions<Game>) => CardGesture;
77
+ useActiveCard: () => SeatCardId<Game> | null;
78
+ useGameShortcuts: (options: ShortcutOptions<Game>) => {
79
+ readonly target: ShortcutTarget<Game> | null;
80
+ readonly hints: readonly ShortcutHint<Game>[];
81
+ };
82
+ useShortcutTarget: (target: ShortcutTarget<Game>) => {
83
+ props: {
84
+ "data-shortcut-target": string;
85
+ "data-gesture-session": string;
86
+ onPointerDown(event: react.PointerEvent<Element>): void;
87
+ onFocus(event: react.FocusEvent<Element>): void;
88
+ onBlur(): void;
89
+ };
90
+ };
91
+ useShortcutHints: (target: ShortcutTarget<Game> | null) => readonly ShortcutHint<Game>[];
76
92
  useDropArea: (binding: DropTarget<Game> | DropAreaBinding<Game> | null) => DropArea;
77
93
  useDragOverlay: () => DragOverlay<Game> | null;
78
94
  };
package/dist/react.js CHANGED
@@ -3,8 +3,76 @@ import {
3
3
  createGestureRecognizer,
4
4
  isSameDropTarget,
5
5
  magneticDropPoint
6
- } from "./chunk-2IGXAYW4.js";
7
- import "./chunk-ES74C2IN.js";
6
+ } from "./chunk-W5G6VMMQ.js";
7
+ import {
8
+ inputValueKey
9
+ } from "./chunk-ES74C2IN.js";
10
+
11
+ // src/react/create-game-hook.tsx
12
+ import { createStore as createStore2 } from "@tanstack/store";
13
+
14
+ // src/react/shortcuts.ts
15
+ import { useLayoutEffect, useRef } from "react";
16
+ function canHandle(event) {
17
+ const target = event.target;
18
+ return !event.defaultPrevented && !event.repeat && !event.isComposing && !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey && !(target instanceof Element && target.closest(
19
+ "input, textarea, select, [contenteditable]:not([contenteditable='false']), [role='textbox']"
20
+ )) && !Array.from(
21
+ document.querySelectorAll(
22
+ "[aria-modal='true'], dialog[open], [role='dialog'], [role='menu'], [role='listbox']"
23
+ )
24
+ ).some(
25
+ (surface) => !surface.closest("[aria-hidden='true']") && surface.checkVisibility({
26
+ visibilityProperty: true,
27
+ opacityProperty: true
28
+ })
29
+ );
30
+ }
31
+ function useShortcutsAdapter(game, controller, session, options, onResult) {
32
+ const latest = useRef({ options, onResult });
33
+ const registration = useRef(null);
34
+ useLayoutEffect(() => {
35
+ latest.current = { options, onResult };
36
+ });
37
+ useLayoutEffect(() => {
38
+ let mounted = true;
39
+ const owned = controller.register(latest.current.options);
40
+ registration.current = owned;
41
+ function keydown(event) {
42
+ if (!canHandle(event)) return;
43
+ const source = game.getOptions().source;
44
+ const seat = game.snapshot?.me;
45
+ const active = () => mounted && game.getOptions().source === source && game.snapshot?.me === seat;
46
+ const pending = controller.handle(
47
+ event.key,
48
+ session.getActiveTarget()
49
+ );
50
+ if (!pending) return;
51
+ event.preventDefault();
52
+ void pending.then(
53
+ (result) => {
54
+ if (active()) latest.current.onResult(result);
55
+ },
56
+ (cause) => {
57
+ if (active())
58
+ latest.current.onResult(
59
+ cause instanceof Error ? cause : new Error(String(cause))
60
+ );
61
+ }
62
+ );
63
+ }
64
+ window.addEventListener("keydown", keydown);
65
+ return () => {
66
+ mounted = false;
67
+ window.removeEventListener("keydown", keydown);
68
+ registration.current = null;
69
+ owned.dispose();
70
+ };
71
+ }, [game, controller, session]);
72
+ useLayoutEffect(() => {
73
+ registration.current?.update(options);
74
+ });
75
+ }
8
76
 
9
77
  // src/react/gesture.tsx
10
78
  import { createStore } from "@tanstack/store";
@@ -38,11 +106,18 @@ var sameDropTarget = (left, right) => left === right || !!left && !!right && isS
38
106
  var sessions = 0;
39
107
  function createGestureSession(game) {
40
108
  const id = `g${++sessions}`;
41
- const store = createStore({ drag: null, inspect: null });
109
+ const store = createStore({
110
+ activeTarget: null,
111
+ drag: null,
112
+ inspect: null
113
+ });
114
+ const targets = /* @__PURE__ */ new Map();
115
+ const sameTarget = (left, right) => inputValueKey(left) === inputValueKey(right);
42
116
  const areas = /* @__PURE__ */ new Map();
43
117
  const followers = /* @__PURE__ */ new Set();
44
118
  let press = null;
45
119
  let hover = null;
120
+ let hoverPoint = null;
46
121
  let focused = null;
47
122
  let alt = false;
48
123
  let pointer = { x: 0, y: 0 };
@@ -51,12 +126,34 @@ function createGestureSession(game) {
51
126
  let disposed = false;
52
127
  const set = (patch) => store.setState((previous) => ({ ...previous, ...patch }));
53
128
  let snapshot = game.snapshot;
129
+ let source = game.getOptions().source;
130
+ function admitted(target) {
131
+ if (!target || !game.snapshot) return null;
132
+ const zones = game.snapshot.frame.zones;
133
+ if (target.kind === "zone")
134
+ return zones[target.zoneId]?.[target.hostId] ? target : null;
135
+ if (target.kind === "card")
136
+ return Object.entries(snapshot?.frame.zones ?? {}).some(
137
+ ([zoneId, hosts]) => Object.entries(hosts).some(
138
+ ([hostId, zone]) => zone.cardIds.includes(target.value) && zones[zoneId]?.[hostId]?.cardIds.includes(target.value)
139
+ )
140
+ ) ? target : null;
141
+ return [...targets.values()].some((read) => sameTarget(read(), target)) ? target : null;
142
+ }
54
143
  const unsubscribe = game.subscribe(() => {
55
- if (snapshot !== game.snapshot) {
144
+ const nextSource = game.getOptions().source;
145
+ const changedLifetime = source !== nextSource || snapshot?.me !== game.snapshot?.me;
146
+ if (changedLifetime || snapshot !== game.snapshot) {
147
+ const nextHover = changedLifetime ? null : admitted(hover);
148
+ const nextFocused = changedLifetime ? null : admitted(focused);
149
+ source = nextSource;
56
150
  snapshot = game.snapshot;
57
151
  press?.recognizer.cancel();
58
152
  alt = false;
59
- set({ inspect: null });
153
+ hover = nextHover;
154
+ focused = nextFocused;
155
+ if (changedLifetime) hoverPoint = null;
156
+ set({ activeTarget: hover ?? focused, inspect: null });
60
157
  }
61
158
  if (press?.dragging && !game.drag?.active) press.recognizer.cancel();
62
159
  const drag = store.get().drag;
@@ -132,9 +229,33 @@ function createGestureSession(game) {
132
229
  hover = null;
133
230
  }
134
231
  function inspectWithAlt() {
135
- const cardId = hover ?? focused;
136
- set({ inspect: alt && cardId && !press ? { cardId, via: "hover" } : null });
232
+ const target = hover ?? focused;
233
+ const cardId = target?.kind === "card" ? target.value : null;
234
+ set({
235
+ activeTarget: target,
236
+ inspect: alt && cardId && !press ? { cardId, via: "hover" } : null
237
+ });
238
+ }
239
+ function hoverAt(at) {
240
+ hoverPoint = at;
241
+ const wasFocused = focused !== null;
242
+ focused = null;
243
+ const hit = document.elementFromPoint(at.x, at.y)?.closest("[data-card-gesture], [data-shortcut-target]");
244
+ const control = hit?.getAttribute("data-gesture-session") === id ? hit : null;
245
+ const targetId = control?.getAttribute("data-shortcut-target");
246
+ const cardId = control?.getAttribute("data-card-gesture");
247
+ const next = targetId ? targets.get(targetId)?.() ?? null : cardId ? { kind: "card", value: cardId } : null;
248
+ if (sameTarget(hover, next) && !wasFocused) return;
249
+ hover = next;
250
+ inspectWithAlt();
137
251
  }
252
+ const browse = (event) => {
253
+ if (event.pointerType !== "mouse" || press) return;
254
+ if (hoverPoint?.x === event.clientX && hoverPoint.y === event.clientY)
255
+ return;
256
+ alt = event.altKey;
257
+ hoverAt({ x: event.clientX, y: event.clientY });
258
+ };
138
259
  const key = (event) => {
139
260
  if (event.key === "Escape") {
140
261
  press?.recognizer.cancel();
@@ -148,12 +269,23 @@ function createGestureSession(game) {
148
269
  const loseFocus = () => {
149
270
  alt = false;
150
271
  clearHover();
272
+ hoverPoint = null;
151
273
  focused = null;
152
- set({ inspect: null });
274
+ set({ activeTarget: null, inspect: null });
153
275
  };
154
276
  addEventListener("keydown", key);
155
277
  addEventListener("keyup", key);
156
278
  addEventListener("blur", loseFocus);
279
+ const leaveWindow = (event) => {
280
+ if (event.pointerType !== "mouse" || event.relatedTarget !== null) return;
281
+ clearHover();
282
+ hoverPoint = null;
283
+ focused = null;
284
+ alt = false;
285
+ inspectWithAlt();
286
+ };
287
+ addEventListener("pointermove", browse);
288
+ addEventListener("pointerout", leaveWindow);
157
289
  function release() {
158
290
  press?.detach();
159
291
  press = null;
@@ -169,6 +301,7 @@ function createGestureSession(game) {
169
301
  return false;
170
302
  const box = element.getBoundingClientRect();
171
303
  clearHover();
304
+ focused = null;
172
305
  press.dragging = true;
173
306
  set({
174
307
  drag: {
@@ -177,7 +310,8 @@ function createGestureSession(game) {
177
310
  size: { width: box.width, height: box.height },
178
311
  settling: false
179
312
  },
180
- inspect: null
313
+ inspect: null,
314
+ activeTarget: null
181
315
  });
182
316
  return true;
183
317
  },
@@ -187,11 +321,15 @@ function createGestureSession(game) {
187
321
  },
188
322
  end(kind, at) {
189
323
  const dragging = press.dragging;
324
+ const pointerType = press.pointerType;
190
325
  if (dragging) retarget(at);
191
326
  release();
192
327
  if (kind !== "tap") swallowNextClick();
193
328
  if (kind === "hold") set({ inspect: null });
194
- if (!dragging) return;
329
+ if (!dragging) {
330
+ if (kind === "browse" && pointerType === "mouse") hoverAt(at);
331
+ return;
332
+ }
195
333
  const before = game.request;
196
334
  game.drag?.drop();
197
335
  const drag = store.get().drag;
@@ -240,24 +378,22 @@ function createGestureSession(game) {
240
378
  }
241
379
  function cardProps(cardId, options, flags) {
242
380
  return {
243
- onPointerDown: (event) => start(event.nativeEvent, cardId, event.currentTarget, options),
244
- onPointerEnter(event) {
245
- if (event.pointerType !== "mouse" || press?.dragging) return;
246
- clearHover();
247
- hover = cardId;
248
- alt = event.altKey;
249
- inspectWithAlt();
250
- },
251
- onPointerLeave() {
252
- if (hover === cardId) clearHover();
253
- inspectWithAlt();
381
+ onPointerDown(event) {
382
+ if (event.pointerType === "mouse") {
383
+ hover = { kind: "card", value: cardId };
384
+ alt = event.altKey;
385
+ inspectWithAlt();
386
+ }
387
+ start(event.nativeEvent, cardId, event.currentTarget, options);
254
388
  },
255
- onFocus() {
256
- focused = cardId;
389
+ onFocus(event) {
390
+ focused = event.currentTarget.matches(":focus-visible") ? { kind: "card", value: cardId } : null;
391
+ if (focused) hover = null;
257
392
  inspectWithAlt();
258
393
  },
259
394
  onBlur() {
260
- if (focused === cardId) focused = null;
395
+ if (focused?.kind === "card" && focused.value === cardId)
396
+ focused = null;
261
397
  inspectWithAlt();
262
398
  },
263
399
  onContextMenu(event) {
@@ -267,7 +403,9 @@ function createGestureSession(game) {
267
403
  onDragStart: (event) => event.preventDefault(),
268
404
  style: options === false ? INSPECTION_STYLE : CARD_STYLE,
269
405
  "data-dragging": flags.dragging || void 0,
270
- "data-inspecting": flags.inspecting ?? void 0
406
+ "data-inspecting": flags.inspecting ?? void 0,
407
+ "data-card-gesture": cardId,
408
+ "data-gesture-session": id
271
409
  };
272
410
  }
273
411
  function overlayRef(element) {
@@ -295,6 +433,43 @@ function createGestureSession(game) {
295
433
  id,
296
434
  store,
297
435
  cardProps,
436
+ getActiveCardId() {
437
+ const target = store.get().activeTarget;
438
+ return target?.kind === "card" ? target.value : null;
439
+ },
440
+ getActiveTarget: () => store.get().activeTarget,
441
+ registerTarget(targetId, read) {
442
+ const target = read();
443
+ targets.set(targetId, read);
444
+ return () => {
445
+ targets.delete(targetId);
446
+ if (sameTarget(hover, target)) hover = null;
447
+ if (sameTarget(focused, target)) focused = null;
448
+ inspectWithAlt();
449
+ };
450
+ },
451
+ targetProps(targetId) {
452
+ return {
453
+ "data-shortcut-target": targetId,
454
+ "data-gesture-session": id,
455
+ onPointerDown(event) {
456
+ if (event.pointerType !== "mouse") return;
457
+ hover = targets.get(targetId)?.() ?? null;
458
+ alt = event.altKey;
459
+ inspectWithAlt();
460
+ },
461
+ onFocus(event) {
462
+ focused = event.currentTarget.matches(":focus-visible") ? targets.get(targetId)?.() ?? null : null;
463
+ if (focused) hover = null;
464
+ inspectWithAlt();
465
+ },
466
+ onBlur() {
467
+ const target = targets.get(targetId)?.() ?? null;
468
+ if (sameTarget(focused, target)) focused = null;
469
+ inspectWithAlt();
470
+ }
471
+ };
472
+ },
298
473
  overlayRef,
299
474
  /** `read` returns the area's current binding; targets resolve when hit. */
300
475
  registerArea(area, read) {
@@ -310,8 +485,11 @@ function createGestureSession(game) {
310
485
  removeEventListener("keydown", key);
311
486
  removeEventListener("keyup", key);
312
487
  removeEventListener("blur", loseFocus);
488
+ removeEventListener("pointermove", browse);
489
+ removeEventListener("pointerout", leaveWindow);
313
490
  unsubscribe();
314
491
  followers.clear();
492
+ targets.clear();
315
493
  if (!guardingClicks) return;
316
494
  removeEventListener("click", swallow, true);
317
495
  removeEventListener("pointerdown", newPress, true);
@@ -333,8 +511,8 @@ import {
333
511
  createContext as createContext2,
334
512
  useContext as useContext2,
335
513
  useId,
336
- useLayoutEffect,
337
- useRef,
514
+ useLayoutEffect as useLayoutEffect2,
515
+ useRef as useRef2,
338
516
  useState
339
517
  } from "react";
340
518
  import { useSelector as useSelector2 } from "@tanstack/react-store";
@@ -344,12 +522,12 @@ function createGameHook() {
344
522
  const Context = createContext2(null);
345
523
  function GameProvider({ children, ...overrides }) {
346
524
  const [owned, setOwned] = useState(null);
347
- const lifetime = useRef(null);
525
+ const lifetime = useRef2(null);
348
526
  const options = {
349
527
  ...defaults,
350
528
  ...overrides
351
529
  };
352
- useLayoutEffect(() => {
530
+ useLayoutEffect2(() => {
353
531
  let current = lifetime.current;
354
532
  if (!current) {
355
533
  const instance = createGameInstance()(options);
@@ -373,7 +551,7 @@ function createGameHook() {
373
551
  });
374
552
  };
375
553
  }, []);
376
- useLayoutEffect(() => {
554
+ useLayoutEffect2(() => {
377
555
  lifetime.current?.instance.setOptions(options);
378
556
  });
379
557
  return owned ? /* @__PURE__ */ jsx(Context.Provider, { value: owned.instance, children: /* @__PURE__ */ jsx(GestureContext.Provider, { value: owned.session, children }) }) : null;
@@ -416,10 +594,15 @@ function createGameHook() {
416
594
  session,
417
595
  (state) => state.inspect?.cardId === cardId ? state.inspect.via : null
418
596
  );
597
+ const isActive = useGestureState(
598
+ session,
599
+ (state) => state.activeTarget?.kind === "card" && state.activeTarget.value === cardId
600
+ );
419
601
  return {
420
602
  props: session.cardProps(cardId, routes, { dragging, inspecting }),
421
603
  canDrag,
422
604
  isDragging: dragging,
605
+ isActive,
423
606
  inspecting
424
607
  };
425
608
  }
@@ -435,11 +618,11 @@ function createGameHook() {
435
618
  const active = dragOf(snapshot)?.active?.target;
436
619
  return !!target && sameDropTarget(active ?? null, target);
437
620
  });
438
- const latest = useRef(erased);
439
- useLayoutEffect(() => {
621
+ const latest = useRef2(erased);
622
+ useLayoutEffect2(() => {
440
623
  latest.current = erased;
441
624
  });
442
- useLayoutEffect(
625
+ useLayoutEffect2(
443
626
  () => session.registerArea(id, () => latest.current),
444
627
  [session, id]
445
628
  );
@@ -453,6 +636,61 @@ function createGameHook() {
453
636
  isOver
454
637
  };
455
638
  }
639
+ const shortcutsOf = (snapshot) => snapshot.shortcuts;
640
+ const noShortcutsConfiguration = createStore2(
641
+ null
642
+ );
643
+ const sameHints = (left, right) => left.length === right.length && left.every((hint, index) => {
644
+ const next = right[index];
645
+ return hint.label === next.label && hint.interaction === next.interaction && hint.keys.length === next.keys.length && hint.keys.every((key, at) => key === next.keys[at]);
646
+ });
647
+ function useShortcutHints(target) {
648
+ const controller = shortcutsOf(useInstance());
649
+ useGame((snapshot) => shortcutsOf(snapshot)?.getHints(target) ?? [], {
650
+ compare: sameHints
651
+ });
652
+ return useSelector2(
653
+ controller?.configuration ?? noShortcutsConfiguration,
654
+ () => controller?.getHints(target) ?? [],
655
+ { compare: sameHints }
656
+ );
657
+ }
658
+ function useGameShortcuts(options) {
659
+ const instance = useInstance();
660
+ const session = useGestureSession();
661
+ const controller = shortcutsOf(instance);
662
+ if (!controller)
663
+ throw new Error(
664
+ "useGameShortcuts requires shortcutsFeature in the game binding."
665
+ );
666
+ useShortcutsAdapter(instance, controller, session, options, (result) => {
667
+ if (result instanceof Error) instance.getOptions().onError?.(result);
668
+ else if (!result.accepted)
669
+ instance.getOptions().onError?.(
670
+ new Error(result.message ?? result.errorCode, { cause: result })
671
+ );
672
+ });
673
+ const target = useGestureState(
674
+ session,
675
+ (state) => state.activeTarget
676
+ );
677
+ const hints = useShortcutHints(target);
678
+ return { target, hints };
679
+ }
680
+ function useShortcutTarget(target) {
681
+ const session = useGestureSession();
682
+ const id = `${session.id}${useId()}`;
683
+ const latest = useRef2(target);
684
+ const targetKey = inputValueKey(target);
685
+ useLayoutEffect2(() => {
686
+ latest.current = target;
687
+ });
688
+ useLayoutEffect2(
689
+ () => session.registerTarget(id, () => latest.current),
690
+ [session, id, targetKey]
691
+ );
692
+ return { props: session.targetProps(id) };
693
+ }
456
694
  function useDragOverlay() {
457
695
  const session = useGestureSession();
458
696
  const drag = useGestureState(session, (state) => state.drag);
@@ -462,11 +700,22 @@ function createGameHook() {
462
700
  ref: session.overlayRef
463
701
  } : null;
464
702
  }
703
+ function useActiveCard() {
704
+ const session = useGestureSession();
705
+ return useGestureState(
706
+ session,
707
+ (state) => state.activeTarget?.kind === "card" ? state.activeTarget.value : null
708
+ );
709
+ }
465
710
  return {
466
711
  GameProvider,
467
712
  useGame,
468
713
  Subscribe,
469
714
  useCardGesture,
715
+ useActiveCard,
716
+ useGameShortcuts,
717
+ useShortcutTarget,
718
+ useShortcutHints,
470
719
  useDropArea,
471
720
  useDragOverlay
472
721
  };