@dreamboard-games/sdk 0.5.0-alpha.21 → 0.5.0-alpha.23

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/index.js CHANGED
@@ -73,7 +73,7 @@ import {
73
73
  } from "./chunk-CZRPUWCT.js";
74
74
 
75
75
  // src/package-set.ts
76
- var DREAMBOARD_SDK_VERSION = true ? "0.5.0-alpha.21" : "0.0.0-development";
76
+ var DREAMBOARD_SDK_VERSION = true ? "0.5.0-alpha.23" : "0.0.0-development";
77
77
  var DREAMBOARD_SDK_PACKAGES = {
78
78
  "@dreamboard-games/sdk": DREAMBOARD_SDK_VERSION
79
79
  };
package/dist/react.d.ts CHANGED
@@ -7,6 +7,19 @@ import 'zod';
7
7
  import 'honeycomb-grid';
8
8
  import '@tanstack/store';
9
9
 
10
+ /** How the dragged card would show where it lands if dropped now. */
11
+ interface DragFit {
12
+ /**
13
+ * The width of the card-sized box marked `data-drop-landing` there,
14
+ * relative to the picked-up card's layout width. Null until the card has
15
+ * been over such a box; between areas it keeps the last one's.
16
+ */
17
+ readonly scale: number | null;
18
+ /** Drawn in a pile's slot. */
19
+ readonly snapped: boolean;
20
+ /** The area's zone would conceal the card. */
21
+ readonly concealed: boolean;
22
+ }
10
23
  interface CardGestureProps {
11
24
  onPointerDown(event: PointerEvent<Element>): void;
12
25
  onFocus(event: FocusEvent<Element>): void;
@@ -83,6 +96,12 @@ interface DragOverlay<G> {
83
96
  readonly target: DropTarget<G> | null;
84
97
  /** A registered zone landing; absent for interactions with no zone destination. */
85
98
  readonly landing: PositionTarget<IdOf<G, "zoneId">, ZoneHostId<G>> | null;
99
+ /**
100
+ * Draw the copy at `fit.scale` once it is known, in place of its lifted
101
+ * size, and as its back while `fit.concealed`. The ref already moves it
102
+ * into a pile's slot while `fit.snapped`.
103
+ */
104
+ readonly fit: DragFit;
86
105
  /** Attach to a fixed-position copy of the card; it follows the pointer. */
87
106
  readonly ref: (element: HTMLElement | null) => (() => void) | undefined;
88
107
  }
@@ -155,4 +174,4 @@ declare function useMarqueeSelection<Id extends string>(options: {
155
174
  };
156
175
  };
157
176
 
158
- export { type CardGesture, type CardGestureOptions, type CardGestureProps, type CardPresentation, type CardRow, type DragOverlay, type DropArea, DropAreaBinding, type DropAreaOptions, type MarqueeBounds, type SelectionOptions, type ZonePresentation, createGameHook, useMarqueeSelection };
177
+ export { type CardGesture, type CardGestureOptions, type CardGestureProps, type CardPresentation, type CardRow, type DragFit, type DragOverlay, type DropArea, DropAreaBinding, type DropAreaOptions, type MarqueeBounds, type SelectionOptions, type ZonePresentation, createGameHook, useMarqueeSelection };
package/dist/react.js CHANGED
@@ -93,6 +93,165 @@ import {
93
93
  createContext,
94
94
  useContext
95
95
  } from "react";
96
+
97
+ // src/headless/drop-snap.ts
98
+ var SNAP = Object.freeze({
99
+ /** A pile catches a card whose centre comes this close to its own. */
100
+ slotCatch: 0.75,
101
+ /** A caught card leaves the pile beyond this. */
102
+ slotRelease: 1.15,
103
+ /** A finger reaches this much further. */
104
+ coarseReach: 0.2,
105
+ /** Another pile takes a caught card only when this much closer. */
106
+ slotSwitch: 0.1,
107
+ /** A card enters a zone once this share of it covers the zone... */
108
+ zoneEnter: 0.35,
109
+ /** ...and leaves it once less than this share does. */
110
+ zoneLeave: 0.1,
111
+ /** How much a card in a pile still follows the pointer. */
112
+ slotGive: 0.15,
113
+ /** How much a card held inside a zone's edge follows the pointer beyond it. */
114
+ edgeGive: 0.25
115
+ });
116
+ var centerOf = (box) => ({
117
+ x: box.x + box.width / 2,
118
+ y: box.y + box.height / 2
119
+ });
120
+ var toward = (from, to, share) => ({
121
+ x: from.x + (to.x - from.x) * share,
122
+ y: from.y + (to.y - from.y) * share
123
+ });
124
+ var contains = (box, point) => point.x >= box.x && point.x <= box.x + box.width && point.y >= box.y && point.y <= box.y + box.height;
125
+ function cover(card, box) {
126
+ const width = Math.min(card.x + card.width, box.x + box.width) - Math.max(card.x, box.x);
127
+ const height = Math.min(card.y + card.height, box.y + box.height) - Math.max(card.y, box.y);
128
+ return width > 0 && height > 0 ? width * height / (card.width * card.height) : 0;
129
+ }
130
+ function snapDrop({
131
+ pointer,
132
+ card,
133
+ areas,
134
+ hit,
135
+ retained,
136
+ coarse
137
+ }) {
138
+ const free = centerOf(card);
139
+ const inSlot = (index, box) => ({
140
+ index,
141
+ center: toward(centerOf(box), free, SNAP.slotGive),
142
+ held: true
143
+ });
144
+ let best = -1;
145
+ let score = Infinity;
146
+ areas.forEach((area, index) => {
147
+ const box = area?.slot ? area.box : null;
148
+ if (!box?.width || !box.height) return;
149
+ const middle = centerOf(box);
150
+ const distance = Math.hypot(
151
+ (free.x - middle.x) / box.width,
152
+ (free.y - middle.y) / box.height
153
+ );
154
+ const kept2 = index === retained;
155
+ const reach = (kept2 ? SNAP.slotRelease : SNAP.slotCatch) + (coarse ? SNAP.coarseReach : 0);
156
+ const value = kept2 ? distance - SNAP.slotSwitch : distance;
157
+ if (distance <= reach && value < score) {
158
+ best = index;
159
+ score = value;
160
+ }
161
+ });
162
+ if (best >= 0) return inSlot(best, areas[best].box);
163
+ const under = hit >= 0 ? areas[hit] : null;
164
+ if (under)
165
+ return under.slot && under.box?.width ? inSlot(hit, under.box) : { index: hit, center: free, held: false };
166
+ const hold = (index, box) => {
167
+ if (contains(box, pointer)) return { index, center: free, held: false };
168
+ const inside = {
169
+ x: Math.max(box.x, Math.min(free.x, box.x + box.width)),
170
+ y: Math.max(box.y, Math.min(free.y, box.y + box.height))
171
+ };
172
+ return { index, center: toward(inside, free, SNAP.edgeGive), held: true };
173
+ };
174
+ const zone = (index) => {
175
+ const area = areas[index];
176
+ return area && !area.slot && area.box?.width ? area.box : null;
177
+ };
178
+ const kept = retained >= 0 ? zone(retained) : null;
179
+ if (kept && (cover(card, kept) >= SNAP.zoneLeave || magneticDropPoint(pointer, kept, true, coarse)))
180
+ return hold(retained, kept);
181
+ let chosen = -1;
182
+ let share = 0;
183
+ areas.forEach((_, index) => {
184
+ const box = zone(index);
185
+ if (!box) return;
186
+ const value = cover(card, box);
187
+ if (value < SNAP.zoneEnter && !magneticDropPoint(pointer, box, false, coarse))
188
+ return;
189
+ const deeper = chosen < 0 || areas[index].depth > areas[chosen].depth;
190
+ if (deeper || areas[index].depth === areas[chosen].depth && value > share) {
191
+ chosen = index;
192
+ share = value;
193
+ }
194
+ });
195
+ return chosen >= 0 ? hold(chosen, zone(chosen)) : { index: -1, center: free, held: false };
196
+ }
197
+
198
+ // src/react/gesture.tsx
199
+ var UNFIT = Object.freeze({
200
+ scale: null,
201
+ snapped: false,
202
+ concealed: false
203
+ });
204
+ var CATCH_UP_MS = 50;
205
+ var SCROLL_EDGE = { fine: 48, coarse: 64 };
206
+ var SCROLL_SPEED = 16;
207
+ var VISUALLY_HIDDEN = {
208
+ position: "fixed",
209
+ width: "1px",
210
+ height: "1px",
211
+ overflow: "hidden",
212
+ clipPath: "inset(50%)",
213
+ whiteSpace: "nowrap"
214
+ };
215
+ function landingIn(area) {
216
+ for (const element of area?.querySelectorAll("[data-drop-landing]") ?? [])
217
+ if (element instanceof HTMLElement && element.closest("[data-drop-area]") === area)
218
+ return element;
219
+ return null;
220
+ }
221
+ function visibleBox(element, clips) {
222
+ let { left, top, right, bottom } = element.getBoundingClientRect();
223
+ for (const clip of clips) {
224
+ const box = clip.getBoundingClientRect();
225
+ left = Math.max(left, box.left);
226
+ top = Math.max(top, box.top);
227
+ right = Math.min(right, box.right);
228
+ bottom = Math.min(bottom, box.bottom);
229
+ }
230
+ return right > left && bottom > top ? { x: left, y: top, width: right - left, height: bottom - top } : null;
231
+ }
232
+ function scrollStep(node, at, edge, frames) {
233
+ const root = node === document.scrollingElement;
234
+ const { overflowX, overflowY } = getComputedStyle(node);
235
+ const box = root ? { left: 0, top: 0, right: innerWidth, bottom: innerHeight } : node.getBoundingClientRect();
236
+ const speed = (gap) => gap < edge ? SCROLL_SPEED * frames * (1 - Math.max(0, gap) / edge) ** 2 : 0;
237
+ const axis = (overflow, position, room, before, after) => (root || overflow === "auto" || overflow === "scroll") && room > 0 ? (position < room ? speed(after) : 0) - (position > 0 ? speed(before) : 0) : 0;
238
+ return {
239
+ x: axis(
240
+ overflowX,
241
+ node.scrollLeft,
242
+ node.scrollWidth - node.clientWidth,
243
+ at.x - box.left,
244
+ box.right - at.x
245
+ ),
246
+ y: axis(
247
+ overflowY,
248
+ node.scrollTop,
249
+ node.scrollHeight - node.clientHeight,
250
+ at.y - box.top,
251
+ box.bottom - at.y
252
+ )
253
+ };
254
+ }
96
255
  var CARD_STYLE = Object.freeze({
97
256
  // Sideways movement stays native scrolling; everything else reaches the recognizer.
98
257
  touchAction: "pan-x",
@@ -152,6 +311,18 @@ function createGestureSession(game) {
152
311
  let domFocused = null;
153
312
  let alt = false;
154
313
  let pointer = { x: 0, y: 0 };
314
+ let goal = { x: 0, y: 0 };
315
+ let offset = { x: 0, y: 0 };
316
+ let lag = { x: 0, y: 0 };
317
+ let shown = { x: 0, y: 0 };
318
+ let held = false;
319
+ let reduced = false;
320
+ let frame = 0;
321
+ let lastFrame = 0;
322
+ let remeasure = false;
323
+ const located = /* @__PURE__ */ new Map();
324
+ const engaged = /* @__PURE__ */ new Set();
325
+ let announcer = null;
155
326
  let swallowing = false;
156
327
  let guardingClicks = false;
157
328
  let disposed = false;
@@ -209,57 +380,174 @@ function createGestureSession(game) {
209
380
  if (drag?.settling && (game.request === null || changedSnapshot || changedLifetime))
210
381
  set({ drag: null });
211
382
  });
212
- function hitArea(at) {
383
+ function hitAreas(at) {
213
384
  for (const element of document.elementsFromPoint(at.x, at.y)) {
214
385
  if (element.closest("[data-drag-overlay]")) continue;
215
- const area = element.closest("[data-drop-area]");
216
- if (area) return area.getAttribute("data-drop-area");
386
+ const ids = [];
387
+ for (let area = element.closest("[data-drop-area]"); area; area = area.parentElement?.closest("[data-drop-area]") ?? null)
388
+ ids.push(area.getAttribute("data-drop-area"));
389
+ if (ids.length) return ids;
217
390
  }
218
- return null;
391
+ return [];
219
392
  }
220
- function retarget(at) {
221
- if (!press?.dragging) return at;
222
- let area = hitArea(at);
223
- const read = area === null ? void 0 : areas.get(area);
224
- let target = read ? resolveDropArea(game.drag, read(at)) : null;
225
- let point = at;
226
- if (!target) {
227
- const candidates = [...areas].sort(
228
- ([left], [right]) => left === press?.area ? -1 : right === press?.area ? 1 : 0
229
- );
230
- let distance = Infinity;
231
- for (const [id2, binding] of candidates) {
232
- const resolved = resolveDropArea(game.drag, binding(at));
233
- if (!resolved || !game.drag?.getDropTargets().some((item) => sameDropTarget(item, resolved)))
234
- continue;
235
- const element = document.querySelector(
236
- `[data-drop-area="${CSS.escape(id2)}"]`
237
- );
238
- if (!(element instanceof HTMLElement)) continue;
239
- const snapped = magneticDropPoint(
240
- at,
241
- element.getBoundingClientRect(),
242
- id2 === press.area,
243
- press.pointerType === "touch"
244
- );
245
- if (!snapped) continue;
246
- const nextDistance = Math.hypot(at.x - snapped.x, at.y - snapped.y);
247
- if (nextDistance >= distance) continue;
248
- area = id2;
249
- target = resolved;
250
- point = snapped;
251
- distance = nextDistance;
252
- if (id2 === press.area) break;
253
- }
393
+ function locate(id2) {
394
+ const known = located.get(id2);
395
+ const element = known?.element?.isConnected ? known.element : document.querySelector(`[data-drop-area="${CSS.escape(id2)}"]`);
396
+ const landing = landingIn(element);
397
+ const slot = landing?.dataset.dropLanding === "slot" ? landing : null;
398
+ if (known && known.element === element && known.slot === slot) return known;
399
+ const clips = [];
400
+ for (let node = (slot ?? element)?.parentElement; node; node = node.parentElement) {
401
+ const { overflowX, overflowY } = getComputedStyle(node);
402
+ if (/auto|scroll|hidden|clip/.test(overflowX + overflowY))
403
+ clips.push(node);
404
+ }
405
+ let depth = 0;
406
+ for (let node = element?.parentElement; node; node = node.parentElement) {
407
+ if (node.hasAttribute("data-drop-area")) depth++;
408
+ }
409
+ const next = {
410
+ element,
411
+ clips,
412
+ slot,
413
+ depth
414
+ };
415
+ located.set(id2, next);
416
+ return next;
417
+ }
418
+ function destinationOf(area, target) {
419
+ const areaPresentation = area ? presentations.get(area)?.() : void 0;
420
+ const position = target?.kind === "position" ? target.value : null;
421
+ const zone = position ?? areaPresentation?.zone;
422
+ const presentation = zone ? [
423
+ areaPresentation,
424
+ ...Array.from(presentations.values(), (read) => read())
425
+ ].find(
426
+ (candidate) => candidate?.zone.zoneId === zone.zoneId && candidate.zone.hostId === zone.hostId
427
+ ) : void 0;
428
+ return { zone, position, presentation };
429
+ }
430
+ const conceals = (presentation, hostId) => presentation.visibility === "hidden" || presentation.visibility === "ownerOnly" && hostId !== game.me?.id;
431
+ function announce(area) {
432
+ const element = area === null ? null : locate(area).element;
433
+ const label = element?.getAttribute("aria-label") ?? element?.getAttribute("aria-labelledby")?.split(/\s+/).map((labelId) => document.getElementById(labelId)?.textContent).join(" ") ?? "";
434
+ if (!announcer?.isConnected) {
435
+ announcer = document.createElement("div");
436
+ announcer.setAttribute("role", "status");
437
+ Object.assign(announcer.style, VISUALLY_HIDDEN);
438
+ document.body.append(announcer);
254
439
  }
255
- if (area !== press.area || !sameDropTarget(target, press.target)) {
440
+ announcer.textContent = label;
441
+ }
442
+ function retarget(at) {
443
+ const drag = store.get().drag;
444
+ if (!press?.dragging || !drag) return;
445
+ pointer = at;
446
+ const ids = [...areas.keys()];
447
+ const found = ids.map((id2) => {
448
+ const target2 = resolveDropArea(game.drag, areas.get(id2)(at));
449
+ if (!target2) return null;
450
+ const { element, clips, slot, depth } = locate(id2);
451
+ const box = element instanceof HTMLElement ? visibleBox(slot ?? element, clips) : null;
452
+ return { target: target2, box, slot: slot !== null, depth };
453
+ });
454
+ const pickup = drag.width > 0 && drag.size.width > 0 ? drag.size.width / drag.width : 1;
455
+ const ratio = drag.fit.scale === null ? 1 : drag.fit.scale / pickup;
456
+ const width = drag.size.width * ratio;
457
+ const height = drag.size.height * ratio;
458
+ const free = {
459
+ x: at.x - (drag.grab.x - drag.size.width / 2) * ratio,
460
+ y: at.y - (drag.grab.y - drag.size.height / 2) * ratio
461
+ };
462
+ const snap = snapDrop({
463
+ pointer: at,
464
+ card: { x: free.x - width / 2, y: free.y - height / 2, width, height },
465
+ areas: found,
466
+ hit: ids.indexOf(hitAreas(at).find((id2) => found[ids.indexOf(id2)]) ?? ""),
467
+ retained: press.area === null ? -1 : ids.indexOf(press.area),
468
+ coarse: press.pointerType === "touch"
469
+ });
470
+ const area = snap.index < 0 ? null : ids[snap.index];
471
+ const target = snap.index < 0 ? null : found[snap.index].target;
472
+ const changed = area !== press.area || !sameDropTarget(target, press.target);
473
+ if (changed) {
256
474
  press.area = area;
257
475
  press.target = target;
258
476
  game.drag?.setDropTarget(target);
259
- const drag = store.get().drag;
260
- if (drag) set({ drag: { ...drag, area, target } });
477
+ announce(area);
478
+ remeasure = true;
261
479
  }
262
- return point;
480
+ const { zone, presentation } = destinationOf(area, target);
481
+ const place = area === null ? null : locate(area);
482
+ const landing = place && (place.slot ?? landingIn(place.element));
483
+ const fit = {
484
+ scale: landing && drag.width > 0 ? landing.offsetWidth / drag.width : drag.fit.scale,
485
+ snapped: snap.held && !!found[snap.index]?.slot,
486
+ concealed: !!presentation && !!zone && conceals(presentation, zone.hostId)
487
+ };
488
+ const refit = Math.abs((fit.scale ?? 0) - (drag.fit.scale ?? 0)) > 0.01 || fit.snapped !== drag.fit.snapped || fit.concealed !== drag.fit.concealed;
489
+ if (changed || refit)
490
+ set({
491
+ drag: {
492
+ ...drag,
493
+ area,
494
+ target,
495
+ fit: refit ? fit : drag.fit
496
+ }
497
+ });
498
+ draw(snap.center, changed || refit || snap.held !== held);
499
+ held = snap.held;
500
+ schedule();
501
+ }
502
+ function draw(next, jump) {
503
+ const nextOffset = { x: next.x - pointer.x, y: next.y - pointer.y };
504
+ if (jump && !reduced)
505
+ lag = {
506
+ x: lag.x + offset.x - nextOffset.x,
507
+ y: lag.y + offset.y - nextOffset.y
508
+ };
509
+ offset = nextOffset;
510
+ goal = next;
511
+ show();
512
+ if (lag.x || lag.y) schedule();
513
+ }
514
+ function show() {
515
+ shown = { x: goal.x + lag.x, y: goal.y + lag.y };
516
+ for (const follow of followers) follow(shown);
517
+ }
518
+ function schedule() {
519
+ if (frame || disposed) return;
520
+ lastFrame = performance.now();
521
+ frame = requestAnimationFrame(tick);
522
+ }
523
+ function tick(now) {
524
+ frame = 0;
525
+ if (!store.get().drag) return;
526
+ const elapsed = Math.min(64, Math.max(0, now - lastFrame));
527
+ const decay = Math.exp(-elapsed / CATCH_UP_MS);
528
+ lag = Math.hypot(lag.x, lag.y) * decay < 0.5 ? { x: 0, y: 0 } : { x: lag.x * decay, y: lag.y * decay };
529
+ const scrolling = press?.dragging ? autoScroll(elapsed / 16.7) : false;
530
+ if (scrolling || remeasure) {
531
+ remeasure = false;
532
+ retarget(pointer);
533
+ } else show();
534
+ if (lag.x || lag.y || scrolling) schedule();
535
+ }
536
+ function autoScroll(frames) {
537
+ const edge = SCROLL_EDGE[press?.pointerType === "touch" ? "coarse" : "fine"];
538
+ const top = document.elementsFromPoint(pointer.x, pointer.y).find((element) => !element.closest("[data-drag-overlay]"));
539
+ for (let node = top ?? null; node; node = node.parentElement) {
540
+ const step = scrollStep(node, pointer, edge, frames);
541
+ if (!step.x && !step.y) {
542
+ if (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth)
543
+ engaged.add(node);
544
+ continue;
545
+ }
546
+ if (!engaged.has(node)) return false;
547
+ node.scrollBy(step.x, step.y);
548
+ return true;
549
+ }
550
+ return false;
263
551
  }
264
552
  function swallow(event) {
265
553
  if (!swallowing || event.detail === 0) return;
@@ -386,12 +674,26 @@ function createGestureSession(game) {
386
674
  clearHover();
387
675
  focused = null;
388
676
  press.dragging = true;
677
+ located.clear();
678
+ engaged.clear();
679
+ if (announcer) announcer.textContent = "";
680
+ reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
681
+ held = false;
682
+ remeasure = false;
683
+ lag = { x: 0, y: 0 };
684
+ goal = shown = {
685
+ x: box.left + box.width / 2,
686
+ y: box.top + box.height / 2
687
+ };
688
+ offset = { x: goal.x - origin.x, y: goal.y - origin.y };
389
689
  set({
390
690
  drag: {
391
691
  cardIds: game.drag?.active?.cardIds ?? [cardId2],
392
692
  cardId: cardId2,
393
693
  grab: { x: origin.x - box.left, y: origin.y - box.top },
394
694
  size: { width: box.width, height: box.height },
695
+ width: element2 instanceof HTMLElement ? element2.offsetWidth : 0,
696
+ fit: UNFIT,
395
697
  settling: false,
396
698
  area: null,
397
699
  target: null,
@@ -402,10 +704,7 @@ function createGestureSession(game) {
402
704
  });
403
705
  return true;
404
706
  },
405
- dragMove(at) {
406
- pointer = retarget(at);
407
- for (const follow of followers) follow(pointer);
408
- },
707
+ dragMove: retarget,
409
708
  browse: scrubTo,
410
709
  end(kind, at) {
411
710
  const dragging = press.dragging;
@@ -423,15 +722,10 @@ function createGestureSession(game) {
423
722
  }
424
723
  const before = game.request;
425
724
  const drag = store.get().drag;
426
- const areaPresentation = drag?.area ? presentations.get(drag.area)?.() : void 0;
427
- const position = drag?.target?.kind === "position" ? drag.target.value : null;
428
- const zone = position ?? areaPresentation?.zone;
429
- const presentation = zone ? [
430
- areaPresentation,
431
- ...Array.from(presentations.values(), (read) => read())
432
- ].find(
433
- (candidate) => candidate?.zone.zoneId === zone.zoneId && candidate.zone.hostId === zone.hostId
434
- ) : void 0;
725
+ const { zone, position, presentation } = destinationOf(
726
+ drag?.area ?? null,
727
+ drag?.target ?? null
728
+ );
435
729
  const destination = zone && game.zones.find(zone.zoneId, zone.hostId);
436
730
  const cards = drag?.cardIds.flatMap((id2) => {
437
731
  const card2 = game.cards.find(id2);
@@ -441,7 +735,7 @@ function createGestureSession(game) {
441
735
  zoneId: zone.zoneId,
442
736
  hostId: zone.hostId,
443
737
  index: position?.index ?? presentation?.index ?? destination.getCards().length,
444
- concealed: presentation ? presentation.visibility === "hidden" || presentation.visibility === "ownerOnly" && zone.hostId !== game.me?.id : cards.some(
738
+ concealed: presentation ? conceals(presentation, zone.hostId) : cards.some(
445
739
  (card2) => card2.zone !== zone.zoneId || card2.hostId !== zone.hostId || card2.hidden
446
740
  ),
447
741
  cards: Object.freeze(cards),
@@ -542,11 +836,11 @@ function createGestureSession(game) {
542
836
  width: px(drag.size.width),
543
837
  height: px(drag.size.height)
544
838
  });
545
- const follow = (at) => {
546
- element.style.left = px(at.x - drag.grab.x);
547
- element.style.top = px(at.y - drag.grab.y);
839
+ const follow = (center) => {
840
+ element.style.left = px(center.x - drag.size.width / 2);
841
+ element.style.top = px(center.y - drag.size.height / 2);
548
842
  };
549
- follow(pointer);
843
+ follow(shown);
550
844
  followers.add(follow);
551
845
  return () => {
552
846
  followers.delete(follow);
@@ -630,6 +924,8 @@ function createGestureSession(game) {
630
924
  dispose() {
631
925
  disposed = true;
632
926
  press?.recognizer.cancel();
927
+ cancelAnimationFrame(frame);
928
+ announcer?.remove();
633
929
  clearHover();
634
930
  removeEventListener("keydown", key);
635
931
  removeEventListener("keyup", key);
@@ -990,6 +1286,7 @@ function createGameHook() {
990
1286
  hostId: drag.landing.hostId,
991
1287
  index: drag.landing.index
992
1288
  } : null,
1289
+ fit: drag.fit,
993
1290
  ref: session.overlayRef
994
1291
  } : null;
995
1292
  }