@jgengine/react 0.11.0 → 0.12.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/actionHud.d.ts +5 -5
  3. package/dist/actionHud.js +14 -12
  4. package/dist/bars.d.ts +103 -0
  5. package/dist/bars.js +180 -0
  6. package/dist/barsPreview.d.ts +4 -0
  7. package/dist/barsPreview.js +47 -0
  8. package/dist/components.d.ts +8 -1
  9. package/dist/components.js +8 -1
  10. package/dist/hooks.d.ts +20 -0
  11. package/dist/hooks.js +38 -0
  12. package/dist/hud.d.ts +21 -4
  13. package/dist/hud.js +38 -27
  14. package/dist/hudFrame.d.ts +47 -0
  15. package/dist/hudFrame.js +80 -0
  16. package/dist/hudLayout.d.ts +13 -1
  17. package/dist/hudLayout.js +18 -12
  18. package/dist/hudLayoutPersist.d.ts +23 -0
  19. package/dist/hudLayoutPersist.js +19 -0
  20. package/dist/hudTheme.d.ts +76 -0
  21. package/dist/hudTheme.js +156 -0
  22. package/dist/hudThemePreview.d.ts +4 -0
  23. package/dist/hudThemePreview.js +37 -0
  24. package/dist/iconTreatment.d.ts +50 -0
  25. package/dist/iconTreatment.js +84 -0
  26. package/dist/iconsPreview.d.ts +4 -0
  27. package/dist/iconsPreview.js +29 -0
  28. package/dist/index.d.ts +9 -0
  29. package/dist/index.js +9 -0
  30. package/dist/map.d.ts +72 -10
  31. package/dist/map.js +84 -15
  32. package/dist/selectionHud.d.ts +11 -5
  33. package/dist/selectionHud.js +11 -5
  34. package/dist/slots.d.ts +44 -0
  35. package/dist/slots.js +37 -0
  36. package/package.json +2 -2
  37. package/skills/jgengine/SKILL.md +7 -4
  38. package/skills/jgengine/api.md +172 -79
  39. package/skills/jgengine/capabilities.md +104 -2
  40. package/skills/jgengine/recipes/minimal-game.md +88 -0
  41. package/skills/jgengine-ui/SKILL.md +15 -10
  42. package/skills/jgengine-ui/api.md +202 -47
  43. package/skills/jgengine-ui/capabilities.md +71 -2
  44. package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
  45. package/skills/jgengine-ui/reference.md +20 -7
  46. package/skills/jgengine-ui/references/visual-scorecard.md +1 -1
  47. package/skills/jgengine/reference-cartridge.md +0 -72
package/dist/hud.js CHANGED
@@ -1,26 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo } from "react";
3
3
  import { groundSpeed } from "@jgengine/core/scene/entityStore";
4
+ import { iconForItemId } from "./gameIcons.js";
5
+ import { hudFrameStyle } from "./hudFrame.js";
6
+ import { IconTreatment, schoolForItem } from "./iconTreatment.js";
4
7
  import { useCurrency, useEntityStat, useGameClock, useGameStore, useInventory, localPlayerEntity } from "./hooks.js";
5
8
  /**
6
- * Drop-in HUD components — good-looking with zero styling, and fully opt-in: a game imports only the
7
- * pieces it wants (`StatBar`, `Hotbar`, `Speedometer`, `Clock`, `WaveBanner`, `Coins`, `Crosshair`)
8
- * and places them itself. The engine imposes none of these; there is no forced HUD. Styling is
9
- * self-contained inline CSS (no Tailwind `@source` needed) with a shared dark-glass look; pass
9
+ * Optional HUD building blocks bound to live game state — not a finished game face. Every game
10
+ * owns custom presentation (see AGENTS.md); import these only as temporary scaffolding, previews,
11
+ * or under game-owned chrome. Prefer headless hooks + game markup for shipped UI. The engine
12
+ * mounts none of these by default. Styling is self-contained dark-glass for demos; pass
10
13
  * `style`/`className` to override. Anything reading the player defaults to the local player entity.
11
14
  *
12
- * @capability hud-components opt-in drop-in health/hotbar/speed/clock/wave/currency HUD widgets
15
+ * @capability hud-components optional health/hotbar/speed/clock/wave/currency building blocks — games own custom chrome
13
16
  */
14
- const PANEL = {
15
- background: "rgba(10,12,16,0.62)",
16
- backdropFilter: "blur(6px)",
17
- WebkitBackdropFilter: "blur(6px)",
18
- border: "1px solid rgba(255,255,255,0.10)",
19
- borderRadius: 10,
20
- color: "#f4f6fb",
21
- fontFamily: "ui-sans-serif, system-ui, -apple-system, sans-serif",
22
- boxShadow: "0 4px 16px rgba(0,0,0,0.35)",
23
- };
17
+ const PANEL = hudFrameStyle("glass");
24
18
  /** Named color ramps for {@link StatBar} tones. `fn` maps fill fraction (0..1) → gradient stops. */
25
19
  const TONES = {
26
20
  health: (t) => (t > 0.5 ? ["#22c55e", "#4ade80"] : t > 0.25 ? ["#f59e0b", "#fbbf24"] : ["#dc2626", "#f87171"]),
@@ -37,6 +31,13 @@ function useLocalPlayerId(entityId) {
37
31
  * A polished stat bar (health/mana/stamina/shield/xp) — a rounded, glassy meter with a tone-colored
38
32
  * fill and an optional value readout. Reads `statId` off `entityId` (defaults to the local player).
39
33
  * Renders nothing until the stat exists.
34
+ *
35
+ * @deprecated The `tone`-switched umbrella is being retired (#1033). Use the atomic, purpose-named,
36
+ * token-themed bars from `@jgengine/react/bars` instead — `HealthBar`, `ShieldBar`, `ManaBar`,
37
+ * `StaminaBar`, `ExperienceBar`, `SoulBar`, `BossBar`, `AmmoCounter` — each does one readout and is
38
+ * restyled globally via the shared `--jg-*` tokens (`barTokens`). Kept as a thin shim during migration.
39
+ *
40
+ * @capability hud-stat-bar optional stat-bound bar building block (health, mana, stamina) — prefer game-owned chrome
40
41
  */
41
42
  export function StatBar({ statId = "health", entityId, tone = "health", label, showValue = true, width = 200, icon, style, className, }) {
42
43
  const id = useLocalPlayerId(entityId);
@@ -49,24 +50,30 @@ export function StatBar({ statId = "health", entityId, tone = "health", label, s
49
50
  return (_jsxs("div", { className: className, style: { ...PANEL, padding: "6px 8px", width, ...style }, "data-stat": statId, children: [(label !== undefined || icon !== undefined || showValue) && (_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 4, fontSize: 11, fontWeight: 600, letterSpacing: 0.3 }, children: [icon, _jsx("span", { style: { textTransform: "uppercase", opacity: 0.75 }, children: label ?? statId }), showValue ? (_jsxs("span", { style: { marginLeft: "auto", fontVariantNumeric: "tabular-nums", opacity: 0.9 }, children: [Math.round(stat.current), _jsxs("span", { style: { opacity: 0.5 }, children: [" / ", Math.round(stat.max)] })] })) : null] })), _jsx("div", { style: { height: 8, borderRadius: 999, background: "rgba(255,255,255,0.10)", overflow: "hidden" }, children: _jsx("div", { style: { width: `${fraction * 100}%`, height: "100%", borderRadius: 999, background: `linear-gradient(90deg, ${from}, ${to})`, transition: "width 160ms ease" } }) })] }));
50
51
  }
51
52
  /**
52
- * A numbered hotbar bound to an inventory — glassy slots, an active-slot highlight, item id + count,
53
- * and a keycap per slot. Place it and pass the `inventoryId`; `activeSlot` highlights the equipped one.
53
+ * A numbered hotbar bound to an inventory — painted iconed slots (a `GameIcon` glyph over a
54
+ * school-keyed gradient with a count badge, #1035), an active-slot highlight, and a keycap per slot.
55
+ * Place it and pass the `inventoryId`; `activeSlot` highlights the equipped one. Supply `itemIcon` to
56
+ * map your item ids to your own glyphs — the default resolves a `GameIcon` from the item id.
57
+ *
58
+ * @capability hud-hotbar optional inventory hotbar building block — games own slot chrome
54
59
  */
55
- export function Hotbar({ inventoryId, activeSlot, keys, slotSize = 46, style, className, }) {
60
+ export function Hotbar({ inventoryId, activeSlot, keys, slotSize = 46, itemIcon, style, className, }) {
56
61
  const slots = useInventory(inventoryId);
57
62
  return (_jsx("div", { className: className, style: { display: "flex", gap: 6, ...style }, "data-inventory": inventoryId, children: slots.map((slot, index) => {
58
63
  const active = index === activeSlot;
59
- return (_jsxs("div", { "data-slot": index, style: {
60
- ...PANEL,
64
+ const keycap = keys?.[index] ?? String(index + 1);
65
+ const custom = slot !== null ? itemIcon?.(slot.itemId) : undefined;
66
+ return (_jsx("div", { "data-slot": index, style: {
61
67
  width: slotSize,
62
68
  height: slotSize,
63
69
  position: "relative",
64
- display: "flex",
65
- alignItems: "center",
66
- justifyContent: "center",
67
- borderColor: active ? "rgba(56,189,248,0.9)" : "rgba(255,255,255,0.10)",
68
- boxShadow: active ? "0 0 0 1px rgba(56,189,248,0.7), 0 4px 16px rgba(0,0,0,0.35)" : PANEL.boxShadow,
69
- }, children: [_jsx("span", { style: { position: "absolute", top: 2, left: 4, fontSize: 9, fontWeight: 700, opacity: 0.55 }, children: keys?.[index] ?? index + 1 }), slot !== null ? (_jsxs("span", { style: { fontSize: 10, textAlign: "center", lineHeight: 1.1, padding: "0 2px", wordBreak: "break-word" }, children: [slot.itemId, slot.count > 1 ? _jsxs("span", { style: { display: "block", opacity: 0.6 }, children: ["\u00D7", slot.count] }) : null] })) : null] }, index));
70
+ }, children: slot !== null ? (custom !== undefined && custom !== null ? (_jsx(IconTreatment, { glyph: custom, size: slotSize, count: slot.count, keycap: keycap, active: active })) : (_jsx(IconTreatment, { ...(iconForItemId(slot.itemId) === null ? {} : { icon: iconForItemId(slot.itemId) }), school: schoolForItem(slot.itemId), size: slotSize, count: slot.count, keycap: keycap, active: active }))) : (_jsx("div", { style: {
71
+ ...PANEL,
72
+ width: slotSize,
73
+ height: slotSize,
74
+ position: "relative",
75
+ borderColor: active ? "rgba(56,189,248,0.9)" : "rgba(255,255,255,0.10)",
76
+ }, children: _jsx("span", { style: { position: "absolute", top: 2, left: 4, fontSize: 9, fontWeight: 700, opacity: 0.55 }, children: keycap }) })) }, index));
70
77
  }) }));
71
78
  }
72
79
  /**
@@ -98,6 +105,8 @@ export function Speedometer({ entityId, scale = 3.6, unit = "km/h", max = 60, si
98
105
  * A time-of-day clock reading the sim calendar — `Day N · HH:MM`, 24h or 12h. `controls` adds
99
106
  * pause + the game's speed multipliers as clickable pills (the "fast-forward" bar), off by default so
100
107
  * a game opts into letting the player scrub time.
108
+ *
109
+ * @capability hud-clock in-game time-of-day readout bound to the sim clock
101
110
  */
102
111
  export function Clock({ format = "24h", showDay = true, controls = false, style, className, }) {
103
112
  const clock = useGameClock();
@@ -132,7 +141,9 @@ function ClockPill({ label, active, onClick }) {
132
141
  export function WaveBanner({ wave, label = "Wave", subtitle, style, className, }) {
133
142
  return (_jsxs("div", { className: className, style: { ...PANEL, padding: "6px 16px", textAlign: "center", display: "inline-block", ...style }, children: [_jsx("div", { style: { fontSize: 10, letterSpacing: 2, textTransform: "uppercase", opacity: 0.65 }, children: label }), _jsx("div", { style: { fontSize: 24, fontWeight: 800, lineHeight: 1.1 }, children: wave }), subtitle !== undefined ? _jsx("div", { style: { fontSize: 11, opacity: 0.7, marginTop: 2 }, children: subtitle }) : null] }));
134
143
  }
135
- /** A currency counter — an icon (emoji/char, default a coin) plus the live amount for `currencyId`. */
144
+ /** A currency counter — an icon (emoji/char, default a coin) plus the live amount for `currencyId`. *
145
+ * @capability hud-coins currency counter widget bound to a currency id
146
+ */
136
147
  export function Coins({ currencyId, icon = "🪙", style, className, }) {
137
148
  const amount = useCurrency(currencyId);
138
149
  return (_jsxs("span", { className: className, style: { ...PANEL, padding: "3px 10px", display: "inline-flex", alignItems: "center", gap: 6, fontWeight: 700, fontVariantNumeric: "tabular-nums", ...style }, "data-currency": currencyId, children: [_jsx("span", { "aria-hidden": true, children: icon }), amount] }));
@@ -0,0 +1,47 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ /** Frame skin — `glass` (the shared dark-glass panel), `plate` (a heavier opaque plate), `retro` (a hard black-outlined frame), or `themed` (driven by the `--jg-frame-*` `HudTheme` tokens). */
3
+ export type HudFrameVariation = "glass" | "plate" | "retro" | "themed";
4
+ /** Frame corner shape — `rounded` (the variation's default radius), `circle` (fully round), or `square` (no radius). */
5
+ export type HudFrameShape = "rounded" | "circle" | "square";
6
+ /**
7
+ * Bare style object for a HUD frame's look (no wrapper element) — spread it onto
8
+ * a widget that already owns its element when it just needs the chrome CSS. The
9
+ * `glass` variation is byte-for-byte the shared dark-glass panel used across the
10
+ * built-in HUD widgets; `plate` is a heavier opaque plate; `retro` is a hard
11
+ * black-outlined frame. `shape` overrides the corner radius.
12
+ *
13
+ * @internal helper behind {@link HudFrame} (and the built-in HUD `PANEL`); widgets normally use `HudFrame`.
14
+ */
15
+ export declare function hudFrameStyle(variation?: HudFrameVariation, shape?: HudFrameShape): CSSProperties;
16
+ /** Props for {@link HudFrame}. */
17
+ export interface HudFrameProps {
18
+ /** Frame skin (default `glass`). */
19
+ variation?: HudFrameVariation;
20
+ /** Corner shape (default `rounded`). */
21
+ shape?: HudFrameShape;
22
+ /** Small uppercase header label rendered at the top-left of the frame. */
23
+ title?: ReactNode;
24
+ /** Header content rendered at the top-right, opposite `title` (e.g. a readout or cardinal). */
25
+ aside?: ReactNode;
26
+ /** Inner padding of the frame. */
27
+ padding?: number | string;
28
+ /** Fixed frame width. */
29
+ width?: number | string;
30
+ /** Accept pointer events (default off, so the frame never eats clicks meant for the game). */
31
+ interactive?: boolean;
32
+ /** Extra class on the frame element. */
33
+ className?: string;
34
+ /** Style overrides merged last, so callers can pin exact gradients/colors. */
35
+ style?: CSSProperties;
36
+ /** Frame body. */
37
+ children?: ReactNode;
38
+ }
39
+ /**
40
+ * Optional framed panel primitive — a single `<div data-hud-frame>` with a `glass`,
41
+ * `plate`, `retro`, or `themed` skin, optional `title`/`aside` header, and caller
42
+ * `style` merged last. Shared building block for demos and token-themed widgets;
43
+ * games own product chrome and art direction (see AGENTS.md).
44
+ *
45
+ * @capability hud-frame optional framed panel skins (glass/plate/retro/themed) — games own product chrome
46
+ */
47
+ export declare function HudFrame({ variation, shape, title, aside, padding, width, interactive, className, style, children, }: HudFrameProps): ReactNode;
@@ -0,0 +1,80 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Bare style object for a HUD frame's look (no wrapper element) — spread it onto
4
+ * a widget that already owns its element when it just needs the chrome CSS. The
5
+ * `glass` variation is byte-for-byte the shared dark-glass panel used across the
6
+ * built-in HUD widgets; `plate` is a heavier opaque plate; `retro` is a hard
7
+ * black-outlined frame. `shape` overrides the corner radius.
8
+ *
9
+ * @internal helper behind {@link HudFrame} (and the built-in HUD `PANEL`); widgets normally use `HudFrame`.
10
+ */
11
+ export function hudFrameStyle(variation = "glass", shape = "rounded") {
12
+ const base = variation === "themed"
13
+ ? {
14
+ background: "var(--jg-frame-bg, rgba(10,12,16,0.62))",
15
+ border: "var(--jg-frame-border, 1px solid rgba(255,255,255,0.10))",
16
+ borderRadius: "var(--jg-frame-radius, 10px)",
17
+ boxShadow: "var(--jg-frame-glow, 0 4px 16px rgba(0,0,0,0.35))",
18
+ color: "var(--jg-bar-text, #f4f6fb)",
19
+ fontFamily: "ui-sans-serif, system-ui, -apple-system, sans-serif",
20
+ }
21
+ : variation === "retro"
22
+ ? {
23
+ border: "2px solid #000",
24
+ borderRadius: 4,
25
+ boxShadow: "4px 4px 0 #000",
26
+ color: "#fff",
27
+ }
28
+ : variation === "plate"
29
+ ? {
30
+ background: "rgba(16,18,24,0.92)",
31
+ border: "1px solid rgba(255,255,255,0.10)",
32
+ borderRadius: 10,
33
+ color: "#f4f6fb",
34
+ fontFamily: "ui-sans-serif, system-ui, -apple-system, sans-serif",
35
+ boxShadow: "0 6px 20px rgba(0,0,0,0.45)",
36
+ }
37
+ : {
38
+ background: "rgba(10,12,16,0.62)",
39
+ backdropFilter: "blur(6px)",
40
+ WebkitBackdropFilter: "blur(6px)",
41
+ border: "1px solid rgba(255,255,255,0.10)",
42
+ borderRadius: 10,
43
+ color: "#f4f6fb",
44
+ fontFamily: "ui-sans-serif, system-ui, -apple-system, sans-serif",
45
+ boxShadow: "0 4px 16px rgba(0,0,0,0.35)",
46
+ };
47
+ if (shape === "circle")
48
+ return { ...base, borderRadius: 9999 };
49
+ if (shape === "square")
50
+ return { ...base, borderRadius: 0 };
51
+ return base;
52
+ }
53
+ /**
54
+ * Optional framed panel primitive — a single `<div data-hud-frame>` with a `glass`,
55
+ * `plate`, `retro`, or `themed` skin, optional `title`/`aside` header, and caller
56
+ * `style` merged last. Shared building block for demos and token-themed widgets;
57
+ * games own product chrome and art direction (see AGENTS.md).
58
+ *
59
+ * @capability hud-frame optional framed panel skins (glass/plate/retro/themed) — games own product chrome
60
+ */
61
+ export function HudFrame({ variation = "glass", shape = "rounded", title, aside, padding, width, interactive, className, style, children, }) {
62
+ const hasHeader = title !== undefined || aside !== undefined;
63
+ return (_jsxs("div", { className: className, "data-hud-frame": true, style: {
64
+ ...hudFrameStyle(variation, shape),
65
+ padding,
66
+ width,
67
+ pointerEvents: interactive === true ? "auto" : undefined,
68
+ ...style,
69
+ }, children: [hasHeader ? (_jsxs("div", { "data-hud-frame-header": true, style: {
70
+ display: "flex",
71
+ justifyContent: "space-between",
72
+ alignItems: "center",
73
+ gap: 8,
74
+ marginBottom: 6,
75
+ fontSize: 10,
76
+ letterSpacing: 1.4,
77
+ textTransform: "uppercase",
78
+ opacity: 0.75,
79
+ }, children: [_jsx("span", { children: title }), aside !== undefined ? _jsx("span", { children: aside }) : null] })) : null, children] }));
80
+ }
@@ -15,6 +15,11 @@ export interface HudEditChord {
15
15
  hold: string;
16
16
  press: string;
17
17
  }
18
+ /**
19
+ * Layout state for `HudCanvas` — panel placements, edit-mode drag/resize, and per-game persistence.
20
+ *
21
+ * @capability hud-layout persistent, live-editable HUD panel layout state for `HudCanvas`
22
+ */
18
23
  export declare function useHudLayout(options?: {
19
24
  storageKey?: string;
20
25
  snap?: number;
@@ -24,7 +29,10 @@ export declare function useHudLayout(options?: {
24
29
  * When provided, hydrates the layout store (and wins over legacy localStorage).
25
30
  */
26
31
  documentUi?: EditorUiDocument;
27
- /** When true (default), canvas moves/resizes write undoable `setUiPanel` patches to a live editor host. */
32
+ /**
33
+ * When true (default), canvas moves/resizes invoke the injected {@link HudLayoutPersist}
34
+ * port (`HudLayoutPersistProvider` / `onPanelCommit`). No-op when no host has provided one.
35
+ */
28
36
  documentPatches?: boolean;
29
37
  }): HudLayoutStore;
30
38
  /**
@@ -33,6 +41,8 @@ export declare function useHudLayout(options?: {
33
41
  * offsets, no manual clearance for sibling panels, the touch-control dock
34
42
  * (`--jg-hud-dock-clearance`), or device safe areas. On compact displays the
35
43
  * whole surface scales down and each panel applies its `compact` behavior.
44
+ *
45
+ * @capability hud-canvas the HUD root surface — design-resolution scaling plus live panel-placement editing (F2+C)
36
46
  */
37
47
  export declare function HudCanvas({ layout, editChord, compactScale, showDuring, className, style, children, }: {
38
48
  layout: HudLayoutStore;
@@ -51,6 +61,8 @@ export declare function HudCanvas({ layout, editChord, compactScale, showDuring,
51
61
  * pointers panels stay draggable through the edit chord; a dragged panel
52
62
  * leaves the flow and keeps its custom placement. On compact displays custom
53
63
  * placements are ignored and the `compact` behavior applies.
64
+ *
65
+ * @capability hud-panel anchored HUD slot — drop any widget in a named, player-movable panel
54
66
  */
55
67
  export declare function HudPanel({ id, anchor, order, compact: compactMode, chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region, width, height, type, className, style, children, }: {
56
68
  id: string;
package/dist/hudLayout.js CHANGED
@@ -9,6 +9,7 @@ import { useDisplayProfile } from "./display.js";
9
9
  import { useEngineState } from "./engineStore.js";
10
10
  import { useGameLayoutMode, useRegisterLayoutRegion } from "./gameViewport.js";
11
11
  import { useOptionalGamePhase } from "./hooks.js";
12
+ import { useHudLayoutPersist } from "./hudLayoutPersist.js";
12
13
  import { useHudViewport } from "./hudViewport.js";
13
14
  /** Whether a HUD element opted into `showDuring` is visible in the current phase; `undefined` = always visible (default). */
14
15
  export function hudVisibleInPhase(showDuring, phase) {
@@ -21,17 +22,6 @@ const EDIT_BAR_Z = 100000;
21
22
  const REGION_GAP = 10;
22
23
  const COMPACT_HUD_SCALE = 0.85;
23
24
  const HUD_ANCHORS = Object.keys(HUD_ANCHOR_FRACTIONS);
24
- function patchDocumentUi(id, panel) {
25
- if (typeof window === "undefined")
26
- return;
27
- const host = window.__jgengineEditorHost;
28
- if (host === undefined)
29
- return;
30
- host.handle({
31
- method: "dispatch",
32
- command: { type: "setUiPanel", id, patch: panel },
33
- });
34
- }
35
25
  function sizeStyle(panel) {
36
26
  if (panel === undefined)
37
27
  return {};
@@ -60,17 +50,29 @@ function ResizeHandles({ axes, onResizeStart, }) {
60
50
  ? handle("s", "ns-resize", { left: 4, right: 4, bottom: -3, height: 6 })
61
51
  : null, axes === "both" ? handle("se", "nwse-resize", { right: -4, bottom: -4, width: 10, height: 10 }) : null] }));
62
52
  }
53
+ /**
54
+ * Layout state for `HudCanvas` — panel placements, edit-mode drag/resize, and per-game persistence.
55
+ *
56
+ * @capability hud-layout persistent, live-editable HUD panel layout state for `HudCanvas`
57
+ */
63
58
  export function useHudLayout(options) {
64
59
  const storageKey = options?.storageKey;
65
60
  const snap = options?.snap;
66
61
  const locked = options?.locked;
67
62
  const documentUi = options?.documentUi;
68
63
  const documentPatches = options?.documentPatches !== false;
64
+ const onPanelCommit = useHudLayoutPersist();
65
+ const onPanelCommitRef = useRef(onPanelCommit);
66
+ onPanelCommitRef.current = onPanelCommit;
69
67
  const layout = useMemo(() => {
70
68
  const store = createHudLayout({
71
69
  snap,
72
70
  locked,
73
- onDocumentPatch: documentPatches ? patchDocumentUi : undefined,
71
+ onDocumentPatch: documentPatches
72
+ ? (id, panel) => {
73
+ onPanelCommitRef.current?.(id, panel);
74
+ }
75
+ : undefined,
74
76
  });
75
77
  if (documentUi !== undefined) {
76
78
  store.applyDocumentUi(documentUi);
@@ -187,6 +189,8 @@ function edgePad(envInset, extra, basePx, scale) {
187
189
  * offsets, no manual clearance for sibling panels, the touch-control dock
188
190
  * (`--jg-hud-dock-clearance`), or device safe areas. On compact displays the
189
191
  * whole surface scales down and each panel applies its `compact` behavior.
192
+ *
193
+ * @capability hud-canvas the HUD root surface — design-resolution scaling plus live panel-placement editing (F2+C)
190
194
  */
191
195
  export function HudCanvas({ layout, editChord, compactScale, showDuring, className, style, children, }) {
192
196
  const canvasRef = useRef(null);
@@ -400,6 +404,8 @@ function HudChip({ label, anchor, children, }) {
400
404
  * pointers panels stay draggable through the edit chord; a dragged panel
401
405
  * leaves the flow and keeps its custom placement. On compact displays custom
402
406
  * placements are ignored and the `compact` behavior applies.
407
+ *
408
+ * @capability hud-panel anchored HUD slot — drop any widget in a named, player-movable panel
403
409
  */
404
410
  export function HudPanel({ id, anchor = "top-left", order, compact: compactMode = "keep", chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, width, height, type, className, style, children, }) {
405
411
  const ctx = useContext(HudCanvasContext);
@@ -0,0 +1,23 @@
1
+ import { type ReactNode } from "react";
2
+ import type { EditorUiPanelLayout } from "@jgengine/core/ui/hudDocument";
3
+ /**
4
+ * Host-owned callback that commits one HUD panel layout into the scene document
5
+ * (typically an undoable `setUiPanel` dispatch). Injected by the editor so product
6
+ * UI never reaches for editor globals.
7
+ */
8
+ export type HudLayoutPersist = (id: string, panel: EditorUiPanelLayout) => void;
9
+ /**
10
+ * Provides {@link HudLayoutPersist} / `onPanelCommit` to descendant `useHudLayout`
11
+ * calls. Mount around game UI when a host can accept document UI patches (editor).
12
+ * Absent provider → canvas moves/resizes are no-ops for document writes.
13
+ * @capability hud-layout-persist inject a host callback so canvas HUD edits commit into the scene document
14
+ */
15
+ export declare function HudLayoutPersistProvider({ onPanelCommit, children, }: {
16
+ onPanelCommit: HudLayoutPersist;
17
+ children?: ReactNode;
18
+ }): import("react").JSX.Element;
19
+ /**
20
+ * Injected panel-commit port, or `null` when no host has provided one.
21
+ * @capability hud-layout-persist-hook read the host panel-commit port from a HUD layout widget
22
+ */
23
+ export declare function useHudLayoutPersist(): HudLayoutPersist | null;
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
3
+ const HudLayoutPersistContext = createContext(null);
4
+ /**
5
+ * Provides {@link HudLayoutPersist} / `onPanelCommit` to descendant `useHudLayout`
6
+ * calls. Mount around game UI when a host can accept document UI patches (editor).
7
+ * Absent provider → canvas moves/resizes are no-ops for document writes.
8
+ * @capability hud-layout-persist inject a host callback so canvas HUD edits commit into the scene document
9
+ */
10
+ export function HudLayoutPersistProvider({ onPanelCommit, children, }) {
11
+ return (_jsx(HudLayoutPersistContext.Provider, { value: onPanelCommit, children: children }));
12
+ }
13
+ /**
14
+ * Injected panel-commit port, or `null` when no host has provided one.
15
+ * @capability hud-layout-persist-hook read the host panel-commit port from a HUD layout widget
16
+ */
17
+ export function useHudLayoutPersist() {
18
+ return useContext(HudLayoutPersistContext);
19
+ }
@@ -0,0 +1,76 @@
1
+ import { type CSSProperties } from "react";
2
+ /**
3
+ * `HudTheme` (#1034): one token object that restyles shared token-driven building blocks at once.
4
+ * It is the superset of the atomic-bar tokens (#1033) — `hudThemeVars(theme)` emits the same
5
+ * `--jg-*` custom properties the bars already read, plus frame / slot / minimap-ring tokens — so
6
+ * setting a **game-authored** theme on any HUD ancestor re-skins those primitives. Purely CSS-token
7
+ * driven (no image assets). Tokens are not a finished UI: every game still owns composition, skin,
8
+ * and art direction (see AGENTS.md). Built-in presets are demo/scaffold starting points only.
9
+ *
10
+ * @capability hud-theme game-authored token object for bars, frames, slots, and minimap ring
11
+ */
12
+ /** Per-readout fill colors plus a shared accent/glow. */
13
+ export interface HudThemePalette {
14
+ health: string;
15
+ healthLow: string;
16
+ mana: string;
17
+ stamina: string;
18
+ shield: string;
19
+ xp: string;
20
+ soul: string;
21
+ ammo: string;
22
+ boss: string;
23
+ /** Accent used for glow/keycap/active states. */
24
+ accent: string;
25
+ }
26
+ /** Frame material + shape tokens (panels, `HudFrame variation="themed"`). */
27
+ export interface HudThemeFrame {
28
+ /** Panel background (gradient or solid). */
29
+ bg: string;
30
+ border: string;
31
+ /** Corner radius (any CSS length, e.g. `"10px"`, `"0"`). */
32
+ radius: string;
33
+ /** Outer glow / drop shadow. */
34
+ glow: string;
35
+ }
36
+ /** Framed-trough tokens shared by every atomic bar. */
37
+ export interface HudThemeBar {
38
+ track: string;
39
+ frame: string;
40
+ height: string;
41
+ radius: string;
42
+ bevel: string;
43
+ text: string;
44
+ }
45
+ /** Action/inventory slot tokens. */
46
+ export interface HudThemeSlot {
47
+ bg: string;
48
+ border: string;
49
+ radius: string;
50
+ }
51
+ /** A full HUD theme. */
52
+ export interface HudTheme {
53
+ palette: HudThemePalette;
54
+ frame: HudThemeFrame;
55
+ bar: HudThemeBar;
56
+ slot: HudThemeSlot;
57
+ /** Minimap chrome ring color. */
58
+ ring: string;
59
+ }
60
+ /** Emits the CSS custom properties for a theme — spread onto any HUD ancestor to re-skin the subtree. */
61
+ export declare function hudThemeVars(theme: HudTheme): CSSProperties;
62
+ /** Default theme tokens for previews/scaffold — not a finished game look; author a custom theme. */
63
+ export declare const defaultHudTheme: HudTheme;
64
+ /** Built-in theme presets for demos/scaffold only — not product identity; games author their own theme. */
65
+ export declare const HUD_THEME_PRESETS: {
66
+ "arcane-stone": HudTheme;
67
+ "survival-wood": HudTheme;
68
+ "military-flat": HudTheme;
69
+ "sleek-hex": HudTheme;
70
+ "elemental-circle": HudTheme;
71
+ "sandbox-slot": HudTheme;
72
+ };
73
+ /** Built-in demo/scaffold theme preset name — not a game identity. */
74
+ export type HudThemePreset = keyof typeof HUD_THEME_PRESETS;
75
+ /** Resolves a preset name (or a full theme) to a `HudTheme`; falls back to the default theme. */
76
+ export declare function resolveHudTheme(theme?: HudThemePreset | HudTheme): HudTheme;
@@ -0,0 +1,156 @@
1
+ import {} from "react";
2
+ /** Emits the CSS custom properties for a theme — spread onto any HUD ancestor to re-skin the subtree. */
3
+ export function hudThemeVars(theme) {
4
+ const { palette, frame, bar, slot } = theme;
5
+ return {
6
+ // atomic-bar tokens (same names the bars read — #1033)
7
+ "--jg-health": palette.health,
8
+ "--jg-health-low": palette.healthLow,
9
+ "--jg-mana": palette.mana,
10
+ "--jg-stamina": palette.stamina,
11
+ "--jg-shield": palette.shield,
12
+ "--jg-xp": palette.xp,
13
+ "--jg-soul": palette.soul,
14
+ "--jg-ammo": palette.ammo,
15
+ "--jg-boss": palette.boss,
16
+ "--jg-bar-track": bar.track,
17
+ "--jg-bar-frame": bar.frame,
18
+ "--jg-bar-height": bar.height,
19
+ "--jg-bar-radius": bar.radius,
20
+ "--jg-bar-bevel": bar.bevel,
21
+ "--jg-bar-text": bar.text,
22
+ // shared chrome tokens
23
+ "--jg-accent": palette.accent,
24
+ "--jg-frame-bg": frame.bg,
25
+ "--jg-frame-border": frame.border,
26
+ "--jg-frame-radius": frame.radius,
27
+ "--jg-frame-glow": frame.glow,
28
+ "--jg-slot-bg": slot.bg,
29
+ "--jg-slot-border": slot.border,
30
+ "--jg-slot-radius": slot.radius,
31
+ "--jg-ring": theme.ring,
32
+ };
33
+ }
34
+ /** Default theme tokens for previews/scaffold — not a finished game look; author a custom theme. */
35
+ export const defaultHudTheme = {
36
+ palette: {
37
+ health: "#e5484d",
38
+ healthLow: "#b21e23",
39
+ mana: "#3b82f6",
40
+ stamina: "#84cc16",
41
+ shield: "#22d3ee",
42
+ xp: "#a855f7",
43
+ soul: "#c084fc",
44
+ ammo: "#f8b84e",
45
+ boss: "#ef4444",
46
+ accent: "#38bdf8",
47
+ },
48
+ frame: {
49
+ bg: "linear-gradient(180deg, rgba(20,24,32,0.86), rgba(10,12,16,0.9))",
50
+ border: "1px solid rgba(255,255,255,0.12)",
51
+ radius: "10px",
52
+ glow: "0 6px 20px rgba(0,0,0,0.45)",
53
+ },
54
+ bar: {
55
+ track: "rgba(0,0,0,0.55)",
56
+ frame: "rgba(0,0,0,0.82)",
57
+ height: "18px",
58
+ radius: "4px",
59
+ bevel: "inset 0 2px 4px rgba(0,0,0,0.55), inset 0 -1px 2px rgba(255,255,255,0.08)",
60
+ text: "#f5f7fa",
61
+ },
62
+ slot: { bg: "rgba(12,14,20,0.7)", border: "1px solid rgba(255,255,255,0.12)", radius: "8px" },
63
+ ring: "rgba(140,170,200,0.35)",
64
+ };
65
+ function theme(overrides) {
66
+ return {
67
+ palette: { ...defaultHudTheme.palette, ...overrides.palette },
68
+ frame: { ...defaultHudTheme.frame, ...overrides.frame },
69
+ bar: { ...defaultHudTheme.bar, ...overrides.bar },
70
+ slot: { ...defaultHudTheme.slot, ...overrides.slot },
71
+ ring: overrides.ring ?? defaultHudTheme.ring,
72
+ };
73
+ }
74
+ /** Built-in theme presets for demos/scaffold only — not product identity; games author their own theme. */
75
+ export const HUD_THEME_PRESETS = {
76
+ "arcane-stone": theme({
77
+ palette: { health: "#c0392b", mana: "#7c5cff", xp: "#d4a017", accent: "#b483ff", shield: "#8e7bd6" },
78
+ frame: {
79
+ bg: "linear-gradient(180deg, #3a3550, #211d33)",
80
+ border: "2px solid #6b5b95",
81
+ radius: "6px",
82
+ glow: "0 0 18px rgba(124,92,255,0.35)",
83
+ },
84
+ bar: { frame: "#160f28", radius: "3px", height: "20px", track: "rgba(0,0,0,0.6)", bevel: "inset 0 2px 5px rgba(0,0,0,0.6)", text: "#efe7ff" },
85
+ slot: { bg: "#241f38", border: "2px solid #6b5b95", radius: "5px" },
86
+ ring: "rgba(124,92,255,0.5)",
87
+ }),
88
+ "survival-wood": theme({
89
+ palette: { health: "#c0553b", stamina: "#a3b545", xp: "#e0a94b", accent: "#d9a441", shield: "#7bb0a0" },
90
+ frame: {
91
+ bg: "linear-gradient(180deg, #4a3b28, #2e2117)",
92
+ border: "3px solid #6b4a2c",
93
+ radius: "4px",
94
+ glow: "0 4px 10px rgba(0,0,0,0.5)",
95
+ },
96
+ bar: { frame: "#2a1c10", radius: "2px", height: "18px", track: "rgba(30,20,10,0.7)", bevel: "inset 0 2px 4px rgba(0,0,0,0.6)", text: "#f3e6cf" },
97
+ slot: { bg: "#3a2a1a", border: "3px solid #6b4a2c", radius: "3px" },
98
+ ring: "rgba(107,74,44,0.6)",
99
+ }),
100
+ "military-flat": theme({
101
+ palette: { health: "#3fa34d", stamina: "#a8b545", xp: "#c9b458", accent: "#7d9b4e", shield: "#5f8fae" },
102
+ frame: {
103
+ bg: "linear-gradient(180deg, #2b3126, #1c2019)",
104
+ border: "1px solid #4a5340",
105
+ radius: "2px",
106
+ glow: "0 2px 6px rgba(0,0,0,0.5)",
107
+ },
108
+ bar: { frame: "#161a12", radius: "0px", height: "16px", track: "rgba(0,0,0,0.6)", bevel: "inset 0 1px 2px rgba(0,0,0,0.7)", text: "#e8efdc" },
109
+ slot: { bg: "#232a1e", border: "1px solid #4a5340", radius: "0px" },
110
+ ring: "rgba(125,155,78,0.5)",
111
+ }),
112
+ "sleek-hex": theme({
113
+ palette: { health: "#ff5a7a", mana: "#33c9ff", xp: "#38e8c0", accent: "#33c9ff", shield: "#33c9ff" },
114
+ frame: {
115
+ bg: "linear-gradient(180deg, rgba(18,24,34,0.9), rgba(8,12,18,0.94))",
116
+ border: "1px solid rgba(51,201,255,0.4)",
117
+ radius: "3px",
118
+ glow: "0 0 16px rgba(51,201,255,0.3)",
119
+ },
120
+ bar: { frame: "rgba(51,201,255,0.5)", radius: "2px", height: "14px", track: "rgba(0,0,0,0.5)", bevel: "inset 0 1px 3px rgba(0,0,0,0.5)", text: "#eafaff" },
121
+ slot: { bg: "rgba(12,18,26,0.8)", border: "1px solid rgba(51,201,255,0.4)", radius: "3px" },
122
+ ring: "rgba(51,201,255,0.55)",
123
+ }),
124
+ "elemental-circle": theme({
125
+ palette: { health: "#ff6b3d", mana: "#4aa8ff", stamina: "#7bd451", xp: "#ffcf40", accent: "#ffcf40", shield: "#4aa8ff" },
126
+ frame: {
127
+ bg: "radial-gradient(circle at 50% 40%, #2c2a3a, #16141f)",
128
+ border: "2px solid #d9a441",
129
+ radius: "9999px",
130
+ glow: "0 0 20px rgba(217,164,65,0.35)",
131
+ },
132
+ bar: { frame: "#1a1626", radius: "9999px", height: "16px", track: "rgba(0,0,0,0.55)", bevel: "inset 0 2px 4px rgba(0,0,0,0.6)", text: "#fff3d6" },
133
+ slot: { bg: "#221f30", border: "2px solid #d9a441", radius: "9999px" },
134
+ ring: "rgba(217,164,65,0.6)",
135
+ }),
136
+ "sandbox-slot": theme({
137
+ palette: { health: "#e0524a", stamina: "#77c043", xp: "#5ac0e0", accent: "#f0c040", shield: "#5ac0e0" },
138
+ frame: {
139
+ bg: "linear-gradient(180deg, #5a5a5a, #3a3a3a)",
140
+ border: "3px solid #2a2a2a",
141
+ radius: "2px",
142
+ glow: "3px 3px 0 rgba(0,0,0,0.5)",
143
+ },
144
+ bar: { frame: "#1e1e1e", radius: "0px", height: "18px", track: "rgba(0,0,0,0.6)", bevel: "inset 0 2px 3px rgba(0,0,0,0.6)", text: "#f4f4f4" },
145
+ slot: { bg: "#4a4a4a", border: "3px solid #2a2a2a", radius: "0px" },
146
+ ring: "rgba(240,192,64,0.5)",
147
+ }),
148
+ };
149
+ /** Resolves a preset name (or a full theme) to a `HudTheme`; falls back to the default theme. */
150
+ export function resolveHudTheme(theme) {
151
+ if (theme === undefined)
152
+ return defaultHudTheme;
153
+ if (typeof theme === "string")
154
+ return HUD_THEME_PRESETS[theme] ?? defaultHudTheme;
155
+ return theme;
156
+ }
@@ -0,0 +1,4 @@
1
+ /** Renders the default theme plus every genre preset as a deterministic matrix. */
2
+ export declare function HudThemePreview({ className }: {
3
+ className?: string;
4
+ }): import("react").JSX.Element;