@jgengine/react 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/actionHud.d.ts +5 -5
  3. package/dist/actionHud.js +14 -12
  4. package/dist/bars.d.ts +103 -0
  5. package/dist/bars.js +180 -0
  6. package/dist/barsPreview.d.ts +4 -0
  7. package/dist/barsPreview.js +47 -0
  8. package/dist/characterSheet.d.ts +120 -0
  9. package/dist/characterSheet.js +134 -0
  10. package/dist/components.d.ts +8 -1
  11. package/dist/components.js +8 -1
  12. package/dist/hooks.d.ts +31 -0
  13. package/dist/hooks.js +53 -0
  14. package/dist/hud.d.ts +21 -4
  15. package/dist/hud.js +38 -27
  16. package/dist/hudFrame.d.ts +47 -0
  17. package/dist/hudFrame.js +80 -0
  18. package/dist/hudLayout.d.ts +13 -1
  19. package/dist/hudLayout.js +18 -12
  20. package/dist/hudLayoutPersist.d.ts +23 -0
  21. package/dist/hudLayoutPersist.js +19 -0
  22. package/dist/hudTheme.d.ts +76 -0
  23. package/dist/hudTheme.js +156 -0
  24. package/dist/hudThemePreview.d.ts +4 -0
  25. package/dist/hudThemePreview.js +37 -0
  26. package/dist/iconTreatment.d.ts +50 -0
  27. package/dist/iconTreatment.js +84 -0
  28. package/dist/iconsPreview.d.ts +4 -0
  29. package/dist/iconsPreview.js +29 -0
  30. package/dist/index.d.ts +12 -0
  31. package/dist/index.js +12 -0
  32. package/dist/inventoryGrid.d.ts +20 -0
  33. package/dist/inventoryGrid.js +135 -0
  34. package/dist/map.d.ts +72 -10
  35. package/dist/map.js +84 -15
  36. package/dist/panels.d.ts +107 -0
  37. package/dist/panels.js +215 -0
  38. package/dist/selectionHud.d.ts +11 -5
  39. package/dist/selectionHud.js +11 -5
  40. package/dist/slots.d.ts +47 -0
  41. package/dist/slots.js +37 -0
  42. package/package.json +2 -2
  43. package/skills/jgengine/SKILL.md +8 -4
  44. package/skills/jgengine/api.md +182 -81
  45. package/skills/jgengine/capabilities.md +104 -2
  46. package/skills/jgengine/recipes/minimal-game.md +88 -0
  47. package/skills/jgengine-ui/SKILL.md +17 -10
  48. package/skills/jgengine-ui/api.md +316 -51
  49. package/skills/jgengine-ui/capabilities.md +135 -2
  50. package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
  51. package/skills/jgengine-ui/reference.md +59 -9
  52. package/skills/jgengine-ui/references/visual-scorecard.md +1 -1
  53. package/skills/jgengine/reference-cartridge.md +0 -72
package/dist/panels.js ADDED
@@ -0,0 +1,215 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState, } from "react";
3
+ import { closePanel, closeTopPanel, createPanelState, focusPanel, isOpen as isPanelOpen, movePanel, openPanel, orderedOpen, panelByHotkey, } from "@jgengine/core/ui/panelModel";
4
+ import { HudFrame } from "./hudFrame.js";
5
+ /**
6
+ * Pure keybind resolver for a panel set: ESC resolves to `closeTop` when a closable window is open, and
7
+ * any other key routes through {@link panelByHotkey} (trying `code` then `key`) to a `toggle`. Returns
8
+ * the intent without touching the DOM, so it unit-tests headless and `usePanels` is a thin shell over it.
9
+ */
10
+ export function panelKeyAction(defs, state, event) {
11
+ if (event.key === "Escape" || event.code === "Escape") {
12
+ return closeTopPanel(state) === state ? { handled: false } : { handled: true, type: "closeTop" };
13
+ }
14
+ const id = (event.code !== undefined ? panelByHotkey(defs, event.code) : null) ??
15
+ (event.key !== undefined ? panelByHotkey(defs, event.key) : null);
16
+ return id === null ? { handled: false } : { handled: true, type: "toggle", id };
17
+ }
18
+ /** @internal True when a key event targets a text input / textarea / contenteditable, where hotkeys must not fire. */
19
+ function isEditableTarget(target) {
20
+ const el = target;
21
+ if (el === null || typeof el.tagName !== "string")
22
+ return false;
23
+ const tag = el.tagName.toLowerCase();
24
+ return tag === "input" || tag === "textarea" || tag === "select" || el.isContentEditable === true;
25
+ }
26
+ /**
27
+ * The DATA/HOOK layer: wrap the core panel model in state and install a window keybind listener —
28
+ * per-panel hotkeys toggle their window and ESC closes the topmost closable one. Hotkeys are ignored
29
+ * while the player is typing in an input/textarea/contenteditable. The listener is cleaned up on
30
+ * unmount and SSR-safe (armed in an effect). Feed the returned manager to {@link PanelHost}, or drive a
31
+ * bespoke window layout from it directly.
32
+ *
33
+ * @capability use-panels headless toggleable-window manager with keybind + ESC handling over the core panel model
34
+ */
35
+ export function usePanels(defs, options) {
36
+ const [state, setState] = useState(() => createPanelState(defs));
37
+ const stateRef = useRef(state);
38
+ stateRef.current = state;
39
+ const defsRef = useRef(defs);
40
+ defsRef.current = defs;
41
+ const onOpenRef = useRef(options?.onOpen);
42
+ onOpenRef.current = options?.onOpen;
43
+ const onCloseRef = useRef(options?.onClose);
44
+ onCloseRef.current = options?.onClose;
45
+ const open = useCallback((id) => {
46
+ const wasOpen = isPanelOpen(stateRef.current, id);
47
+ setState((s) => openPanel(s, id));
48
+ if (!wasOpen)
49
+ onOpenRef.current?.(id);
50
+ }, []);
51
+ const close = useCallback((id) => {
52
+ const wasOpen = isPanelOpen(stateRef.current, id);
53
+ setState((s) => closePanel(s, id));
54
+ if (wasOpen)
55
+ onCloseRef.current?.(id);
56
+ }, []);
57
+ const toggle = useCallback((id) => {
58
+ if (isPanelOpen(stateRef.current, id))
59
+ close(id);
60
+ else
61
+ open(id);
62
+ }, [open, close]);
63
+ const focus = useCallback((id) => setState((s) => focusPanel(s, id)), []);
64
+ const move = useCallback((id, pos) => setState((s) => movePanel(s, id, pos)), []);
65
+ const byHotkey = useCallback((code) => panelByHotkey(defsRef.current, code), []);
66
+ const hotkeysEnabled = options?.hotkeys !== false;
67
+ useEffect(() => {
68
+ if (!hotkeysEnabled || typeof window === "undefined")
69
+ return undefined;
70
+ const onKeyDown = (event) => {
71
+ if (event.defaultPrevented)
72
+ return;
73
+ if (event.key !== "Escape" && isEditableTarget(event.target))
74
+ return;
75
+ const result = panelKeyAction(defsRef.current, stateRef.current, {
76
+ code: event.code,
77
+ key: event.key,
78
+ });
79
+ if (!result.handled)
80
+ return;
81
+ event.preventDefault();
82
+ if (result.type === "closeTop") {
83
+ const topId = topClosableOpen(stateRef.current);
84
+ setState((s) => closeTopPanel(s));
85
+ if (topId !== null)
86
+ onCloseRef.current?.(topId);
87
+ }
88
+ else if (result.type === "toggle" && result.id !== undefined) {
89
+ toggle(result.id);
90
+ }
91
+ };
92
+ window.addEventListener("keydown", onKeyDown);
93
+ return () => window.removeEventListener("keydown", onKeyDown);
94
+ }, [hotkeysEnabled, toggle]);
95
+ return {
96
+ state,
97
+ defs,
98
+ isOpen: (id) => isPanelOpen(state, id),
99
+ open,
100
+ close,
101
+ toggle,
102
+ focus,
103
+ move,
104
+ byHotkey,
105
+ };
106
+ }
107
+ /** @internal The id the next ESC would close, so the caller can fire `onClose` with it. */
108
+ function topClosableOpen(state) {
109
+ const next = closeTopPanel(state);
110
+ if (next === state)
111
+ return null;
112
+ for (const id of Object.keys(state.open)) {
113
+ if (next.open[id] !== true)
114
+ return id;
115
+ }
116
+ return null;
117
+ }
118
+ // ---- Window chrome ---------------------------------------------------------
119
+ /** @internal Begin a pointer drag from the title bar, streaming new positions to `onMove` until release. */
120
+ function beginWindowDrag(event, origin, onMove) {
121
+ if (typeof window === "undefined")
122
+ return;
123
+ const startX = event.clientX;
124
+ const startY = event.clientY;
125
+ const base = { x: origin.x, y: origin.y };
126
+ const onPointerMove = (moveEvent) => {
127
+ onMove({ x: base.x + (moveEvent.clientX - startX), y: base.y + (moveEvent.clientY - startY) });
128
+ };
129
+ const onPointerUp = () => {
130
+ window.removeEventListener("pointermove", onPointerMove);
131
+ window.removeEventListener("pointerup", onPointerUp);
132
+ window.removeEventListener("pointercancel", onPointerUp);
133
+ };
134
+ window.addEventListener("pointermove", onPointerMove);
135
+ window.addEventListener("pointerup", onPointerUp);
136
+ window.addEventListener("pointercancel", onPointerUp);
137
+ }
138
+ const TITLEBAR_STYLE = {
139
+ display: "flex",
140
+ alignItems: "center",
141
+ justifyContent: "space-between",
142
+ gap: 8,
143
+ padding: "6px 8px 6px 10px",
144
+ cursor: "grab",
145
+ touchAction: "none",
146
+ userSelect: "none",
147
+ fontSize: 12,
148
+ fontWeight: 600,
149
+ letterSpacing: 0.3,
150
+ borderBottom: "var(--jg-frame-border, 1px solid rgba(255,255,255,0.10))",
151
+ };
152
+ const CLOSE_BUTTON_STYLE = {
153
+ display: "inline-flex",
154
+ alignItems: "center",
155
+ justifyContent: "center",
156
+ width: 20,
157
+ height: 20,
158
+ padding: 0,
159
+ borderRadius: 6,
160
+ border: "1px solid transparent",
161
+ background: "transparent",
162
+ color: "inherit",
163
+ font: "inherit",
164
+ lineHeight: 1,
165
+ cursor: "pointer",
166
+ opacity: 0.75,
167
+ };
168
+ /** @internal Shared, fully-controlled window view — title bar (drag + close) plus a HudFrame body. */
169
+ function WindowView({ title, ariaLabel, closable, onClose, onFocus, onMove, pos, z, width, variation, shape, className, style, bodyStyle, children, }) {
170
+ return (_jsx("div", { role: "dialog", "aria-label": ariaLabel, "data-jg-window": "", className: className, onPointerDown: () => onFocus?.(), style: { position: "absolute", left: pos.x, top: pos.y, zIndex: z, pointerEvents: "auto", ...style }, children: _jsxs(HudFrame, { variation: variation, shape: shape, width: width, padding: 0, interactive: true, children: [_jsxs("div", { "data-jg-window-titlebar": "", onPointerDown: (event) => {
171
+ if (event.button !== 0)
172
+ return;
173
+ onFocus?.();
174
+ if (onMove !== undefined)
175
+ beginWindowDrag(event, pos, onMove);
176
+ }, style: TITLEBAR_STYLE, children: [_jsx("span", { "data-jg-window-title": "", children: title }), closable ? (_jsx("button", { type: "button", "data-jg-window-close": "", "aria-label": "Close", onClick: onClose, onPointerDown: (event) => event.stopPropagation(), style: CLOSE_BUTTON_STYLE, children: "\u2715" })) : null] }), _jsx("div", { "data-jg-window-body": "", style: { padding: 10, ...bodyStyle }, children: children })] }) }));
177
+ }
178
+ /** @internal Fallback cascade position for a window whose state has no override yet. */
179
+ function cascade(index) {
180
+ return { x: 56 + index * 28, y: 56 + index * 28 };
181
+ }
182
+ /**
183
+ * Renders the open panels of a {@link usePanels} manager as absolutely-positioned, draggable, closable
184
+ * windows in z-order — the WoW window layer. Each window uses {@link HudFrame} chrome, is dragged by its
185
+ * title bar, raises focus on pointer-down, and gets its content from the `render` prop (or a `panels`
186
+ * map). Accessible: every window is a `role="dialog"` with an `aria-label` and a focusable close button;
187
+ * ESC-close is wired by the manager. Unskinned — art-direct via `HudTheme` tokens and the `variation`.
188
+ *
189
+ * @capability panel-host render a manager's open windows as draggable, closable, z-stacked dialogs
190
+ */
191
+ export function PanelHost({ manager, render, panels, variation, shape, width, className, windowClassName, windowStyle, bodyStyle, }) {
192
+ const open = orderedOpen(manager.state, manager.defs);
193
+ const content = (id) => render !== undefined ? render(id) : (panels?.[id] ?? null);
194
+ return (_jsx("div", { "data-jg-panel-host": "", className: className, style: { position: "absolute", inset: 0, pointerEvents: "none" }, children: open.map((def, index) => (_jsx(WindowView, { title: def.title ?? def.id, ariaLabel: def.title ?? def.id, closable: def.closable !== false, onClose: () => manager.close(def.id), onFocus: () => manager.focus(def.id), onMove: (pos) => manager.move(def.id, pos), pos: manager.state.pos[def.id] ?? cascade(index), z: manager.state.z[def.id] ?? 1, width: width, variation: variation, shape: shape, className: windowClassName, style: windowStyle, bodyStyle: bodyStyle, children: content(def.id) }, def.id))) }));
195
+ }
196
+ /**
197
+ * A single standalone window — title bar, close button, and title-bar drag over {@link HudFrame} chrome —
198
+ * usable without the {@link usePanels} manager for a one-off dialog. Position is uncontrolled (seeded by
199
+ * `x`/`y`) unless both `x`/`y` and `onMove` are supplied, in which case the caller owns placement.
200
+ * Accessible: `role="dialog"` with an `aria-label` and a focusable close button.
201
+ *
202
+ * @capability window standalone draggable, closable window primitive over HudFrame — no manager required
203
+ */
204
+ export function Window({ title, ariaLabel, closable = true, onClose, x, y, onMove, z, width, variation, shape, className, style, bodyStyle, children, }) {
205
+ const controlled = x !== undefined && y !== undefined && onMove !== undefined;
206
+ const [localPos, setLocalPos] = useState({ x: x ?? 56, y: y ?? 56 });
207
+ const pos = controlled ? { x: x, y: y } : localPos;
208
+ const handleMove = useCallback((next) => {
209
+ if (!controlled)
210
+ setLocalPos(next);
211
+ onMove?.(next);
212
+ }, [controlled, onMove]);
213
+ const label = ariaLabel ?? (typeof title === "string" ? title : undefined);
214
+ return (_jsx(WindowView, { title: title, ariaLabel: label, closable: closable, onClose: onClose, onMove: handleMove, pos: pos, z: z, width: width, variation: variation, shape: shape, className: className, style: style, bodyStyle: bodyStyle, children: children }));
215
+ }
@@ -35,6 +35,9 @@ export declare function useSelectionView(members: readonly EntitySummaryDef[], o
35
35
  * A compact current/max vital meter for an entity summary; tone maps to a colour ramp. Reuse it as
36
36
  * the vital row inside {@link EntitySummary} or any custom entity panel.
37
37
  *
38
+ * @deprecated The `tone`/`vital`-API umbrella is retired (#1033). Use the atomic, purpose-named,
39
+ * token-themed bars from `@jgengine/react/bars` (`HealthBar`, `ShieldBar`, `ManaBar`, …) and compose
40
+ * your own panel — each does one readout and restyles globally via the shared `--jg-*` tokens.
38
41
  * @capability vital-bar compact current/max vital meter for an entity summary
39
42
  */
40
43
  export declare function VitalBar({ vital, width, showLabel, className, style, }: {
@@ -67,6 +70,9 @@ export declare function EntityPortrait({ entity, selected, size, vitalId, tabInd
67
70
  * "who is selected" summary a single-selection or the head of a multi-selection shows. Swap the vital
68
71
  * rows via `renderVital`.
69
72
  *
73
+ * @deprecated Bundling a whole vitals array in one component is the combo anti-pattern #1033 retires.
74
+ * Compose your own summary — name/tags plus the atomic `@jgengine/react/bars` (`HealthBar`, …) you
75
+ * need — so no engine component packages multiple readouts by default. Kept during migration.
70
76
  * @capability entity-summary primary selected-entity detail panel (name, tags, vitals)
71
77
  */
72
78
  export declare function EntitySummary({ entity, renderVital, className, style, }: {
@@ -111,12 +117,12 @@ export declare function SelectionCollectionChrome({ model, itemSize, gap, window
111
117
  style?: CSSProperties;
112
118
  }): import("react").JSX.Element;
113
119
  /**
114
- * The batteries-included default: a skinned glass panel composing {@link EntitySummary} for the
115
- * primary member beside {@link SelectionCollectionChrome} for the rest. Pass `members` (or a prebuilt
116
- * `model`) and it renders a working RTS-style selection panel; every sub-renderer stays swappable, and
117
- * a game that wants a different frame composes the hook and chrome directly.
120
+ * Convenience composition: skinned glass panel with {@link EntitySummary} for the primary member
121
+ * beside {@link SelectionCollectionChrome} for the rest — demos/scaffold only, not a finished game
122
+ * face. Pass `members` (or a prebuilt `model`); swap sub-renderers or compose the headless pieces
123
+ * under game-owned chrome for product UI.
118
124
  *
119
- * @capability selection-panel drop-in selection panel: primary summary beside a portrait strip / group chips
125
+ * @capability selection-panel optional selection-panel composition: summary + portrait strip / group chips
120
126
  */
121
127
  export declare function SelectionPanel({ members, model: providedModel, groupThreshold, labelOf, windowSize, renderPrimary, renderMember, renderVital, onFocusChange, className, style, }: {
122
128
  members?: readonly EntitySummaryDef[];
@@ -65,6 +65,9 @@ const VITAL_TONES = {
65
65
  * A compact current/max vital meter for an entity summary; tone maps to a colour ramp. Reuse it as
66
66
  * the vital row inside {@link EntitySummary} or any custom entity panel.
67
67
  *
68
+ * @deprecated The `tone`/`vital`-API umbrella is retired (#1033). Use the atomic, purpose-named,
69
+ * token-themed bars from `@jgengine/react/bars` (`HealthBar`, `ShieldBar`, `ManaBar`, …) and compose
70
+ * your own panel — each does one readout and restyles globally via the shared `--jg-*` tokens.
68
71
  * @capability vital-bar compact current/max vital meter for an entity summary
69
72
  */
70
73
  export function VitalBar({ vital, width = 120, showLabel = true, className, style, }) {
@@ -115,6 +118,9 @@ export function EntityPortrait({ entity, selected = false, size = 44, vitalId =
115
118
  * "who is selected" summary a single-selection or the head of a multi-selection shows. Swap the vital
116
119
  * rows via `renderVital`.
117
120
  *
121
+ * @deprecated Bundling a whole vitals array in one component is the combo anti-pattern #1033 retires.
122
+ * Compose your own summary — name/tags plus the atomic `@jgengine/react/bars` (`HealthBar`, …) you
123
+ * need — so no engine component packages multiple readouts by default. Kept during migration.
118
124
  * @capability entity-summary primary selected-entity detail panel (name, tags, vitals)
119
125
  */
120
126
  export function EntitySummary({ entity, renderVital, className, style, }) {
@@ -165,12 +171,12 @@ const PANEL = {
165
171
  font: "500 12px/1.2 ui-sans-serif, system-ui, sans-serif",
166
172
  };
167
173
  /**
168
- * The batteries-included default: a skinned glass panel composing {@link EntitySummary} for the
169
- * primary member beside {@link SelectionCollectionChrome} for the rest. Pass `members` (or a prebuilt
170
- * `model`) and it renders a working RTS-style selection panel; every sub-renderer stays swappable, and
171
- * a game that wants a different frame composes the hook and chrome directly.
174
+ * Convenience composition: skinned glass panel with {@link EntitySummary} for the primary member
175
+ * beside {@link SelectionCollectionChrome} for the rest — demos/scaffold only, not a finished game
176
+ * face. Pass `members` (or a prebuilt `model`); swap sub-renderers or compose the headless pieces
177
+ * under game-owned chrome for product UI.
172
178
  *
173
- * @capability selection-panel drop-in selection panel: primary summary beside a portrait strip / group chips
179
+ * @capability selection-panel optional selection-panel composition: summary + portrait strip / group chips
174
180
  */
175
181
  export function SelectionPanel({ members, model: providedModel, groupThreshold, labelOf, windowSize, renderPrimary, renderMember, renderVital, onFocusChange, className, style, }) {
176
182
  const ownModel = useSelectionView(members ?? [], { groupThreshold, labelOf, onFocusChange });
@@ -0,0 +1,47 @@
1
+ import { type CSSProperties, type ReactNode } from "react";
2
+ import { type IconSchool } from "./iconTreatment.js";
3
+ import type { GameIconName } from "./gameIcons.js";
4
+ /**
5
+ * Atomic, purpose-named slot grids (#1033): one component per category — `EquipmentSlots`,
6
+ * `WeaponSlots`, `ArmorSlots`, `PotionSlots` — for paperdoll / equip layouts where each grid holds a
7
+ * single gear category. A full multi-category backpack that binds to a live inventory with
8
+ * drag/swap/stack/split is a different, supported shape: reach for `<InventoryGrid>` (`inventoryGrid.tsx`)
9
+ * there, and use these atomic grids for the equip slots around it. Each renders a grid of painted
10
+ * {@link IconTreatment} tiles (glyph + gradient + count badge) for filled slots and a themed placeholder
11
+ * for empty ones, all reading the shared `--jg-slot-*` / `--jg-accent` HudTheme tokens (#1034).
12
+ * Composition stays the game's; the engine ships the grid.
13
+ *
14
+ * @capability hud-slots atomic themed equipment/weapon/armor/potion slot grids with painted icons
15
+ */
16
+ /** One slot's content — a `GameIcon` glyph (or custom node), school tint, count, keycap, active flag. */
17
+ export interface SlotItem {
18
+ icon?: GameIconName;
19
+ glyph?: ReactNode;
20
+ school?: IconSchool;
21
+ count?: number;
22
+ keycap?: string;
23
+ active?: boolean;
24
+ /** Accessible label / tooltip. */
25
+ label?: string;
26
+ }
27
+ /** Props shared by every atomic slot grid. */
28
+ export interface SlotGridProps {
29
+ /** Slots in order; `null` (or a slot with no icon/glyph) renders an empty placeholder. */
30
+ items: readonly (SlotItem | null)[];
31
+ /** Tile size in px. Default 44. */
32
+ size?: number;
33
+ /** Grid columns; default = one row (all slots). */
34
+ columns?: number;
35
+ /** Gap between tiles in px. Default 6. */
36
+ gap?: number;
37
+ className?: string;
38
+ style?: CSSProperties;
39
+ }
40
+ /** Worn-gear slots (head/chest/hands/…): one atomic grid, painted icons, themed from tokens. */
41
+ export declare function EquipmentSlots(props: SlotGridProps): import("react").JSX.Element;
42
+ /** Weapon loadout slots. */
43
+ export declare function WeaponSlots(props: SlotGridProps): import("react").JSX.Element;
44
+ /** Armor piece slots. */
45
+ export declare function ArmorSlots(props: SlotGridProps): import("react").JSX.Element;
46
+ /** Consumable (health/shield potion) slots. */
47
+ export declare function PotionSlots(props: SlotGridProps): import("react").JSX.Element;
package/dist/slots.js ADDED
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import {} from "react";
3
+ import { IconTreatment } from "./iconTreatment.js";
4
+ function EmptySlot({ size }) {
5
+ return (_jsx("div", { "data-slot-empty": "", style: {
6
+ width: size,
7
+ height: size,
8
+ borderRadius: "var(--jg-slot-radius, 9px)",
9
+ background: "var(--jg-slot-bg, rgba(12,14,20,0.5))",
10
+ border: "var(--jg-slot-border, 1px solid rgba(255,255,255,0.1))",
11
+ boxShadow: "inset 0 2px 6px rgba(0,0,0,0.45)",
12
+ boxSizing: "border-box",
13
+ } }));
14
+ }
15
+ function SlotGrid({ category, items, size = 44, columns, gap = 6, className, style }) {
16
+ const cols = columns ?? items.length;
17
+ return (_jsx("div", { "data-slots": category, className: className, style: { display: "grid", gridTemplateColumns: `repeat(${Math.max(1, cols)}, ${size}px)`, gap, ...style }, children: items.map((item, index) => {
18
+ const filled = item !== null && (item.icon !== undefined || item.glyph !== undefined);
19
+ return filled ? (_jsx(IconTreatment, { ...(item.icon === undefined ? {} : { icon: item.icon }), ...(item.glyph === undefined ? {} : { glyph: item.glyph }), school: item.school ?? "neutral", size: size, ...(item.count === undefined ? {} : { count: item.count }), ...(item.keycap === undefined ? {} : { keycap: item.keycap }), active: item.active ?? false }, index)) : (_jsx(EmptySlot, { size: size }, index));
20
+ }) }));
21
+ }
22
+ /** Worn-gear slots (head/chest/hands/…): one atomic grid, painted icons, themed from tokens. */
23
+ export function EquipmentSlots(props) {
24
+ return _jsx(SlotGrid, { category: "equipment", ...props });
25
+ }
26
+ /** Weapon loadout slots. */
27
+ export function WeaponSlots(props) {
28
+ return _jsx(SlotGrid, { category: "weapon", ...props });
29
+ }
30
+ /** Armor piece slots. */
31
+ export function ArmorSlots(props) {
32
+ return _jsx(SlotGrid, { category: "armor", ...props });
33
+ }
34
+ /** Consumable (health/shield potion) slots. */
35
+ export function PotionSlots(props) {
36
+ return _jsx(SlotGrid, { category: "potion", ...props });
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgengine/react",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "React UI layer for JGengine: GameProvider, hooks, and headless primitives over @jgengine/core.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -31,7 +31,7 @@
31
31
  "test": "bun test src"
32
32
  },
33
33
  "dependencies": {
34
- "@jgengine/core": "^0.11.0"
34
+ "@jgengine/core": "^0.13.0"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": "^19.0.0",
@@ -9,16 +9,16 @@ Read the repository [README](../../../README.md) for package truth, commands, la
9
9
 
10
10
  ## 1. Establish the target
11
11
 
12
- Capture only decisions that change architecture:
12
+ Treat the pitch as a unique composition of needs, not a genre to fill in — never reach for a genre kit, preset, archetype, or class template ("default sports car", "default RPG", "default FPS") in the SDK or skills (see [AGENTS.md](../../../AGENTS.md)). Use `game-design` to turn the pitch into a testable player promise, verbs, loops, progression, failure, and completion scenario. Use `level-design` when play depends on authored spaces, routes, encounters, exploration, or spatial pacing. Capture only decisions that change architecture:
13
13
 
14
14
  - player point of view and controls
15
15
  - world shape and authored content
16
16
  - core loop, interaction, combat, and progression
17
17
  - solo, local, hosted, or persistent multiplayer
18
- - HUD/menu states and art direction
18
+ - HUD/menu states and **custom** UI art direction (every game owns its presentation — no stock drop-in face)
19
19
  - one observable completion scenario
20
20
 
21
- For a new in-repo game, scaffold with `bun run new:game <id> --name "Title"`. Outside this monorepo, start with `npx jgengine create`; never copy `Games/*`.
21
+ For a new in-repo game, scaffold with `bun run new:game "<id-or-title>"` (thin script alias for `jgengine create`, auto-detects the in-repo variant). Outside this monorepo, start with `npx jgengine create`; never copy `Games/*`.
22
22
 
23
23
  ## 2. Select domains
24
24
 
@@ -26,6 +26,8 @@ Load only rows the target needs. Do not preload every domain.
26
26
 
27
27
  | Need | Skill | Owner |
28
28
  | --- | --- | --- |
29
+ | player promise, pillars, verbs, loops, economy, progression, balance | `game-design` | experience and rule intent |
30
+ | metrics, topology, encounters, navigation, spatial pacing | `level-design` | playable space intent |
29
31
  | scene placement, terrain painting, paths, asset authoring | `jgengine-editor` | editor document and authoring tools |
30
32
  | runtime world, movement, input, interaction, AI, navigation, audio | `jgengine-world` | spatial simulation and world runtime |
31
33
  | game state, systems, inventory, crafting, quests, economy, turns | `jgengine-gameplay` | non-combat rules and state |
@@ -34,15 +36,17 @@ Load only rows the target needs. Do not preload every domain.
34
36
  | models, sprites, materials, audio files, source licensing | `jgengine-assets` | asset discovery and catalogs |
35
37
  | authority, transport, replication, sessions, host persistence | `jgengine-multiplayer` | network topology and persistence adapters |
36
38
 
37
- Scenes normally require both `jgengine-editor` for authoring and `jgengine-world` for runtime consumption. Combat and multiplayer are opt-in, not default intake.
39
+ World content routes editor-first: any request that adds, moves, restyles, or removes something visible in the world — however phrased ("design this world", "place the enemies", "make it look better") — starts in `jgengine-editor`; the other domains consume the authored document. Scenes normally require both `jgengine-editor` for authoring and `jgengine-world` for runtime consumption. Combat and multiplayer are opt-in, not default intake.
38
40
 
39
41
  ## 3. Discover before designing
40
42
 
41
43
  Within each selected domain:
42
44
 
45
+ 0. Fastest start for a common composition: `npx jgengine recipe <name>` prints a vetted, SDK-typechecked snippet (imports + wired code) for `combat-loop`, `boss-telegraph`, `loot`, `quest`, `coop-presence`, `third-person-camera`; `npx jgengine recipe` lists them. Copy that instead of reconning per-file.
43
46
  1. Search `capabilities.md` by intent; it maps needs to imports.
44
47
  2. Open `api.md` only when exact signatures or export inventory are needed.
45
48
  3. Open the linked reference only for a deeper recipe or trap.
49
+ 4. For how several primitives wire into a running loop, read the domain's `recipes/` — connected, genre-free walkthroughs organized by composition seam; this skill's own [recipes/minimal-game.md](recipes/minimal-game.md) is the default whole-game walkthrough. Prefer a recipe over reading a game's source; the games under `Games/*` are not templates or references, and a hybrid game is just a different composition of the same primitives.
46
50
 
47
51
  If no capability fits, identify the upstream package seam before writing game-local code. In this repository, implement a reusable primitive with its first adopter; if editor-owned content cannot be expressed, file the editor gap first.
48
52