@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.
- package/CHANGELOG.md +26 -0
- package/dist/actionHud.d.ts +5 -5
- package/dist/actionHud.js +14 -12
- package/dist/bars.d.ts +103 -0
- package/dist/bars.js +180 -0
- package/dist/barsPreview.d.ts +4 -0
- package/dist/barsPreview.js +47 -0
- package/dist/components.d.ts +8 -1
- package/dist/components.js +8 -1
- package/dist/hooks.d.ts +20 -0
- package/dist/hooks.js +38 -0
- package/dist/hud.d.ts +21 -4
- package/dist/hud.js +38 -27
- package/dist/hudFrame.d.ts +47 -0
- package/dist/hudFrame.js +80 -0
- package/dist/hudLayout.d.ts +13 -1
- package/dist/hudLayout.js +18 -12
- package/dist/hudLayoutPersist.d.ts +23 -0
- package/dist/hudLayoutPersist.js +19 -0
- package/dist/hudTheme.d.ts +76 -0
- package/dist/hudTheme.js +156 -0
- package/dist/hudThemePreview.d.ts +4 -0
- package/dist/hudThemePreview.js +37 -0
- package/dist/iconTreatment.d.ts +50 -0
- package/dist/iconTreatment.js +84 -0
- package/dist/iconsPreview.d.ts +4 -0
- package/dist/iconsPreview.js +29 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/map.d.ts +72 -10
- package/dist/map.js +84 -15
- package/dist/selectionHud.d.ts +11 -5
- package/dist/selectionHud.js +11 -5
- package/dist/slots.d.ts +44 -0
- package/dist/slots.js +37 -0
- package/package.json +2 -2
- package/skills/jgengine/SKILL.md +7 -4
- package/skills/jgengine/api.md +172 -79
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +15 -10
- package/skills/jgengine-ui/api.md +202 -47
- package/skills/jgengine-ui/capabilities.md +71 -2
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +20 -7
- package/skills/jgengine-ui/references/visual-scorecard.md +1 -1
- 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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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 —
|
|
53
|
-
*
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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;
|
package/dist/hudFrame.js
ADDED
|
@@ -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
|
+
}
|
package/dist/hudLayout.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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;
|
package/dist/hudTheme.js
ADDED
|
@@ -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
|
+
}
|