@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,135 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
3
|
+
import { moveGridFocus } from "@jgengine/core/ui/actionModel";
|
|
4
|
+
import { DragGhost, DraggableCard, DropZone, useDragLayer } from "./dragLayer.js";
|
|
5
|
+
import { iconForItemId } from "./gameIcons.js";
|
|
6
|
+
import { IconTreatment, schoolForItem } from "./iconTreatment.js";
|
|
7
|
+
import { useInventoryGrid } from "./hooks.js";
|
|
8
|
+
/**
|
|
9
|
+
* A drop-in NxN inventory grid bound to a live inventory: `<InventoryGrid inventoryId="bag" columns={4} />`
|
|
10
|
+
* renders every slot (painted {@link IconTreatment} tile + count badge for filled, a themed `--jg-slot-*`
|
|
11
|
+
* placeholder for empty), and wires the real mechanics on top of the engine's inventory model —
|
|
12
|
+
* drag-to-move, swap, stack-merge with remainder (all via the `inventory.move` command), and stack-split
|
|
13
|
+
* (shift/right-click a stack, via `inventory.split`). Fully keyboard-accessible: `role="grid"`, arrow-key
|
|
14
|
+
* roving focus, Enter/Space to pick up and drop. Games own placement, skin, and terminology; the engine
|
|
15
|
+
* ships the working grid.
|
|
16
|
+
*
|
|
17
|
+
* @capability inventory-grid-hud drop-in NxN inventory grid with drag/swap/stack/split bound to a live inventory
|
|
18
|
+
*/
|
|
19
|
+
const ARROW_DIRECTIONS = {
|
|
20
|
+
ArrowRight: "right",
|
|
21
|
+
ArrowLeft: "left",
|
|
22
|
+
ArrowUp: "up",
|
|
23
|
+
ArrowDown: "down",
|
|
24
|
+
Home: "first",
|
|
25
|
+
End: "last",
|
|
26
|
+
};
|
|
27
|
+
function EmptySlot({ size, picked }) {
|
|
28
|
+
return (_jsx("div", { "data-slot-empty": "", style: {
|
|
29
|
+
width: size,
|
|
30
|
+
height: size,
|
|
31
|
+
borderRadius: "var(--jg-slot-radius, 9px)",
|
|
32
|
+
background: "var(--jg-slot-bg, rgba(12,14,20,0.5))",
|
|
33
|
+
border: picked
|
|
34
|
+
? "1px solid var(--jg-accent, #38bdf8)"
|
|
35
|
+
: "var(--jg-slot-border, 1px solid rgba(255,255,255,0.1))",
|
|
36
|
+
boxShadow: "inset 0 2px 6px rgba(0,0,0,0.45)",
|
|
37
|
+
boxSizing: "border-box",
|
|
38
|
+
} }));
|
|
39
|
+
}
|
|
40
|
+
/** A drop-in inventory grid with drag/swap/stack-merge/split and full keyboard control, bound to `inventoryId`. */
|
|
41
|
+
export function InventoryGrid({ inventoryId, columns, size = 44, gap = 6, itemIcon, onActivate, className, style, }) {
|
|
42
|
+
const { slots, move, split } = useInventoryGrid(inventoryId);
|
|
43
|
+
const cols = columns ?? Math.max(1, Math.ceil(Math.sqrt(slots.length)));
|
|
44
|
+
const [focus, setFocus] = useState(0);
|
|
45
|
+
const [picked, setPicked] = useState(null);
|
|
46
|
+
const cellRefs = useRef(new Map());
|
|
47
|
+
const layer = useDragLayer({
|
|
48
|
+
onDrop: (info) => {
|
|
49
|
+
const target = info.target === null ? null : Number(info.target);
|
|
50
|
+
if (target !== null && Number.isInteger(target) && target !== info.payload.value) {
|
|
51
|
+
move(info.payload.value, target);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (focus < slots.length)
|
|
57
|
+
cellRefs.current.get(focus)?.focus();
|
|
58
|
+
}, [focus, slots.length]);
|
|
59
|
+
const splitHalf = useCallback((index) => {
|
|
60
|
+
const slot = slots[index];
|
|
61
|
+
if (slot !== null && slot !== undefined && slot.count > 1)
|
|
62
|
+
split(index, Math.floor(slot.count / 2));
|
|
63
|
+
}, [slots, split]);
|
|
64
|
+
const onKeyDown = useCallback((event) => {
|
|
65
|
+
const dir = ARROW_DIRECTIONS[event.key];
|
|
66
|
+
if (dir !== undefined) {
|
|
67
|
+
const next = moveGridFocus(focus, slots.length, cols, dir);
|
|
68
|
+
if (next >= 0) {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
setFocus(next);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
if (picked === null) {
|
|
77
|
+
if (slots[focus] !== null && slots[focus] !== undefined)
|
|
78
|
+
setPicked(focus);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
if (picked !== focus)
|
|
82
|
+
move(picked, focus);
|
|
83
|
+
setPicked(null);
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (event.key === "Escape" && picked !== null) {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
setPicked(null);
|
|
90
|
+
}
|
|
91
|
+
}, [focus, slots, cols, picked, move]);
|
|
92
|
+
const rows = useMemo(() => {
|
|
93
|
+
const out = [];
|
|
94
|
+
for (let i = 0; i < slots.length; i += cols) {
|
|
95
|
+
out.push(Array.from({ length: Math.min(cols, slots.length - i) }, (_, k) => i + k));
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}, [slots.length, cols]);
|
|
99
|
+
const renderTile = (index) => {
|
|
100
|
+
const slot = slots[index] ?? null;
|
|
101
|
+
const isPicked = picked === index;
|
|
102
|
+
const label = slot === null
|
|
103
|
+
? `Empty slot ${index + 1}`
|
|
104
|
+
: `${slot.itemId}${slot.count > 1 ? ` (${slot.count})` : ""}, slot ${index + 1}`;
|
|
105
|
+
const custom = slot !== null ? itemIcon?.(slot.itemId) : undefined;
|
|
106
|
+
const named = slot !== null ? iconForItemId(slot.itemId) : null;
|
|
107
|
+
return (_jsx(DropZone, { id: String(index), layer: layer, children: _jsx("div", { role: "gridcell", "aria-label": label, "aria-selected": isPicked, tabIndex: focus === index ? 0 : -1, "data-slot": index, ref: (node) => {
|
|
108
|
+
if (node === null)
|
|
109
|
+
cellRefs.current.delete(index);
|
|
110
|
+
else
|
|
111
|
+
cellRefs.current.set(index, node);
|
|
112
|
+
}, onFocus: () => setFocus(index), onKeyDown: onKeyDown, onDoubleClick: () => {
|
|
113
|
+
if (slot !== null)
|
|
114
|
+
onActivate?.(index);
|
|
115
|
+
}, onClick: (event) => {
|
|
116
|
+
if (event.shiftKey) {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
splitHalf(index);
|
|
119
|
+
}
|
|
120
|
+
}, onContextMenu: (event) => {
|
|
121
|
+
if (slot !== null && slot.count > 1) {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
splitHalf(index);
|
|
124
|
+
}
|
|
125
|
+
}, style: { width: size, height: size, position: "relative", cursor: slot !== null ? "grab" : "default" }, children: slot !== null ? (_jsx(DraggableCard, { id: String(index), value: index, layer: layer, onRotate: false, children: custom !== undefined && custom !== null ? (_jsx(IconTreatment, { glyph: custom, size: size, count: slot.count, active: isPicked })) : (_jsx(IconTreatment, { ...(named === null ? {} : { icon: named }), school: schoolForItem(slot.itemId), size: size, count: slot.count, active: isPicked })) })) : (_jsx(EmptySlot, { size: size, picked: isPicked })) }) }, index));
|
|
126
|
+
};
|
|
127
|
+
return (_jsxs("div", { role: "grid", "aria-label": `${inventoryId} inventory`, "aria-colcount": cols, className: className, "data-inventory-grid": inventoryId, style: { display: "grid", gap, ...style }, children: [rows.map((row, r) => (_jsx("div", { role: "row", style: { display: "grid", gridTemplateColumns: `repeat(${cols}, ${size}px)`, gap }, children: row.map(renderTile) }, r))), _jsx(DragGhost, { layer: layer, children: (payload) => {
|
|
128
|
+
const slot = slots[payload.value] ?? null;
|
|
129
|
+
if (slot === null)
|
|
130
|
+
return null;
|
|
131
|
+
const named = iconForItemId(slot.itemId);
|
|
132
|
+
const custom = itemIcon?.(slot.itemId);
|
|
133
|
+
return custom !== undefined && custom !== null ? (_jsx(IconTreatment, { glyph: custom, size: size, count: slot.count })) : (_jsx(IconTreatment, { ...(named === null ? {} : { icon: named }), school: schoolForItem(slot.itemId), size: size, count: slot.count }));
|
|
134
|
+
} })] }));
|
|
135
|
+
}
|
package/dist/map.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { type ReactNode } from "react";
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
2
|
import type { FogField } from "@jgengine/core/world/fog";
|
|
3
|
-
import { type MapMarker, type MarkerKindStyle, type MarkerSet } from "@jgengine/core/world/markers";
|
|
3
|
+
import { type MapMarker, type MarkerCollection, type MarkerKindStyle, type MarkerSource, type MarkerSet, type MarkerView } from "@jgengine/core/world/markers";
|
|
4
4
|
import { type MinimapView, type WorldXZ } from "@jgengine/core/world/minimap";
|
|
5
5
|
import { type MapCellStates, type MapRoute, type MapZone } from "@jgengine/core/world/mapLayers";
|
|
6
|
-
|
|
6
|
+
/** Subscribe to a native marker set or external marker source, or read a static marker array. */
|
|
7
|
+
export declare function useMarkers<TMeta = unknown>(markers: MarkerSet<TMeta>): readonly MapMarker<TMeta>[];
|
|
8
|
+
export declare function useMarkers<TMarker extends MarkerView>(markers: readonly TMarker[] | MarkerSource<TMarker>): readonly TMarker[];
|
|
9
|
+
export declare function useMarkers(markers: MarkerCollection): readonly MarkerView[];
|
|
7
10
|
export declare function useFog(fog: FogField): ReturnType<FogField["cells"]>;
|
|
8
11
|
export interface MinimapProps {
|
|
9
|
-
|
|
12
|
+
/** Static views, an external marker source, or a native JGengine MarkerSet. */
|
|
13
|
+
markers: MarkerCollection;
|
|
10
14
|
center: WorldXZ;
|
|
11
15
|
worldRadius: number;
|
|
12
16
|
fog?: FogField;
|
|
@@ -27,6 +31,8 @@ export interface MinimapProps {
|
|
|
27
31
|
/** Click-to-pin seam (#285.6): receives the clicked world XZ via `unprojectFromMinimap`. */
|
|
28
32
|
onWorldClick?: (world: WorldXZ) => void;
|
|
29
33
|
className?: string;
|
|
34
|
+
/** Caller-owned outer chrome overrides, applied after the built-in defaults. */
|
|
35
|
+
style?: CSSProperties;
|
|
30
36
|
title?: string;
|
|
31
37
|
children?: ReactNode;
|
|
32
38
|
}
|
|
@@ -38,12 +44,13 @@ export interface MapBounds {
|
|
|
38
44
|
}
|
|
39
45
|
/**
|
|
40
46
|
* Framed circular minimap: optional baked terrain background, reveal-on-event
|
|
41
|
-
* fog overlay, categorized marker icons, and a facing arrow. Reads
|
|
42
|
-
*
|
|
47
|
+
* fog overlay, categorized marker icons, and a facing arrow. Reads static
|
|
48
|
+
* marker views, an external marker source, or a native `MarkerSet`; supply
|
|
49
|
+
* your own `kindStyles` palette to reskin. Does not require `GameProvider`.
|
|
43
50
|
*
|
|
44
51
|
* @capability minimap framed circular minimap with terrain bake, fog, markers, and facing arrow
|
|
45
52
|
*/
|
|
46
|
-
export declare function Minimap({ markers, center, worldRadius, fog, size, facingYaw, rotate, kindStyles, background, mapBounds, routes, zones, cellStates, onWorldClick, className, title, children, }: MinimapProps): ReactNode;
|
|
53
|
+
export declare function Minimap({ markers, center, worldRadius, fog, size, facingYaw, rotate, kindStyles, background, mapBounds, routes, zones, cellStates, onWorldClick, className, style, title, children, }: MinimapProps): ReactNode;
|
|
47
54
|
/** One dot (or, with `heading`, a rotated arrow) drawn by `MinimapChrome`. */
|
|
48
55
|
export interface MinimapChromeMarker {
|
|
49
56
|
id: string;
|
|
@@ -91,7 +98,7 @@ export interface CompassProps {
|
|
|
91
98
|
/** Facing yaw (`rotationY`, forward = `(sin yaw, cos yaw)`) of the tracked entity — pass it raw, never pre-converted to a bearing. */
|
|
92
99
|
facingYaw: number;
|
|
93
100
|
center?: WorldXZ;
|
|
94
|
-
markers?:
|
|
101
|
+
markers?: MarkerCollection;
|
|
95
102
|
width?: number;
|
|
96
103
|
fov?: number;
|
|
97
104
|
kindStyles?: Record<string, MarkerKindStyle>;
|
|
@@ -99,7 +106,8 @@ export interface CompassProps {
|
|
|
99
106
|
}
|
|
100
107
|
/**
|
|
101
108
|
* Horizontal compass strip centered on the player's facing direction, with the
|
|
102
|
-
* eight cardinals and optional marker pips
|
|
109
|
+
* eight cardinals and optional marker pips from static views, an external
|
|
110
|
+
* source, or a native `MarkerSet`.
|
|
103
111
|
*/
|
|
104
112
|
export declare function Compass({ facingYaw, center, markers, width, fov, kindStyles, className, }: CompassProps): ReactNode;
|
|
105
113
|
/** Props for `MinimapPanel` — all `MinimapProps` plus the zone-label/clock header and compass slots. */
|
|
@@ -125,8 +133,62 @@ export interface MinimapPanelProps extends MinimapProps {
|
|
|
125
133
|
* @capability minimap-panel circular minimap with zone label, clock, and compass composed together
|
|
126
134
|
*/
|
|
127
135
|
export declare function MinimapPanel({ zoneLabel, clock, showCompass, compassProps, headerClassName, zoneLabelClassName, clockClassName, compassClassName, children, ...minimapProps }: MinimapPanelProps): ReactNode;
|
|
136
|
+
/** A colored span across the {@link MinimapTrack} rail, given by 0..1 `start`/`end` fractions (e.g. from core `trackFraction`). */
|
|
137
|
+
export interface MinimapTrackSpan {
|
|
138
|
+
id: string;
|
|
139
|
+
/** 0..1 fraction where the span starts. */
|
|
140
|
+
start: number;
|
|
141
|
+
/** 0..1 fraction where the span ends. */
|
|
142
|
+
end: number;
|
|
143
|
+
/** Fill color; defaults to a translucent white. */
|
|
144
|
+
color?: string;
|
|
145
|
+
/** Tooltip / accessible label for the span. */
|
|
146
|
+
label?: string;
|
|
147
|
+
}
|
|
148
|
+
/** A point marker on the {@link MinimapTrack} rail at a 0..1 fraction (e.g. a gate, the exit, or the player). */
|
|
149
|
+
export interface MinimapTrackPip {
|
|
150
|
+
id: string;
|
|
151
|
+
/** 0..1 fraction along the rail. */
|
|
152
|
+
at: number;
|
|
153
|
+
/** Marker color; defaults to the rail foreground. */
|
|
154
|
+
color?: string;
|
|
155
|
+
/** Accessible label for the marker. */
|
|
156
|
+
label?: string;
|
|
157
|
+
/** Tooltip text (rendered as the `title` attribute). */
|
|
158
|
+
title?: string;
|
|
159
|
+
/** Silhouette: `gate` (thin tall bar) or a `dot`/`exit`/`player` circle of increasing size (default `dot`). */
|
|
160
|
+
shape?: "gate" | "dot" | "exit" | "player";
|
|
161
|
+
}
|
|
162
|
+
/** Props for {@link MinimapTrack}. */
|
|
163
|
+
export interface MinimapTrackProps {
|
|
164
|
+
/** Colored zone spans painted along the rail. */
|
|
165
|
+
spans?: readonly MinimapTrackSpan[];
|
|
166
|
+
/** Point markers (gates, exit, player) placed by 0..1 fraction. */
|
|
167
|
+
pips?: readonly MinimapTrackPip[];
|
|
168
|
+
/** Rail width (default `100%`). */
|
|
169
|
+
width?: number | string;
|
|
170
|
+
/** Rail thickness in px (default 12). */
|
|
171
|
+
height?: number;
|
|
172
|
+
/** Rail track color (default `rgba(255,255,255,0.10)`). */
|
|
173
|
+
railColor?: string;
|
|
174
|
+
className?: string;
|
|
175
|
+
style?: CSSProperties;
|
|
176
|
+
children?: ReactNode;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Horizontal linear track minimap — a rounded progress rail with colored zone
|
|
180
|
+
* `spans` and gate/exit/player `pips` positioned by 0..1 fraction. The
|
|
181
|
+
* structural counterpart to the radial {@link Minimap} for corridor/route
|
|
182
|
+
* games: fractions are supplied by the caller (via core `trackFraction`), so it
|
|
183
|
+
* reads no store and stays presentation-only, sharing chrome with the radial
|
|
184
|
+
* minimap.
|
|
185
|
+
*
|
|
186
|
+
* @capability minimap-track-hud horizontal linear track minimap (corridor/route progress) — colored zone spans and gate/player pips positioned by 0..1 fraction, sharing chrome with the radial Minimap
|
|
187
|
+
*/
|
|
188
|
+
export declare function MinimapTrack({ spans, pips, width, height, railColor, className, style, children, }: MinimapTrackProps): ReactNode;
|
|
128
189
|
export interface WorldMapProps {
|
|
129
|
-
|
|
190
|
+
/** Static views, an external marker source, or a native JGengine MarkerSet. */
|
|
191
|
+
markers: MarkerCollection;
|
|
130
192
|
bounds: MapBounds;
|
|
131
193
|
player?: WorldXZ;
|
|
132
194
|
/** Facing yaw (`rotationY`, forward = `(sin yaw, cos yaw)`) of the player — pass it raw, never pre-converted to a bearing. */
|
package/dist/map.js
CHANGED
|
@@ -68,20 +68,42 @@ function cellStateNodes(layers, project, scale, keyPrefix) {
|
|
|
68
68
|
}
|
|
69
69
|
return nodes;
|
|
70
70
|
}
|
|
71
|
+
function markerAccess(markers) {
|
|
72
|
+
if (Array.isArray(markers)) {
|
|
73
|
+
const snapshot = markers;
|
|
74
|
+
return { subscribe: NO_SUBSCRIBE, getSnapshot: () => snapshot, getServerSnapshot: () => snapshot };
|
|
75
|
+
}
|
|
76
|
+
if ("getSnapshot" in markers) {
|
|
77
|
+
const source = markers;
|
|
78
|
+
return {
|
|
79
|
+
subscribe: (listener) => source.subscribe(listener),
|
|
80
|
+
getSnapshot: () => source.getSnapshot(),
|
|
81
|
+
getServerSnapshot: () => (source.getServerSnapshot ?? source.getSnapshot).call(source),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const markerSet = markers;
|
|
85
|
+
return {
|
|
86
|
+
subscribe: (listener) => markerSet.subscribe(listener),
|
|
87
|
+
getSnapshot: () => markerSet.snapshot(),
|
|
88
|
+
getServerSnapshot: () => markerSet.snapshot(),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
71
91
|
export function useMarkers(markers) {
|
|
72
|
-
|
|
92
|
+
const access = useMemo(() => markerAccess(markers), [markers]);
|
|
93
|
+
return useSyncExternalStore(access.subscribe, access.getSnapshot, access.getServerSnapshot);
|
|
73
94
|
}
|
|
74
95
|
export function useFog(fog) {
|
|
75
96
|
return useSyncExternalStore(fog.subscribe, fog.cells, fog.cells);
|
|
76
97
|
}
|
|
77
98
|
/**
|
|
78
99
|
* Framed circular minimap: optional baked terrain background, reveal-on-event
|
|
79
|
-
* fog overlay, categorized marker icons, and a facing arrow. Reads
|
|
80
|
-
*
|
|
100
|
+
* fog overlay, categorized marker icons, and a facing arrow. Reads static
|
|
101
|
+
* marker views, an external marker source, or a native `MarkerSet`; supply
|
|
102
|
+
* your own `kindStyles` palette to reskin. Does not require `GameProvider`.
|
|
81
103
|
*
|
|
82
104
|
* @capability minimap framed circular minimap with terrain bake, fog, markers, and facing arrow
|
|
83
105
|
*/
|
|
84
|
-
export function Minimap({ markers, center, worldRadius, fog, size = 176, facingYaw = 0, rotate = false, kindStyles = DEFAULT_MARKER_KINDS, background, mapBounds, routes, zones, cellStates, onWorldClick, className, title = "Map", children, }) {
|
|
106
|
+
export function Minimap({ markers, center, worldRadius, fog, size = 176, facingYaw = 0, rotate = false, kindStyles = DEFAULT_MARKER_KINDS, background, mapBounds, routes, zones, cellStates, onWorldClick, className, style, title = "Map", children, }) {
|
|
85
107
|
const markerList = useMarkers(markers);
|
|
86
108
|
const fogCells = useSyncExternalStore(fog?.subscribe ?? NO_SUBSCRIBE, fog?.cells ?? NULL_CELLS, fog?.cells ?? NULL_CELLS);
|
|
87
109
|
const bearing = headingToBearing(facingYaw);
|
|
@@ -93,7 +115,7 @@ export function Minimap({ markers, center, worldRadius, fog, size = 176, facingY
|
|
|
93
115
|
};
|
|
94
116
|
const half = size / 2;
|
|
95
117
|
const clipId = `mm-clip-${size}`;
|
|
96
|
-
const sorted = [...markerList].sort((a, b) => markerKindStyle(a.kind, kindStyles).priority - markerKindStyle(b.kind, kindStyles).priority);
|
|
118
|
+
const sorted = useMemo(() => [...markerList].sort((a, b) => markerKindStyle(a.kind, kindStyles).priority - markerKindStyle(b.kind, kindStyles).priority), [markerList, kindStyles]);
|
|
97
119
|
const fogImage = useMemo(() => {
|
|
98
120
|
if (fogCells === null)
|
|
99
121
|
return null;
|
|
@@ -117,10 +139,12 @@ export function Minimap({ markers, center, worldRadius, fog, size = 176, facingY
|
|
|
117
139
|
borderRadius: 14,
|
|
118
140
|
padding: 8,
|
|
119
141
|
background: "linear-gradient(160deg, rgba(24,28,36,0.94), rgba(12,15,20,0.94))",
|
|
120
|
-
|
|
142
|
+
// Ring color reads the shared HudTheme `--jg-ring` token (#1034); fallback = the built-in ring.
|
|
143
|
+
border: "1px solid var(--jg-ring, rgba(148,163,184,0.28))",
|
|
121
144
|
boxShadow: "0 10px 30px rgba(0,0,0,0.45)",
|
|
122
145
|
color: "#e2e8f0",
|
|
123
146
|
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
147
|
+
...style,
|
|
124
148
|
}, children: [_jsxs("div", { style: {
|
|
125
149
|
display: "flex",
|
|
126
150
|
justifyContent: "space-between",
|
|
@@ -158,10 +182,11 @@ export function Minimap({ markers, center, worldRadius, fog, size = 176, facingY
|
|
|
158
182
|
...routeNodes(routes, projectXZ, "mm"),
|
|
159
183
|
];
|
|
160
184
|
})(), _jsx("circle", { cx: half, cy: half, r: half - 1, fill: "none", stroke: "rgba(148,163,184,0.12)" }), sorted.map((marker) => {
|
|
161
|
-
const
|
|
185
|
+
const kind = marker.kind ?? "marker";
|
|
186
|
+
const style = markerKindStyle(kind, kindStyles);
|
|
162
187
|
const projected = projectToMinimap(marker.position, view);
|
|
163
188
|
const at = projected.inside ? projected : clampToMinimapEdge(projected, size);
|
|
164
|
-
return (_jsxs("g", { "data-marker-kind":
|
|
189
|
+
return (_jsxs("g", { "data-marker-kind": kind, children: [_jsx("circle", { cx: at.x, cy: at.y, r: 7, fill: "rgba(2,6,12,0.55)" }), _jsx("text", { x: at.x, y: at.y + 3.5, textAnchor: "middle", fontSize: 10, fill: style.color, style: { fontWeight: 700 }, children: style.glyph })] }, marker.id));
|
|
165
190
|
}), _jsx("g", { transform: `translate(${half} ${half}) rotate(${(rotate ? 0 : bearing) * (180 / Math.PI)})`, children: _jsx("path", { d: "M0,-9 L6,7 L0,3 L-6,7 Z", fill: "#4ade80", stroke: "#052e16", strokeWidth: 0.75 }) })] }), _jsx("circle", { cx: half, cy: half, r: half - 1, fill: "none", stroke: "rgba(148,163,184,0.4)", strokeWidth: 1.5 }), _jsx("text", { x: half, y: 13, textAnchor: "middle", fontSize: 11, fill: "rgba(226,232,240,0.9)", style: { fontWeight: 700 }, children: "N" })] }), children] }));
|
|
166
191
|
}
|
|
167
192
|
/**
|
|
@@ -197,7 +222,6 @@ export function MinimapChrome({ view, frame = false, frameColor = "rgba(148,163,
|
|
|
197
222
|
const NO_SUBSCRIBE = () => () => undefined;
|
|
198
223
|
const NULL_CELLS = () => null;
|
|
199
224
|
const EMPTY_MARKERS_VALUE = [];
|
|
200
|
-
const EMPTY_MARKERS = () => EMPTY_MARKERS_VALUE;
|
|
201
225
|
const COMPASS_TICKS = [
|
|
202
226
|
{ cardinal: "N", bearing: 0 },
|
|
203
227
|
{ cardinal: "NE", bearing: Math.PI / 4 },
|
|
@@ -210,10 +234,11 @@ const COMPASS_TICKS = [
|
|
|
210
234
|
];
|
|
211
235
|
/**
|
|
212
236
|
* Horizontal compass strip centered on the player's facing direction, with the
|
|
213
|
-
* eight cardinals and optional marker pips
|
|
237
|
+
* eight cardinals and optional marker pips from static views, an external
|
|
238
|
+
* source, or a native `MarkerSet`.
|
|
214
239
|
*/
|
|
215
240
|
export function Compass({ facingYaw, center, markers, width = 340, fov = (Math.PI * 2) / 3, kindStyles = DEFAULT_MARKER_KINDS, className, }) {
|
|
216
|
-
const markerList =
|
|
241
|
+
const markerList = useMarkers(markers ?? EMPTY_MARKERS_VALUE);
|
|
217
242
|
const facingBearing = headingToBearing(facingYaw);
|
|
218
243
|
const half = fov / 2;
|
|
219
244
|
const toX = (bearing) => {
|
|
@@ -259,8 +284,9 @@ export function Compass({ facingYaw, center, markers, width = 340, fov = (Math.P
|
|
|
259
284
|
const x = toX(bearing);
|
|
260
285
|
if (x === null)
|
|
261
286
|
return null;
|
|
262
|
-
const
|
|
263
|
-
|
|
287
|
+
const kind = marker.kind ?? "marker";
|
|
288
|
+
const style = markerKindStyle(kind, kindStyles);
|
|
289
|
+
return (_jsx("span", { "data-compass-marker": kind, style: {
|
|
264
290
|
position: "absolute",
|
|
265
291
|
left: x,
|
|
266
292
|
bottom: 2,
|
|
@@ -293,6 +319,48 @@ export function MinimapPanel({ zoneLabel, clock, showCompass = true, compassProp
|
|
|
293
319
|
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
294
320
|
}, children: [zoneLabel !== undefined ? (_jsx("span", { className: zoneLabelClassName, "data-minimap-chrome-zone": true, style: { fontSize: 12, fontWeight: 700 }, children: zoneLabel })) : (_jsx("span", {})), clock !== undefined ? (_jsx("span", { className: clockClassName, "data-minimap-chrome-clock": true, style: { fontSize: 11, color: "rgba(226,232,240,0.75)" }, children: clock })) : null] })) : null, _jsx(Minimap, { ...minimapProps, children: children }), showCompass ? (_jsx("div", { className: compassClassName, "data-minimap-chrome-compass": true, style: { marginTop: 6 }, children: _jsx(Compass, { facingYaw: minimapProps.facingYaw ?? 0, center: minimapProps.center, markers: minimapProps.markers, width: minimapProps.size ?? 176, ...compassProps }) })) : null] }));
|
|
295
321
|
}
|
|
322
|
+
const TRACK_PIP_DIAMETER = {
|
|
323
|
+
dot: 6,
|
|
324
|
+
exit: 8,
|
|
325
|
+
player: 12,
|
|
326
|
+
};
|
|
327
|
+
/**
|
|
328
|
+
* Horizontal linear track minimap — a rounded progress rail with colored zone
|
|
329
|
+
* `spans` and gate/exit/player `pips` positioned by 0..1 fraction. The
|
|
330
|
+
* structural counterpart to the radial {@link Minimap} for corridor/route
|
|
331
|
+
* games: fractions are supplied by the caller (via core `trackFraction`), so it
|
|
332
|
+
* reads no store and stays presentation-only, sharing chrome with the radial
|
|
333
|
+
* minimap.
|
|
334
|
+
*
|
|
335
|
+
* @capability minimap-track-hud horizontal linear track minimap (corridor/route progress) — colored zone spans and gate/player pips positioned by 0..1 fraction, sharing chrome with the radial Minimap
|
|
336
|
+
*/
|
|
337
|
+
export function MinimapTrack({ spans = [], pips = [], width = "100%", height = 12, railColor = "rgba(255,255,255,0.10)", className, style, children, }) {
|
|
338
|
+
return (_jsxs("div", { className: className, "data-minimap-track": true, style: { position: "relative", width, height, borderRadius: 9999, background: railColor, ...style }, children: [spans.map((span) => (_jsx("div", { "data-track-span": span.id, title: span.label, style: {
|
|
339
|
+
position: "absolute",
|
|
340
|
+
top: 0,
|
|
341
|
+
bottom: 0,
|
|
342
|
+
left: `${span.start * 100}%`,
|
|
343
|
+
width: `${(span.end - span.start) * 100}%`,
|
|
344
|
+
background: span.color ?? "rgba(255,255,255,0.25)",
|
|
345
|
+
opacity: 0.4,
|
|
346
|
+
borderRadius: 9999,
|
|
347
|
+
} }, span.id))), pips.map((pip) => {
|
|
348
|
+
const shape = pip.shape ?? "dot";
|
|
349
|
+
const color = pip.color ?? "#f4f6fb";
|
|
350
|
+
const common = {
|
|
351
|
+
position: "absolute",
|
|
352
|
+
top: "50%",
|
|
353
|
+
left: `${pip.at * 100}%`,
|
|
354
|
+
transform: "translate(-50%, -50%)",
|
|
355
|
+
background: color,
|
|
356
|
+
};
|
|
357
|
+
if (shape === "gate") {
|
|
358
|
+
return (_jsx("div", { "data-track-pip": pip.id, "data-pip-shape": "gate", title: pip.title ?? pip.label, style: { ...common, width: 3, height: height + 6, borderRadius: 2 } }, pip.id));
|
|
359
|
+
}
|
|
360
|
+
const diameter = TRACK_PIP_DIAMETER[shape];
|
|
361
|
+
return (_jsx("div", { "data-track-pip": pip.id, "data-pip-shape": shape, title: pip.title ?? pip.label, style: { ...common, width: diameter, height: diameter, borderRadius: 9999, border: "1px solid rgba(0,0,0,0.6)" } }, pip.id));
|
|
362
|
+
}), children] }));
|
|
363
|
+
}
|
|
296
364
|
function WorldMapFogImage({ fogCells, project, worldW, worldD, width, mapH, }) {
|
|
297
365
|
const fogImage = useMemo(() => {
|
|
298
366
|
if (fogCells === null)
|
|
@@ -361,8 +429,9 @@ export function WorldMap({ markers, bounds, player, facingYaw = 0, fog, backgrou
|
|
|
361
429
|
];
|
|
362
430
|
})(), markerList.map((marker) => {
|
|
363
431
|
const at = project(marker.position[0], marker.position[2]);
|
|
364
|
-
const
|
|
365
|
-
|
|
432
|
+
const kind = marker.kind ?? "marker";
|
|
433
|
+
const style = markerKindStyle(kind, kindStyles);
|
|
434
|
+
return (_jsxs("g", { "data-world-marker": kind, children: [_jsx("circle", { cx: at.x, cy: at.y, r: 9, fill: "rgba(2,6,12,0.6)", stroke: style.color, strokeWidth: 1.2 }), _jsx("text", { x: at.x, y: at.y + 4, textAnchor: "middle", fontSize: 12, fill: style.color, style: { fontWeight: 700 }, children: style.glyph }), marker.label !== undefined ? (_jsx("text", { x: at.x + 12, y: at.y + 4, fontSize: 11, fill: "rgba(226,232,240,0.85)", children: marker.label })) : null] }, marker.id));
|
|
366
435
|
}), player !== undefined
|
|
367
436
|
? (() => {
|
|
368
437
|
const at = project(player[0], player[1]);
|
package/dist/panels.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import { type PanelDef, type PanelPosition, type PanelState } from "@jgengine/core/ui/panelModel";
|
|
3
|
+
import { type HudFrameShape, type HudFrameVariation } from "./hudFrame.js";
|
|
4
|
+
/**
|
|
5
|
+
* React chrome over the headless panel/window model (`@jgengine/core/ui/panelModel`) — the toggleable,
|
|
6
|
+
* draggable, z-stacked windows a WoW-style UI is made of (B for bag, C for character, ESC to close).
|
|
7
|
+
* `usePanels` is the DATA/HOOK layer (open/close/toggle/focus + a keybind listener), `PanelHost` renders
|
|
8
|
+
* the open windows in z-order, and `Window` is a standalone one-off window. All unskinned and
|
|
9
|
+
* token-driven: games reskin via `HudTheme` tokens and by swapping the `HudFrame` variation.
|
|
10
|
+
*/
|
|
11
|
+
/** Intent a key press resolves to over a panel set: toggle a panel, close the top window, or ignore. */
|
|
12
|
+
export interface PanelKeyResult {
|
|
13
|
+
handled: boolean;
|
|
14
|
+
type?: "toggle" | "closeTop";
|
|
15
|
+
id?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Pure keybind resolver for a panel set: ESC resolves to `closeTop` when a closable window is open, and
|
|
19
|
+
* any other key routes through {@link panelByHotkey} (trying `code` then `key`) to a `toggle`. Returns
|
|
20
|
+
* the intent without touching the DOM, so it unit-tests headless and `usePanels` is a thin shell over it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function panelKeyAction(defs: readonly PanelDef[], state: PanelState, event: {
|
|
23
|
+
code?: string;
|
|
24
|
+
key?: string;
|
|
25
|
+
}): PanelKeyResult;
|
|
26
|
+
/** Options for {@link usePanels}. */
|
|
27
|
+
export interface UsePanelsOptions {
|
|
28
|
+
/** Fired when a panel transitions from closed to open (not on refocus of an already-open panel). */
|
|
29
|
+
onOpen?: (id: string) => void;
|
|
30
|
+
/** Fired when an open panel is closed. */
|
|
31
|
+
onClose?: (id: string) => void;
|
|
32
|
+
/** Set false to skip installing the window keybind/ESC listener (default true). */
|
|
33
|
+
hotkeys?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** The live panel manager returned by {@link usePanels} and consumed by {@link PanelHost}. */
|
|
36
|
+
export interface PanelsManager {
|
|
37
|
+
state: PanelState;
|
|
38
|
+
defs: readonly PanelDef[];
|
|
39
|
+
isOpen: (id: string) => boolean;
|
|
40
|
+
open: (id: string) => void;
|
|
41
|
+
close: (id: string) => void;
|
|
42
|
+
toggle: (id: string) => void;
|
|
43
|
+
focus: (id: string) => void;
|
|
44
|
+
move: (id: string, pos: PanelPosition) => void;
|
|
45
|
+
byHotkey: (code: string) => string | null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The DATA/HOOK layer: wrap the core panel model in state and install a window keybind listener —
|
|
49
|
+
* per-panel hotkeys toggle their window and ESC closes the topmost closable one. Hotkeys are ignored
|
|
50
|
+
* while the player is typing in an input/textarea/contenteditable. The listener is cleaned up on
|
|
51
|
+
* unmount and SSR-safe (armed in an effect). Feed the returned manager to {@link PanelHost}, or drive a
|
|
52
|
+
* bespoke window layout from it directly.
|
|
53
|
+
*
|
|
54
|
+
* @capability use-panels headless toggleable-window manager with keybind + ESC handling over the core panel model
|
|
55
|
+
*/
|
|
56
|
+
export declare function usePanels(defs: readonly PanelDef[], options?: UsePanelsOptions): PanelsManager;
|
|
57
|
+
/**
|
|
58
|
+
* Renders the open panels of a {@link usePanels} manager as absolutely-positioned, draggable, closable
|
|
59
|
+
* windows in z-order — the WoW window layer. Each window uses {@link HudFrame} chrome, is dragged by its
|
|
60
|
+
* title bar, raises focus on pointer-down, and gets its content from the `render` prop (or a `panels`
|
|
61
|
+
* map). Accessible: every window is a `role="dialog"` with an `aria-label` and a focusable close button;
|
|
62
|
+
* ESC-close is wired by the manager. Unskinned — art-direct via `HudTheme` tokens and the `variation`.
|
|
63
|
+
*
|
|
64
|
+
* @capability panel-host render a manager's open windows as draggable, closable, z-stacked dialogs
|
|
65
|
+
*/
|
|
66
|
+
export declare function PanelHost({ manager, render, panels, variation, shape, width, className, windowClassName, windowStyle, bodyStyle, }: {
|
|
67
|
+
manager: PanelsManager;
|
|
68
|
+
/** Content for a panel by id. Takes precedence over `panels`. */
|
|
69
|
+
render?: (id: string) => ReactNode;
|
|
70
|
+
/** Static content map by panel id, used when `render` is omitted. */
|
|
71
|
+
panels?: Record<string, ReactNode>;
|
|
72
|
+
variation?: HudFrameVariation;
|
|
73
|
+
shape?: HudFrameShape;
|
|
74
|
+
/** Default window width (per-panel unless overridden by the panel content). */
|
|
75
|
+
width?: number | string;
|
|
76
|
+
className?: string;
|
|
77
|
+
windowClassName?: string;
|
|
78
|
+
windowStyle?: CSSProperties;
|
|
79
|
+
bodyStyle?: CSSProperties;
|
|
80
|
+
}): import("react").JSX.Element;
|
|
81
|
+
/**
|
|
82
|
+
* A single standalone window — title bar, close button, and title-bar drag over {@link HudFrame} chrome —
|
|
83
|
+
* usable without the {@link usePanels} manager for a one-off dialog. Position is uncontrolled (seeded by
|
|
84
|
+
* `x`/`y`) unless both `x`/`y` and `onMove` are supplied, in which case the caller owns placement.
|
|
85
|
+
* Accessible: `role="dialog"` with an `aria-label` and a focusable close button.
|
|
86
|
+
*
|
|
87
|
+
* @capability window standalone draggable, closable window primitive over HudFrame — no manager required
|
|
88
|
+
*/
|
|
89
|
+
export declare function Window({ title, ariaLabel, closable, onClose, x, y, onMove, z, width, variation, shape, className, style, bodyStyle, children, }: {
|
|
90
|
+
title: ReactNode;
|
|
91
|
+
ariaLabel?: string;
|
|
92
|
+
closable?: boolean;
|
|
93
|
+
onClose?: () => void;
|
|
94
|
+
/** Initial (uncontrolled) or current (controlled, with `onMove`) x position. Default 56. */
|
|
95
|
+
x?: number;
|
|
96
|
+
/** Initial (uncontrolled) or current (controlled, with `onMove`) y position. Default 56. */
|
|
97
|
+
y?: number;
|
|
98
|
+
onMove?: (pos: PanelPosition) => void;
|
|
99
|
+
z?: number;
|
|
100
|
+
width?: number | string;
|
|
101
|
+
variation?: HudFrameVariation;
|
|
102
|
+
shape?: HudFrameShape;
|
|
103
|
+
className?: string;
|
|
104
|
+
style?: CSSProperties;
|
|
105
|
+
bodyStyle?: CSSProperties;
|
|
106
|
+
children?: ReactNode;
|
|
107
|
+
}): import("react").JSX.Element;
|