@jgengine/react 0.37.0 → 0.38.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 +63 -0
- package/dist/engineStyles.js +5 -0
- package/dist/gameMenu.d.ts +120 -0
- package/dist/gameMenu.js +390 -0
- package/dist/gameMenuSaves.d.ts +159 -0
- package/dist/gameMenuSaves.js +468 -0
- package/dist/gameMenuSound.d.ts +41 -0
- package/dist/gameMenuSound.js +82 -0
- package/dist/hudTheme.js +4 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/saveThumbnail.d.ts +22 -0
- package/dist/saveThumbnail.js +43 -0
- package/dist/styles.css +5 -0
- package/package.json +2 -2
- package/skills/jgengine/api.md +2 -2
- package/skills/jgengine-ui/SKILL.md +1 -1
- package/skills/jgengine-ui/api.md +68 -0
- package/skills/jgengine-ui/capabilities.md +40 -0
- package/skills/jgengine-ui/reference.md +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The short, quiet click a menu makes when the selection moves or is confirmed. Generated with an
|
|
3
|
+
* oscillator instead of shipped as an audio file, so a game gets menu feedback without authoring a
|
|
4
|
+
* sound and the whole thing unmounts with the screen.
|
|
5
|
+
*/
|
|
6
|
+
/** Which feedback sound to play. */
|
|
7
|
+
export type GameMenuSoundKind = "move" | "confirm" | "back";
|
|
8
|
+
/** The sound player's readable state: on/off, gain, and whether audio is actually running. */
|
|
9
|
+
export interface GameMenuSoundState {
|
|
10
|
+
/** Whether the player will make sound. */
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
/** Peak gain per tick. */
|
|
13
|
+
volume: number;
|
|
14
|
+
/** Whether an audio context exists and is running - what a test or a settings preview reads. */
|
|
15
|
+
ready: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** A menu sound player. Give it to `GameMenu`'s `sound` prop, or call its methods from your own menu. */
|
|
18
|
+
export interface GameMenuSound {
|
|
19
|
+
/** Play one tick. A no-op when audio is unavailable or the browser blocked it. */
|
|
20
|
+
play(kind: GameMenuSoundKind): void;
|
|
21
|
+
/** Release the audio context. Safe to call twice. */
|
|
22
|
+
dispose(): void;
|
|
23
|
+
/** Read the player state without starting audio. */
|
|
24
|
+
state(): GameMenuSoundState;
|
|
25
|
+
/** Apply a saved player state - a settings toggle muting the menu without rebuilding it. */
|
|
26
|
+
restore(next: GameMenuSoundState): void;
|
|
27
|
+
}
|
|
28
|
+
/** How {@link createGameMenuSound} is tuned. */
|
|
29
|
+
export interface GameMenuSoundConfig {
|
|
30
|
+
/** Turn sound off entirely. Default true. */
|
|
31
|
+
enabled?: boolean;
|
|
32
|
+
/** Peak gain per tick. Default 0.045 — audible, never a jingle. */
|
|
33
|
+
volume?: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A tiny WebAudio click generator for menu feedback: one oscillator burst per tick, created on the
|
|
37
|
+
* first play (so a browser autoplay policy does not log an error at mount) and reused after.
|
|
38
|
+
*
|
|
39
|
+
* @capability menu-sound generated click for menu move/confirm/back, no audio asset
|
|
40
|
+
*/
|
|
41
|
+
export declare function createGameMenuSound(config?: GameMenuSoundConfig): GameMenuSound;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The short, quiet click a menu makes when the selection moves or is confirmed. Generated with an
|
|
3
|
+
* oscillator instead of shipped as an audio file, so a game gets menu feedback without authoring a
|
|
4
|
+
* sound and the whole thing unmounts with the screen.
|
|
5
|
+
*/
|
|
6
|
+
const TONES = {
|
|
7
|
+
move: { frequency: 880, ms: 26, gain: 1 },
|
|
8
|
+
confirm: { frequency: 1320, ms: 52, gain: 1.25 },
|
|
9
|
+
back: { frequency: 587, ms: 40, gain: 0.9 },
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* A tiny WebAudio click generator for menu feedback: one oscillator burst per tick, created on the
|
|
13
|
+
* first play (so a browser autoplay policy does not log an error at mount) and reused after.
|
|
14
|
+
*
|
|
15
|
+
* @capability menu-sound generated click for menu move/confirm/back, no audio asset
|
|
16
|
+
*/
|
|
17
|
+
export function createGameMenuSound(config = {}) {
|
|
18
|
+
let enabled = config.enabled ?? true;
|
|
19
|
+
let volume = config.volume ?? 0.045;
|
|
20
|
+
let context = null;
|
|
21
|
+
let broken = false;
|
|
22
|
+
function ensure() {
|
|
23
|
+
if (!enabled || broken)
|
|
24
|
+
return null;
|
|
25
|
+
if (context !== null)
|
|
26
|
+
return context;
|
|
27
|
+
const Ctor = typeof globalThis.AudioContext === "function"
|
|
28
|
+
? globalThis.AudioContext
|
|
29
|
+
: globalThis.webkitAudioContext;
|
|
30
|
+
if (Ctor === undefined) {
|
|
31
|
+
broken = true;
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
context = new Ctor();
|
|
36
|
+
return context;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
broken = true;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
play(kind) {
|
|
45
|
+
const audio = ensure();
|
|
46
|
+
if (audio === null)
|
|
47
|
+
return;
|
|
48
|
+
if (audio.state === "suspended")
|
|
49
|
+
void audio.resume().catch(() => { });
|
|
50
|
+
const tone = TONES[kind];
|
|
51
|
+
const start = audio.currentTime;
|
|
52
|
+
const end = start + tone.ms / 1000;
|
|
53
|
+
const oscillator = audio.createOscillator();
|
|
54
|
+
const gain = audio.createGain();
|
|
55
|
+
oscillator.type = "triangle";
|
|
56
|
+
oscillator.frequency.setValueAtTime(tone.frequency, start);
|
|
57
|
+
oscillator.frequency.exponentialRampToValueAtTime(tone.frequency * 1.18, end);
|
|
58
|
+
gain.gain.setValueAtTime(0.0001, start);
|
|
59
|
+
gain.gain.exponentialRampToValueAtTime(volume * tone.gain, start + 0.006);
|
|
60
|
+
gain.gain.exponentialRampToValueAtTime(0.0001, end);
|
|
61
|
+
oscillator.connect(gain);
|
|
62
|
+
gain.connect(audio.destination);
|
|
63
|
+
oscillator.start(start);
|
|
64
|
+
oscillator.stop(end + 0.01);
|
|
65
|
+
},
|
|
66
|
+
state() {
|
|
67
|
+
return { enabled, volume, ready: context !== null && context.state === "running" };
|
|
68
|
+
},
|
|
69
|
+
restore(next) {
|
|
70
|
+
enabled = next.enabled;
|
|
71
|
+
volume = next.volume;
|
|
72
|
+
if (!enabled)
|
|
73
|
+
this.dispose();
|
|
74
|
+
},
|
|
75
|
+
dispose() {
|
|
76
|
+
const audio = context;
|
|
77
|
+
context = null;
|
|
78
|
+
if (audio !== null)
|
|
79
|
+
void audio.close().catch(() => { });
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
package/dist/hudTheme.js
CHANGED
|
@@ -105,6 +105,10 @@ export function hudThemeVars(theme) {
|
|
|
105
105
|
"--jg-frame-bg": frame.bg,
|
|
106
106
|
"--jg-frame-border": frame.border,
|
|
107
107
|
"--jg-frame-radius": frame.radius,
|
|
108
|
+
// The game menu's two design tokens: a logo sized off the display step and card corners that
|
|
109
|
+
// follow the frame radius, so a menu inherits the theme it is rendered under.
|
|
110
|
+
"--jg-menu-logo-size": "clamp(30px, 4.6vw, 66px)",
|
|
111
|
+
"--jg-menu-card-radius": frame.radius,
|
|
108
112
|
"--jg-frame-glow": frame.glow,
|
|
109
113
|
"--jg-slot-bg": slot.bg,
|
|
110
114
|
"--jg-slot-border": slot.border,
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a rendered frame into the thumbnail a save card shows. Saves keep a small JPEG/WebP data
|
|
3
|
+
* URL (a couple of KB), not a full-resolution PNG, so a slot index still fits in local storage.
|
|
4
|
+
*/
|
|
5
|
+
/** How {@link shrinkCapture} sizes and encodes a capture. */
|
|
6
|
+
export interface SaveThumbnailOptions {
|
|
7
|
+
/** Output width in pixels. Default 320. */
|
|
8
|
+
width?: number;
|
|
9
|
+
/** Output height in pixels. Default 180 (16:9). */
|
|
10
|
+
height?: number;
|
|
11
|
+
/** Encoding quality, 0–1. Default 0.6. */
|
|
12
|
+
quality?: number;
|
|
13
|
+
/** Output format. Default `"image/jpeg"` — smallest for a photo-like frame. */
|
|
14
|
+
type?: "image/jpeg" | "image/webp";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Downscale a captured frame (a data URL from `captureCanvas`) to a save-card thumbnail. Resolves to
|
|
18
|
+
* the thumbnail data URL, or `null` when the capture could not be decoded (a blank or tainted frame).
|
|
19
|
+
*
|
|
20
|
+
* @capability shrink-capture downscale a captured frame to a save-card thumbnail data URL
|
|
21
|
+
*/
|
|
22
|
+
export declare function shrinkCapture(capture: string | null, options?: SaveThumbnailOptions): Promise<string | null>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a rendered frame into the thumbnail a save card shows. Saves keep a small JPEG/WebP data
|
|
3
|
+
* URL (a couple of KB), not a full-resolution PNG, so a slot index still fits in local storage.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Downscale a captured frame (a data URL from `captureCanvas`) to a save-card thumbnail. Resolves to
|
|
7
|
+
* the thumbnail data URL, or `null` when the capture could not be decoded (a blank or tainted frame).
|
|
8
|
+
*
|
|
9
|
+
* @capability shrink-capture downscale a captured frame to a save-card thumbnail data URL
|
|
10
|
+
*/
|
|
11
|
+
export async function shrinkCapture(capture, options = {}) {
|
|
12
|
+
if (capture === null || !capture.startsWith("data:image"))
|
|
13
|
+
return null;
|
|
14
|
+
if (typeof document === "undefined")
|
|
15
|
+
return null;
|
|
16
|
+
const width = options.width ?? 320;
|
|
17
|
+
const height = options.height ?? 180;
|
|
18
|
+
const image = new Image();
|
|
19
|
+
image.decoding = "sync";
|
|
20
|
+
const loaded = await new Promise((resolve) => {
|
|
21
|
+
image.onload = () => resolve(true);
|
|
22
|
+
image.onerror = () => resolve(false);
|
|
23
|
+
image.src = capture;
|
|
24
|
+
});
|
|
25
|
+
if (!loaded)
|
|
26
|
+
return null;
|
|
27
|
+
const canvas = document.createElement("canvas");
|
|
28
|
+
canvas.width = width;
|
|
29
|
+
canvas.height = height;
|
|
30
|
+
const context = canvas.getContext("2d");
|
|
31
|
+
if (context === null)
|
|
32
|
+
return null;
|
|
33
|
+
const scale = Math.max(width / image.width, height / image.height);
|
|
34
|
+
const drawWidth = image.width * scale;
|
|
35
|
+
const drawHeight = image.height * scale;
|
|
36
|
+
context.drawImage(image, (width - drawWidth) / 2, (height - drawHeight) / 2, drawWidth, drawHeight);
|
|
37
|
+
try {
|
|
38
|
+
return canvas.toDataURL(options.type ?? "image/jpeg", options.quality ?? 0.6);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
package/dist/styles.css
CHANGED
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
@keyframes jg-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
26
26
|
@keyframes jg-delta-up { 0% { color: var(--jg-success, #3fa34d); text-shadow: 0 0 10px var(--jg-success, #3fa34d); scale: 1.12; } 100% { scale: 1; } }
|
|
27
27
|
@keyframes jg-delta-down { 0% { color: var(--jg-danger, #e5484d); text-shadow: 0 0 10px var(--jg-danger, #e5484d); scale: 0.94; } 100% { scale: 1; } }
|
|
28
|
+
@keyframes jg-menu-drift { from { transform: scale(1.06) translate3d(-1.4%, -1%, 0); } to { transform: scale(1.12) translate3d(1.6%, 1.2%, 0); } }
|
|
29
|
+
@keyframes jg-menu-rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } }
|
|
30
|
+
@keyframes jg-menu-pane-in { from { opacity: 0; transform: translateX(14px); } to { opacity: 1; transform: translateX(0); } }
|
|
31
|
+
[data-jg-game-menu] * { scrollbar-width: none; }
|
|
32
|
+
[data-jg-game-menu] *::-webkit-scrollbar { width: 0; height: 0; }
|
|
28
33
|
|
|
29
34
|
[data-jg-hud] { 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); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; }
|
|
30
35
|
:where([data-jg-hud]) :where(button, [role="button"], a[href], summary, [data-jg-interactive]):focus-visible { outline: 2px solid var(--jg-accent, #38bdf8); outline-offset: 2px; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.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.38.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.38.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.38.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
289
289
|
|
|
290
290
|
## @jgengine/core/runtime/adapter
|
|
291
291
|
|
|
@@ -29,7 +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`)
|
|
32
|
+
- **Menus come from the shipped blocks, navigated by `useMenuNav`.** `GameMenu` (`@jgengine/react/gameMenu`) is the front-end surface: full-bleed over a live scene or key art, a logo sized by `--jg-menu-logo-size`, animated primary verbs with a generated selection tick, and a detail pane that follows the highlight — with `ContinuePane`, `NewCharacterPane`, `SaveSlotsPane` (cards with thumbnails), `CreditsPane` and the game's own settings surface as its panes, and `GamePauseMenu` the same system sized down. It locks the document scroll, so the surface is exactly the viewport at any size. `TitleMenu`/`MenuScreen` (`@jgengine/react`) remain for a plainer animated screen over the live scene (transform/opacity only, reduced-motion aware, unmounted after exit), `PauseMenu` the small pause shape, `SettingsSection`/`SettingsTabs` the settings surface, `SaveSlotMenu` a bare 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.
|
|
33
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.
|
|
34
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.
|
|
35
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.
|
|
@@ -717,8 +717,12 @@
|
|
|
717
717
|
- `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 …
|
|
718
718
|
- `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.
|
|
719
719
|
- `ConfirmDialogProps` (interface): interface ConfirmDialogProps — Props for {@link ConfirmDialog}.
|
|
720
|
+
- `ContinuePane` (function): function ContinuePane({ slot, onContinue, emptyMessage = "No run saved yet. Start a new game to begin one.", now = Date.now, className, style, }: ContinuePaneProps): ReactNode — The Continue detail pane: the save being continued, read big — captured thumbnail, character, time played, the game's progress summary, when it was saved, and the resume action.
|
|
721
|
+
- `ContinuePaneProps` (interface): interface ContinuePaneProps — Props for {@link ContinuePane}.
|
|
720
722
|
- `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.
|
|
721
723
|
- `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.
|
|
724
|
+
- `CreditsPane` (function): function CreditsPane({ sections, heading = "Credits", notice, className, style }: CreditsPaneProps): ReactNode — The Credits detail pane: a scroller that belongs to the pane. Wheel, arrows, PageUp/PageDown, Home and End scroll it, the native scrollbar is hidden, and the page behind never moves.
|
|
725
|
+
- `CreditsPaneProps` (interface): interface CreditsPaneProps — Props for {@link CreditsPane}.
|
|
722
726
|
- `CreditsPanel` (function): function CreditsPanel({ document, onBack, title = "Credits", backLabel = "Back", open = true, backdrop, className, style, contentClassName, contentStyle, }: CreditsPanelProps): ReactNode — A reachable credits screen: the generated or hand-authored {@link CreditsDocument} in a scrollable panel over the scene, with one back affordance that keyboard and gamepad both reach. The game owns the backdrop and the wording; the block keeps the lines HudTheme-skinned and the exit working.
|
|
723
727
|
- `CreditsPanelProps` (interface): interface CreditsPanelProps — Props for {@link CreditsPanel}.
|
|
724
728
|
- `Crosshair` (function): function Crosshair({ size = 18, gap = 5, thickness = 2, color = "rgba(255,255,255,0.85)", style, className, }: { size?: number; gap?: number; thickness?: number; color?: string; style?: CSSProperties; className?: string; }): React.JSX.Element — A minimal center crosshair reticle — four ticks around a gap. Purely presentational.
|
|
@@ -781,6 +785,18 @@
|
|
|
781
785
|
- `FullscreenMapProps` (interface): interface FullscreenMapProps extends Omit<WorldMapSurfaceProps, "canvasWidth" | "canvasHeight" | "viewport" | "width" | "height" | "style"> — Props for {@link FullscreenMap}.
|
|
782
786
|
- `GameContextBridge` (function): function GameContextBridge({ context, children, }: { context: GameContext | null; children?: ReactNode; }): React.JSX.Element — Re-provide a captured {@link GameContext} across a nested React reconciler boundary. The `<Canvas>` of `@react-three/fiber` renders its children in a *separate* React root, so a `GameProvider` in the outer tree does NOT reach inside it — hooks like `useGameContext` / `useEntityRenderCues` throw. The fix the shell already uses internally: capture the context outside the boundary (with {@link useOptionalGameContext}) and wrap the inner tree in `GameContextBridge`. A `null` context renders children unchanged, so the same code works in showcases with no running game. This is the seam behind `EntityPreview` — reach for it directly when building a bespoke nested canvas.
|
|
783
787
|
- `GameIdentityProvider` (function): function GameIdentityProvider({ source, children, }: { source: IdentitySource; children?: ReactNode; }): React.JSX.Element — ⚠ undocumented
|
|
788
|
+
- `GameMenu` (function): function GameMenu({ items, initialId, onSelect, onBack, onHighlight, detail, logo, eyebrow, tagline, background, art, secondary, footer, controlHint, sound = true, variant = "title", open = true, reducedMotion, label, className, style, detailClassName, detailStyle, }: GameMenuProps): ReactNode — A game front-end menu: full-bleed over a living background, with a logo, an animated primary list and a detail pane that follows the highlighted verb. One system covers the main menu and the pause menu (`variant="pause"`), keyboard and gamepad navigation come from `useMenuNav`, confirmation ticks are generated (no audio asset), and the document never scrolls while it is open.
|
|
789
|
+
- `GameMenuAvatar` (interface): interface GameMenuAvatar — One avatar/portrait choice in {@link NewCharacterPane}.
|
|
790
|
+
- `GameMenuItem` (interface): interface GameMenuItem — A game's front-end menu: a full-bleed surface over a living background, a logo, primary verbs on the left that animate as the selection moves, and a detail pane on the right that changes with the highlighted verb. It is the block a main menu, a pause menu and their sub-screens are built from, so a game composes one menu system instead of a stack of modal panels.
|
|
791
|
+
- `GameMenuLogo` (interface): interface GameMenuLogo — The logo slot. Give it `image` for art, or `text` for a styled wordmark.
|
|
792
|
+
- `GameMenuPalette` (interface): interface GameMenuPalette — One palette choice in {@link NewCharacterPane}.
|
|
793
|
+
- `GameMenuProps` (interface): interface GameMenuProps — Props for {@link GameMenu}.
|
|
794
|
+
- `GameMenuSound` (interface): interface GameMenuSound — A menu sound player. Give it to `GameMenu`'s `sound` prop, or call its methods from your own menu.
|
|
795
|
+
- `GameMenuSoundConfig` (interface): interface GameMenuSoundConfig — How {@link createGameMenuSound} is tuned.
|
|
796
|
+
- `GameMenuSoundKind` (type): type GameMenuSoundKind = "move" | "confirm" | "back" — The short, quiet click a menu makes when the selection moves or is confirmed. Generated with an oscillator instead of shipped as an audio file, so a game gets menu feedback without authoring a sound and the whole thing unmounts with the screen.
|
|
797
|
+
- `GameMenuSoundState` (interface): interface GameMenuSoundState — The sound player's readable state: on/off, gain, and whether audio is actually running.
|
|
798
|
+
- `GameMenuVariant` (type): type GameMenuVariant = "title" | "pause" — How a {@link GameMenu} is used. `pause` is the same system at a smaller scale.
|
|
799
|
+
- `GamePauseMenu` (function): function GamePauseMenu(props: Omit<GameMenuProps, "variant"> & { onResume?: () => void }): ReactNode — A pause menu built from {@link GameMenu}: the same surface and navigation at a smaller scale, so a game composes one menu system rather than a second one. Escape (or B/Start) resumes.
|
|
784
800
|
- `GamePreviewComponent` (type): type GamePreviewComponent = ComponentType<GamePreviewProps> — ⚠ undocumented
|
|
785
801
|
- `GamePreviewProps` (type): type GamePreviewProps = { className?: string; } — ⚠ undocumented
|
|
786
802
|
- `GamePreviewStates` (type): type GamePreviewStates = Record<string, GamePreviewComponent> — ⚠ undocumented
|
|
@@ -889,6 +905,9 @@
|
|
|
889
905
|
- `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.
|
|
890
906
|
- `ModalHostProps` (interface): interface ModalHostProps — Props for {@link ModalHost}.
|
|
891
907
|
- `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.
|
|
908
|
+
- `NewCharacterChoice` (interface): interface NewCharacterChoice — What {@link NewCharacterPane} reports when a run starts.
|
|
909
|
+
- `NewCharacterPane` (function): function NewCharacterPane({ name, onNameChange, palettes, paletteId, onPaletteChange, avatars, avatarId, onAvatarChange, onStart, heading = "New run", nameLabel = "Character name", namePlaceholder = "Name your character", maxLength = 24, startLabel = "Begin", className, style, }: NewCharacterPanePro… — The New Game detail pane: a character name field, an optional palette and avatar choice, and the action that starts the run. The name field is a real form field, so menu key handling leaves typing alone (Enter inside the field starts the run).
|
|
910
|
+
- `NewCharacterPaneProps` (interface): interface NewCharacterPaneProps — Props for {@link NewCharacterPane}.
|
|
892
911
|
- `NotificationBell` (function): function NotificationBell({ store, onClick, icon = "🔔", className, style }: NotificationBellProps): ReactNode — Notification bell — an icon button with an unread-count badge, bound to a notification store. Toggle a {@link NotificationCenter} panel from its click.
|
|
893
912
|
- `NotificationBellProps` (interface): interface NotificationBellProps — Props for {@link NotificationBell}.
|
|
894
913
|
- `NotificationCenter` (function): function NotificationCenter({ store, title = "Notifications", emptyLabel = "You're all caught up.", onClose, formatTime, className, style, }: NotificationCenterProps): ReactNode — Notification center panel — the scrollable log toasts don't provide: newest notifications with a kind-colored marker, title/body, relative time, and an unread highlight; a header with the unread count, mark-all-read, and clear. Clicking an entry marks it read. Bind it to a `createNotificationCenter`.
|
|
@@ -935,11 +954,17 @@
|
|
|
935
954
|
- `RegionRecord` (interface): interface RegionRecord extends LayoutRegion — A region registration: the full `LayoutRegion` plus the live element (dev outlining), rect measured by the shell/react side.
|
|
936
955
|
- `RequireSession` (function): function RequireSession({ fallback, loading, children, }: { fallback?: ReactNode; loading?: ReactNode; children?: ReactNode; }): React.JSX.Element — ⚠ undocumented
|
|
937
956
|
- `RotateDeviceScreen` (function): function RotateDeviceScreen({ title = "Turn your device", description, requiredOrientation = "landscape", accent = "var(--jg-accent, #8ea2ff)", icon, className, style, }: { /** Short headline. */ title?: string; /** One concise explanatory line. Defaults from `requiredOrientation`. */ description?: … — Full-viewport, non-dismissible rotate-device gate shown when a game requires an orientation the device isn't in.
|
|
957
|
+
- `SAVE_SLOT_CHARACTER_KEY` (const): const SAVE_SLOT_CHARACTER_KEY: "character" — The character name a save card shows first, when the game supplies one as its own meta key.
|
|
958
|
+
- `SAVE_SLOT_THUMBNAIL_KEY` (const): const SAVE_SLOT_THUMBNAIL_KEY: "thumbnail" — The save slot `meta` key a card reads as its thumbnail: a data URL or image URL captured when the save was written. Every other meta key is shown as a progress chip and never interpreted.
|
|
938
959
|
- `SETTINGS_TRIGGER_CLASSNAME` (const): const SETTINGS_TRIGGER_CLASSNAME: "pointer-events-auto flex h-8 w-8 items-center justify-center rounded-md border border-[var(--jg-edge-bright)] bg-[var(--jg-surface)]/80 text-base text-[var(--jg-text-dim)] shadow-[0_1px_2px_rgba(0,0,0,0.6)] transition hover:bg-[var(--jg-surface-deep)] hover:text-[v… — Default HudTheme-token skin for {@link SettingsTrigger} — the 8×8 chrome button games re-authored seven times. Pass your own `className` only to override placement or brand chrome; the gear glyph already scales with `text-base` / `1em`.
|
|
939
960
|
- `SUITS` (const): const SUITS: readonly Suit[] — Suits in canonical order (clubs, diamonds, hearts, spades).
|
|
961
|
+
- `SaveSlotCard` (function): function SaveSlotCard({ slot, now = Date.now, recent = false, onSelect, onDelete, loadLabel, className, style, }: SaveSlotCardProps): ReactNode — One save slot as a card: a captured thumbnail, the character name, playtime, the game's progress summary and when it was saved — or, for an empty slot, an empty slot with a New Game affordance. Deleting takes two presses: the first swaps the button for a confirm pair, so a mis-click cannot erase a run.
|
|
962
|
+
- `SaveSlotCardProps` (interface): interface SaveSlotCardProps — Props for {@link SaveSlotCard}.
|
|
940
963
|
- `SaveSlotMenu` (function): function SaveSlotMenu({ model, onLoad, onNew, onDelete, onContinue, title = "Select Save", theme, now = Date.now, metaIcons, className, style, }: SaveSlotMenuProps): ReactNode — A drop-in save-select / profile menu that renders a {@link SaveSlots} index as a column of cards. A filled slot shows its name, the free-string `meta` the game supplied as chips, and a relative "saved" time, with Load and Delete actions; an empty slot shows a "New Game" affordance. A top-level Continue button targets the newest save (`model.mostRecent()`). This is a starting-point game-front-end building block — the game owns final art, wording, and layout; reskin it with {@link SaveSlotMenuTheme} or HudTheme CSS vars. Presentation only: all state lives in the core model, and `meta` keys are never interpreted.
|
|
941
964
|
- `SaveSlotMenuProps` (interface): interface SaveSlotMenuProps — Props for {@link SaveSlotMenu}.
|
|
942
965
|
- `SaveSlotMenuTheme` (interface): interface SaveSlotMenuTheme — Reskin tokens for {@link SaveSlotMenu}. All optional — each falls back to a HudTheme var then a neutral default.
|
|
966
|
+
- `SaveSlotsPane` (function): function SaveSlotsPane({ model, onContinue, onLoad, onNew, onDelete, heading = "Save slots", now = Date.now, columns = 2, className, style, }: SaveSlotsPaneProps): ReactNode — The slot grid for a {@link GameMenu} detail pane: Continue over the most recent save, then one card per slot. Arrow keys move between cards, Enter loads, and the grid scrolls inside its own pane rather than the page.
|
|
967
|
+
- `SaveSlotsPaneProps` (interface): interface SaveSlotsPaneProps — Props for {@link SaveSlotsPane}.
|
|
943
968
|
- `SaveStoreView` (interface): interface SaveStoreView<T> — The reactive slice of a {@link SaveStore} returned by {@link useSaveStore} — the live value plus bound save actions, all re-rendering on change.
|
|
944
969
|
- `Scoreboard` (function): function Scoreboard({ entries, rows, options, title, scoreLabel = "Score", nameFor, formatScore, medalIcon, theme, className, style, }: ScoreboardProps): ReactNode — A reskinnable ranked-score table for the {@link rankLeaderboard} selector: a rank column (medal icon/color for the top three via {@link medalFor}, otherwise the number), a name column, and a score column. The local player's row (`isLocal`) is washed and accented so "you" stands out. Feed it already-ranked `entries`, or raw `rows` plus `options` and it ranks them for you. All colors and spacing come from {@link ScoreboardTheme} / HudTheme `--jg-*` tokens — the component reads the model and never styles by game meaning.
|
|
945
970
|
- `ScoreboardProps` (interface): interface ScoreboardProps — Props for {@link Scoreboard}. Pass either `entries` (already ranked) or `rows` + `options`.
|
|
@@ -1039,6 +1064,7 @@
|
|
|
1039
1064
|
- `canvasCover` (const): const canvasCover: CanvasCover — The page's canvas cover. A full-screen menu (or a game's own opaque overlay) holds it; the shell stops drawing the world, its shadow passes and postfx while it is held, and draws again from the untouched scene when it lifts. A translucent scrim or a side panel must not hold it — anything the scene shows through has to keep drawing.
|
|
1040
1065
|
- `chatTransportFromSync` (function): function chatTransportFromSync(sync: ChatSync): ChatTransport — Lifts a callback-style ChatSync (e.g. createWsBackend().chatSyncFor(serverId)) into the hook-shaped ChatTransport contract. Create once per sync — outside render or inside useMemo — so subscriptions survive re-renders.
|
|
1041
1066
|
- `clerkIdentity` (function): function clerkIdentity(state: ClerkUserState, options?: { signOut?: () => void }): IdentitySource — ⚠ undocumented
|
|
1067
|
+
- `createGameMenuSound` (function): function createGameMenuSound(config: GameMenuSoundConfig = {}): GameMenuSound — A tiny WebAudio click generator for menu feedback: one oscillator burst per tick, created on the first play (so a browser autoplay policy does not log an error at mount) and reused after.
|
|
1042
1068
|
- `createHeldKeyTracker` (function): function createHeldKeyTracker(target: HeldKeyEventTarget): { isDown: (code: string) => boolean; dispose: () => void; } — ⚠ undocumented
|
|
1043
1069
|
- `defaultEquipLayout` (function): function defaultEquipLayout(items: Partial<Record<DefaultEquipSlotId, SlotItem | null>> = {}): EquipSlot[] — A sensible default paperdoll arrangement — worn armor down the left flank, held gear and jewelry down the right — that the game fills by passing worn items keyed by slot id. Every id is optional; unfilled slots render empty. Games that need a different slot set pass their own {@link EquipSlotLayout}.
|
|
1044
1070
|
- `defaultHudTheme` (const): const defaultHudTheme: HudTheme — The engine's default HUD look, applied by the shell at the HUD root of every game that does not opt out: a strong system UI font stack, layered translucent panels with a lit top rim and crisp hairline borders, a 12px type floor, and motion that honours reduced-motion. Games override any part with their own theme on a HUD ancestor.
|
|
@@ -1172,6 +1198,7 @@
|
|
|
1172
1198
|
- `useTicker` (function): function useTicker(hz = 10): number — Re-render at a steady rate. Returns a monotonically increasing tick count driven by a setInterval, for HUD elements that display wall-clock-derived values (cooldowns, cast bars, swing timers) without an engine subscription to hang off. `hz <= 0` disables the ticker.
|
|
1173
1199
|
- `useTimerRead` (function): function useTimerRead(timer: TimerSet, id: string, active = true): TimerRead | null — Subscribe to a single timer and re-read it every animation frame while mounted, so a HUD readout stays live without the game hand-rolling interval math. Reuses one read object (allocation-aware). Returns `null` for an unknown id. Pass `active={false}` to freeze the per-frame tick (e.g. an off-screen HUD).
|
|
1174
1200
|
- `useViewportMetrics` (function): function useViewportMetrics(): ViewportMetrics — Live visible viewport, tracking `window.visualViewport` (mobile browser chrome, pinch-zoom) with a layout-viewport fallback.
|
|
1201
|
+
- `useViewportScrollLock` (function): function useViewportScrollLock(active: boolean): void — Lock the page scroll while `active`, so a fixed full-bleed surface is exactly the viewport and the document behind it never gains a scrollbar. Nested and repeated locks are counted, and the previous inline styles are restored when the last one releases.
|
|
1175
1202
|
- `useVoice` (function): function useVoice(options?: UseVoiceOptions): VoiceState — Mic capture + push-to-talk + channel roster over the VoiceTransport signaling seam. Transmission gates the captured tracks' `enabled` flag; the media plane that actually moves audio bytes (WebRTC/SFU) stays behind the transport, host-supplied. Call once per voice channel and hand the returned state to the voice components.
|
|
1176
1203
|
- `useWaveRunner` (function): function useWaveRunner(runner: WaveRunner): WaveView — Subscribe to a {@link WaveRunner} and re-render whenever it notifies (wave, budget, spawns, alert change). Returns the runner's pooled {@link WaveView}. The runner is driven by the game/demo (call `runner.update(dt)`), not by this hook.
|
|
1177
1204
|
- `useWaypoints` (function): function useWaypoints(source: WaypointTracker | readonly Waypoint[]): readonly Waypoint[] — Subscribe to a {@link WaypointTracker} and re-render on every change, returning its current waypoints. Pass a plain array instead and it is returned as-is (no subscription), so a game can drive the host from a tracker or from its own derived state.
|
|
@@ -1450,6 +1477,42 @@
|
|
|
1450
1477
|
- `iconForItemId` (function): function iconForItemId(itemId: string): GameIconName | null — ⚠ undocumented
|
|
1451
1478
|
- `isGameIconName` (function): function isGameIconName(value: string): value is GameIconName — ⚠ undocumented
|
|
1452
1479
|
|
|
1480
|
+
## @jgengine/react/gameMenu
|
|
1481
|
+
|
|
1482
|
+
- `GameMenu` (function): function GameMenu({ items, initialId, onSelect, onBack, onHighlight, detail, logo, eyebrow, tagline, background, art, secondary, footer, controlHint, sound = true, variant = "title", open = true, reducedMotion, label, className, style, detailClassName, detailStyle, }: GameMenuProps): ReactNode — A game front-end menu: full-bleed over a living background, with a logo, an animated primary list and a detail pane that follows the highlighted verb. One system covers the main menu and the pause menu (`variant="pause"`), keyboard and gamepad navigation come from `useMenuNav`, confirmation ticks are generated (no audio asset), and the document never scrolls while it is open.
|
|
1483
|
+
- `GameMenuItem` (interface): interface GameMenuItem — A game's front-end menu: a full-bleed surface over a living background, a logo, primary verbs on the left that animate as the selection moves, and a detail pane on the right that changes with the highlighted verb. It is the block a main menu, a pause menu and their sub-screens are built from, so a game composes one menu system instead of a stack of modal panels.
|
|
1484
|
+
- `GameMenuLogo` (interface): interface GameMenuLogo — The logo slot. Give it `image` for art, or `text` for a styled wordmark.
|
|
1485
|
+
- `GameMenuProps` (interface): interface GameMenuProps — Props for {@link GameMenu}.
|
|
1486
|
+
- `GameMenuVariant` (type): type GameMenuVariant = "title" | "pause" — How a {@link GameMenu} is used. `pause` is the same system at a smaller scale.
|
|
1487
|
+
- `GamePauseMenu` (function): function GamePauseMenu(props: Omit<GameMenuProps, "variant"> & { onResume?: () => void }): ReactNode — A pause menu built from {@link GameMenu}: the same surface and navigation at a smaller scale, so a game composes one menu system rather than a second one. Escape (or B/Start) resumes.
|
|
1488
|
+
- `useViewportScrollLock` (function): function useViewportScrollLock(active: boolean): void — Lock the page scroll while `active`, so a fixed full-bleed surface is exactly the viewport and the document behind it never gains a scrollbar. Nested and repeated locks are counted, and the previous inline styles are restored when the last one releases.
|
|
1489
|
+
|
|
1490
|
+
## @jgengine/react/gameMenuSaves
|
|
1491
|
+
|
|
1492
|
+
- `ContinuePane` (function): function ContinuePane({ slot, onContinue, emptyMessage = "No run saved yet. Start a new game to begin one.", now = Date.now, className, style, }: ContinuePaneProps): ReactNode — The Continue detail pane: the save being continued, read big — captured thumbnail, character, time played, the game's progress summary, when it was saved, and the resume action.
|
|
1493
|
+
- `ContinuePaneProps` (interface): interface ContinuePaneProps — Props for {@link ContinuePane}.
|
|
1494
|
+
- `CreditsPane` (function): function CreditsPane({ sections, heading = "Credits", notice, className, style }: CreditsPaneProps): ReactNode — The Credits detail pane: a scroller that belongs to the pane. Wheel, arrows, PageUp/PageDown, Home and End scroll it, the native scrollbar is hidden, and the page behind never moves.
|
|
1495
|
+
- `CreditsPaneProps` (interface): interface CreditsPaneProps — Props for {@link CreditsPane}.
|
|
1496
|
+
- `GameMenuAvatar` (interface): interface GameMenuAvatar — One avatar/portrait choice in {@link NewCharacterPane}.
|
|
1497
|
+
- `GameMenuPalette` (interface): interface GameMenuPalette — One palette choice in {@link NewCharacterPane}.
|
|
1498
|
+
- `NewCharacterChoice` (interface): interface NewCharacterChoice — What {@link NewCharacterPane} reports when a run starts.
|
|
1499
|
+
- `NewCharacterPane` (function): function NewCharacterPane({ name, onNameChange, palettes, paletteId, onPaletteChange, avatars, avatarId, onAvatarChange, onStart, heading = "New run", nameLabel = "Character name", namePlaceholder = "Name your character", maxLength = 24, startLabel = "Begin", className, style, }: NewCharacterPanePro… — The New Game detail pane: a character name field, an optional palette and avatar choice, and the action that starts the run. The name field is a real form field, so menu key handling leaves typing alone (Enter inside the field starts the run).
|
|
1500
|
+
- `NewCharacterPaneProps` (interface): interface NewCharacterPaneProps — Props for {@link NewCharacterPane}.
|
|
1501
|
+
- `SAVE_SLOT_CHARACTER_KEY` (const): const SAVE_SLOT_CHARACTER_KEY: "character" — The character name a save card shows first, when the game supplies one as its own meta key.
|
|
1502
|
+
- `SAVE_SLOT_THUMBNAIL_KEY` (const): const SAVE_SLOT_THUMBNAIL_KEY: "thumbnail" — The save slot `meta` key a card reads as its thumbnail: a data URL or image URL captured when the save was written. Every other meta key is shown as a progress chip and never interpreted.
|
|
1503
|
+
- `SaveSlotCard` (function): function SaveSlotCard({ slot, now = Date.now, recent = false, onSelect, onDelete, loadLabel, className, style, }: SaveSlotCardProps): ReactNode — One save slot as a card: a captured thumbnail, the character name, playtime, the game's progress summary and when it was saved — or, for an empty slot, an empty slot with a New Game affordance. Deleting takes two presses: the first swaps the button for a confirm pair, so a mis-click cannot erase a run.
|
|
1504
|
+
- `SaveSlotCardProps` (interface): interface SaveSlotCardProps — Props for {@link SaveSlotCard}.
|
|
1505
|
+
- `SaveSlotsPane` (function): function SaveSlotsPane({ model, onContinue, onLoad, onNew, onDelete, heading = "Save slots", now = Date.now, columns = 2, className, style, }: SaveSlotsPaneProps): ReactNode — The slot grid for a {@link GameMenu} detail pane: Continue over the most recent save, then one card per slot. Arrow keys move between cards, Enter loads, and the grid scrolls inside its own pane rather than the page.
|
|
1506
|
+
- `SaveSlotsPaneProps` (interface): interface SaveSlotsPaneProps — Props for {@link SaveSlotsPane}.
|
|
1507
|
+
|
|
1508
|
+
## @jgengine/react/gameMenuSound
|
|
1509
|
+
|
|
1510
|
+
- `GameMenuSound` (interface): interface GameMenuSound — A menu sound player. Give it to `GameMenu`'s `sound` prop, or call its methods from your own menu.
|
|
1511
|
+
- `GameMenuSoundConfig` (interface): interface GameMenuSoundConfig — How {@link createGameMenuSound} is tuned.
|
|
1512
|
+
- `GameMenuSoundKind` (type): type GameMenuSoundKind = "move" | "confirm" | "back" — The short, quiet click a menu makes when the selection moves or is confirmed. Generated with an oscillator instead of shipped as an audio file, so a game gets menu feedback without authoring a sound and the whole thing unmounts with the screen.
|
|
1513
|
+
- `GameMenuSoundState` (interface): interface GameMenuSoundState — The sound player's readable state: on/off, gain, and whether audio is actually running.
|
|
1514
|
+
- `createGameMenuSound` (function): function createGameMenuSound(config: GameMenuSoundConfig = {}): GameMenuSound — A tiny WebAudio click generator for menu feedback: one oscillator burst per tick, created on the first play (so a browser autoplay policy does not log an error at mount) and reused after.
|
|
1515
|
+
|
|
1453
1516
|
## @jgengine/react/gameViewport
|
|
1454
1517
|
|
|
1455
1518
|
- `GameViewportProvider` (function): function GameViewportProvider({ platforms, className, style, children, }: { platforms?: readonly HudPlatform[]; className?: string; style?: CSSProperties; children?: ReactNode; }): React.JSX.Element — Provides the shared game viewport layout to everything it wraps. Mount it once around the whole game presentation (world, HUD, controls, system UI) so every subsystem reads one coordinated geometry and registers its rect for collision detection. Publishes live `--jg-viewport-*` / `--jg-visual-viewport-*` / `--jg-safe-*` CSS variables on its root.
|
|
@@ -1921,6 +1984,11 @@
|
|
|
1921
1984
|
- `relativeSavedAt` (function): function relativeSavedAt(savedAt: number, nowMs = Date.now()): string — Turn a `savedAt` timestamp into a compact relative label ("just now", "4m ago", "2d ago"). Presentation-only helper — the model stores raw ms.
|
|
1922
1985
|
- `useSaveSlots` (function): function useSaveSlots(model: SaveSlots): SaveSlotMeta[] — Subscribe to a {@link SaveSlots} index and re-render on any change (write, clear, rename, restore). Returns the live list of slots in menu order. Wire it into a save-select screen and mutate the model in event handlers — the menu re-renders itself.
|
|
1923
1986
|
|
|
1987
|
+
## @jgengine/react/saveThumbnail
|
|
1988
|
+
|
|
1989
|
+
- `SaveThumbnailOptions` (interface): interface SaveThumbnailOptions — Turning a rendered frame into the thumbnail a save card shows. Saves keep a small JPEG/WebP data URL (a couple of KB), not a full-resolution PNG, so a slot index still fits in local storage.
|
|
1990
|
+
- `shrinkCapture` (function): function shrinkCapture(capture: string | null, options: SaveThumbnailOptions = {}): Promise<string | null> — Downscale a captured frame (a data URL from `captureCanvas`) to a save-card thumbnail. Resolves to the thumbnail data URL, or `null` when the capture could not be decoded (a blank or tainted frame).
|
|
1991
|
+
|
|
1924
1992
|
## @jgengine/react/scoreboard
|
|
1925
1993
|
|
|
1926
1994
|
- `MedalToken` (type): type MedalToken = "gold" | "silver" | "bronze" — The medal tokens {@link medalFor} emits for the podium.
|
|
@@ -207,6 +207,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
207
207
|
|
|
208
208
|
- `ConfirmDialog` (function) · `import { ConfirmDialog } from "@jgengine/react"`
|
|
209
209
|
|
|
210
|
+
## continue-pane — continue detail pane — the save being resumed with thumbnail, character, playtime and progress
|
|
211
|
+
|
|
212
|
+
- `ContinuePane` (function) · `import { ContinuePane } from "@jgengine/react"`
|
|
213
|
+
|
|
210
214
|
## controller-camera-shake — R3F consumer that applies a core camera-shake controller's pooled per-frame offset to the active camera (translation + pitch/yaw/roll) so the view visibly shakes
|
|
211
215
|
|
|
212
216
|
- `ControllerCameraShake` (function) · `import { ControllerCameraShake } from "@jgengine/shell/camera"`
|
|
@@ -215,6 +219,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
215
219
|
|
|
216
220
|
- `ControlsList` (function) · `import { ControlsList } from "@jgengine/react"`
|
|
217
221
|
|
|
222
|
+
## credits-pane — credits detail pane — sections scrolled inside the pane
|
|
223
|
+
|
|
224
|
+
- `CreditsPane` (function) · `import { CreditsPane } from "@jgengine/react"`
|
|
225
|
+
|
|
218
226
|
## credits-panel — scrollable, navigable credits screen with a back affordance
|
|
219
227
|
|
|
220
228
|
- `CreditsPanel` (function) · `import { CreditsPanel } from "@jgengine/react"`
|
|
@@ -341,6 +349,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
341
349
|
|
|
342
350
|
- `GameContextBridge` (function) · `import { GameContextBridge } from "@jgengine/react"`
|
|
343
351
|
|
|
352
|
+
## game-menu — full-bleed game menu — living background, token-sized logo, animated navigable verbs, detail pane, selection sound, pause variant, no page scrollbar
|
|
353
|
+
|
|
354
|
+
- `GameMenu` (function) · `import { GameMenu } from "@jgengine/react"`
|
|
355
|
+
|
|
356
|
+
## game-pause-menu — pause menu from the shared game-menu system — same navigation and surface, sized down
|
|
357
|
+
|
|
358
|
+
- `GamePauseMenu` (function) · `import { GamePauseMenu } from "@jgengine/react"`
|
|
359
|
+
|
|
344
360
|
## gesture-resume — unlock the audio context on the first pointer/key/touch gesture, so pre-gesture cues play instead of vanishing
|
|
345
361
|
|
|
346
362
|
- `installGestureResume` (function) · `import { installGestureResume } from "@jgengine/shell/audio/gestureResume"`
|
|
@@ -577,6 +593,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
577
593
|
|
|
578
594
|
- `MenuScreen` (function) · `import { MenuScreen } from "@jgengine/react"`
|
|
579
595
|
|
|
596
|
+
## menu-sound — generated click for menu move/confirm/back, no audio asset
|
|
597
|
+
|
|
598
|
+
- `createGameMenuSound` (function) · `import { createGameMenuSound } from "@jgengine/react"`
|
|
599
|
+
|
|
580
600
|
## menu-stack-reducer — pure push/back/replace/reset transitions for a menu screen stack
|
|
581
601
|
|
|
582
602
|
- `menuStackReducer` (function) · `import { menuStackReducer } from "@jgengine/react/menuRouter"`
|
|
@@ -647,6 +667,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
647
667
|
|
|
648
668
|
- `WorldNameplates` (function) · `import { WorldNameplates } from "@jgengine/shell/world/WorldHud"`
|
|
649
669
|
|
|
670
|
+
## new-character-pane — character creation pane — name field, palette and avatar choice, start action
|
|
671
|
+
|
|
672
|
+
- `NewCharacterPane` (function) · `import { NewCharacterPane } from "@jgengine/react"`
|
|
673
|
+
|
|
650
674
|
## notice-feed — render ctx.game.notices as an animated toned stack (enter/exit motion, ×N stacking, HUD avoidance)
|
|
651
675
|
|
|
652
676
|
- `NoticeStack` (function) · `import { NoticeStack } from "@jgengine/react/notices"`
|
|
@@ -785,10 +809,18 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
785
809
|
|
|
786
810
|
- `resolveGameLook` (function) · `import { resolveGameLook } from "@jgengine/core/ui"`
|
|
787
811
|
|
|
812
|
+
## save-slot-card — save slot as a card — thumbnail, character, playtime, progress, timestamp, confirmed delete
|
|
813
|
+
|
|
814
|
+
- `SaveSlotCard` (function) · `import { SaveSlotCard } from "@jgengine/react"`
|
|
815
|
+
|
|
788
816
|
## save-slot-menu — drop-in save-select / profile menu that renders a core save-slots index as New / Continue / Load / Delete cards with free-string meta chips and relative save times, theme-skinnable
|
|
789
817
|
|
|
790
818
|
- `SaveSlotMenu` (function) · `import { SaveSlotMenu } from "@jgengine/react"`
|
|
791
819
|
|
|
820
|
+
## save-slots-pane — slot grid pane for the game menu — Continue newest, per-slot cards, keyboard navigation
|
|
821
|
+
|
|
822
|
+
- `SaveSlotsPane` (function) · `import { SaveSlotsPane } from "@jgengine/react"`
|
|
823
|
+
|
|
792
824
|
## scene-capture-binding — expose the in-Canvas scene-capture function to outside-Canvas HUD (photo mode)
|
|
793
825
|
|
|
794
826
|
- `SceneCaptureBinding` (function) · `import { SceneCaptureBinding } from "@jgengine/shell/render/sceneCapture"`
|
|
@@ -845,6 +877,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
845
877
|
|
|
846
878
|
- `ShopGrid` (function) · `import { ShopGrid } from "@jgengine/react"`
|
|
847
879
|
|
|
880
|
+
## shrink-capture — downscale a captured frame to a save-card thumbnail data URL
|
|
881
|
+
|
|
882
|
+
- `shrinkCapture` (function) · `import { shrinkCapture } from "@jgengine/react/saveThumbnail"`
|
|
883
|
+
|
|
848
884
|
## speed-format — render a m/s speed as km/h, mph, knots, or m/s for speedometers and telemetry HUDs
|
|
849
885
|
|
|
850
886
|
- `formatSpeed` (function) · `import { formatSpeed } from "@jgengine/core/ui"`
|
|
@@ -1019,6 +1055,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
1019
1055
|
|
|
1020
1056
|
- `useT` (function) · `import { useT } from "@jgengine/react"`
|
|
1021
1057
|
|
|
1058
|
+
## use-viewport-scroll-lock — count-aware document scroll lock for full-bleed surfaces
|
|
1059
|
+
|
|
1060
|
+
- `useViewportScrollLock` (function) · `import { useViewportScrollLock } from "@jgengine/react"`
|
|
1061
|
+
|
|
1022
1062
|
## use-wave-runner — React hook that subscribes to a wave runner and returns its pooled current-wave view
|
|
1023
1063
|
|
|
1024
1064
|
- `useWaveRunner` (function) · `import { useWaveRunner } from "@jgengine/react"`
|
|
@@ -243,7 +243,7 @@ Focus must look authored while remaining accessible. Menus should support keyboa
|
|
|
243
243
|
|
|
244
244
|
Compose it from the game's own markup over the headless `StartScreen` (`@jgengine/react`, `start-screen` capability) — a full-bleed `data-jg-menu` overlay that centers your content and offers an opt-in settings corner; it imposes no look. Menu rows are the game's own buttons and focus handling restyled into its chrome; the shipped pieces to compose in are `ControlsList` (`@jgengine/react`) for the control legend and `KeyHint`/`Keycap` for key badges (`KeyHint` already renders nothing on a coarse pointer) — never shipped as raw scaffold.
|
|
245
245
|
|
|
246
|
-
**The engine ships the front-end machinery; the game owns its look.** `MenuScreen` (`@jgengine/react/menuScreen`) is one animated screen over the live scene: transform/opacity enter and exit that unmount when the exit finishes, a translucent scrim (`"none"` keeps the 3D scene fully visible), and a `backdrop` slot for a poster or preview. `TitleMenu` (`@jgengine/react/titleMenu`) is the title shape
|
|
246
|
+
**The engine ships the front-end machinery; the game owns its look.** `GameMenu` (`@jgengine/react/gameMenu`) is the game menu: full-bleed `position: fixed` surface with the document scroll locked, a living background (the live scene behind it, a `background` layer, or drifting `art`), a logo sized by `--jg-menu-logo-size`, primary verbs whose highlight slides, glows and ticks (generated WebAudio, no asset), and a detail pane per verb. Ship the panes it exports: `ContinuePane` (the save being resumed), `NewCharacterPane` (name, palette, avatar), `SaveSlotsPane` (cards with a `meta.thumbnail` capture, playtime, progress, timestamp, two-press delete), `CreditsPane` (scroller inside the pane) and the game's real settings surface. `GamePauseMenu` is the same system at `variant="pause"`. Games supply the logo, art, wording, verbs and each pane's content; saves come from `createSaveSlots` + `refreshSaveSlots`, and a thumbnail from `captureCanvas` + `shrinkCapture`. `MenuScreen` (`@jgengine/react/menuScreen`) is one animated screen over the live scene: transform/opacity enter and exit that unmount when the exit finishes, a translucent scrim (`"none"` keeps the 3D scene fully visible), and a `backdrop` slot for a poster or preview. `TitleMenu` (`@jgengine/react/titleMenu`) is the plainer title shape - wordmark, eyebrow, tagline, verb list, footer, control hint. All read `useMenuNav` (`@jgengine/react/menuNav`): arrow keys/WASD/Home/End, a standard gamepad's d-pad and left stick (with hold-to-repeat), Enter/Space/A confirm, Escape/Backspace/B/Start back. Tab is never intercepted and typing in a field is never swallowed, so no menu traps focus; keys still work if the canvas kept focus. A game that composes its own markup gets the same behavior from `useMenuNav` alone.
|
|
247
247
|
|
|
248
248
|
**The front-end entries, when the game has them.** A menu is more than one Start button. Offer the real front-end verbs a player expects, gated to what the game actually supports:
|
|
249
249
|
|