@jgengine/react 0.12.0 → 0.14.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.
@@ -115,6 +115,9 @@
115
115
  - `LayoutRegion` (interface): interface LayoutRegion — A physical rectangle a UI subsystem occupies, published to the shared registry.
116
116
  - `LookPreset` (type): type LookPreset = "cinematic" | "flat" — Named default-look preset composing the existing lighting/sky/fog/post knobs into one field. `"cinematic"` (the default when unset) draws a scene lit like a shipped game — a real day sky with a view-following shadow-casting sun + hemisphere fill, a network-free image-based-lighting environment so PBR surfaces catch soft reflections, and a tuned tone-map/bloom/AO/vignette post stack. `"flat"` opts out of the sky/IBL/post rig to the bare ambient+directional default (pre-#773). The upgraded default primitive materials — tuned roughness/metalness plus subtle procedural surface detail so un-modeled boxes/capsules stop reading as flat plastic — apply under both presets.
117
117
  - `MobileHudBehavior` (type): type MobileHudBehavior = | "persistent" | "compact" | "icon" | "transient" | "hidden" | "sheet" | "modal" — How a HUD element adapts on phones.
118
+ - `PanelDef` (interface): interface PanelDef — A caller-authored panel/window declaration — the DATA input to the model. Only `id` is required so a bag, a character sheet, and a one-off dialog all share the shape.
119
+ - `PanelPosition` (interface): interface PanelPosition — Headless, serializable model for a set of player-toggled windows/panels — the data layer behind a WoW-style UI where B opens the bag, C the character sheet, and ESC closes the topmost window. It carries *which panels are open*, their focus/z stacking order, per-panel position overrides, and exclusive-group membership, with zero rendering opinion. The React chrome in `@jgengine/react/panels` consumes this; a game keeps layout, skin, keybinds, and window content caller-owned. Pure and immutable: every reducer returns a new state and never mutates its input.
120
+ - `PanelState` (interface): interface PanelState — Serializable state for a set of panels: which are `open`, their `z` focus stack (higher renders on top), optional per-panel `pos` overrides, and the `meta` registry (group/closable) captured from the defs so the reducers below need no defs argument. Plain JSON — persist and rehydrate it freely.
118
121
  - `PopoverOptions` (interface): interface PopoverOptions — Options for {@link placePopover}.
119
122
  - `PopoverPlacement` (interface): interface PopoverPlacement — The resolved popover position — the side it actually opened on and its clamped top-left.
120
123
  - `PopoverSide` (type): type PopoverSide = "top" | "bottom" | "left" | "right" — Which side of its anchor a popover opens toward.
@@ -149,19 +152,28 @@
149
152
  - `actionCost` (function): function actionCost(resourceId: string, amount: number, available?: number): ActionCost — Build an {@link ActionCost}. `met` is true unless `available` is provided and below `amount`.
150
153
  - `actionTooltip` (function): function actionTooltip(action: ResolvedAction): TooltipContent — Build {@link TooltipContent} from a {@link ResolvedAction}: title from the label, the group as a subtitle, the description as the body, and the blocking reasons as warning notes. The single place an action's tooltip copy is assembled, so every renderer shows the same thing.
151
154
  - `busVolumeSettingId` (function): function busVolumeSettingId(busId: string): string — ⚠ undocumented
155
+ - `closePanel` (function): function closePanel(state: PanelState, id: string): PanelState — Close panel `id`. No-op if it is already closed. Its position override is kept so reopening restores the last placement.
156
+ - `closeTopPanel` (function): function closeTopPanel(state: PanelState): PanelState — Close the topmost closable open panel — the ESC handler. Skips panels pinned with `closable: false` and returns the same state when nothing closable is open.
157
+ - `createPanelState` (function): function createPanelState(defs: readonly PanelDef[]): PanelState — Build the initial {@link PanelState} from the panel defs: panels flagged `initial` start open (in declaration order, respecting group exclusivity), and every def's group/closable is captured into the state so later reducers need no defs.
152
158
  - `createSettingsStore` (function): function createSettingsStore(storage: Pick<WebStorageLike, "getItem" | "setItem"> | null | undefined = defaultStorage()): SettingsStore — Reactive, localStorage-backed settings store shared by the shell wiring and React hooks.
159
+ - `focusPanel` (function): function focusPanel(state: PanelState, id: string): PanelState — Raise open panel `id` to the top of the focus stack (assign it the highest z). No-op if it is closed or already on top, so a pointer-down that refocuses the top window returns the same state.
153
160
  - `formatDelta` (function): function formatDelta(seconds: number, decimals: 0 | 1 | 2 = 2): string — Format a signed time gap as `+m:ss.ff` / `-m:ss.ff`, for race deltas and split times.
154
161
  - `formatDistance` (function): function formatDistance(meters: number, options: DistanceFormat = {}): string — Format a distance given in meters as a HUD-ready string, switching to km automatically past 1000m when `unit: "auto"`.
155
162
  - `formatDuration` (function): function formatDuration(seconds: number, options: DurationFormat = {}): string — Format a duration in seconds as a clock string (`m:ss`, `m:ss.ff`, or `h:mm:ss`), the shape every timer and racing HUD needs.
156
163
  - `formatOrdinal` (function): function formatOrdinal(value: number): string — English ordinal for a placement number: 1 → "1st", 2 → "2nd", 3 → "3rd", 11 → "11th".
157
164
  - `formatSpeed` (function): function formatSpeed(metersPerSecond: number, options: SpeedFormat = {}): string — Format a speed given in meters/second as a HUD-ready string in km/h, mph, knots, or m/s — the one conversion table every speedometer and telemetry readout should share.
158
165
  - `hudScaleForViewport` (function): function hudScaleForViewport(fit: Required<HudFitConfig>, viewport: HudSize): number — The one scaling rule for every display: the ratio of the live viewport to the authored design size along the limiting axis, clamped. 1 on a viewport at or above design size; smoothly below 1 down to `minScale` on phones.
166
+ - `isOpen` (function): function isOpen(state: PanelState, id: string): boolean — Whether panel `id` is currently open.
159
167
  - `listHudPanelTypes` (function): function listHudPanelTypes(): HudPanelTypeDef[] — Every registered panel type, sorted by id — editor palette / agent listing.
160
168
  - `moveGridFocus` (function): function moveGridFocus(current: number, count: number, columns: number, dir: FocusDirection, options?: GridFocusOptions): number — Pure grid focus math for keyboard and controller navigation of an action card laid out in `columns`-wide rows. Given the current index (`-1` for none) it returns the next index, clamped to `[0, count)` — or wrapped when `wrap` is set. `left/right` move within/across columns, `up/down` across rows, `next/prev` walk the flat order, `first/last` jump to the ends. Renderer-agnostic: radial and list layouts pass `columns: 1` (or `count`) to get sensible 1-D stepping.
169
+ - `movePanel` (function): function movePanel(state: PanelState, id: string, pos: PanelPosition): PanelState — Set panel `id`'s position override — the drag commit. Does not require the panel to be open, so a caller can pre-place a window before it is shown.
161
170
  - `moveSelectionFocus` (function): function moveSelectionFocus(count: number, current: number, dir: SelectionFocusDirection): number — Pure next-focus math for the selection strip: step the focus index by one member (wrapping), or jump to an end. `-1` (no focus) steps to the first/last member. Returns `-1` for an empty selection.
171
+ - `openPanel` (function): function openPanel(state: PanelState, id: string): PanelState — Open panel `id`, raising it to the top of the focus stack. If it belongs to a `group`, the other open panels in that group are closed (exclusive windows). Opening an already-open panel just refocuses it. Position overrides are preserved.
172
+ - `orderedOpen` (function): function orderedOpen(state: PanelState, defs: readonly PanelDef[]): PanelDef[] — The open panels' defs sorted ascending by z — the render order for a window host (map to elements so the last, highest-z window paints on top). Defs unknown to the state are skipped.
162
173
  - `orientationGateActive` (function): function orientationGateActive(requirement: OrientationRequirement, liveOrientation: LayoutOrientation): boolean — The rotate gate blocks gameplay: a hard requirement (or `unsupported`) the live orientation doesn't satisfy.
163
174
  - `orientationHintActive` (function): function orientationHintActive(requirement: OrientationRequirement, liveOrientation: LayoutOrientation): boolean — An advisory rotate hint applies: a preference (not a hard gate) the live orientation doesn't satisfy.
164
- - `overflowingPanels` (function): function overflowingPanels(panels: readonly { id: string; rect: HudRect }[], viewport: HudSize, tolerance = 1.5): HudOverflow[] — Every panel rect that escapes the viewport — the data behind the HUD overflow gate.
175
+ - `overflowingPanels` (function): function overflowingPanels(panels: readonly { id: string; rect: HudRect }[], viewport: HudSize, tolerance = 1.5): HudOverflow[] — Every panel rect that escapes the viewport — the data behind the HUD overflow gate. A rect is only reported when it is partly visible *and* crosses an edge; a fully off-screen (closed/hidden) surface is skipped, so parked drawers and unopened menus never raise a false alarm.
176
+ - `panelByHotkey` (function): function panelByHotkey(defs: readonly PanelDef[], code: string): string | null — Find the panel whose `hotkey` matches `code`, case-insensitively — the keybind router. `code` may be a `KeyboardEvent.code` (`"KeyB"`) or key (`"b"`); it is compared verbatim to each def's `hotkey`, so author the hotkey in whichever form the caller feeds in. Returns the first match's id, or null.
165
177
  - `placePopover` (function): function placePopover(anchor: UiRect, content: UiSize, viewport: UiSize, options?: PopoverOptions): PopoverPlacement — Position a popover/tooltip of `content` size against its `anchor` rectangle inside `viewport`: open on the `preferred` side, flip to the opposite when that side lacks room and the opposite has more, then clamp the top-left so the box stays fully on screen (respecting `margin`). Pure and deterministic — no DOM reads — so it is SSR-safe and unit-testable, and the React tooltip is a thin shell over it.
166
178
  - `registerHudPanelType` (function): function registerHudPanelType(def: HudPanelTypeDef): void — Register a HUD panel type so the editor can list/add/configure it and canvas resize knows axes.
167
179
  - `resizePanelSize` (function): function resizePanelSize(current: HudSize, delta: { dw: number; dh: number }, axes: HudResizeAxes, limits?: { minWidth?: number; maxWidth?: number; minHeight?: number; maxHeight?: number; }): HudSize — Semantic resize: apply pixel deltas only on growable axes, clamped to min/max. Never scales content — callers reflow layout size (track length, list rows).
@@ -174,6 +186,7 @@
174
186
  - `selectionWindow` (function): function selectionWindow(members: readonly EntitySummaryDef[], scroll: number, size: number): SelectionWindow — Compute a virtualization window over a large selection: a `size`-length slice starting near `scroll`, clamped so the window never runs past the end. Render only `items`, offsetting by `start * itemSize`, to keep a thousand-unit selection from mounting a thousand portraits.
175
187
  - `summarizeSelection` (function): function summarizeSelection(members: readonly EntitySummaryDef[], options?: SummarizeSelectionOptions): SelectionView — Summarize a selected-entity list into a {@link SelectionView}: resolve the primary focus, keep the ordered members, and bucket them by `kind`. Pure and stable — deterministic group ordering and a clamped focus index, so it is safe to call every render and to assert on in a test.
176
188
  - `swingTimerState` (function): function swingTimerState(player: SwingPlayerInput, target: SwingTargetInput | null, prevPeriod: number, prevTimer: number): SwingTimerState — Pure swing-timer bar state — no hidden state, no clock, no DOM. The caller threads `prevPeriod`/`prevTimer` back each frame. The period is recovered on the reset edge (when `swingTimer` jumps up = a new swing began) as `max(swingTimer, weapon.speed)`, so the fill is correct even without knowing the weapon's exact cadence. Hidden unless auto-attacking a live, non-object target.
189
+ - `togglePanel` (function): function togglePanel(state: PanelState, id: string): PanelState — Toggle panel `id` — open it (with group exclusivity) if closed, close it if open. The one-key behavior behind a keybind like `B` for the bag.
177
190
 
178
191
  ## @jgengine/core/ui/actionModel
179
192
 
@@ -243,7 +256,7 @@
243
256
  - `HudPlatform` (type): type HudPlatform = "web" | "mobile" — Where a game is meant to be played. `"web"` alone keeps today's desktop-first HUD; adding `"mobile"` turns on design-resolution fit scaling on compact displays.
244
257
  - `HudViewportConfig` (interface): interface HudViewportConfig extends HudFitConfig — Per-game HUD viewport declaration carried on `PlayableGame.hudFit`; `mobile` overrides the fit on compact displays so the owner can tune the phone layout separately.
245
258
  - `hudScaleForViewport` (function): function hudScaleForViewport(fit: Required<HudFitConfig>, viewport: HudSize): number — The one scaling rule for every display: the ratio of the live viewport to the authored design size along the limiting axis, clamped. 1 on a viewport at or above design size; smoothly below 1 down to `minScale` on phones.
246
- - `overflowingPanels` (function): function overflowingPanels(panels: readonly { id: string; rect: HudRect }[], viewport: HudSize, tolerance = 1.5): HudOverflow[] — Every panel rect that escapes the viewport — the data behind the HUD overflow gate.
259
+ - `overflowingPanels` (function): function overflowingPanels(panels: readonly { id: string; rect: HudRect }[], viewport: HudSize, tolerance = 1.5): HudOverflow[] — Every panel rect that escapes the viewport — the data behind the HUD overflow gate. A rect is only reported when it is partly visible *and* crosses an edge; a fully off-screen (closed/hidden) surface is skipped, so parked drawers and unopened menus never raise a false alarm.
247
260
  - `rectOverflow` (function): function rectOverflow(rect: HudRect, viewport: HudSize, tolerance = 1.5): Omit<HudOverflow, "id"> | null — Non-null when any edge of `rect` falls outside `viewport` by more than `tolerance` px; each field is the overflow distance past that edge (0 when inside).
248
261
  - `resolveHudFit` (function): function resolveHudFit(config: HudViewportConfig | undefined, mobile: boolean): Required<HudFitConfig> — ⚠ undocumented
249
262
 
@@ -259,6 +272,22 @@
259
272
  - `resolveMobileOrientationRule` (function): function resolveMobileOrientationRule(orientation: GameOrientation | undefined): MobileOrientationRule — Normalize the `orientation` field (legacy string or object) to a single mobile rule.
260
273
  - `resolveOrientationRequirement` (function): function resolveOrientationRequirement(orientation: GameOrientation | undefined, platform: "mobile" | "desktop"): OrientationRequirement — Resolve the game's orientation declaration into a concrete requirement for a platform. Desktop is always unconstrained.
261
274
 
275
+ ## @jgengine/core/ui/panelModel
276
+
277
+ - `PanelDef` (interface): interface PanelDef — A caller-authored panel/window declaration — the DATA input to the model. Only `id` is required so a bag, a character sheet, and a one-off dialog all share the shape.
278
+ - `PanelPosition` (interface): interface PanelPosition — Headless, serializable model for a set of player-toggled windows/panels — the data layer behind a WoW-style UI where B opens the bag, C the character sheet, and ESC closes the topmost window. It carries *which panels are open*, their focus/z stacking order, per-panel position overrides, and exclusive-group membership, with zero rendering opinion. The React chrome in `@jgengine/react/panels` consumes this; a game keeps layout, skin, keybinds, and window content caller-owned. Pure and immutable: every reducer returns a new state and never mutates its input.
279
+ - `PanelState` (interface): interface PanelState — Serializable state for a set of panels: which are `open`, their `z` focus stack (higher renders on top), optional per-panel `pos` overrides, and the `meta` registry (group/closable) captured from the defs so the reducers below need no defs argument. Plain JSON — persist and rehydrate it freely.
280
+ - `closePanel` (function): function closePanel(state: PanelState, id: string): PanelState — Close panel `id`. No-op if it is already closed. Its position override is kept so reopening restores the last placement.
281
+ - `closeTopPanel` (function): function closeTopPanel(state: PanelState): PanelState — Close the topmost closable open panel — the ESC handler. Skips panels pinned with `closable: false` and returns the same state when nothing closable is open.
282
+ - `createPanelState` (function): function createPanelState(defs: readonly PanelDef[]): PanelState — Build the initial {@link PanelState} from the panel defs: panels flagged `initial` start open (in declaration order, respecting group exclusivity), and every def's group/closable is captured into the state so later reducers need no defs.
283
+ - `focusPanel` (function): function focusPanel(state: PanelState, id: string): PanelState — Raise open panel `id` to the top of the focus stack (assign it the highest z). No-op if it is closed or already on top, so a pointer-down that refocuses the top window returns the same state.
284
+ - `isOpen` (function): function isOpen(state: PanelState, id: string): boolean — Whether panel `id` is currently open.
285
+ - `movePanel` (function): function movePanel(state: PanelState, id: string, pos: PanelPosition): PanelState — Set panel `id`'s position override — the drag commit. Does not require the panel to be open, so a caller can pre-place a window before it is shown.
286
+ - `openPanel` (function): function openPanel(state: PanelState, id: string): PanelState — Open panel `id`, raising it to the top of the focus stack. If it belongs to a `group`, the other open panels in that group are closed (exclusive windows). Opening an already-open panel just refocuses it. Position overrides are preserved.
287
+ - `orderedOpen` (function): function orderedOpen(state: PanelState, defs: readonly PanelDef[]): PanelDef[] — The open panels' defs sorted ascending by z — the render order for a window host (map to elements so the last, highest-z window paints on top). Defs unknown to the state are skipped.
288
+ - `panelByHotkey` (function): function panelByHotkey(defs: readonly PanelDef[], code: string): string | null — Find the panel whose `hotkey` matches `code`, case-insensitively — the keybind router. `code` may be a `KeyboardEvent.code` (`"KeyB"`) or key (`"b"`); it is compared verbatim to each def's `hotkey`, so author the hotkey in whichever form the caller feeds in. Returns the first match's id, or null.
289
+ - `togglePanel` (function): function togglePanel(state: PanelState, id: string): PanelState — Toggle panel `id` — open it (with group exclusivity) if closed, close it if open. The one-key behavior behind a keybind like `B` for the bag.
290
+
262
291
  ## @jgengine/core/ui/selectionModel
263
292
 
264
293
  - `EntitySummaryDef` (interface): interface EntitySummaryDef — A caller-provided summary of one selectable entity — the DATA input. `kind` is the grouping key used to bucket a large selection (unit type, catalog id); `icon` is a portrait id the renderer resolves. Only `id` is required.
@@ -317,6 +346,8 @@
317
346
  - `CardFace` (function): function CardFace({ rank, suit, faceDown = false, width = 64, height = 90, className, style, children, }: { rank: Rank; suit: Suit; faceDown?: boolean; width?: number; height?: number; className?: string; style?: CSSProperties; children?: ReactNode; }): React.JSX.Element — A single playing-card face: rank + suit pips, or a patterned back when `faceDown`.
318
347
  - `CardStack` (function): function CardStack({ pile, zone, cardOf, ...rest }: { pile: CardPileState; zone: string; cardOf: (id: string) => PlayingCard; } & Omit<Parameters<typeof StackedPile>[0], "cards">): React.JSX.Element — `StackedPile` bound to a headless `CardPileState` zone: reads the ordered card ids from `pile.zones[zone]` and resolves each to a `PlayingCard` via `cardOf`.
319
348
  - `ChannelTabs` (function): function ChannelTabs({ channels, active, onSelect, className, tabClassName, activeTabClassName, renderTab, }: { channels?: readonly string[]; active: string; onSelect: (channelId: string) => void; className?: string; tabClassName?: string; activeTabClassName?: string; renderTab?: (channelId: string,… — ⚠ undocumented
349
+ - `CharacterSheet` (function): function CharacterSheet({ slots, stats, name, subtitle, portrait, header, title = "Character", variation = "themed", slotSize, onSlotActivate, className, style, }: CharacterSheetProps): ReactNode — Drop-in character sheet: a {@link HudFrame} wrapping an optional name/subtitle/portrait header, the equip-slot {@link Paperdoll}, and the derived-stat {@link StatList}. The game supplies slots, stats, header content, and the activation callback, and owns the window that toggles it (this component does not manage its own open/close). Pure composition over existing primitives and HudTheme tokens.
350
+ - `CharacterSheetProps` (interface): interface CharacterSheetProps — Props for {@link CharacterSheet}.
320
351
  - `ChatBubble` (interface): interface ChatBubble — ⚠ undocumented
321
352
  - `ChatBubblesOptions` (interface): interface ChatBubblesOptions — ⚠ undocumented
322
353
  - `ChatInput` (function): function ChatInput({ channelId, className, inputClassName, buttonClassName, placeholder, sendLabel, onSent, onRejected, }: { channelId: string; className?: string; inputClassName?: string; buttonClassName?: string; placeholder?: string; sendLabel?: ReactNode; onSent?: (message: ChatMessage) => void;… — ⚠ undocumented
@@ -334,6 +365,7 @@
334
365
  - `CurrencyPill` (function): function CurrencyPill({ currencyId, className }: { currencyId: string; className?: string }): React.JSX.Element — ⚠ undocumented
335
366
  - `DEFAULT_BAR_TOKENS` (const): const DEFAULT_BAR_TOKENS: Required<BarTokens> — Built-in default values for every vitals token — scaffolding/preview only, not a shipped look.
336
367
  - `DeathScreen` (function): function DeathScreen({ statId = "health", open, className, children, }: { statId?: string; open?: boolean; className?: string; children?: ReactNode; }): React.JSX.Element — ⚠ undocumented
368
+ - `DefaultEquipSlotId` (type): type DefaultEquipSlotId = | "head" | "chest" | "hands" | "legs" | "feet" | "mainHand" | "offHand" | "ring1" | "ring2" | "trinket" — Slot ids used by {@link defaultEquipLayout}'s convenience arrangement.
337
369
  - `DialogueBox` (function): function DialogueBox({ dialogue, onChoice, rng, className, lineClassName, speakerClassName, choicesClassName, choiceClassName, checkClassName, }: { dialogue: DialogueDef; onChoice?: (choice: DialogueChoice, result: CheckResult | null) => void; rng?: () => number; className?: string; lineClassName?: … — ⚠ undocumented
338
370
  - `DialogueCheck` (interface): interface DialogueCheck — ⚠ undocumented
339
371
  - `DialogueChoice` (interface): interface DialogueChoice — ⚠ undocumented
@@ -350,6 +382,9 @@
350
382
  - `EmoteWheel` (function): function EmoteWheel({ emotes, radius, open = true, className, emoteClassName, onPlayed, onRejected, renderEmote, }: { emotes: readonly string[]; radius?: number; open?: boolean; className?: string; emoteClassName?: string; onPlayed?: (emoteId: string) => void; onRejected?: (reason: string) => void; … — ⚠ undocumented
351
383
  - `EntityPortrait` (function): function EntityPortrait({ entity, selected = false, size = 44, vitalId = "health", tabIndex, onSelect, renderIcon, className, style, }: { entity: EntitySummaryDef; selected?: boolean; size?: number; vitalId?: string; tabIndex?: number; onSelect?: (id: string) => void; renderIcon?: (entity: EntitySum… — A selectable entity portrait tile — icon/initials, an optional thin health strip, and complete selected/hover/focus states as a real `<button>` (`aria-pressed`). The unit-of-a-selection chrome; swap the face via `renderIcon`. Accessible and keyboard-focusable.
352
384
  - `EntitySummary` (function): function EntitySummary({ entity, renderVital, className, style, }: { entity: EntitySummaryDef; renderVital?: (vital: EntityVital) => ReactNode; className?: string; style?: CSSProperties; }): React.JSX.Element — The primary-entity detail chrome — name, tags, and full vital bars for the focused member. The "who is selected" summary a single-selection or the head of a multi-selection shows. Swap the vital rows via `renderVital`.
385
+ - `EquipSlot` (interface): interface EquipSlot — One named equip slot: a stable `id`, a display `label`, its worn `item` (or `null` = empty), and where it sits.
386
+ - `EquipSlotLayout` (type): type EquipSlotLayout = readonly EquipSlot[] — An ordered set of equip slots that make up a paperdoll. Caller-supplied; see {@link defaultEquipLayout}.
387
+ - `EquipSlotPosition` (type): type EquipSlotPosition = "left" | "right" | "bottom" — Character-sheet / paperdoll composition (#1033 follow-up): a drop-in `<CharacterSheet>` that arranges caller-supplied equip slots as a paperdoll around a portrait and lists derived stats, all inside a {@link HudFrame}. It reuses the atomic slot rendering ({@link EquipmentSlots}) and the shared `--jg-slot-*` / `--jg-accent` HudTheme tokens (#1034) — the game owns which slots and which stats matter (both are props), the skin, and the window that toggles it open/closed. No genre list is baked in; {@link defaultEquipLayout} is a convenience arrangement the game can override wholesale.
353
388
  - `EquipmentSlots` (function): function EquipmentSlots(props: SlotGridProps): React.JSX.Element — Worn-gear slots (head/chest/hands/…): one atomic grid, painted icons, themed from tokens.
354
389
  - `EventMeterView` (interface): interface EventMeterView — A rendered snapshot of an {@link EventMeter}: current value, fill fraction, active tier, and ready-to-consume flag.
355
390
  - `EventfulEngineStore` (interface): interface EventfulEngineStore<TEventMap extends object> — ⚠ undocumented
@@ -357,6 +392,7 @@
357
392
  - `FriendRequestsList` (function): function FriendRequestsList({ className, rowClassName, acceptClassName, declineClassName, emptyState, renderRequest, }: { className?: string; rowClassName?: string; acceptClassName?: string; declineClassName?: string; emptyState?: ReactNode; renderRequest?: (request: FriendRequestEntry) => ReactNode… — ⚠ undocumented
358
393
  - `FriendRow` (function): function FriendRow({ friend, className, dotClassName, children, }: { friend: FriendEntry; className?: string; dotClassName?: string; children?: ReactNode; }): React.JSX.Element — ⚠ undocumented
359
394
  - `FriendsList` (function): function FriendsList({ className, rowClassName, dotClassName, emptyState, renderFriend, }: { className?: string; rowClassName?: string; dotClassName?: string; emptyState?: ReactNode; renderFriend?: (friend: FriendEntry) => ReactNode; }): React.JSX.Element — ⚠ undocumented
395
+ - `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.
360
396
  - `GameIdentityProvider` (function): function GameIdentityProvider({ source, children, }: { source: IdentitySource; children?: ReactNode; }): React.JSX.Element — ⚠ undocumented
361
397
  - `GamePreviewComponent` (type): type GamePreviewComponent = ComponentType<GamePreviewProps> — ⚠ undocumented
362
398
  - `GamePreviewProps` (type): type GamePreviewProps = { className?: string; } — ⚠ undocumented
@@ -390,6 +426,9 @@
390
426
  - `IconTreatmentProps` (interface): interface IconTreatmentProps — Props for {@link IconTreatment}.
391
427
  - `IconsPreview` (function): function IconsPreview({ className }: { className?: string }): React.JSX.Element — Renders the school-gradient row and a treated-icon hotbar — the deterministic #1035 fixture.
392
428
  - `IdentitySource` (interface): interface IdentitySource — ⚠ undocumented
429
+ - `InventoryGrid` (function): function InventoryGrid({ inventoryId, columns, size = 44, gap = 6, itemIcon, onActivate, className, style, }: InventoryGridProps): React.JSX.Element — A drop-in inventory grid with drag/swap/stack-merge/split and full keyboard control, bound to `inventoryId`.
430
+ - `InventoryGridBinding` (interface): interface InventoryGridBinding — Live slots plus `move`/`split` actions bound to `inventoryId`, routed through the notifying `inventory.move`/`inventory.split` commands so React re-renders.
431
+ - `InventoryGridProps` (interface): interface InventoryGridProps — Props for {@link InventoryGrid}.
393
432
  - `InviteToWorldButton` (function): function InviteToWorldButton({ toUserId, target, className, children, onInvited, onRejected, }: { toUserId: string; target: WorldInviteTarget; className?: string; children?: ReactNode; onInvited?: (inviteId: string) => void; onRejected?: (reason: string) => void; }): React.JSX.Element | null — ⚠ undocumented
394
433
  - `JoinByCode` (function): function JoinByCode({ onJoin, className, inputClassName, buttonClassName, placeholder, children, }: { onJoin: (code: string) => void; className?: string; inputClassName?: string; buttonClassName?: string; placeholder?: string; children?: ReactNode; }): React.JSX.Element — ⚠ undocumented
395
434
  - `KeyHint` (function): function KeyHint({ children, className, style, }: { children: ReactNode; className?: string; style?: CSSProperties; }): React.JSX.Element | null — Wraps a keyboard/mouse control hint (a cap plus what it does). Renders nothing on coarse-pointer devices — a touchscreen has no keyboard, so the hint is noise and only fights the on-screen controls for space. The `data-jg-kbd-hint` marker is also hidden by an engine stylesheet as a hydration-safe backstop, so a hint stays hidden on touch even before this component's effect runs. Put the touch equivalent (a `TouchControls` dock, a tappable button) on the coarse branch instead.
@@ -412,6 +451,11 @@
412
451
  - `MinimapTrackPip` (interface): interface MinimapTrackPip — A point marker on the {@link MinimapTrack} rail at a 0..1 fraction (e.g. a gate, the exit, or the player).
413
452
  - `MinimapTrackProps` (interface): interface MinimapTrackProps — Props for {@link MinimapTrack}.
414
453
  - `MinimapTrackSpan` (interface): interface MinimapTrackSpan — A colored span across the {@link MinimapTrack} rail, given by 0..1 `start`/`end` fractions (e.g. from core `trackFraction`).
454
+ - `PanelHost` (function): function PanelHost({ manager, render, panels, variation, shape, width, zIndexBase = 40, className, windowClassName, windowStyle, bodyStyle, }: { manager: PanelsManager; /** Content for a panel by id. Takes precedence over `panels`. */ render?: (id: string) => ReactNode; /** Static content map by pan… — Renders the open panels of a {@link usePanels} manager as absolutely-positioned, draggable, closable windows in z-order — the WoW window layer. Each window uses {@link HudFrame} chrome, is dragged by its title bar, raises focus on pointer-down, and gets its content from the `render` prop (or a `panels` map). Accessible: every window is a `role="dialog"` with an `aria-label` and a focusable close button; ESC-close is wired by the manager. Unskinned — art-direct via `HudTheme` tokens and the `variation`.
455
+ - `PanelKeyResult` (interface): interface PanelKeyResult — React chrome over the headless panel/window model (`@jgengine/core/ui/panelModel`) — the toggleable, draggable, z-stacked windows a WoW-style UI is made of (B for bag, C for character, ESC to close). `usePanels` is the DATA/HOOK layer (open/close/toggle/focus + a keybind listener), `PanelHost` renders the open windows in z-order, and `Window` is a standalone one-off window. All unskinned and token-driven: games reskin via `HudTheme` tokens and by swapping the `HudFrame` variation.
456
+ - `PanelsManager` (interface): interface PanelsManager — The live panel manager returned by {@link usePanels} and consumed by {@link PanelHost}.
457
+ - `Paperdoll` (function): function Paperdoll({ slots, portrait, slotSize = 44, onSlotActivate, className, style }: PaperdollProps): ReactNode — Arranges caller-supplied equip slots as a paperdoll — worn gear in two flanking columns around a center `portrait` (or a silhouette placeholder), with any `bottom` slots in a row beneath it. Each slot reuses the atomic {@link EquipmentSlots} tile (filled icon or themed empty placeholder), carries a label, and is a focusable `role="group"` with an `aria-label`; `onSlotActivate` fires on click/Enter/Space so games can wire drag or an equip picker. Composition and skin stay the game's.
458
+ - `PaperdollProps` (interface): interface PaperdollProps — Props for {@link Paperdoll}.
415
459
  - `PartyFrame` (function): function PartyFrame({ className, rowClassName, dotClassName, emptyState, renderMember, }: { className?: string; rowClassName?: string; dotClassName?: string; emptyState?: ReactNode; renderMember?: (member: PartyMemberEntry) => ReactNode; }): React.JSX.Element — ⚠ undocumented
416
460
  - `PartyInviteToast` (function): function PartyInviteToast({ className, acceptClassName, declineClassName, renderInvite, }: { className?: string; acceptClassName?: string; declineClassName?: string; renderInvite?: (invite: PartyInviteEntry) => ReactNode; }): React.JSX.Element | null — ⚠ undocumented
417
461
  - `PartyMemberRow` (function): function PartyMemberRow({ member, className, dotClassName, children, }: { member: PartyMemberEntry; className?: string; dotClassName?: string; children?: ReactNode; }): React.JSX.Element — ⚠ undocumented
@@ -450,7 +494,7 @@
450
494
  - `SkillCheckBar` (function): function SkillCheckBar({ config, startedAt, className, trackClassName, zoneClassName, markerClassName, renderStatus, }: { config: SkillCheckConfig; startedAt: number; className?: string; trackClassName?: string; zoneClassName?: string; markerClassName?: string; renderStatus?: (result: SkillCheckResu… — ⚠ undocumented
451
495
  - `SlotGrid` (function): function SlotGrid({ inventoryId, className, renderSlot, }: { inventoryId: string; className?: string; renderSlot?: (slot: InventorySlot, index: number) => ReactNode; }): React.JSX.Element — ⚠ undocumented
452
496
  - `SlotGridProps` (interface): interface SlotGridProps — Props shared by every atomic slot grid.
453
- - `SlotItem` (interface): interface SlotItem — Atomic, purpose-named slot grids (#1033): one component per category — `EquipmentSlots`, `WeaponSlots`, `ArmorSlots`, `PotionSlots` — never a mega inventory that bundles every category. Each renders a grid of painted {@link IconTreatment} tiles (glyph + gradient + count badge) for filled slots and a themed placeholder for empty ones, all reading the shared `--jg-slot-*` / `--jg-accent` HudTheme tokens (#1034). Composition stays the game's; the engine ships the grid.
497
+ - `SlotItem` (interface): interface SlotItem — Atomic, purpose-named slot grids (#1033): one component per category — `EquipmentSlots`, `WeaponSlots`, `ArmorSlots`, `PotionSlots` — for paperdoll / equip layouts where each grid holds a single gear category. A full multi-category backpack that binds to a live inventory with drag/swap/stack/split is a different, supported shape: reach for `<InventoryGrid>` (`inventoryGrid.tsx`) there, and use these atomic grids for the equip slots around it. Each renders a grid of painted {@link IconTreatment} tiles (glyph + gradient + count badge) for filled slots and a themed placeholder for empty ones, all reading the shared `--jg-slot-*` / `--jg-accent` HudTheme tokens (#1034). Composition stays the game's; the engine ships the grid.
454
498
  - `SoulBar` (const): const SoulBar: (props: AtomicBarProps) => React.JSX.Element — Resource-charge / soul-box meter — segmented by default.
455
499
  - `SpeakingIndicator` (function): function SpeakingIndicator({ voice, userId, className, threshold = 0.01, children, }: { voice: VoiceState; userId: string; className?: string; threshold?: number; children?: ReactNode; }): React.JSX.Element — ⚠ undocumented
456
500
  - `Speedometer` (function): function Speedometer({ entityId, scale = 3.6, unit = "km/h", max = 60, size = 96, style, className, }: { entityId?: string; scale?: number; unit?: string; max?: number; size?: number; style?: CSSProperties; className?: string; }): React.JSX.Element — A speedometer for an entity (defaults to the local player) — an SVG arc gauge + a digital readout. `scale` converts world units/second to your display unit (3.6 → km/h, 2.237 → mph); `max` sets the gauge's top of scale.
@@ -459,12 +503,16 @@
459
503
  - `StartScreen` (function): function StartScreen({ open = true, className, style, children, settings, settingsPlacement = "top-right", settingsClassName, settingsWrapperClassName, }: { open?: boolean; className?: string; style?: CSSProperties; children?: ReactNode; settings?: boolean | ReactNode; settingsPlacement?: StartScree… — Composable title/attract-screen scaffold: a full-bleed `data-jg-menu` overlay that positions and centers the game's own content, with an opt-in settings corner. It imposes no look — the game supplies the title, art, and buttons as children and styles the container through `className`/`style` (per the composable-chrome rule: a placement hook, not a mandated menu).
460
504
  - `StartScreenCorner` (type): type StartScreenCorner = "top-left" | "top-right" | "bottom-left" | "bottom-right" — Which corner of a `StartScreen` the opt-in settings slot is pinned to.
461
505
  - `StatBar` (function): function StatBar({ statId = "health", entityId, tone = "health", label, showValue = true, width = 200, icon, style, className, }: { statId?: string; entityId?: string; tone?: StatTone; label?: string; showValue?: boolean; width?: number; icon?: ReactNode; style?: CSSProperties; className?: string; }… — A polished stat bar (health/mana/stamina/shield/xp) — a rounded, glassy meter with a tone-colored fill and an optional value readout. Reads `statId` off `entityId` (defaults to the local player). Renders nothing until the stat exists.
506
+ - `StatList` (function): function StatList({ stats, className, style }: StatListProps): ReactNode — Renders derived character stats as clean small-caps label→value rows, token-themed. Rows carrying a `group` field are bucketed under a small uppercase header (consecutive same-group rows share one). Pure and prop-driven — the game supplies the rows (from `useEntityStat`/derived values); the engine ships no stat list of its own.
507
+ - `StatListProps` (interface): interface StatListProps — Props for {@link StatList}.
462
508
  - `StatMeter` (function): function StatMeter({ instanceId, statId, className, fillClassName, }: { instanceId: string; statId: string; className?: string; fillClassName?: string; }): React.JSX.Element | null — A headless, unstyled stat meter — an accessible `role="progressbar"` you paint entirely through `className`/`fillClassName`. This is the class-hook primitive for teams supplying their own CSS; for a batteries-included themed bar use the atomic, token-styled components in `@jgengine/react/bars` (`HealthBar`, `ShieldBar`, …). Renamed from `HealthBar` in #1033 so the purpose-named atomic bar owns that name and no two components share it.
509
+ - `StatRow` (interface): interface StatRow — One derived-stat row. `group` optionally buckets rows under a small header.
463
510
  - `StatTone` (type): type StatTone = keyof typeof TONES — A stat tone — picks the bar's color ramp.
464
511
  - `Suit` (type): type Suit = "clubs" | "diamonds" | "hearts" | "spades" — The four French-deck suits.
465
512
  - `ToastStack` (function): function ToastStack({ action, limit = 4, className, renderToast, }: { action: string; limit?: number; className?: string; renderToast?: (entry: FeedEntry, index: number) => ReactNode; }): React.JSX.Element | null — Render `ctx.game.feed`'s entries for `action` as a newest-first toast stack — the feed-backed sibling of `@jgengine/core/game/toasts`' `createToastQueue`. Reach for this when the message source is already an engine event bound onto `ctx.game.feed` (kill feed, quest updates, loot log); reach for a `createToastQueue` when the game raises ad-hoc messages that need their own TTL independent of the feed's ring buffer.
466
513
  - `UseActionBarOptions` (interface): interface UseActionBarOptions — Options for {@link useActionBar}.
467
514
  - `UseAxisChannelResult` (interface): interface UseAxisChannelResult — ⚠ undocumented
515
+ - `UsePanelsOptions` (interface): interface UsePanelsOptions — Options for {@link usePanels}.
468
516
  - `UseSelectionViewOptions` (interface): interface UseSelectionViewOptions — Options for {@link useSelectionView}.
469
517
  - `UseVoiceOptions` (interface): interface UseVoiceOptions — ⚠ undocumented
470
518
  - `UserBadge` (function): function UserBadge({ className, avatarClassName, nameClassName, renderBadge, }: { className?: string; avatarClassName?: string; nameClassName?: string; renderBadge?: (session: AuthSession) => ReactNode; }): React.JSX.Element | null — ⚠ undocumented
@@ -474,6 +522,7 @@
474
522
  - `VoiceState` (interface): interface VoiceState — ⚠ undocumented
475
523
  - `WaveBanner` (function): function WaveBanner({ wave, label = "Wave", subtitle, style, className, }: { wave: number | string; label?: string; subtitle?: ReactNode; style?: CSSProperties; className?: string; }): React.JSX.Element — A wave / round banner — a bold centered pill for "WAVE 3" style callouts, with an optional subtitle (enemies remaining, timer). Pure display: pass the current `wave` and whatever subtitle you track.
476
524
  - `WeaponSlots` (function): function WeaponSlots(props: SlotGridProps): React.JSX.Element — Weapon loadout slots.
525
+ - `Window` (function): function Window({ title, ariaLabel, closable = true, onClose, x, y, onMove, z, width, variation, shape, className, style, bodyStyle, children, }: { title: ReactNode; ariaLabel?: string; closable?: boolean; onClose?: () => void; /** Initial (uncontrolled) or current (controlled, with `onMove`) x posi… — A single standalone window — title bar, close button, and title-bar drag over {@link HudFrame} chrome — usable without the {@link usePanels} manager for a one-off dialog. Position is uncontrolled (seeded by `x`/`y`) unless both `x`/`y` and `onMove` are supplied, in which case the caller owns placement. Accessible: `role="dialog"` with an `aria-label` and a focusable close button.
477
526
  - `WorldBrowser` (function): function WorldBrowser({ listings, onJoin, className, rowClassName, joinClassName, emptyState, renderListing, }: { listings: readonly SessionListing[]; onJoin: (listing: SessionListing) => void; className?: string; rowClassName?: string; joinClassName?: string; emptyState?: ReactNode; renderListing?:… — ⚠ undocumented
478
527
  - `WorldBrowserState` (interface): interface WorldBrowserState — ⚠ undocumented
479
528
  - `WorldInviteToast` (function): function WorldInviteToast({ className, acceptClassName, declineClassName, onAccepted, renderInvite, }: { className?: string; acceptClassName?: string; declineClassName?: string; onAccepted: (target: WorldInviteTarget) => void; renderInvite?: (invite: WorldInvite) => ReactNode; }): React.JSX.Element … — ⚠ undocumented
@@ -487,6 +536,7 @@
487
536
  - `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.
488
537
  - `clerkIdentity` (function): function clerkIdentity(state: ClerkUserState, options?: { signOut?: () => void }): IdentitySource — ⚠ undocumented
489
538
  - `createHeldKeyTracker` (function): function createHeldKeyTracker(target: HeldKeyEventTarget): { isDown: (code: string) => boolean; dispose: () => void; } — ⚠ undocumented
539
+ - `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}.
490
540
  - `defaultHudTheme` (const): const defaultHudTheme: HudTheme — Default theme tokens for previews/scaffold — not a finished game look; author a custom theme.
491
541
  - `eventMeterNeedsHeartbeat` (function): function eventMeterNeedsHeartbeat(meter: EventMeter, previous: EventMeterView | null): boolean — True when `meter`'s value/fraction/tier/ready diverge from `previous` — the re-render check `useEventMeter` polls on its heartbeat.
492
542
  - `guestIdentity` (function): function guestIdentity(seed?: string): IdentitySource — ⚠ undocumented
@@ -495,6 +545,7 @@
495
545
  - `isRedSuit` (function): function isRedSuit(suit: Suit): boolean — True for the red suits (hearts, diamonds).
496
546
  - `latestChatBubbles` (function): function latestChatBubbles(messages: readonly ChatMessage[], nowMs: number, ttlMs: number): ChatBubble[] — ⚠ undocumented
497
547
  - `localPlayerEntity` (function): function localPlayerEntity(ctx: GameContext): SceneEntity | null — ⚠ undocumented
548
+ - `panelKeyAction` (function): function panelKeyAction(defs: readonly PanelDef[], state: PanelState, event: { code?: string; key?: string }): PanelKeyResult — Pure keybind resolver for a panel set: ESC resolves to `closeTop` when a closable window is open, and any other key routes through {@link panelByHotkey} (trying `code` then `key`) to a `toggle`. Returns the intent without touching the DOM, so it unit-tests headless and `usePanels` is a thin shell over it.
498
549
  - `resolveDialogueInvoke` (function): function resolveDialogueInvoke(choice: DialogueChoice, result: CheckResult | null): { command: string; args?: unknown } | null — ⚠ undocumented
499
550
  - `resolveHudTheme` (function): function resolveHudTheme(theme?: HudThemePreset | HudTheme): HudTheme — Resolves a preset name (or a full theme) to a `HudTheme`; falls back to the default theme.
500
551
  - `runDialogueChoice` (function): function runDialogueChoice(commands: { run(name: string, input?: unknown): unknown }, choice: DialogueChoice, result: CheckResult | null): void — Route a {@link DialogueBox} choice through the `features.dialogue` bridge: resolve the choice's invoke (honoring a skill-check `result`), run that command, and otherwise close the dialogue — the write side that replaces a per-game `onChoice` that hand-rolls `resolveDialogueInvoke` + `dialogue.close`.
@@ -534,6 +585,7 @@
534
585
  - `useHudLayoutPersist` (function): function useHudLayoutPersist(): HudLayoutPersist | null — Injected panel-commit port, or `null` when no host has provided one.
535
586
  - `useHudViewport` (function): function useHudViewport(): HudViewportContextValue | null — ⚠ undocumented
536
587
  - `useInventory` (function): function useInventory(inventoryId: string): readonly InventorySlot[] — ⚠ undocumented
588
+ - `useInventoryGrid` (function): function useInventoryGrid(inventoryId: string): InventoryGridBinding — Binds a live inventory to a HUD grid: `slots` from {@link useInventory}, and `move`/`split` that run the built-in `inventory.move`/`inventory.split` commands (which notify, so the grid re-renders).
537
589
  - `useKeyedStore` (function): function useKeyedStore<T>(handle: KeyedStoreHandle<T>, id: string): T — Subscribe a component to one owner's slot of a typed keyed-family store defined with `defineKeyedStore`. Returns the current value for `id` (or the definition's initial before any write for that id), re-rendering only when that owner's slot changes — the cast-free, boilerplate-free replacement for a hand-written `useGameStore((ctx) => ctx.game.store.get(`prefix:${id}`) as T)`.
538
590
  - `useLayoutCollisions` (function): function useLayoutCollisions(): readonly LayoutCollision[] — Live forbidden/warned region collisions (empty outside a provider).
539
591
  - `useLeaderboard` (function): function useLeaderboard(stat: string, options: { scope: LeaderboardScope; limit?: number }): { userId: string; value: number }[] — ⚠ undocumented
@@ -544,6 +596,7 @@
544
596
  - `useOpenDialogueId` (function): function useOpenDialogueId(): string | null — The id of the dialogue `ctx.game.dialogue` (or a `talkable(id)` prompt) currently has open, or `null`. The read side of the `features.dialogue` bridge — a panel looks the id up in its own dialogue catalog and renders {@link DialogueBox}, with no per-game open/close store.
545
597
  - `useOptionalGameContext` (function): function useOptionalGameContext(): GameContext | null — The game context if a `GameProvider` is present, otherwise `null` — for chrome that may render outside a running game (showcases, previews).
546
598
  - `useOptionalGamePhase` (function): function useOptionalGamePhase(): GamePhase — Live run phase that degrades to `"playing"` when rendered outside a `GameProvider` (component showcases, previews), so phase-gated chrome never throws.
599
+ - `usePanels` (function): function usePanels(defs: readonly PanelDef[], options?: UsePanelsOptions): PanelsManager — The DATA/HOOK layer: wrap the core panel model in state and install a window keybind listener — per-panel hotkeys toggle their window and ESC closes the topmost closable one. Hotkeys are ignored while the player is typing in an input/textarea/contenteditable. The listener is cleaned up on unmount and SSR-safe (armed in an effect). Feed the returned manager to {@link PanelHost}, or drive a bespoke window layout from it directly.
547
600
  - `useParty` (function): function useParty(): PartyMemberEntry[] — ⚠ undocumented
548
601
  - `usePartyInvites` (function): function usePartyInvites(): PartyInviteEntry[] — ⚠ undocumented
549
602
  - `usePlayer` (function): function usePlayer(): { userId: string; isNew: boolean } — ⚠ undocumented
@@ -618,6 +671,21 @@
618
671
  - `Suit` (type): type Suit = "clubs" | "diamonds" | "hearts" | "spades" — The four French-deck suits.
619
672
  - `isRedSuit` (function): function isRedSuit(suit: Suit): boolean — True for the red suits (hearts, diamonds).
620
673
 
674
+ ## @jgengine/react/characterSheet
675
+
676
+ - `CharacterSheet` (function): function CharacterSheet({ slots, stats, name, subtitle, portrait, header, title = "Character", variation = "themed", slotSize, onSlotActivate, className, style, }: CharacterSheetProps): ReactNode — Drop-in character sheet: a {@link HudFrame} wrapping an optional name/subtitle/portrait header, the equip-slot {@link Paperdoll}, and the derived-stat {@link StatList}. The game supplies slots, stats, header content, and the activation callback, and owns the window that toggles it (this component does not manage its own open/close). Pure composition over existing primitives and HudTheme tokens.
677
+ - `CharacterSheetProps` (interface): interface CharacterSheetProps — Props for {@link CharacterSheet}.
678
+ - `DefaultEquipSlotId` (type): type DefaultEquipSlotId = | "head" | "chest" | "hands" | "legs" | "feet" | "mainHand" | "offHand" | "ring1" | "ring2" | "trinket" — Slot ids used by {@link defaultEquipLayout}'s convenience arrangement.
679
+ - `EquipSlot` (interface): interface EquipSlot — One named equip slot: a stable `id`, a display `label`, its worn `item` (or `null` = empty), and where it sits.
680
+ - `EquipSlotLayout` (type): type EquipSlotLayout = readonly EquipSlot[] — An ordered set of equip slots that make up a paperdoll. Caller-supplied; see {@link defaultEquipLayout}.
681
+ - `EquipSlotPosition` (type): type EquipSlotPosition = "left" | "right" | "bottom" — Character-sheet / paperdoll composition (#1033 follow-up): a drop-in `<CharacterSheet>` that arranges caller-supplied equip slots as a paperdoll around a portrait and lists derived stats, all inside a {@link HudFrame}. It reuses the atomic slot rendering ({@link EquipmentSlots}) and the shared `--jg-slot-*` / `--jg-accent` HudTheme tokens (#1034) — the game owns which slots and which stats matter (both are props), the skin, and the window that toggles it open/closed. No genre list is baked in; {@link defaultEquipLayout} is a convenience arrangement the game can override wholesale.
682
+ - `Paperdoll` (function): function Paperdoll({ slots, portrait, slotSize = 44, onSlotActivate, className, style }: PaperdollProps): ReactNode — Arranges caller-supplied equip slots as a paperdoll — worn gear in two flanking columns around a center `portrait` (or a silhouette placeholder), with any `bottom` slots in a row beneath it. Each slot reuses the atomic {@link EquipmentSlots} tile (filled icon or themed empty placeholder), carries a label, and is a focusable `role="group"` with an `aria-label`; `onSlotActivate` fires on click/Enter/Space so games can wire drag or an equip picker. Composition and skin stay the game's.
683
+ - `PaperdollProps` (interface): interface PaperdollProps — Props for {@link Paperdoll}.
684
+ - `StatList` (function): function StatList({ stats, className, style }: StatListProps): ReactNode — Renders derived character stats as clean small-caps label→value rows, token-themed. Rows carrying a `group` field are bucketed under a small uppercase header (consecutive same-group rows share one). Pure and prop-driven — the game supplies the rows (from `useEntityStat`/derived values); the engine ships no stat list of its own.
685
+ - `StatListProps` (interface): interface StatListProps — Props for {@link StatList}.
686
+ - `StatRow` (interface): interface StatRow — One derived-stat row. `group` optionally buckets rows under a small header.
687
+ - `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}.
688
+
621
689
  ## @jgengine/react/chat
622
690
 
623
691
  - `ChannelTabs` (function): function ChannelTabs({ channels, active, onSelect, className, tabClassName, activeTabClassName, renderTab, }: { channels?: readonly string[]; active: string; onSelect: (channelId: string) => void; className?: string; tabClassName?: string; activeTabClassName?: string; renderTab?: (channelId: string,… — ⚠ undocumented
@@ -709,6 +777,7 @@
709
777
 
710
778
  - `AbilitySlotBindingOptions` (interface): interface AbilitySlotBindingOptions — ⚠ undocumented
711
779
  - `EventMeterView` (interface): interface EventMeterView — A rendered snapshot of an {@link EventMeter}: current value, fill fraction, active tier, and ready-to-consume flag.
780
+ - `InventoryGridBinding` (interface): interface InventoryGridBinding — Live slots plus `move`/`split` actions bound to `inventoryId`, routed through the notifying `inventory.move`/`inventory.split` commands so React re-renders.
712
781
  - `UseAxisChannelResult` (interface): interface UseAxisChannelResult — ⚠ undocumented
713
782
  - `WorldBrowserState` (interface): interface WorldBrowserState — ⚠ undocumented
714
783
  - `abilityKitNeedsHeartbeat` (function): function abilityKitNeedsHeartbeat(kit: AbilityKit, resourceAvailable?: number): boolean — ⚠ undocumented
@@ -733,6 +802,7 @@
733
802
  - `useGameStoreValue` (function): function useGameStoreValue<T>(key: string, fallback: T): T — Subscribe to a single keyed slice of the game store, returning `fallback` until the key is set.
734
803
  - `useHeldKeys` (function): function useHeldKeys(): (code: string) => boolean — Held-key predicate backed by window keydown/keyup/blur listeners (blur clears held state so a released-off-window key doesn't stick). SSR-safe: listeners attach in an effect, never at module scope. The returned predicate is stable across renders.
735
804
  - `useInventory` (function): function useInventory(inventoryId: string): readonly InventorySlot[] — ⚠ undocumented
805
+ - `useInventoryGrid` (function): function useInventoryGrid(inventoryId: string): InventoryGridBinding — Binds a live inventory to a HUD grid: `slots` from {@link useInventory}, and `move`/`split` that run the built-in `inventory.move`/`inventory.split` commands (which notify, so the grid re-renders).
736
806
  - `useLeaderboard` (function): function useLeaderboard(stat: string, options: { scope: LeaderboardScope; limit?: number }): { userId: string; value: number }[] — ⚠ undocumented
737
807
  - `useLiveMarkers` (function): function useLiveMarkers(rebuild: (markers: MarkerSet, ctx: GameContext) => void, options?: { intervalMs?: number; deps?: DependencyList }): MarkerSet — A self-ticking {@link MarkerSet} kept in sync with the live scene: on a heartbeat (default 100ms, and once immediately) it clears the set and calls `rebuild(markers, ctx)` to repopulate it from the current scene, so a HUD map never hand-rolls a `setInterval` + `markers.clear()` + rescan. Each heartbeat also re-renders the calling component, so player-centered props derived from the live scene each tick (`center`, `facingYaw`, zone name) stay fresh — this fully subsumes the hand-rolled ticker, not just the marker rebuild. The set is stable across renders and re-seeded whenever `ctx`, `intervalMs`, or the supplied `deps` change. SSR-safe: the heartbeat only arms when `window` is available.
738
808
  - `useLocalPlayerDead` (function): function useLocalPlayerDead(healthStatId = "health"): boolean — ⚠ undocumented
@@ -840,6 +910,11 @@
840
910
  - `useAuthedPlayer` (function): function useAuthedPlayer(options?: { guestSeed?: string }): PlayerIdentity | null — ⚠ undocumented
841
911
  - `useSession` (function): function useSession(): IdentitySource — ⚠ undocumented
842
912
 
913
+ ## @jgengine/react/inventoryGrid
914
+
915
+ - `InventoryGrid` (function): function InventoryGrid({ inventoryId, columns, size = 44, gap = 6, itemIcon, onActivate, className, style, }: InventoryGridProps): React.JSX.Element — A drop-in inventory grid with drag/swap/stack-merge/split and full keyboard control, bound to `inventoryId`.
916
+ - `InventoryGridProps` (interface): interface InventoryGridProps — Props for {@link InventoryGrid}.
917
+
843
918
  ## @jgengine/react/keyHint
844
919
 
845
920
  - `KeyHint` (function): function KeyHint({ children, className, style, }: { children: ReactNode; className?: string; style?: CSSProperties; }): React.JSX.Element | null — Wraps a keyboard/mouse control hint (a cap plus what it does). Renders nothing on coarse-pointer devices — a touchscreen has no keyboard, so the hint is noise and only fights the on-screen controls for space. The `data-jg-kbd-hint` marker is also hidden by an engine stylesheet as a hydration-safe backstop, so a hint stays hidden on touch even before this component's effect runs. Put the touch equivalent (a `TouchControls` dock, a tappable button) on the coarse branch instead.
@@ -873,6 +948,16 @@
873
948
  - `useFog` (function): function useFog(fog: FogField): ReturnType<FogField["cells"]> — ⚠ undocumented
874
949
  - `useMarkers` (function): function useMarkers<TMeta = unknown>(markers: MarkerSet<TMeta>): readonly MapMarker<TMeta>[] — Subscribe to a native marker set or external marker source, or read a static marker array.
875
950
 
951
+ ## @jgengine/react/panels
952
+
953
+ - `PanelHost` (function): function PanelHost({ manager, render, panels, variation, shape, width, zIndexBase = 40, className, windowClassName, windowStyle, bodyStyle, }: { manager: PanelsManager; /** Content for a panel by id. Takes precedence over `panels`. */ render?: (id: string) => ReactNode; /** Static content map by pan… — Renders the open panels of a {@link usePanels} manager as absolutely-positioned, draggable, closable windows in z-order — the WoW window layer. Each window uses {@link HudFrame} chrome, is dragged by its title bar, raises focus on pointer-down, and gets its content from the `render` prop (or a `panels` map). Accessible: every window is a `role="dialog"` with an `aria-label` and a focusable close button; ESC-close is wired by the manager. Unskinned — art-direct via `HudTheme` tokens and the `variation`.
954
+ - `PanelKeyResult` (interface): interface PanelKeyResult — React chrome over the headless panel/window model (`@jgengine/core/ui/panelModel`) — the toggleable, draggable, z-stacked windows a WoW-style UI is made of (B for bag, C for character, ESC to close). `usePanels` is the DATA/HOOK layer (open/close/toggle/focus + a keybind listener), `PanelHost` renders the open windows in z-order, and `Window` is a standalone one-off window. All unskinned and token-driven: games reskin via `HudTheme` tokens and by swapping the `HudFrame` variation.
955
+ - `PanelsManager` (interface): interface PanelsManager — The live panel manager returned by {@link usePanels} and consumed by {@link PanelHost}.
956
+ - `UsePanelsOptions` (interface): interface UsePanelsOptions — Options for {@link usePanels}.
957
+ - `Window` (function): function Window({ title, ariaLabel, closable = true, onClose, x, y, onMove, z, width, variation, shape, className, style, bodyStyle, children, }: { title: ReactNode; ariaLabel?: string; closable?: boolean; onClose?: () => void; /** Initial (uncontrolled) or current (controlled, with `onMove`) x posi… — A single standalone window — title bar, close button, and title-bar drag over {@link HudFrame} chrome — usable without the {@link usePanels} manager for a one-off dialog. Position is uncontrolled (seeded by `x`/`y`) unless both `x`/`y` and `onMove` are supplied, in which case the caller owns placement. Accessible: `role="dialog"` with an `aria-label` and a focusable close button.
958
+ - `panelKeyAction` (function): function panelKeyAction(defs: readonly PanelDef[], state: PanelState, event: { code?: string; key?: string }): PanelKeyResult — Pure keybind resolver for a panel set: ESC resolves to `closeTop` when a closable window is open, and any other key routes through {@link panelByHotkey} (trying `code` then `key`) to a `toggle`. Returns the intent without touching the DOM, so it unit-tests headless and `usePanels` is a thin shell over it.
959
+ - `usePanels` (function): function usePanels(defs: readonly PanelDef[], options?: UsePanelsOptions): PanelsManager — The DATA/HOOK layer: wrap the core panel model in state and install a window keybind listener — per-panel hotkeys toggle their window and ESC closes the topmost closable one. Hotkeys are ignored while the player is typing in an input/textarea/contenteditable. The listener is cleaned up on unmount and SSR-safe (armed in an effect). Feed the returned manager to {@link PanelHost}, or drive a bespoke window layout from it directly.
960
+
876
961
  ## @jgengine/react/preview
877
962
 
878
963
  - `GamePreviewComponent` (type): type GamePreviewComponent = ComponentType<GamePreviewProps> — ⚠ undocumented
@@ -881,6 +966,7 @@
881
966
 
882
967
  ## @jgengine/react/provider
883
968
 
969
+ - `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.
884
970
  - `GameProvider` (function): function GameProvider({ context, children }: { context: GameContext; children?: ReactNode }): React.JSX.Element — ⚠ undocumented
885
971
  - `useGameContext` (function): function useGameContext(): GameContext — ⚠ undocumented
886
972
  - `useOptionalGameContext` (function): function useOptionalGameContext(): GameContext | null — The game context if a `GameProvider` is present, otherwise `null` — for chrome that may render outside a running game (showcases, previews).
@@ -932,7 +1018,7 @@
932
1018
  - `EquipmentSlots` (function): function EquipmentSlots(props: SlotGridProps): React.JSX.Element — Worn-gear slots (head/chest/hands/…): one atomic grid, painted icons, themed from tokens.
933
1019
  - `PotionSlots` (function): function PotionSlots(props: SlotGridProps): React.JSX.Element — Consumable (health/shield potion) slots.
934
1020
  - `SlotGridProps` (interface): interface SlotGridProps — Props shared by every atomic slot grid.
935
- - `SlotItem` (interface): interface SlotItem — Atomic, purpose-named slot grids (#1033): one component per category — `EquipmentSlots`, `WeaponSlots`, `ArmorSlots`, `PotionSlots` — never a mega inventory that bundles every category. Each renders a grid of painted {@link IconTreatment} tiles (glyph + gradient + count badge) for filled slots and a themed placeholder for empty ones, all reading the shared `--jg-slot-*` / `--jg-accent` HudTheme tokens (#1034). Composition stays the game's; the engine ships the grid.
1021
+ - `SlotItem` (interface): interface SlotItem — Atomic, purpose-named slot grids (#1033): one component per category — `EquipmentSlots`, `WeaponSlots`, `ArmorSlots`, `PotionSlots` — for paperdoll / equip layouts where each grid holds a single gear category. A full multi-category backpack that binds to a live inventory with drag/swap/stack/split is a different, supported shape: reach for `<InventoryGrid>` (`inventoryGrid.tsx`) there, and use these atomic grids for the equip slots around it. Each renders a grid of painted {@link IconTreatment} tiles (glyph + gradient + count badge) for filled slots and a themed placeholder for empty ones, all reading the shared `--jg-slot-*` / `--jg-accent` HudTheme tokens (#1034). Composition stays the game's; the engine ships the grid.
936
1022
  - `WeaponSlots` (function): function WeaponSlots(props: SlotGridProps): React.JSX.Element — Weapon loadout slots.
937
1023
 
938
1024
  ## @jgengine/react/social
@@ -1036,7 +1122,9 @@
1036
1122
 
1037
1123
  ## @jgengine/shell/camera
1038
1124
 
1125
+ - `CAMERA_TRANSPARENT_USERDATA` (const): const CAMERA_TRANSPARENT_USERDATA: { readonly jgCameraTransparent: true } — userData applied to the group wrapping author `WorldOverlay` decor so the spring-arm ignores it by default.
1039
1126
  - `CameraFollowListener` (type): type CameraFollowListener = (state: CameraFollowState) => void — ⚠ undocumented
1127
+ - `CameraOccluder` (interface): interface CameraOccluder — Camera spring-arm occlusion filtering.
1040
1128
  - `CameraShakeChannel` (interface): interface CameraShakeChannel — ⚠ undocumented
1041
1129
  - `CameraShakeContext` (const): const CameraShakeContext: React.Context<CameraShakeChannel> — ⚠ undocumented
1042
1130
  - `GAME_SIM_FRAME_PRIORITY` (const): const GAME_SIM_FRAME_PRIORITY: 0 — Run simulation/movement before orbit follow so poses are current.
@@ -1053,6 +1141,7 @@
1053
1141
  - `PlayerFovState` (interface): interface PlayerFovState — ⚠ undocumented
1054
1142
  - `ViewmodelProps` (interface): interface ViewmodelProps — Props handed to a custom viewmodel component (#542): a live cue ref (velocity/bob/firing/reloading/recoil/hit) for the followed entity, driven from your own `useFrame` — read `cuesRef.current` there rather than storing it as render state.
1055
1143
  - `defaultCameraShakeChannel` (const): const defaultCameraShakeChannel: CameraShakeChannel — Process-wide default channel. A shell mounts its own channel via `CameraShakeContext`, but game systems that have no React context (e.g. a `loop.onTick` reacting to `entity.died`) can import `cameraShake` and feed the default channel directly.
1144
+ - `isCameraOccluderTransparent` (function): function isCameraOccluderTransparent(object: CameraOccluder | null | undefined): boolean — Should the camera spring-arm ignore this raycast hit? Walks the object up its `.parent` chain and honors the nearest camera tag: `jgCameraCollide === true` blocks (opt back in), `jgCameraTransparent === true` passes through. Untagged geometry blocks as before, so engine-owned ground/entities are unaffected.
1056
1145
  - `readFirstPersonMuzzle` (function): function readFirstPersonMuzzle(target: THREE.Vector3): boolean — World position of the first-person weapon muzzle, or false when no viewmodel is mounted.
1057
1146
  - `usePlayerFov` (function): function usePlayerFov(): PlayerFovState — ⚠ undocumented
1058
1147
 
@@ -1091,6 +1180,12 @@
1091
1180
 
1092
1181
  - `CameraBlendScratch` (interface): interface CameraBlendScratch — ⚠ undocumented
1093
1182
 
1183
+ ## @jgengine/shell/camera/cameraCollision
1184
+
1185
+ - `CAMERA_TRANSPARENT_USERDATA` (const): const CAMERA_TRANSPARENT_USERDATA: { readonly jgCameraTransparent: true } — userData applied to the group wrapping author `WorldOverlay` decor so the spring-arm ignores it by default.
1186
+ - `CameraOccluder` (interface): interface CameraOccluder — Camera spring-arm occlusion filtering.
1187
+ - `isCameraOccluderTransparent` (function): function isCameraOccluderTransparent(object: CameraOccluder | null | undefined): boolean — Should the camera spring-arm ignore this raycast hit? Walks the object up its `.parent` chain and honors the nearest camera tag: `jgCameraCollide === true` blocks (opt back in), `jgCameraTransparent === true` passes through. Untagged geometry blocks as before, so engine-owned ground/entities are unaffected.
1188
+
1094
1189
  ## @jgengine/shell/camera/cameraRigs
1095
1190
 
1096
1191
  - `CAMERA_POST_FRAME_PRIORITY` (const): const CAMERA_POST_FRAME_PRIORITY: number — ⚠ undocumented
@@ -1305,7 +1400,7 @@
1305
1400
 
1306
1401
  ## @jgengine/shell/environment/RoadRibbons
1307
1402
 
1308
- - `RoadRibbons` (function): function RoadRibbons({ road, field }: { road: RoadEnvironmentDescriptor; field: TerrainField }): React.JSX.Element | null — Renders one `road()` descriptor: an asphalt ribbon plus optional dashed centerline, draped on the terrain.
1403
+ - `RoadRibbons` (function): function RoadRibbons({ road, field, exclusions = [], }: { road: RoadEnvironmentDescriptor; field: TerrainField; exclusions?: readonly DashExclusion[]; }): React.JSX.Element | null — Renders one `road()` descriptor: an asphalt ribbon, a curb/edge strip along each border, plus an optional dashed centerline (draped on the terrain, and interrupted through any `exclusions` — the welded junction patches — so the center line does not paint across intersections).
1309
1404
 
1310
1405
  ## @jgengine/shell/environment/VolumetricClouds
1311
1406
 
@@ -1401,6 +1496,13 @@
1401
1496
 
1402
1497
  - `createGradePass` (function): function createGradePass(config: GradeConfig = {}): ShaderPass — Build the display-space colour-grade pass (lift/gain/gamma, saturation, vignette, grain). Advance `uniforms.uTime.value` each frame to animate the grain.
1403
1498
 
1499
+ ## @jgengine/shell/postfx/postfxOverlay
1500
+
1501
+ - `POSTFX_OVERLAY_USERDATA` (const): const POSTFX_OVERLAY_USERDATA: { readonly jgPostfxOverlay: true } — Spread onto a group's `userData` to mark its subtree as a screen/world overlay effect — additive glows, telegraphs, tracers, debug gizmos — that post-processing scene prepasses (GTAO's normal/depth render, DOF's depth render) must skip. Those prepasses render the scene with `overrideMaterial`, which turns transparent overlay quads into opaque stamps in the AO/DOF buffers (visible as black squares over hit sparks). The shell marks its own combat VFX and collision-debug mounts; games mark custom overlay effects the same way.
1502
+ - `hidePostfxOverlays` (function): function hidePostfxOverlays(scene: THREE.Object3D, out: THREE.Object3D[]): void — Hide every currently visible marked object under `scene`, recording what was hidden into `out` (cleared first, no allocation on the hot path) so {@link restorePostfxOverlays} can undo exactly that set.
1503
+ - `isPostfxOverlay` (function): function isPostfxOverlay(object: { userData?: Record<string, unknown> }): boolean — True when the object opted out of postfx scene prepasses via {@link POSTFX_OVERLAY_USERDATA}.
1504
+ - `restorePostfxOverlays` (function): function restorePostfxOverlays(hidden: THREE.Object3D[]): void — Restore visibility for the objects hidden by {@link hidePostfxOverlays} and clear the list.
1505
+
1404
1506
  ## @jgengine/shell/registry
1405
1507
 
1406
1508
  - `GameRegistry` (type): type GameRegistry = Record<string, () => Promise<PlayableGame>> — ⚠ undocumented
@@ -1409,6 +1511,15 @@
1409
1511
  - `RenderObject` (type): type RenderObject = (object: SceneObject) => ReactNode — ⚠ undocumented
1410
1512
  - `resolveGameLoader` (function): function resolveGameLoader(registry: GameRegistry, gameId: string, fallbackGameId?: string): (() => Promise<PlayableGame>) | undefined — ⚠ undocumented
1411
1513
 
1514
+ ## @jgengine/shell/render/EntityPreview
1515
+
1516
+ - `EntityPreview` (function): function EntityPreview({ children, model, instanceId, mood = "studio", backdrop, turntable = 0, faceCamera = false, forward, environment = true, cameraPosition = [0, 1, 3], fov = 35, dpr = [1, 2], className, style, }: EntityPreviewProps): React.JSX.Element — Drop-in live 3D preview of a single entity — the character-screen portrait, unit inspector, or loadout viewer. It owns all the plumbing a game would otherwise re-derive by hand: a nested `<Canvas>`, the {@link GameContextBridge} that carries the running `GameContext` across the R3F reconciler boundary (so the subject's `useEntityRenderCues` / `useGameContext` hooks resolve and it mirrors the live game), a {@link StudioStage} 3-point lighting rig with optional turntable / face-camera, and a capture-friendly GL surface. The game keeps ownership of its `renderEntity`: pass it as `children`, or hand a native `model` (+ `instanceId`) for an engine-composed body. Bound to a live `instanceId`, the portrait walks, flinches, and topples in lockstep with the world instead of a canned spin.
1517
+ - `EntityPreviewProps` (interface): interface EntityPreviewProps — Props for {@link EntityPreview}.
1518
+
1519
+ ## @jgengine/shell/render/PartMotion
1520
+
1521
+ - `PartMotionRig` (function): function PartMotionRig({ parts, model, instanceId, renderPart, children, }: { parts: readonly ModelPart[]; model: ModelConfig; instanceId?: string; /** Renders one part's model content (the shell passes its part renderer to avoid an import cycle). */ renderPart: (part: ModelPart, index: number) => R… — Procedural motion rig for a rig-less part-composed character (`ModelPart.role` — see `@jgengine/core/game/partAnimation`). Wraps the whole composition in a root group that bobs, breathes, flinches on `combat.hitReaction`, and topples on `entity.died`, while each role-tagged part swings around its authored transform — legs/arms counter-phase from the entity's live movement speed, head counter-sway, tail wag, wing flap. Untagged parts render as static kit pieces. Children are the base model content (primitive, attachments).
1522
+
1412
1523
  ## @jgengine/shell/render/SceneLighting
1413
1524
 
1414
1525
  - `BackdropFog` (function): function BackdropFog({ fog }: { fog: BackdropConfig["fog"] }): React.JSX.Element | null — ⚠ undocumented
@@ -1416,15 +1527,30 @@
1416
1527
 
1417
1528
  ## @jgengine/shell/render/SceneModels
1418
1529
 
1419
- - `EntityModel` (function): function EntityModel({ model, instanceId }: { model: ModelConfig; instanceId?: string }): React.JSX.Element — ⚠ undocumented
1530
+ - `EntityModel` (function): function EntityModel({ model, instanceId, measure, }: { model: ModelConfig; instanceId?: string; measure?: MeasureTarget; }): React.JSX.Element — ⚠ undocumented
1420
1531
  - `EntitySprite` (function): function EntitySprite({ sprite }: { sprite: EntitySpriteConfig }): React.JSX.Element — ⚠ undocumented
1421
- - `IsolatedEntityModel` (function): function IsolatedEntityModel({ model, instanceId, fallback, }: { model: ModelConfig; instanceId?: string; fallback?: ReactNode; }): React.JSX.Element — ⚠ undocumented
1532
+ - `IsolatedEntityModel` (function): function IsolatedEntityModel({ model, instanceId, measure, fallback, }: { model: ModelConfig; instanceId?: string; measure?: MeasureTarget; fallback?: ReactNode; }): React.JSX.Element — ⚠ undocumented
1533
+ - `MeasureTarget` (interface): interface MeasureTarget — Where a measured model reports its rendered bounds: an entity kind or an object catalog id.
1422
1534
 
1423
1535
  ## @jgengine/shell/render/assetBase
1424
1536
 
1425
1537
  - `installAssetBase` (function): function installAssetBase(base: string): void — Installs the app base URL (pass `import.meta.env.BASE_URL`) so root-absolute asset paths load from under it. Call once at app startup, before any game loads. Bases that are not root-absolute (`/`, `./`) reset to the pass-through default. Also registers {@link resolveAssetBaseUrl} on `THREE.DefaultLoadingManager`, covering `TextureLoader`, drei's `useTexture` / `useGLTF`, and every other loader on the default manager.
1426
1538
  - `resolveAssetBaseUrl` (function): function resolveAssetBaseUrl(url: string): string — Resolves a root-absolute asset URL (`/models/…`, `/materials/…`) against the installed app base. Relative, protocol-relative, absolute-scheme (`https:`, `blob:`, `data:`), and already-based URLs pass through unchanged. Installed as the URL modifier on THREE loading managers so every model and texture load resolves correctly wherever the app is mounted.
1427
1539
 
1540
+ ## @jgengine/shell/render/measureBounds
1541
+
1542
+ - `MEASURE_EXCLUDE_KEY` (const): const MEASURE_EXCLUDE_KEY: "jgMeasureExclude" — Subtrees flagged with this userData key are excluded from bounds measurement — debug gizmos, selection quads, effect billboards that should not inflate the hitbox. Sprites and invisible subtrees are always excluded.
1543
+ - `MeasuredBoundsGroup` (function): function MeasuredBoundsGroup({ target, measureKey, children, }: { target: "entity" | "object"; measureKey: string; children: ReactNode; }): React.JSX.Element — Wraps custom-rendered actor content (`renderEntity` / `renderObject`) and reports its measured entity-local bounds into collider resolution, so the hitbox wraps what is actually on screen instead of the fixed-size fallback box. A successful measure re-reports only when the subtree gained or lost meshes (async fill-in), not when idle animation nudges the same meshes.
1544
+ - `MeasuredLocalBounds` (interface): interface MeasuredLocalBounds — Entity-local AABB of the meshes under a measured root, plus how many meshes contributed — the count lets callers detect async subtree fill-in without diffing bounds.
1545
+ - `measureLocalBounds` (function): function measureLocalBounds(root: THREE.Object3D): MeasuredLocalBounds | null — Measure the meshes under `root` in `root`'s own frame (root's transform is the measuring frame, children compose their local matrices below it). Returns `null` when nothing measurable is mounted. Skinned meshes measure at bind pose — the standard engine approximation.
1546
+ - `reportMeasuredBounds` (function): function reportMeasuredBounds(ctx: GameContext, target: "entity" | "object", key: string, bounds: MeasuredLocalBounds): void — Forward a measured result into the context (entity kind or object catalog id), deduping repeat reports of the same shape so remounts and multi-instance kinds don't churn collider resolution.
1547
+
1548
+ ## @jgengine/shell/render/measureCollisionMesh
1549
+
1550
+ - `MeasuredCollisionTriangles` (interface): interface MeasuredCollisionTriangles extends CollisionMeshSource — Entity-local triangle soup of the meshes under a measured root, plus the contributing mesh count (same async fill-in detection as measured bounds). Satisfies {@link CollisionMeshSource}.
1551
+ - `measureLocalCollisionTriangles` (function): function measureLocalCollisionTriangles(root: THREE.Object3D, transform?: { scale: number; offset: readonly [number, number, number] }): MeasuredCollisionTriangles | null — Collect the triangle soup of the meshes under `root` in `root`'s own frame — the mesh-accurate counterpart to `measureLocalBounds`, with the same visibility/sprite/{@link MEASURE_EXCLUDE_KEY} exclusions (skinned meshes contribute their bind pose). An optional uniform `scale` + `offset` maps the result into entity-local space when the caller mounts the root scaled/translated (the model-primitive path). Returns `null` when nothing is measurable or the soup exceeds the triangle budget — callers keep their box fallback.
1552
+ - `reportMeasuredCollisionMesh` (function): function reportMeasuredCollisionMesh(ctx: GameContext, target: "entity" | "object", key: string, triangles: MeasuredCollisionTriangles): void — Forward measured triangles into the context (entity kind or object catalog id), deduping repeat reports of the same soup so remounts and multi-instance kinds don't rebuild the BVH.
1553
+
1428
1554
  ## @jgengine/shell/render/modelRender
1429
1555
 
1430
1556
  - `MaterialCache` (interface): interface MaterialCache — ⚠ undocumented
@@ -1440,6 +1566,10 @@
1440
1566
 
1441
1567
  - `useEntityRenderCues` (function): function useEntityRenderCues(instanceId: string | undefined, tuning?: RenderCueTuning): MutableRefObject<EntityRenderCues> — Live motion + animation cues for one entity, read from a mutable ref inside your own `useFrame` — no re-render per frame, no diffing the parent group's position, no game-side module map for attack/hit timing. Backs both custom `renderEntity` rigs and a custom first-person viewmodel (#542): call it with `entity.id` / the local player's userId and drive bob/recoil/reload poses from `cuesRef.current` inside the calling component's own `useFrame`.
1442
1568
 
1569
+ ## @jgengine/shell/render/useModelAnimation
1570
+
1571
+ - `useModelAnimation` (function): function useModelAnimation(scene: THREE.Object3D, clips: THREE.AnimationClip[], animationInput: ModelAnimationConfig | "auto" | "none" | undefined, instanceId?: string): void — The engine's model animation driver as a standalone hook — the same mixer `EntityModel` runs, for games that render a cloned scene themselves (custom materials, procedural composition). Handles `"auto"` derivation from the GLB's clip names, speed-driven idle/walk/run crossfades read from the entity's live position when `instanceId` is set, one-shots fired from `entity.animation` / `combat.hitReaction` / `entity.died`, held poses, and the death clamp.
1572
+
1443
1573
  ## @jgengine/shell/replay/useSessionRecorder
1444
1574
 
1445
1575
  - `RecordedPose` (interface): interface RecordedPose — ⚠ undocumented
@@ -1975,7 +2105,7 @@
1975
2105
  - `WorldBarSample` (interface): interface WorldBarSample — ⚠ undocumented
1976
2106
  - `WorldEntityBars` (function): function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, maxDistance = 60, }: { statId: string; height?: number; roles?: readonly CatalogEntityRole[]; resolveRole?: (entity: SceneEntity) => CatalogEntityRole | undefined; /** Hide bars for entities farther than this from the player (world… — ⚠ undocumented
1977
2107
  - `WorldFloatText` (function): function WorldFloatText({ height = 1.9, lifeMs = 950 }: { height?: number; lifeMs?: number }): React.JSX.Element — ⚠ undocumented
1978
- - `WorldNameplates` (function): function WorldNameplates({ statId = "health", height = 2.3, roles, resolveRole, maxDistance = 40, tickMs = 120, className, nameplateClassName, nameClassName, barClassName, fillClassName, renderNameplate, }: WorldNameplatesProps): React.JSX.Element — Billboarded name + 78×6px HP bar over every nearby non-local entity that passes `roles`/`maxDistance` — headless (className/data-* slots on every part, `renderNameplate` for a full swap), turned on declaratively via `defineGame({ nameplates })` rather than mounted by hand.
2108
+ - `WorldNameplates` (function): function WorldNameplates({ statId = "health", height = 2.3, roles, resolveRole, maxDistance = 40, tickMs = 120, showHealth = true, className, nameplateClassName, nameClassName, barClassName, fillClassName, renderNameplate, }: WorldNameplatesProps): React.JSX.Element — Billboarded name + 78×6px HP bar over every nearby non-local entity that passes `roles`/`maxDistance` — headless (className/data-* slots on every part, `renderNameplate` for a full swap), turned on declaratively via `defineGame({ nameplates })` rather than mounted by hand.
1979
2109
  - `WorldNameplatesProps` (interface): interface WorldNameplatesProps — Props for `WorldNameplates` — entity filter, refresh rate, and headless className/render hooks.
1980
2110
  - `WorldObjectHighlights` (function): function WorldObjectHighlights({ color = "#facc15", radius, y = 0.05 }: WorldObjectHighlightsProps): React.JSX.Element — Ground ring over every `ctx.scene.object.selection`-ed placed object — the object-layer counterpart to `WorldEntityBars`/`WorldNameplates`. Mount it once in the game's scene (headless: no defaults are imposed beyond a visible ring) instead of hand-rolling a selection highlight through `WorldOverlay` against external state.
1981
2111
  - `WorldObjectHighlightsProps` (interface): interface WorldObjectHighlightsProps — Props for {@link WorldObjectHighlights}.