@dreamboard-games/sdk 0.5.0-alpha.2 → 0.5.0-alpha.21
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/README.md +462 -53
- package/dist/board-element-CqtxU5VZ.d.ts +13 -0
- package/dist/chunk-3RFGPBES.js +1792 -0
- package/dist/chunk-3RFGPBES.js.map +1 -0
- package/dist/chunk-CZRPUWCT.js +1602 -0
- package/dist/chunk-CZRPUWCT.js.map +1 -0
- package/dist/chunk-DMSXVMYY.js +7199 -0
- package/dist/chunk-DMSXVMYY.js.map +1 -0
- package/dist/chunk-U6R7FAH4.js +630 -0
- package/dist/chunk-U6R7FAH4.js.map +1 -0
- package/dist/chunk-YT3RPO5J.js +635 -0
- package/dist/chunk-YT3RPO5J.js.map +1 -0
- package/dist/diagnostics-ZAPEBlJP.d.ts +64 -0
- package/dist/extract-CwJWDrSD.d.ts +6850 -0
- package/dist/index.d.ts +5177 -1
- package/dist/index.js +1990 -6
- package/dist/index.js.map +1 -1
- package/dist/react.d.ts +158 -0
- package/dist/react.js +1119 -0
- package/dist/react.js.map +1 -0
- package/dist/reducer.d.ts +1028 -97
- package/dist/reducer.js +4892 -125
- package/dist/reducer.js.map +1 -1
- package/dist/shortcuts-B_RrufOM.d.ts +822 -0
- package/dist/testing.d.ts +529 -569
- package/dist/testing.js +2588 -934
- package/dist/testing.js.map +1 -1
- package/dist/types-BxXRINRF.d.ts +118 -0
- package/package.json +39 -144
- package/dist/HandView-Bi2lxzQP.d.ts +0 -79
- package/dist/ResourceCounter-BFxJknqp.d.ts +0 -98
- package/dist/ThemeProvider-CElaL2ik.d.ts +0 -99
- package/dist/attributes-DbvyMbXw.d.ts +0 -68
- package/dist/browser-interaction.d.ts +0 -910
- package/dist/browser-interaction.js +0 -119
- package/dist/browser-interaction.js.map +0 -1
- package/dist/chunk-32PFKDV7.js +0 -11705
- package/dist/chunk-32PFKDV7.js.map +0 -1
- package/dist/chunk-3BAD4VC2.js +0 -263
- package/dist/chunk-3BAD4VC2.js.map +0 -1
- package/dist/chunk-3SKDNDPG.js +0 -104
- package/dist/chunk-3SKDNDPG.js.map +0 -1
- package/dist/chunk-5YV2WHJ4.js +0 -25
- package/dist/chunk-5YV2WHJ4.js.map +0 -1
- package/dist/chunk-ANRB4XKA.js +0 -90
- package/dist/chunk-ANRB4XKA.js.map +0 -1
- package/dist/chunk-BI4G3PM2.js +0 -1864
- package/dist/chunk-BI4G3PM2.js.map +0 -1
- package/dist/chunk-EVCLTY4Q.js +0 -935
- package/dist/chunk-EVCLTY4Q.js.map +0 -1
- package/dist/chunk-GDPKTBUR.js +0 -564
- package/dist/chunk-GDPKTBUR.js.map +0 -1
- package/dist/chunk-H6VDGFL5.js +0 -2903
- package/dist/chunk-H6VDGFL5.js.map +0 -1
- package/dist/chunk-LR3ZTWQF.js +0 -5960
- package/dist/chunk-LR3ZTWQF.js.map +0 -1
- package/dist/chunk-PF7L4BMG.js +0 -117
- package/dist/chunk-PF7L4BMG.js.map +0 -1
- package/dist/chunk-PN5O6GG2.js +0 -32
- package/dist/chunk-PN5O6GG2.js.map +0 -1
- package/dist/chunk-PZ5AY32C.js +0 -10
- package/dist/chunk-PZ5AY32C.js.map +0 -1
- package/dist/chunk-S2JEDVWH.js +0 -7165
- package/dist/chunk-S2JEDVWH.js.map +0 -1
- package/dist/chunk-T3ZKNUZ7.js +0 -1
- package/dist/chunk-T3ZKNUZ7.js.map +0 -1
- package/dist/chunk-UGH54WZ2.js +0 -223
- package/dist/chunk-UGH54WZ2.js.map +0 -1
- package/dist/chunk-VCDWP5VL.js +0 -746
- package/dist/chunk-VCDWP5VL.js.map +0 -1
- package/dist/chunk-VDXOF4FW.js +0 -69
- package/dist/chunk-VDXOF4FW.js.map +0 -1
- package/dist/chunk-WHR5UW3F.js +0 -1988
- package/dist/chunk-WHR5UW3F.js.map +0 -1
- package/dist/chunk-Y75CFE77.js +0 -17
- package/dist/chunk-Y75CFE77.js.map +0 -1
- package/dist/chunk-Z7QCREWI.js +0 -3435
- package/dist/chunk-Z7QCREWI.js.map +0 -1
- package/dist/chunk-ZZGN2ATS.js +0 -1941
- package/dist/chunk-ZZGN2ATS.js.map +0 -1
- package/dist/components-gAbIz7hK.d.ts +0 -1284
- package/dist/definitions-BgmV_GhC.d.ts +0 -300
- package/dist/diagnostics-DIi3ina7.d.ts +0 -50
- package/dist/digest.d.ts +0 -8
- package/dist/game-CsZScpcn.d.ts +0 -763
- package/dist/hex-board-view-aKkblDp6.d.ts +0 -1228
- package/dist/index-CcT9Q7N7.d.ts +0 -193
- package/dist/index.d-DjzoK7zn.d.ts +0 -2101
- package/dist/package-set.d.ts +0 -13
- package/dist/package-set.js +0 -12
- package/dist/package-set.js.map +0 -1
- package/dist/player-state-Cqpyeql0.d.ts +0 -371
- package/dist/plugin-runtime-contract.d.ts +0 -17
- package/dist/plugin-runtime-contract.js +0 -92
- package/dist/plugin-runtime-contract.js.map +0 -1
- package/dist/primitive-props-BNHDkgd7.d.ts +0 -16
- package/dist/protocol-dYgafTYY.d.ts +0 -314
- package/dist/reducer/advanced.d.ts +0 -82
- package/dist/reducer/advanced.js +0 -51
- package/dist/reducer/advanced.js.map +0 -1
- package/dist/reducer-contract.d.ts +0 -11
- package/dist/reducer-contract.js +0 -16
- package/dist/reducer-contract.js.map +0 -1
- package/dist/reference-games/index.d.ts +0 -31
- package/dist/reference-games/index.js +0 -48
- package/dist/reference-games/index.js.map +0 -1
- package/dist/runtime/primitives.d.ts +0 -250
- package/dist/runtime/primitives.js +0 -189
- package/dist/runtime/primitives.js.map +0 -1
- package/dist/runtime/runtime-api.d.ts +0 -2
- package/dist/runtime/runtime-api.js +0 -1
- package/dist/runtime/runtime-api.js.map +0 -1
- package/dist/runtime/workspace-contract.d.ts +0 -392
- package/dist/runtime/workspace-contract.js +0 -28
- package/dist/runtime/workspace-contract.js.map +0 -1
- package/dist/runtime-RJ5orDJU.d.ts +0 -1699
- package/dist/runtime-api-Bz5pwNU_.d.ts +0 -296
- package/dist/runtime-json-CQ9QbLZ5.d.ts +0 -5
- package/dist/runtime.d.ts +0 -83
- package/dist/runtime.js +0 -242
- package/dist/runtime.js.map +0 -1
- package/dist/schema.d.ts +0 -379
- package/dist/stale-contract-artifact-error-XLaweZtF.d.ts +0 -18
- package/dist/testing-compiler.d.ts +0 -37
- package/dist/testing-compiler.js +0 -272
- package/dist/testing-compiler.js.map +0 -1
- package/dist/testing-runtime.d.ts +0 -119
- package/dist/testing-runtime.js +0 -212
- package/dist/testing-runtime.js.map +0 -1
- package/dist/types-DJj5MJkl.d.ts +0 -256
- package/dist/types-DR7DoB1x.d.ts +0 -28
- package/dist/types-JWCYHmu7.d.ts +0 -122
- package/dist/types.d.ts +0 -1958
- package/dist/types.js +0 -14
- package/dist/types.js.map +0 -1
- package/dist/ui/components.d.ts +0 -17
- package/dist/ui/components.js +0 -216
- package/dist/ui/components.js.map +0 -1
- package/dist/ui/defaults.d.ts +0 -19
- package/dist/ui/defaults.js +0 -104
- package/dist/ui/defaults.js.map +0 -1
- package/dist/ui/player-state.d.ts +0 -2
- package/dist/ui/player-state.js +0 -1
- package/dist/ui/player-state.js.map +0 -1
- package/dist/ui/plugin-styles.css +0 -2
- package/dist/ui-contract-GTUPkf8z.d.ts +0 -1168
- package/dist/ui.d.ts +0 -317
- package/dist/ui.js +0 -277
- package/dist/ui.js.map +0 -1
- package/dist/views-BpuWcOyN.d.ts +0 -1641
package/dist/react.js
ADDED
|
@@ -0,0 +1,1119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createGameInstance,
|
|
3
|
+
createGestureRecognizer,
|
|
4
|
+
isSameDropTarget,
|
|
5
|
+
magneticDropPoint
|
|
6
|
+
} from "./chunk-3RFGPBES.js";
|
|
7
|
+
import {
|
|
8
|
+
inputValueKey
|
|
9
|
+
} from "./chunk-CZRPUWCT.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:not([type='radio']), 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
|
+
let source = game.getOptions().source;
|
|
42
|
+
let seat = game.snapshot?.me;
|
|
43
|
+
let lifetime = 0;
|
|
44
|
+
const unsubscribe = game.subscribe(() => {
|
|
45
|
+
const nextSource = game.getOptions().source;
|
|
46
|
+
const nextSeat = game.snapshot?.me;
|
|
47
|
+
if (source === nextSource && seat === nextSeat) return;
|
|
48
|
+
source = nextSource;
|
|
49
|
+
seat = nextSeat;
|
|
50
|
+
++lifetime;
|
|
51
|
+
});
|
|
52
|
+
function keydown(event) {
|
|
53
|
+
if (!canHandle(event)) return;
|
|
54
|
+
const started = lifetime;
|
|
55
|
+
const active = () => mounted && lifetime === started;
|
|
56
|
+
const pending = controller.handle(
|
|
57
|
+
event.key,
|
|
58
|
+
session.getActiveTarget(),
|
|
59
|
+
session.getShortcutActivity()
|
|
60
|
+
);
|
|
61
|
+
if (!pending) return;
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
void pending.then(
|
|
64
|
+
(result) => {
|
|
65
|
+
if (active()) latest.current.onResult(result);
|
|
66
|
+
},
|
|
67
|
+
(cause) => {
|
|
68
|
+
if (active())
|
|
69
|
+
latest.current.onResult(
|
|
70
|
+
cause instanceof Error ? cause : new Error(String(cause))
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
window.addEventListener("keydown", keydown);
|
|
76
|
+
return () => {
|
|
77
|
+
mounted = false;
|
|
78
|
+
unsubscribe();
|
|
79
|
+
window.removeEventListener("keydown", keydown);
|
|
80
|
+
registration.current = null;
|
|
81
|
+
owned.dispose();
|
|
82
|
+
};
|
|
83
|
+
}, [game, controller, session]);
|
|
84
|
+
useLayoutEffect(() => {
|
|
85
|
+
registration.current?.update(options);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/react/gesture.tsx
|
|
90
|
+
import { createStore } from "@tanstack/store";
|
|
91
|
+
import { useSelector } from "@tanstack/react-store";
|
|
92
|
+
import {
|
|
93
|
+
createContext,
|
|
94
|
+
useContext
|
|
95
|
+
} from "react";
|
|
96
|
+
var CARD_STYLE = Object.freeze({
|
|
97
|
+
// Sideways movement stays native scrolling; everything else reaches the recognizer.
|
|
98
|
+
touchAction: "pan-x",
|
|
99
|
+
userSelect: "none",
|
|
100
|
+
WebkitUserSelect: "none",
|
|
101
|
+
WebkitTouchCallout: "none"
|
|
102
|
+
});
|
|
103
|
+
var INSPECTION_STYLE = Object.freeze({
|
|
104
|
+
...CARD_STYLE,
|
|
105
|
+
// Inspection-only cards should not block their table's native scrolling.
|
|
106
|
+
touchAction: "manipulation"
|
|
107
|
+
});
|
|
108
|
+
var ROW_STYLE = Object.freeze({
|
|
109
|
+
// A row that fits never scrolls; its cards' sideways movement scrubs instead.
|
|
110
|
+
touchAction: "none"
|
|
111
|
+
});
|
|
112
|
+
var px = (value) => `${value}px`;
|
|
113
|
+
function resolveDropArea(drag, binding) {
|
|
114
|
+
if (!drag || !binding) return null;
|
|
115
|
+
if ("interactionKey" in binding)
|
|
116
|
+
return drag.getIsDropTarget(binding) ? binding : null;
|
|
117
|
+
const { position } = binding;
|
|
118
|
+
const matches = drag.getDropTargets().filter(
|
|
119
|
+
(target2) => target2.kind === (position ? "position" : "interaction") && target2.interactionKey === binding.interaction && (!binding.input || target2.cardInputKey === binding.input) && (!position || target2.kind === "position" && inputValueKey(target2.value) === inputValueKey(position))
|
|
120
|
+
);
|
|
121
|
+
if (position)
|
|
122
|
+
return matches.length === 1 && drag.getIsDropTarget(matches[0]) ? matches[0] : null;
|
|
123
|
+
if (matches.length !== 1) return null;
|
|
124
|
+
const target = {
|
|
125
|
+
...matches[0],
|
|
126
|
+
...binding.params === void 0 ? {} : { params: binding.params }
|
|
127
|
+
};
|
|
128
|
+
return drag.getIsDropTarget(target) ? target : null;
|
|
129
|
+
}
|
|
130
|
+
var sameDropTarget = (left, right) => left === right || !!left && !!right && isSameDropTarget(left, right);
|
|
131
|
+
var sessions = 0;
|
|
132
|
+
function createGestureSession(game) {
|
|
133
|
+
const id = `g${++sessions}`;
|
|
134
|
+
const store = createStore({
|
|
135
|
+
activeTarget: null,
|
|
136
|
+
focusedTarget: null,
|
|
137
|
+
pointerActive: false,
|
|
138
|
+
drag: null,
|
|
139
|
+
inspect: null
|
|
140
|
+
});
|
|
141
|
+
const targets = /* @__PURE__ */ new Map();
|
|
142
|
+
const sameTarget = (left, right) => inputValueKey(left) === inputValueKey(right);
|
|
143
|
+
const areas = /* @__PURE__ */ new Map();
|
|
144
|
+
const presentations = /* @__PURE__ */ new Map();
|
|
145
|
+
const rows = /* @__PURE__ */ new Map();
|
|
146
|
+
const controls = /* @__PURE__ */ new Map();
|
|
147
|
+
const followers = /* @__PURE__ */ new Set();
|
|
148
|
+
let press = null;
|
|
149
|
+
let hover = null;
|
|
150
|
+
let hoverPoint = null;
|
|
151
|
+
let focused = null;
|
|
152
|
+
let domFocused = null;
|
|
153
|
+
let alt = false;
|
|
154
|
+
let pointer = { x: 0, y: 0 };
|
|
155
|
+
let swallowing = false;
|
|
156
|
+
let guardingClicks = false;
|
|
157
|
+
let disposed = false;
|
|
158
|
+
const set = (patch) => store.setState((previous) => {
|
|
159
|
+
const next = { ...previous, ...patch };
|
|
160
|
+
return {
|
|
161
|
+
...next,
|
|
162
|
+
focusedTarget: domFocused,
|
|
163
|
+
pointerActive: press !== null || next.drag !== null
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
let snapshot = game.snapshot;
|
|
167
|
+
let source = game.getOptions().source;
|
|
168
|
+
function admitted(target) {
|
|
169
|
+
if (!target || !game.snapshot) return null;
|
|
170
|
+
const zones = game.snapshot.frame.zones;
|
|
171
|
+
if (target.kind === "zone")
|
|
172
|
+
return zones[target.zoneId]?.[target.hostId] ? target : null;
|
|
173
|
+
if (target.kind === "card")
|
|
174
|
+
return Object.entries(snapshot?.frame.zones ?? {}).some(
|
|
175
|
+
([zoneId, hosts]) => Object.entries(hosts).some(
|
|
176
|
+
([hostId, zone]) => zone.cardIds.includes(target.value) && zones[zoneId]?.[hostId]?.cardIds.includes(target.value)
|
|
177
|
+
)
|
|
178
|
+
) ? target : null;
|
|
179
|
+
return [...targets.values()].some((read) => sameTarget(read(), target)) ? target : null;
|
|
180
|
+
}
|
|
181
|
+
function admittedFocus(target) {
|
|
182
|
+
if (!target || !sameTarget(target, targetOf(document.activeElement)))
|
|
183
|
+
return null;
|
|
184
|
+
if (target.kind !== "card") return admitted(target);
|
|
185
|
+
return Object.values(game.snapshot?.frame.zones ?? {}).some(
|
|
186
|
+
(hosts) => Object.values(hosts).some((zone) => zone.cardIds.includes(target.value))
|
|
187
|
+
) ? target : null;
|
|
188
|
+
}
|
|
189
|
+
const unsubscribe = game.subscribe(() => {
|
|
190
|
+
const nextSource = game.getOptions().source;
|
|
191
|
+
const changedLifetime = source !== nextSource || snapshot?.me !== game.snapshot?.me;
|
|
192
|
+
const changedSnapshot = snapshot !== game.snapshot;
|
|
193
|
+
if (changedLifetime || snapshot !== game.snapshot) {
|
|
194
|
+
press?.recognizer.cancel();
|
|
195
|
+
const nextHover = changedLifetime ? null : admitted(hover);
|
|
196
|
+
const nextFocused = changedLifetime ? null : admitted(focused);
|
|
197
|
+
const nextDomFocused = changedLifetime ? null : admittedFocus(domFocused);
|
|
198
|
+
source = nextSource;
|
|
199
|
+
snapshot = game.snapshot;
|
|
200
|
+
alt = false;
|
|
201
|
+
hover = nextHover;
|
|
202
|
+
focused = nextFocused;
|
|
203
|
+
domFocused = nextDomFocused;
|
|
204
|
+
if (changedLifetime) hoverPoint = null;
|
|
205
|
+
set({ activeTarget: hover ?? focused, inspect: null });
|
|
206
|
+
}
|
|
207
|
+
if (press?.dragging && !game.drag?.active) press.recognizer.cancel();
|
|
208
|
+
const drag = store.get().drag;
|
|
209
|
+
if (drag?.settling && (game.request === null || changedSnapshot || changedLifetime))
|
|
210
|
+
set({ drag: null });
|
|
211
|
+
});
|
|
212
|
+
function hitArea(at) {
|
|
213
|
+
for (const element of document.elementsFromPoint(at.x, at.y)) {
|
|
214
|
+
if (element.closest("[data-drag-overlay]")) continue;
|
|
215
|
+
const area = element.closest("[data-drop-area]");
|
|
216
|
+
if (area) return area.getAttribute("data-drop-area");
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
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
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (area !== press.area || !sameDropTarget(target, press.target)) {
|
|
256
|
+
press.area = area;
|
|
257
|
+
press.target = target;
|
|
258
|
+
game.drag?.setDropTarget(target);
|
|
259
|
+
const drag = store.get().drag;
|
|
260
|
+
if (drag) set({ drag: { ...drag, area, target } });
|
|
261
|
+
}
|
|
262
|
+
return point;
|
|
263
|
+
}
|
|
264
|
+
function swallow(event) {
|
|
265
|
+
if (!swallowing || event.detail === 0) return;
|
|
266
|
+
swallowing = false;
|
|
267
|
+
event.stopPropagation();
|
|
268
|
+
event.preventDefault();
|
|
269
|
+
}
|
|
270
|
+
function newPress() {
|
|
271
|
+
swallowing = false;
|
|
272
|
+
}
|
|
273
|
+
function swallowNextClick() {
|
|
274
|
+
swallowing = true;
|
|
275
|
+
if (guardingClicks) return;
|
|
276
|
+
guardingClicks = true;
|
|
277
|
+
addEventListener("click", swallow, true);
|
|
278
|
+
addEventListener("pointerdown", newPress, true);
|
|
279
|
+
}
|
|
280
|
+
function clearHover() {
|
|
281
|
+
hover = null;
|
|
282
|
+
}
|
|
283
|
+
function inspectWithAlt() {
|
|
284
|
+
const target = hover ?? focused;
|
|
285
|
+
const cardId = target?.kind === "card" ? target.value : null;
|
|
286
|
+
const pending = store.get().drag;
|
|
287
|
+
set({
|
|
288
|
+
activeTarget: target,
|
|
289
|
+
inspect: alt && cardId && !press && !(pending?.settling && pending.cardIds.includes(cardId)) ? { cardId, via: "hover" } : null
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
function targetOf(element) {
|
|
293
|
+
const hit = element?.closest("[data-card-gesture], [data-shortcut-target]");
|
|
294
|
+
const control = hit?.getAttribute("data-gesture-session") === id ? hit : null;
|
|
295
|
+
const targetId = control?.getAttribute("data-shortcut-target");
|
|
296
|
+
const cardId = control?.getAttribute("data-card-gesture");
|
|
297
|
+
return targetId ? targets.get(targetId)?.() ?? null : cardId ? { kind: "card", value: cardId } : null;
|
|
298
|
+
}
|
|
299
|
+
function hoverAt(at) {
|
|
300
|
+
hoverPoint = at;
|
|
301
|
+
const wasFocused = focused !== null;
|
|
302
|
+
focused = null;
|
|
303
|
+
const next = targetOf(document.elementFromPoint(at.x, at.y));
|
|
304
|
+
if (sameTarget(hover, next) && !wasFocused) return;
|
|
305
|
+
hover = next;
|
|
306
|
+
inspectWithAlt();
|
|
307
|
+
}
|
|
308
|
+
const browse = (event) => {
|
|
309
|
+
if (event.pointerType !== "mouse" || press) return;
|
|
310
|
+
if (hoverPoint?.x === event.clientX && hoverPoint.y === event.clientY)
|
|
311
|
+
return;
|
|
312
|
+
alt = event.altKey;
|
|
313
|
+
hoverAt({ x: event.clientX, y: event.clientY });
|
|
314
|
+
};
|
|
315
|
+
const key = (event) => {
|
|
316
|
+
if (event.key === "Escape") {
|
|
317
|
+
press?.recognizer.cancel();
|
|
318
|
+
alt = false;
|
|
319
|
+
set({ inspect: null });
|
|
320
|
+
} else if (event.key === "Alt") {
|
|
321
|
+
alt = event.type === "keydown";
|
|
322
|
+
inspectWithAlt();
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
const loseFocus = () => {
|
|
326
|
+
alt = false;
|
|
327
|
+
clearHover();
|
|
328
|
+
hoverPoint = null;
|
|
329
|
+
focused = null;
|
|
330
|
+
domFocused = null;
|
|
331
|
+
set({ activeTarget: null, inspect: null });
|
|
332
|
+
};
|
|
333
|
+
addEventListener("keydown", key);
|
|
334
|
+
addEventListener("keyup", key);
|
|
335
|
+
addEventListener("blur", loseFocus);
|
|
336
|
+
const leaveWindow = (event) => {
|
|
337
|
+
if (event.pointerType !== "mouse" || event.relatedTarget !== null) return;
|
|
338
|
+
clearHover();
|
|
339
|
+
hoverPoint = null;
|
|
340
|
+
focused = null;
|
|
341
|
+
alt = false;
|
|
342
|
+
inspectWithAlt();
|
|
343
|
+
};
|
|
344
|
+
addEventListener("pointermove", browse);
|
|
345
|
+
addEventListener("pointerout", leaveWindow);
|
|
346
|
+
function release() {
|
|
347
|
+
press?.detach();
|
|
348
|
+
press = null;
|
|
349
|
+
set({});
|
|
350
|
+
}
|
|
351
|
+
function start(event, cardId, element, options) {
|
|
352
|
+
if (disposed || press || event.button !== 0) return;
|
|
353
|
+
const pending = store.get().drag;
|
|
354
|
+
if (pending?.settling && pending.cardIds.includes(cardId)) return;
|
|
355
|
+
let card = { cardId, element, options };
|
|
356
|
+
const row = event.pointerType === "mouse" ? null : element.closest("[data-card-row]");
|
|
357
|
+
const cardAt = rows.get(row?.getAttribute("data-card-row") ?? "");
|
|
358
|
+
const scrubbing = cardAt !== void 0;
|
|
359
|
+
const activate = (next) => {
|
|
360
|
+
if ((hover?.kind === "card" ? hover.value : null) === next) return;
|
|
361
|
+
hover = next === null ? null : { kind: "card", value: next };
|
|
362
|
+
inspectWithAlt();
|
|
363
|
+
};
|
|
364
|
+
function scrubTo(at) {
|
|
365
|
+
const next = cardAt(at);
|
|
366
|
+
const control = next === null ? null : row.querySelector(`[data-card-gesture="${CSS.escape(next)}"]`);
|
|
367
|
+
const routes = controls.get(
|
|
368
|
+
control?.getAttribute("data-card-control") ?? ""
|
|
369
|
+
);
|
|
370
|
+
card = next === null || !control || !routes ? null : { cardId: next, element: control, options: routes() };
|
|
371
|
+
activate(card?.cardId ?? null);
|
|
372
|
+
}
|
|
373
|
+
const recognizer = createGestureRecognizer(
|
|
374
|
+
event,
|
|
375
|
+
{
|
|
376
|
+
// A hold comes before any movement, so the pressed card is still chosen.
|
|
377
|
+
hold() {
|
|
378
|
+
set({ inspect: { cardId, via: "hold" } });
|
|
379
|
+
},
|
|
380
|
+
dragStart(origin) {
|
|
381
|
+
if (!card) return false;
|
|
382
|
+
const { cardId: cardId2, element: element2, options: options2 } = card;
|
|
383
|
+
if (options2 === false || !game.drag?.begin(cardId2, options2))
|
|
384
|
+
return false;
|
|
385
|
+
const box = element2.getBoundingClientRect();
|
|
386
|
+
clearHover();
|
|
387
|
+
focused = null;
|
|
388
|
+
press.dragging = true;
|
|
389
|
+
set({
|
|
390
|
+
drag: {
|
|
391
|
+
cardIds: game.drag?.active?.cardIds ?? [cardId2],
|
|
392
|
+
cardId: cardId2,
|
|
393
|
+
grab: { x: origin.x - box.left, y: origin.y - box.top },
|
|
394
|
+
size: { width: box.width, height: box.height },
|
|
395
|
+
settling: false,
|
|
396
|
+
area: null,
|
|
397
|
+
target: null,
|
|
398
|
+
landing: null
|
|
399
|
+
},
|
|
400
|
+
inspect: null,
|
|
401
|
+
activeTarget: null
|
|
402
|
+
});
|
|
403
|
+
return true;
|
|
404
|
+
},
|
|
405
|
+
dragMove(at) {
|
|
406
|
+
pointer = retarget(at);
|
|
407
|
+
for (const follow of followers) follow(pointer);
|
|
408
|
+
},
|
|
409
|
+
browse: scrubTo,
|
|
410
|
+
end(kind, at) {
|
|
411
|
+
const dragging = press.dragging;
|
|
412
|
+
const pointerType = press.pointerType;
|
|
413
|
+
const chosen = scrubbing && kind === "browse" ? card?.element : null;
|
|
414
|
+
if (dragging) retarget(at);
|
|
415
|
+
release();
|
|
416
|
+
if (scrubbing) activate(null);
|
|
417
|
+
if (chosen instanceof HTMLElement) chosen.click();
|
|
418
|
+
if (kind !== "tap") swallowNextClick();
|
|
419
|
+
if (kind === "hold") set({ inspect: null });
|
|
420
|
+
if (!dragging) {
|
|
421
|
+
if (kind === "browse" && pointerType === "mouse") hoverAt(at);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const before = game.request;
|
|
425
|
+
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;
|
|
435
|
+
const destination = zone && game.zones.find(zone.zoneId, zone.hostId);
|
|
436
|
+
const cards = drag?.cardIds.flatMap((id2) => {
|
|
437
|
+
const card2 = game.cards.find(id2);
|
|
438
|
+
return card2 ? [card2] : [];
|
|
439
|
+
}) ?? [];
|
|
440
|
+
const landing = destination && zone && cards.length ? Object.freeze({
|
|
441
|
+
zoneId: zone.zoneId,
|
|
442
|
+
hostId: zone.hostId,
|
|
443
|
+
index: position?.index ?? presentation?.index ?? destination.getCards().length,
|
|
444
|
+
concealed: presentation ? presentation.visibility === "hidden" || presentation.visibility === "ownerOnly" && zone.hostId !== game.me?.id : cards.some(
|
|
445
|
+
(card2) => card2.zone !== zone.zoneId || card2.hostId !== zone.hostId || card2.hidden
|
|
446
|
+
),
|
|
447
|
+
cards: Object.freeze(cards),
|
|
448
|
+
snapshot: game.snapshot
|
|
449
|
+
}) : null;
|
|
450
|
+
game.drag?.drop();
|
|
451
|
+
if (drag && game.request !== null && game.request !== before)
|
|
452
|
+
set({ drag: { ...drag, settling: true, landing } });
|
|
453
|
+
else set({ drag: null });
|
|
454
|
+
},
|
|
455
|
+
cancel(kind) {
|
|
456
|
+
const dragging = press.dragging;
|
|
457
|
+
release();
|
|
458
|
+
if (scrubbing) activate(null);
|
|
459
|
+
if (kind !== "tap") swallowNextClick();
|
|
460
|
+
if (kind === "hold") set({ inspect: null });
|
|
461
|
+
if (dragging) {
|
|
462
|
+
game.drag?.cancel();
|
|
463
|
+
set({ drag: null });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
{ scrub: scrubbing }
|
|
468
|
+
);
|
|
469
|
+
const move = (next) => recognizer.move(next);
|
|
470
|
+
const up = (next) => recognizer.up(next);
|
|
471
|
+
const cancel = (next) => recognizer.cancel(next);
|
|
472
|
+
const blur = () => recognizer.cancel();
|
|
473
|
+
addEventListener("pointermove", move);
|
|
474
|
+
addEventListener("pointerup", up);
|
|
475
|
+
addEventListener("pointercancel", cancel);
|
|
476
|
+
addEventListener("blur", blur);
|
|
477
|
+
if (event.pointerType === "mouse") {
|
|
478
|
+
try {
|
|
479
|
+
element.setPointerCapture(event.pointerId);
|
|
480
|
+
} catch {
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
press = {
|
|
484
|
+
recognizer,
|
|
485
|
+
pointerType: event.pointerType,
|
|
486
|
+
dragging: false,
|
|
487
|
+
area: null,
|
|
488
|
+
target: null,
|
|
489
|
+
detach() {
|
|
490
|
+
removeEventListener("pointermove", move);
|
|
491
|
+
removeEventListener("pointerup", up);
|
|
492
|
+
removeEventListener("pointercancel", cancel);
|
|
493
|
+
removeEventListener("blur", blur);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
set({});
|
|
497
|
+
}
|
|
498
|
+
function cardProps(cardId, control, options, flags) {
|
|
499
|
+
return {
|
|
500
|
+
onPointerDown(event) {
|
|
501
|
+
if (event.pointerType === "mouse") {
|
|
502
|
+
hover = { kind: "card", value: cardId };
|
|
503
|
+
alt = event.altKey;
|
|
504
|
+
inspectWithAlt();
|
|
505
|
+
}
|
|
506
|
+
start(event.nativeEvent, cardId, event.currentTarget, options);
|
|
507
|
+
},
|
|
508
|
+
onFocus(event) {
|
|
509
|
+
focused = event.currentTarget.matches(":focus-visible") ? { kind: "card", value: cardId } : null;
|
|
510
|
+
domFocused = { kind: "card", value: cardId };
|
|
511
|
+
if (focused) hover = null;
|
|
512
|
+
inspectWithAlt();
|
|
513
|
+
},
|
|
514
|
+
onBlur() {
|
|
515
|
+
if (focused?.kind === "card" && focused.value === cardId)
|
|
516
|
+
focused = null;
|
|
517
|
+
if (domFocused?.kind === "card" && domFocused.value === cardId)
|
|
518
|
+
domFocused = null;
|
|
519
|
+
inspectWithAlt();
|
|
520
|
+
},
|
|
521
|
+
onContextMenu(event) {
|
|
522
|
+
if (press && press.pointerType !== "mouse" || swallowing)
|
|
523
|
+
event.preventDefault();
|
|
524
|
+
},
|
|
525
|
+
onDragStart: (event) => event.preventDefault(),
|
|
526
|
+
style: options === false ? INSPECTION_STYLE : CARD_STYLE,
|
|
527
|
+
"data-dragging": flags.dragging || void 0,
|
|
528
|
+
"data-inspecting": flags.inspecting ?? void 0,
|
|
529
|
+
"data-card-gesture": cardId,
|
|
530
|
+
"data-card-control": control,
|
|
531
|
+
"data-gesture-session": id
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function overlayRef(element) {
|
|
535
|
+
const drag = store.get().drag;
|
|
536
|
+
if (!element || !drag) return;
|
|
537
|
+
element.setAttribute("data-drag-overlay", "");
|
|
538
|
+
Object.assign(element.style, {
|
|
539
|
+
position: "fixed",
|
|
540
|
+
margin: "0",
|
|
541
|
+
pointerEvents: "none",
|
|
542
|
+
width: px(drag.size.width),
|
|
543
|
+
height: px(drag.size.height)
|
|
544
|
+
});
|
|
545
|
+
const follow = (at) => {
|
|
546
|
+
element.style.left = px(at.x - drag.grab.x);
|
|
547
|
+
element.style.top = px(at.y - drag.grab.y);
|
|
548
|
+
};
|
|
549
|
+
follow(pointer);
|
|
550
|
+
followers.add(follow);
|
|
551
|
+
return () => {
|
|
552
|
+
followers.delete(follow);
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
return {
|
|
556
|
+
id,
|
|
557
|
+
store,
|
|
558
|
+
cardProps,
|
|
559
|
+
getActiveCardId() {
|
|
560
|
+
const target = store.get().activeTarget;
|
|
561
|
+
return target?.kind === "card" ? target.value : null;
|
|
562
|
+
},
|
|
563
|
+
getActiveTarget: () => store.get().activeTarget,
|
|
564
|
+
getShortcutActivity: () => ({
|
|
565
|
+
// Removal can change DOM focus without emitting blur. Admit the current mounted control.
|
|
566
|
+
focusedTarget: sameTarget(domFocused, targetOf(document.activeElement)) ? domFocused : null,
|
|
567
|
+
pointerActive: store.get().pointerActive
|
|
568
|
+
}),
|
|
569
|
+
registerTarget(targetId, read) {
|
|
570
|
+
const target = read();
|
|
571
|
+
targets.set(targetId, read);
|
|
572
|
+
return () => {
|
|
573
|
+
targets.delete(targetId);
|
|
574
|
+
if (sameTarget(hover, target)) hover = null;
|
|
575
|
+
if (sameTarget(focused, target)) focused = null;
|
|
576
|
+
if (sameTarget(domFocused, target)) domFocused = null;
|
|
577
|
+
inspectWithAlt();
|
|
578
|
+
};
|
|
579
|
+
},
|
|
580
|
+
targetProps(targetId) {
|
|
581
|
+
return {
|
|
582
|
+
"data-shortcut-target": targetId,
|
|
583
|
+
"data-gesture-session": id,
|
|
584
|
+
onPointerDown(event) {
|
|
585
|
+
if (event.pointerType !== "mouse") return;
|
|
586
|
+
hover = targets.get(targetId)?.() ?? null;
|
|
587
|
+
alt = event.altKey;
|
|
588
|
+
inspectWithAlt();
|
|
589
|
+
},
|
|
590
|
+
onFocus(event) {
|
|
591
|
+
focused = event.currentTarget.matches(":focus-visible") ? targets.get(targetId)?.() ?? null : null;
|
|
592
|
+
domFocused = targets.get(targetId)?.() ?? null;
|
|
593
|
+
if (focused) hover = null;
|
|
594
|
+
inspectWithAlt();
|
|
595
|
+
},
|
|
596
|
+
onBlur() {
|
|
597
|
+
const target = targets.get(targetId)?.() ?? null;
|
|
598
|
+
if (sameTarget(focused, target)) focused = null;
|
|
599
|
+
if (sameTarget(domFocused, target)) domFocused = null;
|
|
600
|
+
inspectWithAlt();
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
},
|
|
604
|
+
overlayRef,
|
|
605
|
+
/** `read` returns the area's current binding; targets resolve when hit. */
|
|
606
|
+
registerArea(area, read, presentation) {
|
|
607
|
+
areas.set(area, read);
|
|
608
|
+
presentations.set(area, presentation);
|
|
609
|
+
return () => {
|
|
610
|
+
areas.delete(area);
|
|
611
|
+
presentations.delete(area);
|
|
612
|
+
};
|
|
613
|
+
},
|
|
614
|
+
/** `read` returns a mounted card control's current drag routes. */
|
|
615
|
+
registerControl(control, read) {
|
|
616
|
+
controls.set(control, read);
|
|
617
|
+
return () => {
|
|
618
|
+
controls.delete(control);
|
|
619
|
+
};
|
|
620
|
+
},
|
|
621
|
+
registerRow(row, cardAt) {
|
|
622
|
+
rows.set(row, cardAt);
|
|
623
|
+
return () => {
|
|
624
|
+
rows.delete(row);
|
|
625
|
+
};
|
|
626
|
+
},
|
|
627
|
+
rowProps(row) {
|
|
628
|
+
return { "data-card-row": row, style: ROW_STYLE };
|
|
629
|
+
},
|
|
630
|
+
dispose() {
|
|
631
|
+
disposed = true;
|
|
632
|
+
press?.recognizer.cancel();
|
|
633
|
+
clearHover();
|
|
634
|
+
removeEventListener("keydown", key);
|
|
635
|
+
removeEventListener("keyup", key);
|
|
636
|
+
removeEventListener("blur", loseFocus);
|
|
637
|
+
removeEventListener("pointermove", browse);
|
|
638
|
+
removeEventListener("pointerout", leaveWindow);
|
|
639
|
+
unsubscribe();
|
|
640
|
+
followers.clear();
|
|
641
|
+
targets.clear();
|
|
642
|
+
areas.clear();
|
|
643
|
+
presentations.clear();
|
|
644
|
+
rows.clear();
|
|
645
|
+
controls.clear();
|
|
646
|
+
if (!guardingClicks) return;
|
|
647
|
+
removeEventListener("click", swallow, true);
|
|
648
|
+
removeEventListener("pointerdown", newPress, true);
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
var GestureContext = createContext(null);
|
|
653
|
+
function useGestureSession() {
|
|
654
|
+
const session = useContext(GestureContext);
|
|
655
|
+
if (!session) throw new Error("Gesture hooks require their GameProvider.");
|
|
656
|
+
return session;
|
|
657
|
+
}
|
|
658
|
+
function useGestureState(session, select) {
|
|
659
|
+
return useSelector(session.store, select);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/react/create-game-hook.tsx
|
|
663
|
+
import {
|
|
664
|
+
createContext as createContext2,
|
|
665
|
+
useContext as useContext2,
|
|
666
|
+
useId,
|
|
667
|
+
useLayoutEffect as useLayoutEffect2,
|
|
668
|
+
useRef as useRef2,
|
|
669
|
+
useState,
|
|
670
|
+
useMemo
|
|
671
|
+
} from "react";
|
|
672
|
+
import { useSelector as useSelector2 } from "@tanstack/react-store";
|
|
673
|
+
import { jsx } from "react/jsx-runtime";
|
|
674
|
+
function createGameHook() {
|
|
675
|
+
return function(defaults) {
|
|
676
|
+
const Context = createContext2(null);
|
|
677
|
+
function GameProvider({ children, ...overrides }) {
|
|
678
|
+
const [owned, setOwned] = useState(null);
|
|
679
|
+
const lifetime = useRef2(null);
|
|
680
|
+
const options = {
|
|
681
|
+
...defaults,
|
|
682
|
+
...overrides
|
|
683
|
+
};
|
|
684
|
+
useLayoutEffect2(() => {
|
|
685
|
+
let current = lifetime.current;
|
|
686
|
+
if (!current) {
|
|
687
|
+
const instance = createGameInstance()(options);
|
|
688
|
+
current = {
|
|
689
|
+
instance,
|
|
690
|
+
// One bound game crosses into the framework adapter's erased runtime.
|
|
691
|
+
session: createGestureSession(instance),
|
|
692
|
+
generation: 0
|
|
693
|
+
};
|
|
694
|
+
lifetime.current = current;
|
|
695
|
+
setOwned({ instance, session: current.session });
|
|
696
|
+
}
|
|
697
|
+
const owned2 = current;
|
|
698
|
+
++owned2.generation;
|
|
699
|
+
return () => {
|
|
700
|
+
const generation = ++owned2.generation;
|
|
701
|
+
queueMicrotask(() => {
|
|
702
|
+
if (owned2.generation !== generation) return;
|
|
703
|
+
owned2.session.dispose();
|
|
704
|
+
owned2.instance.dispose();
|
|
705
|
+
if (lifetime.current === owned2) lifetime.current = null;
|
|
706
|
+
});
|
|
707
|
+
};
|
|
708
|
+
}, []);
|
|
709
|
+
useLayoutEffect2(() => {
|
|
710
|
+
lifetime.current?.instance.setOptions(options);
|
|
711
|
+
});
|
|
712
|
+
return owned ? /* @__PURE__ */ jsx(Context.Provider, { value: owned.instance, children: /* @__PURE__ */ jsx(GestureContext.Provider, { value: owned.session, children }) }) : null;
|
|
713
|
+
}
|
|
714
|
+
function useInstance() {
|
|
715
|
+
const instance = useContext2(Context);
|
|
716
|
+
if (!instance) throw new Error("useGame requires its GameProvider.");
|
|
717
|
+
return instance;
|
|
718
|
+
}
|
|
719
|
+
function useGame(selector, options) {
|
|
720
|
+
const instance = useInstance();
|
|
721
|
+
const selected = useSelector2(
|
|
722
|
+
instance.store,
|
|
723
|
+
(snapshot) => selector ? { value: selector(snapshot) } : { snapshot },
|
|
724
|
+
{
|
|
725
|
+
compare: (previous, next) => "value" in previous && "value" in next ? (options?.compare ?? Object.is)(previous.value, next.value) : "snapshot" in previous && "snapshot" in next && Object.is(previous.snapshot, next.snapshot)
|
|
726
|
+
}
|
|
727
|
+
);
|
|
728
|
+
return "value" in selected ? selected.value : instance;
|
|
729
|
+
}
|
|
730
|
+
function Subscribe({
|
|
731
|
+
selector,
|
|
732
|
+
compare,
|
|
733
|
+
children
|
|
734
|
+
}) {
|
|
735
|
+
return children(useGame(selector, { compare }));
|
|
736
|
+
}
|
|
737
|
+
const dragOf = (snapshot) => snapshot.drag;
|
|
738
|
+
function useCardGesture(cardId, options) {
|
|
739
|
+
const session = useGestureSession();
|
|
740
|
+
const control = `${session.id}${useId()}`;
|
|
741
|
+
const routes = options.drag;
|
|
742
|
+
const latestRoutes = useRef2(routes);
|
|
743
|
+
useLayoutEffect2(() => {
|
|
744
|
+
latestRoutes.current = routes;
|
|
745
|
+
});
|
|
746
|
+
useLayoutEffect2(
|
|
747
|
+
() => session.registerControl(control, () => latestRoutes.current),
|
|
748
|
+
[session, control]
|
|
749
|
+
);
|
|
750
|
+
const canDrag = useGame(
|
|
751
|
+
(snapshot) => routes !== false && (dragOf(snapshot)?.getCanDrag(cardId, routes) ?? false)
|
|
752
|
+
);
|
|
753
|
+
const dragging = useGestureState(
|
|
754
|
+
session,
|
|
755
|
+
(state) => state.drag?.cardIds.includes(cardId) ?? false
|
|
756
|
+
);
|
|
757
|
+
const pending = useGestureState(
|
|
758
|
+
session,
|
|
759
|
+
(state) => !!state.drag?.settling && state.drag.cardIds.includes(cardId)
|
|
760
|
+
);
|
|
761
|
+
const inspecting = useGestureState(
|
|
762
|
+
session,
|
|
763
|
+
(state) => state.inspect?.cardId === cardId ? state.inspect.via : null
|
|
764
|
+
);
|
|
765
|
+
const isActive = useGestureState(
|
|
766
|
+
session,
|
|
767
|
+
(state) => state.activeTarget?.kind === "card" && state.activeTarget.value === cardId
|
|
768
|
+
);
|
|
769
|
+
return {
|
|
770
|
+
props: session.cardProps(cardId, control, routes, {
|
|
771
|
+
dragging,
|
|
772
|
+
inspecting
|
|
773
|
+
}),
|
|
774
|
+
canDrag,
|
|
775
|
+
isDragging: dragging,
|
|
776
|
+
isPending: pending,
|
|
777
|
+
isActive,
|
|
778
|
+
inspecting
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
function useDropArea(binding, options) {
|
|
782
|
+
const session = useGestureSession();
|
|
783
|
+
const id = `${session.id}${useId()}`;
|
|
784
|
+
const erased = binding;
|
|
785
|
+
const fixed = typeof erased === "function" ? null : erased;
|
|
786
|
+
const target = useGame(
|
|
787
|
+
(snapshot) => resolveDropArea(dragOf(snapshot), fixed),
|
|
788
|
+
{ compare: sameDropTarget }
|
|
789
|
+
);
|
|
790
|
+
const isOver = useGame((snapshot) => {
|
|
791
|
+
const active = dragOf(snapshot)?.active?.target;
|
|
792
|
+
return !!target && sameDropTarget(active ?? null, target);
|
|
793
|
+
});
|
|
794
|
+
const following = useGestureState(
|
|
795
|
+
session,
|
|
796
|
+
(state) => typeof erased === "function" && state.drag?.area === id && state.drag.target !== null
|
|
797
|
+
);
|
|
798
|
+
const latest = useRef2(erased);
|
|
799
|
+
const latestPresentation = useRef2(options);
|
|
800
|
+
useLayoutEffect2(() => {
|
|
801
|
+
latest.current = erased;
|
|
802
|
+
latestPresentation.current = options;
|
|
803
|
+
});
|
|
804
|
+
useLayoutEffect2(
|
|
805
|
+
() => session.registerArea(
|
|
806
|
+
id,
|
|
807
|
+
(point) => {
|
|
808
|
+
const current = latest.current;
|
|
809
|
+
return typeof current === "function" ? current(point) : current;
|
|
810
|
+
},
|
|
811
|
+
() => latestPresentation.current
|
|
812
|
+
),
|
|
813
|
+
[session, id]
|
|
814
|
+
);
|
|
815
|
+
return {
|
|
816
|
+
props: {
|
|
817
|
+
"data-drop-area": id,
|
|
818
|
+
"data-drop-target": target || following ? "true" : void 0,
|
|
819
|
+
"data-drop-over": isOver || following || void 0
|
|
820
|
+
},
|
|
821
|
+
isEligible: target !== null || following,
|
|
822
|
+
isOver: isOver || following
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
function useZonePresentation(zoneId, hostId, options) {
|
|
826
|
+
const game = useGame();
|
|
827
|
+
const session = useGestureSession();
|
|
828
|
+
const landing = useGestureState(
|
|
829
|
+
session,
|
|
830
|
+
(state) => state.drag?.settling ? state.drag.landing : null
|
|
831
|
+
);
|
|
832
|
+
const zone = game.zones.find(zoneId, hostId);
|
|
833
|
+
const actual = useMemo(
|
|
834
|
+
() => zone?.getCards() ?? [],
|
|
835
|
+
[zone]
|
|
836
|
+
);
|
|
837
|
+
const active = landing?.snapshot === game.snapshot ? landing : null;
|
|
838
|
+
const incoming = active?.zoneId === zoneId && active.hostId === hostId;
|
|
839
|
+
const outgoing = active?.cards.filter(
|
|
840
|
+
(card) => card.zone === zoneId && card.hostId === hostId
|
|
841
|
+
) ?? [];
|
|
842
|
+
const cards = useMemo(() => {
|
|
843
|
+
const ordered = options ? options.order.flatMap((id) => {
|
|
844
|
+
const card = actual.find((card2) => card2.id === id);
|
|
845
|
+
return card ? [card] : [];
|
|
846
|
+
}) : actual;
|
|
847
|
+
if (!active) return ordered;
|
|
848
|
+
const moving = new Set(active.cards.map((card) => card.id));
|
|
849
|
+
const remaining = ordered.filter((card) => !moving.has(card.id));
|
|
850
|
+
if (incoming) {
|
|
851
|
+
const before = ordered.slice(0, active.index).filter((card) => moving.has(card.id)).length;
|
|
852
|
+
const index = Math.max(
|
|
853
|
+
0,
|
|
854
|
+
Math.min(remaining.length, active.index - before)
|
|
855
|
+
);
|
|
856
|
+
remaining.splice(
|
|
857
|
+
index,
|
|
858
|
+
0,
|
|
859
|
+
...active.cards
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
return remaining;
|
|
863
|
+
}, [actual, active, incoming, options]);
|
|
864
|
+
const source = game.getOptions().source;
|
|
865
|
+
const me = game.me?.id;
|
|
866
|
+
const count = (zone?.count ?? 0) - outgoing.length + (incoming ? active.cards.length : 0);
|
|
867
|
+
const previous = useRef2({
|
|
868
|
+
source,
|
|
869
|
+
me,
|
|
870
|
+
ids: new Set(actual.map((card) => card.id)),
|
|
871
|
+
incoming: 0,
|
|
872
|
+
count: zone?.count ?? 0
|
|
873
|
+
});
|
|
874
|
+
const newlyArriving = previous.current.source === source && previous.current.me === me ? cards.filter(
|
|
875
|
+
(card) => !previous.current.ids.has(card.id) && !active?.cards.some((pending) => pending.id === card.id)
|
|
876
|
+
).map((card) => card.id) : [];
|
|
877
|
+
const arrivingIds = previous.current.incoming > 0 && newlyArriving.length === previous.current.incoming && count === previous.current.count ? [] : newlyArriving;
|
|
878
|
+
useLayoutEffect2(() => {
|
|
879
|
+
previous.current = {
|
|
880
|
+
source,
|
|
881
|
+
me,
|
|
882
|
+
ids: new Set([...actual, ...cards].map((card) => card.id)),
|
|
883
|
+
incoming: incoming ? active.cards.length : 0,
|
|
884
|
+
count
|
|
885
|
+
};
|
|
886
|
+
});
|
|
887
|
+
return {
|
|
888
|
+
cards,
|
|
889
|
+
count,
|
|
890
|
+
arrivingIds
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
function useCardPresentation(cardId) {
|
|
894
|
+
const game = useGame();
|
|
895
|
+
const session = useGestureSession();
|
|
896
|
+
const landing = useGestureState(
|
|
897
|
+
session,
|
|
898
|
+
(state) => state.drag?.settling ? state.drag.landing : null
|
|
899
|
+
);
|
|
900
|
+
const active = landing?.snapshot === game.snapshot ? landing : null;
|
|
901
|
+
const captured = active?.cards.find((card2) => card2.id === cardId);
|
|
902
|
+
const card = game.cards.find(cardId) ?? captured;
|
|
903
|
+
return {
|
|
904
|
+
card,
|
|
905
|
+
isPending: !!captured,
|
|
906
|
+
hidden: card?.hidden === true || !!captured && active?.concealed === true
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
const shortcutsOf = (snapshot) => snapshot.shortcuts;
|
|
910
|
+
const noShortcutsConfiguration = createStore2(
|
|
911
|
+
null
|
|
912
|
+
);
|
|
913
|
+
const sameHints = (left, right) => left.length === right.length && left.every((hint, index) => {
|
|
914
|
+
const next = right[index];
|
|
915
|
+
return hint.label === next.label && hint.kind === next.kind && (hint.kind !== "interaction" || next.kind === "interaction" && hint.interaction === next.interaction) && hint.keys.length === next.keys.length && hint.keys.every((key, at) => key === next.keys[at]);
|
|
916
|
+
});
|
|
917
|
+
function useShortcutHints(target) {
|
|
918
|
+
const controller = shortcutsOf(useInstance());
|
|
919
|
+
const session = useGestureSession();
|
|
920
|
+
useSelector2(session.store, () => session.getShortcutActivity(), {
|
|
921
|
+
compare: (left, right) => left.focusedTarget === right.focusedTarget && left.pointerActive === right.pointerActive
|
|
922
|
+
});
|
|
923
|
+
useGame(
|
|
924
|
+
(snapshot) => shortcutsOf(snapshot)?.getHints(
|
|
925
|
+
target,
|
|
926
|
+
session.getShortcutActivity()
|
|
927
|
+
) ?? [],
|
|
928
|
+
{
|
|
929
|
+
compare: sameHints
|
|
930
|
+
}
|
|
931
|
+
);
|
|
932
|
+
return useSelector2(
|
|
933
|
+
controller?.configuration ?? noShortcutsConfiguration,
|
|
934
|
+
() => controller?.getHints(
|
|
935
|
+
target,
|
|
936
|
+
session.getShortcutActivity()
|
|
937
|
+
) ?? [],
|
|
938
|
+
{ compare: sameHints }
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
function useGameShortcuts(options) {
|
|
942
|
+
const instance = useInstance();
|
|
943
|
+
const session = useGestureSession();
|
|
944
|
+
const controller = shortcutsOf(instance);
|
|
945
|
+
if (!controller)
|
|
946
|
+
throw new Error(
|
|
947
|
+
"useGameShortcuts requires shortcutsFeature in the game binding."
|
|
948
|
+
);
|
|
949
|
+
useShortcutsAdapter(instance, controller, session, options, (result) => {
|
|
950
|
+
if (result instanceof Error) instance.getOptions().onError?.(result);
|
|
951
|
+
else if (result.kind === "interaction" && !result.result.accepted)
|
|
952
|
+
instance.getOptions().onError?.(
|
|
953
|
+
new Error(result.result.message ?? result.result.errorCode, {
|
|
954
|
+
cause: result.result
|
|
955
|
+
})
|
|
956
|
+
);
|
|
957
|
+
});
|
|
958
|
+
const target = useGestureState(
|
|
959
|
+
session,
|
|
960
|
+
(state) => state.activeTarget
|
|
961
|
+
);
|
|
962
|
+
const hints = useShortcutHints(target);
|
|
963
|
+
return { target, hints };
|
|
964
|
+
}
|
|
965
|
+
function useShortcutTarget(target) {
|
|
966
|
+
const session = useGestureSession();
|
|
967
|
+
const id = `${session.id}${useId()}`;
|
|
968
|
+
const latest = useRef2(target);
|
|
969
|
+
const targetKey = inputValueKey(target);
|
|
970
|
+
useLayoutEffect2(() => {
|
|
971
|
+
latest.current = target;
|
|
972
|
+
});
|
|
973
|
+
useLayoutEffect2(
|
|
974
|
+
() => session.registerTarget(id, () => latest.current),
|
|
975
|
+
[session, id, targetKey]
|
|
976
|
+
);
|
|
977
|
+
return { props: session.targetProps(id) };
|
|
978
|
+
}
|
|
979
|
+
function useDragOverlay() {
|
|
980
|
+
const session = useGestureSession();
|
|
981
|
+
const drag = useGestureState(session, (state) => state.drag);
|
|
982
|
+
return drag ? {
|
|
983
|
+
cardId: drag.cardId,
|
|
984
|
+
cardIds: drag.cardIds,
|
|
985
|
+
settling: drag.settling,
|
|
986
|
+
// The drag feature resolved this target from this game's routes.
|
|
987
|
+
target: drag.target,
|
|
988
|
+
landing: drag.landing ? {
|
|
989
|
+
zoneId: drag.landing.zoneId,
|
|
990
|
+
hostId: drag.landing.hostId,
|
|
991
|
+
index: drag.landing.index
|
|
992
|
+
} : null,
|
|
993
|
+
ref: session.overlayRef
|
|
994
|
+
} : null;
|
|
995
|
+
}
|
|
996
|
+
function useCardRow(cardAt) {
|
|
997
|
+
const session = useGestureSession();
|
|
998
|
+
const id = `${session.id}${useId()}`;
|
|
999
|
+
const latest = useRef2(cardAt);
|
|
1000
|
+
useLayoutEffect2(() => {
|
|
1001
|
+
latest.current = cardAt;
|
|
1002
|
+
});
|
|
1003
|
+
useLayoutEffect2(
|
|
1004
|
+
() => session.registerRow(id, (point) => latest.current(point)),
|
|
1005
|
+
[session, id]
|
|
1006
|
+
);
|
|
1007
|
+
return { props: session.rowProps(id) };
|
|
1008
|
+
}
|
|
1009
|
+
function useActiveCard() {
|
|
1010
|
+
const session = useGestureSession();
|
|
1011
|
+
return useGestureState(
|
|
1012
|
+
session,
|
|
1013
|
+
(state) => state.activeTarget?.kind === "card" ? state.activeTarget.value : null
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
return {
|
|
1017
|
+
GameProvider,
|
|
1018
|
+
useGame,
|
|
1019
|
+
Subscribe,
|
|
1020
|
+
useCardGesture,
|
|
1021
|
+
useCardRow,
|
|
1022
|
+
useActiveCard,
|
|
1023
|
+
useGameShortcuts,
|
|
1024
|
+
useShortcutTarget,
|
|
1025
|
+
useShortcutHints,
|
|
1026
|
+
useDropArea,
|
|
1027
|
+
useDragOverlay,
|
|
1028
|
+
useZonePresentation,
|
|
1029
|
+
useCardPresentation
|
|
1030
|
+
};
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// src/react/marquee.ts
|
|
1035
|
+
import { useEffect, useRef as useRef3, useState as useState2 } from "react";
|
|
1036
|
+
function useMarqueeSelection(options) {
|
|
1037
|
+
const latest = useRef3(options);
|
|
1038
|
+
latest.current = options;
|
|
1039
|
+
const [bounds, setBounds] = useState2(null);
|
|
1040
|
+
const cleanup = useRef3(null);
|
|
1041
|
+
useEffect(() => () => cleanup.current?.(), []);
|
|
1042
|
+
useEffect(() => {
|
|
1043
|
+
if (!options.enabled) {
|
|
1044
|
+
cleanup.current?.();
|
|
1045
|
+
setBounds(null);
|
|
1046
|
+
}
|
|
1047
|
+
}, [options.enabled]);
|
|
1048
|
+
return {
|
|
1049
|
+
bounds,
|
|
1050
|
+
props: {
|
|
1051
|
+
onPointerDown(event) {
|
|
1052
|
+
if (!latest.current.enabled || event.button !== 0 || event.pointerType === "touch" || event.altKey || event.ctrlKey || event.metaKey || event.target.closest(
|
|
1053
|
+
"button, input, select, textarea, a, [contenteditable], [data-card-gesture]"
|
|
1054
|
+
))
|
|
1055
|
+
return;
|
|
1056
|
+
event.preventDefault();
|
|
1057
|
+
cleanup.current?.();
|
|
1058
|
+
const x = event.clientX, y = event.clientY, pointerId = event.pointerId;
|
|
1059
|
+
const area = event.currentTarget;
|
|
1060
|
+
const additive = event.shiftKey;
|
|
1061
|
+
const doc = event.currentTarget.ownerDocument;
|
|
1062
|
+
let rectangle = null;
|
|
1063
|
+
function move(next) {
|
|
1064
|
+
if (next.pointerId !== pointerId) return;
|
|
1065
|
+
if (!rectangle && Math.hypot(next.clientX - x, next.clientY - y) < 8)
|
|
1066
|
+
return;
|
|
1067
|
+
const box = area.getBoundingClientRect();
|
|
1068
|
+
const right = Math.min(box.right, Math.max(x, next.clientX));
|
|
1069
|
+
const bottom = Math.min(box.bottom, Math.max(y, next.clientY));
|
|
1070
|
+
const left = Math.max(box.left, Math.min(x, next.clientX));
|
|
1071
|
+
const top = Math.max(box.top, Math.min(y, next.clientY));
|
|
1072
|
+
rectangle = { left, top, width: right - left, height: bottom - top };
|
|
1073
|
+
setBounds(rectangle);
|
|
1074
|
+
next.preventDefault();
|
|
1075
|
+
}
|
|
1076
|
+
function stop() {
|
|
1077
|
+
doc.removeEventListener("pointermove", move);
|
|
1078
|
+
doc.removeEventListener("pointerup", up);
|
|
1079
|
+
doc.removeEventListener("pointercancel", cancel);
|
|
1080
|
+
doc.removeEventListener("keydown", key);
|
|
1081
|
+
doc.defaultView?.removeEventListener("blur", cancel);
|
|
1082
|
+
cleanup.current = null;
|
|
1083
|
+
}
|
|
1084
|
+
function cancel() {
|
|
1085
|
+
stop();
|
|
1086
|
+
setBounds(null);
|
|
1087
|
+
}
|
|
1088
|
+
function key(next) {
|
|
1089
|
+
if (next.key === "Escape") {
|
|
1090
|
+
next.preventDefault();
|
|
1091
|
+
cancel();
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
function up(next) {
|
|
1095
|
+
if (next.pointerId !== pointerId) return;
|
|
1096
|
+
const ids = rectangle ? latest.current.getCards().filter(({ element }) => {
|
|
1097
|
+
const box = element.getBoundingClientRect();
|
|
1098
|
+
const region = rectangle;
|
|
1099
|
+
return box.right > region.left && box.left < region.left + region.width && box.bottom > region.top && box.top < region.top + region.height;
|
|
1100
|
+
}).map((card) => card.id) : [];
|
|
1101
|
+
stop();
|
|
1102
|
+
setBounds(null);
|
|
1103
|
+
latest.current.onSelect(ids, additive);
|
|
1104
|
+
}
|
|
1105
|
+
cleanup.current = stop;
|
|
1106
|
+
doc.addEventListener("pointermove", move, { passive: false });
|
|
1107
|
+
doc.addEventListener("pointerup", up);
|
|
1108
|
+
doc.addEventListener("pointercancel", cancel);
|
|
1109
|
+
doc.addEventListener("keydown", key);
|
|
1110
|
+
doc.defaultView?.addEventListener("blur", cancel);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
export {
|
|
1116
|
+
createGameHook,
|
|
1117
|
+
useMarqueeSelection
|
|
1118
|
+
};
|
|
1119
|
+
//# sourceMappingURL=react.js.map
|