@jgengine/react 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +39 -2
  2. package/LICENSE +203 -661
  3. package/README.md +1 -1
  4. package/dist/actionHud.d.ts +174 -0
  5. package/dist/actionHud.js +294 -0
  6. package/dist/bars.d.ts +103 -0
  7. package/dist/bars.js +180 -0
  8. package/dist/barsPreview.d.ts +4 -0
  9. package/dist/barsPreview.js +47 -0
  10. package/dist/cards.d.ts +59 -0
  11. package/dist/cards.js +116 -0
  12. package/dist/chat.d.ts +3 -1
  13. package/dist/chat.js +2 -2
  14. package/dist/components.d.ts +28 -1
  15. package/dist/components.js +36 -1
  16. package/dist/display.d.ts +1 -0
  17. package/dist/display.js +1 -0
  18. package/dist/engineStore.d.ts +3 -0
  19. package/dist/engineStore.js +3 -0
  20. package/dist/fogOverlay.d.ts +3 -0
  21. package/dist/fogOverlay.js +3 -0
  22. package/dist/gameViewport.js +16 -0
  23. package/dist/hooks.d.ts +40 -0
  24. package/dist/hooks.js +69 -1
  25. package/dist/hud.d.ts +104 -0
  26. package/dist/hud.js +155 -0
  27. package/dist/hudFrame.d.ts +47 -0
  28. package/dist/hudFrame.js +80 -0
  29. package/dist/hudLayout.d.ts +31 -2
  30. package/dist/hudLayout.js +127 -10
  31. package/dist/hudLayoutPersist.d.ts +23 -0
  32. package/dist/hudLayoutPersist.js +19 -0
  33. package/dist/hudTheme.d.ts +76 -0
  34. package/dist/hudTheme.js +156 -0
  35. package/dist/hudThemePreview.d.ts +4 -0
  36. package/dist/hudThemePreview.js +37 -0
  37. package/dist/iconTreatment.d.ts +50 -0
  38. package/dist/iconTreatment.js +84 -0
  39. package/dist/iconsPreview.d.ts +4 -0
  40. package/dist/iconsPreview.js +29 -0
  41. package/dist/index.d.ts +17 -0
  42. package/dist/index.js +17 -0
  43. package/dist/keyHint.d.ts +30 -0
  44. package/dist/keyHint.js +31 -0
  45. package/dist/map.d.ts +141 -11
  46. package/dist/map.js +138 -16
  47. package/dist/save.d.ts +25 -0
  48. package/dist/save.js +37 -0
  49. package/dist/selectSnapshot.d.ts +2 -0
  50. package/dist/selectSnapshot.js +2 -0
  51. package/dist/selectionHud.d.ts +139 -0
  52. package/dist/selectionHud.js +188 -0
  53. package/dist/skillCheckPaint.d.ts +2 -0
  54. package/dist/skillCheckPaint.js +2 -0
  55. package/dist/slots.d.ts +44 -0
  56. package/dist/slots.js +37 -0
  57. package/dist/startScreen.d.ts +62 -0
  58. package/dist/startScreen.js +62 -0
  59. package/dist/store.d.ts +24 -0
  60. package/dist/store.js +9 -0
  61. package/package.json +4 -4
  62. package/skills/jgengine/SKILL.md +54 -0
  63. package/skills/jgengine/api.md +527 -0
  64. package/skills/jgengine/capabilities.md +131 -0
  65. package/skills/jgengine/recipes/minimal-game.md +88 -0
  66. package/skills/jgengine-ui/SKILL.md +41 -0
  67. package/skills/jgengine-ui/api.md +2007 -0
  68. package/skills/jgengine-ui/capabilities.md +211 -0
  69. package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
  70. package/skills/jgengine-ui/reference.md +513 -0
  71. package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
  72. package/llms.txt +0 -1724
@@ -1,5 +1,6 @@
1
1
  import { type CSSProperties, type ReactNode } from "react";
2
2
  import { type HudAnchor, type HudLayoutStore } from "@jgengine/core/ui/hudLayout";
3
+ import { type EditorUiDocument } from "@jgengine/core/ui/hudDocument";
3
4
  import { type HudPriority, type MobileHudBehavior } from "@jgengine/core/ui/gameLayout";
4
5
  import type { GamePhase } from "@jgengine/core/game/gamePhase";
5
6
  /** Whether a HUD element opted into `showDuring` is visible in the current phase; `undefined` = always visible (default). */
@@ -14,10 +15,25 @@ export interface HudEditChord {
14
15
  hold: string;
15
16
  press: string;
16
17
  }
18
+ /**
19
+ * Layout state for `HudCanvas` — panel placements, edit-mode drag/resize, and per-game persistence.
20
+ *
21
+ * @capability hud-layout persistent, live-editable HUD panel layout state for `HudCanvas`
22
+ */
17
23
  export declare function useHudLayout(options?: {
18
24
  storageKey?: string;
19
25
  snap?: number;
20
26
  locked?: boolean;
27
+ /**
28
+ * Scene-document `ui` section — source of truth for panel placement/size.
29
+ * When provided, hydrates the layout store (and wins over legacy localStorage).
30
+ */
31
+ documentUi?: EditorUiDocument;
32
+ /**
33
+ * When true (default), canvas moves/resizes invoke the injected {@link HudLayoutPersist}
34
+ * port (`HudLayoutPersistProvider` / `onPanelCommit`). No-op when no host has provided one.
35
+ */
36
+ documentPatches?: boolean;
21
37
  }): HudLayoutStore;
22
38
  /**
23
39
  * Full-viewport HUD surface. Panels declared with `HudPanel` flow into nine
@@ -25,6 +41,8 @@ export declare function useHudLayout(options?: {
25
41
  * offsets, no manual clearance for sibling panels, the touch-control dock
26
42
  * (`--jg-hud-dock-clearance`), or device safe areas. On compact displays the
27
43
  * whole surface scales down and each panel applies its `compact` behavior.
44
+ *
45
+ * @capability hud-canvas the HUD root surface — design-resolution scaling plus live panel-placement editing (F2+C)
28
46
  */
29
47
  export declare function HudCanvas({ layout, editChord, compactScale, showDuring, className, style, children, }: {
30
48
  layout: HudLayoutStore;
@@ -43,8 +61,10 @@ export declare function HudCanvas({ layout, editChord, compactScale, showDuring,
43
61
  * pointers panels stay draggable through the edit chord; a dragged panel
44
62
  * leaves the flow and keeps its custom placement. On compact displays custom
45
63
  * placements are ignored and the `compact` behavior applies.
64
+ *
65
+ * @capability hud-panel anchored HUD slot — drop any widget in a named, player-movable panel
46
66
  */
47
- export declare function HudPanel({ id, anchor, order, compact: compactMode, chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region, className, style, children, }: {
67
+ export declare function HudPanel({ id, anchor, order, compact: compactMode, chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region, width, height, type, className, style, children, }: {
48
68
  id: string;
49
69
  anchor?: HudAnchor;
50
70
  /** Stack position within the region, ascending outward from the screen edge. Default 0. */
@@ -55,7 +75,10 @@ export declare function HudPanel({ id, anchor, order, compact: compactMode, chip
55
75
  chip?: string;
56
76
  /** `false` lets pointer events pass through to the game (read-only panels). Default true. */
57
77
  interactive?: boolean;
58
- /** Legacy pixel inset from the anchor; only used as the reset placement for dragged panels. */
78
+ /**
79
+ * Fallback pixel inset from the anchor when the scene document has no `ui.panels[id]`.
80
+ * Document layout is the source of truth once authored; TSX props are fallback-only.
81
+ */
59
82
  inset?: {
60
83
  x: number;
61
84
  y: number;
@@ -73,6 +96,12 @@ export declare function HudPanel({ id, anchor, order, compact: compactMode, chip
73
96
  collisionGroup?: string;
74
97
  /** Register this panel as a collision-tracked layout region. Default true. */
75
98
  region?: boolean;
99
+ /** Fallback width when the document has not authored a size. */
100
+ width?: number;
101
+ /** Fallback height when the document has not authored a size. */
102
+ height?: number;
103
+ /** Panel type id ({@link registerHudPanelType}) — drives resize axes + ParamSchema. */
104
+ type?: string;
76
105
  className?: string;
77
106
  style?: CSSProperties;
78
107
  children?: ReactNode;
package/dist/hudLayout.js CHANGED
@@ -1,13 +1,15 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createContext, useContext, useEffect, useMemo, useRef, useState, } from "react";
3
3
  import { createPortal } from "react-dom";
4
- import { HUD_ANCHOR_FRACTIONS, anchoredPlacement, createHudLayout, isPanelDraggable, } from "@jgengine/core/ui/hudLayout";
4
+ import { HUD_ANCHOR_FRACTIONS, anchoredPlacement, createHudLayout, isPanelDraggable, registerActiveHudLayout, } from "@jgengine/core/ui/hudLayout";
5
+ import { resizePanelSize, resolvePanelResize, } from "@jgengine/core/ui/hudDocument";
5
6
  import { hudScaleForViewport, overflowingPanels, resolveHudFit } from "@jgengine/core/ui/hudScale";
6
7
  import { isMobileMode, } from "@jgengine/core/ui/gameLayout";
7
8
  import { useDisplayProfile } from "./display.js";
8
9
  import { useEngineState } from "./engineStore.js";
9
10
  import { useGameLayoutMode, useRegisterLayoutRegion } from "./gameViewport.js";
10
11
  import { useOptionalGamePhase } from "./hooks.js";
12
+ import { useHudLayoutPersist } from "./hudLayoutPersist.js";
11
13
  import { useHudViewport } from "./hudViewport.js";
12
14
  /** Whether a HUD element opted into `showDuring` is visible in the current phase; `undefined` = always visible (default). */
13
15
  export function hudVisibleInPhase(showDuring, phase) {
@@ -20,18 +22,74 @@ const EDIT_BAR_Z = 100000;
20
22
  const REGION_GAP = 10;
21
23
  const COMPACT_HUD_SCALE = 0.85;
22
24
  const HUD_ANCHORS = Object.keys(HUD_ANCHOR_FRACTIONS);
25
+ function sizeStyle(panel) {
26
+ if (panel === undefined)
27
+ return {};
28
+ return {
29
+ ...(panel.width === undefined ? {} : { width: panel.width }),
30
+ ...(panel.height === undefined ? {} : { height: panel.height }),
31
+ };
32
+ }
33
+ function ResizeHandles({ axes, onResizeStart, }) {
34
+ if (axes === "none")
35
+ return null;
36
+ const handle = (edge, cursor, style) => (_jsx("div", { "data-hud-resize": edge, onPointerDown: (event) => {
37
+ event.stopPropagation();
38
+ onResizeStart(edge, event);
39
+ }, style: {
40
+ position: "absolute",
41
+ zIndex: 3,
42
+ pointerEvents: "auto",
43
+ background: "rgba(255, 255, 255, 0.85)",
44
+ cursor,
45
+ ...style,
46
+ } }, edge));
47
+ return (_jsxs(_Fragment, { children: [axes === "x" || axes === "both"
48
+ ? handle("e", "ew-resize", { top: 4, bottom: 4, right: -3, width: 6 })
49
+ : null, axes === "y" || axes === "both"
50
+ ? handle("s", "ns-resize", { left: 4, right: 4, bottom: -3, height: 6 })
51
+ : null, axes === "both" ? handle("se", "nwse-resize", { right: -4, bottom: -4, width: 10, height: 10 }) : null] }));
52
+ }
53
+ /**
54
+ * Layout state for `HudCanvas` — panel placements, edit-mode drag/resize, and per-game persistence.
55
+ *
56
+ * @capability hud-layout persistent, live-editable HUD panel layout state for `HudCanvas`
57
+ */
23
58
  export function useHudLayout(options) {
24
59
  const storageKey = options?.storageKey;
25
60
  const snap = options?.snap;
26
61
  const locked = options?.locked;
62
+ const documentUi = options?.documentUi;
63
+ const documentPatches = options?.documentPatches !== false;
64
+ const onPanelCommit = useHudLayoutPersist();
65
+ const onPanelCommitRef = useRef(onPanelCommit);
66
+ onPanelCommitRef.current = onPanelCommit;
27
67
  const layout = useMemo(() => {
28
- const store = createHudLayout({ snap, locked });
29
- if (storageKey !== undefined && typeof localStorage !== "undefined") {
68
+ const store = createHudLayout({
69
+ snap,
70
+ locked,
71
+ onDocumentPatch: documentPatches
72
+ ? (id, panel) => {
73
+ onPanelCommitRef.current?.(id, panel);
74
+ }
75
+ : undefined,
76
+ });
77
+ if (documentUi !== undefined) {
78
+ store.applyDocumentUi(documentUi);
79
+ }
80
+ else if (storageKey !== undefined && typeof localStorage !== "undefined") {
30
81
  store.hydrate(localStorage.getItem(STORAGE_PREFIX + storageKey));
31
82
  }
32
83
  return store;
33
- }, [storageKey, snap, locked]);
84
+ }, [storageKey, snap, locked, documentPatches]);
34
85
  useEffect(() => {
86
+ if (documentUi !== undefined) {
87
+ layout.applyDocumentUi(documentUi);
88
+ }
89
+ }, [layout, documentUi]);
90
+ useEffect(() => {
91
+ if (documentUi !== undefined)
92
+ return;
35
93
  if (storageKey === undefined || typeof localStorage === "undefined")
36
94
  return;
37
95
  let timer = null;
@@ -50,7 +108,7 @@ export function useHudLayout(options) {
50
108
  localStorage.setItem(STORAGE_PREFIX + storageKey, layout.serialize());
51
109
  }
52
110
  };
53
- }, [layout, storageKey]);
111
+ }, [layout, storageKey, documentUi]);
54
112
  return layout;
55
113
  }
56
114
  const HudCanvasContext = createContext(null);
@@ -131,6 +189,8 @@ function edgePad(envInset, extra, basePx, scale) {
131
189
  * offsets, no manual clearance for sibling panels, the touch-control dock
132
190
  * (`--jg-hud-dock-clearance`), or device safe areas. On compact displays the
133
191
  * whole surface scales down and each panel applies its `compact` behavior.
192
+ *
193
+ * @capability hud-canvas the HUD root surface — design-resolution scaling plus live panel-placement editing (F2+C)
134
194
  */
135
195
  export function HudCanvas({ layout, editChord, compactScale, showDuring, className, style, children, }) {
136
196
  const canvasRef = useRef(null);
@@ -160,6 +220,7 @@ export function HudCanvas({ layout, editChord, compactScale, showDuring, classNa
160
220
  }, []);
161
221
  const value = useMemo(() => ({ layout, canvasRef, regions, compact }), [layout, regions, compact]);
162
222
  const editing = useEngineState(layout).editing;
223
+ useEffect(() => registerActiveHudLayout(layout), [layout]);
163
224
  const chordEnabled = editChord !== false;
164
225
  const hold = editChord === false ? undefined : (editChord?.hold ?? "F2");
165
226
  const press = editChord === false ? undefined : (editChord?.press ?? "KeyC");
@@ -343,8 +404,10 @@ function HudChip({ label, anchor, children, }) {
343
404
  * pointers panels stay draggable through the edit chord; a dragged panel
344
405
  * leaves the flow and keeps its custom placement. On compact displays custom
345
406
  * placements are ignored and the `compact` behavior applies.
407
+ *
408
+ * @capability hud-panel anchored HUD slot — drop any widget in a named, player-movable panel
346
409
  */
347
- export function HudPanel({ id, anchor = "top-left", order, compact: compactMode = "keep", chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, className, style, children, }) {
410
+ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode = "keep", chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, width, height, type, className, style, children, }) {
348
411
  const ctx = useContext(HudCanvasContext);
349
412
  const phase = useOptionalGamePhase();
350
413
  const layoutMode = useGameLayoutMode();
@@ -355,12 +418,18 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
355
418
  const registeredOnRef = useRef(null);
356
419
  if (registeredOnRef.current !== layout) {
357
420
  registeredOnRef.current = layout;
358
- layout.register(id, anchoredPlacement(anchor, inset ?? { x: 16, y: 16 }));
421
+ layout.register(id, anchoredPlacement(anchor, inset ?? { x: 16, y: 16 }), {
422
+ width,
423
+ height,
424
+ type,
425
+ });
359
426
  }
360
427
  const layoutState = useEngineState(layout);
361
428
  const panel = layoutState.panels[id];
362
429
  const draggable = !compact && locked !== true && isPanelDraggable(layoutState, id);
363
430
  const editing = layoutState.editing && draggable;
431
+ const resizeSpec = resolvePanelResize(panel?.type ?? type);
432
+ const resizable = editing && resizeSpec.axes !== "none";
364
433
  const rootRef = useRef(null);
365
434
  const dragRef = useRef(null);
366
435
  const suppressClickRef = useRef(false);
@@ -368,6 +437,7 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
368
437
  const [dragging, setDragging] = useState(false);
369
438
  useEffect(() => () => detachRef.current?.(), []);
370
439
  const regionHidden = panel === undefined ||
440
+ panel.visible === false ||
371
441
  (compact && compactMode === "hide") ||
372
442
  (onMobile && mobileBehavior === "hidden") ||
373
443
  !hudVisibleInPhase(showDuring, phase);
@@ -383,6 +453,8 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
383
453
  const onPointerDown = (event) => {
384
454
  if (!draggable || event.button !== 0)
385
455
  return;
456
+ if (event.target?.closest?.("[data-hud-resize]") != null)
457
+ return;
386
458
  const root = rootRef.current;
387
459
  const canvas = canvasRef.current;
388
460
  if (root === null || canvas === null)
@@ -439,6 +511,44 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
439
511
  window.addEventListener("pointerup", onEnd);
440
512
  window.addEventListener("pointercancel", onEnd);
441
513
  };
514
+ const onResizeStart = (edge, event) => {
515
+ if (!resizable || event.button !== 0)
516
+ return;
517
+ const root = rootRef.current;
518
+ if (root === null)
519
+ return;
520
+ const startWidth = panel?.width ?? root.getBoundingClientRect().width;
521
+ const startHeight = panel?.height ?? root.getBoundingClientRect().height;
522
+ const pointerId = event.pointerId;
523
+ const startX = event.clientX;
524
+ const startY = event.clientY;
525
+ suppressClickRef.current = true;
526
+ layout.bringToFront(id);
527
+ const onMove = (moveEvent) => {
528
+ if (moveEvent.pointerId !== pointerId)
529
+ return;
530
+ const dw = edge === "s" ? 0 : moveEvent.clientX - startX;
531
+ const dh = edge === "e" ? 0 : moveEvent.clientY - startY;
532
+ const next = resizePanelSize({ width: startWidth, height: startHeight }, { dw, dh }, resizeSpec.axes, resizeSpec);
533
+ layout.resize(id, next);
534
+ };
535
+ const onEnd = (endEvent) => {
536
+ if (endEvent.pointerId !== pointerId)
537
+ return;
538
+ detachRef.current?.();
539
+ };
540
+ detachRef.current?.();
541
+ const detach = () => {
542
+ window.removeEventListener("pointermove", onMove);
543
+ window.removeEventListener("pointerup", onEnd);
544
+ window.removeEventListener("pointercancel", onEnd);
545
+ detachRef.current = null;
546
+ };
547
+ detachRef.current = detach;
548
+ window.addEventListener("pointermove", onMove);
549
+ window.addEventListener("pointerup", onEnd);
550
+ window.addEventListener("pointercancel", onEnd);
551
+ };
442
552
  const onClickCapture = (event) => {
443
553
  if (!suppressClickRef.current)
444
554
  return;
@@ -458,15 +568,18 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
458
568
  color: "rgba(255, 255, 255, 0.9)",
459
569
  font: "10px/1.5 system-ui, sans-serif",
460
570
  whiteSpace: "nowrap",
461
- }, children: id })] })) : null;
571
+ }, children: id }), resizable ? _jsx(ResizeHandles, { axes: resizeSpec.axes, onResizeStart: onResizeStart }) : null] })) : null;
462
572
  if (panel === undefined)
463
573
  return null;
574
+ if (panel.visible === false)
575
+ return null;
464
576
  if (compact && compactMode === "hide")
465
577
  return null;
466
578
  if (onMobile && mobileBehavior === "hidden")
467
579
  return null;
468
580
  if (!hudVisibleInPhase(showDuring, phase))
469
581
  return null;
582
+ const authoredSize = sizeStyle(panel);
470
583
  const flow = compact || !panel.moved;
471
584
  if (flow) {
472
585
  const regionEl = regions[compact ? anchor : panel.placement.anchor];
@@ -474,7 +587,7 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
474
587
  return null;
475
588
  const chipped = compact && compactMode === "chip";
476
589
  const passThrough = interactive === false && !editing && !chipped;
477
- return createPortal(_jsxs("div", { ref: rootRef, "data-hud-panel": id, "data-dragging": dragging ? "" : undefined, "data-hud-priority": priority, "data-hud-mobile-behavior": mobileBehavior, className: className, onPointerDown: onPointerDown, onClickCapture: onClickCapture, style: {
590
+ return createPortal(_jsxs("div", { ref: rootRef, "data-hud-panel": id, "data-dragging": dragging ? "" : undefined, "data-hud-priority": priority, "data-hud-mobile-behavior": mobileBehavior, "data-hud-type": panel.type ?? type, className: className, onPointerDown: onPointerDown, onClickCapture: onClickCapture, style: {
478
591
  position: "relative",
479
592
  order: order ?? 0,
480
593
  pointerEvents: passThrough ? "none" : "auto",
@@ -483,6 +596,8 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
483
596
  cursor: dragging ? "grabbing" : editing ? "grab" : undefined,
484
597
  outline: editing ? "1px dashed rgba(255, 255, 255, 0.6)" : undefined,
485
598
  outlineOffset: editing ? 2 : undefined,
599
+ boxSizing: "border-box",
600
+ ...authoredSize,
486
601
  ...style,
487
602
  }, children: [chipped ? (_jsx(HudChip, { label: chip ?? id, anchor: anchor, children: children })) : (children), editingChrome] }), regionEl);
488
603
  }
@@ -503,7 +618,7 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
503
618
  if (fx === 0.5 || fy === 0.5) {
504
619
  position.transform = `translate(${fx === 0.5 ? "-50%" : "0"}, ${fy === 0.5 ? "-50%" : "0"})`;
505
620
  }
506
- return (_jsxs("div", { ref: rootRef, "data-hud-panel": id, "data-dragging": dragging ? "" : undefined, className: className, onPointerDown: onPointerDown, onClickCapture: onClickCapture, style: {
621
+ return (_jsxs("div", { ref: rootRef, "data-hud-panel": id, "data-dragging": dragging ? "" : undefined, "data-hud-type": panel.type ?? type, className: className, onPointerDown: onPointerDown, onClickCapture: onClickCapture, style: {
507
622
  position: "absolute",
508
623
  ...position,
509
624
  zIndex: panel.z,
@@ -513,6 +628,8 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
513
628
  cursor: dragging ? "grabbing" : editing ? "grab" : undefined,
514
629
  outline: editing ? "1px dashed rgba(255, 255, 255, 0.6)" : undefined,
515
630
  outlineOffset: editing ? 2 : undefined,
631
+ boxSizing: "border-box",
632
+ ...authoredSize,
516
633
  ...style,
517
634
  }, children: [children, editingChrome] }));
518
635
  }
@@ -0,0 +1,23 @@
1
+ import { type ReactNode } from "react";
2
+ import type { EditorUiPanelLayout } from "@jgengine/core/ui/hudDocument";
3
+ /**
4
+ * Host-owned callback that commits one HUD panel layout into the scene document
5
+ * (typically an undoable `setUiPanel` dispatch). Injected by the editor so product
6
+ * UI never reaches for editor globals.
7
+ */
8
+ export type HudLayoutPersist = (id: string, panel: EditorUiPanelLayout) => void;
9
+ /**
10
+ * Provides {@link HudLayoutPersist} / `onPanelCommit` to descendant `useHudLayout`
11
+ * calls. Mount around game UI when a host can accept document UI patches (editor).
12
+ * Absent provider → canvas moves/resizes are no-ops for document writes.
13
+ * @capability hud-layout-persist inject a host callback so canvas HUD edits commit into the scene document
14
+ */
15
+ export declare function HudLayoutPersistProvider({ onPanelCommit, children, }: {
16
+ onPanelCommit: HudLayoutPersist;
17
+ children?: ReactNode;
18
+ }): import("react").JSX.Element;
19
+ /**
20
+ * Injected panel-commit port, or `null` when no host has provided one.
21
+ * @capability hud-layout-persist-hook read the host panel-commit port from a HUD layout widget
22
+ */
23
+ export declare function useHudLayoutPersist(): HudLayoutPersist | null;
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
3
+ const HudLayoutPersistContext = createContext(null);
4
+ /**
5
+ * Provides {@link HudLayoutPersist} / `onPanelCommit` to descendant `useHudLayout`
6
+ * calls. Mount around game UI when a host can accept document UI patches (editor).
7
+ * Absent provider → canvas moves/resizes are no-ops for document writes.
8
+ * @capability hud-layout-persist inject a host callback so canvas HUD edits commit into the scene document
9
+ */
10
+ export function HudLayoutPersistProvider({ onPanelCommit, children, }) {
11
+ return (_jsx(HudLayoutPersistContext.Provider, { value: onPanelCommit, children: children }));
12
+ }
13
+ /**
14
+ * Injected panel-commit port, or `null` when no host has provided one.
15
+ * @capability hud-layout-persist-hook read the host panel-commit port from a HUD layout widget
16
+ */
17
+ export function useHudLayoutPersist() {
18
+ return useContext(HudLayoutPersistContext);
19
+ }
@@ -0,0 +1,76 @@
1
+ import { type CSSProperties } from "react";
2
+ /**
3
+ * `HudTheme` (#1034): one token object that restyles shared token-driven building blocks at once.
4
+ * It is the superset of the atomic-bar tokens (#1033) — `hudThemeVars(theme)` emits the same
5
+ * `--jg-*` custom properties the bars already read, plus frame / slot / minimap-ring tokens — so
6
+ * setting a **game-authored** theme on any HUD ancestor re-skins those primitives. Purely CSS-token
7
+ * driven (no image assets). Tokens are not a finished UI: every game still owns composition, skin,
8
+ * and art direction (see AGENTS.md). Built-in presets are demo/scaffold starting points only.
9
+ *
10
+ * @capability hud-theme game-authored token object for bars, frames, slots, and minimap ring
11
+ */
12
+ /** Per-readout fill colors plus a shared accent/glow. */
13
+ export interface HudThemePalette {
14
+ health: string;
15
+ healthLow: string;
16
+ mana: string;
17
+ stamina: string;
18
+ shield: string;
19
+ xp: string;
20
+ soul: string;
21
+ ammo: string;
22
+ boss: string;
23
+ /** Accent used for glow/keycap/active states. */
24
+ accent: string;
25
+ }
26
+ /** Frame material + shape tokens (panels, `HudFrame variation="themed"`). */
27
+ export interface HudThemeFrame {
28
+ /** Panel background (gradient or solid). */
29
+ bg: string;
30
+ border: string;
31
+ /** Corner radius (any CSS length, e.g. `"10px"`, `"0"`). */
32
+ radius: string;
33
+ /** Outer glow / drop shadow. */
34
+ glow: string;
35
+ }
36
+ /** Framed-trough tokens shared by every atomic bar. */
37
+ export interface HudThemeBar {
38
+ track: string;
39
+ frame: string;
40
+ height: string;
41
+ radius: string;
42
+ bevel: string;
43
+ text: string;
44
+ }
45
+ /** Action/inventory slot tokens. */
46
+ export interface HudThemeSlot {
47
+ bg: string;
48
+ border: string;
49
+ radius: string;
50
+ }
51
+ /** A full HUD theme. */
52
+ export interface HudTheme {
53
+ palette: HudThemePalette;
54
+ frame: HudThemeFrame;
55
+ bar: HudThemeBar;
56
+ slot: HudThemeSlot;
57
+ /** Minimap chrome ring color. */
58
+ ring: string;
59
+ }
60
+ /** Emits the CSS custom properties for a theme — spread onto any HUD ancestor to re-skin the subtree. */
61
+ export declare function hudThemeVars(theme: HudTheme): CSSProperties;
62
+ /** Default theme tokens for previews/scaffold — not a finished game look; author a custom theme. */
63
+ export declare const defaultHudTheme: HudTheme;
64
+ /** Built-in theme presets for demos/scaffold only — not product identity; games author their own theme. */
65
+ export declare const HUD_THEME_PRESETS: {
66
+ "arcane-stone": HudTheme;
67
+ "survival-wood": HudTheme;
68
+ "military-flat": HudTheme;
69
+ "sleek-hex": HudTheme;
70
+ "elemental-circle": HudTheme;
71
+ "sandbox-slot": HudTheme;
72
+ };
73
+ /** Built-in demo/scaffold theme preset name — not a game identity. */
74
+ export type HudThemePreset = keyof typeof HUD_THEME_PRESETS;
75
+ /** Resolves a preset name (or a full theme) to a `HudTheme`; falls back to the default theme. */
76
+ export declare function resolveHudTheme(theme?: HudThemePreset | HudTheme): HudTheme;
@@ -0,0 +1,156 @@
1
+ import {} from "react";
2
+ /** Emits the CSS custom properties for a theme — spread onto any HUD ancestor to re-skin the subtree. */
3
+ export function hudThemeVars(theme) {
4
+ const { palette, frame, bar, slot } = theme;
5
+ return {
6
+ // atomic-bar tokens (same names the bars read — #1033)
7
+ "--jg-health": palette.health,
8
+ "--jg-health-low": palette.healthLow,
9
+ "--jg-mana": palette.mana,
10
+ "--jg-stamina": palette.stamina,
11
+ "--jg-shield": palette.shield,
12
+ "--jg-xp": palette.xp,
13
+ "--jg-soul": palette.soul,
14
+ "--jg-ammo": palette.ammo,
15
+ "--jg-boss": palette.boss,
16
+ "--jg-bar-track": bar.track,
17
+ "--jg-bar-frame": bar.frame,
18
+ "--jg-bar-height": bar.height,
19
+ "--jg-bar-radius": bar.radius,
20
+ "--jg-bar-bevel": bar.bevel,
21
+ "--jg-bar-text": bar.text,
22
+ // shared chrome tokens
23
+ "--jg-accent": palette.accent,
24
+ "--jg-frame-bg": frame.bg,
25
+ "--jg-frame-border": frame.border,
26
+ "--jg-frame-radius": frame.radius,
27
+ "--jg-frame-glow": frame.glow,
28
+ "--jg-slot-bg": slot.bg,
29
+ "--jg-slot-border": slot.border,
30
+ "--jg-slot-radius": slot.radius,
31
+ "--jg-ring": theme.ring,
32
+ };
33
+ }
34
+ /** Default theme tokens for previews/scaffold — not a finished game look; author a custom theme. */
35
+ export const defaultHudTheme = {
36
+ palette: {
37
+ health: "#e5484d",
38
+ healthLow: "#b21e23",
39
+ mana: "#3b82f6",
40
+ stamina: "#84cc16",
41
+ shield: "#22d3ee",
42
+ xp: "#a855f7",
43
+ soul: "#c084fc",
44
+ ammo: "#f8b84e",
45
+ boss: "#ef4444",
46
+ accent: "#38bdf8",
47
+ },
48
+ frame: {
49
+ bg: "linear-gradient(180deg, rgba(20,24,32,0.86), rgba(10,12,16,0.9))",
50
+ border: "1px solid rgba(255,255,255,0.12)",
51
+ radius: "10px",
52
+ glow: "0 6px 20px rgba(0,0,0,0.45)",
53
+ },
54
+ bar: {
55
+ track: "rgba(0,0,0,0.55)",
56
+ frame: "rgba(0,0,0,0.82)",
57
+ height: "18px",
58
+ radius: "4px",
59
+ bevel: "inset 0 2px 4px rgba(0,0,0,0.55), inset 0 -1px 2px rgba(255,255,255,0.08)",
60
+ text: "#f5f7fa",
61
+ },
62
+ slot: { bg: "rgba(12,14,20,0.7)", border: "1px solid rgba(255,255,255,0.12)", radius: "8px" },
63
+ ring: "rgba(140,170,200,0.35)",
64
+ };
65
+ function theme(overrides) {
66
+ return {
67
+ palette: { ...defaultHudTheme.palette, ...overrides.palette },
68
+ frame: { ...defaultHudTheme.frame, ...overrides.frame },
69
+ bar: { ...defaultHudTheme.bar, ...overrides.bar },
70
+ slot: { ...defaultHudTheme.slot, ...overrides.slot },
71
+ ring: overrides.ring ?? defaultHudTheme.ring,
72
+ };
73
+ }
74
+ /** Built-in theme presets for demos/scaffold only — not product identity; games author their own theme. */
75
+ export const HUD_THEME_PRESETS = {
76
+ "arcane-stone": theme({
77
+ palette: { health: "#c0392b", mana: "#7c5cff", xp: "#d4a017", accent: "#b483ff", shield: "#8e7bd6" },
78
+ frame: {
79
+ bg: "linear-gradient(180deg, #3a3550, #211d33)",
80
+ border: "2px solid #6b5b95",
81
+ radius: "6px",
82
+ glow: "0 0 18px rgba(124,92,255,0.35)",
83
+ },
84
+ bar: { frame: "#160f28", radius: "3px", height: "20px", track: "rgba(0,0,0,0.6)", bevel: "inset 0 2px 5px rgba(0,0,0,0.6)", text: "#efe7ff" },
85
+ slot: { bg: "#241f38", border: "2px solid #6b5b95", radius: "5px" },
86
+ ring: "rgba(124,92,255,0.5)",
87
+ }),
88
+ "survival-wood": theme({
89
+ palette: { health: "#c0553b", stamina: "#a3b545", xp: "#e0a94b", accent: "#d9a441", shield: "#7bb0a0" },
90
+ frame: {
91
+ bg: "linear-gradient(180deg, #4a3b28, #2e2117)",
92
+ border: "3px solid #6b4a2c",
93
+ radius: "4px",
94
+ glow: "0 4px 10px rgba(0,0,0,0.5)",
95
+ },
96
+ bar: { frame: "#2a1c10", radius: "2px", height: "18px", track: "rgba(30,20,10,0.7)", bevel: "inset 0 2px 4px rgba(0,0,0,0.6)", text: "#f3e6cf" },
97
+ slot: { bg: "#3a2a1a", border: "3px solid #6b4a2c", radius: "3px" },
98
+ ring: "rgba(107,74,44,0.6)",
99
+ }),
100
+ "military-flat": theme({
101
+ palette: { health: "#3fa34d", stamina: "#a8b545", xp: "#c9b458", accent: "#7d9b4e", shield: "#5f8fae" },
102
+ frame: {
103
+ bg: "linear-gradient(180deg, #2b3126, #1c2019)",
104
+ border: "1px solid #4a5340",
105
+ radius: "2px",
106
+ glow: "0 2px 6px rgba(0,0,0,0.5)",
107
+ },
108
+ bar: { frame: "#161a12", radius: "0px", height: "16px", track: "rgba(0,0,0,0.6)", bevel: "inset 0 1px 2px rgba(0,0,0,0.7)", text: "#e8efdc" },
109
+ slot: { bg: "#232a1e", border: "1px solid #4a5340", radius: "0px" },
110
+ ring: "rgba(125,155,78,0.5)",
111
+ }),
112
+ "sleek-hex": theme({
113
+ palette: { health: "#ff5a7a", mana: "#33c9ff", xp: "#38e8c0", accent: "#33c9ff", shield: "#33c9ff" },
114
+ frame: {
115
+ bg: "linear-gradient(180deg, rgba(18,24,34,0.9), rgba(8,12,18,0.94))",
116
+ border: "1px solid rgba(51,201,255,0.4)",
117
+ radius: "3px",
118
+ glow: "0 0 16px rgba(51,201,255,0.3)",
119
+ },
120
+ bar: { frame: "rgba(51,201,255,0.5)", radius: "2px", height: "14px", track: "rgba(0,0,0,0.5)", bevel: "inset 0 1px 3px rgba(0,0,0,0.5)", text: "#eafaff" },
121
+ slot: { bg: "rgba(12,18,26,0.8)", border: "1px solid rgba(51,201,255,0.4)", radius: "3px" },
122
+ ring: "rgba(51,201,255,0.55)",
123
+ }),
124
+ "elemental-circle": theme({
125
+ palette: { health: "#ff6b3d", mana: "#4aa8ff", stamina: "#7bd451", xp: "#ffcf40", accent: "#ffcf40", shield: "#4aa8ff" },
126
+ frame: {
127
+ bg: "radial-gradient(circle at 50% 40%, #2c2a3a, #16141f)",
128
+ border: "2px solid #d9a441",
129
+ radius: "9999px",
130
+ glow: "0 0 20px rgba(217,164,65,0.35)",
131
+ },
132
+ bar: { frame: "#1a1626", radius: "9999px", height: "16px", track: "rgba(0,0,0,0.55)", bevel: "inset 0 2px 4px rgba(0,0,0,0.6)", text: "#fff3d6" },
133
+ slot: { bg: "#221f30", border: "2px solid #d9a441", radius: "9999px" },
134
+ ring: "rgba(217,164,65,0.6)",
135
+ }),
136
+ "sandbox-slot": theme({
137
+ palette: { health: "#e0524a", stamina: "#77c043", xp: "#5ac0e0", accent: "#f0c040", shield: "#5ac0e0" },
138
+ frame: {
139
+ bg: "linear-gradient(180deg, #5a5a5a, #3a3a3a)",
140
+ border: "3px solid #2a2a2a",
141
+ radius: "2px",
142
+ glow: "3px 3px 0 rgba(0,0,0,0.5)",
143
+ },
144
+ bar: { frame: "#1e1e1e", radius: "0px", height: "18px", track: "rgba(0,0,0,0.6)", bevel: "inset 0 2px 3px rgba(0,0,0,0.6)", text: "#f4f4f4" },
145
+ slot: { bg: "#4a4a4a", border: "3px solid #2a2a2a", radius: "0px" },
146
+ ring: "rgba(240,192,64,0.5)",
147
+ }),
148
+ };
149
+ /** Resolves a preset name (or a full theme) to a `HudTheme`; falls back to the default theme. */
150
+ export function resolveHudTheme(theme) {
151
+ if (theme === undefined)
152
+ return defaultHudTheme;
153
+ if (typeof theme === "string")
154
+ return HUD_THEME_PRESETS[theme] ?? defaultHudTheme;
155
+ return theme;
156
+ }
@@ -0,0 +1,4 @@
1
+ /** Renders the default theme plus every genre preset as a deterministic matrix. */
2
+ export declare function HudThemePreview({ className }: {
3
+ className?: string;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import {} from "react";
3
+ import { AmmoCounter, ExperienceBar, HealthBar, ManaBar, ShieldBar } from "./bars.js";
4
+ import { HudFrame } from "./hudFrame.js";
5
+ import { HUD_THEME_PRESETS, defaultHudTheme, hudThemeVars } from "./hudTheme.js";
6
+ /**
7
+ * A deterministic preset matrix for {@link HudTheme} (#1034): every genre preset × (bars + a themed
8
+ * frame + a slot row), rendered from static values. One `hudThemeVars(theme)` block per cell drives
9
+ * the atomic bars, the `HudFrame variation="themed"`, and the slot chrome together — proof that a
10
+ * single theme restyles all the shared chrome at once. Renders identically every time (screenshots).
11
+ */
12
+ function Slot({ index }) {
13
+ return (_jsx("div", { style: {
14
+ width: 34,
15
+ height: 34,
16
+ background: "var(--jg-slot-bg, rgba(12,14,20,0.7))",
17
+ border: "var(--jg-slot-border, 1px solid rgba(255,255,255,0.12))",
18
+ borderRadius: "var(--jg-slot-radius, 8px)",
19
+ display: "flex",
20
+ alignItems: "center",
21
+ justifyContent: "center",
22
+ fontSize: 10,
23
+ color: "var(--jg-bar-text, #f4f6fb)",
24
+ opacity: index === 0 ? 1 : 0.6,
25
+ }, children: index + 1 }));
26
+ }
27
+ function ThemeCell({ name, theme }) {
28
+ return (_jsx("div", { style: { ...hudThemeVars(theme), width: 260 }, children: _jsx(HudFrame, { variation: "themed", title: name, padding: 12, children: _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 7 }, children: [_jsx(HealthBar, { value: 78, max: 100, label: "HP", width: "100%" }), _jsx(ManaBar, { value: 50, max: 80, label: "MP", width: "100%" }), _jsx(ShieldBar, { value: 30, max: 60, label: "SH", width: "100%" }), _jsx(ExperienceBar, { value: 4, max: 10, segments: 10, showValue: false, label: "XP", width: "100%" }), _jsx(AmmoCounter, { loaded: 17, reserve: 90, magazine: 30, width: "100%" }), _jsx("div", { style: { display: "flex", gap: 6, marginTop: 2 }, children: [0, 1, 2, 3].map((index) => (_jsx(Slot, { index: index }, index))) })] }) }) }));
29
+ }
30
+ /** Renders the default theme plus every genre preset as a deterministic matrix. */
31
+ export function HudThemePreview({ className }) {
32
+ const cells = [
33
+ ["default", defaultHudTheme],
34
+ ...Object.entries(HUD_THEME_PRESETS),
35
+ ];
36
+ return (_jsx("div", { "data-hud-theme-preview": true, className: className, style: { display: "flex", gap: 18, flexWrap: "wrap", padding: 22, fontFamily: "system-ui, sans-serif", background: "#0e1216" }, children: cells.map(([name, theme]) => (_jsx(ThemeCell, { name: name, theme: theme }, name))) }));
37
+ }