@jgengine/react 0.10.0 → 0.11.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 +13 -2
- package/LICENSE +203 -661
- package/README.md +1 -1
- package/dist/actionHud.d.ts +174 -0
- package/dist/actionHud.js +292 -0
- package/dist/cards.d.ts +59 -0
- package/dist/cards.js +116 -0
- package/dist/chat.d.ts +3 -1
- package/dist/chat.js +2 -2
- package/dist/components.d.ts +20 -0
- package/dist/components.js +28 -0
- package/dist/display.d.ts +1 -0
- package/dist/display.js +1 -0
- package/dist/engineStore.d.ts +3 -0
- package/dist/engineStore.js +3 -0
- package/dist/fogOverlay.d.ts +3 -0
- package/dist/fogOverlay.js +3 -0
- package/dist/gameViewport.js +16 -0
- package/dist/hooks.d.ts +20 -0
- package/dist/hooks.js +31 -1
- package/dist/hud.d.ts +87 -0
- package/dist/hud.js +144 -0
- package/dist/hudLayout.d.ts +19 -2
- package/dist/hudLayout.js +121 -10
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/keyHint.d.ts +30 -0
- package/dist/keyHint.js +31 -0
- package/dist/map.d.ts +69 -1
- package/dist/map.js +54 -1
- package/dist/save.d.ts +25 -0
- package/dist/save.js +37 -0
- package/dist/selectSnapshot.d.ts +2 -0
- package/dist/selectSnapshot.js +2 -0
- package/dist/selectionHud.d.ts +133 -0
- package/dist/selectionHud.js +182 -0
- package/dist/skillCheckPaint.d.ts +2 -0
- package/dist/skillCheckPaint.js +2 -0
- package/dist/startScreen.d.ts +62 -0
- package/dist/startScreen.js +62 -0
- package/dist/store.d.ts +24 -0
- package/dist/store.js +9 -0
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +51 -0
- package/skills/jgengine/api.md +434 -0
- package/skills/jgengine/capabilities.md +29 -0
- package/skills/jgengine/reference-cartridge.md +72 -0
- package/skills/jgengine-ui/SKILL.md +36 -0
- package/skills/jgengine-ui/api.md +1852 -0
- package/skills/jgengine-ui/capabilities.md +142 -0
- package/skills/jgengine-ui/reference.md +500 -0
- package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
- package/llms.txt +0 -1724
package/dist/hudLayout.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type CSSProperties, type ReactNode } from "react";
|
|
2
2
|
import { type HudAnchor, type HudLayoutStore } from "@jgengine/core/ui/hudLayout";
|
|
3
|
+
import { type EditorUiDocument } from "@jgengine/core/ui/hudDocument";
|
|
3
4
|
import { type HudPriority, type MobileHudBehavior } from "@jgengine/core/ui/gameLayout";
|
|
4
5
|
import type { GamePhase } from "@jgengine/core/game/gamePhase";
|
|
5
6
|
/** Whether a HUD element opted into `showDuring` is visible in the current phase; `undefined` = always visible (default). */
|
|
@@ -18,6 +19,13 @@ export declare function useHudLayout(options?: {
|
|
|
18
19
|
storageKey?: string;
|
|
19
20
|
snap?: number;
|
|
20
21
|
locked?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Scene-document `ui` section — source of truth for panel placement/size.
|
|
24
|
+
* When provided, hydrates the layout store (and wins over legacy localStorage).
|
|
25
|
+
*/
|
|
26
|
+
documentUi?: EditorUiDocument;
|
|
27
|
+
/** When true (default), canvas moves/resizes write undoable `setUiPanel` patches to a live editor host. */
|
|
28
|
+
documentPatches?: boolean;
|
|
21
29
|
}): HudLayoutStore;
|
|
22
30
|
/**
|
|
23
31
|
* Full-viewport HUD surface. Panels declared with `HudPanel` flow into nine
|
|
@@ -44,7 +52,7 @@ export declare function HudCanvas({ layout, editChord, compactScale, showDuring,
|
|
|
44
52
|
* leaves the flow and keeps its custom placement. On compact displays custom
|
|
45
53
|
* placements are ignored and the `compact` behavior applies.
|
|
46
54
|
*/
|
|
47
|
-
export declare function HudPanel({ id, anchor, order, compact: compactMode, chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region, className, style, children, }: {
|
|
55
|
+
export declare function HudPanel({ id, anchor, order, compact: compactMode, chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region, width, height, type, className, style, children, }: {
|
|
48
56
|
id: string;
|
|
49
57
|
anchor?: HudAnchor;
|
|
50
58
|
/** Stack position within the region, ascending outward from the screen edge. Default 0. */
|
|
@@ -55,7 +63,10 @@ export declare function HudPanel({ id, anchor, order, compact: compactMode, chip
|
|
|
55
63
|
chip?: string;
|
|
56
64
|
/** `false` lets pointer events pass through to the game (read-only panels). Default true. */
|
|
57
65
|
interactive?: boolean;
|
|
58
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Fallback pixel inset from the anchor when the scene document has no `ui.panels[id]`.
|
|
68
|
+
* Document layout is the source of truth once authored; TSX props are fallback-only.
|
|
69
|
+
*/
|
|
59
70
|
inset?: {
|
|
60
71
|
x: number;
|
|
61
72
|
y: number;
|
|
@@ -73,6 +84,12 @@ export declare function HudPanel({ id, anchor, order, compact: compactMode, chip
|
|
|
73
84
|
collisionGroup?: string;
|
|
74
85
|
/** Register this panel as a collision-tracked layout region. Default true. */
|
|
75
86
|
region?: boolean;
|
|
87
|
+
/** Fallback width when the document has not authored a size. */
|
|
88
|
+
width?: number;
|
|
89
|
+
/** Fallback height when the document has not authored a size. */
|
|
90
|
+
height?: number;
|
|
91
|
+
/** Panel type id ({@link registerHudPanelType}) — drives resize axes + ParamSchema. */
|
|
92
|
+
type?: string;
|
|
76
93
|
className?: string;
|
|
77
94
|
style?: CSSProperties;
|
|
78
95
|
children?: ReactNode;
|
package/dist/hudLayout.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
|
-
import { HUD_ANCHOR_FRACTIONS, anchoredPlacement, createHudLayout, isPanelDraggable, } from "@jgengine/core/ui/hudLayout";
|
|
4
|
+
import { HUD_ANCHOR_FRACTIONS, anchoredPlacement, createHudLayout, isPanelDraggable, registerActiveHudLayout, } from "@jgengine/core/ui/hudLayout";
|
|
5
|
+
import { resizePanelSize, resolvePanelResize, } from "@jgengine/core/ui/hudDocument";
|
|
5
6
|
import { hudScaleForViewport, overflowingPanels, resolveHudFit } from "@jgengine/core/ui/hudScale";
|
|
6
7
|
import { isMobileMode, } from "@jgengine/core/ui/gameLayout";
|
|
7
8
|
import { useDisplayProfile } from "./display.js";
|
|
@@ -20,18 +21,73 @@ const EDIT_BAR_Z = 100000;
|
|
|
20
21
|
const REGION_GAP = 10;
|
|
21
22
|
const COMPACT_HUD_SCALE = 0.85;
|
|
22
23
|
const HUD_ANCHORS = Object.keys(HUD_ANCHOR_FRACTIONS);
|
|
24
|
+
function patchDocumentUi(id, panel) {
|
|
25
|
+
if (typeof window === "undefined")
|
|
26
|
+
return;
|
|
27
|
+
const host = window.__jgengineEditorHost;
|
|
28
|
+
if (host === undefined)
|
|
29
|
+
return;
|
|
30
|
+
host.handle({
|
|
31
|
+
method: "dispatch",
|
|
32
|
+
command: { type: "setUiPanel", id, patch: panel },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function sizeStyle(panel) {
|
|
36
|
+
if (panel === undefined)
|
|
37
|
+
return {};
|
|
38
|
+
return {
|
|
39
|
+
...(panel.width === undefined ? {} : { width: panel.width }),
|
|
40
|
+
...(panel.height === undefined ? {} : { height: panel.height }),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function ResizeHandles({ axes, onResizeStart, }) {
|
|
44
|
+
if (axes === "none")
|
|
45
|
+
return null;
|
|
46
|
+
const handle = (edge, cursor, style) => (_jsx("div", { "data-hud-resize": edge, onPointerDown: (event) => {
|
|
47
|
+
event.stopPropagation();
|
|
48
|
+
onResizeStart(edge, event);
|
|
49
|
+
}, style: {
|
|
50
|
+
position: "absolute",
|
|
51
|
+
zIndex: 3,
|
|
52
|
+
pointerEvents: "auto",
|
|
53
|
+
background: "rgba(255, 255, 255, 0.85)",
|
|
54
|
+
cursor,
|
|
55
|
+
...style,
|
|
56
|
+
} }, edge));
|
|
57
|
+
return (_jsxs(_Fragment, { children: [axes === "x" || axes === "both"
|
|
58
|
+
? handle("e", "ew-resize", { top: 4, bottom: 4, right: -3, width: 6 })
|
|
59
|
+
: null, axes === "y" || axes === "both"
|
|
60
|
+
? handle("s", "ns-resize", { left: 4, right: 4, bottom: -3, height: 6 })
|
|
61
|
+
: null, axes === "both" ? handle("se", "nwse-resize", { right: -4, bottom: -4, width: 10, height: 10 }) : null] }));
|
|
62
|
+
}
|
|
23
63
|
export function useHudLayout(options) {
|
|
24
64
|
const storageKey = options?.storageKey;
|
|
25
65
|
const snap = options?.snap;
|
|
26
66
|
const locked = options?.locked;
|
|
67
|
+
const documentUi = options?.documentUi;
|
|
68
|
+
const documentPatches = options?.documentPatches !== false;
|
|
27
69
|
const layout = useMemo(() => {
|
|
28
|
-
const store = createHudLayout({
|
|
29
|
-
|
|
70
|
+
const store = createHudLayout({
|
|
71
|
+
snap,
|
|
72
|
+
locked,
|
|
73
|
+
onDocumentPatch: documentPatches ? patchDocumentUi : undefined,
|
|
74
|
+
});
|
|
75
|
+
if (documentUi !== undefined) {
|
|
76
|
+
store.applyDocumentUi(documentUi);
|
|
77
|
+
}
|
|
78
|
+
else if (storageKey !== undefined && typeof localStorage !== "undefined") {
|
|
30
79
|
store.hydrate(localStorage.getItem(STORAGE_PREFIX + storageKey));
|
|
31
80
|
}
|
|
32
81
|
return store;
|
|
33
|
-
}, [storageKey, snap, locked]);
|
|
82
|
+
}, [storageKey, snap, locked, documentPatches]);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (documentUi !== undefined) {
|
|
85
|
+
layout.applyDocumentUi(documentUi);
|
|
86
|
+
}
|
|
87
|
+
}, [layout, documentUi]);
|
|
34
88
|
useEffect(() => {
|
|
89
|
+
if (documentUi !== undefined)
|
|
90
|
+
return;
|
|
35
91
|
if (storageKey === undefined || typeof localStorage === "undefined")
|
|
36
92
|
return;
|
|
37
93
|
let timer = null;
|
|
@@ -50,7 +106,7 @@ export function useHudLayout(options) {
|
|
|
50
106
|
localStorage.setItem(STORAGE_PREFIX + storageKey, layout.serialize());
|
|
51
107
|
}
|
|
52
108
|
};
|
|
53
|
-
}, [layout, storageKey]);
|
|
109
|
+
}, [layout, storageKey, documentUi]);
|
|
54
110
|
return layout;
|
|
55
111
|
}
|
|
56
112
|
const HudCanvasContext = createContext(null);
|
|
@@ -160,6 +216,7 @@ export function HudCanvas({ layout, editChord, compactScale, showDuring, classNa
|
|
|
160
216
|
}, []);
|
|
161
217
|
const value = useMemo(() => ({ layout, canvasRef, regions, compact }), [layout, regions, compact]);
|
|
162
218
|
const editing = useEngineState(layout).editing;
|
|
219
|
+
useEffect(() => registerActiveHudLayout(layout), [layout]);
|
|
163
220
|
const chordEnabled = editChord !== false;
|
|
164
221
|
const hold = editChord === false ? undefined : (editChord?.hold ?? "F2");
|
|
165
222
|
const press = editChord === false ? undefined : (editChord?.press ?? "KeyC");
|
|
@@ -344,7 +401,7 @@ function HudChip({ label, anchor, children, }) {
|
|
|
344
401
|
* leaves the flow and keeps its custom placement. On compact displays custom
|
|
345
402
|
* placements are ignored and the `compact` behavior applies.
|
|
346
403
|
*/
|
|
347
|
-
export function HudPanel({ id, anchor = "top-left", order, compact: compactMode = "keep", chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, className, style, children, }) {
|
|
404
|
+
export function HudPanel({ id, anchor = "top-left", order, compact: compactMode = "keep", chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, width, height, type, className, style, children, }) {
|
|
348
405
|
const ctx = useContext(HudCanvasContext);
|
|
349
406
|
const phase = useOptionalGamePhase();
|
|
350
407
|
const layoutMode = useGameLayoutMode();
|
|
@@ -355,12 +412,18 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
|
|
|
355
412
|
const registeredOnRef = useRef(null);
|
|
356
413
|
if (registeredOnRef.current !== layout) {
|
|
357
414
|
registeredOnRef.current = layout;
|
|
358
|
-
layout.register(id, anchoredPlacement(anchor, inset ?? { x: 16, y: 16 })
|
|
415
|
+
layout.register(id, anchoredPlacement(anchor, inset ?? { x: 16, y: 16 }), {
|
|
416
|
+
width,
|
|
417
|
+
height,
|
|
418
|
+
type,
|
|
419
|
+
});
|
|
359
420
|
}
|
|
360
421
|
const layoutState = useEngineState(layout);
|
|
361
422
|
const panel = layoutState.panels[id];
|
|
362
423
|
const draggable = !compact && locked !== true && isPanelDraggable(layoutState, id);
|
|
363
424
|
const editing = layoutState.editing && draggable;
|
|
425
|
+
const resizeSpec = resolvePanelResize(panel?.type ?? type);
|
|
426
|
+
const resizable = editing && resizeSpec.axes !== "none";
|
|
364
427
|
const rootRef = useRef(null);
|
|
365
428
|
const dragRef = useRef(null);
|
|
366
429
|
const suppressClickRef = useRef(false);
|
|
@@ -368,6 +431,7 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
|
|
|
368
431
|
const [dragging, setDragging] = useState(false);
|
|
369
432
|
useEffect(() => () => detachRef.current?.(), []);
|
|
370
433
|
const regionHidden = panel === undefined ||
|
|
434
|
+
panel.visible === false ||
|
|
371
435
|
(compact && compactMode === "hide") ||
|
|
372
436
|
(onMobile && mobileBehavior === "hidden") ||
|
|
373
437
|
!hudVisibleInPhase(showDuring, phase);
|
|
@@ -383,6 +447,8 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
|
|
|
383
447
|
const onPointerDown = (event) => {
|
|
384
448
|
if (!draggable || event.button !== 0)
|
|
385
449
|
return;
|
|
450
|
+
if (event.target?.closest?.("[data-hud-resize]") != null)
|
|
451
|
+
return;
|
|
386
452
|
const root = rootRef.current;
|
|
387
453
|
const canvas = canvasRef.current;
|
|
388
454
|
if (root === null || canvas === null)
|
|
@@ -439,6 +505,44 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
|
|
|
439
505
|
window.addEventListener("pointerup", onEnd);
|
|
440
506
|
window.addEventListener("pointercancel", onEnd);
|
|
441
507
|
};
|
|
508
|
+
const onResizeStart = (edge, event) => {
|
|
509
|
+
if (!resizable || event.button !== 0)
|
|
510
|
+
return;
|
|
511
|
+
const root = rootRef.current;
|
|
512
|
+
if (root === null)
|
|
513
|
+
return;
|
|
514
|
+
const startWidth = panel?.width ?? root.getBoundingClientRect().width;
|
|
515
|
+
const startHeight = panel?.height ?? root.getBoundingClientRect().height;
|
|
516
|
+
const pointerId = event.pointerId;
|
|
517
|
+
const startX = event.clientX;
|
|
518
|
+
const startY = event.clientY;
|
|
519
|
+
suppressClickRef.current = true;
|
|
520
|
+
layout.bringToFront(id);
|
|
521
|
+
const onMove = (moveEvent) => {
|
|
522
|
+
if (moveEvent.pointerId !== pointerId)
|
|
523
|
+
return;
|
|
524
|
+
const dw = edge === "s" ? 0 : moveEvent.clientX - startX;
|
|
525
|
+
const dh = edge === "e" ? 0 : moveEvent.clientY - startY;
|
|
526
|
+
const next = resizePanelSize({ width: startWidth, height: startHeight }, { dw, dh }, resizeSpec.axes, resizeSpec);
|
|
527
|
+
layout.resize(id, next);
|
|
528
|
+
};
|
|
529
|
+
const onEnd = (endEvent) => {
|
|
530
|
+
if (endEvent.pointerId !== pointerId)
|
|
531
|
+
return;
|
|
532
|
+
detachRef.current?.();
|
|
533
|
+
};
|
|
534
|
+
detachRef.current?.();
|
|
535
|
+
const detach = () => {
|
|
536
|
+
window.removeEventListener("pointermove", onMove);
|
|
537
|
+
window.removeEventListener("pointerup", onEnd);
|
|
538
|
+
window.removeEventListener("pointercancel", onEnd);
|
|
539
|
+
detachRef.current = null;
|
|
540
|
+
};
|
|
541
|
+
detachRef.current = detach;
|
|
542
|
+
window.addEventListener("pointermove", onMove);
|
|
543
|
+
window.addEventListener("pointerup", onEnd);
|
|
544
|
+
window.addEventListener("pointercancel", onEnd);
|
|
545
|
+
};
|
|
442
546
|
const onClickCapture = (event) => {
|
|
443
547
|
if (!suppressClickRef.current)
|
|
444
548
|
return;
|
|
@@ -458,15 +562,18 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
|
|
|
458
562
|
color: "rgba(255, 255, 255, 0.9)",
|
|
459
563
|
font: "10px/1.5 system-ui, sans-serif",
|
|
460
564
|
whiteSpace: "nowrap",
|
|
461
|
-
}, children: id })] })) : null;
|
|
565
|
+
}, children: id }), resizable ? _jsx(ResizeHandles, { axes: resizeSpec.axes, onResizeStart: onResizeStart }) : null] })) : null;
|
|
462
566
|
if (panel === undefined)
|
|
463
567
|
return null;
|
|
568
|
+
if (panel.visible === false)
|
|
569
|
+
return null;
|
|
464
570
|
if (compact && compactMode === "hide")
|
|
465
571
|
return null;
|
|
466
572
|
if (onMobile && mobileBehavior === "hidden")
|
|
467
573
|
return null;
|
|
468
574
|
if (!hudVisibleInPhase(showDuring, phase))
|
|
469
575
|
return null;
|
|
576
|
+
const authoredSize = sizeStyle(panel);
|
|
470
577
|
const flow = compact || !panel.moved;
|
|
471
578
|
if (flow) {
|
|
472
579
|
const regionEl = regions[compact ? anchor : panel.placement.anchor];
|
|
@@ -474,7 +581,7 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
|
|
|
474
581
|
return null;
|
|
475
582
|
const chipped = compact && compactMode === "chip";
|
|
476
583
|
const passThrough = interactive === false && !editing && !chipped;
|
|
477
|
-
return createPortal(_jsxs("div", { ref: rootRef, "data-hud-panel": id, "data-dragging": dragging ? "" : undefined, "data-hud-priority": priority, "data-hud-mobile-behavior": mobileBehavior, className: className, onPointerDown: onPointerDown, onClickCapture: onClickCapture, style: {
|
|
584
|
+
return createPortal(_jsxs("div", { ref: rootRef, "data-hud-panel": id, "data-dragging": dragging ? "" : undefined, "data-hud-priority": priority, "data-hud-mobile-behavior": mobileBehavior, "data-hud-type": panel.type ?? type, className: className, onPointerDown: onPointerDown, onClickCapture: onClickCapture, style: {
|
|
478
585
|
position: "relative",
|
|
479
586
|
order: order ?? 0,
|
|
480
587
|
pointerEvents: passThrough ? "none" : "auto",
|
|
@@ -483,6 +590,8 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
|
|
|
483
590
|
cursor: dragging ? "grabbing" : editing ? "grab" : undefined,
|
|
484
591
|
outline: editing ? "1px dashed rgba(255, 255, 255, 0.6)" : undefined,
|
|
485
592
|
outlineOffset: editing ? 2 : undefined,
|
|
593
|
+
boxSizing: "border-box",
|
|
594
|
+
...authoredSize,
|
|
486
595
|
...style,
|
|
487
596
|
}, children: [chipped ? (_jsx(HudChip, { label: chip ?? id, anchor: anchor, children: children })) : (children), editingChrome] }), regionEl);
|
|
488
597
|
}
|
|
@@ -503,7 +612,7 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
|
|
|
503
612
|
if (fx === 0.5 || fy === 0.5) {
|
|
504
613
|
position.transform = `translate(${fx === 0.5 ? "-50%" : "0"}, ${fy === 0.5 ? "-50%" : "0"})`;
|
|
505
614
|
}
|
|
506
|
-
return (_jsxs("div", { ref: rootRef, "data-hud-panel": id, "data-dragging": dragging ? "" : undefined, className: className, onPointerDown: onPointerDown, onClickCapture: onClickCapture, style: {
|
|
615
|
+
return (_jsxs("div", { ref: rootRef, "data-hud-panel": id, "data-dragging": dragging ? "" : undefined, "data-hud-type": panel.type ?? type, className: className, onPointerDown: onPointerDown, onClickCapture: onClickCapture, style: {
|
|
507
616
|
position: "absolute",
|
|
508
617
|
...position,
|
|
509
618
|
zIndex: panel.z,
|
|
@@ -513,6 +622,8 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
|
|
|
513
622
|
cursor: dragging ? "grabbing" : editing ? "grab" : undefined,
|
|
514
623
|
outline: editing ? "1px dashed rgba(255, 255, 255, 0.6)" : undefined,
|
|
515
624
|
outlineOffset: editing ? 2 : undefined,
|
|
625
|
+
boxSizing: "border-box",
|
|
626
|
+
...authoredSize,
|
|
516
627
|
...style,
|
|
517
628
|
}, children: [children, editingChrome] }));
|
|
518
629
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,10 +5,18 @@ 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 "./store.js";
|
|
8
9
|
export * from "./settings.js";
|
|
10
|
+
export * from "./save.js";
|
|
9
11
|
export * from "./components.js";
|
|
12
|
+
export * from "./hud.js";
|
|
13
|
+
export * from "./keyHint.js";
|
|
14
|
+
export * from "./startScreen.js";
|
|
10
15
|
export * from "./engineStore.js";
|
|
11
16
|
export * from "./dragLayer.js";
|
|
17
|
+
export * from "./cards.js";
|
|
18
|
+
export * from "./actionHud.js";
|
|
19
|
+
export * from "./selectionHud.js";
|
|
12
20
|
export * from "./hudLayout.js";
|
|
13
21
|
export * from "./hudViewport.js";
|
|
14
22
|
export * from "./gameViewport.js";
|
package/dist/index.js
CHANGED
|
@@ -5,10 +5,18 @@ 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 "./store.js";
|
|
8
9
|
export * from "./settings.js";
|
|
10
|
+
export * from "./save.js";
|
|
9
11
|
export * from "./components.js";
|
|
12
|
+
export * from "./hud.js";
|
|
13
|
+
export * from "./keyHint.js";
|
|
14
|
+
export * from "./startScreen.js";
|
|
10
15
|
export * from "./engineStore.js";
|
|
11
16
|
export * from "./dragLayer.js";
|
|
17
|
+
export * from "./cards.js";
|
|
18
|
+
export * from "./actionHud.js";
|
|
19
|
+
export * from "./selectionHud.js";
|
|
12
20
|
export * from "./hudLayout.js";
|
|
13
21
|
export * from "./hudViewport.js";
|
|
14
22
|
export * from "./gameViewport.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A single keyboard/mouse key rendered as a cap. Tagged `data-jg-kbd` so tooling
|
|
4
|
+
* can find it; on its own it stays visible (hiding a lone cap would strand its
|
|
5
|
+
* label). Wrap keys + their meaning in `KeyHint` to make the whole hint vanish
|
|
6
|
+
* on touch.
|
|
7
|
+
*
|
|
8
|
+
* @capability keycap styled keyboard/mouse key cap for control hints
|
|
9
|
+
*/
|
|
10
|
+
export declare function Keycap({ children, className, style, }: {
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
className?: string;
|
|
13
|
+
style?: CSSProperties;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
15
|
+
/**
|
|
16
|
+
* Wraps a keyboard/mouse control hint (a cap plus what it does). Renders nothing
|
|
17
|
+
* on coarse-pointer devices — a touchscreen has no keyboard, so the hint is
|
|
18
|
+
* noise and only fights the on-screen controls for space. The `data-jg-kbd-hint`
|
|
19
|
+
* marker is also hidden by an engine stylesheet as a hydration-safe backstop, so
|
|
20
|
+
* a hint stays hidden on touch even before this component's effect runs. Put the
|
|
21
|
+
* touch equivalent (a `TouchControls` dock, a tappable button) on the coarse
|
|
22
|
+
* branch instead.
|
|
23
|
+
*
|
|
24
|
+
* @capability key-hint keyboard/mouse control hint that hides itself on touch
|
|
25
|
+
*/
|
|
26
|
+
export declare function KeyHint({ children, className, style, }: {
|
|
27
|
+
children: ReactNode;
|
|
28
|
+
className?: string;
|
|
29
|
+
style?: CSSProperties;
|
|
30
|
+
}): import("react").JSX.Element | null;
|
package/dist/keyHint.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import {} from "react";
|
|
3
|
+
import { useDisplayProfile } from "./display.js";
|
|
4
|
+
/**
|
|
5
|
+
* A single keyboard/mouse key rendered as a cap. Tagged `data-jg-kbd` so tooling
|
|
6
|
+
* can find it; on its own it stays visible (hiding a lone cap would strand its
|
|
7
|
+
* label). Wrap keys + their meaning in `KeyHint` to make the whole hint vanish
|
|
8
|
+
* on touch.
|
|
9
|
+
*
|
|
10
|
+
* @capability keycap styled keyboard/mouse key cap for control hints
|
|
11
|
+
*/
|
|
12
|
+
export function Keycap({ children, className, style, }) {
|
|
13
|
+
return (_jsx("kbd", { "data-jg-kbd": "", className: className, style: style, children: children }));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Wraps a keyboard/mouse control hint (a cap plus what it does). Renders nothing
|
|
17
|
+
* on coarse-pointer devices — a touchscreen has no keyboard, so the hint is
|
|
18
|
+
* noise and only fights the on-screen controls for space. The `data-jg-kbd-hint`
|
|
19
|
+
* marker is also hidden by an engine stylesheet as a hydration-safe backstop, so
|
|
20
|
+
* a hint stays hidden on touch even before this component's effect runs. Put the
|
|
21
|
+
* touch equivalent (a `TouchControls` dock, a tappable button) on the coarse
|
|
22
|
+
* branch instead.
|
|
23
|
+
*
|
|
24
|
+
* @capability key-hint keyboard/mouse control hint that hides itself on touch
|
|
25
|
+
*/
|
|
26
|
+
export function KeyHint({ children, className, style, }) {
|
|
27
|
+
const { coarsePointer } = useDisplayProfile();
|
|
28
|
+
if (coarsePointer)
|
|
29
|
+
return null;
|
|
30
|
+
return (_jsx("span", { "data-jg-kbd-hint": "", className: className, style: style, children: children }));
|
|
31
|
+
}
|
package/dist/map.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import type { FogField } from "@jgengine/core/world/fog";
|
|
3
3
|
import { type MapMarker, type MarkerKindStyle, type MarkerSet } from "@jgengine/core/world/markers";
|
|
4
|
-
import { type WorldXZ } from "@jgengine/core/world/minimap";
|
|
4
|
+
import { type MinimapView, type WorldXZ } from "@jgengine/core/world/minimap";
|
|
5
5
|
import { type MapCellStates, type MapRoute, type MapZone } from "@jgengine/core/world/mapLayers";
|
|
6
6
|
export declare function useMarkers(markers: MarkerSet): readonly MapMarker[];
|
|
7
7
|
export declare function useFog(fog: FogField): ReturnType<FogField["cells"]>;
|
|
@@ -40,8 +40,53 @@ export interface MapBounds {
|
|
|
40
40
|
* Framed circular minimap: optional baked terrain background, reveal-on-event
|
|
41
41
|
* fog overlay, categorized marker icons, and a facing arrow. Reads a core
|
|
42
42
|
* `MarkerSet` / `FogField`; supply your own `kindStyles` palette to reskin.
|
|
43
|
+
*
|
|
44
|
+
* @capability minimap framed circular minimap with terrain bake, fog, markers, and facing arrow
|
|
43
45
|
*/
|
|
44
46
|
export declare function Minimap({ markers, center, worldRadius, fog, size, facingYaw, rotate, kindStyles, background, mapBounds, routes, zones, cellStates, onWorldClick, className, title, children, }: MinimapProps): ReactNode;
|
|
47
|
+
/** One dot (or, with `heading`, a rotated arrow) drawn by `MinimapChrome`. */
|
|
48
|
+
export interface MinimapChromeMarker {
|
|
49
|
+
id: string;
|
|
50
|
+
position: WorldXZ | readonly [number, number, number];
|
|
51
|
+
/** Bearing (radians) to draw an arrow instead of a dot — pass `headingToBearing(yaw)` for the player/other facing blips. */
|
|
52
|
+
heading?: number;
|
|
53
|
+
color?: string;
|
|
54
|
+
radius?: number;
|
|
55
|
+
glyph?: string;
|
|
56
|
+
glyphColor?: string;
|
|
57
|
+
strokeColor?: string;
|
|
58
|
+
strokeWidth?: number;
|
|
59
|
+
/** Clamp to the minimap edge when outside `view.worldRadius` (off-screen indicator); `false` hides it instead. Default `true`. */
|
|
60
|
+
clampToEdge?: boolean;
|
|
61
|
+
}
|
|
62
|
+
/** Props for `MinimapChrome`. */
|
|
63
|
+
export interface MinimapChromeProps {
|
|
64
|
+
/** Shared with `projectToMinimap`/`clampToMinimapEdge` — pass `view.rotate` to keep a rotating map's arrows pointed correctly. */
|
|
65
|
+
view: MinimapView;
|
|
66
|
+
/** Ring outline (+ `cardinalLabel`). Default `false` — most games draw their own frame/panel around the chrome. */
|
|
67
|
+
frame?: boolean;
|
|
68
|
+
frameColor?: string;
|
|
69
|
+
frameStrokeWidth?: number;
|
|
70
|
+
/** Label drawn at the top of the ring when `frame` is on; `false` omits it. */
|
|
71
|
+
cardinalLabel?: string | false;
|
|
72
|
+
markers?: readonly MinimapChromeMarker[];
|
|
73
|
+
markerColor?: string;
|
|
74
|
+
markerRadius?: number;
|
|
75
|
+
className?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Headless minimap chrome — nests inside a game's own `<svg>`: an optional
|
|
79
|
+
* ring frame, and edge-clamped marker dots that draw as directional arrows
|
|
80
|
+
* when a marker carries a `heading` (the compass-arrow / player-blip layer
|
|
81
|
+
* ~8 games were re-drawing by hand over `projectToMinimap`/`clampToMinimapEdge`).
|
|
82
|
+
* Renders a bare `<g>` — no background, panel, or title; compose your own
|
|
83
|
+
* content layer (routes, zones, terrain) as sibling SVG nodes for full control
|
|
84
|
+
* of z-order, or call `MinimapChrome` twice (once for `frame`, again for
|
|
85
|
+
* `markers`) to sandwich custom content between the ring and the blips.
|
|
86
|
+
*
|
|
87
|
+
* @capability minimap-chrome ring frame plus edge-clamped, heading-aware marker dots to nest inside a game's own `<svg>`
|
|
88
|
+
*/
|
|
89
|
+
export declare function MinimapChrome({ view, frame, frameColor, frameStrokeWidth, cardinalLabel, markers, markerColor, markerRadius, className, }: MinimapChromeProps): ReactNode;
|
|
45
90
|
export interface CompassProps {
|
|
46
91
|
/** Facing yaw (`rotationY`, forward = `(sin yaw, cos yaw)`) of the tracked entity — pass it raw, never pre-converted to a bearing. */
|
|
47
92
|
facingYaw: number;
|
|
@@ -57,6 +102,29 @@ export interface CompassProps {
|
|
|
57
102
|
* eight cardinals and optional marker pips (bearing to each `MarkerSet` entry).
|
|
58
103
|
*/
|
|
59
104
|
export declare function Compass({ facingYaw, center, markers, width, fov, kindStyles, className, }: CompassProps): ReactNode;
|
|
105
|
+
/** Props for `MinimapPanel` — all `MinimapProps` plus the zone-label/clock header and compass slots. */
|
|
106
|
+
export interface MinimapPanelProps extends MinimapProps {
|
|
107
|
+
/** Zone/area name slot rendered in the chrome header — the game supplies its own zone lookup. */
|
|
108
|
+
zoneLabel?: ReactNode;
|
|
109
|
+
/** Clock readout slot rendered in the chrome header — pair with `useGameClock()` + `calendar()`. */
|
|
110
|
+
clock?: ReactNode;
|
|
111
|
+
/** Compass strip beneath the circle. Default true; pass `false` to omit it entirely. */
|
|
112
|
+
showCompass?: boolean;
|
|
113
|
+
compassProps?: Partial<Omit<CompassProps, "className">>;
|
|
114
|
+
headerClassName?: string;
|
|
115
|
+
zoneLabelClassName?: string;
|
|
116
|
+
clockClassName?: string;
|
|
117
|
+
compassClassName?: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Composed circular-minimap chrome: optional zone-label + clock header above
|
|
121
|
+
* the `Minimap`, optional `Compass` strip below. Purely a wiring layer over
|
|
122
|
+
* the existing primitives — the game supplies the zone name and clock text,
|
|
123
|
+
* this only places them; omit either slot and the header disappears.
|
|
124
|
+
*
|
|
125
|
+
* @capability minimap-panel circular minimap with zone label, clock, and compass composed together
|
|
126
|
+
*/
|
|
127
|
+
export declare function MinimapPanel({ zoneLabel, clock, showCompass, compassProps, headerClassName, zoneLabelClassName, clockClassName, compassClassName, children, ...minimapProps }: MinimapPanelProps): ReactNode;
|
|
60
128
|
export interface WorldMapProps {
|
|
61
129
|
markers: MarkerSet;
|
|
62
130
|
bounds: MapBounds;
|
package/dist/map.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useSyncExternalStore } from "react";
|
|
3
3
|
import { DEFAULT_MARKER_KINDS, markerKindStyle, } from "@jgengine/core/world/markers";
|
|
4
4
|
import { bearingToCardinal, clampToMinimapEdge, compassBearing, headingToBearing, projectToMinimap, relativeBearing, unprojectFromMinimap, } from "@jgengine/core/world/minimap";
|
|
@@ -78,6 +78,8 @@ export function useFog(fog) {
|
|
|
78
78
|
* Framed circular minimap: optional baked terrain background, reveal-on-event
|
|
79
79
|
* fog overlay, categorized marker icons, and a facing arrow. Reads a core
|
|
80
80
|
* `MarkerSet` / `FogField`; supply your own `kindStyles` palette to reskin.
|
|
81
|
+
*
|
|
82
|
+
* @capability minimap framed circular minimap with terrain bake, fog, markers, and facing arrow
|
|
81
83
|
*/
|
|
82
84
|
export function Minimap({ markers, center, worldRadius, fog, size = 176, facingYaw = 0, rotate = false, kindStyles = DEFAULT_MARKER_KINDS, background, mapBounds, routes, zones, cellStates, onWorldClick, className, title = "Map", children, }) {
|
|
83
85
|
const markerList = useMarkers(markers);
|
|
@@ -162,6 +164,36 @@ export function Minimap({ markers, center, worldRadius, fog, size = 176, facingY
|
|
|
162
164
|
return (_jsxs("g", { "data-marker-kind": marker.kind, children: [_jsx("circle", { cx: at.x, cy: at.y, r: 7, fill: "rgba(2,6,12,0.55)" }), _jsx("text", { x: at.x, y: at.y + 3.5, textAnchor: "middle", fontSize: 10, fill: style.color, style: { fontWeight: 700 }, children: style.glyph })] }, marker.id));
|
|
163
165
|
}), _jsx("g", { transform: `translate(${half} ${half}) rotate(${(rotate ? 0 : bearing) * (180 / Math.PI)})`, children: _jsx("path", { d: "M0,-9 L6,7 L0,3 L-6,7 Z", fill: "#4ade80", stroke: "#052e16", strokeWidth: 0.75 }) })] }), _jsx("circle", { cx: half, cy: half, r: half - 1, fill: "none", stroke: "rgba(148,163,184,0.4)", strokeWidth: 1.5 }), _jsx("text", { x: half, y: 13, textAnchor: "middle", fontSize: 11, fill: "rgba(226,232,240,0.9)", style: { fontWeight: 700 }, children: "N" })] }), children] }));
|
|
164
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Headless minimap chrome — nests inside a game's own `<svg>`: an optional
|
|
169
|
+
* ring frame, and edge-clamped marker dots that draw as directional arrows
|
|
170
|
+
* when a marker carries a `heading` (the compass-arrow / player-blip layer
|
|
171
|
+
* ~8 games were re-drawing by hand over `projectToMinimap`/`clampToMinimapEdge`).
|
|
172
|
+
* Renders a bare `<g>` — no background, panel, or title; compose your own
|
|
173
|
+
* content layer (routes, zones, terrain) as sibling SVG nodes for full control
|
|
174
|
+
* of z-order, or call `MinimapChrome` twice (once for `frame`, again for
|
|
175
|
+
* `markers`) to sandwich custom content between the ring and the blips.
|
|
176
|
+
*
|
|
177
|
+
* @capability minimap-chrome ring frame plus edge-clamped, heading-aware marker dots to nest inside a game's own `<svg>`
|
|
178
|
+
*/
|
|
179
|
+
export function MinimapChrome({ view, frame = false, frameColor = "rgba(148,163,184,0.4)", frameStrokeWidth = 1.5, cardinalLabel = "N", markers = [], markerColor = "#e2e8f0", markerRadius = 5, className, }) {
|
|
180
|
+
const half = view.size / 2;
|
|
181
|
+
const rotate = view.rotate ?? 0;
|
|
182
|
+
return (_jsxs("g", { className: className, "data-minimap-chrome": true, children: [frame ? (_jsxs(_Fragment, { children: [_jsx("circle", { cx: half, cy: half, r: half - 1, fill: "none", stroke: frameColor, strokeWidth: frameStrokeWidth }), cardinalLabel !== false ? (_jsx("text", { x: half, y: 13, textAnchor: "middle", fontSize: 11, fill: frameColor, style: { fontWeight: 700 }, children: cardinalLabel })) : null] })) : null, markers.map((marker) => {
|
|
183
|
+
const projected = projectToMinimap(marker.position, view);
|
|
184
|
+
const clamp = marker.clampToEdge ?? true;
|
|
185
|
+
if (!projected.inside && !clamp)
|
|
186
|
+
return null;
|
|
187
|
+
const at = projected.inside ? projected : clampToMinimapEdge(projected, view.size);
|
|
188
|
+
const color = marker.color ?? markerColor;
|
|
189
|
+
const radius = marker.radius ?? markerRadius;
|
|
190
|
+
if (marker.heading !== undefined) {
|
|
191
|
+
const deg = (marker.heading - rotate) * (180 / Math.PI);
|
|
192
|
+
return (_jsx("g", { "data-minimap-marker": marker.id, transform: `translate(${at.x} ${at.y}) rotate(${deg})`, children: _jsx("path", { d: `M0,${-radius} L${radius * 0.7},${radius * 0.8} L0,${radius * 0.35} L${-radius * 0.7},${radius * 0.8} Z`, fill: color, stroke: marker.strokeColor ?? "rgba(0,0,0,0.6)", strokeWidth: marker.strokeWidth ?? 0.75 }) }, marker.id));
|
|
193
|
+
}
|
|
194
|
+
return (_jsxs("g", { "data-minimap-marker": marker.id, children: [_jsx("circle", { cx: at.x, cy: at.y, r: radius, fill: color, stroke: marker.strokeColor, strokeWidth: marker.strokeWidth }), marker.glyph !== undefined ? (_jsx("text", { x: at.x, y: at.y + 3.5, textAnchor: "middle", fontSize: 10, fill: marker.glyphColor ?? "#0f172a", style: { fontWeight: 700 }, children: marker.glyph })) : null] }, marker.id));
|
|
195
|
+
})] }));
|
|
196
|
+
}
|
|
165
197
|
const NO_SUBSCRIBE = () => () => undefined;
|
|
166
198
|
const NULL_CELLS = () => null;
|
|
167
199
|
const EMPTY_MARKERS_VALUE = [];
|
|
@@ -240,6 +272,27 @@ export function Compass({ facingYaw, center, markers, width = 340, fov = (Math.P
|
|
|
240
272
|
})
|
|
241
273
|
: null] }));
|
|
242
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Composed circular-minimap chrome: optional zone-label + clock header above
|
|
277
|
+
* the `Minimap`, optional `Compass` strip below. Purely a wiring layer over
|
|
278
|
+
* the existing primitives — the game supplies the zone name and clock text,
|
|
279
|
+
* this only places them; omit either slot and the header disappears.
|
|
280
|
+
*
|
|
281
|
+
* @capability minimap-panel circular minimap with zone label, clock, and compass composed together
|
|
282
|
+
*/
|
|
283
|
+
export function MinimapPanel({ zoneLabel, clock, showCompass = true, compassProps, headerClassName, zoneLabelClassName, clockClassName, compassClassName, children, ...minimapProps }) {
|
|
284
|
+
const hasHeader = zoneLabel !== undefined || clock !== undefined;
|
|
285
|
+
return (_jsxs("div", { "data-minimap-chrome": true, children: [hasHeader ? (_jsxs("div", { className: headerClassName, "data-minimap-chrome-header": true, style: {
|
|
286
|
+
display: "flex",
|
|
287
|
+
justifyContent: "space-between",
|
|
288
|
+
alignItems: "baseline",
|
|
289
|
+
gap: 8,
|
|
290
|
+
marginBottom: 4,
|
|
291
|
+
width: minimapProps.size ?? 176,
|
|
292
|
+
color: "#e2e8f0",
|
|
293
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
294
|
+
}, children: [zoneLabel !== undefined ? (_jsx("span", { className: zoneLabelClassName, "data-minimap-chrome-zone": true, style: { fontSize: 12, fontWeight: 700 }, children: zoneLabel })) : (_jsx("span", {})), clock !== undefined ? (_jsx("span", { className: clockClassName, "data-minimap-chrome-clock": true, style: { fontSize: 11, color: "rgba(226,232,240,0.75)" }, children: clock })) : null] })) : null, _jsx(Minimap, { ...minimapProps, children: children }), showCompass ? (_jsx("div", { className: compassClassName, "data-minimap-chrome-compass": true, style: { marginTop: 6 }, children: _jsx(Compass, { facingYaw: minimapProps.facingYaw ?? 0, center: minimapProps.center, markers: minimapProps.markers, width: minimapProps.size ?? 176, ...compassProps }) })) : null] }));
|
|
295
|
+
}
|
|
243
296
|
function WorldMapFogImage({ fogCells, project, worldW, worldD, width, mapH, }) {
|
|
244
297
|
const fogImage = useMemo(() => {
|
|
245
298
|
if (fogCells === null)
|
package/dist/save.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SaveStatus, SaveStore } from "@jgengine/core/game/saveStore";
|
|
2
|
+
/** The reactive slice of a {@link SaveStore} returned by {@link useSaveStore} — the live value plus bound save actions, all re-rendering on change. */
|
|
3
|
+
export interface SaveStoreView<T> {
|
|
4
|
+
value: T;
|
|
5
|
+
status: SaveStatus;
|
|
6
|
+
slot: string;
|
|
7
|
+
/** True while the initial `load()` is in flight — gate a loading screen on it. */
|
|
8
|
+
loading: boolean;
|
|
9
|
+
/** True while a write is in flight — show a "Saving…" indicator. */
|
|
10
|
+
saving: boolean;
|
|
11
|
+
set: (value: T) => void;
|
|
12
|
+
patch: (mutate: (previous: T) => T) => void;
|
|
13
|
+
save: () => Promise<void>;
|
|
14
|
+
clear: () => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Bind a {@link SaveStore} to React: returns the live `value` and `status` and
|
|
18
|
+
* re-renders whenever either changes. By default it calls `load()` once on
|
|
19
|
+
* mount (pass `{ load: false }` to hydrate yourself). The same hook works for
|
|
20
|
+
* offline (localStorage) and cloud (Convex/DB) saves — only the store's backend
|
|
21
|
+
* differs.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useSaveStore<T>(store: SaveStore<T>, options?: {
|
|
24
|
+
load?: boolean;
|
|
25
|
+
}): SaveStoreView<T>;
|
package/dist/save.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Bind a {@link SaveStore} to React: returns the live `value` and `status` and
|
|
4
|
+
* re-renders whenever either changes. By default it calls `load()` once on
|
|
5
|
+
* mount (pass `{ load: false }` to hydrate yourself). The same hook works for
|
|
6
|
+
* offline (localStorage) and cloud (Convex/DB) saves — only the store's backend
|
|
7
|
+
* differs.
|
|
8
|
+
*/
|
|
9
|
+
export function useSaveStore(store, options) {
|
|
10
|
+
useSyncExternalStore(store.subscribe, store.revision, store.revision);
|
|
11
|
+
const shouldLoad = options?.load !== false;
|
|
12
|
+
const loadedFor = useRef(null);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!shouldLoad)
|
|
15
|
+
return;
|
|
16
|
+
if (loadedFor.current === store)
|
|
17
|
+
return;
|
|
18
|
+
loadedFor.current = store;
|
|
19
|
+
void store.load();
|
|
20
|
+
}, [store, shouldLoad]);
|
|
21
|
+
const set = useCallback((value) => store.set(value), [store]);
|
|
22
|
+
const patch = useCallback((mutate) => void store.patch(mutate), [store]);
|
|
23
|
+
const save = useCallback(() => store.save(), [store]);
|
|
24
|
+
const clear = useCallback(() => store.clear(), [store]);
|
|
25
|
+
const status = store.status();
|
|
26
|
+
return {
|
|
27
|
+
value: store.value(),
|
|
28
|
+
status,
|
|
29
|
+
slot: store.slot(),
|
|
30
|
+
loading: status === "loading",
|
|
31
|
+
saving: status === "saving",
|
|
32
|
+
set,
|
|
33
|
+
patch,
|
|
34
|
+
save,
|
|
35
|
+
clear,
|
|
36
|
+
};
|
|
37
|
+
}
|
package/dist/selectSnapshot.d.ts
CHANGED
|
@@ -3,5 +3,7 @@ export type SelectCache<TSnapshot, TSelected> = {
|
|
|
3
3
|
snapshot: TSnapshot;
|
|
4
4
|
value: TSelected;
|
|
5
5
|
};
|
|
6
|
+
/** @internal */
|
|
6
7
|
export declare function createSelectCache<TSnapshot, TSelected>(): SelectCache<TSnapshot, TSelected>;
|
|
8
|
+
/** @internal */
|
|
7
9
|
export declare function readSelectSnapshot<TSnapshot, TSelected>(cache: SelectCache<TSnapshot, TSelected>, snapshot: TSnapshot, select: (snapshot: TSnapshot) => TSelected, isEqual?: (previous: TSelected, next: TSelected) => boolean): TSelected;
|