@jgengine/react 0.22.0 → 0.23.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 +96 -0
- package/dist/accessibility.d.ts +5 -0
- package/dist/accessibility.js +9 -0
- package/dist/achievements.js +4 -3
- package/dist/actionHud.d.ts +6 -1
- package/dist/actionHud.js +37 -17
- package/dist/bars.js +5 -2
- package/dist/barsPreview.js +2 -1
- package/dist/characterSheet.js +5 -4
- package/dist/coachMarks.js +3 -2
- package/dist/codex.js +4 -3
- package/dist/components.js +9 -2
- package/dist/creditsScreen.js +2 -1
- package/dist/engineStyles.d.ts +18 -0
- package/dist/engineStyles.js +119 -0
- package/dist/fastTravel.js +4 -3
- package/dist/hud.js +7 -6
- package/dist/hudButton.d.ts +29 -0
- package/dist/hudButton.js +31 -0
- package/dist/hudChromePreview.d.ts +8 -0
- package/dist/hudChromePreview.js +27 -0
- package/dist/hudFrame.js +15 -11
- package/dist/hudInteract.d.ts +26 -0
- package/dist/hudInteract.js +28 -0
- package/dist/hudLayout.js +4 -3
- package/dist/hudPanel.d.ts +31 -0
- package/dist/hudPanel.js +32 -0
- package/dist/hudTheme.d.ts +88 -1
- package/dist/hudTheme.js +123 -8
- package/dist/hudThemePreview.js +2 -1
- package/dist/hudThemeRoot.d.ts +15 -0
- package/dist/hudThemeRoot.js +20 -0
- package/dist/hudTokens.d.ts +39 -0
- package/dist/hudTokens.js +56 -0
- package/dist/iconTreatment.js +3 -2
- package/dist/iconsPreview.js +2 -1
- package/dist/keybindingMenu.js +3 -2
- package/dist/map.js +9 -8
- package/dist/modals.d.ts +6 -3
- package/dist/modals.js +42 -52
- package/dist/notices.d.ts +27 -0
- package/dist/notices.js +145 -0
- package/dist/notifications.js +4 -3
- package/dist/panels.d.ts +9 -2
- package/dist/panels.js +78 -35
- package/dist/photoMode.js +2 -1
- package/dist/presence.d.ts +9 -0
- package/dist/presence.js +25 -0
- package/dist/previewFixtures.js +6 -0
- package/dist/questTracker.js +3 -2
- package/dist/quickMenu.js +6 -5
- package/dist/saveSlots.js +4 -3
- package/dist/selectionHud.js +4 -3
- package/dist/shopGrid.js +2 -1
- package/dist/statusEffectBar.js +2 -1
- package/dist/streetGeometryPreview.js +3 -2
- package/dist/styles.css +72 -0
- package/dist/talentTree.js +3 -2
- package/dist/tweenNumber.d.ts +37 -0
- package/dist/tweenNumber.js +66 -0
- package/dist/uiCues.d.ts +20 -0
- package/dist/uiCues.js +37 -0
- package/dist/waveHud.js +2 -1
- package/dist/waypointMarkers.js +2 -1
- package/package.json +10 -6
- package/skills/jgengine/api.md +18 -18
- package/skills/jgengine-ui/SKILL.md +4 -0
- package/skills/jgengine-ui/api.md +233 -37
- package/skills/jgengine-ui/capabilities.md +52 -0
- package/skills/jgengine-ui/reference-primitives.md +4 -4
package/dist/modals.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useReducer, } from "react";
|
|
2
|
+
import { useCallback, useEffect, useReducer, useRef, } from "react";
|
|
3
3
|
import { MODAL_CANCEL, MODAL_CONFIRM, } from "@jgengine/core/ui/modalStack";
|
|
4
|
-
import {
|
|
4
|
+
import { useReducedMotion } from "./accessibility.js";
|
|
5
5
|
import { useDialogBehavior } from "./dialogBehavior.js";
|
|
6
|
+
import { useEngineStyles } from "./engineStyles.js";
|
|
7
|
+
import { HudButton } from "./hudButton.js";
|
|
8
|
+
import { useHudInteract } from "./hudInteract.js";
|
|
9
|
+
import { HudPanel } from "./hudPanel.js";
|
|
10
|
+
import { hudType } from "./hudTokens.js";
|
|
11
|
+
import { usePresence } from "./presence.js";
|
|
6
12
|
/**
|
|
7
13
|
* Subscribe to a modal stack and re-render on every change, returning the active modal and depth.
|
|
8
14
|
*
|
|
@@ -13,6 +19,7 @@ export function useModalStack(stack) {
|
|
|
13
19
|
useEffect(() => stack.subscribe(bump), [stack]);
|
|
14
20
|
return { top: stack.top(), depth: stack.depth() };
|
|
15
21
|
}
|
|
22
|
+
const MODAL_EXIT_MS = 140;
|
|
16
23
|
/**
|
|
17
24
|
* Full-screen modal host: watches a stack, renders only the topmost modal centered over a dimmed
|
|
18
25
|
* backdrop, and manages accessibility — the panel is a `role="dialog" aria-modal`, focus moves into
|
|
@@ -22,8 +29,27 @@ export function useModalStack(stack) {
|
|
|
22
29
|
*
|
|
23
30
|
* @capability modal-host full-screen modal/dialog host over a modal stack — dimmed backdrop, focus trap, Esc/backdrop-to-cancel, aria-modal dialog
|
|
24
31
|
*/
|
|
25
|
-
export function ModalHost({ stack, children, showBackdrop = true, backdrop, closeOnBackdrop = true, closeOnEsc = true, zIndexBase = 80, className, style, }) {
|
|
26
|
-
|
|
32
|
+
export function ModalHost({ stack, children, showBackdrop = true, backdrop, closeOnBackdrop = true, closeOnEsc = true, zIndexBase = 80, onInteract, className, style, }) {
|
|
33
|
+
useEngineStyles();
|
|
34
|
+
const { top: live } = useModalStack(stack);
|
|
35
|
+
const last = useRef(live);
|
|
36
|
+
if (live !== null)
|
|
37
|
+
last.current = live;
|
|
38
|
+
const presence = usePresence(live !== null, useReducedMotion() ? 0 : MODAL_EXIT_MS);
|
|
39
|
+
const top = live ?? (presence === "closed" ? last.current : null);
|
|
40
|
+
const report = useHudInteract("modal", top?.kind, onInteract);
|
|
41
|
+
const reportedId = useRef(null);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const id = live?.id ?? null;
|
|
44
|
+
if (id === reportedId.current)
|
|
45
|
+
return;
|
|
46
|
+
const wasOpen = reportedId.current !== null;
|
|
47
|
+
reportedId.current = id;
|
|
48
|
+
if (id !== null)
|
|
49
|
+
report("open");
|
|
50
|
+
else if (wasOpen)
|
|
51
|
+
report("close");
|
|
52
|
+
}, [live, report]);
|
|
27
53
|
const controls = {
|
|
28
54
|
resolve: useCallback((result, payload) => stack.resolve(result, payload !== undefined ? { payload } : undefined), [stack]),
|
|
29
55
|
confirm: useCallback((payload) => stack.resolve(MODAL_CONFIRM, payload !== undefined ? { payload } : undefined), [stack]),
|
|
@@ -37,7 +63,7 @@ export function ModalHost({ stack, children, showBackdrop = true, backdrop, clos
|
|
|
37
63
|
if (top === null)
|
|
38
64
|
return null;
|
|
39
65
|
const dim = backdrop ?? "var(--jg-backdrop, rgba(4,7,12,0.62))";
|
|
40
|
-
return (_jsxs("div", { "data-modal-host": "", className: className, style: {
|
|
66
|
+
return (_jsxs("div", { "data-modal-host": "", "data-state": live === null ? "closed" : "open", className: className, style: {
|
|
41
67
|
position: "fixed",
|
|
42
68
|
inset: 0,
|
|
43
69
|
zIndex: zIndexBase,
|
|
@@ -46,37 +72,14 @@ export function ModalHost({ stack, children, showBackdrop = true, backdrop, clos
|
|
|
46
72
|
justifyContent: "center",
|
|
47
73
|
pointerEvents: "auto",
|
|
48
74
|
...style,
|
|
49
|
-
}, children: [showBackdrop ? (_jsx("div", { "data-modal-backdrop": "", onClick: closeOnBackdrop ? () => controls.cancel() : undefined, style: { position: "fixed", inset: 0, background: dim } })) : null, _jsx("div", { ref: panelRef, role: "dialog", "aria-modal": true, "aria-label": typeof top.payload === "object" && top.payload !== null && "title" in top.payload ? String(top.payload.title ?? top.kind) : top.kind, "data-modal": top.kind, "data-modal-id": top.id, tabIndex: -1, style: { position: "relative", pointerEvents: "auto", outline: "none", maxWidth: "calc(100vw - 32px)", maxHeight: "calc(100vh - 32px)" }, children: children(top, controls) })] }));
|
|
75
|
+
}, children: [showBackdrop ? (_jsx("div", { "data-modal-backdrop": "", onClick: closeOnBackdrop ? () => controls.cancel() : undefined, style: { position: "fixed", inset: 0, background: dim, pointerEvents: live === null ? "none" : undefined } })) : null, _jsx("div", { ref: panelRef, role: "dialog", "aria-modal": true, "aria-label": typeof top.payload === "object" && top.payload !== null && "title" in top.payload ? String(top.payload.title ?? top.kind) : top.kind, "data-modal": top.kind, "data-modal-id": top.id, tabIndex: -1, style: { position: "relative", pointerEvents: live === null ? "none" : "auto", outline: "none", maxWidth: "calc(100vw - 32px)", maxHeight: "calc(100vh - 32px)" }, children: children(top, controls) })] }));
|
|
50
76
|
}
|
|
51
|
-
const DIALOG_TITLE = { margin: 0, fontSize:
|
|
52
|
-
const DIALOG_BODY = { margin: "10px 0 0", fontSize:
|
|
77
|
+
const DIALOG_TITLE = { margin: 0, fontFamily: "var(--jg-font-display, inherit)", fontSize: hudType("lg"), fontWeight: 700, letterSpacing: 0.1, lineHeight: 1.3 };
|
|
78
|
+
const DIALOG_BODY = { margin: "10px 0 0", fontSize: hudType("md"), lineHeight: 1.55, color: "var(--jg-text-dim, rgba(226,232,240,0.82))" };
|
|
53
79
|
const BUTTON_ROW = { display: "flex", gap: 10, marginTop: 20, justifyContent: "flex-end" };
|
|
54
|
-
const BUTTON_BASE = {
|
|
55
|
-
appearance: "none",
|
|
56
|
-
borderRadius: "var(--jg-slot-radius, 8px)",
|
|
57
|
-
border: "1px solid transparent",
|
|
58
|
-
fontSize: 13,
|
|
59
|
-
fontWeight: 600,
|
|
60
|
-
padding: "8px 16px",
|
|
61
|
-
cursor: "pointer",
|
|
62
|
-
font: "inherit",
|
|
63
|
-
};
|
|
64
|
-
function primaryButtonStyle(danger) {
|
|
65
|
-
return {
|
|
66
|
-
...BUTTON_BASE,
|
|
67
|
-
background: danger ? "#e5484d" : "var(--jg-accent, #38bdf8)",
|
|
68
|
-
color: danger ? "#fff" : "#06121b",
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
const GHOST_BUTTON = {
|
|
72
|
-
...BUTTON_BASE,
|
|
73
|
-
background: "var(--jg-slot-bg, rgba(148,163,184,0.12))",
|
|
74
|
-
color: "var(--jg-bar-text, #e2e8f0)",
|
|
75
|
-
border: "var(--jg-slot-border, 1px solid rgba(148,163,184,0.28))",
|
|
76
|
-
};
|
|
77
80
|
/**
|
|
78
81
|
* A generic two-button confirmation dialog — title, optional body, and Cancel / Confirm buttons over
|
|
79
|
-
* a `HudTheme`-token-driven {@link
|
|
82
|
+
* a `HudTheme`-token-driven {@link HudPanel}. Presentation-only: wire `onConfirm`/`onCancel` to a
|
|
80
83
|
* modal stack's `resolve` (typically inside a {@link ModalHost} render callback) and reskin via
|
|
81
84
|
* `HudTheme` tokens or `danger` for a destructive action. It interprets nothing — the game supplies
|
|
82
85
|
* the copy.
|
|
@@ -84,28 +87,22 @@ const GHOST_BUTTON = {
|
|
|
84
87
|
* @capability confirm-dialog generic themeable confirm/cancel dialog — title, body, two buttons, danger styling
|
|
85
88
|
*/
|
|
86
89
|
export function ConfirmDialog({ title, body, confirmLabel = "Confirm", cancelLabel = "Cancel", onConfirm, onCancel, danger = false, width = 360, className, style, }) {
|
|
87
|
-
return (_jsxs(
|
|
90
|
+
return (_jsxs(HudPanel, { elevation: "modal", width: width, padding: 20, className: className, style: style, children: [_jsx("h2", { "data-confirm-title": "", style: DIALOG_TITLE, children: title }), body !== undefined ? _jsx("p", { "data-confirm-body": "", style: DIALOG_BODY, children: body }) : null, _jsxs("div", { style: BUTTON_ROW, children: [_jsx(HudButton, { "data-confirm-cancel": "", cue: "cancel", onClick: onCancel, children: cancelLabel }), _jsx(HudButton, { "data-confirm-accept": "", cue: "confirm", variant: danger ? "danger" : "primary", onClick: onConfirm, children: confirmLabel })] })] }));
|
|
88
91
|
}
|
|
89
92
|
const PAUSE_TITLE = {
|
|
90
93
|
margin: "0 0 14px",
|
|
91
|
-
fontSize:
|
|
94
|
+
fontSize: hudType("xs"),
|
|
92
95
|
fontWeight: 700,
|
|
93
|
-
letterSpacing:
|
|
96
|
+
letterSpacing: "0.18em",
|
|
94
97
|
textTransform: "uppercase",
|
|
95
98
|
textAlign: "center",
|
|
96
99
|
color: "var(--jg-accent, #38bdf8)",
|
|
97
100
|
};
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
display: "block",
|
|
101
|
-
width: "100%",
|
|
102
|
-
textAlign: "center",
|
|
103
|
-
padding: "10px 16px",
|
|
104
|
-
marginTop: 8,
|
|
105
|
-
};
|
|
101
|
+
const PAUSE_ITEM = { marginTop: 8 };
|
|
102
|
+
const PAUSE_DANGER_ITEM = { ...PAUSE_ITEM, color: "var(--jg-danger, #e5484d)", borderColor: "rgba(229,72,77,0.45)" };
|
|
106
103
|
/**
|
|
107
104
|
* A reskinnable pause-menu building block: a primary Resume button plus a game-filled list of slot
|
|
108
|
-
* items (Settings, Save, Quit, …), stacked in a `HudTheme`-token-driven {@link
|
|
105
|
+
* items (Settings, Save, Quit, …), stacked in a `HudTheme`-token-driven {@link HudPanel}. It ships
|
|
109
106
|
* the common shape every game's pause screen needs and nothing genre-specific — the game supplies the
|
|
110
107
|
* title, item labels/order, and destructive styling, and reskins with `HudTheme` tokens. Drop it into
|
|
111
108
|
* a {@link ModalHost} keyed on a `"pause"` modal, or render it standalone.
|
|
@@ -113,12 +110,5 @@ const PAUSE_ITEM_BASE = {
|
|
|
113
110
|
* @capability pause-menu reskinnable pause-menu building block — Resume plus a game-filled Settings/Quit slot list, HudTheme-driven
|
|
114
111
|
*/
|
|
115
112
|
export function PauseMenu({ title = "Paused", onResume, resumeLabel = "Resume", items = [], width = 300, className, style, }) {
|
|
116
|
-
return (_jsxs(
|
|
117
|
-
...PAUSE_ITEM_BASE,
|
|
118
|
-
...(item.danger
|
|
119
|
-
? { background: "transparent", color: "#f0787c", border: "1px solid rgba(229,72,77,0.5)" }
|
|
120
|
-
: GHOST_BUTTON),
|
|
121
|
-
opacity: item.disabled === true ? 0.5 : 1,
|
|
122
|
-
cursor: item.disabled === true ? "not-allowed" : "pointer",
|
|
123
|
-
}, children: item.label }, item.id)))] }));
|
|
113
|
+
return (_jsxs(HudPanel, { elevation: "modal", width: width, padding: 20, className: className, style: style, children: [_jsx("div", { "data-pause-title": "", style: PAUSE_TITLE, children: title }), _jsx(HudButton, { "data-pause-resume": "", cue: "resume", variant: "primary", size: "lg", block: true, onClick: onResume, children: resumeLabel }), items.map((item) => (_jsx(HudButton, { "data-pause-item": item.id, cue: `pause:${item.id}`, size: "lg", block: true, onClick: item.onSelect, disabled: item.disabled, style: item.danger ? PAUSE_DANGER_ITEM : PAUSE_ITEM, children: item.label }, item.id)))] }));
|
|
124
114
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { Notice, NoticeFeed } from "@jgengine/core/game/notices";
|
|
3
|
+
export type { Notice, NoticeTone } from "@jgengine/core/game/notices";
|
|
4
|
+
/** Where {@link NoticeStack} sits: below top-edge HUD bars, over the play area, or above bottom action bars. */
|
|
5
|
+
export type NoticePlacement = "top" | "center" | "bottom";
|
|
6
|
+
type Shown = {
|
|
7
|
+
notice: Notice;
|
|
8
|
+
leaving: boolean;
|
|
9
|
+
};
|
|
10
|
+
/** Keeps removed notices on screen, in place, for the exit animation; new ones join the end. @internal */
|
|
11
|
+
export declare function reconcileShown(previous: readonly Shown[], live: readonly Notice[]): Shown[];
|
|
12
|
+
/** Live notices of `feed`, re-rendering on change and pruning each one when it expires. */
|
|
13
|
+
export declare function useNoticeList(feed: NoticeFeed | null): readonly Notice[];
|
|
14
|
+
/**
|
|
15
|
+
* Animated, toned notice stack for a {@link NoticeFeed} — the default renderer the shell mounts for
|
|
16
|
+
* `ctx.game.notices`. Uses `--jg-*` theme vars with fallbacks, 14px text, and never takes pointer input.
|
|
17
|
+
*
|
|
18
|
+
* @capability notice-feed render ctx.game.notices as an animated toned stack (enter/exit motion, ×N stacking)
|
|
19
|
+
*/
|
|
20
|
+
export declare function NoticeStack({ feed, placement, scale, renderNotice, }: {
|
|
21
|
+
/** Defaults to the surrounding game's `ctx.game.notices`. */
|
|
22
|
+
feed?: NoticeFeed;
|
|
23
|
+
placement?: NoticePlacement;
|
|
24
|
+
/** Multiplies text and spacing (the shell passes the player's UI scale). */
|
|
25
|
+
scale?: number;
|
|
26
|
+
renderNotice?: (notice: Notice) => ReactNode;
|
|
27
|
+
}): import("react").JSX.Element | null;
|
package/dist/notices.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState, useSyncExternalStore } from "react";
|
|
3
|
+
import { useOptionalGameContext } from "./provider.js";
|
|
4
|
+
const EXIT_MS = 200;
|
|
5
|
+
const EMPTY = [];
|
|
6
|
+
const NO_FEED_SUBSCRIBE = () => () => undefined;
|
|
7
|
+
const TONE_COLOR = {
|
|
8
|
+
info: "var(--jg-accent, #7cc4ff)",
|
|
9
|
+
success: "var(--jg-success, #3ecf8e)",
|
|
10
|
+
warn: "var(--jg-warning, #f5b544)",
|
|
11
|
+
deny: "var(--jg-danger, #f26d6d)",
|
|
12
|
+
};
|
|
13
|
+
const TONE_LABEL = { info: "Info", success: "Done", warn: "Warning", deny: "Can't do that" };
|
|
14
|
+
const PLACEMENT_STYLE = {
|
|
15
|
+
top: { top: "clamp(64px, 14%, 150px)" },
|
|
16
|
+
center: { top: "34%" },
|
|
17
|
+
bottom: { bottom: "clamp(120px, 22%, 230px)", flexDirection: "column-reverse" },
|
|
18
|
+
};
|
|
19
|
+
const KEYFRAMES = `
|
|
20
|
+
@keyframes jg-notice-in { from { opacity: 0; transform: translateY(-8px) scale(0.97); } to { opacity: 1; transform: none; } }
|
|
21
|
+
@keyframes jg-notice-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(-6px) scale(0.98); } }
|
|
22
|
+
@keyframes jg-notice-shake { 0%,100% { transform: none; } 20% { transform: translateX(-5px); } 45% { transform: translateX(4px); } 70% { transform: translateX(-2px); } }
|
|
23
|
+
@keyframes jg-notice-pop { 0% { transform: scale(1); } 40% { transform: scale(1.035); } 100% { transform: scale(1); } }
|
|
24
|
+
[data-jg-notice] { animation: jg-notice-in 180ms cubic-bezier(.2,.8,.2,1) both; }
|
|
25
|
+
[data-jg-notice][data-leaving] { animation: jg-notice-out ${EXIT_MS}ms ease-in both; }
|
|
26
|
+
[data-jg-notice-body][data-bump="deny"] { animation: jg-notice-shake 260ms ease-out; }
|
|
27
|
+
[data-jg-notice-body][data-bump="other"] { animation: jg-notice-pop 220ms ease-out; }
|
|
28
|
+
@media (prefers-reduced-motion: reduce) { [data-jg-notice], [data-jg-notice][data-leaving], [data-jg-notice-body][data-bump] { animation-duration: 1ms; } }
|
|
29
|
+
`;
|
|
30
|
+
function ToneGlyph({ tone }) {
|
|
31
|
+
const path = tone === "deny"
|
|
32
|
+
? "M5 5l6 6M11 5l-6 6"
|
|
33
|
+
: tone === "success"
|
|
34
|
+
? "M4.5 8.5l2.5 2.5 4.5-5"
|
|
35
|
+
: tone === "warn"
|
|
36
|
+
? "M8 4.5v4.5M8 11.4v.1"
|
|
37
|
+
: "M8 7.2v4.3M8 4.6v.1";
|
|
38
|
+
return (_jsx("span", { "aria-hidden": true, style: {
|
|
39
|
+
flex: "none",
|
|
40
|
+
width: 20,
|
|
41
|
+
height: 20,
|
|
42
|
+
borderRadius: 999,
|
|
43
|
+
display: "grid",
|
|
44
|
+
placeItems: "center",
|
|
45
|
+
background: TONE_COLOR[tone],
|
|
46
|
+
color: "var(--jg-surface-deep, #0b1018)",
|
|
47
|
+
}, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: path }) }) }));
|
|
48
|
+
}
|
|
49
|
+
/** Keeps removed notices on screen, in place, for the exit animation; new ones join the end. @internal */
|
|
50
|
+
export function reconcileShown(previous, live) {
|
|
51
|
+
const byId = new Map(live.map((notice) => [notice.id, notice]));
|
|
52
|
+
const next = previous.map((entry) => {
|
|
53
|
+
const current = byId.get(entry.notice.id);
|
|
54
|
+
return current === undefined ? { notice: entry.notice, leaving: true } : { notice: current, leaving: false };
|
|
55
|
+
});
|
|
56
|
+
const known = new Set(previous.map((entry) => entry.notice.id));
|
|
57
|
+
for (const notice of live)
|
|
58
|
+
if (!known.has(notice.id))
|
|
59
|
+
next.push({ notice, leaving: false });
|
|
60
|
+
return next;
|
|
61
|
+
}
|
|
62
|
+
/** Live notices of `feed`, re-rendering on change and pruning each one when it expires. */
|
|
63
|
+
export function useNoticeList(feed) {
|
|
64
|
+
const list = useSyncExternalStore(feed?.subscribe ?? NO_FEED_SUBSCRIBE, feed === null ? () => EMPTY : feed.list, feed === null ? () => EMPTY : feed.list);
|
|
65
|
+
const [, setRetry] = useState(0);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (feed === null || list.length === 0)
|
|
68
|
+
return;
|
|
69
|
+
const soonest = Math.min(...list.map((notice) => notice.expiresAt));
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
feed.prune();
|
|
72
|
+
// A feed on a non-wall clock may not have expired yet; poll gently until it does.
|
|
73
|
+
if (feed.list() === list)
|
|
74
|
+
setRetry((value) => value + 1);
|
|
75
|
+
}, Math.max(16, soonest - Date.now()));
|
|
76
|
+
return () => clearTimeout(timer);
|
|
77
|
+
});
|
|
78
|
+
return list;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Animated, toned notice stack for a {@link NoticeFeed} — the default renderer the shell mounts for
|
|
82
|
+
* `ctx.game.notices`. Uses `--jg-*` theme vars with fallbacks, 14px text, and never takes pointer input.
|
|
83
|
+
*
|
|
84
|
+
* @capability notice-feed render ctx.game.notices as an animated toned stack (enter/exit motion, ×N stacking)
|
|
85
|
+
*/
|
|
86
|
+
export function NoticeStack({ feed, placement = "top", scale = 1, renderNotice, }) {
|
|
87
|
+
const ctx = useOptionalGameContext();
|
|
88
|
+
const source = feed ?? ctx?.game.notices ?? null;
|
|
89
|
+
const live = useNoticeList(source);
|
|
90
|
+
const [shown, setShown] = useState(() => reconcileShown([], live));
|
|
91
|
+
const [seenLive, setSeenLive] = useState(live);
|
|
92
|
+
if (seenLive !== live) {
|
|
93
|
+
setSeenLive(live);
|
|
94
|
+
setShown(reconcileShown(shown, live));
|
|
95
|
+
}
|
|
96
|
+
const anyLeaving = shown.some((entry) => entry.leaving);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (!anyLeaving)
|
|
99
|
+
return;
|
|
100
|
+
const timer = setTimeout(() => setShown((previous) => previous.filter((entry) => !entry.leaving)), EXIT_MS);
|
|
101
|
+
return () => clearTimeout(timer);
|
|
102
|
+
}, [anyLeaving]);
|
|
103
|
+
if (shown.length === 0)
|
|
104
|
+
return null;
|
|
105
|
+
return (_jsxs("div", { "data-jg-notices": placement, role: "status", "aria-live": "polite", style: {
|
|
106
|
+
position: "absolute",
|
|
107
|
+
left: "50%",
|
|
108
|
+
transform: "translateX(-50%)",
|
|
109
|
+
zIndex: 45,
|
|
110
|
+
display: "flex",
|
|
111
|
+
flexDirection: "column",
|
|
112
|
+
alignItems: "center",
|
|
113
|
+
gap: 6 * scale,
|
|
114
|
+
width: "min(460px, calc(100% - 32px))",
|
|
115
|
+
pointerEvents: "none",
|
|
116
|
+
...PLACEMENT_STYLE[placement],
|
|
117
|
+
}, children: [_jsx("style", { children: KEYFRAMES }), shown.map(({ notice, leaving }) => (_jsx("div", { "data-jg-notice": notice.tone, "data-leaving": leaving ? "" : undefined, style: { maxWidth: "100%" }, children: renderNotice !== undefined ? renderNotice(notice) : (_jsxs("div", { "data-jg-notice-body": true, "data-bump": notice.count > 1 ? (notice.tone === "deny" ? "deny" : "other") : undefined, style: {
|
|
118
|
+
display: "flex",
|
|
119
|
+
alignItems: "center",
|
|
120
|
+
gap: 10 * scale,
|
|
121
|
+
padding: `${8 * scale}px ${14 * scale}px ${8 * scale}px ${10 * scale}px`,
|
|
122
|
+
borderRadius: 10 * scale,
|
|
123
|
+
background: "var(--jg-surface, rgba(13, 18, 27, 0.9))",
|
|
124
|
+
border: "1px solid var(--jg-edge, rgba(255, 255, 255, 0.12))",
|
|
125
|
+
boxShadow: `inset 3px 0 0 ${TONE_COLOR[notice.tone]}, 0 8px 24px rgba(0, 0, 0, 0.35)`,
|
|
126
|
+
backdropFilter: "blur(8px)",
|
|
127
|
+
WebkitBackdropFilter: "blur(8px)",
|
|
128
|
+
color: "var(--jg-text, #f1f5f9)",
|
|
129
|
+
fontFamily: "var(--jg-font-body, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif)",
|
|
130
|
+
fontSize: 14 * scale,
|
|
131
|
+
fontWeight: 600,
|
|
132
|
+
lineHeight: 1.3,
|
|
133
|
+
letterSpacing: "0.01em",
|
|
134
|
+
textShadow: "0 1px 1px rgba(0, 0, 0, 0.35)",
|
|
135
|
+
}, children: [_jsx(ToneGlyph, { tone: notice.tone }), _jsxs("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" }, children: [TONE_LABEL[notice.tone], ": "] }), _jsx("span", { children: notice.text }), notice.count > 1 ? (_jsxs("span", { style: {
|
|
136
|
+
flex: "none",
|
|
137
|
+
fontSize: 12 * scale,
|
|
138
|
+
fontWeight: 700,
|
|
139
|
+
fontVariantNumeric: "tabular-nums",
|
|
140
|
+
padding: `${1 * scale}px ${6 * scale}px`,
|
|
141
|
+
borderRadius: 999,
|
|
142
|
+
background: "rgba(255, 255, 255, 0.12)",
|
|
143
|
+
color: "var(--jg-text-dim, rgba(241, 245, 249, 0.8))",
|
|
144
|
+
}, children: ["\u00D7", notice.count] })) : null] }, notice.count)) }, notice.id)))] }));
|
|
145
|
+
}
|
package/dist/notifications.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useSyncExternalStore } from "react";
|
|
3
|
+
import { hudType } from "./hudTokens.js";
|
|
3
4
|
/** Subscribe to a notification store's live list. */
|
|
4
5
|
export function useNotifications(store) {
|
|
5
6
|
return useSyncExternalStore(store.subscribe, store.list, store.list);
|
|
@@ -55,7 +56,7 @@ export function NotificationBell({ store, onClick, icon = "🔔", className, sty
|
|
|
55
56
|
borderRadius: 9999,
|
|
56
57
|
background: "#ef4444",
|
|
57
58
|
color: "#fff",
|
|
58
|
-
fontSize:
|
|
59
|
+
fontSize: hudType("xs"),
|
|
59
60
|
fontWeight: 700,
|
|
60
61
|
display: "flex",
|
|
61
62
|
alignItems: "center",
|
|
@@ -80,7 +81,7 @@ export function NotificationCenter({ store, title = "Notifications", emptyLabel
|
|
|
80
81
|
borderRadius: 6,
|
|
81
82
|
background: "transparent",
|
|
82
83
|
color: "rgba(226,232,240,0.85)",
|
|
83
|
-
fontSize:
|
|
84
|
+
fontSize: hudType("xs"),
|
|
84
85
|
padding: "3px 8px",
|
|
85
86
|
cursor: "pointer",
|
|
86
87
|
};
|
|
@@ -107,5 +108,5 @@ export function NotificationCenter({ store, title = "Notifications", emptyLabel
|
|
|
107
108
|
color: "#e2e8f0",
|
|
108
109
|
textAlign: "left",
|
|
109
110
|
cursor: "pointer",
|
|
110
|
-
}, children: [_jsx("span", { style: { marginTop: 5, width: 8, height: 8, flexShrink: 0, borderRadius: 9999, background: kindColor(entry.kind) } }), _jsxs("span", { style: { display: "flex", flexDirection: "column", gap: 2, minWidth: 0, flex: 1 }, children: [_jsxs("span", { style: { display: "flex", justifyContent: "space-between", gap: 8 }, children: [_jsx("span", { style: { fontSize: 13, fontWeight: entry.read ? 500 : 700 }, children: entry.title }), _jsx("span", { style: { fontSize:
|
|
111
|
+
}, children: [_jsx("span", { style: { marginTop: 5, width: 8, height: 8, flexShrink: 0, borderRadius: 9999, background: kindColor(entry.kind) } }), _jsxs("span", { style: { display: "flex", flexDirection: "column", gap: 2, minWidth: 0, flex: 1 }, children: [_jsxs("span", { style: { display: "flex", justifyContent: "space-between", gap: 8 }, children: [_jsx("span", { style: { fontSize: 13, fontWeight: entry.read ? 500 : 700 }, children: entry.title }), _jsx("span", { style: { fontSize: hudType("xs"), color: "rgba(148,163,184,0.7)", whiteSpace: "nowrap" }, children: time(entry.at) })] }), entry.body !== undefined ? (_jsx("span", { style: { fontSize: hudType("xs"), color: "rgba(203,213,225,0.75)" }, children: entry.body })) : null] })] }) }, entry.id))) }))] }));
|
|
111
112
|
}
|
package/dist/panels.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type CSSProperties, type ReactNode } from "react";
|
|
2
2
|
import { type PanelDef, type PanelPosition, type PanelState } from "@jgengine/core/ui/panelModel";
|
|
3
3
|
import { type HudFrameShape, type HudFrameVariation } from "./hudFrame.js";
|
|
4
|
+
import { type HudInteractListener } from "./hudInteract.js";
|
|
4
5
|
/**
|
|
5
6
|
* React chrome over the headless panel/window model (`@jgengine/core/ui/panelModel`) — the toggleable,
|
|
6
7
|
* draggable, z-stacked windows a WoW-style UI is made of (B for bag, C for character, ESC to close).
|
|
@@ -69,8 +70,10 @@ export declare function usePanels(defs: readonly PanelDef[], options?: UsePanels
|
|
|
69
70
|
*
|
|
70
71
|
* @capability panel-host render a manager's open windows as draggable, closable, z-stacked dialogs above the HUD
|
|
71
72
|
*/
|
|
72
|
-
export declare function PanelHost({ manager, render, panels, variation, shape, width, zIndexBase, className, windowClassName, windowStyle, bodyStyle, }: {
|
|
73
|
+
export declare function PanelHost({ manager, render, panels, variation, shape, width, zIndexBase, className, windowClassName, windowStyle, bodyStyle, onInteract, }: {
|
|
73
74
|
manager: PanelsManager;
|
|
75
|
+
/** Called as windows open and close (`cue` is `panel:<id>`) — the seam a UI sound system hooks. */
|
|
76
|
+
onInteract?: HudInteractListener;
|
|
74
77
|
/** Content for a panel by id. Takes precedence over `panels`. */
|
|
75
78
|
render?: (id: string) => ReactNode;
|
|
76
79
|
/** Static content map by panel id, used when `render` is omitted. */
|
|
@@ -94,10 +97,14 @@ export declare function PanelHost({ manager, render, panels, variation, shape, w
|
|
|
94
97
|
*
|
|
95
98
|
* @capability window standalone draggable, closable window primitive over HudFrame — no manager required
|
|
96
99
|
*/
|
|
97
|
-
export declare function Window({ title, ariaLabel, closable, onClose, x, y, onMove, z, width, variation, shape, className, style, bodyStyle, children, }: {
|
|
100
|
+
export declare function Window({ title, ariaLabel, closable, onClose, x, y, onMove, z, width, variation, shape, className, style, bodyStyle, cue, onInteract, children, }: {
|
|
98
101
|
title: ReactNode;
|
|
99
102
|
ariaLabel?: string;
|
|
100
103
|
closable?: boolean;
|
|
104
|
+
/** Feedback cue name, rendered as `data-jg-cue` and passed to `onInteract`. */
|
|
105
|
+
cue?: string;
|
|
106
|
+
/** Called with `open` when the window mounts — the seam a UI sound system hooks. */
|
|
107
|
+
onInteract?: HudInteractListener;
|
|
101
108
|
onClose?: () => void;
|
|
102
109
|
/** Initial (uncontrolled) or current (controlled, with `onMove`) x position. Default 56. */
|
|
103
110
|
x?: number;
|
package/dist/panels.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
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 { useCallback, useEffect, useRef, useState, } from "react";
|
|
3
3
|
import { closePanel, closeTopPanel, createPanelState, focusPanel, isOpen as isPanelOpen, movePanel, openPanel, orderedOpen, panelByHotkey, } from "@jgengine/core/ui/panelModel";
|
|
4
|
+
import { useReducedMotion } from "./accessibility.js";
|
|
5
|
+
import { HudButton } from "./hudButton.js";
|
|
4
6
|
import { HudFrame } from "./hudFrame.js";
|
|
7
|
+
import { useHudInteract } from "./hudInteract.js";
|
|
8
|
+
import { HudPanel } from "./hudPanel.js";
|
|
9
|
+
import { hudType } from "./hudTokens.js";
|
|
5
10
|
import { observeBrowserSuspension } from "./browserLifecycle.js";
|
|
11
|
+
import { usePresence } from "./presence.js";
|
|
6
12
|
/**
|
|
7
13
|
* Pure keybind resolver for a panel set: ESC resolves to `closeTop` when a closable window is open, and
|
|
8
14
|
* any other key routes through {@link panelByHotkey} (trying `code` then `key`) to a `toggle`. Returns
|
|
@@ -168,43 +174,78 @@ const TITLEBAR_STYLE = {
|
|
|
168
174
|
alignItems: "center",
|
|
169
175
|
justifyContent: "space-between",
|
|
170
176
|
gap: 8,
|
|
171
|
-
padding: "
|
|
177
|
+
padding: "8px 8px 8px 12px",
|
|
172
178
|
cursor: "grab",
|
|
173
179
|
touchAction: "none",
|
|
174
180
|
userSelect: "none",
|
|
175
|
-
fontSize:
|
|
176
|
-
fontWeight:
|
|
177
|
-
letterSpacing: 0.
|
|
178
|
-
borderBottom: "var(--jg-
|
|
179
|
-
};
|
|
180
|
-
const CLOSE_BUTTON_STYLE = {
|
|
181
|
-
display: "inline-flex",
|
|
182
|
-
alignItems: "center",
|
|
183
|
-
justifyContent: "center",
|
|
184
|
-
width: 20,
|
|
185
|
-
height: 20,
|
|
186
|
-
padding: 0,
|
|
187
|
-
borderRadius: 6,
|
|
188
|
-
border: "1px solid transparent",
|
|
189
|
-
background: "transparent",
|
|
190
|
-
color: "inherit",
|
|
191
|
-
font: "inherit",
|
|
192
|
-
lineHeight: 1,
|
|
193
|
-
cursor: "pointer",
|
|
194
|
-
opacity: 0.75,
|
|
181
|
+
fontSize: hudType("sm"),
|
|
182
|
+
fontWeight: 650,
|
|
183
|
+
letterSpacing: 0.2,
|
|
184
|
+
borderBottom: "1px solid var(--jg-edge, rgba(255,255,255,0.10))",
|
|
195
185
|
};
|
|
186
|
+
const CLOSE_BUTTON_STYLE = { width: 24, height: 24, color: "inherit", opacity: 0.8 };
|
|
187
|
+
/** Exit-transition length for windows; matches the stylesheet's `jg-panel-out` at `--jg-motion-fast`. */
|
|
188
|
+
const WINDOW_EXIT_MS = 140;
|
|
196
189
|
/** @internal Shared, fully-controlled window view — title bar (drag + close) plus a HudFrame body. */
|
|
197
|
-
function WindowView({ title, ariaLabel, closable, onClose, onFocus, onMove, pos, z, width, variation, shape, className, style, bodyStyle, children, }) {
|
|
190
|
+
function WindowView({ title, ariaLabel, open = true, closable, onClose, onFocus, onMove, pos, z, width, variation, shape, cue, onInteract, className, style, bodyStyle, children, }) {
|
|
198
191
|
const retireDrag = useRef(null);
|
|
199
192
|
useEffect(() => () => retireDrag.current?.(), []);
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
if (!open)
|
|
195
|
+
retireDrag.current?.();
|
|
196
|
+
}, [open]);
|
|
197
|
+
const reduced = useReducedMotion();
|
|
198
|
+
const presence = usePresence(open, reduced ? 0 : WINDOW_EXIT_MS);
|
|
199
|
+
const report = useHudInteract("window", cue, onInteract);
|
|
200
|
+
const reported = useRef(null);
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (reported.current === open)
|
|
203
|
+
return;
|
|
204
|
+
reported.current = open;
|
|
205
|
+
report(open ? "open" : "close");
|
|
206
|
+
}, [open, report]);
|
|
207
|
+
if (presence === "unmounted")
|
|
208
|
+
return null;
|
|
209
|
+
const frameBody = (_jsxs(_Fragment, { children: [_jsxs("div", { "data-jg-window-titlebar": "", onPointerDown: (event) => {
|
|
210
|
+
if (event.button !== 0 || !event.isPrimary || retireDrag.current !== null)
|
|
211
|
+
return;
|
|
212
|
+
onFocus?.();
|
|
213
|
+
if (onMove !== undefined) {
|
|
214
|
+
retireDrag.current = beginWindowDrag(event, pos, onMove, () => { retireDrag.current = null; });
|
|
215
|
+
}
|
|
216
|
+
}, style: TITLEBAR_STYLE, children: [_jsx("span", { "data-jg-window-title": "", children: title }), closable ? (_jsx(HudButton, { variant: "ghost", size: "icon", "data-jg-window-close": "", "aria-label": "Close", cue: "window-close", onClick: onClose, onPointerDown: (event) => event.stopPropagation(), style: CLOSE_BUTTON_STYLE, children: "\u2715" })) : null] }), _jsx("div", { "data-jg-window-body": "", style: { padding: 12, ...bodyStyle }, children: children })] }));
|
|
217
|
+
return (_jsx("div", { role: "dialog", "aria-label": ariaLabel, "data-jg-window": "", "data-state": presence, "data-jg-cue": cue, className: className, onPointerDown: () => onFocus?.(), style: { position: "absolute", left: pos.x, top: pos.y, zIndex: z, pointerEvents: "auto", ...style }, children: variation === undefined ? (_jsx(HudPanel, { elevation: "modal", padding: 0, width: width, style: shape === "square" ? { borderRadius: 0 } : undefined, children: frameBody })) : (_jsx(HudFrame, { variation: variation, shape: shape, width: width, padding: 0, interactive: true, children: frameBody })) }));
|
|
218
|
+
}
|
|
219
|
+
/** @internal Keeps just-closed panels rendered (closing) for `exitMs` so their exit transition plays. */
|
|
220
|
+
function useLeavingPanels(open, exitMs) {
|
|
221
|
+
const previous = useRef(open);
|
|
222
|
+
const timers = useRef(new Map());
|
|
223
|
+
const [leaving, setLeaving] = useState([]);
|
|
224
|
+
const openKey = open.map((def) => def.id).join("\u0000");
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
const openIds = new Set(open.map((def) => def.id));
|
|
227
|
+
const gone = exitMs <= 0 ? [] : previous.current.map((def, index) => ({ def, index })).filter(({ def }) => !openIds.has(def.id));
|
|
228
|
+
previous.current = open;
|
|
229
|
+
for (const id of openIds) {
|
|
230
|
+
clearTimeout(timers.current.get(id));
|
|
231
|
+
timers.current.delete(id);
|
|
232
|
+
}
|
|
233
|
+
for (const { def } of gone) {
|
|
234
|
+
timers.current.set(def.id, setTimeout(() => {
|
|
235
|
+
timers.current.delete(def.id);
|
|
236
|
+
setLeaving((current) => current.filter((entry) => entry.def.id !== def.id));
|
|
237
|
+
}, exitMs));
|
|
238
|
+
}
|
|
239
|
+
setLeaving((current) => {
|
|
240
|
+
const kept = current.filter(({ def }) => !openIds.has(def.id) && !gone.some((g) => g.def.id === def.id));
|
|
241
|
+
return kept.length === 0 && gone.length === 0 && current.length === 0 ? current : [...kept, ...gone];
|
|
242
|
+
});
|
|
243
|
+
}, [openKey, exitMs]);
|
|
244
|
+
useEffect(() => () => {
|
|
245
|
+
for (const timer of timers.current.values())
|
|
246
|
+
clearTimeout(timer);
|
|
247
|
+
}, []);
|
|
248
|
+
return leaving;
|
|
208
249
|
}
|
|
209
250
|
/** @internal Fallback cascade position for a window whose state has no override yet. */
|
|
210
251
|
function cascade(index) {
|
|
@@ -225,10 +266,12 @@ function cascade(index) {
|
|
|
225
266
|
*
|
|
226
267
|
* @capability panel-host render a manager's open windows as draggable, closable, z-stacked dialogs above the HUD
|
|
227
268
|
*/
|
|
228
|
-
export function PanelHost({ manager, render, panels, variation, shape, width, zIndexBase = 40, className, windowClassName, windowStyle, bodyStyle, }) {
|
|
269
|
+
export function PanelHost({ manager, render, panels, variation, shape, width, zIndexBase = 40, className, windowClassName, windowStyle, bodyStyle, onInteract, }) {
|
|
229
270
|
const open = orderedOpen(manager.state, manager.defs);
|
|
271
|
+
const leaving = useLeavingPanels(open, useReducedMotion() ? 0 : WINDOW_EXIT_MS);
|
|
230
272
|
const content = (id) => render !== undefined ? render(id) : (panels?.[id] ?? null);
|
|
231
|
-
|
|
273
|
+
const windows = [...open.map((def, index) => ({ def, index, open: true })), ...leaving.map((entry) => ({ ...entry, open: false }))];
|
|
274
|
+
return (_jsx("div", { "data-jg-panel-host": "", className: className, style: { position: "absolute", inset: 0, zIndex: zIndexBase, pointerEvents: "none" }, children: windows.map(({ def, index, open: isOpenWindow }) => (_jsx(WindowView, { open: isOpenWindow, cue: `panel:${def.id}`, onInteract: onInteract, title: def.title ?? def.id, ariaLabel: def.title ?? def.id, closable: def.closable !== false, onClose: () => manager.close(def.id), onFocus: () => manager.focus(def.id), onMove: (pos) => manager.move(def.id, pos), pos: manager.state.pos[def.id] ?? cascade(index), z: manager.state.z[def.id] ?? 1, width: width, variation: variation, shape: shape, className: windowClassName, style: windowStyle, bodyStyle: bodyStyle, children: content(def.id) }, def.id))) }));
|
|
232
275
|
}
|
|
233
276
|
/**
|
|
234
277
|
* A single standalone window — title bar, close button, and title-bar drag over {@link HudFrame} chrome —
|
|
@@ -238,7 +281,7 @@ export function PanelHost({ manager, render, panels, variation, shape, width, zI
|
|
|
238
281
|
*
|
|
239
282
|
* @capability window standalone draggable, closable window primitive over HudFrame — no manager required
|
|
240
283
|
*/
|
|
241
|
-
export function Window({ title, ariaLabel, closable = true, onClose, x, y, onMove, z, width, variation, shape, className, style, bodyStyle, children, }) {
|
|
284
|
+
export function Window({ title, ariaLabel, closable = true, onClose, x, y, onMove, z, width, variation, shape, className, style, bodyStyle, cue, onInteract, children, }) {
|
|
242
285
|
const controlled = x !== undefined && y !== undefined && onMove !== undefined;
|
|
243
286
|
const [localPos, setLocalPos] = useState({ x: x ?? 56, y: y ?? 56 });
|
|
244
287
|
const pos = controlled ? { x: x, y: y } : localPos;
|
|
@@ -248,5 +291,5 @@ export function Window({ title, ariaLabel, closable = true, onClose, x, y, onMov
|
|
|
248
291
|
onMove?.(next);
|
|
249
292
|
}, [controlled, onMove]);
|
|
250
293
|
const label = ariaLabel ?? (typeof title === "string" ? title : undefined);
|
|
251
|
-
return (_jsx(WindowView, { title: title, ariaLabel: label, closable: closable, onClose: onClose, onMove: handleMove, pos: pos, z: z, width: width, variation: variation, shape: shape, className: className, style: style, bodyStyle: bodyStyle, children: children }));
|
|
294
|
+
return (_jsx(WindowView, { title: title, ariaLabel: label, closable: closable, onClose: onClose, onMove: handleMove, pos: pos, z: z, width: width, variation: variation, shape: shape, className: className, style: style, bodyStyle: bodyStyle, cue: cue, onInteract: onInteract, children: children }));
|
|
252
295
|
}
|
package/dist/photoMode.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useSyncExternalStore } from "react";
|
|
3
|
+
import { hudType } from "./hudTokens.js";
|
|
3
4
|
/** Subscribe to a photo-mode store's live state. */
|
|
4
5
|
export function usePhotoMode(store) {
|
|
5
6
|
return useSyncExternalStore(store.subscribe, store.get, store.get);
|
|
@@ -35,5 +36,5 @@ export function PhotoModeControls({ store, onCapture, className, style }) {
|
|
|
35
36
|
color: "#e2e8f0",
|
|
36
37
|
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
37
38
|
...style,
|
|
38
|
-
}, children: [_jsx("span", { style: { fontSize:
|
|
39
|
+
}, children: [_jsx("span", { style: { fontSize: hudType("xs"), letterSpacing: 1.4, textTransform: "uppercase", color: "rgba(203,213,225,0.75)", padding: "0 4px" }, children: "\uD83D\uDCF7 Photo" }), _jsx("button", { type: "button", "data-photo-hide-hud": true, "aria-pressed": state.hideHud, onClick: () => store.setHideHud(!state.hideHud), style: { ...BTN, ...(state.hideHud ? { borderColor: "rgba(56,189,248,0.6)", color: "#bae6fd" } : {}) }, children: state.hideHud ? "HUD hidden" : "HUD shown" }), onCapture !== undefined ? (_jsx("button", { type: "button", "data-photo-capture": true, onClick: onCapture, style: { ...BTN, borderColor: "rgba(74,222,128,0.6)", color: "#bbf7d0" }, children: "Capture" })) : null, _jsx("button", { type: "button", "data-photo-exit": true, onClick: () => store.exit(), style: BTN, children: "Exit" })] }));
|
|
39
40
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Mount state for an element with an exit transition: `closed` keeps it mounted until the exit ends. */
|
|
2
|
+
export type PresenceState = "open" | "closed" | "unmounted";
|
|
3
|
+
/**
|
|
4
|
+
* Keeps an element mounted for `exitMs` after `open` turns false, so a `data-state="closed"` exit
|
|
5
|
+
* animation can play. Returns `"unmounted"` once the exit has finished (or at once when `exitMs` is 0).
|
|
6
|
+
*
|
|
7
|
+
* @capability presence keep a closing element mounted through its exit transition
|
|
8
|
+
*/
|
|
9
|
+
export declare function usePresence(open: boolean, exitMs: number): PresenceState;
|
package/dist/presence.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Keeps an element mounted for `exitMs` after `open` turns false, so a `data-state="closed"` exit
|
|
4
|
+
* animation can play. Returns `"unmounted"` once the exit has finished (or at once when `exitMs` is 0).
|
|
5
|
+
*
|
|
6
|
+
* @capability presence keep a closing element mounted through its exit transition
|
|
7
|
+
*/
|
|
8
|
+
export function usePresence(open, exitMs) {
|
|
9
|
+
const [mounted, setMounted] = useState(open);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (open) {
|
|
12
|
+
setMounted(true);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (exitMs <= 0) {
|
|
16
|
+
setMounted(false);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const timer = setTimeout(() => setMounted(false), exitMs);
|
|
20
|
+
return () => clearTimeout(timer);
|
|
21
|
+
}, [open, exitMs]);
|
|
22
|
+
if (open)
|
|
23
|
+
return "open";
|
|
24
|
+
return mounted ? "closed" : "unmounted";
|
|
25
|
+
}
|
package/dist/previewFixtures.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BarsPreview } from "./barsPreview.js";
|
|
2
|
+
import { HudChromePreview } from "./hudChromePreview.js";
|
|
2
3
|
import { HudThemePreview } from "./hudThemePreview.js";
|
|
3
4
|
import { IconsPreview } from "./iconsPreview.js";
|
|
4
5
|
import { StreetGeometryPreview } from "./streetGeometryPreview.js";
|
|
@@ -20,6 +21,11 @@ import { ScrollRailPreview } from "./scrollRailPreview.js";
|
|
|
20
21
|
*/
|
|
21
22
|
export const PREVIEW_FIXTURES = {
|
|
22
23
|
ScrollRailPreview: { name: "ScrollRailPreview", description: "Reachable caller-owned settings categories in shared scrolling chrome.", component: ScrollRailPreview },
|
|
24
|
+
HudChromePreview: {
|
|
25
|
+
name: "HudChromePreview",
|
|
26
|
+
description: "Shared HUD chrome under the default root theme: frame, vitals, action bar, window, dialogs, buttons.",
|
|
27
|
+
component: HudChromePreview,
|
|
28
|
+
},
|
|
23
29
|
HudThemePreview: {
|
|
24
30
|
name: "HudThemePreview",
|
|
25
31
|
description: "Every HudTheme genre preset × atomic bars + a themed frame + a slot row.",
|