@jgengine/react 0.19.0 → 0.21.0
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/CHANGELOG.md +346 -0
- package/dist/actionHud.js +1 -1
- package/dist/bars.d.ts +1 -1
- package/dist/bars.js +1 -1
- package/dist/barsPreview.d.ts +1 -1
- package/dist/barsPreview.js +9 -2
- package/dist/browserLifecycle.d.ts +26 -0
- package/dist/browserLifecycle.js +42 -0
- package/dist/dialogBehavior.d.ts +22 -0
- package/dist/dialogBehavior.js +109 -0
- package/dist/dragLayer.d.ts +7 -0
- package/dist/dragLayer.js +156 -100
- package/dist/hooks.d.ts +5 -1
- package/dist/hooks.js +3 -1
- package/dist/hudTheme.d.ts +1 -1
- package/dist/hudTheme.js +3 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/map.js +106 -42
- package/dist/modals.js +8 -52
- package/dist/panels.js +45 -14
- package/dist/previewFixtures.js +3 -1
- package/dist/scrollRail.d.ts +18 -0
- package/dist/scrollRail.js +55 -0
- package/dist/scrollRailPreview.d.ts +4 -0
- package/dist/scrollRailPreview.js +8 -0
- package/dist/voice.js +120 -13
- package/package.json +2 -2
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/jgengine/SKILL.md +22 -2
- package/skills/jgengine/api.md +28 -17
- package/skills/jgengine/recipes/minimal-game.md +7 -2
- package/skills/jgengine-ui/SKILL.md +3 -1
- package/skills/jgengine-ui/api.md +204 -67
- package/skills/jgengine-ui/capabilities.md +115 -1
- package/skills/jgengine-ui/reference-primitives.md +93 -3
- package/skills/jgengine-ui/reference.md +2 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
const owners = new WeakMap();
|
|
3
|
+
function focusableWithin(root) {
|
|
4
|
+
return Array.from(root.querySelectorAll('a[href],button,textarea,input,select,[tabindex]')).filter((element) => {
|
|
5
|
+
if (element.tabIndex < 0 || element.matches(":disabled") || element.closest("[hidden],[inert]"))
|
|
6
|
+
return false;
|
|
7
|
+
const style = element.ownerDocument.defaultView?.getComputedStyle(element);
|
|
8
|
+
return style?.visibility !== "hidden" && style?.visibility !== "collapse" && element.getClientRects().length > 0;
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
function focusInitial(root, options) {
|
|
12
|
+
const controls = focusableWithin(root);
|
|
13
|
+
const preferred = options.initialFocus === "root" ? root : options.initialFocus?.();
|
|
14
|
+
(preferred !== null && preferred !== undefined && (preferred === root || controls.includes(preferred))
|
|
15
|
+
? preferred : controls[0] ?? root).focus();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Focus entry, Tab trapping, Escape requests, and focus restoration for a caller-owned dialog.
|
|
19
|
+
* Attach the returned ref to the dialog root and supply `role="dialog"`, `aria-modal`, an accessible
|
|
20
|
+
* name, and `tabIndex={-1}`. The most recently opened dialog handles keys, including portal roots.
|
|
21
|
+
* Closing an inner dialog restores its opener; cleanup never steals focus from another active dialog.
|
|
22
|
+
* Replacing the root while open keeps its position and original return target.
|
|
23
|
+
*
|
|
24
|
+
* @capability dialog-behavior controlled dialog focus and keyboard behavior without owning markup or screen state
|
|
25
|
+
*/
|
|
26
|
+
export function useDialogBehavior(options) {
|
|
27
|
+
const ref = useRef(null);
|
|
28
|
+
const activeOwner = useRef(null);
|
|
29
|
+
const latest = useRef(options);
|
|
30
|
+
latest.current = options;
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const root = ref.current;
|
|
33
|
+
if (!options.open || root === null)
|
|
34
|
+
return;
|
|
35
|
+
const document = root.ownerDocument;
|
|
36
|
+
const stack = owners.get(document) ?? [];
|
|
37
|
+
owners.set(document, stack);
|
|
38
|
+
const owner = { root, restore: document.activeElement };
|
|
39
|
+
activeOwner.current = owner;
|
|
40
|
+
const child = stack.findIndex((entry) => root.contains(entry.root));
|
|
41
|
+
if (child < 0)
|
|
42
|
+
stack.push(owner);
|
|
43
|
+
else
|
|
44
|
+
stack.splice(child, 0, owner);
|
|
45
|
+
const keydown = (event) => {
|
|
46
|
+
if (stack.at(-1) !== owner || event.defaultPrevented)
|
|
47
|
+
return;
|
|
48
|
+
if (event.key === "Escape" && latest.current.onClose !== undefined) {
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
event.stopPropagation();
|
|
51
|
+
latest.current.onClose();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (event.key !== "Tab")
|
|
55
|
+
return;
|
|
56
|
+
const root = owner.root;
|
|
57
|
+
const controls = focusableWithin(root);
|
|
58
|
+
const first = controls[0], last = controls.at(-1);
|
|
59
|
+
const active = document.activeElement;
|
|
60
|
+
if (first === undefined || !root.contains(active) || active === root ||
|
|
61
|
+
(event.shiftKey ? active === first : active === last)) {
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
event.stopPropagation();
|
|
64
|
+
(event.shiftKey ? last : first)?.focus();
|
|
65
|
+
if (first === undefined)
|
|
66
|
+
root.focus();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
document.addEventListener("keydown", keydown, true);
|
|
70
|
+
return () => {
|
|
71
|
+
document.removeEventListener("keydown", keydown, true);
|
|
72
|
+
const wasTop = stack.at(-1) === owner;
|
|
73
|
+
const index = stack.indexOf(owner);
|
|
74
|
+
if (index >= 0)
|
|
75
|
+
stack.splice(index, 1);
|
|
76
|
+
for (const entry of stack) {
|
|
77
|
+
if (entry.restore !== null && owner.root.contains(entry.restore))
|
|
78
|
+
entry.restore = owner.restore;
|
|
79
|
+
}
|
|
80
|
+
if (wasTop && owner.restore?.isConnected && !owner.restore.closest("[hidden],[inert]")) {
|
|
81
|
+
owner.restore.focus();
|
|
82
|
+
}
|
|
83
|
+
if (stack.length === 0)
|
|
84
|
+
owners.delete(document);
|
|
85
|
+
activeOwner.current = null;
|
|
86
|
+
};
|
|
87
|
+
}, [options.open]);
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const root = ref.current;
|
|
90
|
+
const owner = activeOwner.current;
|
|
91
|
+
if (!options.open || root === null || owner === null || owner.root === root)
|
|
92
|
+
return;
|
|
93
|
+
const stack = owners.get(owner.root.ownerDocument);
|
|
94
|
+
for (const entry of stack ?? []) {
|
|
95
|
+
if (entry !== owner && entry.restore !== null && owner.root.contains(entry.restore))
|
|
96
|
+
entry.restore = root;
|
|
97
|
+
}
|
|
98
|
+
owner.root = root;
|
|
99
|
+
if (stack?.at(-1) === owner)
|
|
100
|
+
focusInitial(root, latest.current);
|
|
101
|
+
});
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const root = ref.current;
|
|
104
|
+
if (!options.open || root === null || owners.get(root.ownerDocument)?.at(-1)?.root !== root)
|
|
105
|
+
return;
|
|
106
|
+
focusInitial(root, latest.current);
|
|
107
|
+
}, [options.open, options.focusKey]);
|
|
108
|
+
return ref;
|
|
109
|
+
}
|
package/dist/dragLayer.d.ts
CHANGED
|
@@ -45,6 +45,13 @@ export interface DragLayer<T> {
|
|
|
45
45
|
cancel(): void;
|
|
46
46
|
attachGhost(element: HTMLElement | null): void;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Pointer-owned drag state shared by cards and inventory grids. Registered DropZones are hit-tested
|
|
50
|
+
* through pointer capture; release finishes once, while cancellation, blur and unmount release ownership.
|
|
51
|
+
* Callers own drop policy, rotation and presentation; pointer movement paints the ghost without rerendering it.
|
|
52
|
+
*
|
|
53
|
+
* @capability drag-layer composable pointer-owned drag, drop targets and cancellation for custom cards and inventory UI
|
|
54
|
+
*/
|
|
48
55
|
export declare function useDragLayer<T>(options?: {
|
|
49
56
|
onDrop?: (info: DropInfo<T>) => void;
|
|
50
57
|
}): DragLayer<T>;
|
package/dist/dragLayer.js
CHANGED
|
@@ -2,25 +2,41 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
3
3
|
import { cellFromPoint, } from "@jgengine/core/inventory/shapedGrid";
|
|
4
4
|
const EMPTY_POINT = { x: 0, y: 0 };
|
|
5
|
+
const targetRegistries = new WeakMap();
|
|
6
|
+
/**
|
|
7
|
+
* Pointer-owned drag state shared by cards and inventory grids. Registered DropZones are hit-tested
|
|
8
|
+
* through pointer capture; release finishes once, while cancellation, blur and unmount release ownership.
|
|
9
|
+
* Callers own drop policy, rotation and presentation; pointer movement paints the ghost without rerendering it.
|
|
10
|
+
*
|
|
11
|
+
* @capability drag-layer composable pointer-owned drag, drop targets and cancellation for custom cards and inventory UI
|
|
12
|
+
*/
|
|
5
13
|
export function useDragLayer(options) {
|
|
6
14
|
const [state, setState] = useState({
|
|
7
|
-
payload: null,
|
|
8
|
-
point: EMPTY_POINT,
|
|
9
|
-
origin: EMPTY_POINT,
|
|
10
|
-
overTarget: null,
|
|
11
|
-
overCell: null,
|
|
15
|
+
payload: null, point: EMPTY_POINT, origin: EMPTY_POINT, overTarget: null, overCell: null,
|
|
12
16
|
});
|
|
13
17
|
const pointRef = useRef(EMPTY_POINT);
|
|
14
18
|
const ghostRef = useRef(null);
|
|
15
|
-
const moveRef = useRef(null);
|
|
16
19
|
const payloadRef = useRef(null);
|
|
17
20
|
const overRef = useRef({ target: null, cell: null });
|
|
21
|
+
const overElement = useRef(null);
|
|
22
|
+
const targets = useRef(new Map());
|
|
23
|
+
const ownerRef = useRef(null);
|
|
24
|
+
const latest = useRef(options);
|
|
25
|
+
latest.current = options;
|
|
18
26
|
const detach = useCallback(() => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
const owner = ownerRef.current;
|
|
28
|
+
ownerRef.current = null;
|
|
29
|
+
if (owner === null)
|
|
30
|
+
return;
|
|
31
|
+
owner.detach();
|
|
32
|
+
if (owner.source.hasPointerCapture(owner.pointerId))
|
|
33
|
+
owner.source.releasePointerCapture(owner.pointerId);
|
|
23
34
|
}, []);
|
|
35
|
+
useEffect(() => () => {
|
|
36
|
+
detach();
|
|
37
|
+
payloadRef.current = null;
|
|
38
|
+
ghostRef.current = null;
|
|
39
|
+
}, [detach]);
|
|
24
40
|
const writeGhost = useCallback((point, rotation) => {
|
|
25
41
|
const el = ghostRef.current;
|
|
26
42
|
if (el === null)
|
|
@@ -29,91 +45,139 @@ export function useDragLayer(options) {
|
|
|
29
45
|
el.style.top = `${point.y}px`;
|
|
30
46
|
el.style.transform = `translate(-50%, -50%) rotate(${rotation * 90}deg)`;
|
|
31
47
|
}, []);
|
|
32
|
-
const beginDrag = useCallback((payload, event) => {
|
|
33
|
-
const point = { x: event.clientX, y: event.clientY };
|
|
34
|
-
const nextPayload = { id: payload.id, value: payload.value, rotation: payload.rotation ?? 0 };
|
|
35
|
-
payloadRef.current = nextPayload;
|
|
36
|
-
overRef.current = { target: null, cell: null };
|
|
37
|
-
pointRef.current = point;
|
|
38
|
-
setState({
|
|
39
|
-
payload: nextPayload,
|
|
40
|
-
point,
|
|
41
|
-
origin: point,
|
|
42
|
-
overTarget: null,
|
|
43
|
-
overCell: null,
|
|
44
|
-
});
|
|
45
|
-
writeGhost(point, nextPayload.rotation);
|
|
46
|
-
const onMove = (moveEvent) => {
|
|
47
|
-
if (payloadRef.current === null)
|
|
48
|
-
return;
|
|
49
|
-
const next = { x: moveEvent.clientX, y: moveEvent.clientY };
|
|
50
|
-
pointRef.current = next;
|
|
51
|
-
writeGhost(next, payloadRef.current.rotation);
|
|
52
|
-
};
|
|
53
|
-
detach();
|
|
54
|
-
moveRef.current = onMove;
|
|
55
|
-
window.addEventListener("pointermove", onMove);
|
|
56
|
-
}, [detach, writeGhost]);
|
|
57
|
-
const rotate = useCallback((quarterTurns = 1) => {
|
|
58
|
-
setState((prev) => {
|
|
59
|
-
if (prev.payload === null)
|
|
60
|
-
return prev;
|
|
61
|
-
const rotation = (((prev.payload.rotation + quarterTurns) % 4) + 4) % 4;
|
|
62
|
-
const payload = { ...prev.payload, rotation };
|
|
63
|
-
payloadRef.current = payload;
|
|
64
|
-
writeGhost(pointRef.current, rotation);
|
|
65
|
-
return { ...prev, payload, point: pointRef.current };
|
|
66
|
-
});
|
|
67
|
-
}, [writeGhost]);
|
|
68
48
|
const setTarget = useCallback((target, cell = null) => {
|
|
49
|
+
const previous = overRef.current;
|
|
50
|
+
if (previous.target === target && previous.cell?.[0] === cell?.[0] && previous.cell?.[1] === cell?.[1])
|
|
51
|
+
return;
|
|
69
52
|
overRef.current = { target, cell };
|
|
70
53
|
setState((prev) => ({ ...prev, overTarget: target, overCell: cell, point: pointRef.current }));
|
|
71
54
|
}, []);
|
|
55
|
+
const resolveTarget = useCallback((document, point) => {
|
|
56
|
+
if (targets.current.size === 0)
|
|
57
|
+
return;
|
|
58
|
+
let element = document.elementFromPoint(point.x, point.y);
|
|
59
|
+
while (element !== null) {
|
|
60
|
+
const target = targets.current.get(element);
|
|
61
|
+
if (target !== undefined) {
|
|
62
|
+
overElement.current = element;
|
|
63
|
+
const rect = element.getBoundingClientRect();
|
|
64
|
+
setTarget(target.id, target.cellSize === undefined ? null
|
|
65
|
+
: cellFromPoint({ x: point.x - rect.left, y: point.y - rect.top }, target.cellSize));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
element = element.parentElement;
|
|
69
|
+
}
|
|
70
|
+
overElement.current = null;
|
|
71
|
+
setTarget(null);
|
|
72
|
+
}, [setTarget]);
|
|
72
73
|
const reset = useCallback(() => {
|
|
73
74
|
detach();
|
|
74
75
|
payloadRef.current = null;
|
|
75
76
|
overRef.current = { target: null, cell: null };
|
|
77
|
+
overElement.current = null;
|
|
76
78
|
pointRef.current = EMPTY_POINT;
|
|
77
|
-
setState({
|
|
78
|
-
payload: null,
|
|
79
|
-
point: EMPTY_POINT,
|
|
80
|
-
origin: EMPTY_POINT,
|
|
81
|
-
overTarget: null,
|
|
82
|
-
overCell: null,
|
|
83
|
-
});
|
|
79
|
+
setState({ payload: null, point: EMPTY_POINT, origin: EMPTY_POINT, overTarget: null, overCell: null });
|
|
84
80
|
}, [detach]);
|
|
85
81
|
const endDrag = useCallback(() => {
|
|
86
82
|
const payload = payloadRef.current;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
cell: overRef.current.cell,
|
|
93
|
-
point: pointRef.current,
|
|
94
|
-
};
|
|
95
|
-
options?.onDrop?.(info);
|
|
96
|
-
}
|
|
83
|
+
if (payload === null)
|
|
84
|
+
return null;
|
|
85
|
+
const info = {
|
|
86
|
+
payload, target: overRef.current.target, cell: overRef.current.cell, point: pointRef.current,
|
|
87
|
+
};
|
|
97
88
|
reset();
|
|
89
|
+
latest.current?.onDrop?.(info);
|
|
98
90
|
return info;
|
|
99
|
-
}, [
|
|
91
|
+
}, [reset]);
|
|
92
|
+
const beginDrag = useCallback((payload, event) => {
|
|
93
|
+
if (event.button !== 0 || !event.isPrimary || payloadRef.current !== null)
|
|
94
|
+
return;
|
|
95
|
+
const source = event.currentTarget;
|
|
96
|
+
const view = source.ownerDocument.defaultView;
|
|
97
|
+
if (view === null)
|
|
98
|
+
return;
|
|
99
|
+
const point = { x: event.clientX, y: event.clientY };
|
|
100
|
+
const nextPayload = { id: payload.id, value: payload.value, rotation: payload.rotation ?? 0 };
|
|
101
|
+
payloadRef.current = nextPayload;
|
|
102
|
+
overRef.current = { target: null, cell: null };
|
|
103
|
+
pointRef.current = point;
|
|
104
|
+
setState({ payload: nextPayload, point, origin: point, overTarget: null, overCell: null });
|
|
105
|
+
writeGhost(point, nextPayload.rotation);
|
|
106
|
+
resolveTarget(source.ownerDocument, point);
|
|
107
|
+
const onMove = (move) => {
|
|
108
|
+
if (move.pointerId !== ownerRef.current?.pointerId || payloadRef.current === null)
|
|
109
|
+
return;
|
|
110
|
+
if (!source.isConnected) {
|
|
111
|
+
reset();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const next = { x: move.clientX, y: move.clientY };
|
|
115
|
+
pointRef.current = next;
|
|
116
|
+
writeGhost(next, payloadRef.current.rotation);
|
|
117
|
+
resolveTarget(source.ownerDocument, next);
|
|
118
|
+
};
|
|
119
|
+
const onUp = (up) => {
|
|
120
|
+
if (up.pointerId !== ownerRef.current?.pointerId || up.button !== 0)
|
|
121
|
+
return;
|
|
122
|
+
if (!source.isConnected) {
|
|
123
|
+
reset();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
pointRef.current = { x: up.clientX, y: up.clientY };
|
|
127
|
+
resolveTarget(source.ownerDocument, pointRef.current);
|
|
128
|
+
endDrag();
|
|
129
|
+
};
|
|
130
|
+
const onCancel = (cancel) => { if (cancel.pointerId === ownerRef.current?.pointerId)
|
|
131
|
+
reset(); };
|
|
132
|
+
const onKey = (key) => {
|
|
133
|
+
if (key.key !== "Escape" || key.defaultPrevented)
|
|
134
|
+
return;
|
|
135
|
+
key.preventDefault();
|
|
136
|
+
key.stopPropagation();
|
|
137
|
+
reset();
|
|
138
|
+
};
|
|
139
|
+
ownerRef.current = { source, pointerId: event.pointerId, detach: () => {
|
|
140
|
+
view.removeEventListener("pointermove", onMove, true);
|
|
141
|
+
view.removeEventListener("pointerup", onUp, true);
|
|
142
|
+
view.removeEventListener("pointercancel", onCancel, true);
|
|
143
|
+
view.removeEventListener("lostpointercapture", onCancel, true);
|
|
144
|
+
view.removeEventListener("blur", reset);
|
|
145
|
+
view.removeEventListener("keydown", onKey, true);
|
|
146
|
+
} };
|
|
147
|
+
view.addEventListener("pointermove", onMove, true);
|
|
148
|
+
view.addEventListener("pointerup", onUp, true);
|
|
149
|
+
view.addEventListener("pointercancel", onCancel, true);
|
|
150
|
+
view.addEventListener("lostpointercapture", onCancel, true);
|
|
151
|
+
view.addEventListener("blur", reset);
|
|
152
|
+
view.addEventListener("keydown", onKey, true);
|
|
153
|
+
source.setPointerCapture(event.pointerId);
|
|
154
|
+
}, [endDrag, reset, resolveTarget, writeGhost]);
|
|
155
|
+
targetRegistries.set(beginDrag, (element, id, cellSize) => {
|
|
156
|
+
targets.current.set(element, { id, cellSize });
|
|
157
|
+
return () => {
|
|
158
|
+
targets.current.delete(element);
|
|
159
|
+
if (overElement.current === element) {
|
|
160
|
+
overElement.current = null;
|
|
161
|
+
setTarget(null);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
const rotate = useCallback((quarterTurns = 1) => {
|
|
166
|
+
const payload = payloadRef.current;
|
|
167
|
+
if (payload === null)
|
|
168
|
+
return;
|
|
169
|
+
const rotation = (((payload.rotation + quarterTurns) % 4) + 4) % 4;
|
|
170
|
+
const next = { ...payload, rotation };
|
|
171
|
+
payloadRef.current = next;
|
|
172
|
+
writeGhost(pointRef.current, rotation);
|
|
173
|
+
setState((prev) => ({ ...prev, payload: next, point: pointRef.current }));
|
|
174
|
+
}, [writeGhost]);
|
|
100
175
|
const attachGhost = useCallback((element) => {
|
|
101
176
|
ghostRef.current = element;
|
|
102
|
-
if (element !== null && payloadRef.current !== null)
|
|
177
|
+
if (element !== null && payloadRef.current !== null)
|
|
103
178
|
writeGhost(pointRef.current, payloadRef.current.rotation);
|
|
104
|
-
}
|
|
105
179
|
}, [writeGhost]);
|
|
106
|
-
return {
|
|
107
|
-
state,
|
|
108
|
-
dragging: state.payload !== null,
|
|
109
|
-
pointRef,
|
|
110
|
-
beginDrag,
|
|
111
|
-
rotate,
|
|
112
|
-
setTarget,
|
|
113
|
-
endDrag,
|
|
114
|
-
cancel: reset,
|
|
115
|
-
attachGhost,
|
|
116
|
-
};
|
|
180
|
+
return { state, dragging: state.payload !== null, pointRef, beginDrag, rotate, setTarget, endDrag, cancel: reset, attachGhost };
|
|
117
181
|
}
|
|
118
182
|
export function DragGhost({ layer, className, style, children, }) {
|
|
119
183
|
const ref = useRef(null);
|
|
@@ -136,12 +200,17 @@ export function DragGhost({ layer, className, style, children, }) {
|
|
|
136
200
|
}
|
|
137
201
|
export function DraggableCard({ id, value, layer, className, children, onRotate, }) {
|
|
138
202
|
const isDragging = layer.state.payload?.id === id;
|
|
139
|
-
|
|
203
|
+
const latest = useRef(layer);
|
|
204
|
+
latest.current = layer;
|
|
205
|
+
useEffect(() => () => {
|
|
206
|
+
if (latest.current.state.payload?.id === id)
|
|
207
|
+
latest.current.cancel();
|
|
208
|
+
}, [id, layer.cancel]);
|
|
209
|
+
return (_jsx("div", { className: className, "data-card": id, "data-dragging": isDragging ? "" : undefined, style: { touchAction: "none" }, onPointerDown: (event) => {
|
|
210
|
+
if (event.button !== 0 || !event.isPrimary)
|
|
211
|
+
return;
|
|
140
212
|
event.preventDefault();
|
|
141
213
|
layer.beginDrag({ id, value }, event);
|
|
142
|
-
}, onPointerUp: () => {
|
|
143
|
-
if (layer.dragging)
|
|
144
|
-
layer.endDrag();
|
|
145
214
|
}, onContextMenu: (event) => {
|
|
146
215
|
if (onRotate === false)
|
|
147
216
|
return;
|
|
@@ -153,24 +222,11 @@ export function DraggableCard({ id, value, layer, className, children, onRotate,
|
|
|
153
222
|
export function DropZone({ id, layer, className, activeClassName, cellSize, children, }) {
|
|
154
223
|
const ref = useRef(null);
|
|
155
224
|
const active = layer.state.overTarget === id && layer.dragging;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}, [cellSize]);
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
const element = ref.current;
|
|
227
|
+
if (element !== null)
|
|
228
|
+
return targetRegistries.get(layer.beginDrag)?.(element, id, cellSize);
|
|
229
|
+
}, [layer.beginDrag, id, cellSize]);
|
|
162
230
|
const composed = useMemo(() => [className, active ? activeClassName : undefined].filter(Boolean).join(" ") || undefined, [className, active, activeClassName]);
|
|
163
|
-
return (_jsx("div", { ref: ref, className: composed, "data-dropzone": id, "data-active": active ? "" : undefined,
|
|
164
|
-
if (layer.dragging)
|
|
165
|
-
layer.setTarget(id, resolveCell(event));
|
|
166
|
-
}, onPointerMove: (event) => {
|
|
167
|
-
if (layer.dragging && layer.state.overTarget === id)
|
|
168
|
-
layer.setTarget(id, resolveCell(event));
|
|
169
|
-
}, onPointerLeave: () => {
|
|
170
|
-
if (layer.dragging && layer.state.overTarget === id)
|
|
171
|
-
layer.setTarget(null, null);
|
|
172
|
-
}, onPointerUp: () => {
|
|
173
|
-
if (layer.dragging)
|
|
174
|
-
layer.endDrag();
|
|
175
|
-
}, children: children }));
|
|
231
|
+
return (_jsx("div", { ref: ref, className: composed, "data-dropzone": id, "data-active": active ? "" : undefined, children: children }));
|
|
176
232
|
}
|
package/dist/hooks.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type DependencyList } from "react";
|
|
|
2
2
|
import { AxisChannel, type AxisChannelConfig } from "@jgengine/core/input/axisInput";
|
|
3
3
|
import { type MarkerSet } from "@jgengine/core/world/markers";
|
|
4
4
|
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
5
|
+
import { type DispatchedCommandResult } from "@jgengine/core/runtime/commandDispatch";
|
|
5
6
|
import type { AbilityKit, AbilitySlotSnapshot } from "@jgengine/core/combat/abilityKit";
|
|
6
7
|
import type { EventMeter } from "@jgengine/core/stats/eventMeter";
|
|
7
8
|
import type { GameEvents } from "@jgengine/core/game/events";
|
|
@@ -23,8 +24,11 @@ import { type GamePhase } from "@jgengine/core/game/gamePhase";
|
|
|
23
24
|
export declare function useGameStore<T>(selector: (ctx: GameContext) => T, isEqual?: (previous: T, next: T) => boolean): T;
|
|
24
25
|
/** Subscribe to a single keyed slice of the game store, returning `fallback` until the key is set. */
|
|
25
26
|
export declare function useGameStoreValue<T>(key: string, fallback: T): T;
|
|
27
|
+
/** UI command surface. Hosted commands return a promise; await it to display acknowledgement or rejection. Registration and runAs remain trusted local APIs. */
|
|
26
28
|
export declare function useGame(): {
|
|
27
|
-
commands: GameContext["game"]["commands"]
|
|
29
|
+
commands: Omit<GameContext["game"]["commands"], "run"> & {
|
|
30
|
+
run(name: string, input: unknown): DispatchedCommandResult;
|
|
31
|
+
};
|
|
28
32
|
events: GameEvents;
|
|
29
33
|
};
|
|
30
34
|
export declare function usePlayer(): {
|
package/dist/hooks.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
2
2
|
import { AxisChannel } from "@jgengine/core/input/axisInput";
|
|
3
3
|
import { createMarkerSet } from "@jgengine/core/world/markers";
|
|
4
|
+
import { dispatchCommand } from "@jgengine/core/runtime/commandDispatch";
|
|
4
5
|
import { browseSessions, } from "@jgengine/core/multiplayer/matchmaking";
|
|
5
6
|
import { resolveActivePrompt, } from "@jgengine/core/interaction/proximityPrompt";
|
|
6
7
|
import { gamePhase, setGamePhase } from "@jgengine/core/game/gamePhase";
|
|
@@ -21,9 +22,10 @@ export function useGameStore(selector, isEqual = Object.is) {
|
|
|
21
22
|
export function useGameStoreValue(key, fallback) {
|
|
22
23
|
return useGameStore((ctx) => ctx.game.store.get(key) ?? fallback);
|
|
23
24
|
}
|
|
25
|
+
/** UI command surface. Hosted commands return a promise; await it to display acknowledgement or rejection. Registration and runAs remain trusted local APIs. */
|
|
24
26
|
export function useGame() {
|
|
25
27
|
const ctx = useGameContext();
|
|
26
|
-
return useMemo(() => ({ commands: ctx.game.commands, events: ctx.game.events }), [ctx]);
|
|
28
|
+
return useMemo(() => ({ commands: { ...ctx.game.commands, run: (name, input) => dispatchCommand(ctx, name, input) }, events: ctx.game.events }), [ctx]);
|
|
27
29
|
}
|
|
28
30
|
export function usePlayer() {
|
|
29
31
|
const ctx = useGameContext();
|
package/dist/hudTheme.d.ts
CHANGED
|
@@ -110,7 +110,7 @@ export interface HudTheme {
|
|
|
110
110
|
ring: string;
|
|
111
111
|
skin?: HudSkin;
|
|
112
112
|
}
|
|
113
|
-
/** Emits
|
|
113
|
+
/** Emits theme CSS properties, resetting inherited bar fills when absent. Spread onto a HUD ancestor. */
|
|
114
114
|
export declare function hudThemeVars(theme: HudTheme): CSSProperties;
|
|
115
115
|
/** Default theme tokens for previews/scaffold — not a finished game look; author a custom theme. */
|
|
116
116
|
export declare const defaultHudTheme: HudTheme;
|
package/dist/hudTheme.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {} from "react";
|
|
2
2
|
import { hudSkinVars } from "./hudSkin.js";
|
|
3
3
|
export { hudSkinVars } from "./hudSkin.js";
|
|
4
|
-
/** Emits
|
|
4
|
+
/** Emits theme CSS properties, resetting inherited bar fills when absent. Spread onto a HUD ancestor. */
|
|
5
5
|
export function hudThemeVars(theme) {
|
|
6
6
|
const { palette, surface, status, font, rarity, frame, bar, slot } = theme;
|
|
7
7
|
return {
|
|
@@ -13,7 +13,8 @@ export function hudThemeVars(theme) {
|
|
|
13
13
|
"--jg-slot-slice": "0",
|
|
14
14
|
"--jg-slot-scale": "1",
|
|
15
15
|
"--jg-bar-skin-track": "none",
|
|
16
|
-
|
|
16
|
+
// `initial` clears inherited skins and lets var() fall back to the bar's palette color.
|
|
17
|
+
"--jg-bar-skin-fill": "initial",
|
|
17
18
|
"--jg-skin-cursor": "auto",
|
|
18
19
|
...hudSkinVars(theme.skin),
|
|
19
20
|
// atomic-bar tokens (same names the bars read — #1033)
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from "./chatBubbles.js";
|
|
|
5
5
|
export * from "./voice.js";
|
|
6
6
|
export * from "./social.js";
|
|
7
7
|
export * from "./hooks.js";
|
|
8
|
+
export * from "./browserLifecycle.js";
|
|
8
9
|
export * from "./useDebouncedCommit.js";
|
|
9
10
|
export * from "./store.js";
|
|
10
11
|
export * from "./settings.js";
|
|
@@ -72,3 +73,5 @@ export * from "./previewFixtures.js";
|
|
|
72
73
|
export * from "./streetGeometryPreview.js";
|
|
73
74
|
export * from "./saveSlots.js";
|
|
74
75
|
export * from "./cameraShake.js";
|
|
76
|
+
export * from "./dialogBehavior.js";
|
|
77
|
+
export * from "./scrollRail.js";
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export * from "./chatBubbles.js";
|
|
|
5
5
|
export * from "./voice.js";
|
|
6
6
|
export * from "./social.js";
|
|
7
7
|
export * from "./hooks.js";
|
|
8
|
+
export * from "./browserLifecycle.js";
|
|
8
9
|
export * from "./useDebouncedCommit.js";
|
|
9
10
|
export * from "./store.js";
|
|
10
11
|
export * from "./settings.js";
|
|
@@ -72,3 +73,5 @@ export * from "./previewFixtures.js";
|
|
|
72
73
|
export * from "./streetGeometryPreview.js";
|
|
73
74
|
export * from "./saveSlots.js";
|
|
74
75
|
export * from "./cameraShake.js";
|
|
76
|
+
export * from "./dialogBehavior.js";
|
|
77
|
+
export * from "./scrollRail.js";
|