@jgengine/react 0.11.0 → 0.13.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 +52 -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/characterSheet.d.ts +120 -0
- package/dist/characterSheet.js +134 -0
- package/dist/components.d.ts +8 -1
- package/dist/components.js +8 -1
- package/dist/hooks.d.ts +31 -0
- package/dist/hooks.js +53 -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 +12 -0
- package/dist/index.js +12 -0
- package/dist/inventoryGrid.d.ts +20 -0
- package/dist/inventoryGrid.js +135 -0
- package/dist/map.d.ts +72 -10
- package/dist/map.js +84 -15
- package/dist/panels.d.ts +107 -0
- package/dist/panels.js +215 -0
- package/dist/selectionHud.d.ts +11 -5
- package/dist/selectionHud.js +11 -5
- package/dist/slots.d.ts +47 -0
- package/dist/slots.js +37 -0
- package/package.json +2 -2
- package/skills/jgengine/SKILL.md +8 -4
- package/skills/jgengine/api.md +182 -81
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +17 -10
- package/skills/jgengine-ui/api.md +316 -51
- package/skills/jgengine-ui/capabilities.md +135 -2
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +59 -9
- package/skills/jgengine-ui/references/visual-scorecard.md +1 -1
- package/skills/jgengine/reference-cartridge.md +0 -72
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import {} from "react";
|
|
3
|
+
import { EquipmentSlots } from "./slots.js";
|
|
4
|
+
import { HudFrame } from "./hudFrame.js";
|
|
5
|
+
const DEFAULT_ARRANGEMENT = [
|
|
6
|
+
{ id: "head", label: "Head", position: "left" },
|
|
7
|
+
{ id: "chest", label: "Chest", position: "left" },
|
|
8
|
+
{ id: "hands", label: "Hands", position: "left" },
|
|
9
|
+
{ id: "legs", label: "Legs", position: "left" },
|
|
10
|
+
{ id: "feet", label: "Feet", position: "left" },
|
|
11
|
+
{ id: "mainHand", label: "Main Hand", position: "right" },
|
|
12
|
+
{ id: "offHand", label: "Off Hand", position: "right" },
|
|
13
|
+
{ id: "ring1", label: "Ring", position: "right" },
|
|
14
|
+
{ id: "ring2", label: "Ring", position: "right" },
|
|
15
|
+
{ id: "trinket", label: "Trinket", position: "right" },
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* A sensible default paperdoll arrangement — worn armor down the left flank, held gear and jewelry
|
|
19
|
+
* down the right — that the game fills by passing worn items keyed by slot id. Every id is optional;
|
|
20
|
+
* unfilled slots render empty. Games that need a different slot set pass their own {@link EquipSlotLayout}.
|
|
21
|
+
*/
|
|
22
|
+
export function defaultEquipLayout(items = {}) {
|
|
23
|
+
return DEFAULT_ARRANGEMENT.map((slot) => ({
|
|
24
|
+
id: slot.id,
|
|
25
|
+
label: slot.label,
|
|
26
|
+
item: items[slot.id] ?? null,
|
|
27
|
+
position: slot.position,
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
function SlotLabel({ children }) {
|
|
31
|
+
return (_jsx("span", { style: {
|
|
32
|
+
fontSize: 9,
|
|
33
|
+
letterSpacing: 0.6,
|
|
34
|
+
fontVariant: "small-caps",
|
|
35
|
+
textTransform: "lowercase",
|
|
36
|
+
color: "var(--jg-bar-text, #f4f6fb)",
|
|
37
|
+
opacity: 0.62,
|
|
38
|
+
whiteSpace: "nowrap",
|
|
39
|
+
}, children: children }));
|
|
40
|
+
}
|
|
41
|
+
function EquipSlotCell({ slot, slotSize, align, onSlotActivate, }) {
|
|
42
|
+
const interactive = onSlotActivate !== undefined;
|
|
43
|
+
const activate = interactive ? () => onSlotActivate(slot.id) : undefined;
|
|
44
|
+
return (_jsxs("div", { "data-equip-slot": slot.id, role: "group", "aria-label": slot.label, tabIndex: interactive ? 0 : undefined, ...(activate === undefined ? {} : { onClick: activate }), ...(activate === undefined
|
|
45
|
+
? {}
|
|
46
|
+
: {
|
|
47
|
+
onKeyDown: (event) => {
|
|
48
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
activate();
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
}), style: {
|
|
54
|
+
display: "flex",
|
|
55
|
+
flexDirection: "column",
|
|
56
|
+
alignItems: align === "center" ? "center" : align === "right" ? "flex-end" : "flex-start",
|
|
57
|
+
gap: 3,
|
|
58
|
+
cursor: interactive ? "pointer" : undefined,
|
|
59
|
+
}, children: [_jsx(EquipmentSlots, { items: [slot.item], size: slotSize }), _jsx(SlotLabel, { children: slot.label })] }));
|
|
60
|
+
}
|
|
61
|
+
function Silhouette({ size }) {
|
|
62
|
+
return (_jsx("div", { "data-paperdoll-silhouette": "", "aria-hidden": true, style: {
|
|
63
|
+
width: size,
|
|
64
|
+
height: Math.round(size * 2.6),
|
|
65
|
+
borderRadius: "var(--jg-slot-radius, 9px)",
|
|
66
|
+
background: "var(--jg-slot-bg, rgba(12,14,20,0.5))",
|
|
67
|
+
border: "var(--jg-slot-border, 1px solid rgba(255,255,255,0.1))",
|
|
68
|
+
boxShadow: "inset 0 2px 10px rgba(0,0,0,0.5)",
|
|
69
|
+
display: "flex",
|
|
70
|
+
alignItems: "center",
|
|
71
|
+
justifyContent: "center",
|
|
72
|
+
color: "var(--jg-bar-text, #f4f6fb)",
|
|
73
|
+
opacity: 0.28,
|
|
74
|
+
fontSize: Math.round(size * 0.9),
|
|
75
|
+
boxSizing: "border-box",
|
|
76
|
+
}, children: _jsx("span", { "aria-hidden": true, children: "\u265F" }) }));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Arranges caller-supplied equip slots as a paperdoll — worn gear in two flanking columns around a
|
|
80
|
+
* center `portrait` (or a silhouette placeholder), with any `bottom` slots in a row beneath it. Each
|
|
81
|
+
* slot reuses the atomic {@link EquipmentSlots} tile (filled icon or themed empty placeholder), carries
|
|
82
|
+
* a label, and is a focusable `role="group"` with an `aria-label`; `onSlotActivate` fires on
|
|
83
|
+
* click/Enter/Space so games can wire drag or an equip picker. Composition and skin stay the game's.
|
|
84
|
+
*
|
|
85
|
+
* @capability character-paperdoll equip-slot paperdoll — flanking columns around a portrait, activatable slots
|
|
86
|
+
*/
|
|
87
|
+
export function Paperdoll({ slots, portrait, slotSize = 44, onSlotActivate, className, style }) {
|
|
88
|
+
const left = slots.filter((s) => (s.position ?? "left") === "left");
|
|
89
|
+
const right = slots.filter((s) => s.position === "right");
|
|
90
|
+
const bottom = slots.filter((s) => s.position === "bottom");
|
|
91
|
+
const column = (cells, align) => (_jsx("div", { style: { display: "flex", flexDirection: "column", gap: 10, alignItems: align === "right" ? "flex-end" : "flex-start" }, children: cells.map((slot) => (_jsx(EquipSlotCell, { slot: slot, slotSize: slotSize, align: align, ...(onSlotActivate === undefined ? {} : { onSlotActivate }) }, slot.id))) }));
|
|
92
|
+
return (_jsxs("div", { "data-paperdoll": "", className: className, style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 12, ...style }, children: [_jsxs("div", { style: { display: "flex", gap: 16, alignItems: "flex-start", justifyContent: "center" }, children: [column(left, "right"), _jsx("div", { "data-paperdoll-portrait": "", style: { display: "flex", alignItems: "center", justifyContent: "center", alignSelf: "stretch" }, children: portrait ?? _jsx(Silhouette, { size: slotSize * 1.4 }) }), column(right, "left")] }), bottom.length > 0 ? (_jsx("div", { style: { display: "flex", gap: 12, justifyContent: "center" }, children: bottom.map((slot) => (_jsx(EquipSlotCell, { slot: slot, slotSize: slotSize, align: "center", ...(onSlotActivate === undefined ? {} : { onSlotActivate }) }, slot.id))) })) : null] }));
|
|
93
|
+
}
|
|
94
|
+
function StatListRow({ row }) {
|
|
95
|
+
return (_jsxs("div", { "data-stat-row": row.id, style: { display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 12, padding: "2px 0" }, children: [_jsx("span", { style: { fontSize: 11, fontVariant: "small-caps", letterSpacing: 0.5, color: "var(--jg-bar-text, #f4f6fb)", opacity: 0.72 }, children: row.label }), _jsxs("span", { style: { fontSize: 12, fontWeight: 700, fontVariantNumeric: "tabular-nums", color: "var(--jg-accent, #38bdf8)" }, children: [row.value, row.hint !== undefined ? (_jsx("span", { style: { marginLeft: 5, fontWeight: 500, color: "var(--jg-bar-text, #f4f6fb)", opacity: 0.5 }, children: row.hint })) : null] })] }));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Renders derived character stats as clean small-caps label→value rows, token-themed. Rows carrying a
|
|
99
|
+
* `group` field are bucketed under a small uppercase header (consecutive same-group rows share one).
|
|
100
|
+
* Pure and prop-driven — the game supplies the rows (from `useEntityStat`/derived values); the engine
|
|
101
|
+
* ships no stat list of its own.
|
|
102
|
+
*
|
|
103
|
+
* @capability character-statlist derived-stat readout — grouped small-caps label→value rows, token-themed
|
|
104
|
+
*/
|
|
105
|
+
export function StatList({ stats, className, style }) {
|
|
106
|
+
const groups = [];
|
|
107
|
+
for (const row of stats) {
|
|
108
|
+
const last = groups[groups.length - 1];
|
|
109
|
+
if (last !== undefined && last.name === row.group)
|
|
110
|
+
last.rows.push(row);
|
|
111
|
+
else
|
|
112
|
+
groups.push({ name: row.group, rows: [row] });
|
|
113
|
+
}
|
|
114
|
+
return (_jsx("div", { "data-stat-list": "", className: className, style: { display: "flex", flexDirection: "column", gap: 4, ...style }, children: groups.map((group, index) => (_jsxs("div", { "data-stat-group": group.name ?? "", children: [group.name !== undefined ? (_jsx("div", { style: {
|
|
115
|
+
fontSize: 9,
|
|
116
|
+
letterSpacing: 1.4,
|
|
117
|
+
textTransform: "uppercase",
|
|
118
|
+
opacity: 0.55,
|
|
119
|
+
margin: "6px 0 2px",
|
|
120
|
+
color: "var(--jg-bar-text, #f4f6fb)",
|
|
121
|
+
}, children: group.name })) : null, group.rows.map((row) => (_jsx(StatListRow, { row: row }, row.id)))] }, index))) }));
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Drop-in character sheet: a {@link HudFrame} wrapping an optional name/subtitle/portrait header, the
|
|
125
|
+
* equip-slot {@link Paperdoll}, and the derived-stat {@link StatList}. The game supplies slots, stats,
|
|
126
|
+
* header content, and the activation callback, and owns the window that toggles it (this component does
|
|
127
|
+
* not manage its own open/close). Pure composition over existing primitives and HudTheme tokens.
|
|
128
|
+
*
|
|
129
|
+
* @capability character-sheet drop-in paperdoll + derived-stat sheet in a HudFrame — game supplies slots/stats
|
|
130
|
+
*/
|
|
131
|
+
export function CharacterSheet({ slots, stats, name, subtitle, portrait, header, title = "Character", variation = "themed", slotSize, onSlotActivate, className, style, }) {
|
|
132
|
+
const hasHeader = header !== undefined || name !== undefined || subtitle !== undefined;
|
|
133
|
+
return (_jsx(HudFrame, { variation: variation, title: title, interactive: true, padding: 14, ...(className === undefined ? {} : { className }), ...(style === undefined ? {} : { style }), children: _jsxs("div", { "data-character-sheet": "", style: { display: "flex", flexDirection: "column", gap: 12, minWidth: 260 }, children: [hasHeader ? (_jsx("div", { "data-character-header": "", style: { display: "flex", flexDirection: "column", gap: 1 }, children: header ?? (_jsxs(_Fragment, { children: [name !== undefined ? _jsx("div", { style: { fontSize: 16, fontWeight: 800, letterSpacing: 0.2 }, children: name }) : null, subtitle !== undefined ? (_jsx("div", { style: { fontSize: 11, opacity: 0.65, fontVariant: "small-caps", letterSpacing: 0.4 }, children: subtitle })) : null] })) })) : null, _jsxs("div", { style: { display: "flex", gap: 18, alignItems: "flex-start", flexWrap: "wrap" }, children: [_jsx(Paperdoll, { slots: slots, ...(portrait === undefined ? {} : { portrait }), ...(slotSize === undefined ? {} : { slotSize }), ...(onSlotActivate === undefined ? {} : { onSlotActivate }) }), _jsx(StatList, { stats: stats, style: { flex: "1 1 140px", minWidth: 140 } })] })] }) }));
|
|
134
|
+
}
|
package/dist/components.d.ts
CHANGED
|
@@ -12,7 +12,14 @@ export declare function SlotGrid({ inventoryId, className, renderSlot, }: {
|
|
|
12
12
|
className?: string;
|
|
13
13
|
renderSlot?: (slot: InventorySlot, index: number) => ReactNode;
|
|
14
14
|
}): import("react").JSX.Element;
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* A headless, unstyled stat meter — an accessible `role="progressbar"` you paint entirely through
|
|
17
|
+
* `className`/`fillClassName`. This is the class-hook primitive for teams supplying their own CSS;
|
|
18
|
+
* for a batteries-included themed bar use the atomic, token-styled components in
|
|
19
|
+
* `@jgengine/react/bars` (`HealthBar`, `ShieldBar`, …). Renamed from `HealthBar` in #1033 so the
|
|
20
|
+
* purpose-named atomic bar owns that name and no two components share it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function StatMeter({ instanceId, statId, className, fillClassName, }: {
|
|
16
23
|
instanceId: string;
|
|
17
24
|
statId: string;
|
|
18
25
|
className?: string;
|
package/dist/components.js
CHANGED
|
@@ -17,7 +17,14 @@ export function SlotGrid({ inventoryId, className, renderSlot, }) {
|
|
|
17
17
|
? `${slot.itemId} x${slot.count}`
|
|
18
18
|
: null }, index))) }));
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* A headless, unstyled stat meter — an accessible `role="progressbar"` you paint entirely through
|
|
22
|
+
* `className`/`fillClassName`. This is the class-hook primitive for teams supplying their own CSS;
|
|
23
|
+
* for a batteries-included themed bar use the atomic, token-styled components in
|
|
24
|
+
* `@jgengine/react/bars` (`HealthBar`, `ShieldBar`, …). Renamed from `HealthBar` in #1033 so the
|
|
25
|
+
* purpose-named atomic bar owns that name and no two components share it.
|
|
26
|
+
*/
|
|
27
|
+
export function StatMeter({ instanceId, statId, className, fillClassName, }) {
|
|
21
28
|
const stat = useEntityStat(instanceId, statId);
|
|
22
29
|
if (stat === null)
|
|
23
30
|
return null;
|
package/dist/hooks.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { type DependencyList } from "react";
|
|
1
2
|
import { AxisChannel, type AxisChannelConfig } from "@jgengine/core/input/axisInput";
|
|
3
|
+
import { type MarkerSet } from "@jgengine/core/world/markers";
|
|
2
4
|
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
3
5
|
import type { AbilityKit, AbilitySlotSnapshot } from "@jgengine/core/combat/abilityKit";
|
|
4
6
|
import type { EventMeter } from "@jgengine/core/stats/eventMeter";
|
|
@@ -53,6 +55,17 @@ export declare function useNearestWorldItem(radius: number): WorldItemRecord | n
|
|
|
53
55
|
export declare function useEntityStat(instanceId: string, statId: string): StatValue | null;
|
|
54
56
|
export declare function useTarget(fromInstanceId: string): string | null;
|
|
55
57
|
export declare function useInventory(inventoryId: string): readonly InventorySlot[];
|
|
58
|
+
/** Live slots plus `move`/`split` actions bound to `inventoryId`, routed through the notifying `inventory.move`/`inventory.split` commands so React re-renders. */
|
|
59
|
+
export interface InventoryGridBinding {
|
|
60
|
+
slots: readonly InventorySlot[];
|
|
61
|
+
move(from: number, to: number): void;
|
|
62
|
+
split(slot: number, amount: number, toSlot?: number): void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Binds a live inventory to a HUD grid: `slots` from {@link useInventory}, and `move`/`split` that run
|
|
66
|
+
* the built-in `inventory.move`/`inventory.split` commands (which notify, so the grid re-renders).
|
|
67
|
+
*/
|
|
68
|
+
export declare function useInventoryGrid(inventoryId: string): InventoryGridBinding;
|
|
56
69
|
export declare function useCurrency(currencyId: string): number;
|
|
57
70
|
export declare function useFeed({ action, limit }: {
|
|
58
71
|
action: string;
|
|
@@ -123,6 +136,24 @@ export interface EventMeterView {
|
|
|
123
136
|
* @capability event-meter-hud render a core event/heat meter's live value, fraction, tier, and ready state in a HUD gauge
|
|
124
137
|
*/
|
|
125
138
|
export declare function useEventMeter(meter: EventMeter, options?: AbilitySlotBindingOptions): EventMeterView;
|
|
139
|
+
/**
|
|
140
|
+
* A self-ticking {@link MarkerSet} kept in sync with the live scene: on a
|
|
141
|
+
* heartbeat (default 100ms, and once immediately) it clears the set and calls
|
|
142
|
+
* `rebuild(markers, ctx)` to repopulate it from the current scene, so a HUD map
|
|
143
|
+
* never hand-rolls a `setInterval` + `markers.clear()` + rescan. Each heartbeat
|
|
144
|
+
* also re-renders the calling component, so player-centered props derived from
|
|
145
|
+
* the live scene each tick (`center`, `facingYaw`, zone name) stay fresh — this
|
|
146
|
+
* fully subsumes the hand-rolled ticker, not just the marker rebuild. The set is
|
|
147
|
+
* stable across renders and re-seeded whenever `ctx`, `intervalMs`, or the
|
|
148
|
+
* supplied `deps` change. SSR-safe: the heartbeat only arms when `window` is
|
|
149
|
+
* available.
|
|
150
|
+
*
|
|
151
|
+
* @capability live-markers a self-ticking MarkerSet kept in sync from the live scene each HUD tick — pass a rebuild(markers, ctx) that clears+repopulates, no hand-rolled setInterval
|
|
152
|
+
*/
|
|
153
|
+
export declare function useLiveMarkers(rebuild: (markers: MarkerSet, ctx: GameContext) => void, options?: {
|
|
154
|
+
intervalMs?: number;
|
|
155
|
+
deps?: DependencyList;
|
|
156
|
+
}): MarkerSet;
|
|
126
157
|
type HeldKeyEventTarget = Pick<Window, "addEventListener" | "removeEventListener">;
|
|
127
158
|
export declare function createHeldKeyTracker(target: HeldKeyEventTarget): {
|
|
128
159
|
isDown: (code: string) => boolean;
|
package/dist/hooks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
2
2
|
import { AxisChannel } from "@jgengine/core/input/axisInput";
|
|
3
|
+
import { createMarkerSet } from "@jgengine/core/world/markers";
|
|
3
4
|
import { browseSessions, } from "@jgengine/core/multiplayer/matchmaking";
|
|
4
5
|
import { resolveActivePrompt, } from "@jgengine/core/interaction/proximityPrompt";
|
|
5
6
|
import { gamePhase, setGamePhase } from "@jgengine/core/game/gamePhase";
|
|
@@ -86,6 +87,21 @@ export function useTarget(fromInstanceId) {
|
|
|
86
87
|
export function useInventory(inventoryId) {
|
|
87
88
|
return useGameStore((ctx) => ctx.player.inventory.state(inventoryId).slots);
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Binds a live inventory to a HUD grid: `slots` from {@link useInventory}, and `move`/`split` that run
|
|
92
|
+
* the built-in `inventory.move`/`inventory.split` commands (which notify, so the grid re-renders).
|
|
93
|
+
*/
|
|
94
|
+
export function useInventoryGrid(inventoryId) {
|
|
95
|
+
const slots = useInventory(inventoryId);
|
|
96
|
+
const { commands } = useGame();
|
|
97
|
+
const move = useCallback((from, to) => {
|
|
98
|
+
commands.run("inventory.move", { inventoryId, from, to });
|
|
99
|
+
}, [commands, inventoryId]);
|
|
100
|
+
const split = useCallback((slot, amount, toSlot) => {
|
|
101
|
+
commands.run("inventory.split", toSlot === undefined ? { inventoryId, slot, amount } : { inventoryId, slot, amount, toSlot });
|
|
102
|
+
}, [commands, inventoryId]);
|
|
103
|
+
return useMemo(() => ({ slots, move, split }), [slots, move, split]);
|
|
104
|
+
}
|
|
89
105
|
export function useCurrency(currencyId) {
|
|
90
106
|
return useGameStore((ctx) => ctx.game.economy.balance(ctx.player.userId, currencyId));
|
|
91
107
|
}
|
|
@@ -275,6 +291,43 @@ export function useEventMeter(meter, options) {
|
|
|
275
291
|
previous.current = view;
|
|
276
292
|
return view;
|
|
277
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* A self-ticking {@link MarkerSet} kept in sync with the live scene: on a
|
|
296
|
+
* heartbeat (default 100ms, and once immediately) it clears the set and calls
|
|
297
|
+
* `rebuild(markers, ctx)` to repopulate it from the current scene, so a HUD map
|
|
298
|
+
* never hand-rolls a `setInterval` + `markers.clear()` + rescan. Each heartbeat
|
|
299
|
+
* also re-renders the calling component, so player-centered props derived from
|
|
300
|
+
* the live scene each tick (`center`, `facingYaw`, zone name) stay fresh — this
|
|
301
|
+
* fully subsumes the hand-rolled ticker, not just the marker rebuild. The set is
|
|
302
|
+
* stable across renders and re-seeded whenever `ctx`, `intervalMs`, or the
|
|
303
|
+
* supplied `deps` change. SSR-safe: the heartbeat only arms when `window` is
|
|
304
|
+
* available.
|
|
305
|
+
*
|
|
306
|
+
* @capability live-markers a self-ticking MarkerSet kept in sync from the live scene each HUD tick — pass a rebuild(markers, ctx) that clears+repopulates, no hand-rolled setInterval
|
|
307
|
+
*/
|
|
308
|
+
export function useLiveMarkers(rebuild, options) {
|
|
309
|
+
const ctx = useGameContext();
|
|
310
|
+
const markers = useMemo(() => createMarkerSet(() => ctx.time.now()), [ctx]);
|
|
311
|
+
const rebuildRef = useRef(rebuild);
|
|
312
|
+
rebuildRef.current = rebuild;
|
|
313
|
+
const intervalMs = options?.intervalMs ?? 100;
|
|
314
|
+
const deps = options?.deps ?? [];
|
|
315
|
+
const [, forceTick] = useState(0);
|
|
316
|
+
useEffect(() => {
|
|
317
|
+
const run = () => {
|
|
318
|
+
markers.clear();
|
|
319
|
+
rebuildRef.current(markers, ctx);
|
|
320
|
+
forceTick((value) => (value + 1) % 1_000_000);
|
|
321
|
+
};
|
|
322
|
+
run();
|
|
323
|
+
if (intervalMs <= 0 || typeof window === "undefined")
|
|
324
|
+
return undefined;
|
|
325
|
+
const id = window.setInterval(run, intervalMs);
|
|
326
|
+
return () => window.clearInterval(id);
|
|
327
|
+
// deps is spread so callers can re-seed on their own inputs.
|
|
328
|
+
}, [markers, ctx, intervalMs, ...deps]);
|
|
329
|
+
return markers;
|
|
330
|
+
}
|
|
278
331
|
export function createHeldKeyTracker(target) {
|
|
279
332
|
const held = new Set();
|
|
280
333
|
const onKeyDown = (event) => held.add(event.code);
|
package/dist/hud.d.ts
CHANGED
|
@@ -7,6 +7,13 @@ export type StatTone = keyof typeof TONES;
|
|
|
7
7
|
* A polished stat bar (health/mana/stamina/shield/xp) — a rounded, glassy meter with a tone-colored
|
|
8
8
|
* fill and an optional value readout. Reads `statId` off `entityId` (defaults to the local player).
|
|
9
9
|
* Renders nothing until the stat exists.
|
|
10
|
+
*
|
|
11
|
+
* @deprecated The `tone`-switched umbrella is being retired (#1033). Use the atomic, purpose-named,
|
|
12
|
+
* token-themed bars from `@jgengine/react/bars` instead — `HealthBar`, `ShieldBar`, `ManaBar`,
|
|
13
|
+
* `StaminaBar`, `ExperienceBar`, `SoulBar`, `BossBar`, `AmmoCounter` — each does one readout and is
|
|
14
|
+
* restyled globally via the shared `--jg-*` tokens (`barTokens`). Kept as a thin shim during migration.
|
|
15
|
+
*
|
|
16
|
+
* @capability hud-stat-bar optional stat-bound bar building block (health, mana, stamina) — prefer game-owned chrome
|
|
10
17
|
*/
|
|
11
18
|
export declare function StatBar({ statId, entityId, tone, label, showValue, width, icon, style, className, }: {
|
|
12
19
|
statId?: string;
|
|
@@ -20,14 +27,20 @@ export declare function StatBar({ statId, entityId, tone, label, showValue, widt
|
|
|
20
27
|
className?: string;
|
|
21
28
|
}): import("react").JSX.Element | null;
|
|
22
29
|
/**
|
|
23
|
-
* A numbered hotbar bound to an inventory —
|
|
24
|
-
*
|
|
30
|
+
* A numbered hotbar bound to an inventory — painted iconed slots (a `GameIcon` glyph over a
|
|
31
|
+
* school-keyed gradient with a count badge, #1035), an active-slot highlight, and a keycap per slot.
|
|
32
|
+
* Place it and pass the `inventoryId`; `activeSlot` highlights the equipped one. Supply `itemIcon` to
|
|
33
|
+
* map your item ids to your own glyphs — the default resolves a `GameIcon` from the item id.
|
|
34
|
+
*
|
|
35
|
+
* @capability hud-hotbar optional inventory hotbar building block — games own slot chrome
|
|
25
36
|
*/
|
|
26
|
-
export declare function Hotbar({ inventoryId, activeSlot, keys, slotSize, style, className, }: {
|
|
37
|
+
export declare function Hotbar({ inventoryId, activeSlot, keys, slotSize, itemIcon, style, className, }: {
|
|
27
38
|
inventoryId: string;
|
|
28
39
|
activeSlot?: number;
|
|
29
40
|
keys?: readonly string[];
|
|
30
41
|
slotSize?: number;
|
|
42
|
+
/** Caller-supplied item id → icon registry; return null/undefined to fall back to the default glyph. */
|
|
43
|
+
itemIcon?: (itemId: string) => ReactNode;
|
|
31
44
|
style?: CSSProperties;
|
|
32
45
|
className?: string;
|
|
33
46
|
}): import("react").JSX.Element;
|
|
@@ -49,6 +62,8 @@ export declare function Speedometer({ entityId, scale, unit, max, size, style, c
|
|
|
49
62
|
* A time-of-day clock reading the sim calendar — `Day N · HH:MM`, 24h or 12h. `controls` adds
|
|
50
63
|
* pause + the game's speed multipliers as clickable pills (the "fast-forward" bar), off by default so
|
|
51
64
|
* a game opts into letting the player scrub time.
|
|
65
|
+
*
|
|
66
|
+
* @capability hud-clock in-game time-of-day readout bound to the sim clock
|
|
52
67
|
*/
|
|
53
68
|
export declare function Clock({ format, showDay, controls, style, className, }: {
|
|
54
69
|
format?: "24h" | "12h";
|
|
@@ -68,7 +83,9 @@ export declare function WaveBanner({ wave, label, subtitle, style, className, }:
|
|
|
68
83
|
style?: CSSProperties;
|
|
69
84
|
className?: string;
|
|
70
85
|
}): import("react").JSX.Element;
|
|
71
|
-
/** A currency counter — an icon (emoji/char, default a coin) plus the live amount for `currencyId`.
|
|
86
|
+
/** A currency counter — an icon (emoji/char, default a coin) plus the live amount for `currencyId`. *
|
|
87
|
+
* @capability hud-coins currency counter widget bound to a currency id
|
|
88
|
+
*/
|
|
72
89
|
export declare function Coins({ currencyId, icon, style, className, }: {
|
|
73
90
|
currencyId: string;
|
|
74
91
|
icon?: ReactNode;
|
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;
|