@jgengine/react 0.24.0 → 0.25.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 +27 -0
- package/dist/engineStyles.d.ts +6 -3
- package/dist/engineStyles.js +13 -7
- package/dist/hudChromePreview.js +2 -2
- package/dist/hudLayout.js +3 -1
- package/dist/hudPanel.d.ts +15 -29
- package/dist/hudPanel.js +5 -30
- package/dist/hudSurface.d.ts +38 -0
- package/dist/hudSurface.js +40 -0
- package/dist/modals.d.ts +2 -2
- package/dist/modals.js +5 -5
- package/dist/notices.d.ts +18 -3
- package/dist/notices.js +65 -11
- package/dist/panels.js +2 -2
- package/dist/styles.css +7 -4
- package/package.json +2 -2
- package/skills/jgengine/api.md +2 -2
- package/skills/jgengine-ui/SKILL.md +3 -3
- package/skills/jgengine-ui/api.md +16 -9
- package/skills/jgengine-ui/capabilities.md +5 -1
- package/skills/jgengine-ui/reference-primitives.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -26,6 +26,33 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
26
26
|
`packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
|
|
27
27
|
-->
|
|
28
28
|
|
|
29
|
+
## 0.25.0
|
|
30
|
+
|
|
31
|
+
### Migrate
|
|
32
|
+
|
|
33
|
+
- **Bump lockstep SDK packages to `^0.25.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.22.0`; `@jgengine/github` is `0.12.0`.
|
|
34
|
+
- The frame panel from `@jgengine/react/hudPanel` is now `HudSurface` in `@jgengine/react/hudSurface` (`HudSurfaceProps`, `HudSurfaceElevation`). `HudPanel`, `HudPanelProps` and `HudPanelElevation` from `@jgengine/react/hudPanel` stay as deprecated aliases for this release. The `HudCanvas` layout slot keeps the name `HudPanel`.
|
|
35
|
+
- Engine HUD control sounds (button click/hover, panel and window open/close) no longer turn off with `notices: false` or `notices.cues: false`. Set `audio.hudCues: false` to silence them; `notices.cues` now covers notice sounds only.
|
|
36
|
+
- The notice stack now defaults to z-index 85 (was 45), above `ModalHost`. Pass `notices.zIndex` to put it back under a game overlay.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- `HudSurface` takes `as` (`section`, `aside`, `nav`, ...) to render as a landmark.
|
|
41
|
+
- `notices.offset`, `notices.avoidHud` and `notices.zIndex`, with matching `NoticeStack` props. The offset adds to the device safe-area inset.
|
|
42
|
+
- `ctx.game.notices.push({ text, silent: true })` shows a notice without its sound.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- The engine stylesheet (`@jgengine/react/styles.css` and the injected copy) sits in the `components.jgengine` cascade layer, after a `theme, base, components, utilities` layer order, so plain game CSS and Tailwind utilities override engine buttons and panels without `!important`.
|
|
47
|
+
- `HudSurface` padding comes from `--jg-panel-padding` (default 12px); the `padding` prop sets that variable instead of an inline `padding`, so a game class or media query can override it.
|
|
48
|
+
- `HudButton` labels wrap (`white-space: normal`, line height 1.2, 4px vertical padding) instead of overflowing on one line.
|
|
49
|
+
- The shell renders the notice stack inside the HUD theme root, so notices use the game's `hudTheme` colours and font.
|
|
50
|
+
- A top or bottom notice stack moves clear of `HudCanvas` panels and touch controls registered in its column (Harbor Heat's courier card, action bars).
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- Flowing `HudCanvas` panels now register their layout region once their anchor region mounts; before, most never registered, so layout collision checks and region consumers missed them.
|
|
55
|
+
|
|
29
56
|
## 0.24.0
|
|
30
57
|
|
|
31
58
|
### Migrate
|
package/dist/engineStyles.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The engine stylesheet: every `jg-*` keyframe the shared blocks and registry components animate
|
|
3
|
-
* with, the `[data-jg-*]` interaction-state rules behind {@link HudButton} / {@link
|
|
3
|
+
* with, the `[data-jg-*]` interaction-state rules behind {@link HudButton} / {@link HudSurface}, the
|
|
4
4
|
* quiet hover/press/focus defaults for plain buttons inside the HUD root, and reduced-motion
|
|
5
5
|
* handling (`prefers-reduced-motion` or `[data-reduced-motion="true"]` zero `--jg-motion-scale`).
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Every rule sits in the `components.jgengine` cascade layer, so unlayered game CSS, a game's own
|
|
7
|
+
* `components` rules and Tailwind utilities win without `!important`. It first declares
|
|
8
|
+
* `theme, base, components, utilities` so Tailwind v4's preflight stays below engine controls even
|
|
9
|
+
* when the engine sheet loads first. The shell injects it; games that mount outside the shell import
|
|
10
|
+
* `@jgengine/react/styles.css`, which holds the same text.
|
|
8
11
|
*/
|
|
9
12
|
export declare const ENGINE_STYLESHEET: string;
|
|
10
13
|
/** Injects {@link ENGINE_STYLESHEET} into `doc` once. Safe to call repeatedly and during SSR (no-op). */
|
package/dist/engineStyles.js
CHANGED
|
@@ -16,13 +16,18 @@ const ENABLED = ':not(:disabled, [aria-disabled="true"])';
|
|
|
16
16
|
const KILL_MOTION = "animation-duration: 1ms !important; animation-delay: 0ms !important; animation-iteration-count: 1 !important; transition-duration: 1ms !important; transition-delay: 0ms !important;";
|
|
17
17
|
/**
|
|
18
18
|
* The engine stylesheet: every `jg-*` keyframe the shared blocks and registry components animate
|
|
19
|
-
* with, the `[data-jg-*]` interaction-state rules behind {@link HudButton} / {@link
|
|
19
|
+
* with, the `[data-jg-*]` interaction-state rules behind {@link HudButton} / {@link HudSurface}, the
|
|
20
20
|
* quiet hover/press/focus defaults for plain buttons inside the HUD root, and reduced-motion
|
|
21
21
|
* handling (`prefers-reduced-motion` or `[data-reduced-motion="true"]` zero `--jg-motion-scale`).
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* Every rule sits in the `components.jgengine` cascade layer, so unlayered game CSS, a game's own
|
|
23
|
+
* `components` rules and Tailwind utilities win without `!important`. It first declares
|
|
24
|
+
* `theme, base, components, utilities` so Tailwind v4's preflight stays below engine controls even
|
|
25
|
+
* when the engine sheet loads first. The shell injects it; games that mount outside the shell import
|
|
26
|
+
* `@jgengine/react/styles.css`, which holds the same text.
|
|
24
27
|
*/
|
|
25
28
|
export const ENGINE_STYLESHEET = `/* @jgengine/react engine stylesheet */
|
|
29
|
+
@layer theme, base, components, utilities;
|
|
30
|
+
@layer components.jgengine {
|
|
26
31
|
:root { --jg-motion-scale: 1; }
|
|
27
32
|
@media (prefers-reduced-motion: reduce) { :root { --jg-motion-scale: 0; } }
|
|
28
33
|
[data-reduced-motion="true"] { --jg-motion-scale: 0; }
|
|
@@ -56,7 +61,7 @@ export const ENGINE_STYLESHEET = `/* @jgengine/react engine stylesheet */
|
|
|
56
61
|
}
|
|
57
62
|
:where([data-jg-hud]) :where(button, [role="button"]):where(${ENABLED}):active { scale: var(--jg-press-scale, 0.97); }
|
|
58
63
|
|
|
59
|
-
[data-jg-button] { appearance: none; box-sizing: border-box; position: relative; isolation: isolate; display: inline-flex; align-items: center; justify-content: center; gap: var(--jg-space-sm, 8px); min-height: 36px; padding:
|
|
64
|
+
[data-jg-button] { appearance: none; box-sizing: border-box; position: relative; isolation: isolate; display: inline-flex; align-items: center; justify-content: center; gap: var(--jg-space-sm, 8px); min-height: 36px; padding: var(--jg-space-xs, 4px) var(--jg-space-lg, 16px); margin: 0; font-family: var(--jg-font-body, ${HUD_FONT_STACK}); font-size: var(--jg-type-md, 14px); font-weight: 600; line-height: 1.2; letter-spacing: 0.01em; text-align: center; white-space: normal; color: var(--jg-text, #f5f7fa); background: linear-gradient(180deg, rgba(255,255,255,0.10), rgba(255,255,255,0.03)), var(--jg-slot-bg, rgba(12,15,21,0.78)); border: var(--jg-slot-border, 1px solid rgba(255,255,255,0.13)); border-radius: var(--jg-slot-radius, 8px); box-shadow: ${HIGHLIGHT}, ${RAISED}; cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; transition: translate ${FAST} ${EASE}, scale ${FAST} ${SPRING}, box-shadow ${FAST} ${EASE}, border-color ${FAST} ${EASE}, background-color ${FAST} ${EASE}; }
|
|
60
65
|
[data-jg-button]::after { content: ""; position: absolute; inset: 0; z-index: -1; border-radius: inherit; background: var(--jg-hover-overlay, rgba(255,255,255,0.07)); opacity: 0; pointer-events: none; transition: opacity ${FAST} ${EASE}; }
|
|
61
66
|
@media (hover: hover) {
|
|
62
67
|
[data-jg-button]${ENABLED}:hover { translate: 0 var(--jg-hover-lift, -1px); box-shadow: ${HIGHLIGHT}, ${OVERLAY}; }
|
|
@@ -70,12 +75,12 @@ export const ENGINE_STYLESHEET = `/* @jgengine/react engine stylesheet */
|
|
|
70
75
|
[data-jg-button][data-variant="danger"] { color: #fff; background: linear-gradient(180deg, rgba(255,255,255,0.16), rgba(255,255,255,0) 60%), var(--jg-danger, #e5484d); border: 1px solid rgba(255,255,255,0.22); }
|
|
71
76
|
[data-jg-button][data-variant="ghost"] { background: transparent; border-color: transparent; box-shadow: none; }
|
|
72
77
|
[data-jg-button][data-variant="ghost"]${ENABLED}:hover { box-shadow: none; }
|
|
73
|
-
[data-jg-button][data-size="sm"] { min-height: 28px; padding:
|
|
74
|
-
[data-jg-button][data-size="lg"] { min-height: 44px; padding:
|
|
78
|
+
[data-jg-button][data-size="sm"] { min-height: 28px; padding-inline: var(--jg-space-md, 12px); font-size: var(--jg-type-sm, 13px); }
|
|
79
|
+
[data-jg-button][data-size="lg"] { min-height: 44px; padding-inline: var(--jg-space-xl, 24px); font-size: var(--jg-type-lg, 16px); }
|
|
75
80
|
[data-jg-button][data-size="icon"] { min-height: 0; width: 28px; height: 28px; padding: 0; font-size: var(--jg-type-sm, 13px); }
|
|
76
81
|
[data-jg-button][data-block] { display: flex; width: 100%; }
|
|
77
82
|
|
|
78
|
-
[data-jg-panel] { box-sizing: border-box; color: var(--jg-text, #f5f7fa); font-family: var(--jg-font-body, ${HUD_FONT_STACK}); background: var(--jg-frame-bg, rgba(12,15,21,0.9)); border: var(--jg-frame-border, 1px solid rgba(255,255,255,0.13)); border-radius: var(--jg-frame-radius, 10px); box-shadow: var(--jg-frame-glow, ${HIGHLIGHT}, ${RAISED}); }
|
|
83
|
+
[data-jg-panel] { box-sizing: border-box; padding: var(--jg-panel-padding, 12px); color: var(--jg-text, #f5f7fa); font-family: var(--jg-font-body, ${HUD_FONT_STACK}); background: var(--jg-frame-bg, rgba(12,15,21,0.9)); border: var(--jg-frame-border, 1px solid rgba(255,255,255,0.13)); border-radius: var(--jg-frame-radius, 10px); box-shadow: var(--jg-frame-glow, ${HIGHLIGHT}, ${RAISED}); }
|
|
79
84
|
[data-jg-panel][data-elevation="overlay"] { box-shadow: var(--jg-frame-glow, ${HIGHLIGHT}), ${OVERLAY}; }
|
|
80
85
|
[data-jg-panel][data-elevation="modal"] { box-shadow: var(--jg-frame-glow, ${HIGHLIGHT}), ${MODAL}; }
|
|
81
86
|
[data-jg-panel][data-state="open"] { animation: jg-panel-in ${BASE} ${EASE_OUT} both; }
|
|
@@ -94,6 +99,7 @@ export const ENGINE_STYLESHEET = `/* @jgengine/react engine stylesheet */
|
|
|
94
99
|
@media (hover: hover) { [data-action-id][data-enabled]:hover { translate: 0 var(--jg-hover-lift, -1px); } }
|
|
95
100
|
[data-action-id][data-enabled]:active { translate: 0 0; scale: var(--jg-press-scale, 0.97); }
|
|
96
101
|
[data-action-id]:focus-visible { outline: none; box-shadow: ${FOCUS}; }
|
|
102
|
+
}
|
|
97
103
|
`;
|
|
98
104
|
const STYLE_ID = "jg-engine-styles";
|
|
99
105
|
/** Injects {@link ENGINE_STYLESHEET} into `doc` once. Safe to call repeatedly and during SSR (no-op). */
|
package/dist/hudChromePreview.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ActionButton } from "./actionHud.js";
|
|
|
4
4
|
import { HealthBar, ManaBar } from "./bars.js";
|
|
5
5
|
import { HudButton } from "./hudButton.js";
|
|
6
6
|
import { HudFrame } from "./hudFrame.js";
|
|
7
|
-
import {
|
|
7
|
+
import { HudSurface } from "./hudSurface.js";
|
|
8
8
|
import { HudThemeRoot } from "./hudThemeRoot.js";
|
|
9
9
|
import { ConfirmDialog, PauseMenu } from "./modals.js";
|
|
10
10
|
import { Window } from "./panels.js";
|
|
@@ -23,5 +23,5 @@ const noop = () => undefined;
|
|
|
23
23
|
* variants. Screenshots identically; the before/after evidence for theme and primitive changes.
|
|
24
24
|
*/
|
|
25
25
|
export function HudChromePreview({ className }) {
|
|
26
|
-
return (_jsx("div", { className: className, style: { position: "relative", width: "100%", height: "100%", minHeight: 720, overflow: "hidden", background: "radial-gradient(ellipse at 30% 20%, #3b5a46, #1b2a24 55%, #0f1714)" }, children: _jsxs(HudThemeRoot, { children: [_jsx("div", { style: { position: "absolute", left: 24, top: 24, width: 280 }, children: _jsx(HudFrame, { title: "Warden \u00B7 Lv 12", aside: _jsx(TweenNumber, { value: 12480 }), padding: 12, children: _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [_jsx(HealthBar, { value: 78, max: 100, label: "HP", width: "100%" }), _jsx(ManaBar, { value: 50, max: 80, label: "MP", width: "100%" })] }) }) }), _jsx("div", { style: { position: "absolute", left: 24, bottom: 24, display: "flex", gap: 8 }, children: ACTIONS.map((action, index) => (_jsx(ActionButton, { action: action, focused: index === 0 }, action.id))) }), _jsx(Window, { title: "Inventory", x: 340, y: 24, width: 260, closable: true, onClose: noop, children: _jsx("div", { style: { display: "grid", gridTemplateColumns: "repeat(5, 1fr)", gap: 6 }, children: Array.from({ length: 10 }, (_, index) => (_jsx("div", { style: { aspectRatio: "1", background: "var(--jg-slot-bg)", border: "var(--jg-slot-border)", borderRadius: "var(--jg-slot-radius)" } }, index))) }) }), _jsx("div", { style: { position: "absolute", left: 640, top: 24 }, children: _jsx(ConfirmDialog, { title: "Abandon the expedition?", body: "Your haul stays in the wagon, but the crew returns home.", confirmLabel: "Abandon", danger: true, onConfirm: noop, onCancel: noop }) }), _jsx("div", { style: { position: "absolute", left: 1040, top: 24 }, children: _jsx(PauseMenu, { onResume: noop, items: [{ id: "settings", label: "Settings", onSelect: noop }, { id: "quit", label: "Quit to title", onSelect: noop, danger: true }] }) }), _jsx("div", { style: { position: "absolute", left: 340, top: 300 }, children: _jsx(
|
|
26
|
+
return (_jsx("div", { className: className, style: { position: "relative", width: "100%", height: "100%", minHeight: 720, overflow: "hidden", background: "radial-gradient(ellipse at 30% 20%, #3b5a46, #1b2a24 55%, #0f1714)" }, children: _jsxs(HudThemeRoot, { children: [_jsx("div", { style: { position: "absolute", left: 24, top: 24, width: 280 }, children: _jsx(HudFrame, { title: "Warden \u00B7 Lv 12", aside: _jsx(TweenNumber, { value: 12480 }), padding: 12, children: _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [_jsx(HealthBar, { value: 78, max: 100, label: "HP", width: "100%" }), _jsx(ManaBar, { value: 50, max: 80, label: "MP", width: "100%" })] }) }) }), _jsx("div", { style: { position: "absolute", left: 24, bottom: 24, display: "flex", gap: 8 }, children: ACTIONS.map((action, index) => (_jsx(ActionButton, { action: action, focused: index === 0 }, action.id))) }), _jsx(Window, { title: "Inventory", x: 340, y: 24, width: 260, closable: true, onClose: noop, children: _jsx("div", { style: { display: "grid", gridTemplateColumns: "repeat(5, 1fr)", gap: 6 }, children: Array.from({ length: 10 }, (_, index) => (_jsx("div", { style: { aspectRatio: "1", background: "var(--jg-slot-bg)", border: "var(--jg-slot-border)", borderRadius: "var(--jg-slot-radius)" } }, index))) }) }), _jsx("div", { style: { position: "absolute", left: 640, top: 24 }, children: _jsx(ConfirmDialog, { title: "Abandon the expedition?", body: "Your haul stays in the wagon, but the crew returns home.", confirmLabel: "Abandon", danger: true, onConfirm: noop, onCancel: noop }) }), _jsx("div", { style: { position: "absolute", left: 1040, top: 24 }, children: _jsx(PauseMenu, { onResume: noop, items: [{ id: "settings", label: "Settings", onSelect: noop }, { id: "quit", label: "Quit to title", onSelect: noop, danger: true }] }) }), _jsx("div", { style: { position: "absolute", left: 340, top: 300 }, children: _jsx(HudSurface, { title: "Buttons", padding: 14, children: _jsxs("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", width: 420 }, children: [_jsx(HudButton, { variant: "primary", children: "Deploy" }), _jsx(HudButton, { children: "Inspect" }), _jsx(HudButton, { variant: "ghost", children: "Skip" }), _jsx(HudButton, { variant: "danger", children: "Scrap" }), _jsx(HudButton, { disabled: true, children: "Locked" }), _jsx(HudButton, { size: "sm", children: "Small" })] }) }) })] }) }));
|
|
27
27
|
}
|
package/dist/hudLayout.js
CHANGED
|
@@ -579,7 +579,9 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
|
|
|
579
579
|
allowOverlapWith,
|
|
580
580
|
collisionGroup,
|
|
581
581
|
}), [id, mobileBehavior, priority, allowOverlapWith, collisionGroup]);
|
|
582
|
-
|
|
582
|
+
// A flowing panel portals into its anchor region, which HudCanvas only provides after its first commit.
|
|
583
|
+
const awaitingRegion = panel !== undefined && (compact || !panel.moved) && regions[compact ? anchor : panel.placement.anchor] === undefined;
|
|
584
|
+
useRegisterLayoutRegion(regionSpec, rootRef, region && !regionHidden && !awaitingRegion);
|
|
583
585
|
const onPointerDown = (event) => {
|
|
584
586
|
if (!draggable || event.button !== 0)
|
|
585
587
|
return;
|
package/dist/hudPanel.d.ts
CHANGED
|
@@ -1,31 +1,17 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type HudInteractListener } from "./hudInteract.js";
|
|
3
|
-
/** Shadow height of a {@link HudPanel}. */
|
|
4
|
-
export type HudPanelElevation = "raised" | "overlay" | "modal";
|
|
5
|
-
/** Props for {@link HudPanel}. */
|
|
6
|
-
export interface HudPanelProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
7
|
-
/** Show or hide with an enter/exit transition. Unset renders a static panel with no transition. */
|
|
8
|
-
open?: boolean;
|
|
9
|
-
/** Uppercase header label at the top-left. */
|
|
10
|
-
title?: ReactNode;
|
|
11
|
-
/** Header content at the top-right, opposite `title`. */
|
|
12
|
-
aside?: ReactNode;
|
|
13
|
-
/** Shadow height. Default `raised`. */
|
|
14
|
-
elevation?: HudPanelElevation;
|
|
15
|
-
padding?: number | string;
|
|
16
|
-
width?: number | string;
|
|
17
|
-
/** Accept pointer events. Default `true`. */
|
|
18
|
-
interactive?: boolean;
|
|
19
|
-
/** Feedback cue name, rendered as `data-jg-cue` and passed to `onInteract`. */
|
|
20
|
-
cue?: string;
|
|
21
|
-
/** Called with `open` and `close` as the panel shows and hides. */
|
|
22
|
-
onInteract?: HudInteractListener;
|
|
23
|
-
}
|
|
1
|
+
import { type HudSurfaceElevation, type HudSurfaceProps } from "./hudSurface.js";
|
|
24
2
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* (`data-state="open" | "closed"`); reduced motion skips both. `className` and `style` restyle it.
|
|
28
|
-
*
|
|
29
|
-
* @capability hud-panel themed panel frame with elevation, header slots, and open/close transitions
|
|
3
|
+
* Old name of {@link HudSurfaceElevation} (`@jgengine/react/hudSurface`).
|
|
4
|
+
* @deprecated Use `HudSurfaceElevation`; this alias goes in the next release.
|
|
30
5
|
*/
|
|
31
|
-
export
|
|
6
|
+
export type HudPanelElevation = HudSurfaceElevation;
|
|
7
|
+
/**
|
|
8
|
+
* Old name of {@link HudSurfaceProps} (`@jgengine/react/hudSurface`).
|
|
9
|
+
* @deprecated Use `HudSurfaceProps`; this alias goes in the next release.
|
|
10
|
+
*/
|
|
11
|
+
export type HudPanelProps = HudSurfaceProps;
|
|
12
|
+
/**
|
|
13
|
+
* Old name of the themed frame {@link HudSurface} (`@jgengine/react/hudSurface`), renamed so it no longer
|
|
14
|
+
* shares a name with the `HudCanvas` layout slot `HudPanel`.
|
|
15
|
+
* @deprecated Use `HudSurface`; this alias goes in the next release.
|
|
16
|
+
*/
|
|
17
|
+
export declare const HudPanel: import("react").ForwardRefExoticComponent<HudSurfaceProps & import("react").RefAttributes<HTMLElement>>;
|
package/dist/hudPanel.js
CHANGED
|
@@ -1,32 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { forwardRef, useEffect, useRef } from "react";
|
|
3
|
-
import { useReducedMotion } from "./accessibility.js";
|
|
4
|
-
import { useEngineStyles } from "./engineStyles.js";
|
|
5
|
-
import { useHudInteract } from "./hudInteract.js";
|
|
6
|
-
import { usePresence } from "./presence.js";
|
|
7
|
-
const EXIT_MS = 140;
|
|
1
|
+
import { HudSurface } from "./hudSurface.js";
|
|
8
2
|
/**
|
|
9
|
-
*
|
|
10
|
-
* a
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* @capability hud-panel themed panel frame with elevation, header slots, and open/close transitions
|
|
3
|
+
* Old name of the themed frame {@link HudSurface} (`@jgengine/react/hudSurface`), renamed so it no longer
|
|
4
|
+
* shares a name with the `HudCanvas` layout slot `HudPanel`.
|
|
5
|
+
* @deprecated Use `HudSurface`; this alias goes in the next release.
|
|
14
6
|
*/
|
|
15
|
-
export const HudPanel =
|
|
16
|
-
useEngineStyles();
|
|
17
|
-
const reduced = useReducedMotion();
|
|
18
|
-
const shown = open ?? true;
|
|
19
|
-
const presence = usePresence(shown, reduced ? 0 : EXIT_MS);
|
|
20
|
-
const report = useHudInteract("panel", cue, onInteract);
|
|
21
|
-
const wasOpen = useRef(shown);
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
if (wasOpen.current === shown)
|
|
24
|
-
return;
|
|
25
|
-
wasOpen.current = shown;
|
|
26
|
-
report(shown ? "open" : "close");
|
|
27
|
-
}, [shown, report]);
|
|
28
|
-
if (presence === "unmounted")
|
|
29
|
-
return null;
|
|
30
|
-
const hasHeader = title !== undefined || aside !== undefined;
|
|
31
|
-
return (_jsxs("div", { ...rest, ref: ref, "data-jg-panel": "", "data-state": open === undefined ? undefined : presence, "data-elevation": elevation, "data-jg-cue": cue, style: { padding, width, pointerEvents: interactive ? "auto" : undefined, ...style }, children: [hasHeader ? (_jsxs("div", { "data-jg-panel-header": "", style: { marginBottom: 8 }, children: [_jsx("span", { children: title }), aside !== undefined ? _jsx("span", { children: aside }) : null] })) : null, children] }));
|
|
32
|
-
});
|
|
7
|
+
export const HudPanel = HudSurface;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import { type HudInteractListener } from "./hudInteract.js";
|
|
3
|
+
/** Shadow height of a {@link HudSurface}. */
|
|
4
|
+
export type HudSurfaceElevation = "raised" | "overlay" | "modal";
|
|
5
|
+
/** Block elements a {@link HudSurface} can render as, so it can be a landmark. */
|
|
6
|
+
export type HudSurfaceElement = "div" | "section" | "aside" | "nav" | "header" | "footer" | "article" | "main" | "form" | "dialog";
|
|
7
|
+
/** Props for {@link HudSurface}. */
|
|
8
|
+
export interface HudSurfaceProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
9
|
+
/** Element to render. Default `div`; `section`, `aside`, `nav` and the like make it a landmark. */
|
|
10
|
+
as?: HudSurfaceElement;
|
|
11
|
+
/** Show or hide with an enter/exit transition. Unset renders a static panel with no transition. */
|
|
12
|
+
open?: boolean;
|
|
13
|
+
/** Uppercase header label at the top-left. */
|
|
14
|
+
title?: ReactNode;
|
|
15
|
+
/** Header content at the top-right, opposite `title`. */
|
|
16
|
+
aside?: ReactNode;
|
|
17
|
+
/** Shadow height. Default `raised`. */
|
|
18
|
+
elevation?: HudSurfaceElevation;
|
|
19
|
+
/** Sets `--jg-panel-padding` (default 12px from the stylesheet), so a game class or media query still overrides it. */
|
|
20
|
+
padding?: number | string;
|
|
21
|
+
width?: number | string;
|
|
22
|
+
/** Accept pointer events. Default `true`. */
|
|
23
|
+
interactive?: boolean;
|
|
24
|
+
/** Feedback cue name, rendered as `data-jg-cue` and passed to `onInteract`. */
|
|
25
|
+
cue?: string;
|
|
26
|
+
/** Called with `open` and `close` as the panel shows and hides. */
|
|
27
|
+
onInteract?: HudInteractListener;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Themed panel/window frame over the `HudTheme` frame, elevation, and motion tokens. It enters with
|
|
31
|
+
* a short rise-and-fade and stays mounted through its exit when `open` turns false
|
|
32
|
+
* (`data-state="open" | "closed"`); reduced motion skips both. Padding comes from
|
|
33
|
+
* `--jg-panel-padding`, and the engine rules sit in a cascade layer, so a game's `className`
|
|
34
|
+
* restyles it without `!important`. Not the `HudCanvas` layout slot (`HudPanel` in `@jgengine/react/hudLayout`).
|
|
35
|
+
*
|
|
36
|
+
* @capability hud-surface themed panel frame with elevation, header slots, landmark element, and open/close transitions
|
|
37
|
+
*/
|
|
38
|
+
export declare const HudSurface: import("react").ForwardRefExoticComponent<HudSurfaceProps & import("react").RefAttributes<HTMLElement>>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useRef } from "react";
|
|
3
|
+
import { useReducedMotion } from "./accessibility.js";
|
|
4
|
+
import { useEngineStyles } from "./engineStyles.js";
|
|
5
|
+
import { useHudInteract } from "./hudInteract.js";
|
|
6
|
+
import { usePresence } from "./presence.js";
|
|
7
|
+
const EXIT_MS = 140;
|
|
8
|
+
/**
|
|
9
|
+
* Themed panel/window frame over the `HudTheme` frame, elevation, and motion tokens. It enters with
|
|
10
|
+
* a short rise-and-fade and stays mounted through its exit when `open` turns false
|
|
11
|
+
* (`data-state="open" | "closed"`); reduced motion skips both. Padding comes from
|
|
12
|
+
* `--jg-panel-padding`, and the engine rules sit in a cascade layer, so a game's `className`
|
|
13
|
+
* restyles it without `!important`. Not the `HudCanvas` layout slot (`HudPanel` in `@jgengine/react/hudLayout`).
|
|
14
|
+
*
|
|
15
|
+
* @capability hud-surface themed panel frame with elevation, header slots, landmark element, and open/close transitions
|
|
16
|
+
*/
|
|
17
|
+
export const HudSurface = forwardRef(function HudSurface({ as: Element = "div", open, title, aside, elevation = "raised", padding, width, interactive = true, cue, onInteract, style, children, ...rest }, ref) {
|
|
18
|
+
useEngineStyles();
|
|
19
|
+
const reduced = useReducedMotion();
|
|
20
|
+
const shown = open ?? true;
|
|
21
|
+
const presence = usePresence(shown, reduced ? 0 : EXIT_MS);
|
|
22
|
+
const report = useHudInteract("panel", cue, onInteract);
|
|
23
|
+
const wasOpen = useRef(shown);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (wasOpen.current === shown)
|
|
26
|
+
return;
|
|
27
|
+
wasOpen.current = shown;
|
|
28
|
+
report(shown ? "open" : "close");
|
|
29
|
+
}, [shown, report]);
|
|
30
|
+
if (presence === "unmounted")
|
|
31
|
+
return null;
|
|
32
|
+
const hasHeader = title !== undefined || aside !== undefined;
|
|
33
|
+
const frameStyle = {
|
|
34
|
+
width,
|
|
35
|
+
pointerEvents: interactive ? "auto" : undefined,
|
|
36
|
+
...(padding === undefined ? {} : { "--jg-panel-padding": typeof padding === "number" ? `${padding}px` : padding }),
|
|
37
|
+
...style,
|
|
38
|
+
};
|
|
39
|
+
return (_jsxs(Element, { ...rest, ref: ref, "data-jg-panel": "", "data-state": open === undefined ? undefined : presence, "data-elevation": elevation, "data-jg-cue": cue, style: frameStyle, children: [hasHeader ? (_jsxs("div", { "data-jg-panel-header": "", style: { marginBottom: 8 }, children: [_jsx("span", { children: title }), aside !== undefined ? _jsx("span", { children: aside }) : null] })) : null, children] }));
|
|
40
|
+
});
|
package/dist/modals.d.ts
CHANGED
|
@@ -88,7 +88,7 @@ export interface ConfirmDialogProps {
|
|
|
88
88
|
}
|
|
89
89
|
/**
|
|
90
90
|
* A generic two-button confirmation dialog — title, optional body, and Cancel / Confirm buttons over
|
|
91
|
-
* a `HudTheme`-token-driven {@link
|
|
91
|
+
* a `HudTheme`-token-driven {@link HudSurface}. Presentation-only: wire `onConfirm`/`onCancel` to a
|
|
92
92
|
* modal stack's `resolve` (typically inside a {@link ModalHost} render callback) and reskin via
|
|
93
93
|
* `HudTheme` tokens or `danger` for a destructive action. It interprets nothing — the game supplies
|
|
94
94
|
* the copy.
|
|
@@ -124,7 +124,7 @@ export interface PauseMenuProps {
|
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
126
|
* A reskinnable pause-menu building block: a primary Resume button plus a game-filled list of slot
|
|
127
|
-
* items (Settings, Save, Quit, …), stacked in a `HudTheme`-token-driven {@link
|
|
127
|
+
* items (Settings, Save, Quit, …), stacked in a `HudTheme`-token-driven {@link HudSurface}. It ships
|
|
128
128
|
* the common shape every game's pause screen needs and nothing genre-specific — the game supplies the
|
|
129
129
|
* title, item labels/order, and destructive styling, and reskins with `HudTheme` tokens. Drop it into
|
|
130
130
|
* a {@link ModalHost} keyed on a `"pause"` modal, or render it standalone.
|
package/dist/modals.js
CHANGED
|
@@ -6,7 +6,7 @@ import { useDialogBehavior } from "./dialogBehavior.js";
|
|
|
6
6
|
import { useEngineStyles } from "./engineStyles.js";
|
|
7
7
|
import { HudButton } from "./hudButton.js";
|
|
8
8
|
import { useHudInteract } from "./hudInteract.js";
|
|
9
|
-
import {
|
|
9
|
+
import { HudSurface } from "./hudSurface.js";
|
|
10
10
|
import { hudType } from "./hudTokens.js";
|
|
11
11
|
import { usePresence } from "./presence.js";
|
|
12
12
|
/**
|
|
@@ -79,7 +79,7 @@ const DIALOG_BODY = { margin: "10px 0 0", fontSize: hudType("md"), lineHeight: 1
|
|
|
79
79
|
const BUTTON_ROW = { display: "flex", gap: 10, marginTop: 20, justifyContent: "flex-end" };
|
|
80
80
|
/**
|
|
81
81
|
* A generic two-button confirmation dialog — title, optional body, and Cancel / Confirm buttons over
|
|
82
|
-
* a `HudTheme`-token-driven {@link
|
|
82
|
+
* a `HudTheme`-token-driven {@link HudSurface}. Presentation-only: wire `onConfirm`/`onCancel` to a
|
|
83
83
|
* modal stack's `resolve` (typically inside a {@link ModalHost} render callback) and reskin via
|
|
84
84
|
* `HudTheme` tokens or `danger` for a destructive action. It interprets nothing — the game supplies
|
|
85
85
|
* the copy.
|
|
@@ -87,7 +87,7 @@ const BUTTON_ROW = { display: "flex", gap: 10, marginTop: 20, justifyContent: "f
|
|
|
87
87
|
* @capability confirm-dialog generic themeable confirm/cancel dialog — title, body, two buttons, danger styling
|
|
88
88
|
*/
|
|
89
89
|
export function ConfirmDialog({ title, body, confirmLabel = "Confirm", cancelLabel = "Cancel", onConfirm, onCancel, danger = false, width = 360, className, style, }) {
|
|
90
|
-
return (_jsxs(
|
|
90
|
+
return (_jsxs(HudSurface, { 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 })] })] }));
|
|
91
91
|
}
|
|
92
92
|
const PAUSE_TITLE = {
|
|
93
93
|
margin: "0 0 14px",
|
|
@@ -102,7 +102,7 @@ const PAUSE_ITEM = { marginTop: 8 };
|
|
|
102
102
|
const PAUSE_DANGER_ITEM = { ...PAUSE_ITEM, color: "var(--jg-danger, #e5484d)", borderColor: "rgba(229,72,77,0.45)" };
|
|
103
103
|
/**
|
|
104
104
|
* A reskinnable pause-menu building block: a primary Resume button plus a game-filled list of slot
|
|
105
|
-
* items (Settings, Save, Quit, …), stacked in a `HudTheme`-token-driven {@link
|
|
105
|
+
* items (Settings, Save, Quit, …), stacked in a `HudTheme`-token-driven {@link HudSurface}. It ships
|
|
106
106
|
* the common shape every game's pause screen needs and nothing genre-specific — the game supplies the
|
|
107
107
|
* title, item labels/order, and destructive styling, and reskins with `HudTheme` tokens. Drop it into
|
|
108
108
|
* a {@link ModalHost} keyed on a `"pause"` modal, or render it standalone.
|
|
@@ -110,5 +110,5 @@ const PAUSE_DANGER_ITEM = { ...PAUSE_ITEM, color: "var(--jg-danger, #e5484d)", b
|
|
|
110
110
|
* @capability pause-menu reskinnable pause-menu building block — Resume plus a game-filled Settings/Quit slot list, HudTheme-driven
|
|
111
111
|
*/
|
|
112
112
|
export function PauseMenu({ title = "Paused", onResume, resumeLabel = "Resume", items = [], width = 300, className, style, }) {
|
|
113
|
-
return (_jsxs(
|
|
113
|
+
return (_jsxs(HudSurface, { 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)))] }));
|
|
114
114
|
}
|
package/dist/notices.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import type { Notice, NoticeFeed } from "@jgengine/core/game/notices";
|
|
3
|
+
import type { LayoutRect, LayoutRegion } from "@jgengine/core/ui/gameLayout";
|
|
3
4
|
export type { Notice, NoticeTone } from "@jgengine/core/game/notices";
|
|
4
5
|
/** Where {@link NoticeStack} sits: below top-edge HUD bars, over the play area, or above bottom action bars. */
|
|
5
6
|
export type NoticePlacement = "top" | "center" | "bottom";
|
|
7
|
+
/**
|
|
8
|
+
* How far a top or bottom notice stack must move toward the screen middle to clear the registered
|
|
9
|
+
* layout regions in its column. `stack` is its current rect, already moved by `shift`; regions
|
|
10
|
+
* reaching past the container's middle (full-height sidebars, screens) are ignored. @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare function noticeAvoidShift(placement: NoticePlacement, stack: LayoutRect, shift: number, container: LayoutRect, regions: readonly Pick<LayoutRegion, "kind" | "rect">[], gap?: number): number;
|
|
6
13
|
type Shown = {
|
|
7
14
|
notice: Notice;
|
|
8
15
|
leaving: boolean;
|
|
@@ -13,14 +20,22 @@ export declare function reconcileShown(previous: readonly Shown[], live: readonl
|
|
|
13
20
|
export declare function useNoticeList(feed: NoticeFeed | null): readonly Notice[];
|
|
14
21
|
/**
|
|
15
22
|
* Animated, toned notice stack for a {@link NoticeFeed} — the default renderer the shell mounts for
|
|
16
|
-
* `ctx.game.notices
|
|
23
|
+
* `ctx.game.notices` inside the HUD theme root. Uses `--jg-*` theme vars with fallbacks, 14px text, and
|
|
24
|
+
* never takes pointer input. A top or bottom stack keeps clear of HUD panels registered in its column
|
|
25
|
+
* and sits above modal overlays.
|
|
17
26
|
*
|
|
18
|
-
* @capability notice-feed render ctx.game.notices as an animated toned stack (enter/exit motion, ×N stacking)
|
|
27
|
+
* @capability notice-feed render ctx.game.notices as an animated toned stack (enter/exit motion, ×N stacking, HUD avoidance)
|
|
19
28
|
*/
|
|
20
|
-
export declare function NoticeStack({ feed, placement, scale, renderNotice, }: {
|
|
29
|
+
export declare function NoticeStack({ feed, placement, offset, avoidHud, zIndex, scale, renderNotice, }: {
|
|
21
30
|
/** Defaults to the surrounding game's `ctx.game.notices`. */
|
|
22
31
|
feed?: NoticeFeed;
|
|
23
32
|
placement?: NoticePlacement;
|
|
33
|
+
/** Distance from the band's screen edge (px or CSS length), added to the safe-area inset. */
|
|
34
|
+
offset?: number | string;
|
|
35
|
+
/** Move a top/bottom stack clear of registered HUD panels and touch controls in its column. Default true. */
|
|
36
|
+
avoidHud?: boolean;
|
|
37
|
+
/** Default 85: above `ModalHost` (80) and `PanelHost` (40). */
|
|
38
|
+
zIndex?: number;
|
|
24
39
|
/** Multiplies text and spacing (the shell passes the player's UI scale). */
|
|
25
40
|
scale?: number;
|
|
26
41
|
renderNotice?: (notice: Notice) => ReactNode;
|
package/dist/notices.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState, useSyncExternalStore } from "react";
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
+
import { useGameViewportLayout } from "./gameViewport.js";
|
|
3
4
|
import { useOptionalGameContext } from "./provider.js";
|
|
4
5
|
const EXIT_MS = 200;
|
|
5
6
|
const EMPTY = [];
|
|
@@ -11,11 +12,48 @@ const TONE_COLOR = {
|
|
|
11
12
|
deny: "var(--jg-danger, #f26d6d)",
|
|
12
13
|
};
|
|
13
14
|
const TONE_LABEL = { info: "Info", success: "Done", warn: "Warning", deny: "Can't do that" };
|
|
14
|
-
const
|
|
15
|
-
top:
|
|
16
|
-
center:
|
|
17
|
-
bottom:
|
|
15
|
+
const DEFAULT_OFFSET = {
|
|
16
|
+
top: "clamp(64px, 14%, 150px)",
|
|
17
|
+
center: "34%",
|
|
18
|
+
bottom: "clamp(120px, 22%, 230px)",
|
|
18
19
|
};
|
|
20
|
+
const AVOID_GAP = 8;
|
|
21
|
+
function placementStyle(placement, offset) {
|
|
22
|
+
const edge = offset === undefined ? DEFAULT_OFFSET[placement] : typeof offset === "number" ? `${offset}px` : offset;
|
|
23
|
+
if (placement === "bottom")
|
|
24
|
+
return { bottom: `calc(var(--jg-safe-bottom, 0px) + ${edge})`, flexDirection: "column-reverse" };
|
|
25
|
+
if (placement === "center")
|
|
26
|
+
return { top: edge };
|
|
27
|
+
return { top: `calc(var(--jg-safe-top, 0px) + ${edge})` };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* How far a top or bottom notice stack must move toward the screen middle to clear the registered
|
|
31
|
+
* layout regions in its column. `stack` is its current rect, already moved by `shift`; regions
|
|
32
|
+
* reaching past the container's middle (full-height sidebars, screens) are ignored. @internal
|
|
33
|
+
*/
|
|
34
|
+
export function noticeAvoidShift(placement, stack, shift, container, regions, gap = AVOID_GAP) {
|
|
35
|
+
if (placement === "center")
|
|
36
|
+
return 0;
|
|
37
|
+
const middle = (container.top + container.bottom) / 2;
|
|
38
|
+
let need = 0;
|
|
39
|
+
for (const { kind, rect } of regions) {
|
|
40
|
+
if (kind === "screen" || rect.right <= rect.left || rect.bottom <= rect.top)
|
|
41
|
+
continue;
|
|
42
|
+
if (rect.right <= stack.left || rect.left >= stack.right)
|
|
43
|
+
continue;
|
|
44
|
+
if (placement === "top") {
|
|
45
|
+
if (rect.bottom > middle)
|
|
46
|
+
continue;
|
|
47
|
+
need = Math.max(need, rect.bottom + gap - (stack.top - shift));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
if (rect.top < middle)
|
|
51
|
+
continue;
|
|
52
|
+
need = Math.max(need, stack.bottom + shift - (rect.top - gap));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return Math.max(0, Math.round(need));
|
|
56
|
+
}
|
|
19
57
|
const KEYFRAMES = `
|
|
20
58
|
@keyframes jg-notice-in { from { opacity: 0; transform: translateY(-8px) scale(0.97); } to { opacity: 1; transform: none; } }
|
|
21
59
|
@keyframes jg-notice-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(-6px) scale(0.98); } }
|
|
@@ -79,11 +117,13 @@ export function useNoticeList(feed) {
|
|
|
79
117
|
}
|
|
80
118
|
/**
|
|
81
119
|
* Animated, toned notice stack for a {@link NoticeFeed} — the default renderer the shell mounts for
|
|
82
|
-
* `ctx.game.notices
|
|
120
|
+
* `ctx.game.notices` inside the HUD theme root. Uses `--jg-*` theme vars with fallbacks, 14px text, and
|
|
121
|
+
* never takes pointer input. A top or bottom stack keeps clear of HUD panels registered in its column
|
|
122
|
+
* and sits above modal overlays.
|
|
83
123
|
*
|
|
84
|
-
* @capability notice-feed render ctx.game.notices as an animated toned stack (enter/exit motion, ×N stacking)
|
|
124
|
+
* @capability notice-feed render ctx.game.notices as an animated toned stack (enter/exit motion, ×N stacking, HUD avoidance)
|
|
85
125
|
*/
|
|
86
|
-
export function NoticeStack({ feed, placement = "top", scale = 1, renderNotice, }) {
|
|
126
|
+
export function NoticeStack({ feed, placement = "top", offset, avoidHud = true, zIndex = 85, scale = 1, renderNotice, }) {
|
|
87
127
|
const ctx = useOptionalGameContext();
|
|
88
128
|
const source = feed ?? ctx?.game.notices ?? null;
|
|
89
129
|
const live = useNoticeList(source);
|
|
@@ -93,6 +133,19 @@ export function NoticeStack({ feed, placement = "top", scale = 1, renderNotice,
|
|
|
93
133
|
setSeenLive(live);
|
|
94
134
|
setShown(reconcileShown(shown, live));
|
|
95
135
|
}
|
|
136
|
+
const { regions } = useGameViewportLayout();
|
|
137
|
+
const stackRef = useRef(null);
|
|
138
|
+
const [shift, setShift] = useState(0);
|
|
139
|
+
const visible = shown.length > 0;
|
|
140
|
+
useLayoutEffect(() => {
|
|
141
|
+
const element = stackRef.current;
|
|
142
|
+
const container = element?.offsetParent;
|
|
143
|
+
const next = element == null || container == null || !avoidHud
|
|
144
|
+
? 0
|
|
145
|
+
: noticeAvoidShift(placement, element.getBoundingClientRect(), shift, container.getBoundingClientRect(), regions);
|
|
146
|
+
if (next !== shift)
|
|
147
|
+
setShift(next);
|
|
148
|
+
}, [regions, visible, placement, avoidHud, shift]);
|
|
96
149
|
const anyLeaving = shown.some((entry) => entry.leaving);
|
|
97
150
|
useEffect(() => {
|
|
98
151
|
if (!anyLeaving)
|
|
@@ -102,18 +155,19 @@ export function NoticeStack({ feed, placement = "top", scale = 1, renderNotice,
|
|
|
102
155
|
}, [anyLeaving]);
|
|
103
156
|
if (shown.length === 0)
|
|
104
157
|
return null;
|
|
105
|
-
return (_jsxs("div", { "data-jg-notices": placement, role: "status", "aria-live": "polite", style: {
|
|
158
|
+
return (_jsxs("div", { ref: stackRef, "data-jg-notices": placement, role: "status", "aria-live": "polite", style: {
|
|
106
159
|
position: "absolute",
|
|
107
160
|
left: "50%",
|
|
108
161
|
transform: "translateX(-50%)",
|
|
109
|
-
zIndex
|
|
162
|
+
zIndex,
|
|
110
163
|
display: "flex",
|
|
111
164
|
flexDirection: "column",
|
|
112
165
|
alignItems: "center",
|
|
113
166
|
gap: 6 * scale,
|
|
114
167
|
width: "min(460px, calc(100% - 32px))",
|
|
115
168
|
pointerEvents: "none",
|
|
116
|
-
...
|
|
169
|
+
...placementStyle(placement, offset),
|
|
170
|
+
...(shift === 0 ? {} : placement === "bottom" ? { marginBottom: shift } : { marginTop: shift }),
|
|
117
171
|
}, 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
172
|
display: "flex",
|
|
119
173
|
alignItems: "center",
|
package/dist/panels.js
CHANGED
|
@@ -5,7 +5,7 @@ import { useReducedMotion } from "./accessibility.js";
|
|
|
5
5
|
import { HudButton } from "./hudButton.js";
|
|
6
6
|
import { HudFrame } from "./hudFrame.js";
|
|
7
7
|
import { useHudInteract } from "./hudInteract.js";
|
|
8
|
-
import {
|
|
8
|
+
import { HudSurface } from "./hudSurface.js";
|
|
9
9
|
import { hudType } from "./hudTokens.js";
|
|
10
10
|
import { observeBrowserSuspension } from "./browserLifecycle.js";
|
|
11
11
|
import { usePresence } from "./presence.js";
|
|
@@ -214,7 +214,7 @@ function WindowView({ title, ariaLabel, open = true, closable, onClose, onFocus,
|
|
|
214
214
|
retireDrag.current = beginWindowDrag(event, pos, onMove, () => { retireDrag.current = null; });
|
|
215
215
|
}
|
|
216
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(
|
|
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(HudSurface, { 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
218
|
}
|
|
219
219
|
/** @internal Keeps just-closed panels rendered (closing) for `exitMs` so their exit transition plays. */
|
|
220
220
|
function useLeavingPanels(open, exitMs) {
|
package/dist/styles.css
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/* @jgengine/react engine stylesheet */
|
|
2
|
+
@layer theme, base, components, utilities;
|
|
3
|
+
@layer components.jgengine {
|
|
2
4
|
:root { --jg-motion-scale: 1; }
|
|
3
5
|
@media (prefers-reduced-motion: reduce) { :root { --jg-motion-scale: 0; } }
|
|
4
6
|
[data-reduced-motion="true"] { --jg-motion-scale: 0; }
|
|
@@ -32,7 +34,7 @@
|
|
|
32
34
|
}
|
|
33
35
|
:where([data-jg-hud]) :where(button, [role="button"]):where(:not(:disabled, [aria-disabled="true"])):active { scale: var(--jg-press-scale, 0.97); }
|
|
34
36
|
|
|
35
|
-
[data-jg-button] { appearance: none; box-sizing: border-box; position: relative; isolation: isolate; display: inline-flex; align-items: center; justify-content: center; gap: var(--jg-space-sm, 8px); min-height: 36px; padding:
|
|
37
|
+
[data-jg-button] { appearance: none; box-sizing: border-box; position: relative; isolation: isolate; display: inline-flex; align-items: center; justify-content: center; gap: var(--jg-space-sm, 8px); min-height: 36px; padding: var(--jg-space-xs, 4px) var(--jg-space-lg, 16px); margin: 0; font-family: var(--jg-font-body, "Inter Variable", Inter, "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif); font-size: var(--jg-type-md, 14px); font-weight: 600; line-height: 1.2; letter-spacing: 0.01em; text-align: center; white-space: normal; color: var(--jg-text, #f5f7fa); background: linear-gradient(180deg, rgba(255,255,255,0.10), rgba(255,255,255,0.03)), var(--jg-slot-bg, rgba(12,15,21,0.78)); border: var(--jg-slot-border, 1px solid rgba(255,255,255,0.13)); border-radius: var(--jg-slot-radius, 8px); box-shadow: var(--jg-highlight, inset 0 1px 0 rgba(255,255,255,0.08)), var(--jg-elevation-raised, 0 1px 2px rgba(0,0,0,0.45), 0 4px 12px -2px rgba(0,0,0,0.4)); cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; transition: translate calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease, cubic-bezier(0.2, 0, 0, 1)), scale calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease-spring, cubic-bezier(0.34, 1.56, 0.64, 1)), box-shadow calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease, cubic-bezier(0.2, 0, 0, 1)), border-color calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease, cubic-bezier(0.2, 0, 0, 1)), background-color calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease, cubic-bezier(0.2, 0, 0, 1)); }
|
|
36
38
|
[data-jg-button]::after { content: ""; position: absolute; inset: 0; z-index: -1; border-radius: inherit; background: var(--jg-hover-overlay, rgba(255,255,255,0.07)); opacity: 0; pointer-events: none; transition: opacity calc(var(--jg-motion-fast, 120ms) * var(--jg-motion-scale, 1)) var(--jg-ease, cubic-bezier(0.2, 0, 0, 1)); }
|
|
37
39
|
@media (hover: hover) {
|
|
38
40
|
[data-jg-button]:not(:disabled, [aria-disabled="true"]):hover { translate: 0 var(--jg-hover-lift, -1px); box-shadow: var(--jg-highlight, inset 0 1px 0 rgba(255,255,255,0.08)), var(--jg-elevation-overlay, 0 2px 4px rgba(0,0,0,0.4), 0 10px 24px -4px rgba(0,0,0,0.5)); }
|
|
@@ -46,12 +48,12 @@
|
|
|
46
48
|
[data-jg-button][data-variant="danger"] { color: #fff; background: linear-gradient(180deg, rgba(255,255,255,0.16), rgba(255,255,255,0) 60%), var(--jg-danger, #e5484d); border: 1px solid rgba(255,255,255,0.22); }
|
|
47
49
|
[data-jg-button][data-variant="ghost"] { background: transparent; border-color: transparent; box-shadow: none; }
|
|
48
50
|
[data-jg-button][data-variant="ghost"]:not(:disabled, [aria-disabled="true"]):hover { box-shadow: none; }
|
|
49
|
-
[data-jg-button][data-size="sm"] { min-height: 28px; padding:
|
|
50
|
-
[data-jg-button][data-size="lg"] { min-height: 44px; padding:
|
|
51
|
+
[data-jg-button][data-size="sm"] { min-height: 28px; padding-inline: var(--jg-space-md, 12px); font-size: var(--jg-type-sm, 13px); }
|
|
52
|
+
[data-jg-button][data-size="lg"] { min-height: 44px; padding-inline: var(--jg-space-xl, 24px); font-size: var(--jg-type-lg, 16px); }
|
|
51
53
|
[data-jg-button][data-size="icon"] { min-height: 0; width: 28px; height: 28px; padding: 0; font-size: var(--jg-type-sm, 13px); }
|
|
52
54
|
[data-jg-button][data-block] { display: flex; width: 100%; }
|
|
53
55
|
|
|
54
|
-
[data-jg-panel] { box-sizing: border-box; color: var(--jg-text, #f5f7fa); font-family: var(--jg-font-body, "Inter Variable", Inter, "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif); background: var(--jg-frame-bg, rgba(12,15,21,0.9)); border: var(--jg-frame-border, 1px solid rgba(255,255,255,0.13)); border-radius: var(--jg-frame-radius, 10px); box-shadow: var(--jg-frame-glow, var(--jg-highlight, inset 0 1px 0 rgba(255,255,255,0.08)), var(--jg-elevation-raised, 0 1px 2px rgba(0,0,0,0.45), 0 4px 12px -2px rgba(0,0,0,0.4))); }
|
|
56
|
+
[data-jg-panel] { box-sizing: border-box; padding: var(--jg-panel-padding, 12px); color: var(--jg-text, #f5f7fa); font-family: var(--jg-font-body, "Inter Variable", Inter, "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif); background: var(--jg-frame-bg, rgba(12,15,21,0.9)); border: var(--jg-frame-border, 1px solid rgba(255,255,255,0.13)); border-radius: var(--jg-frame-radius, 10px); box-shadow: var(--jg-frame-glow, var(--jg-highlight, inset 0 1px 0 rgba(255,255,255,0.08)), var(--jg-elevation-raised, 0 1px 2px rgba(0,0,0,0.45), 0 4px 12px -2px rgba(0,0,0,0.4))); }
|
|
55
57
|
[data-jg-panel][data-elevation="overlay"] { box-shadow: var(--jg-frame-glow, var(--jg-highlight, inset 0 1px 0 rgba(255,255,255,0.08))), var(--jg-elevation-overlay, 0 2px 4px rgba(0,0,0,0.4), 0 10px 24px -4px rgba(0,0,0,0.5)); }
|
|
56
58
|
[data-jg-panel][data-elevation="modal"] { box-shadow: var(--jg-frame-glow, var(--jg-highlight, inset 0 1px 0 rgba(255,255,255,0.08))), var(--jg-elevation-modal, 0 4px 8px rgba(0,0,0,0.35), 0 24px 56px -12px rgba(0,0,0,0.7)); }
|
|
57
59
|
[data-jg-panel][data-state="open"] { animation: jg-panel-in calc(var(--jg-motion-base, 180ms) * var(--jg-motion-scale, 1)) var(--jg-ease-out, cubic-bezier(0.16, 1, 0.3, 1)) both; }
|
|
@@ -70,3 +72,4 @@
|
|
|
70
72
|
@media (hover: hover) { [data-action-id][data-enabled]:hover { translate: 0 var(--jg-hover-lift, -1px); } }
|
|
71
73
|
[data-action-id][data-enabled]:active { translate: 0 0; scale: var(--jg-press-scale, 0.97); }
|
|
72
74
|
[data-action-id]:focus-visible { outline: none; box-shadow: var(--jg-focus-ring, 0 0 0 2px #0a0c10, 0 0 0 4px var(--jg-accent, #38bdf8)); }
|
|
75
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "React UI layer for JGengine: GameProvider, hooks, and headless primitives over @jgengine/core.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"styles": "bun scripts/styles.ts"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@jgengine/core": "^0.
|
|
38
|
+
"@jgengine/core": "^0.25.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"react": "^19.0.0",
|
package/skills/jgengine/api.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
8
8
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
9
|
-
- `VERSION` (const): const VERSION: "0.
|
|
9
|
+
- `VERSION` (const): const VERSION: "0.25.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
10
10
|
|
|
11
11
|
## @jgengine/core/authoring
|
|
12
12
|
|
|
@@ -232,7 +232,7 @@
|
|
|
232
232
|
|
|
233
233
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
234
234
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
235
|
-
- `VERSION` (const): const VERSION: "0.
|
|
235
|
+
- `VERSION` (const): const VERSION: "0.25.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
236
236
|
|
|
237
237
|
## @jgengine/core/runtime/adapter
|
|
238
238
|
|
|
@@ -32,8 +32,8 @@ Existing React games keep their entity store and use the focused
|
|
|
32
32
|
- **Settings is game-owned and reachable from a sensible place.** Every game ships settings as *its* settings — skinned and placed inline with its UI (main menu and pause), never the engine's stock face floated into a corner. Credits are a reachable in-game screen, not attribution buried in a build artifact.
|
|
33
33
|
- **Route the front-end with `useMenuRouter`, and generate the credits.** `useMenuRouter(root)` is the screen stack behind title → save select → settings → credits: `open`/`back`/`replace`/`reset`, `path` for depth, and Escape-to-back bound by default (`escapeToBack: false` when the game owns Escape). `menuStackReducer` is the same transition as a pure function. For credits, `creditsForSourceIds(ids)` (`@jgengine/assets/credits`) turns the pack ids a game declares to `buildCatalog` into licensed, attributed sections, `mergeCredits` folds them into hand-authored lines, and `CreditsScreen` renders the result HudTheme-skinned.
|
|
34
34
|
- **Skin the shared chrome with one `HudTheme` token object.** `hudThemeVars(theme)` emits the whole `--jg-*` vocabulary — bar fills and troughs, `HudFrame variation="themed"`, painted slots, the minimap ring, plus surfaces, edges, text, status colors, fonts, and rarity — so one theme spread on any HUD ancestor reskins the `@jgengine/react` primitives and the `registry/jgengine` blocks together. Author your own with `deriveHudTheme({ accent, surface })` or `hudTheme({ ... })`; `HUD_THEME_PRESETS` / `resolveHudTheme("<preset>")` are demo/scaffold starting points, never a game's identity. A component that needs a token no theme emits fails `check-hud-tokens` — add it to `HudTheme` rather than inventing a second vocabulary. Caller `style`/`renderItem` overrides still win.
|
|
35
|
-
- **The shell applies `defaultHudTheme` at the GameUI root.** `HudThemeRoot` wraps every `GameUI` in a `display: contents` `[data-jg-hud]` element carrying the theme vars and injects the engine stylesheet (`jg-*` keyframes, `[data-jg-*]` interaction states, reduced motion). Set `defineGame({ hudTheme })` to swap the root theme, `hudTheme: false` to opt out; a theme spread inside `GameUI` still wins. Games outside the shell add `@import "@jgengine/react/styles.css";` to `src/index.css` (the `jgengine create` template does). The theme also carries a type scale with a 12px floor, spacing, motion, elevation, and hover/press/focus/disabled tokens; read them in inline styles through `hudType`, `hudSpace`, `hudTransition`, `hudElevation`, and `HUD_FOCUS_RING` (`@jgengine/react/hudTokens`) — never hardcode text under 12px.
|
|
36
|
-
- **Build interactive chrome from `HudButton`, `
|
|
35
|
+
- **The shell applies `defaultHudTheme` at the GameUI root.** `HudThemeRoot` wraps every `GameUI` in a `display: contents` `[data-jg-hud]` element carrying the theme vars and injects the engine stylesheet (`jg-*` keyframes, `[data-jg-*]` interaction states, reduced motion). Set `defineGame({ hudTheme })` to swap the root theme, `hudTheme: false` to opt out; a theme spread inside `GameUI` still wins. Games outside the shell add `@import "@jgengine/react/styles.css";` to `src/index.css` (the `jgengine create` template does). Every engine rule sits in the `components.jgengine` cascade layer, so plain game CSS and Tailwind utilities override engine buttons and panels without `!important`. The theme also carries a type scale with a 12px floor, spacing, motion, elevation, and hover/press/focus/disabled tokens; read them in inline styles through `hudType`, `hudSpace`, `hudTransition`, `hudElevation`, and `HUD_FOCUS_RING` (`@jgengine/react/hudTokens`) — never hardcode text under 12px.
|
|
36
|
+
- **Build interactive chrome from `HudButton`, `HudSurface`, and `TweenNumber`.** `HudButton` gives hover lift, press scale, focus ring, and disabled from tokens, and wraps long labels; `HudSurface` (`@jgengine/react/hudSurface`; `HudPanel` from `@jgengine/react/hudPanel` is its deprecated old name, not the `HudCanvas` slot) gives elevation, header slots, an `as` landmark element, padding from `--jg-panel-padding`, and an `open` enter/exit transition; `TweenNumber` / `useTweenedValue` roll counters with an up/down delta flash. Each takes `cue` (`data-jg-cue`) and `onInteract`; `HudInteractProvider` routes every primitive's hover/press/activate/open/close to one listener; the shell mounts one that plays the matching UI cue, so engine primitives sound with no wiring (`cue="confirm"` picks the activate sound). `audio.hudCues: false` silences those control sounds; notice sounds are separate (`notices.cues`).
|
|
37
37
|
- **Paint slot icons with `IconTreatment`, never render raw `itemId` strings.** It draws a `GameIcon` glyph over a school/element-keyed gradient with gloss, vignette, and optional count/keycap badges, reading the `--jg-slot-*` / `--jg-accent` tokens. `treatedItemIcon(itemId)` / `treatedActionIcon(action)` are ready adapters for hotbars and ability bars.
|
|
38
38
|
- **Player death gets a visible screen and respawn feedback — never a silent teleport.** Build the moment `game-design` requires: an authored death/downed overlay that reads the lethal event, communicates the consequence, and surfaces the way back (respawn/revive/restart). Drive it from `createDownedState` (`@jgengine/core/combat`) rather than a hand-rolled flag, and mount it inside `HudCanvas`.
|
|
39
39
|
- **Browser interruption listeners are shared.** Use `useBrowserSuspension` from `@jgengine/react/browserLifecycle` for blur, hidden-page and pointer-lock loss. Keep the decision to pause, release controls or preserve multiplayer simulation in the callback; use options to disable unwanted triggers.
|
|
@@ -43,7 +43,7 @@ Existing React games keep their entity store and use the focused
|
|
|
43
43
|
|
|
44
44
|
## Shared-world interaction rules
|
|
45
45
|
|
|
46
|
-
- **Refusals and notices are engine feedback, not per-game toasts.** Return `{ reason: "insufficient-gold", message: "Needs 40 more gold" }` from a command's `validate`: the shell shows `message` in the shared notice feed with the deny cue and stacks repeats. Push other player messages with `ctx.game.notices.push({ text, tone })` (info/success/warn/deny) from game logic instead of a hand-built toast list. Tune or hide
|
|
46
|
+
- **Refusals and notices are engine feedback, not per-game toasts.** Return `{ reason: "insufficient-gold", message: "Needs 40 more gold" }` from a command's `validate`: the shell shows `message` in the shared notice feed with the deny cue and stacks repeats. Push other player messages with `ctx.game.notices.push({ text, tone })` (info/success/warn/deny) from game logic instead of a hand-built toast list. The stack renders inside the HUD theme root, so it takes the game's `hudTheme` colours and font; a top or bottom stack moves clear of `HudCanvas` panels and touch controls in its column and sits above `ModalHost` (z 85). Tune or hide it with `notices: { placement, offset, avoidHud, zIndex, cues, hidden }` (`hidden` keeps cues and lets the game render `useNoticeList(ctx.game.notices)` itself); `push({ text, silent: true })` skips one notice's sound.
|
|
47
47
|
- **UI sounds come from `useUiCue()`.** Engine primitives already sound through the shell; for game-owned markup call `cue("click" | "hover" | "open" | "close" | "confirm" | "deny" | "notify")` from buttons and panels; replace a default by declaring `audio.sounds["ui.click"]`, or silence with `audio.uiCues: { hover: false }` / `false`.
|
|
48
48
|
- Every rejected verb displays its reason on the triggering surface. Use `useServerSession(transport, gameId)` for join lifecycle. Join failure blocks play with `JoinGate` and retry; returning `{ ok: false }` without feedback is incomplete.
|
|
49
49
|
- Placement previews display claim cost and affordability inline with `TerritoryOverlay`.
|
|
@@ -601,7 +601,7 @@
|
|
|
601
601
|
- `ComboMeterTheme` (interface): interface ComboMeterTheme — Reskin tokens for {@link ComboMeterHud}.
|
|
602
602
|
- `Compass` (function): function Compass({ facingYaw, center, markers, width = 340, fov = (Math.PI * 2) / 3, kindStyles = DEFAULT_MARKER_KINDS, className, }: CompassProps): ReactNode — Horizontal compass strip centered on the player's facing direction, with the eight cardinals and optional marker pips from static views, an external source, or a native `MarkerSet`.
|
|
603
603
|
- `CompassProps` (interface): interface CompassProps { facingYaw: number; center?: WorldXZ; markers?: MarkerCollection; width?: number; fov?: number; kindStyles?: Record<string, MarkerKindStyle>; className?: string } — ⚠ undocumented · used by `Compass`: Horizontal compass strip centered on the player's facing direction, with the eight cardinals and optional marker pips from static views, an …
|
|
604
|
-
- `ConfirmDialog` (function): function ConfirmDialog({ title, body, confirmLabel = "Confirm", cancelLabel = "Cancel", onConfirm, onCancel, danger = false, width = 360, className, style, }: ConfirmDialogProps): ReactNode — A generic two-button confirmation dialog — title, optional body, and Cancel / Confirm buttons over a `HudTheme`-token-driven {@link
|
|
604
|
+
- `ConfirmDialog` (function): function ConfirmDialog({ title, body, confirmLabel = "Confirm", cancelLabel = "Cancel", onConfirm, onCancel, danger = false, width = 360, className, style, }: ConfirmDialogProps): ReactNode — A generic two-button confirmation dialog — title, optional body, and Cancel / Confirm buttons over a `HudTheme`-token-driven {@link HudSurface}. Presentation-only: wire `onConfirm`/`onCancel` to a modal stack's `resolve` (typically inside a {@link ModalHost} render callback) and reskin via `HudTheme` tokens or `danger` for a destructive action. It interprets nothing — the game supplies the copy.
|
|
605
605
|
- `ConfirmDialogProps` (interface): interface ConfirmDialogProps — Props for {@link ConfirmDialog}.
|
|
606
606
|
- `ControlHint` (interface): interface ControlHint — One row of a control legend. Name the game action(s) whose bound key(s) to show (`action`) so the glyphs come straight from the keybind map — never re-typed — or give literal `keys` for controls that live outside the map (`"Mouse"`, `"LMB"`). `label` says what the control does.
|
|
607
607
|
- `ControlsList` (function): function ControlsList({ bindings, controls, hideOnCoarsePointer = true, separator = " / ", className, rowClassName, keysClassName, keyClassName, separatorClassName, labelClassName, renderKey, renderRow, }: { bindings?: ActionCodesMap; controls: readonly ControlHint[]; hideOnCoarsePointer?: boolean; … — Renders a controls legend whose key glyphs come from the game's keybind map, so bindings live in one place and never drift from a hand-typed table. Hides itself on coarse-pointer devices (a touchscreen has no keyboard) via the same `data-jg-kbd-hint` marker as `KeyHint`. Headless: every part carries a `className` slot and `data-*` hook; pass `renderKey`/`renderRow` to fully own the markup.
|
|
@@ -772,7 +772,7 @@
|
|
|
772
772
|
- `PartyFrame` (function): function PartyFrame({ className, rowClassName, dotClassName, emptyState, renderMember, }: { className?: string; rowClassName?: string; dotClassName?: string; emptyState?: ReactNode; renderMember?: (member: PartyMemberEntry) => ReactNode; }): React.JSX.Element — ⚠ undocumented
|
|
773
773
|
- `PartyInviteToast` (function): function PartyInviteToast({ className, acceptClassName, declineClassName, renderInvite, }: { className?: string; acceptClassName?: string; declineClassName?: string; renderInvite?: (invite: PartyInviteEntry) => ReactNode; }): React.JSX.Element | null — ⚠ undocumented
|
|
774
774
|
- `PartyMemberRow` (function): function PartyMemberRow({ member, className, dotClassName, children, }: { member: PartyMemberEntry; className?: string; dotClassName?: string; children?: ReactNode; }): React.JSX.Element — ⚠ undocumented
|
|
775
|
-
- `PauseMenu` (function): function PauseMenu({ title = "Paused", onResume, resumeLabel = "Resume", items = [], width = 300, className, style, }: PauseMenuProps): ReactNode — A reskinnable pause-menu building block: a primary Resume button plus a game-filled list of slot items (Settings, Save, Quit, …), stacked in a `HudTheme`-token-driven {@link
|
|
775
|
+
- `PauseMenu` (function): function PauseMenu({ title = "Paused", onResume, resumeLabel = "Resume", items = [], width = 300, className, style, }: PauseMenuProps): ReactNode — A reskinnable pause-menu building block: a primary Resume button plus a game-filled list of slot items (Settings, Save, Quit, …), stacked in a `HudTheme`-token-driven {@link HudSurface}. It ships the common shape every game's pause screen needs and nothing genre-specific — the game supplies the title, item labels/order, and destructive styling, and reskins with `HudTheme` tokens. Drop it into a {@link ModalHost} keyed on a `"pause"` modal, or render it standalone.
|
|
776
776
|
- `PauseMenuItem` (interface): interface PauseMenuItem — One selectable row in a {@link PauseMenu}'s slot list (Settings, Save, Quit, …).
|
|
777
777
|
- `PauseMenuProps` (interface): interface PauseMenuProps — Props for {@link PauseMenu}.
|
|
778
778
|
- `PhotoModeControls` (function): function PhotoModeControls({ store, onCapture, className, style }: PhotoModeControlsProps): ReactNode — Photo-mode toolbar — a hide/show-HUD toggle, a capture button, and an exit button, bound to a `createPhotoModeStore`. Presentation-only: the game wires `onCapture` to shell `captureCanvas`/`downloadImage` and reads `hideHud` to drop its gameplay HUD.
|
|
@@ -1241,7 +1241,7 @@
|
|
|
1241
1241
|
|
|
1242
1242
|
## @jgengine/react/engineStyles
|
|
1243
1243
|
|
|
1244
|
-
- `ENGINE_STYLESHEET` (const): const ENGINE_STYLESHEET: string — The engine stylesheet: every `jg-*` keyframe the shared blocks and registry components animate with, the `[data-jg-*]` interaction-state rules behind {@link HudButton} / {@link
|
|
1244
|
+
- `ENGINE_STYLESHEET` (const): const ENGINE_STYLESHEET: string — The engine stylesheet: every `jg-*` keyframe the shared blocks and registry components animate with, the `[data-jg-*]` interaction-state rules behind {@link HudButton} / {@link HudSurface}, the quiet hover/press/focus defaults for plain buttons inside the HUD root, and reduced-motion handling (`prefers-reduced-motion` or `[data-reduced-motion="true"]` zero `--jg-motion-scale`). Every rule sits in the `components.jgengine` cascade layer, so unlayered game CSS, a game's own `components` rules and Tailwind utilities win without `!important`. It first declares `theme, base, components, utilities` so Tailwind v4's preflight stays below engine controls even when the engine sheet loads first. The shell injects it; games that mount outside the shell import `@jgengine/react/styles.css`, which holds the same text.
|
|
1245
1245
|
- `ensureEngineStyles` (function): function ensureEngineStyles(doc: Document | undefined = typeof document === "undefined" ? undefined : document): void — Injects {@link ENGINE_STYLESHEET} into `doc` once. Safe to call repeatedly and during SSR (no-op).
|
|
1246
1246
|
- `useEngineStyles` (function): function useEngineStyles(): void — Ensures the engine stylesheet is in the document before the calling component paints. Engine primitives call it, so they animate and show interaction states even outside the shell.
|
|
1247
1247
|
|
|
@@ -1407,9 +1407,9 @@
|
|
|
1407
1407
|
|
|
1408
1408
|
## @jgengine/react/hudPanel
|
|
1409
1409
|
|
|
1410
|
-
- `HudPanel` (const): const HudPanel: React.ForwardRefExoticComponent<
|
|
1411
|
-
- `HudPanelElevation` (type): type HudPanelElevation =
|
|
1412
|
-
- `HudPanelProps` (
|
|
1410
|
+
- `HudPanel` (const): const HudPanel: React.ForwardRefExoticComponent<HudSurfaceProps & React.RefAttributes<HTMLElement>> — Old name of the themed frame {@link HudSurface} (`@jgengine/react/hudSurface`), renamed so it no longer shares a name with the `HudCanvas` layout slot `HudPanel`.
|
|
1411
|
+
- `HudPanelElevation` (type): type HudPanelElevation = HudSurfaceElevation — Old name of {@link HudSurfaceElevation} (`@jgengine/react/hudSurface`).
|
|
1412
|
+
- `HudPanelProps` (type): type HudPanelProps = HudSurfaceProps — Old name of {@link HudSurfaceProps} (`@jgengine/react/hudSurface`).
|
|
1413
1413
|
|
|
1414
1414
|
## @jgengine/react/hudSkin
|
|
1415
1415
|
|
|
@@ -1422,6 +1422,13 @@
|
|
|
1422
1422
|
|
|
1423
1423
|
- `HudSkinPreview` (function): function HudSkinPreview({ className }: { className?: string }): React.JSX.Element — Procedural two-skin fixture for visual regression captures.
|
|
1424
1424
|
|
|
1425
|
+
## @jgengine/react/hudSurface
|
|
1426
|
+
|
|
1427
|
+
- `HudSurface` (const): const HudSurface: React.ForwardRefExoticComponent<HudSurfaceProps & React.RefAttributes<HTMLElement>> — Themed panel/window frame over the `HudTheme` frame, elevation, and motion tokens. It enters with a short rise-and-fade and stays mounted through its exit when `open` turns false (`data-state="open" | "closed"`); reduced motion skips both. Padding comes from `--jg-panel-padding`, and the engine rules sit in a cascade layer, so a game's `className` restyles it without `!important`. Not the `HudCanvas` layout slot (`HudPanel` in `@jgengine/react/hudLayout`).
|
|
1428
|
+
- `HudSurfaceElement` (type): type HudSurfaceElement = "div" | "section" | "aside" | "nav" | "header" | "footer" | "article" | "main" | "form" | "dialog" — Block elements a {@link HudSurface} can render as, so it can be a landmark.
|
|
1429
|
+
- `HudSurfaceElevation` (type): type HudSurfaceElevation = "raised" | "overlay" | "modal" — Shadow height of a {@link HudSurface}.
|
|
1430
|
+
- `HudSurfaceProps` (interface): interface HudSurfaceProps extends Omit<HTMLAttributes<HTMLElement>, "title"> — Props for {@link HudSurface}.
|
|
1431
|
+
|
|
1425
1432
|
## @jgengine/react/hudTheme
|
|
1426
1433
|
|
|
1427
1434
|
- `HUD_THEME_PRESETS` (const): const HUD_THEME_PRESETS: { "arcane-stone": HudTheme; "survival-wood": HudTheme; "military-flat": HudTheme; "sleek-hex": HudTheme; "elemental-circle": HudTheme; "sandbox-slot": HudTheme; ember: HudTheme; synthwave: HudTheme; fieldkit: HudTheme; } — Built-in theme presets for demos/scaffold only — not product identity; games author their own theme.
|
|
@@ -1600,13 +1607,13 @@
|
|
|
1600
1607
|
|
|
1601
1608
|
## @jgengine/react/modals
|
|
1602
1609
|
|
|
1603
|
-
- `ConfirmDialog` (function): function ConfirmDialog({ title, body, confirmLabel = "Confirm", cancelLabel = "Cancel", onConfirm, onCancel, danger = false, width = 360, className, style, }: ConfirmDialogProps): ReactNode — A generic two-button confirmation dialog — title, optional body, and Cancel / Confirm buttons over a `HudTheme`-token-driven {@link
|
|
1610
|
+
- `ConfirmDialog` (function): function ConfirmDialog({ title, body, confirmLabel = "Confirm", cancelLabel = "Cancel", onConfirm, onCancel, danger = false, width = 360, className, style, }: ConfirmDialogProps): ReactNode — A generic two-button confirmation dialog — title, optional body, and Cancel / Confirm buttons over a `HudTheme`-token-driven {@link HudSurface}. Presentation-only: wire `onConfirm`/`onCancel` to a modal stack's `resolve` (typically inside a {@link ModalHost} render callback) and reskin via `HudTheme` tokens or `danger` for a destructive action. It interprets nothing — the game supplies the copy.
|
|
1604
1611
|
- `ConfirmDialogProps` (interface): interface ConfirmDialogProps — Props for {@link ConfirmDialog}.
|
|
1605
1612
|
- `ModalControls` (interface): interface ModalControls — Stack-bound controls handed to a {@link ModalHost} render callback.
|
|
1606
1613
|
- `ModalHost` (function): function ModalHost({ stack, children, showBackdrop = true, backdrop, closeOnBackdrop = true, closeOnEsc = true, zIndexBase = 80, onInteract, className, style, }: ModalHostProps): ReactNode — Full-screen modal host: watches a stack, renders only the topmost modal centered over a dimmed backdrop, and manages accessibility — the panel is a `role="dialog" aria-modal`, focus moves into it on open and is trapped with Tab/Shift+Tab cycling, Escape and (optionally) a backdrop click cancel the top modal, and focus returns to the previously focused element on close. It renders nothing when the stack is empty. Skin the panel and backdrop via `HudTheme` tokens.
|
|
1607
1614
|
- `ModalHostProps` (interface): interface ModalHostProps — Props for {@link ModalHost}.
|
|
1608
1615
|
- `ModalStackView` (interface): interface ModalStackView — React chrome over the headless modal stack (`@jgengine/core/ui/modalStack`) — the blocking-overlay layer a game raises over its scene: a pause menu, a "quit to menu?" confirm, a reward popup. {@link useModalStack} is the DATA/HOOK layer (subscribe + the active record), {@link ModalHost} renders the top modal with a backdrop, focus trap, and Esc-to-cancel, and {@link ConfirmDialog} / {@link PauseMenu} are ready-to-wire, HudTheme-token-driven building blocks. All unskinned and game-owned: the model never interprets a modal's `kind`, so a game switches on it to pick which of these (or its own) content to show, and reskins everything via `HudTheme` tokens and the frame.
|
|
1609
|
-
- `PauseMenu` (function): function PauseMenu({ title = "Paused", onResume, resumeLabel = "Resume", items = [], width = 300, className, style, }: PauseMenuProps): ReactNode — A reskinnable pause-menu building block: a primary Resume button plus a game-filled list of slot items (Settings, Save, Quit, …), stacked in a `HudTheme`-token-driven {@link
|
|
1616
|
+
- `PauseMenu` (function): function PauseMenu({ title = "Paused", onResume, resumeLabel = "Resume", items = [], width = 300, className, style, }: PauseMenuProps): ReactNode — A reskinnable pause-menu building block: a primary Resume button plus a game-filled list of slot items (Settings, Save, Quit, …), stacked in a `HudTheme`-token-driven {@link HudSurface}. It ships the common shape every game's pause screen needs and nothing genre-specific — the game supplies the title, item labels/order, and destructive styling, and reskins with `HudTheme` tokens. Drop it into a {@link ModalHost} keyed on a `"pause"` modal, or render it standalone.
|
|
1610
1617
|
- `PauseMenuItem` (interface): interface PauseMenuItem — One selectable row in a {@link PauseMenu}'s slot list (Settings, Save, Quit, …).
|
|
1611
1618
|
- `PauseMenuProps` (interface): interface PauseMenuProps — Props for {@link PauseMenu}.
|
|
1612
1619
|
- `useModalStack` (function): function useModalStack(stack: ModalStack): ModalStackView — Subscribe to a modal stack and re-render on every change, returning the active modal and depth.
|
|
@@ -1615,7 +1622,7 @@
|
|
|
1615
1622
|
|
|
1616
1623
|
- `Notice` (interface): interface Notice — A live notice as the HUD renders it.
|
|
1617
1624
|
- `NoticePlacement` (type): type NoticePlacement = "top" | "center" | "bottom" — Where {@link NoticeStack} sits: below top-edge HUD bars, over the play area, or above bottom action bars.
|
|
1618
|
-
- `NoticeStack` (function): function NoticeStack({ feed, placement = "top", scale = 1, renderNotice, }: { /** Defaults to the surrounding game's `ctx.game.notices`. */ feed?: NoticeFeed; placement?: NoticePlacement; /**
|
|
1625
|
+
- `NoticeStack` (function): function NoticeStack({ feed, placement = "top", offset, avoidHud = true, zIndex = 85, scale = 1, renderNotice, }: { /** Defaults to the surrounding game's `ctx.game.notices`. */ feed?: NoticeFeed; placement?: NoticePlacement; /** Distance from the band's screen edge (px or CSS length), added to the … — Animated, toned notice stack for a {@link NoticeFeed} — the default renderer the shell mounts for `ctx.game.notices` inside the HUD theme root. Uses `--jg-*` theme vars with fallbacks, 14px text, and never takes pointer input. A top or bottom stack keeps clear of HUD panels registered in its column and sits above modal overlays.
|
|
1619
1626
|
- `NoticeTone` (type): type NoticeTone = "info" | "success" | "warn" | "deny" — How a notice reads: neutral info, a win, a caution, or a refused action.
|
|
1620
1627
|
- `useNoticeList` (function): function useNoticeList(feed: NoticeFeed | null): readonly Notice[] — Live notices of `feed`, re-rendering on change and pruning each one when it expires.
|
|
1621
1628
|
|
|
@@ -336,6 +336,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
336
336
|
|
|
337
337
|
- `StatBar` (function) · `import { StatBar } from "@jgengine/react"`
|
|
338
338
|
|
|
339
|
+
## hud-surface — themed panel frame with elevation, header slots, landmark element, and open/close transitions
|
|
340
|
+
|
|
341
|
+
- `HudSurface` (const) · `import { HudSurface } from "@jgengine/react/hudSurface"`
|
|
342
|
+
|
|
339
343
|
## hud-theme — game-authored token object covering bars, frames, slots, chrome, text, status colors, fonts, and rarity
|
|
340
344
|
|
|
341
345
|
- `HudThemePalette` (interface) · `import { HudThemePalette } from "@jgengine/react"`
|
|
@@ -502,7 +506,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
502
506
|
|
|
503
507
|
- `WorldNameplates` (function) · `import { WorldNameplates } from "@jgengine/shell/world/WorldHud"`
|
|
504
508
|
|
|
505
|
-
## notice-feed — render ctx.game.notices as an animated toned stack (enter/exit motion, ×N stacking)
|
|
509
|
+
## notice-feed — render ctx.game.notices as an animated toned stack (enter/exit motion, ×N stacking, HUD avoidance)
|
|
506
510
|
|
|
507
511
|
- `NoticeStack` (function) · `import { NoticeStack } from "@jgengine/react/notices"`
|
|
508
512
|
|
|
@@ -70,7 +70,7 @@ instead of mirroring external entities into `createMarkerSet`.
|
|
|
70
70
|
|
|
71
71
|
**Toggleable game windows** are already shipped, not hand-rolled open/close state: `usePanels(defs)` + `<PanelHost>` + `<Window>` (`@jgengine/react`) over the headless `@jgengine/core/ui/panelModel` are THE way to build openable windows — bag (`B`), character (`C`), spellbook, quest log — with keybind toggles, drag-to-move, close buttons, ESC-to-close, and z-stack ordering all handled. Declare each window in `defs` (id, keybind, title), mount one `<PanelHost>`, and render each panel's body inside a `<Window>`; the game owns the body content and skin, the model owns focus/stacking/keybind wiring. Never re-derive window toggling from local `useState` booleans and hand-rolled keydown listeners.
|
|
72
72
|
|
|
73
|
-
Window title-bar movement belongs to one primary pointer. Release, cancellation, capture loss, blur, and window unmount retire that gesture's listeners; another pointer cannot move it. Controlled positions and panel z-order remain caller-owned. Windows without a `variation` render on `
|
|
73
|
+
Window title-bar movement belongs to one primary pointer. Release, cancellation, capture loss, blur, and window unmount retire that gesture's listeners; another pointer cannot move it. Controlled positions and panel z-order remain caller-owned. Windows without a `variation` render on `HudSurface` (theme frame plus modal elevation) and rise in on open; `PanelHost` keeps a closed window mounted for its short exit transition, and closing retires any drag in progress. `ModalHost` fades its backdrop and lifts the dialog in, and plays the same exit when the stack empties; `ConfirmDialog` and `PauseMenu` are built from `HudSurface` and `HudButton`.
|
|
74
74
|
|
|
75
75
|
**Character sheet / paperdoll** is already shipped, not hand-rolled: `<CharacterSheet>` / `<Paperdoll>` / `<StatList>` (`@jgengine/react`) compose the paperdoll equip layout (worn `EquipmentSlots` arranged around a portrait) and the derived-stat readout into one window body. `Paperdoll` is the equip-slot arrangement, `StatList` the derived-stat rows, `CharacterSheet` the batteries-included default that pairs them — the game composes and reskins them (slot layout, stat labels/terminology, skin) rather than re-deriving a paperdoll from raw divs and manual stat math.
|
|
76
76
|
|