@jgengine/react 0.9.0 → 0.11.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 (62) hide show
  1. package/CHANGELOG.md +34 -3
  2. package/LICENSE +203 -661
  3. package/README.md +1 -1
  4. package/dist/actionHud.d.ts +174 -0
  5. package/dist/actionHud.js +292 -0
  6. package/dist/cards.d.ts +59 -0
  7. package/dist/cards.js +116 -0
  8. package/dist/chat.d.ts +6 -4
  9. package/dist/chat.js +16 -7
  10. package/dist/chatBubbles.js +2 -1
  11. package/dist/components.d.ts +20 -0
  12. package/dist/components.js +29 -1
  13. package/dist/display.d.ts +1 -0
  14. package/dist/display.js +1 -0
  15. package/dist/engineStore.d.ts +3 -0
  16. package/dist/engineStore.js +3 -0
  17. package/dist/fogOverlay.d.ts +3 -0
  18. package/dist/fogOverlay.js +3 -0
  19. package/dist/gameViewport.d.ts +54 -0
  20. package/dist/gameViewport.js +356 -0
  21. package/dist/hooks.d.ts +22 -0
  22. package/dist/hooks.js +58 -12
  23. package/dist/hud.d.ts +87 -0
  24. package/dist/hud.js +144 -0
  25. package/dist/hudLayout.d.ts +39 -4
  26. package/dist/hudLayout.js +152 -11
  27. package/dist/index.d.ts +10 -0
  28. package/dist/index.js +10 -0
  29. package/dist/keyHint.d.ts +30 -0
  30. package/dist/keyHint.js +31 -0
  31. package/dist/map.d.ts +69 -1
  32. package/dist/map.js +54 -1
  33. package/dist/provider.d.ts +2 -0
  34. package/dist/provider.js +4 -0
  35. package/dist/rotateDevice.d.ts +24 -0
  36. package/dist/rotateDevice.js +83 -0
  37. package/dist/save.d.ts +25 -0
  38. package/dist/save.js +37 -0
  39. package/dist/selectSnapshot.d.ts +2 -0
  40. package/dist/selectSnapshot.js +2 -0
  41. package/dist/selectionHud.d.ts +133 -0
  42. package/dist/selectionHud.js +182 -0
  43. package/dist/settings.d.ts +3 -0
  44. package/dist/skillCheckPaint.d.ts +2 -0
  45. package/dist/skillCheckPaint.js +2 -0
  46. package/dist/social.d.ts +3 -3
  47. package/dist/social.js +31 -10
  48. package/dist/startScreen.d.ts +62 -0
  49. package/dist/startScreen.js +62 -0
  50. package/dist/store.d.ts +24 -0
  51. package/dist/store.js +9 -0
  52. package/package.json +4 -4
  53. package/skills/jgengine/SKILL.md +51 -0
  54. package/skills/jgengine/api.md +434 -0
  55. package/skills/jgengine/capabilities.md +29 -0
  56. package/skills/jgengine/reference-cartridge.md +72 -0
  57. package/skills/jgengine-ui/SKILL.md +36 -0
  58. package/skills/jgengine-ui/api.md +1852 -0
  59. package/skills/jgengine-ui/capabilities.md +142 -0
  60. package/skills/jgengine-ui/reference.md +500 -0
  61. package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
  62. package/llms.txt +0 -1633
package/dist/chat.d.ts CHANGED
@@ -23,7 +23,7 @@ export declare function ChatInput({ channelId, className, inputClassName, button
23
23
  sendLabel?: ReactNode;
24
24
  onSent?: (message: ChatMessage) => void;
25
25
  onRejected?: (reason: string) => void;
26
- }): import("react").JSX.Element;
26
+ }): import("react").JSX.Element | null;
27
27
  export declare function ChannelTabs({ channels, active, onSelect, className, tabClassName, activeTabClassName, renderTab, }: {
28
28
  channels?: readonly string[];
29
29
  active: string;
@@ -32,8 +32,8 @@ export declare function ChannelTabs({ channels, active, onSelect, className, tab
32
32
  tabClassName?: string;
33
33
  activeTabClassName?: string;
34
34
  renderTab?: (channelId: string, isActive: boolean) => ReactNode;
35
- }): import("react").JSX.Element;
36
- export declare function ChatPanel({ channels, initialChannel, limit, className, tabsClassName, tabClassName, activeTabClassName, logClassName, messageClassName, inputClassName, inputFieldClassName, sendButtonClassName, placeholder, renderMessage, onRejected, }: {
35
+ }): import("react").JSX.Element | null;
36
+ export declare function ChatPanel({ channels, initialChannel, limit, className, tabsClassName, tabClassName, activeTabClassName, logClassName, messageClassName, inputClassName, inputFieldClassName, sendButtonClassName, placeholder, renderMessage, renderTab, onRejected, }: {
37
37
  channels?: readonly string[];
38
38
  initialChannel?: string;
39
39
  limit?: number;
@@ -48,5 +48,7 @@ export declare function ChatPanel({ channels, initialChannel, limit, className,
48
48
  sendButtonClassName?: string;
49
49
  placeholder?: string;
50
50
  renderMessage?: (message: ChatMessage) => ReactNode;
51
+ /** Overrides a tab's label — e.g. render channel id `"proximity"` as "Nearby". */
52
+ renderTab?: (channelId: string, isActive: boolean) => ReactNode;
51
53
  onRejected?: (reason: string) => void;
52
- }): import("react").JSX.Element;
54
+ }): import("react").JSX.Element | null;
package/dist/chat.js CHANGED
@@ -37,21 +37,27 @@ export function ChatLog({ channelId, limit, className, messageClassName, renderM
37
37
  export function ChatInput({ channelId, className, inputClassName, buttonClassName, placeholder, sendLabel, onSent, onRejected, }) {
38
38
  const ctx = useGameContext();
39
39
  const [value, setValue] = useState("");
40
- function submit(event) {
40
+ const chat = ctx.game.chat;
41
+ if (chat === undefined)
42
+ return null;
43
+ const submit = (event) => {
41
44
  event.preventDefault();
42
- const result = ctx.game.chat.send(ctx.player.userId, channelId, value);
45
+ const result = chat.send(ctx.player.userId, channelId, value);
43
46
  if ("reason" in result) {
44
47
  onRejected?.(result.reason);
45
48
  return;
46
49
  }
47
50
  setValue("");
48
51
  onSent?.(result.message);
49
- }
52
+ };
50
53
  return (_jsxs("form", { className: className, "data-chat-input": channelId, onSubmit: submit, children: [_jsx("input", { className: inputClassName, type: "text", value: value, placeholder: placeholder, onChange: (event) => setValue(event.target.value) }), _jsx("button", { type: "submit", className: buttonClassName, "data-chat-send": true, children: sendLabel ?? "Send" })] }));
51
54
  }
52
55
  export function ChannelTabs({ channels, active, onSelect, className, tabClassName, activeTabClassName, renderTab, }) {
53
56
  const ctx = useGameContext();
54
- const ids = channels ?? ctx.game.chat.channels().map((def) => def.id);
57
+ const chat = ctx.game.chat;
58
+ if (chat === undefined)
59
+ return null;
60
+ const ids = channels ?? chat.channels().map((def) => def.id);
55
61
  return (_jsx("div", { className: className, role: "tablist", "data-chat-tabs": true, children: ids.map((channelId) => {
56
62
  const isActive = channelId === active;
57
63
  const classes = [tabClassName, isActive ? activeTabClassName : undefined]
@@ -60,9 +66,12 @@ export function ChannelTabs({ channels, active, onSelect, className, tabClassNam
60
66
  return (_jsx("button", { type: "button", role: "tab", "aria-selected": isActive, className: classes.length > 0 ? classes : undefined, "data-channel": channelId, onClick: () => onSelect(channelId), children: renderTab !== undefined ? renderTab(channelId, isActive) : channelId }, channelId));
61
67
  }) }));
62
68
  }
63
- export function ChatPanel({ channels, initialChannel, limit, className, tabsClassName, tabClassName, activeTabClassName, logClassName, messageClassName, inputClassName, inputFieldClassName, sendButtonClassName, placeholder, renderMessage, onRejected, }) {
69
+ export function ChatPanel({ channels, initialChannel, limit, className, tabsClassName, tabClassName, activeTabClassName, logClassName, messageClassName, inputClassName, inputFieldClassName, sendButtonClassName, placeholder, renderMessage, renderTab, onRejected, }) {
64
70
  const ctx = useGameContext();
65
- const ids = channels ?? ctx.game.chat.channels().map((def) => def.id);
71
+ const chat = ctx.game.chat;
72
+ const ids = channels ?? chat?.channels().map((def) => def.id) ?? [];
66
73
  const [active, setActive] = useState(initialChannel ?? ids[0] ?? "global");
67
- return (_jsxs("section", { className: className, "data-chat-panel": true, children: [_jsx(ChannelTabs, { channels: ids, active: active, onSelect: setActive, className: tabsClassName, tabClassName: tabClassName, activeTabClassName: activeTabClassName }), _jsx(ChatLog, { channelId: active, limit: limit, className: logClassName, messageClassName: messageClassName, renderMessage: renderMessage }), _jsx(ChatInput, { channelId: active, className: inputClassName, inputClassName: inputFieldClassName, buttonClassName: sendButtonClassName, placeholder: placeholder, onRejected: onRejected })] }));
74
+ if (chat === undefined)
75
+ return null;
76
+ return (_jsxs("section", { className: className, "data-chat-panel": true, children: [_jsx(ChannelTabs, { channels: ids, active: active, onSelect: setActive, className: tabsClassName, tabClassName: tabClassName, activeTabClassName: activeTabClassName, renderTab: renderTab }), _jsx(ChatLog, { channelId: active, limit: limit, className: logClassName, messageClassName: messageClassName, renderMessage: renderMessage }), _jsx(ChatInput, { channelId: active, className: inputClassName, inputClassName: inputFieldClassName, buttonClassName: sendButtonClassName, placeholder: placeholder, onRejected: onRejected })] }));
68
77
  }
@@ -4,6 +4,7 @@ const DEFAULT_CHAT_BUBBLE_CHANNEL = "proximity";
4
4
  const DEFAULT_CHAT_BUBBLE_TTL_MS = 4000;
5
5
  const DEFAULT_CHAT_BUBBLE_LIMIT = 8;
6
6
  const CHAT_BUBBLE_TICK_MS = 500;
7
+ const EMPTY_CHAT_MESSAGES = [];
7
8
  export function latestChatBubbles(messages, nowMs, ttlMs) {
8
9
  const cutoff = nowMs - ttlMs;
9
10
  const latestByUser = new Map();
@@ -28,7 +29,7 @@ export function useChatBubbles(options) {
28
29
  const channelId = options?.channelId ?? DEFAULT_CHAT_BUBBLE_CHANNEL;
29
30
  const ttlMs = options?.ttlMs ?? DEFAULT_CHAT_BUBBLE_TTL_MS;
30
31
  const limit = options?.limit ?? DEFAULT_CHAT_BUBBLE_LIMIT;
31
- const messages = useGameStore((ctx) => ctx.game.chat.history(channelId, { limit }));
32
+ const messages = useGameStore((ctx) => ctx.game.chat?.history(channelId, { limit }) ?? EMPTY_CHAT_MESSAGES);
32
33
  const [now, setNow] = useState(() => Date.now());
33
34
  const hasLiveMessage = messages.some((message) => message.at > now - ttlMs);
34
35
  useEffect(() => {
@@ -84,6 +84,20 @@ export declare function DialogueBox({ dialogue, onChoice, rng, className, lineCl
84
84
  choiceClassName?: string;
85
85
  checkClassName?: string;
86
86
  }): import("react").JSX.Element;
87
+ /**
88
+ * The id of the dialogue `ctx.game.dialogue` (or a `talkable(id)` prompt) currently has open, or `null`.
89
+ * The read side of the `features.dialogue` bridge — a panel looks the id up in its own dialogue catalog and
90
+ * renders {@link DialogueBox}, with no per-game open/close store.
91
+ */
92
+ export declare function useOpenDialogueId(): string | null;
93
+ /**
94
+ * Route a {@link DialogueBox} choice through the `features.dialogue` bridge: resolve the choice's invoke
95
+ * (honoring a skill-check `result`), run that command, and otherwise close the dialogue — the write side that
96
+ * replaces a per-game `onChoice` that hand-rolls `resolveDialogueInvoke` + `dialogue.close`.
97
+ */
98
+ export declare function runDialogueChoice(commands: {
99
+ run(name: string, input?: unknown): unknown;
100
+ }, choice: DialogueChoice, result: CheckResult | null): void;
87
101
  export declare function SkillCheckBar({ config, startedAt, className, trackClassName, zoneClassName, markerClassName, renderStatus, }: {
88
102
  config: SkillCheckConfig;
89
103
  startedAt: number;
@@ -106,6 +120,12 @@ export declare function CaptureOdds({ chance, className, fillClassName, }: {
106
120
  className?: string;
107
121
  fillClassName?: string;
108
122
  }): import("react").JSX.Element;
123
+ /**
124
+ * Render `ctx.game.feed`'s entries for `action` as a newest-first toast stack — the feed-backed sibling of
125
+ * `@jgengine/core/game/toasts`' `createToastQueue`. Reach for this when the message source is already an engine
126
+ * event bound onto `ctx.game.feed` (kill feed, quest updates, loot log); reach for a `createToastQueue` when the
127
+ * game raises ad-hoc messages that need their own TTL independent of the feed's ring buffer.
128
+ */
109
129
  export declare function ToastStack({ action, limit, className, renderToast, }: {
110
130
  action: string;
111
131
  limit?: number;
@@ -3,8 +3,10 @@ import { useEffect, useRef, useState } from "react";
3
3
  import { evaluateSkillCheck, } from "@jgengine/core/interaction/skillCheck";
4
4
  import { pendingQteStep } from "@jgengine/core/interaction/qte";
5
5
  import { rollCheck } from "@jgengine/core/stats/rollCheck";
6
+ import { dialogueSlot } from "@jgengine/core/game/dialogue";
6
7
  import { useGameContext } from "./provider.js";
7
8
  import { useCurrency, useEntityStat, useFeed, useInventory, useLocalPlayerDead } from "./hooks.js";
9
+ import { useStore } from "./store.js";
8
10
  import { paintQteStepDom, paintSkillCheckDom } from "./skillCheckPaint.js";
9
11
  export { paintQteStepDom, paintSkillCheckDom } from "./skillCheckPaint.js";
10
12
  export function SlotGrid({ inventoryId, className, renderSlot, }) {
@@ -30,7 +32,7 @@ export function CurrencyPill({ currencyId, className }) {
30
32
  export function ProximityPrompt({ prompt, className, }) {
31
33
  const display = prompt.display;
32
34
  if (display.kind === "keybind") {
33
- return (_jsx("span", { className: className, "data-prompt": "keybind", "data-action": display.actionId, children: _jsx("kbd", { children: display.actionId }) }));
35
+ return (_jsxs("span", { className: className, "data-prompt": "keybind", "data-action": display.actionId, children: [_jsx("kbd", { children: display.actionId }), display.label !== undefined ? _jsx("span", { "data-prompt-label": true, children: display.label }) : null] }));
34
36
  }
35
37
  if (display.kind === "gauge") {
36
38
  return (_jsx("span", { className: className, "data-prompt": "gauge", "data-gauge": display.gaugeId, children: display.gaugeId }));
@@ -55,6 +57,26 @@ export function DialogueBox({ dialogue, onChoice, rng, className, lineClassName,
55
57
  ? ` (${choice.check.advantage})`
56
58
  : ""] }))] }, choice.label))) }, index)) : (_jsxs("p", { className: lineClassName, children: [_jsx("span", { className: speakerClassName, "data-speaker": true, children: line.speaker }), _jsx("span", { children: line.text })] }, index))) }));
57
59
  }
60
+ /**
61
+ * The id of the dialogue `ctx.game.dialogue` (or a `talkable(id)` prompt) currently has open, or `null`.
62
+ * The read side of the `features.dialogue` bridge — a panel looks the id up in its own dialogue catalog and
63
+ * renders {@link DialogueBox}, with no per-game open/close store.
64
+ */
65
+ export function useOpenDialogueId() {
66
+ return useStore(dialogueSlot, (value) => value ?? null);
67
+ }
68
+ /**
69
+ * Route a {@link DialogueBox} choice through the `features.dialogue` bridge: resolve the choice's invoke
70
+ * (honoring a skill-check `result`), run that command, and otherwise close the dialogue — the write side that
71
+ * replaces a per-game `onChoice` that hand-rolls `resolveDialogueInvoke` + `dialogue.close`.
72
+ */
73
+ export function runDialogueChoice(commands, choice, result) {
74
+ const invoke = resolveDialogueInvoke(choice, result);
75
+ if (invoke !== null && invoke !== undefined)
76
+ commands.run(invoke.command, invoke.args ?? {});
77
+ else
78
+ commands.run("dialogue.close", {});
79
+ }
58
80
  export function SkillCheckBar({ config, startedAt, className, trackClassName, zoneClassName, markerClassName, renderStatus, }) {
59
81
  const ctx = useGameContext();
60
82
  const rootRef = useRef(null);
@@ -119,6 +141,12 @@ function defaultToast(entry) {
119
141
  }
120
142
  return typeof entry.data === "string" ? entry.data : JSON.stringify(entry.data);
121
143
  }
144
+ /**
145
+ * Render `ctx.game.feed`'s entries for `action` as a newest-first toast stack — the feed-backed sibling of
146
+ * `@jgengine/core/game/toasts`' `createToastQueue`. Reach for this when the message source is already an engine
147
+ * event bound onto `ctx.game.feed` (kill feed, quest updates, loot log); reach for a `createToastQueue` when the
148
+ * game raises ad-hoc messages that need their own TTL independent of the feed's ring buffer.
149
+ */
122
150
  export function ToastStack({ action, limit = 4, className, renderToast, }) {
123
151
  const entries = useFeed({ action, limit });
124
152
  if (entries.length === 0)
package/dist/display.d.ts CHANGED
@@ -11,4 +11,5 @@ export interface DisplayProfile {
11
11
  compact: boolean;
12
12
  portrait: boolean;
13
13
  }
14
+ /** @internal */
14
15
  export declare function useDisplayProfile(): DisplayProfile;
package/dist/display.js CHANGED
@@ -40,6 +40,7 @@ function subscribe(onChange) {
40
40
  list.removeEventListener("change", onChange);
41
41
  };
42
42
  }
43
+ /** @internal */
43
44
  export function useDisplayProfile() {
44
45
  return useSyncExternalStore(subscribe, readProfile, () => SERVER_PROFILE);
45
46
  }
@@ -5,6 +5,9 @@ export interface ReadableEngineStore<TState> {
5
5
  export interface EventfulEngineStore<TEventMap extends object> {
6
6
  on<K extends keyof TEventMap>(eventName: K, listener: (payload: TEventMap[K]) => void): () => void;
7
7
  }
8
+ /** @internal */
8
9
  export declare function useEngineState<TState>(store: ReadableEngineStore<TState>): TState;
10
+ /** @internal */
9
11
  export declare function useEngineStore<TState, TSelected>(store: ReadableEngineStore<TState>, selector: (state: TState) => TSelected, isEqual?: (previous: TSelected, next: TSelected) => boolean): TSelected;
12
+ /** @internal */
10
13
  export declare function useEngineEvent<TEventMap extends object, K extends keyof TEventMap>(store: EventfulEngineStore<TEventMap>, eventName: K, handler: (payload: TEventMap[K]) => void): void;
@@ -1,10 +1,12 @@
1
1
  import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
2
2
  import { createSelectCache, readSelectSnapshot } from "./selectSnapshot.js";
3
+ /** @internal */
3
4
  export function useEngineState(store) {
4
5
  const subscribe = useCallback((onChange) => store.subscribe(() => onChange()), [store]);
5
6
  const getSnapshot = useCallback(() => store.getState(), [store]);
6
7
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
7
8
  }
9
+ /** @internal */
8
10
  export function useEngineStore(store, selector, isEqual = Object.is) {
9
11
  const selectorRef = useRef(selector);
10
12
  selectorRef.current = selector;
@@ -15,6 +17,7 @@ export function useEngineStore(store, selector, isEqual = Object.is) {
15
17
  const getSnapshot = useCallback(() => readSelectSnapshot(cacheRef.current, store.getState(), (state) => selectorRef.current(state), (previous, next) => isEqualRef.current(previous, next)), [store]);
16
18
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
17
19
  }
20
+ /** @internal */
18
21
  export function useEngineEvent(store, eventName, handler) {
19
22
  const handlerRef = useRef(handler);
20
23
  handlerRef.current = handler;
@@ -10,11 +10,14 @@ export interface FogPaintSurface {
10
10
  fillRect(x: number, y: number, width: number, height: number): void;
11
11
  clearRect(x: number, y: number, width: number, height: number): void;
12
12
  }
13
+ /** @internal */
13
14
  export declare function forEachUnrevealedFogCell(fog: FogCells, visit: (col: number, row: number) => void): number;
15
+ /** @internal */
14
16
  export declare function paintFogOverlay(surface: FogPaintSurface, canvasSize: {
15
17
  width: number;
16
18
  height: number;
17
19
  }, fog: FogCells, cellRect: (col: number, row: number) => FogCellRect | null, fill?: string): number;
20
+ /** @internal */
18
21
  export declare function createFogDataUrl(fog: FogCells, canvasSize: {
19
22
  width: number;
20
23
  height: number;
@@ -1,3 +1,4 @@
1
+ /** @internal */
1
2
  export function forEachUnrevealedFogCell(fog, visit) {
2
3
  let count = 0;
3
4
  for (let row = 0; row < fog.rows; row += 1) {
@@ -10,6 +11,7 @@ export function forEachUnrevealedFogCell(fog, visit) {
10
11
  }
11
12
  return count;
12
13
  }
14
+ /** @internal */
13
15
  export function paintFogOverlay(surface, canvasSize, fog, cellRect, fill = "rgba(11, 15, 20, 0.82)") {
14
16
  surface.clearRect(0, 0, canvasSize.width, canvasSize.height);
15
17
  surface.fillStyle = fill;
@@ -20,6 +22,7 @@ export function paintFogOverlay(surface, canvasSize, fog, cellRect, fill = "rgba
20
22
  surface.fillRect(rect.x, rect.y, rect.width, rect.height);
21
23
  });
22
24
  }
25
+ /** @internal */
23
26
  export function createFogDataUrl(fog, canvasSize, cellRect, fill = "rgba(11, 15, 20, 0.82)") {
24
27
  if (typeof document === "undefined")
25
28
  return null;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Shared mobile-composition runtime. `GameViewportProvider` allocates the live
3
+ * game viewport once — tracking `visualViewport`, safe-area insets, orientation
4
+ * and layout mode — and hosts a registry every UI subsystem publishes its
5
+ * occupied rectangle to. HUD panels, touch-control zones, and system chrome all
6
+ * register here so the engine can detect forbidden overlaps instead of each
7
+ * subsystem independently claiming an edge.
8
+ */
9
+ import { type CSSProperties, type ReactNode, type RefObject } from "react";
10
+ import { type GameLayoutMode, type GameViewportLayout, type LayoutCollision, type LayoutRect, type LayoutRegion } from "@jgengine/core/ui/gameLayout";
11
+ import type { LayoutOrientation } from "@jgengine/core/ui/orientation";
12
+ import type { HudPlatform } from "@jgengine/core/ui/hudScale";
13
+ /** A region registration: the full `LayoutRegion` plus the live element (dev outlining), rect measured by the shell/react side. */
14
+ export interface RegionRecord extends LayoutRegion {
15
+ element?: HTMLElement | null;
16
+ }
17
+ /** A region descriptor without its measured rectangle — the caller supplies geometry through `useRegisterLayoutRegion`. */
18
+ export type LayoutRegionSpec = Omit<LayoutRegion, "rect">;
19
+ /** Live viewport rectangles: the layout viewport and the visible `visualViewport`. */
20
+ export interface ViewportMetrics {
21
+ layout: LayoutRect;
22
+ visual: LayoutRect;
23
+ }
24
+ /** Live visible viewport, tracking `window.visualViewport` (mobile browser chrome, pinch-zoom) with a layout-viewport fallback. */
25
+ export declare function useViewportMetrics(): ViewportMetrics;
26
+ /**
27
+ * Provides the shared game viewport layout to everything it wraps. Mount it
28
+ * once around the whole game presentation (world, HUD, controls, system UI) so
29
+ * every subsystem reads one coordinated geometry and registers its rect for
30
+ * collision detection. Publishes live `--jg-viewport-*` / `--jg-visual-viewport-*`
31
+ * / `--jg-safe-*` CSS variables on its root.
32
+ */
33
+ export declare function GameViewportProvider({ platforms, className, style, children, }: {
34
+ platforms?: readonly HudPlatform[];
35
+ className?: string;
36
+ style?: CSSProperties;
37
+ children?: ReactNode;
38
+ }): import("react").JSX.Element;
39
+ /** The live shared viewport layout. Returns a neutral default outside a `GameViewportProvider` so it never throws in previews. */
40
+ export declare function useGameViewportLayout(): GameViewportLayout;
41
+ /** The resolved explicit composition mode (`desktop-wide` … `mobile-portrait`). */
42
+ export declare function useGameLayoutMode(): GameLayoutMode;
43
+ /** The live device orientation. */
44
+ export declare function useGameOrientation(): LayoutOrientation;
45
+ /** Rectangles reserved by touch controls and system UI — HUD placement should avoid these. */
46
+ export declare function useReservedControlZones(): readonly LayoutRect[];
47
+ /** Live forbidden/warned region collisions (empty outside a provider). */
48
+ export declare function useLayoutCollisions(): readonly LayoutCollision[];
49
+ /**
50
+ * Register the element behind `ref` as a layout region and keep its measured
51
+ * rectangle live (ResizeObserver + viewport changes). No-op outside a
52
+ * `GameViewportProvider`, so a component using it still works in isolation.
53
+ */
54
+ export declare function useRegisterLayoutRegion(spec: LayoutRegionSpec, ref: RefObject<HTMLElement | null>, enabled?: boolean): void;