@jgengine/react 0.13.0 → 0.15.0-next.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 +529 -1
- package/dist/accessibility.d.ts +39 -0
- package/dist/accessibility.js +61 -0
- package/dist/achievements.d.ts +50 -0
- package/dist/achievements.js +94 -0
- package/dist/cameraShake.d.ts +52 -0
- package/dist/cameraShake.js +53 -0
- package/dist/coachMarks.d.ts +72 -0
- package/dist/coachMarks.js +136 -0
- package/dist/codex.d.ts +25 -0
- package/dist/codex.js +62 -0
- package/dist/comboMeter.d.ts +63 -0
- package/dist/comboMeter.js +93 -0
- package/dist/components.js +4 -3
- package/dist/creditsScreen.d.ts +19 -0
- package/dist/creditsScreen.js +31 -0
- package/dist/cutscene.d.ts +66 -0
- package/dist/cutscene.js +132 -0
- package/dist/damageDirection.d.ts +51 -0
- package/dist/damageDirection.js +103 -0
- package/dist/dialogueView.d.ts +69 -0
- package/dist/dialogueView.js +111 -0
- package/dist/entityFrames.d.ts +114 -0
- package/dist/entityFrames.js +55 -0
- package/dist/fastTravel.d.ts +36 -0
- package/dist/fastTravel.js +103 -0
- package/dist/floatingText.d.ts +84 -0
- package/dist/floatingText.js +61 -0
- package/dist/gameViewport.js +8 -12
- package/dist/hooks.d.ts +42 -0
- package/dist/hooks.js +85 -0
- package/dist/hudLayout.js +138 -9
- package/dist/hudTheme.d.ts +104 -7
- package/dist/hudTheme.js +300 -2
- package/dist/i18n.d.ts +29 -0
- package/dist/i18n.js +44 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +34 -0
- package/dist/interactionPrompt.d.ts +80 -0
- package/dist/interactionPrompt.js +112 -0
- package/dist/keybindingMenu.d.ts +59 -0
- package/dist/keybindingMenu.js +146 -0
- package/dist/killFeed.d.ts +63 -0
- package/dist/killFeed.js +88 -0
- package/dist/map.d.ts +123 -2
- package/dist/map.js +239 -36
- package/dist/menuRouter.d.ts +51 -0
- package/dist/menuRouter.js +49 -0
- package/dist/modals.d.ts +131 -0
- package/dist/modals.js +168 -0
- package/dist/notifications.d.ts +39 -0
- package/dist/notifications.js +111 -0
- package/dist/objectiveBanner.d.ts +62 -0
- package/dist/objectiveBanner.js +129 -0
- package/dist/panels.d.ts +10 -2
- package/dist/panels.js +9 -3
- package/dist/photoMode.d.ts +21 -0
- package/dist/photoMode.js +39 -0
- package/dist/previewFixtures.d.ts +30 -0
- package/dist/previewFixtures.js +46 -0
- package/dist/provider.d.ts +15 -0
- package/dist/provider.js +17 -1
- package/dist/questTracker.d.ts +24 -0
- package/dist/questTracker.js +31 -0
- package/dist/quickMenu.d.ts +39 -0
- package/dist/quickMenu.js +79 -0
- package/dist/radialMenu.d.ts +50 -0
- package/dist/radialMenu.js +69 -0
- package/dist/saveSlots.d.ts +81 -0
- package/dist/saveSlots.js +138 -0
- package/dist/scoreboard.d.ts +67 -0
- package/dist/scoreboard.js +71 -0
- package/dist/settings.d.ts +9 -3
- package/dist/settings.js +10 -4
- package/dist/shopGrid.d.ts +70 -0
- package/dist/shopGrid.js +129 -0
- package/dist/statusEffectBar.d.ts +52 -0
- package/dist/statusEffectBar.js +57 -0
- package/dist/streetGeometryPreview.d.ts +7 -0
- package/dist/streetGeometryPreview.js +119 -0
- package/dist/talentTree.d.ts +60 -0
- package/dist/talentTree.js +217 -0
- package/dist/timerReadout.d.ts +90 -0
- package/dist/timerReadout.js +111 -0
- package/dist/useDebouncedCommit.d.ts +54 -0
- package/dist/useDebouncedCommit.js +81 -0
- package/dist/waveHud.d.ts +49 -0
- package/dist/waveHud.js +53 -0
- package/dist/waypointMarkers.d.ts +80 -0
- package/dist/waypointMarkers.js +116 -0
- package/package.json +3 -3
- package/skills/ce-handoff/SKILL.md +149 -0
- package/skills/jgengine/SKILL.md +7 -3
- package/skills/jgengine/api.md +17 -7
- package/skills/jgengine/capabilities.md +12 -0
- package/skills/jgengine/recipes/minimal-game.md +7 -4
- package/skills/jgengine-ui/SKILL.md +8 -3
- package/skills/jgengine-ui/api.md +732 -23
- package/skills/jgengine-ui/capabilities.md +371 -2
- package/skills/jgengine-ui/recipes/overhead-entity-frames.md +136 -0
- package/skills/jgengine-ui/reference.md +7 -4
package/dist/gameViewport.js
CHANGED
|
@@ -219,18 +219,14 @@ export function GameViewportProvider({ platforms, className, style, children, })
|
|
|
219
219
|
}, [regions, metrics.layout, visual, vw, vh, coarsePointer, mobileSupported, safeArea]);
|
|
220
220
|
const collisions = useMemo(() => detectLayoutCollisions(regions), [regions]);
|
|
221
221
|
const forbidden = useMemo(() => collisions.filter((c) => c.severity === "forbid"), [collisions]);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
return;
|
|
222
|
+
const collisionReport = forbidden.length === 0 ? undefined : JSON.stringify(forbidden);
|
|
223
|
+
const lastWarnedRef = useRef(undefined);
|
|
224
|
+
if (collisionReport !== lastWarnedRef.current) {
|
|
225
|
+
lastWarnedRef.current = collisionReport;
|
|
226
|
+
if (collisionReport !== undefined) {
|
|
227
|
+
console.warn(`[jgengine] ${formatLayoutCollisions(forbidden)}\n mode=${layout.mode} viewport=${Math.round(vw)}x${Math.round(vh)} safe=${safeArea.top}/${safeArea.right}/${safeArea.bottom}/${safeArea.left}`);
|
|
229
228
|
}
|
|
230
|
-
|
|
231
|
-
root.setAttribute("data-jg-layout-collision", report);
|
|
232
|
-
console.warn(`[jgengine] ${formatLayoutCollisions(forbidden)}\n mode=${layout.mode} viewport=${Math.round(vw)}x${Math.round(vh)} safe=${safeArea.top}/${safeArea.right}/${safeArea.bottom}/${safeArea.left}`);
|
|
233
|
-
}, [forbidden, layout.mode, vw, vh, safeArea]);
|
|
229
|
+
}
|
|
234
230
|
useEffect(() => {
|
|
235
231
|
const ids = new Set();
|
|
236
232
|
for (const collision of forbidden) {
|
|
@@ -265,7 +261,7 @@ export function GameViewportProvider({ platforms, className, style, children, })
|
|
|
265
261
|
"--jg-safe-left": `${safeArea.left}px`,
|
|
266
262
|
...style,
|
|
267
263
|
};
|
|
268
|
-
return (_jsx(RegionRegistryContext.Provider, { value: registry, children: _jsx(GameLayoutContext.Provider, { value: layout, children: _jsxs("div", { ref: rootRef, "data-jg-viewport": "", "data-jg-layout-mode": layout.mode, "data-jg-orientation": layout.orientation, className: className, style: rootStyle, children: [_jsx("div", { ref: probeRef, "aria-hidden": true, style: SAFE_AREA_PROBE_STYLE }), children] }) }) }));
|
|
264
|
+
return (_jsx(RegionRegistryContext.Provider, { value: registry, children: _jsx(GameLayoutContext.Provider, { value: layout, children: _jsxs("div", { ref: rootRef, "data-jg-viewport": "", "data-jg-layout-mode": layout.mode, "data-jg-orientation": layout.orientation, "data-jg-layout-collision": collisionReport, className: className, style: rootStyle, children: [_jsx("div", { ref: probeRef, "aria-hidden": true, style: SAFE_AREA_PROBE_STYLE }), children] }) }) }));
|
|
269
265
|
}
|
|
270
266
|
/** The live shared viewport layout. Returns a neutral default outside a `GameViewportProvider` so it never throws in previews. */
|
|
271
267
|
export function useGameViewportLayout() {
|
package/dist/hooks.d.ts
CHANGED
|
@@ -175,4 +175,46 @@ export interface UseAxisChannelResult {
|
|
|
175
175
|
* constant at the call site) unless a rebind is intended.
|
|
176
176
|
*/
|
|
177
177
|
export declare function useAxisChannel(config: AxisChannelConfig): UseAxisChannelResult;
|
|
178
|
+
/**
|
|
179
|
+
* Re-render at a steady rate. Returns a monotonically increasing tick count driven by a
|
|
180
|
+
* setInterval, for HUD elements that display wall-clock-derived values (cooldowns, cast bars,
|
|
181
|
+
* swing timers) without an engine subscription to hang off. `hz <= 0` disables the ticker.
|
|
182
|
+
*
|
|
183
|
+
* @capability hud-ticker re-render a HUD element at a steady hz for time-derived readouts (cooldowns, cast/swing bars) — no hand-rolled setInterval effects
|
|
184
|
+
*/
|
|
185
|
+
export declare function useTicker(hz?: number): number;
|
|
186
|
+
/** Minimal add/removeEventListener surface accepted by useDomEvent (window, document, elements). */
|
|
187
|
+
export interface DomEventTarget {
|
|
188
|
+
addEventListener(type: string, listener: (event: never) => void, options?: unknown): void;
|
|
189
|
+
removeEventListener(type: string, listener: (event: never) => void, options?: unknown): void;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Attach a DOM event listener with automatic cleanup. `resolveTarget` runs inside the effect, so
|
|
193
|
+
* `() => window` and ref-reading resolvers are SSR-safe; return null to skip attaching. The
|
|
194
|
+
* handler is kept in a ref, so a fresh closure per render never re-binds the listener.
|
|
195
|
+
* Re-binds only when `type` or `options.capture`/`options.passive` change.
|
|
196
|
+
*
|
|
197
|
+
* @capability dom-event attach a window/document/element event listener with automatic cleanup and a stable handler ref — no hand-rolled addEventListener effects
|
|
198
|
+
*/
|
|
199
|
+
export declare function useDomEvent<E>(resolveTarget: () => DomEventTarget | null, type: string, handler: (event: E) => void, options?: {
|
|
200
|
+
capture?: boolean;
|
|
201
|
+
passive?: boolean;
|
|
202
|
+
}): void;
|
|
203
|
+
/**
|
|
204
|
+
* Run a requestAnimationFrame loop while `active`, calling `onFrame(deltaSeconds)` each frame.
|
|
205
|
+
* The callback is kept in a ref so re-renders never restart the loop; cleanup cancels the
|
|
206
|
+
* pending frame. For scene-graph work inside a canvas prefer R3F's useFrame — this is for
|
|
207
|
+
* DOM-side animation outside the renderer.
|
|
208
|
+
*
|
|
209
|
+
* @capability raf-loop run a cancellable requestAnimationFrame loop for DOM-side animation with delta seconds — no hand-rolled RAF effects
|
|
210
|
+
*/
|
|
211
|
+
export declare function useRafLoop(onFrame: (deltaSeconds: number) => void, active?: boolean): void;
|
|
212
|
+
/**
|
|
213
|
+
* Pin a scrollable element to its bottom whenever `dep` changes (typically a length or the list
|
|
214
|
+
* itself). Attach the returned ref to the scroll container. Owns the log/chat/console
|
|
215
|
+
* scroll-to-bottom effect so panels don't hand-roll it.
|
|
216
|
+
*
|
|
217
|
+
* @capability auto-scroll pin a log/chat/console panel to its newest line as entries arrive — no hand-rolled scrollTop effects
|
|
218
|
+
*/
|
|
219
|
+
export declare function useAutoScroll<T extends HTMLElement>(dep: unknown): import("react").RefObject<T | null>;
|
|
178
220
|
export {};
|
package/dist/hooks.js
CHANGED
|
@@ -375,3 +375,88 @@ export function useAxisChannel(config) {
|
|
|
375
375
|
const channel = useMemo(() => new AxisChannel(config), [config]);
|
|
376
376
|
return { channel, isDown };
|
|
377
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* Re-render at a steady rate. Returns a monotonically increasing tick count driven by a
|
|
380
|
+
* setInterval, for HUD elements that display wall-clock-derived values (cooldowns, cast bars,
|
|
381
|
+
* swing timers) without an engine subscription to hang off. `hz <= 0` disables the ticker.
|
|
382
|
+
*
|
|
383
|
+
* @capability hud-ticker re-render a HUD element at a steady hz for time-derived readouts (cooldowns, cast/swing bars) — no hand-rolled setInterval effects
|
|
384
|
+
*/
|
|
385
|
+
export function useTicker(hz = 10) {
|
|
386
|
+
const [tick, setTick] = useState(0);
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
if (!Number.isFinite(hz) || hz <= 0)
|
|
389
|
+
return undefined;
|
|
390
|
+
const timer = setInterval(() => setTick((value) => value + 1), 1000 / hz);
|
|
391
|
+
return () => clearInterval(timer);
|
|
392
|
+
}, [hz]);
|
|
393
|
+
return tick;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Attach a DOM event listener with automatic cleanup. `resolveTarget` runs inside the effect, so
|
|
397
|
+
* `() => window` and ref-reading resolvers are SSR-safe; return null to skip attaching. The
|
|
398
|
+
* handler is kept in a ref, so a fresh closure per render never re-binds the listener.
|
|
399
|
+
* Re-binds only when `type` or `options.capture`/`options.passive` change.
|
|
400
|
+
*
|
|
401
|
+
* @capability dom-event attach a window/document/element event listener with automatic cleanup and a stable handler ref — no hand-rolled addEventListener effects
|
|
402
|
+
*/
|
|
403
|
+
export function useDomEvent(resolveTarget, type, handler, options) {
|
|
404
|
+
const handlerRef = useRef(handler);
|
|
405
|
+
handlerRef.current = handler;
|
|
406
|
+
const capture = options?.capture ?? false;
|
|
407
|
+
const passive = options?.passive;
|
|
408
|
+
useEffect(() => {
|
|
409
|
+
const target = resolveTarget();
|
|
410
|
+
if (target === null)
|
|
411
|
+
return undefined;
|
|
412
|
+
const listener = (event) => handlerRef.current(event);
|
|
413
|
+
const listenerOptions = passive === undefined ? { capture } : { capture, passive };
|
|
414
|
+
target.addEventListener(type, listener, listenerOptions);
|
|
415
|
+
return () => target.removeEventListener(type, listener, listenerOptions);
|
|
416
|
+
// resolveTarget is intentionally excluded: it is expected to be an inline closure over a
|
|
417
|
+
// stable target (window, document, a ref) and re-running on its identity would re-bind
|
|
418
|
+
// every render.
|
|
419
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
420
|
+
}, [type, capture, passive]);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Run a requestAnimationFrame loop while `active`, calling `onFrame(deltaSeconds)` each frame.
|
|
424
|
+
* The callback is kept in a ref so re-renders never restart the loop; cleanup cancels the
|
|
425
|
+
* pending frame. For scene-graph work inside a canvas prefer R3F's useFrame — this is for
|
|
426
|
+
* DOM-side animation outside the renderer.
|
|
427
|
+
*
|
|
428
|
+
* @capability raf-loop run a cancellable requestAnimationFrame loop for DOM-side animation with delta seconds — no hand-rolled RAF effects
|
|
429
|
+
*/
|
|
430
|
+
export function useRafLoop(onFrame, active = true) {
|
|
431
|
+
const frameRef = useRef(onFrame);
|
|
432
|
+
frameRef.current = onFrame;
|
|
433
|
+
useEffect(() => {
|
|
434
|
+
if (!active || typeof requestAnimationFrame !== "function")
|
|
435
|
+
return undefined;
|
|
436
|
+
let handle = 0;
|
|
437
|
+
let last = performance.now();
|
|
438
|
+
const step = (now) => {
|
|
439
|
+
frameRef.current(Math.max(0, now - last) / 1000);
|
|
440
|
+
last = now;
|
|
441
|
+
handle = requestAnimationFrame(step);
|
|
442
|
+
};
|
|
443
|
+
handle = requestAnimationFrame(step);
|
|
444
|
+
return () => cancelAnimationFrame(handle);
|
|
445
|
+
}, [active]);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Pin a scrollable element to its bottom whenever `dep` changes (typically a length or the list
|
|
449
|
+
* itself). Attach the returned ref to the scroll container. Owns the log/chat/console
|
|
450
|
+
* scroll-to-bottom effect so panels don't hand-roll it.
|
|
451
|
+
*
|
|
452
|
+
* @capability auto-scroll pin a log/chat/console panel to its newest line as entries arrive — no hand-rolled scrollTop effects
|
|
453
|
+
*/
|
|
454
|
+
export function useAutoScroll(dep) {
|
|
455
|
+
const ref = useRef(null);
|
|
456
|
+
useEffect(() => {
|
|
457
|
+
const node = ref.current;
|
|
458
|
+
if (node !== null)
|
|
459
|
+
node.scrollTop = node.scrollHeight;
|
|
460
|
+
}, [dep]);
|
|
461
|
+
return ref;
|
|
462
|
+
}
|
package/dist/hudLayout.js
CHANGED
|
@@ -22,6 +22,91 @@ const EDIT_BAR_Z = 100000;
|
|
|
22
22
|
const REGION_GAP = 10;
|
|
23
23
|
const COMPACT_HUD_SCALE = 0.85;
|
|
24
24
|
const HUD_ANCHORS = Object.keys(HUD_ANCHOR_FRACTIONS);
|
|
25
|
+
/**
|
|
26
|
+
* Attribute a game puts on any HUD surface it *wants* to bleed past the viewport
|
|
27
|
+
* (a full-bleed backdrop, a deliberately clipped ticker), so the boundary check
|
|
28
|
+
* skips it. Opt-out, on purpose: the default is that shown UI must fit on screen.
|
|
29
|
+
*/
|
|
30
|
+
const ALLOW_OVERFLOW_ATTR = "data-hud-allow-overflow";
|
|
31
|
+
/** Opt-in marker for a custom window mounted deeper than a direct HudCanvas child. */
|
|
32
|
+
const WINDOW_ATTR = "data-hud-window";
|
|
33
|
+
/**
|
|
34
|
+
* Dev-only, prod-safe. The on-screen boundary overlay must never reach players,
|
|
35
|
+
* so anything we cannot positively identify as a dev build resolves to `false`.
|
|
36
|
+
*/
|
|
37
|
+
function isDevBuild() {
|
|
38
|
+
try {
|
|
39
|
+
const meta = import.meta;
|
|
40
|
+
if (meta.env?.DEV === true)
|
|
41
|
+
return true;
|
|
42
|
+
if (meta.env?.PROD === true)
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* import.meta.env unavailable under this bundler */
|
|
47
|
+
}
|
|
48
|
+
const nodeEnv = globalThis.process
|
|
49
|
+
?.env?.NODE_ENV;
|
|
50
|
+
if (typeof nodeEnv === "string")
|
|
51
|
+
return nodeEnv !== "production";
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
/** True unless the element (or an ancestor) is hidden, collapsed, or removed from the a11y tree. */
|
|
55
|
+
function isSurfaceShown(el) {
|
|
56
|
+
if (el.closest('[aria-hidden="true"]') !== null)
|
|
57
|
+
return false;
|
|
58
|
+
const checkable = el;
|
|
59
|
+
if (typeof checkable.checkVisibility === "function") {
|
|
60
|
+
return checkable.checkVisibility({
|
|
61
|
+
contentVisibilityAuto: true,
|
|
62
|
+
opacityProperty: true,
|
|
63
|
+
visibilityProperty: true,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (typeof getComputedStyle !== "function")
|
|
67
|
+
return true;
|
|
68
|
+
const style = getComputedStyle(el);
|
|
69
|
+
return style.visibility !== "hidden" && style.display !== "none" && Number(style.opacity) > 0.01;
|
|
70
|
+
}
|
|
71
|
+
function surfaceId(el) {
|
|
72
|
+
return (el.getAttribute("data-hud-panel") ??
|
|
73
|
+
el.getAttribute(WINDOW_ATTR) ??
|
|
74
|
+
el.getAttribute("data-hud-id") ??
|
|
75
|
+
el.getAttribute("aria-label") ??
|
|
76
|
+
(el.id.length > 0 ? el.id : null) ??
|
|
77
|
+
"<ui>");
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Every HUD surface whose bounds we hold to the viewport: registered panels,
|
|
81
|
+
* any element tagged as a window, and the game's own direct children of the
|
|
82
|
+
* canvas (custom modals dropped straight into the HUD layer). Opted-out and
|
|
83
|
+
* hidden surfaces are dropped here so a closed drawer never reaches the check.
|
|
84
|
+
*/
|
|
85
|
+
function collectBoundarySurfaces(canvas) {
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
const out = [];
|
|
88
|
+
const consider = (el) => {
|
|
89
|
+
if (seen.has(el))
|
|
90
|
+
return;
|
|
91
|
+
seen.add(el);
|
|
92
|
+
if (el.closest(`[${ALLOW_OVERFLOW_ATTR}]`) !== null)
|
|
93
|
+
return;
|
|
94
|
+
if (!isSurfaceShown(el))
|
|
95
|
+
return;
|
|
96
|
+
out.push({ id: surfaceId(el), el });
|
|
97
|
+
};
|
|
98
|
+
for (const el of canvas.querySelectorAll(`[data-hud-panel],[${WINDOW_ATTR}]`))
|
|
99
|
+
consider(el);
|
|
100
|
+
for (const el of Array.from(canvas.children)) {
|
|
101
|
+
if (el.hasAttribute("data-hud-region") ||
|
|
102
|
+
el.hasAttribute("data-hud-edit-bar") ||
|
|
103
|
+
el.hasAttribute("data-hud-overflow-overlay")) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
consider(el);
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
25
110
|
function sizeStyle(panel) {
|
|
26
111
|
if (panel === undefined)
|
|
27
112
|
return {};
|
|
@@ -200,6 +285,7 @@ export function HudCanvas({ layout, editChord, compactScale, showDuring, classNa
|
|
|
200
285
|
const fitEnabled = hudViewport?.fitEnabled === true;
|
|
201
286
|
const [viewport, setViewport] = useState(null);
|
|
202
287
|
const [regions, setRegions] = useState({});
|
|
288
|
+
const [offenders, setOffenders] = useState([]);
|
|
203
289
|
const regionRefs = useMemo(() => {
|
|
204
290
|
const refs = {};
|
|
205
291
|
for (const anchor of HUD_ANCHORS) {
|
|
@@ -285,18 +371,20 @@ export function HudCanvas({ layout, editChord, compactScale, showDuring, classNa
|
|
|
285
371
|
return;
|
|
286
372
|
let frame = 0;
|
|
287
373
|
let lastReport = "";
|
|
374
|
+
const dev = isDevBuild();
|
|
288
375
|
const check = () => {
|
|
289
376
|
frame = 0;
|
|
290
377
|
const hostRect = host.getBoundingClientRect();
|
|
291
378
|
if (hostRect.width <= 0 || hostRect.height <= 0)
|
|
292
379
|
return;
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
380
|
+
const viewportSize = { width: hostRect.width, height: hostRect.height };
|
|
381
|
+
const surfaces = [];
|
|
382
|
+
for (const surface of collectBoundarySurfaces(canvas)) {
|
|
383
|
+
const rect = surface.el.getBoundingClientRect();
|
|
296
384
|
if (rect.width === 0 && rect.height === 0)
|
|
297
385
|
continue;
|
|
298
|
-
|
|
299
|
-
id:
|
|
386
|
+
surfaces.push({
|
|
387
|
+
id: surface.id,
|
|
300
388
|
rect: {
|
|
301
389
|
x: rect.left - hostRect.left,
|
|
302
390
|
y: rect.top - hostRect.top,
|
|
@@ -305,8 +393,10 @@ export function HudCanvas({ layout, editChord, compactScale, showDuring, classNa
|
|
|
305
393
|
},
|
|
306
394
|
});
|
|
307
395
|
}
|
|
308
|
-
|
|
309
|
-
|
|
396
|
+
// overflowingPanels reports only surfaces that are partly on screen *and*
|
|
397
|
+
// cross an edge — a closed/parked/off-screen menu is never an offender.
|
|
398
|
+
const escaped = overflowingPanels(surfaces, viewportSize);
|
|
399
|
+
const report = escaped.length === 0 ? "" : JSON.stringify(escaped);
|
|
310
400
|
if (report === lastReport)
|
|
311
401
|
return;
|
|
312
402
|
lastReport = report;
|
|
@@ -315,7 +405,13 @@ export function HudCanvas({ layout, editChord, compactScale, showDuring, classNa
|
|
|
315
405
|
}
|
|
316
406
|
else {
|
|
317
407
|
canvas.setAttribute("data-hud-overflow", report);
|
|
318
|
-
console.warn(`[jgengine] HUD
|
|
408
|
+
console.warn(`[jgengine] HUD UI escapes the ${Math.round(hostRect.width)}x${Math.round(hostRect.height)} viewport: ${report}`);
|
|
409
|
+
}
|
|
410
|
+
if (dev) {
|
|
411
|
+
setOffenders(escaped.map((overflow) => {
|
|
412
|
+
const rect = surfaces.find((s) => s.id === overflow.id)?.rect;
|
|
413
|
+
return { ...overflow, x: rect?.x ?? 0, y: rect?.y ?? 0, width: rect?.width ?? 0, height: rect?.height ?? 0 };
|
|
414
|
+
}));
|
|
319
415
|
}
|
|
320
416
|
};
|
|
321
417
|
const schedule = () => {
|
|
@@ -365,7 +461,40 @@ export function HudCanvas({ layout, editChord, compactScale, showDuring, classNa
|
|
|
365
461
|
pointerEvents: "none",
|
|
366
462
|
zIndex: 1,
|
|
367
463
|
...REGION_LAYOUT[anchor],
|
|
368
|
-
} }, anchor))), children,
|
|
464
|
+
} }, anchor))), children, offenders.length > 0 ? (_jsx("div", { "data-hud-overflow-overlay": "", style: { position: "absolute", inset: 0, pointerEvents: "none", zIndex: EDIT_BAR_Z - 1 }, children: offenders.map((o) => (_jsx("div", { style: {
|
|
465
|
+
position: "absolute",
|
|
466
|
+
left: o.x / scale,
|
|
467
|
+
top: o.y / scale,
|
|
468
|
+
width: o.width / scale,
|
|
469
|
+
height: o.height / scale,
|
|
470
|
+
border: "2px solid rgba(255, 64, 64, 0.95)",
|
|
471
|
+
boxShadow: "0 0 0 2px rgba(255, 64, 64, 0.3)",
|
|
472
|
+
boxSizing: "border-box",
|
|
473
|
+
}, children: _jsxs("span", { style: {
|
|
474
|
+
position: "absolute",
|
|
475
|
+
left: 0,
|
|
476
|
+
bottom: "100%",
|
|
477
|
+
marginBottom: 2,
|
|
478
|
+
padding: "2px 6px",
|
|
479
|
+
borderRadius: 4,
|
|
480
|
+
background: "rgba(180, 20, 20, 0.95)",
|
|
481
|
+
color: "#fff",
|
|
482
|
+
font: "600 10px/1.4 system-ui, sans-serif",
|
|
483
|
+
whiteSpace: "nowrap",
|
|
484
|
+
}, children: ["\u26A0 ", o.id, " escapes viewport (", overflowEdges(o), ")"] }) }, o.id))) })) : null, editing ? _jsx(HudEditBar, { layout: layout }) : null] }) }));
|
|
485
|
+
}
|
|
486
|
+
/** Human-readable edge list for the dev boundary badge, e.g. `right +260 · bottom +56`. */
|
|
487
|
+
function overflowEdges(overflow) {
|
|
488
|
+
const parts = [];
|
|
489
|
+
if (overflow.left > 0)
|
|
490
|
+
parts.push(`left +${overflow.left}`);
|
|
491
|
+
if (overflow.top > 0)
|
|
492
|
+
parts.push(`top +${overflow.top}`);
|
|
493
|
+
if (overflow.right > 0)
|
|
494
|
+
parts.push(`right +${overflow.right}`);
|
|
495
|
+
if (overflow.bottom > 0)
|
|
496
|
+
parts.push(`bottom +${overflow.bottom}`);
|
|
497
|
+
return parts.length > 0 ? parts.join(" · ") : "edge";
|
|
369
498
|
}
|
|
370
499
|
function chipAlignment(anchor) {
|
|
371
500
|
if (anchor.endsWith("right"))
|
package/dist/hudTheme.d.ts
CHANGED
|
@@ -1,27 +1,73 @@
|
|
|
1
1
|
import { type CSSProperties } from "react";
|
|
2
2
|
/**
|
|
3
|
-
* `HudTheme` (#1034): one token object that restyles shared token-driven building
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* setting a **game-authored** theme on any HUD ancestor
|
|
3
|
+
* `HudTheme` (#1034, #1573): one token object that restyles every shared token-driven building
|
|
4
|
+
* block at once — the atomic bars (#1033), frames, slots, the minimap ring, and the chrome the
|
|
5
|
+
* registry blocks read (surfaces, edges, text, status colors, fonts, rarity). `hudThemeVars(theme)`
|
|
6
|
+
* emits the whole `--jg-*` vocabulary, so setting a **game-authored** theme on any HUD ancestor
|
|
7
|
+
* re-skins `packages/react` primitives and `registry/jgengine` components together. Purely CSS-token
|
|
7
8
|
* driven (no image assets). Tokens are not a finished UI: every game still owns composition, skin,
|
|
8
9
|
* and art direction (see AGENTS.md). Built-in presets are demo/scaffold starting points only.
|
|
9
10
|
*
|
|
10
|
-
* @capability hud-theme game-authored token object
|
|
11
|
+
* @capability hud-theme game-authored token object covering bars, frames, slots, chrome, text, status colors, fonts, and rarity
|
|
11
12
|
*/
|
|
12
|
-
/** Per-readout fill colors plus a shared accent/glow. */
|
|
13
|
+
/** Per-readout fill colors plus a shared accent/glow. Each `*Deep` is the shaded companion fill. */
|
|
13
14
|
export interface HudThemePalette {
|
|
14
15
|
health: string;
|
|
15
16
|
healthLow: string;
|
|
17
|
+
healthDeep: string;
|
|
16
18
|
mana: string;
|
|
19
|
+
manaDeep: string;
|
|
17
20
|
stamina: string;
|
|
21
|
+
staminaDeep: string;
|
|
18
22
|
shield: string;
|
|
23
|
+
shieldDeep: string;
|
|
19
24
|
xp: string;
|
|
25
|
+
xpDeep: string;
|
|
20
26
|
soul: string;
|
|
21
27
|
ammo: string;
|
|
22
28
|
boss: string;
|
|
23
29
|
/** Accent used for glow/keycap/active states. */
|
|
24
30
|
accent: string;
|
|
31
|
+
/** Shaded accent for gradients and pressed states. */
|
|
32
|
+
accentDeep: string;
|
|
33
|
+
/** Translucent accent for outer glows. */
|
|
34
|
+
accentGlow: string;
|
|
35
|
+
/** Soft accent wash for large background gradients. */
|
|
36
|
+
accentDim: string;
|
|
37
|
+
}
|
|
38
|
+
/** Chrome surfaces, edges, and text — what panels, overlays, and menus are built out of. */
|
|
39
|
+
export interface HudThemeSurface {
|
|
40
|
+
surface: string;
|
|
41
|
+
surfaceDeep: string;
|
|
42
|
+
edge: string;
|
|
43
|
+
edgeBright: string;
|
|
44
|
+
text: string;
|
|
45
|
+
textDim: string;
|
|
46
|
+
/** Full-screen dim behind modals and overlays. */
|
|
47
|
+
backdrop: string;
|
|
48
|
+
}
|
|
49
|
+
/** Semantic status colors — outcome, threat, and relationship coloring. */
|
|
50
|
+
export interface HudThemeStatus {
|
|
51
|
+
danger: string;
|
|
52
|
+
warning: string;
|
|
53
|
+
success: string;
|
|
54
|
+
hostile: string;
|
|
55
|
+
friendly: string;
|
|
56
|
+
neutral: string;
|
|
57
|
+
}
|
|
58
|
+
/** Typography roles. Any CSS `font-family` list. */
|
|
59
|
+
export interface HudThemeFont {
|
|
60
|
+
display: string;
|
|
61
|
+
numeric: string;
|
|
62
|
+
body: string;
|
|
63
|
+
}
|
|
64
|
+
/** Item-rarity coloring shared by loot cards, tooltips, and inventory slots. */
|
|
65
|
+
export interface HudThemeRarity {
|
|
66
|
+
common: string;
|
|
67
|
+
uncommon: string;
|
|
68
|
+
rare: string;
|
|
69
|
+
epic: string;
|
|
70
|
+
legendary: string;
|
|
25
71
|
}
|
|
26
72
|
/** Frame material + shape tokens (panels, `HudFrame variation="themed"`). */
|
|
27
73
|
export interface HudThemeFrame {
|
|
@@ -48,9 +94,13 @@ export interface HudThemeSlot {
|
|
|
48
94
|
border: string;
|
|
49
95
|
radius: string;
|
|
50
96
|
}
|
|
51
|
-
/** A full HUD theme. */
|
|
97
|
+
/** A full HUD theme. Author one with {@link hudTheme} or {@link deriveHudTheme}. */
|
|
52
98
|
export interface HudTheme {
|
|
53
99
|
palette: HudThemePalette;
|
|
100
|
+
surface: HudThemeSurface;
|
|
101
|
+
status: HudThemeStatus;
|
|
102
|
+
font: HudThemeFont;
|
|
103
|
+
rarity: HudThemeRarity;
|
|
54
104
|
frame: HudThemeFrame;
|
|
55
105
|
bar: HudThemeBar;
|
|
56
106
|
slot: HudThemeSlot;
|
|
@@ -61,6 +111,26 @@ export interface HudTheme {
|
|
|
61
111
|
export declare function hudThemeVars(theme: HudTheme): CSSProperties;
|
|
62
112
|
/** Default theme tokens for previews/scaffold — not a finished game look; author a custom theme. */
|
|
63
113
|
export declare const defaultHudTheme: HudTheme;
|
|
114
|
+
/** Per-group partial overrides accepted by {@link hudTheme}. */
|
|
115
|
+
export interface HudThemeOverrides {
|
|
116
|
+
palette?: Partial<HudThemePalette>;
|
|
117
|
+
surface?: Partial<HudThemeSurface>;
|
|
118
|
+
status?: Partial<HudThemeStatus>;
|
|
119
|
+
font?: Partial<HudThemeFont>;
|
|
120
|
+
rarity?: Partial<HudThemeRarity>;
|
|
121
|
+
frame?: Partial<HudThemeFrame>;
|
|
122
|
+
bar?: Partial<HudThemeBar>;
|
|
123
|
+
slot?: Partial<HudThemeSlot>;
|
|
124
|
+
ring?: string;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Builds a full {@link HudTheme} from per-group partials, filling the rest from
|
|
128
|
+
* {@link defaultHudTheme}. Reach for this when a game wants to name exact values; use
|
|
129
|
+
* {@link deriveHudTheme} when a seed color pair is enough.
|
|
130
|
+
*
|
|
131
|
+
* @capability hud-theme-author fill a full HudTheme from per-group partial overrides
|
|
132
|
+
*/
|
|
133
|
+
export declare function hudTheme(overrides: HudThemeOverrides): HudTheme;
|
|
64
134
|
/** Built-in theme presets for demos/scaffold only — not product identity; games author their own theme. */
|
|
65
135
|
export declare const HUD_THEME_PRESETS: {
|
|
66
136
|
"arcane-stone": HudTheme;
|
|
@@ -69,8 +139,35 @@ export declare const HUD_THEME_PRESETS: {
|
|
|
69
139
|
"sleek-hex": HudTheme;
|
|
70
140
|
"elemental-circle": HudTheme;
|
|
71
141
|
"sandbox-slot": HudTheme;
|
|
142
|
+
ember: HudTheme;
|
|
143
|
+
synthwave: HudTheme;
|
|
144
|
+
fieldkit: HudTheme;
|
|
72
145
|
};
|
|
73
146
|
/** Built-in demo/scaffold theme preset name — not a game identity. */
|
|
74
147
|
export type HudThemePreset = keyof typeof HUD_THEME_PRESETS;
|
|
148
|
+
/** Seed colors for {@link deriveHudTheme}. Only `accent` and `surface` are required. */
|
|
149
|
+
export interface HudThemeSeed {
|
|
150
|
+
accent: string;
|
|
151
|
+
surface: string;
|
|
152
|
+
text?: string;
|
|
153
|
+
health?: string;
|
|
154
|
+
mana?: string;
|
|
155
|
+
stamina?: string;
|
|
156
|
+
xp?: string;
|
|
157
|
+
shield?: string;
|
|
158
|
+
danger?: string;
|
|
159
|
+
warning?: string;
|
|
160
|
+
friendly?: string;
|
|
161
|
+
fontDisplay?: string;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Derives a coherent full theme from an accent/surface pair — shades, edges, dim text, frames,
|
|
165
|
+
* bar troughs, and slots all fall out of the seed, so a game gets one consistent look across the
|
|
166
|
+
* `packages/react` primitives and the `registry/jgengine` blocks without naming 50 tokens.
|
|
167
|
+
* Hex seeds only (`#rgb` or `#rrggbb`).
|
|
168
|
+
*
|
|
169
|
+
* @capability hud-theme-derive build a full HudTheme from an accent + surface seed pair
|
|
170
|
+
*/
|
|
171
|
+
export declare function deriveHudTheme(seed: HudThemeSeed): HudTheme;
|
|
75
172
|
/** Resolves a preset name (or a full theme) to a `HudTheme`; falls back to the default theme. */
|
|
76
173
|
export declare function resolveHudTheme(theme?: HudThemePreset | HudTheme): HudTheme;
|