@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.
Files changed (101) hide show
  1. package/CHANGELOG.md +529 -1
  2. package/dist/accessibility.d.ts +39 -0
  3. package/dist/accessibility.js +61 -0
  4. package/dist/achievements.d.ts +50 -0
  5. package/dist/achievements.js +94 -0
  6. package/dist/cameraShake.d.ts +52 -0
  7. package/dist/cameraShake.js +53 -0
  8. package/dist/coachMarks.d.ts +72 -0
  9. package/dist/coachMarks.js +136 -0
  10. package/dist/codex.d.ts +25 -0
  11. package/dist/codex.js +62 -0
  12. package/dist/comboMeter.d.ts +63 -0
  13. package/dist/comboMeter.js +93 -0
  14. package/dist/components.js +4 -3
  15. package/dist/creditsScreen.d.ts +19 -0
  16. package/dist/creditsScreen.js +31 -0
  17. package/dist/cutscene.d.ts +66 -0
  18. package/dist/cutscene.js +132 -0
  19. package/dist/damageDirection.d.ts +51 -0
  20. package/dist/damageDirection.js +103 -0
  21. package/dist/dialogueView.d.ts +69 -0
  22. package/dist/dialogueView.js +111 -0
  23. package/dist/entityFrames.d.ts +114 -0
  24. package/dist/entityFrames.js +55 -0
  25. package/dist/fastTravel.d.ts +36 -0
  26. package/dist/fastTravel.js +103 -0
  27. package/dist/floatingText.d.ts +84 -0
  28. package/dist/floatingText.js +61 -0
  29. package/dist/gameViewport.js +8 -12
  30. package/dist/hooks.d.ts +42 -0
  31. package/dist/hooks.js +85 -0
  32. package/dist/hudLayout.js +138 -9
  33. package/dist/hudTheme.d.ts +104 -7
  34. package/dist/hudTheme.js +300 -2
  35. package/dist/i18n.d.ts +29 -0
  36. package/dist/i18n.js +44 -0
  37. package/dist/index.d.ts +34 -0
  38. package/dist/index.js +34 -0
  39. package/dist/interactionPrompt.d.ts +80 -0
  40. package/dist/interactionPrompt.js +112 -0
  41. package/dist/keybindingMenu.d.ts +59 -0
  42. package/dist/keybindingMenu.js +146 -0
  43. package/dist/killFeed.d.ts +63 -0
  44. package/dist/killFeed.js +88 -0
  45. package/dist/map.d.ts +123 -2
  46. package/dist/map.js +239 -36
  47. package/dist/menuRouter.d.ts +51 -0
  48. package/dist/menuRouter.js +49 -0
  49. package/dist/modals.d.ts +131 -0
  50. package/dist/modals.js +168 -0
  51. package/dist/notifications.d.ts +39 -0
  52. package/dist/notifications.js +111 -0
  53. package/dist/objectiveBanner.d.ts +62 -0
  54. package/dist/objectiveBanner.js +129 -0
  55. package/dist/panels.d.ts +10 -2
  56. package/dist/panels.js +9 -3
  57. package/dist/photoMode.d.ts +21 -0
  58. package/dist/photoMode.js +39 -0
  59. package/dist/previewFixtures.d.ts +30 -0
  60. package/dist/previewFixtures.js +46 -0
  61. package/dist/provider.d.ts +15 -0
  62. package/dist/provider.js +17 -1
  63. package/dist/questTracker.d.ts +24 -0
  64. package/dist/questTracker.js +31 -0
  65. package/dist/quickMenu.d.ts +39 -0
  66. package/dist/quickMenu.js +79 -0
  67. package/dist/radialMenu.d.ts +50 -0
  68. package/dist/radialMenu.js +69 -0
  69. package/dist/saveSlots.d.ts +81 -0
  70. package/dist/saveSlots.js +138 -0
  71. package/dist/scoreboard.d.ts +67 -0
  72. package/dist/scoreboard.js +71 -0
  73. package/dist/settings.d.ts +9 -3
  74. package/dist/settings.js +10 -4
  75. package/dist/shopGrid.d.ts +70 -0
  76. package/dist/shopGrid.js +129 -0
  77. package/dist/statusEffectBar.d.ts +52 -0
  78. package/dist/statusEffectBar.js +57 -0
  79. package/dist/streetGeometryPreview.d.ts +7 -0
  80. package/dist/streetGeometryPreview.js +119 -0
  81. package/dist/talentTree.d.ts +60 -0
  82. package/dist/talentTree.js +217 -0
  83. package/dist/timerReadout.d.ts +90 -0
  84. package/dist/timerReadout.js +111 -0
  85. package/dist/useDebouncedCommit.d.ts +54 -0
  86. package/dist/useDebouncedCommit.js +81 -0
  87. package/dist/waveHud.d.ts +49 -0
  88. package/dist/waveHud.js +53 -0
  89. package/dist/waypointMarkers.d.ts +80 -0
  90. package/dist/waypointMarkers.js +116 -0
  91. package/package.json +3 -3
  92. package/skills/ce-handoff/SKILL.md +149 -0
  93. package/skills/jgengine/SKILL.md +7 -3
  94. package/skills/jgengine/api.md +17 -7
  95. package/skills/jgengine/capabilities.md +12 -0
  96. package/skills/jgengine/recipes/minimal-game.md +7 -4
  97. package/skills/jgengine-ui/SKILL.md +8 -3
  98. package/skills/jgengine-ui/api.md +732 -23
  99. package/skills/jgengine-ui/capabilities.md +371 -2
  100. package/skills/jgengine-ui/recipes/overhead-entity-frames.md +136 -0
  101. package/skills/jgengine-ui/reference.md +7 -4
@@ -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
- useEffect(() => {
223
- const root = rootRef.current;
224
- if (root === null)
225
- return;
226
- if (forbidden.length === 0) {
227
- root.removeAttribute("data-jg-layout-collision");
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
- const report = JSON.stringify(forbidden);
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 panels = [];
294
- for (const el of canvas.querySelectorAll("[data-hud-panel]")) {
295
- const rect = el.getBoundingClientRect();
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
- panels.push({
299
- id: el.getAttribute("data-hud-panel") ?? "?",
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
- const offenders = overflowingPanels(panels, { width: hostRect.width, height: hostRect.height });
309
- const report = offenders.length === 0 ? "" : JSON.stringify(offenders);
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 panels overflow the ${Math.round(hostRect.width)}x${Math.round(hostRect.height)} viewport: ${report}`);
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, editing ? _jsx(HudEditBar, { layout: layout }) : null] }) }));
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"))
@@ -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 blocks at once.
4
- * It is the superset of the atomic-bar tokens (#1033) — `hudThemeVars(theme)` emits the same
5
- * `--jg-*` custom properties the bars already read, plus frame / slot / minimap-ring tokens — so
6
- * setting a **game-authored** theme on any HUD ancestor re-skins those primitives. Purely CSS-token
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 for bars, frames, slots, and minimap ring
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;