@jgengine/react 0.36.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.
@@ -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.36.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.36.0"
38
+ "@jgengine/core": "^0.38.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "react": "^19.0.0",
@@ -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.36.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
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.36.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
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`) 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
+ - **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.
@@ -2829,6 +2897,8 @@
2829
2897
 
2830
2898
  ## @jgengine/shell/render/CascadedShadows
2831
2899
 
2900
+ - `CASCADE_DEPTH_STEP_TEXELS` (const): const CASCADE_DEPTH_STEP_TEXELS: 8 — Depth-axis steps worth of a texel the snapped light may drift before it re-anchors.
2901
+ - `CascadeRefreshState` (interface): interface CascadeRefreshState — Per-frame cascade refresh bookkeeping: the centre each map was last rendered with.
2832
2902
  - `CsmMaterialSetup` (interface): interface CsmMaterialSetup — Structural slice of CSM so the patcher is testable without a renderer.
2833
2903
  - `patchSceneMaterials` (function): function patchSceneMaterials(scene: THREE.Scene, csm: CsmMaterialSetup, patched: WeakSet<THREE.Material>): void — Run CSM's `setupMaterial` over every unpatched standard material in the scene, chaining rather than clobbering a material's own `onBeforeCompile`.
2834
2904
 
@@ -3034,6 +3104,12 @@
3034
3104
 
3035
3105
  - `drawProceduralTileLayer` (function): function drawProceduralTileLayer(canvas: CanvasFixture): TilemapWorldConfig — Draw the deterministic tile atlas used by textured tile-layer captures and tests.
3036
3106
 
3107
+ ## @jgengine/shell/render/triangleAttribution
3108
+
3109
+ - `TriangleAttribution` (interface): interface TriangleAttribution — Where one scene's triangles are, biggest line first.
3110
+ - `TriangleLine` (interface): interface TriangleLine — One line of the triangle table: the draws sharing one geometry inside one named model.
3111
+ - `attributeTriangles` (function): function attributeTriangles(root: THREE.Object3D, limit = 12): TriangleAttribution — Sum a scene's triangles by model and geometry, which is the attribution a triangle budget needs: a total says a frame is heavy, a table says which authored object made it heavy. Walks the graph once; call it on demand, never per frame.
3112
+
3037
3113
  ## @jgengine/shell/render/triplanarMaterial
3038
3114
 
3039
3115
  - `TriplanarAssetMaterial` (function): function TriplanarAssetMaterial({ asset, ...options }: { asset: MaterialAsset } & { tileMetres?: number; sharpness?: number; normalScale?: number }): React.JSX.Element — Mounts {@link useTriplanarAssetMaterial} on a mesh; the R3F renderer owns disposal.
@@ -3101,6 +3177,13 @@
3101
3177
 
3102
3178
  - `ScatterModelSource` (interface): interface ScatterModelSource — One instanceable draw source harvested from a resolved GLB — geometry stays shared with the loader cache, `localMatrix` bakes the model's own transform (from its scene root) plus scale/anchor normalization.
3103
3179
 
3180
+ ## @jgengine/shell/scatter/scatterVariants
3181
+
3182
+ - `ScatterVariantCamera` (interface): interface ScatterVariantCamera — A camera position the level pass measures from.
3183
+ - `ScatterVariantMember` (interface): interface ScatterVariantMember — A scatter placement as the level pass reads it: a stable id and a world position.
3184
+ - `scatterVariantTriangles` (function): function scatterVariantTriangles(counts: readonly number[], triangles: readonly number[]): number — Triangle total for per-level instance counts, so a fixture reports real geometry, not instance counts.
3185
+ - `writeScatterVariantLevel` (function): function writeScatterVariantLevel(level: number, selector: ModelVariantSelector, camera: ScatterVariantCamera, members: readonly ScatterVariantMember[], transforms: readonly THREE.Matrix4[], localMatrix: THREE.Matrix4, out: THREE.Matrix4[], keep?: (index: number) => boolean): number — Writes the matrices of the members the shared variant selector put on `level` into `out`, in source order, and returns how many were written. `out` is the level mesh's reserved capacity (one matrix per member), so a frame writes matrices and `count` without allocating.
3186
+
3104
3187
  ## @jgengine/shell/scene
3105
3188
 
3106
3189
  - `AuthoredObjects` (function): function AuthoredObjects({ document, field, verticalOffset = 0, onExisting = "keep", synchronize = false, excludeKinds, }: AuthoredObjectsProps): null — Places every editor marker with a catalog id into `ctx.scene.object`, grounded on `field`. WorldScene renders them through the game's `objectModels` seam — no bespoke place loop. Pure headless twin: `resolveAuthoredObjects` / `placeAuthoredObjects`.
@@ -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"`
@@ -919,6 +955,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
919
955
 
920
956
  - `TransformGizmo` (const) · `import { TransformGizmo } from "@jgengine/shell/structures"`
921
957
 
958
+ ## triangle-attribution — per-model triangle table with instance counts, for perf probes
959
+
960
+ - `attributeTriangles` (function) · `import { attributeTriangles } from "@jgengine/shell/render/triangleAttribution"`
961
+
922
962
  ## tween-number — rolling number readout with an up/down delta flash
923
963
 
924
964
  - `TweenNumber` (function) · `import { TweenNumber } from "@jgengine/react/tweenNumber"`
@@ -1015,6 +1055,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
1015
1055
 
1016
1056
  - `useT` (function) · `import { useT } from "@jgengine/react"`
1017
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
+
1018
1062
  ## use-wave-runner — React hook that subscribes to a wave runner and returns its pooled current-wave view
1019
1063
 
1020
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 — wordmark, eyebrow, tagline, verb list, footer, control hint — where the game supplies the wording, verbs and art and skins it with `--jg-*` tokens. Both 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.
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