@jgengine/react 0.27.0 → 0.28.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 +130 -0
- package/dist/accessibilitySettings.d.ts +25 -0
- package/dist/accessibilitySettings.js +44 -0
- package/dist/creditsPanel.d.ts +29 -0
- package/dist/creditsPanel.js +35 -0
- package/dist/hudTheme.js +2 -1
- package/dist/hudThemeRoot.d.ts +9 -2
- package/dist/hudThemeRoot.js +11 -4
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/loadingScreen.d.ts +42 -0
- package/dist/loadingScreen.js +108 -0
- package/dist/menuNav.d.ts +63 -0
- package/dist/menuNav.js +257 -0
- package/dist/menuScreen.d.ts +43 -0
- package/dist/menuScreen.js +98 -0
- package/dist/modals.js +13 -1
- package/dist/settingsBlocks.d.ts +85 -0
- package/dist/settingsBlocks.js +204 -0
- package/dist/titleMenu.d.ts +60 -0
- package/dist/titleMenu.js +66 -0
- package/package.json +2 -2
- package/skills/jgengine/api.md +2 -2
- package/skills/jgengine-ui/SKILL.md +1 -0
- package/skills/jgengine-ui/api.md +200 -9
- package/skills/jgengine-ui/capabilities.md +103 -1
- package/skills/jgengine-ui/reference-primitives.md +2 -0
- package/skills/jgengine-ui/reference.md +8 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { hudSpace, hudType, hudTransition } from "./hudTokens.js";
|
|
4
|
+
import { ScrollRail } from "./scrollRail.js";
|
|
5
|
+
import { useDebouncedCommit } from "./useDebouncedCommit.js";
|
|
6
|
+
const rowStyle = {
|
|
7
|
+
display: "flex",
|
|
8
|
+
alignItems: "center",
|
|
9
|
+
justifyContent: "space-between",
|
|
10
|
+
gap: hudSpace("md"),
|
|
11
|
+
padding: `${hudSpace("sm")} ${hudSpace("md")}`,
|
|
12
|
+
borderRadius: 10,
|
|
13
|
+
background: "var(--jg-surface-deep, rgba(255,255,255,0.04))",
|
|
14
|
+
border: "1px solid var(--jg-edge, rgba(255,255,255,0.08))",
|
|
15
|
+
};
|
|
16
|
+
const labelStyle = {
|
|
17
|
+
fontSize: hudType("sm"),
|
|
18
|
+
color: "var(--jg-text, #ececef)",
|
|
19
|
+
};
|
|
20
|
+
const valueStyle = {
|
|
21
|
+
fontSize: hudType("sm"),
|
|
22
|
+
color: "var(--jg-text, #ececef)",
|
|
23
|
+
fontVariantNumeric: "tabular-nums",
|
|
24
|
+
minWidth: 48,
|
|
25
|
+
textAlign: "right",
|
|
26
|
+
};
|
|
27
|
+
function RowShell({ label, children }) {
|
|
28
|
+
return (_jsxs("div", { "data-settings-row": "", style: rowStyle, children: [_jsx("span", { style: labelStyle, children: label }), _jsx("div", { style: { display: "flex", alignItems: "center", gap: hudSpace("sm"), minWidth: 0 }, children: children })] }));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* One slider row from a {@link SettingsRow}: mirrors the thumb locally and commits on release, so a
|
|
32
|
+
* drag never storms the settings store or the graphics rebuild it drives.
|
|
33
|
+
*
|
|
34
|
+
* @capability settings-slider a settings slider row that mirrors locally and persists on release
|
|
35
|
+
*/
|
|
36
|
+
export function SettingsSlider({ row }) {
|
|
37
|
+
const { value, onInput, flush } = useDebouncedCommit(Number(row.value), (next) => row.set(next));
|
|
38
|
+
return (_jsxs(RowShell, { label: row.label, children: [_jsx("input", { type: "range", min: row.min, max: row.max, step: row.step, value: value, "aria-label": row.label, onChange: (event) => onInput(Number(event.target.value)), onPointerUp: flush, onKeyUp: flush, onBlur: flush, style: { flex: 1, minWidth: 120, accentColor: "var(--jg-accent, #6ee7a8)" } }), _jsx("span", { style: valueStyle, children: row.format?.(value) ?? String(value) })] }));
|
|
39
|
+
}
|
|
40
|
+
/** One on/off row from a {@link SettingsRow}. */
|
|
41
|
+
export function SettingsToggle({ row }) {
|
|
42
|
+
const on = Boolean(row.value);
|
|
43
|
+
return (_jsx(RowShell, { label: row.label, children: _jsx("button", { type: "button", role: "switch", "aria-checked": on, "aria-label": row.label, onClick: () => row.set(!on), style: {
|
|
44
|
+
position: "relative",
|
|
45
|
+
width: 48,
|
|
46
|
+
height: 26,
|
|
47
|
+
borderRadius: 999,
|
|
48
|
+
border: "1px solid var(--jg-edge-bright, rgba(255,255,255,0.16))",
|
|
49
|
+
background: on ? "var(--jg-accent, #6ee7a8)" : "var(--jg-surface-deep, rgba(15,18,24,0.9))",
|
|
50
|
+
boxShadow: on ? "0 0 12px var(--jg-accent-glow, rgba(110,231,168,0.35))" : "none",
|
|
51
|
+
cursor: "pointer",
|
|
52
|
+
transition: hudTransition(["background", "box-shadow"]),
|
|
53
|
+
}, children: _jsx("span", { style: {
|
|
54
|
+
position: "absolute",
|
|
55
|
+
top: 3,
|
|
56
|
+
left: on ? 25 : 3,
|
|
57
|
+
width: 18,
|
|
58
|
+
height: 18,
|
|
59
|
+
borderRadius: 999,
|
|
60
|
+
background: "var(--jg-text, #fff)",
|
|
61
|
+
transition: hudTransition("left"),
|
|
62
|
+
} }) }) }));
|
|
63
|
+
}
|
|
64
|
+
/** One choice row from a {@link SettingsRow}, rendered as a pressed-state option group. */
|
|
65
|
+
export function SettingsSelect({ row }) {
|
|
66
|
+
const current = String(row.value);
|
|
67
|
+
return (_jsx(RowShell, { label: row.label, children: _jsx("div", { role: "group", "aria-label": row.label, style: { display: "flex", flexWrap: "wrap", justifyContent: "flex-end", gap: 4 }, children: (row.options ?? []).map((option) => {
|
|
68
|
+
const selected = option.value === current;
|
|
69
|
+
return (_jsx("button", { type: "button", "aria-pressed": selected, onClick: () => row.set(option.value), style: {
|
|
70
|
+
minHeight: 30,
|
|
71
|
+
padding: `0 ${hudSpace("sm")}`,
|
|
72
|
+
borderRadius: 7,
|
|
73
|
+
border: "1px solid transparent",
|
|
74
|
+
background: selected ? "var(--jg-accent, #6ee7a8)" : "transparent",
|
|
75
|
+
color: selected ? "var(--jg-surface-deep, #0b0b0f)" : "var(--jg-text-dim, #94a3a0)",
|
|
76
|
+
fontSize: hudType("sm"),
|
|
77
|
+
fontWeight: 600,
|
|
78
|
+
cursor: "pointer",
|
|
79
|
+
transition: hudTransition(["background", "color"]),
|
|
80
|
+
}, children: option.label }, option.value));
|
|
81
|
+
}) }) }));
|
|
82
|
+
}
|
|
83
|
+
/** Render the control a {@link SettingsRow} kind asks for. */
|
|
84
|
+
export function SettingsRowControl({ row }) {
|
|
85
|
+
if (row.kind === "slider")
|
|
86
|
+
return _jsx(SettingsSlider, { row: row });
|
|
87
|
+
if (row.kind === "toggle")
|
|
88
|
+
return _jsx(SettingsToggle, { row: row });
|
|
89
|
+
return _jsx(SettingsSelect, { row: row });
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A controls row for one action: its label, the bound key (click to rebind), a conflict/armed state
|
|
93
|
+
* and reset. The caller owns capture; this is the row's presentation.
|
|
94
|
+
*
|
|
95
|
+
* @capability settings-keybind-row one rebindable controls row — key glyph, armed state and reset
|
|
96
|
+
*/
|
|
97
|
+
export function SettingsKeybindControl({ row, armed = false, onArm, buttonProps }) {
|
|
98
|
+
return (_jsxs("div", { "data-settings-keybind": row.action, "data-armed": armed ? "" : undefined, style: rowStyle, children: [_jsx("span", { style: labelStyle, children: row.label }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: hudSpace("xs") }, children: [_jsx("button", { type: "button", "data-settings-capture": "", ...buttonProps, onClick: onArm, disabled: armed, title: "Click, then press a key \u2014 Escape cancels", style: {
|
|
99
|
+
minWidth: 84,
|
|
100
|
+
minHeight: 30,
|
|
101
|
+
padding: `0 ${hudSpace("sm")}`,
|
|
102
|
+
borderRadius: 7,
|
|
103
|
+
border: `1px solid ${armed ? "var(--jg-accent, #6ee7a8)" : "var(--jg-edge-bright, rgba(255,255,255,0.14))"}`,
|
|
104
|
+
background: armed ? "var(--jg-accent-glow, rgba(110,231,168,0.25))" : "var(--jg-surface-deep, rgba(15,18,24,0.9))",
|
|
105
|
+
color: armed ? "var(--jg-accent, #6ee7a8)" : "var(--jg-text, #ececef)",
|
|
106
|
+
fontFamily: "var(--jg-font-numeric, monospace)",
|
|
107
|
+
fontSize: hudType("sm"),
|
|
108
|
+
cursor: "pointer",
|
|
109
|
+
}, children: armed ? "Press…" : row.bindingLabel }), _jsx("button", { type: "button", "data-settings-reset": "", onClick: () => row.reset(), "aria-label": `Reset ${row.label}`, tabIndex: row.isDefault ? -1 : 0, style: {
|
|
110
|
+
width: 30,
|
|
111
|
+
height: 30,
|
|
112
|
+
borderRadius: 7,
|
|
113
|
+
border: "none",
|
|
114
|
+
background: "transparent",
|
|
115
|
+
color: "var(--jg-text-dim, #94a3a0)",
|
|
116
|
+
visibility: row.isDefault ? "hidden" : "visible",
|
|
117
|
+
cursor: "pointer",
|
|
118
|
+
}, children: "\u21BA" })] })] }));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* One settings category as a block: its rows, then its rebindable actions with click-to-capture. The
|
|
122
|
+
* game picks where it sits and how wide it is; the block reads HudTheme tokens and the live rows the
|
|
123
|
+
* settings controller hands it, and writes every change straight back to that controller's store.
|
|
124
|
+
*
|
|
125
|
+
* @capability settings-section settings category block — rows plus click-to-capture keybind rows, HudTheme-styled, store-backed
|
|
126
|
+
*/
|
|
127
|
+
export function SettingsSection({ category, heading = true, capture = true, className, style, onCaptureChange, }) {
|
|
128
|
+
const [armed, setArmed] = useState(null);
|
|
129
|
+
const callbacks = useRef({ onCaptureChange });
|
|
130
|
+
callbacks.current = { onCaptureChange };
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
callbacks.current.onCaptureChange?.(armed);
|
|
133
|
+
}, [armed]);
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (armed === null)
|
|
136
|
+
return;
|
|
137
|
+
const row = category.keybinds.find((candidate) => candidate.action === armed);
|
|
138
|
+
if (row === undefined) {
|
|
139
|
+
setArmed(null);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const onKey = (event) => {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
event.stopPropagation();
|
|
145
|
+
if (event.code === "Escape") {
|
|
146
|
+
setArmed(null);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
row.rebind(event.code);
|
|
150
|
+
setArmed(null);
|
|
151
|
+
};
|
|
152
|
+
const onPointer = (event) => {
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
event.stopPropagation();
|
|
155
|
+
row.rebind(`mouse${event.button}`);
|
|
156
|
+
setArmed(null);
|
|
157
|
+
};
|
|
158
|
+
const onContext = (event) => event.preventDefault();
|
|
159
|
+
window.addEventListener("keydown", onKey, true);
|
|
160
|
+
window.addEventListener("pointerdown", onPointer, true);
|
|
161
|
+
window.addEventListener("contextmenu", onContext, true);
|
|
162
|
+
return () => {
|
|
163
|
+
window.removeEventListener("keydown", onKey, true);
|
|
164
|
+
window.removeEventListener("pointerdown", onPointer, true);
|
|
165
|
+
window.removeEventListener("contextmenu", onContext, true);
|
|
166
|
+
};
|
|
167
|
+
}, [armed, category.keybinds]);
|
|
168
|
+
return (_jsxs("section", { "data-settings-section": category.id, className: className, style: { display: "flex", flexDirection: "column", gap: hudSpace("xs"), ...style }, children: [heading ? (_jsx("h3", { style: {
|
|
169
|
+
margin: 0,
|
|
170
|
+
marginBottom: hudSpace("xs"),
|
|
171
|
+
fontFamily: "var(--jg-font-display, inherit)",
|
|
172
|
+
fontSize: hudType("xs"),
|
|
173
|
+
fontWeight: 700,
|
|
174
|
+
letterSpacing: "0.18em",
|
|
175
|
+
textTransform: "uppercase",
|
|
176
|
+
color: "var(--jg-accent, #6ee7a8)",
|
|
177
|
+
}, children: category.label })) : null, category.rows.map((row) => (_jsx(SettingsRowControl, { row: row }, row.id))), category.keybinds.map((row) => (_jsx(SettingsKeybindControl, { row: row, armed: armed === row.action, onArm: capture ? () => setArmed(row.action) : undefined }, row.action)))] }));
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The category tab rail for a settings screen: a scrollable row of buttons with pressed state. The
|
|
181
|
+
* game owns the surrounding layout; this block keeps every category reachable on narrow screens.
|
|
182
|
+
*
|
|
183
|
+
* @capability settings-tabs scrollable settings category tabs with pressed state
|
|
184
|
+
*/
|
|
185
|
+
export function SettingsTabs({ categories, activeId, onSelect, leading, tabProps, className, style }) {
|
|
186
|
+
const tabs = leading === undefined ? categories : [{ id: leading.id, label: leading.label }, ...categories];
|
|
187
|
+
return (_jsx("div", { className: className, style: style, children: _jsx(ScrollRail, { label: "settings categories", children: tabs.map((tab) => {
|
|
188
|
+
const selected = tab.id === activeId;
|
|
189
|
+
return (_jsx("button", { type: "button", "data-settings-tab": tab.id, ...(tabProps === undefined ? {} : tabProps(tab.id)), "aria-pressed": selected, onClick: () => onSelect(tab.id), style: {
|
|
190
|
+
flexShrink: 0,
|
|
191
|
+
minHeight: 34,
|
|
192
|
+
padding: `0 ${hudSpace("md")}`,
|
|
193
|
+
marginRight: hudSpace("xs"),
|
|
194
|
+
borderRadius: 8,
|
|
195
|
+
border: "1px solid transparent",
|
|
196
|
+
background: selected ? "var(--jg-accent, #6ee7a8)" : "transparent",
|
|
197
|
+
color: selected ? "var(--jg-surface-deep, #0b0b0f)" : "var(--jg-text-dim, #94a3a0)",
|
|
198
|
+
fontSize: hudType("sm"),
|
|
199
|
+
fontWeight: 600,
|
|
200
|
+
whiteSpace: "nowrap",
|
|
201
|
+
cursor: "pointer",
|
|
202
|
+
}, children: tab.label }, tab.id));
|
|
203
|
+
}) }) }));
|
|
204
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { type MenuScreenScrim, type MenuScreenTransition } from "./menuScreen.js";
|
|
3
|
+
/** One verb on a {@link TitleMenu}: New Game, Continue, Settings, Credits, Quit. */
|
|
4
|
+
export interface TitleMenuItem {
|
|
5
|
+
/** Stable id the game routes on. */
|
|
6
|
+
id: string;
|
|
7
|
+
/** Player-facing label. */
|
|
8
|
+
label: string;
|
|
9
|
+
/** One line under the label (save summary, a "no save yet" note). */
|
|
10
|
+
hint?: string;
|
|
11
|
+
/** Render as the screen's primary action. */
|
|
12
|
+
primary?: boolean;
|
|
13
|
+
/** Show but skip it in navigation and clicks. */
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Props for {@link TitleMenu}. */
|
|
17
|
+
export interface TitleMenuProps {
|
|
18
|
+
/** The game's wordmark or title. Free markup, so a game can art-direct it. */
|
|
19
|
+
title: ReactNode;
|
|
20
|
+
/** Small line above the title (studio, genre, chapter). */
|
|
21
|
+
eyebrow?: ReactNode;
|
|
22
|
+
/** One line under the title. */
|
|
23
|
+
tagline?: ReactNode;
|
|
24
|
+
/** The front-end verbs, in focus order. */
|
|
25
|
+
items: readonly TitleMenuItem[];
|
|
26
|
+
/** A verb was chosen. */
|
|
27
|
+
onSelect: (id: string) => void;
|
|
28
|
+
/** Back/Escape. Omit at the root screen, where there is nowhere to go. */
|
|
29
|
+
onBack?: () => void;
|
|
30
|
+
/** Footer left slot — build version, copyright. */
|
|
31
|
+
footer?: ReactNode;
|
|
32
|
+
/** Footer right slot — replaces the default control hint. */
|
|
33
|
+
controlHint?: ReactNode;
|
|
34
|
+
/** Artwork, a poster, or a live 3D scene painted behind the menu. */
|
|
35
|
+
backdrop?: ReactNode;
|
|
36
|
+
/** Extra content under the verbs (a save summary, a control legend). */
|
|
37
|
+
children?: ReactNode;
|
|
38
|
+
open?: boolean;
|
|
39
|
+
transition?: MenuScreenTransition;
|
|
40
|
+
/** Dim/blur treatment over the scene behind. Default `"dim"`. */
|
|
41
|
+
scrim?: MenuScreenScrim;
|
|
42
|
+
enterMs?: number;
|
|
43
|
+
exitMs?: number;
|
|
44
|
+
reducedMotion?: boolean;
|
|
45
|
+
onClosed?: () => void;
|
|
46
|
+
className?: string;
|
|
47
|
+
style?: CSSProperties;
|
|
48
|
+
contentClassName?: string;
|
|
49
|
+
contentStyle?: CSSProperties;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A game front-end title screen: wordmark, tagline, the game's verbs in a keyboard/gamepad-navigable
|
|
53
|
+
* list, a footer and an optional backdrop. The engine owns navigation, focus and the enter/exit
|
|
54
|
+
* animation; the game owns the title markup, wording, verbs, art and every colour through HudTheme
|
|
55
|
+
* tokens. Pass the live scene (or artwork) as `backdrop` so the title renders over 3D instead of a
|
|
56
|
+
* blank page.
|
|
57
|
+
*
|
|
58
|
+
* @capability title-menu title-screen block — wordmark, tagline, navigable verb list, footer, backdrop slot, animated over a live scene
|
|
59
|
+
*/
|
|
60
|
+
export declare function TitleMenu({ title, eyebrow, tagline, items, onSelect, onBack, footer, controlHint, backdrop, children, open, transition, scrim, enterMs, exitMs, reducedMotion, onClosed, className, style, contentClassName, contentStyle, }: TitleMenuProps): ReactNode;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { hudSpace, hudType } from "./hudTokens.js";
|
|
3
|
+
import { HudButton } from "./hudButton.js";
|
|
4
|
+
import { useMenuNav } from "./menuNav.js";
|
|
5
|
+
import { MenuScreen } from "./menuScreen.js";
|
|
6
|
+
const column = {
|
|
7
|
+
display: "flex",
|
|
8
|
+
flexDirection: "column",
|
|
9
|
+
gap: hudSpace("md"),
|
|
10
|
+
width: "min(92vw, 460px)",
|
|
11
|
+
};
|
|
12
|
+
const itemButton = {
|
|
13
|
+
display: "flex",
|
|
14
|
+
flexDirection: "column",
|
|
15
|
+
alignItems: "flex-start",
|
|
16
|
+
gap: 2,
|
|
17
|
+
textAlign: "left",
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A game front-end title screen: wordmark, tagline, the game's verbs in a keyboard/gamepad-navigable
|
|
21
|
+
* list, a footer and an optional backdrop. The engine owns navigation, focus and the enter/exit
|
|
22
|
+
* animation; the game owns the title markup, wording, verbs, art and every colour through HudTheme
|
|
23
|
+
* tokens. Pass the live scene (or artwork) as `backdrop` so the title renders over 3D instead of a
|
|
24
|
+
* blank page.
|
|
25
|
+
*
|
|
26
|
+
* @capability title-menu title-screen block — wordmark, tagline, navigable verb list, footer, backdrop slot, animated over a live scene
|
|
27
|
+
*/
|
|
28
|
+
export function TitleMenu({ title, eyebrow, tagline, items, onSelect, onBack, footer, controlHint, backdrop, children, open = true, transition = "slide", scrim = "dim", enterMs, exitMs, reducedMotion, onClosed, className, style, contentClassName, contentStyle, }) {
|
|
29
|
+
const disabled = items.filter((item) => item.disabled === true).map((item) => item.id);
|
|
30
|
+
const nav = useMenuNav({
|
|
31
|
+
ids: items.map((item) => item.id),
|
|
32
|
+
disabled,
|
|
33
|
+
enabled: open,
|
|
34
|
+
onConfirm: onSelect,
|
|
35
|
+
...(onBack === undefined ? {} : { onBack }),
|
|
36
|
+
});
|
|
37
|
+
return (_jsx(MenuScreen, { open: open, transition: transition, scrim: scrim, ...(enterMs === undefined ? {} : { enterMs }), ...(exitMs === undefined ? {} : { exitMs }), ...(reducedMotion === undefined ? {} : { reducedMotion }), ...(onClosed === undefined ? {} : { onClosed }), label: "Title menu", ...(backdrop === undefined ? {} : { backdrop }), className: className, style: style, contentClassName: contentClassName, contentStyle: contentStyle, children: _jsxs("div", { "data-title-menu": "", style: column, ...nav.containerProps, children: [eyebrow === undefined ? null : (_jsx("p", { "data-title-eyebrow": "", style: {
|
|
38
|
+
margin: 0,
|
|
39
|
+
fontSize: hudType("xs"),
|
|
40
|
+
letterSpacing: "0.24em",
|
|
41
|
+
textTransform: "uppercase",
|
|
42
|
+
color: "var(--jg-accent, #6ee7a8)",
|
|
43
|
+
}, children: eyebrow })), _jsx("div", { "data-title-wordmark": "", style: {
|
|
44
|
+
fontFamily: "var(--jg-font-display, inherit)",
|
|
45
|
+
fontSize: hudType("display"),
|
|
46
|
+
fontWeight: 800,
|
|
47
|
+
lineHeight: 1.02,
|
|
48
|
+
letterSpacing: "-0.01em",
|
|
49
|
+
color: "var(--jg-text, #f5f7fa)",
|
|
50
|
+
textShadow: "0 2px 18px rgba(0,0,0,0.55)",
|
|
51
|
+
}, children: title }), tagline === undefined ? null : (_jsx("p", { style: { margin: 0, fontSize: hudType("sm"), color: "var(--jg-text-dim, #94a3a0)", maxWidth: "34ch" }, children: tagline })), _jsx("nav", { "data-title-items": "", style: { display: "flex", flexDirection: "column", gap: hudSpace("xs"), marginTop: hudSpace("sm") }, children: items.map((item) => {
|
|
52
|
+
const props = nav.itemProps(item.id);
|
|
53
|
+
return (_jsxs(HudButton, { ...props, variant: item.primary === true ? "primary" : "secondary", size: "lg", block: true, disabled: item.disabled === true, "data-title-item": item.id, onClick: () => {
|
|
54
|
+
if (item.disabled !== true)
|
|
55
|
+
onSelect(item.id);
|
|
56
|
+
}, style: itemButton, children: [_jsx("span", { style: { fontSize: hudType("md"), fontWeight: 700 }, children: item.label }), item.hint === undefined ? null : (_jsx("span", { style: { fontSize: hudType("xs"), fontWeight: 500, opacity: 0.78 }, children: item.hint }))] }, item.id));
|
|
57
|
+
}) }), children, _jsxs("footer", { "data-title-footer": "", style: {
|
|
58
|
+
display: "flex",
|
|
59
|
+
alignItems: "center",
|
|
60
|
+
justifyContent: "space-between",
|
|
61
|
+
gap: hudSpace("md"),
|
|
62
|
+
marginTop: hudSpace("md"),
|
|
63
|
+
fontSize: hudType("xs"),
|
|
64
|
+
color: "var(--jg-text-dim, #94a3a0)",
|
|
65
|
+
}, children: [_jsx("span", { children: footer }), _jsx("span", { "data-jg-kbd-hint": controlHint === undefined ? "" : undefined, children: controlHint ?? "↑↓ move · Enter select · Esc back" })] })] }) }));
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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.28.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.28.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
10
10
|
|
|
11
11
|
## @jgengine/core/authoring
|
|
12
12
|
|
|
@@ -285,7 +285,7 @@
|
|
|
285
285
|
|
|
286
286
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
287
287
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
288
|
-
- `VERSION` (const): const VERSION: "0.
|
|
288
|
+
- `VERSION` (const): const VERSION: "0.28.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
289
289
|
|
|
290
290
|
## @jgengine/core/runtime/adapter
|
|
291
291
|
|
|
@@ -29,6 +29,7 @@ Existing React games keep their entity store and use the focused
|
|
|
29
29
|
|
|
30
30
|
- **Every game owns its UI end-to-end.** Custom composition, skin, placement, terminology, motion, and one main menu. The website/runner is a bare loader. Composing and reskinning the shipped blocks is correct use; a genre HUD kit, theme preset, or unarranged default stack dropped in as the game's identity is not.
|
|
31
31
|
- **The main menu is a game front-end, not marketing.** The runner loads straight into the game's own menu; it offers the real front-end verbs the game supports — New Game, Continue/Load (save slots), Create/Choose character or class, Settings, Credits — each an authored screen in the game's art direction, not a landing page or raw scaffold. See [reference.md](reference.md#main-menu).
|
|
32
|
+
- **Menus come from the shipped blocks, navigated by `useMenuNav`.** `TitleMenu`/`MenuScreen` (`@jgengine/react`) give the animated screen over the live scene (transform/opacity only, reduced-motion aware, unmounted after exit), `PauseMenu` the pause shape, `SettingsSection`/`SettingsTabs` the settings surface, `SaveSlotMenu` the slot list, `CreditsPanel` the credits screen, `LoadingScreen` the boot screen with rotating tips. `useMenuNav` (`@jgengine/react/menuNav`) is the one keyboard + gamepad focus model for any menu: roving focus, confirm/back, disabled rows skipped, no focus trap. Games keep wording, layout, art and terminology.
|
|
32
33
|
- **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
34
|
- **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
35
|
- **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.
|