@jgengine/react 0.9.0 → 0.10.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.
@@ -1,5 +1,9 @@
1
1
  import { type CSSProperties, type ReactNode } from "react";
2
2
  import { type HudAnchor, type HudLayoutStore } from "@jgengine/core/ui/hudLayout";
3
+ import { type HudPriority, type MobileHudBehavior } from "@jgengine/core/ui/gameLayout";
4
+ import type { GamePhase } from "@jgengine/core/game/gamePhase";
5
+ /** Whether a HUD element opted into `showDuring` is visible in the current phase; `undefined` = always visible (default). */
6
+ export declare function hudVisibleInPhase(showDuring: readonly GamePhase[] | undefined, phase: GamePhase): boolean;
3
7
  /**
4
8
  * How a panel behaves on compact (phone-scale) displays. `keep` stays visible
5
9
  * at the global compact scale, `chip` collapses to a small tap-to-expand
@@ -22,15 +26,17 @@ export declare function useHudLayout(options?: {
22
26
  * (`--jg-hud-dock-clearance`), or device safe areas. On compact displays the
23
27
  * whole surface scales down and each panel applies its `compact` behavior.
24
28
  */
25
- export declare function HudCanvas({ layout, editChord, compactScale, className, style, children, }: {
29
+ export declare function HudCanvas({ layout, editChord, compactScale, showDuring, className, style, children, }: {
26
30
  layout: HudLayoutStore;
27
31
  editChord?: HudEditChord | false;
28
32
  /** Zoom applied to the whole HUD on compact displays. Default 0.85. */
29
33
  compactScale?: number;
34
+ /** Opt-in play-phase gate: render the HUD only during these phases (e.g. `["playing"]`). Omit for always-visible (default). */
35
+ showDuring?: readonly GamePhase[];
30
36
  className?: string;
31
37
  style?: CSSProperties;
32
38
  children?: ReactNode;
33
- }): import("react").JSX.Element;
39
+ }): import("react").JSX.Element | null;
34
40
  /**
35
41
  * A HUD block that lives in one of the nine anchor regions. Panels sharing a
36
42
  * region stack outward from the screen edge in ascending `order`. On fine
@@ -38,7 +44,7 @@ export declare function HudCanvas({ layout, editChord, compactScale, className,
38
44
  * leaves the flow and keeps its custom placement. On compact displays custom
39
45
  * placements are ignored and the `compact` behavior applies.
40
46
  */
41
- export declare function HudPanel({ id, anchor, order, compact: compactMode, chip, interactive, inset, locked, className, style, children, }: {
47
+ export declare function HudPanel({ id, anchor, order, compact: compactMode, chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region, className, style, children, }: {
42
48
  id: string;
43
49
  anchor?: HudAnchor;
44
50
  /** Stack position within the region, ascending outward from the screen edge. Default 0. */
@@ -55,6 +61,18 @@ export declare function HudPanel({ id, anchor, order, compact: compactMode, chip
55
61
  y: number;
56
62
  };
57
63
  locked?: boolean;
64
+ /** Opt-in play-phase gate: render this panel only during these phases. Omit for always-visible (default). */
65
+ showDuring?: readonly GamePhase[];
66
+ /** Gameplay-importance tier, surfaced to layout tooling. `critical` stays visible; `tertiary` is the first to fold away. */
67
+ priority?: HudPriority;
68
+ /** How the panel adapts on mobile. `"hidden"` unmounts on phones; `"transient"` softens its collision policy; others tag the element for game CSS. */
69
+ mobileBehavior?: MobileHudBehavior;
70
+ /** Region ids this panel may overlap without a forbidden-collision report. */
71
+ allowOverlapWith?: readonly string[];
72
+ /** Regions sharing a non-empty group never collide with each other. */
73
+ collisionGroup?: string;
74
+ /** Register this panel as a collision-tracked layout region. Default true. */
75
+ region?: boolean;
58
76
  className?: string;
59
77
  style?: CSSProperties;
60
78
  children?: ReactNode;
package/dist/hudLayout.js CHANGED
@@ -3,9 +3,16 @@ import { createContext, useContext, useEffect, useMemo, useRef, useState, } from
3
3
  import { createPortal } from "react-dom";
4
4
  import { HUD_ANCHOR_FRACTIONS, anchoredPlacement, createHudLayout, isPanelDraggable, } from "@jgengine/core/ui/hudLayout";
5
5
  import { hudScaleForViewport, overflowingPanels, resolveHudFit } from "@jgengine/core/ui/hudScale";
6
+ import { isMobileMode, } from "@jgengine/core/ui/gameLayout";
6
7
  import { useDisplayProfile } from "./display.js";
7
8
  import { useEngineState } from "./engineStore.js";
9
+ import { useGameLayoutMode, useRegisterLayoutRegion } from "./gameViewport.js";
10
+ import { useOptionalGamePhase } from "./hooks.js";
8
11
  import { useHudViewport } from "./hudViewport.js";
12
+ /** Whether a HUD element opted into `showDuring` is visible in the current phase; `undefined` = always visible (default). */
13
+ export function hudVisibleInPhase(showDuring, phase) {
14
+ return showDuring === undefined || showDuring.includes(phase);
15
+ }
9
16
  const STORAGE_PREFIX = "jg:hud:";
10
17
  const DRAG_THRESHOLD_PX = 4;
11
18
  const PERSIST_DELAY_MS = 200;
@@ -125,8 +132,9 @@ function edgePad(envInset, extra, basePx, scale) {
125
132
  * (`--jg-hud-dock-clearance`), or device safe areas. On compact displays the
126
133
  * whole surface scales down and each panel applies its `compact` behavior.
127
134
  */
128
- export function HudCanvas({ layout, editChord, compactScale, className, style, children, }) {
135
+ export function HudCanvas({ layout, editChord, compactScale, showDuring, className, style, children, }) {
129
136
  const canvasRef = useRef(null);
137
+ const phase = useOptionalGamePhase();
130
138
  const { compact } = useDisplayProfile();
131
139
  const hudViewport = useHudViewport();
132
140
  const fitEnabled = hudViewport?.fitEnabled === true;
@@ -286,6 +294,8 @@ export function HudCanvas({ layout, editChord, compactScale, className, style, c
286
294
  "--jg-hud-pad-right": edgePad("env(safe-area-inset-right, 0px)", null, basePad, scale),
287
295
  ...style,
288
296
  };
297
+ if (!hudVisibleInPhase(showDuring, phase))
298
+ return null;
289
299
  return (_jsx(HudCanvasContext.Provider, { value: value, children: _jsxs("div", { ref: canvasRef, "data-hud-canvas": "", "data-hud-editing": editing ? "" : undefined, className: className, style: canvasStyle, children: [HUD_ANCHORS.map((anchor) => (_jsx("div", { ref: regionRefs[anchor], "data-hud-region": anchor, style: {
290
300
  position: "absolute",
291
301
  display: "flex",
@@ -334,11 +344,14 @@ function HudChip({ label, anchor, children, }) {
334
344
  * leaves the flow and keeps its custom placement. On compact displays custom
335
345
  * placements are ignored and the `compact` behavior applies.
336
346
  */
337
- export function HudPanel({ id, anchor = "top-left", order, compact: compactMode = "keep", chip, interactive, inset, locked, className, style, children, }) {
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, }) {
338
348
  const ctx = useContext(HudCanvasContext);
349
+ const phase = useOptionalGamePhase();
350
+ const layoutMode = useGameLayoutMode();
339
351
  if (ctx === null)
340
352
  throw new Error("HudPanel must be rendered inside a HudCanvas");
341
353
  const { layout, canvasRef, regions, compact } = ctx;
354
+ const onMobile = isMobileMode(layoutMode);
342
355
  const registeredOnRef = useRef(null);
343
356
  if (registeredOnRef.current !== layout) {
344
357
  registeredOnRef.current = layout;
@@ -354,6 +367,19 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
354
367
  const detachRef = useRef(null);
355
368
  const [dragging, setDragging] = useState(false);
356
369
  useEffect(() => () => detachRef.current?.(), []);
370
+ const regionHidden = panel === undefined ||
371
+ (compact && compactMode === "hide") ||
372
+ (onMobile && mobileBehavior === "hidden") ||
373
+ !hudVisibleInPhase(showDuring, phase);
374
+ const regionSpec = useMemo(() => ({
375
+ id,
376
+ kind: "hud",
377
+ collisionPolicy: (mobileBehavior === "transient" ? "warn" : "forbid"),
378
+ priority,
379
+ allowOverlapWith,
380
+ collisionGroup,
381
+ }), [id, mobileBehavior, priority, allowOverlapWith, collisionGroup]);
382
+ useRegisterLayoutRegion(regionSpec, rootRef, region && !regionHidden);
357
383
  const onPointerDown = (event) => {
358
384
  if (!draggable || event.button !== 0)
359
385
  return;
@@ -437,6 +463,10 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
437
463
  return null;
438
464
  if (compact && compactMode === "hide")
439
465
  return null;
466
+ if (onMobile && mobileBehavior === "hidden")
467
+ return null;
468
+ if (!hudVisibleInPhase(showDuring, phase))
469
+ return null;
440
470
  const flow = compact || !panel.moved;
441
471
  if (flow) {
442
472
  const regionEl = regions[compact ? anchor : panel.placement.anchor];
@@ -444,7 +474,7 @@ export function HudPanel({ id, anchor = "top-left", order, compact: compactMode
444
474
  return null;
445
475
  const chipped = compact && compactMode === "chip";
446
476
  const passThrough = interactive === false && !editing && !chipped;
447
- return createPortal(_jsxs("div", { ref: rootRef, "data-hud-panel": id, "data-dragging": dragging ? "" : undefined, className: className, onPointerDown: onPointerDown, onClickCapture: onClickCapture, style: {
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: {
448
478
  position: "relative",
449
479
  order: order ?? 0,
450
480
  pointerEvents: passThrough ? "none" : "auto",
package/dist/index.d.ts CHANGED
@@ -11,6 +11,8 @@ export * from "./engineStore.js";
11
11
  export * from "./dragLayer.js";
12
12
  export * from "./hudLayout.js";
13
13
  export * from "./hudViewport.js";
14
+ export * from "./gameViewport.js";
15
+ export * from "./rotateDevice.js";
14
16
  export * from "./display.js";
15
17
  export * from "./map.js";
16
18
  export * from "./preview.js";
package/dist/index.js CHANGED
@@ -11,6 +11,8 @@ export * from "./engineStore.js";
11
11
  export * from "./dragLayer.js";
12
12
  export * from "./hudLayout.js";
13
13
  export * from "./hudViewport.js";
14
+ export * from "./gameViewport.js";
15
+ export * from "./rotateDevice.js";
14
16
  export * from "./display.js";
15
17
  export * from "./map.js";
16
18
  export * from "./preview.js";
@@ -5,3 +5,5 @@ export declare function GameProvider({ context, children }: {
5
5
  children?: ReactNode;
6
6
  }): import("react").JSX.Element;
7
7
  export declare function useGameContext(): GameContext;
8
+ /** The game context if a `GameProvider` is present, otherwise `null` — for chrome that may render outside a running game (showcases, previews). */
9
+ export declare function useOptionalGameContext(): GameContext | null;
package/dist/provider.js CHANGED
@@ -10,3 +10,7 @@ export function useGameContext() {
10
10
  throw new Error("useGameContext must be used within <GameProvider>");
11
11
  return ctx;
12
12
  }
13
+ /** The game context if a `GameProvider` is present, otherwise `null` — for chrome that may render outside a running game (showcases, previews). */
14
+ export function useOptionalGameContext() {
15
+ return useContext(GameReactContext);
16
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Engine-owned rotate-device screen. A polished, non-dismissible full-viewport
3
+ * gate shown when a game requires an orientation the device isn't in. It owns
4
+ * the visible viewport (visualViewport-sized), respects safe areas, blocks all
5
+ * pointer/touch input to the game beneath, and is themeable through `--jg-*`
6
+ * tokens. Reduced-motion respected. This is game UI, not a website toast.
7
+ */
8
+ import type { CSSProperties, ReactNode } from "react";
9
+ import type { LayoutOrientation } from "@jgengine/core/ui/orientation";
10
+ /** Full-viewport, non-dismissible rotate-device gate shown when a game requires an orientation the device isn't in. */
11
+ export declare function RotateDeviceScreen({ title, description, requiredOrientation, accent, icon, className, style, }: {
12
+ /** Short headline. */
13
+ title?: string;
14
+ /** One concise explanatory line. Defaults from `requiredOrientation`. */
15
+ description?: string;
16
+ /** Orientation the game needs. Drives the rotation animation direction. */
17
+ requiredOrientation?: LayoutOrientation;
18
+ /** Illustration/token color. */
19
+ accent?: string;
20
+ /** Custom illustration; defaults to an animated phone glyph. */
21
+ icon?: ReactNode;
22
+ className?: string;
23
+ style?: CSSProperties;
24
+ }): import("react").JSX.Element;
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ const STYLE_ID = "jg-rotate-device-style";
3
+ const ROTATE_CSS = `
4
+ .jg-rotate-root {
5
+ position: fixed;
6
+ inset: 0;
7
+ z-index: 2147483000;
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ width: var(--jg-visual-viewport-width, 100vw);
12
+ height: var(--jg-visual-viewport-height, 100dvh);
13
+ min-height: var(--jg-visual-viewport-height, 100vh);
14
+ padding: calc(env(safe-area-inset-top, 0px) + 24px) calc(env(safe-area-inset-right, 0px) + 24px)
15
+ calc(env(safe-area-inset-bottom, 0px) + 24px) calc(env(safe-area-inset-left, 0px) + 24px);
16
+ color: var(--jg-text, #f4f4f6);
17
+ background:
18
+ radial-gradient(120% 120% at 50% 0%, var(--jg-accent-dim, rgba(120, 130, 160, 0.18)), transparent 60%),
19
+ var(--jg-surface, #0b0d13);
20
+ touch-action: none;
21
+ user-select: none;
22
+ -webkit-user-select: none;
23
+ overflow: hidden;
24
+ }
25
+ .jg-rotate-inner {
26
+ display: flex;
27
+ flex-direction: column;
28
+ align-items: center;
29
+ gap: 22px;
30
+ text-align: center;
31
+ max-width: 22rem;
32
+ }
33
+ .jg-rotate-phone {
34
+ transform-origin: 50% 50%;
35
+ }
36
+ .jg-rotate-title {
37
+ margin: 0;
38
+ font: 700 clamp(1.25rem, 5vw, 1.75rem) / 1.15 system-ui, -apple-system, "Segoe UI", sans-serif;
39
+ letter-spacing: -0.01em;
40
+ }
41
+ .jg-rotate-desc {
42
+ margin: 0;
43
+ font: 400 clamp(0.9rem, 3.4vw, 1.05rem) / 1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
44
+ color: var(--jg-text-dim, rgba(244, 244, 246, 0.66));
45
+ }
46
+ @media (prefers-reduced-motion: no-preference) {
47
+ .jg-rotate-phone--to-landscape { animation: jg-rotate-to-landscape 2.6s cubic-bezier(0.65, 0, 0.35, 1) infinite; }
48
+ .jg-rotate-phone--to-portrait { animation: jg-rotate-to-portrait 2.6s cubic-bezier(0.65, 0, 0.35, 1) infinite; }
49
+ }
50
+ @keyframes jg-rotate-to-landscape {
51
+ 0%, 22% { transform: rotate(0deg); }
52
+ 55%, 78% { transform: rotate(-90deg); }
53
+ 100% { transform: rotate(-90deg); }
54
+ }
55
+ @keyframes jg-rotate-to-portrait {
56
+ 0%, 22% { transform: rotate(-90deg); }
57
+ 55%, 78% { transform: rotate(0deg); }
58
+ 100% { transform: rotate(0deg); }
59
+ }
60
+ `;
61
+ function ensureStyle() {
62
+ if (typeof document === "undefined")
63
+ return;
64
+ if (document.getElementById(STYLE_ID) !== null)
65
+ return;
66
+ const el = document.createElement("style");
67
+ el.id = STYLE_ID;
68
+ el.textContent = ROTATE_CSS;
69
+ document.head.appendChild(el);
70
+ }
71
+ function PhoneGlyph({ accent }) {
72
+ return (_jsxs("svg", { width: "96", height: "96", viewBox: "0 0 96 96", fill: "none", "aria-hidden": true, focusable: "false", children: [_jsx("rect", { x: "34", y: "10", width: "28", height: "52", rx: "6", stroke: accent, strokeWidth: "3", fill: "none" }), _jsx("rect", { x: "39", y: "18", width: "18", height: "34", rx: "2", fill: accent, opacity: "0.22" }), _jsx("circle", { cx: "48", cy: "57.5", r: "1.8", fill: accent }), _jsx("path", { d: "M22 74a30 30 0 0 1 52 0", stroke: accent, strokeWidth: "3", strokeLinecap: "round", fill: "none", opacity: "0.5" }), _jsx("path", { d: "M70 68l6 8-10 1z", fill: accent, opacity: "0.5" })] }));
73
+ }
74
+ /** Full-viewport, non-dismissible rotate-device gate shown when a game requires an orientation the device isn't in. */
75
+ export function RotateDeviceScreen({ title = "Turn your device", description, requiredOrientation = "landscape", accent = "var(--jg-accent, #8ea2ff)", icon, className, style, }) {
76
+ ensureStyle();
77
+ const line = description ??
78
+ (requiredOrientation === "landscape"
79
+ ? "Rotate to landscape to play."
80
+ : "Rotate to portrait to play.");
81
+ const spin = requiredOrientation === "landscape" ? "jg-rotate-phone--to-landscape" : "jg-rotate-phone--to-portrait";
82
+ return (_jsx("div", { className: className === undefined ? "jg-rotate-root" : `jg-rotate-root ${className}`, style: style, "data-jg-orientation-gate": requiredOrientation, role: "dialog", "aria-modal": "true", "aria-label": title, onPointerDownCapture: (e) => e.stopPropagation(), onTouchStartCapture: (e) => e.stopPropagation(), children: _jsxs("div", { className: "jg-rotate-inner", children: [_jsx("div", { className: `jg-rotate-phone ${spin}`, children: icon ?? _jsx(PhoneGlyph, { accent: accent }) }), _jsx("h2", { className: "jg-rotate-title", children: title }), _jsx("p", { className: "jg-rotate-desc", children: line })] }) }));
83
+ }
@@ -8,6 +8,7 @@ export declare function SettingsProvider({ store, children }: {
8
8
  export declare function useSettingsStore(): SettingsStore;
9
9
  /** Read + write one persisted setting; re-renders when the value changes anywhere. */
10
10
  export declare function useSetting<T extends SettingValue>(id: string, fallback: T): readonly [T, (value: SettingValue) => void];
11
+ /** One editable setting rendered in a settings menu category. */
11
12
  export interface SettingsRow {
12
13
  id: string;
13
14
  label: string;
@@ -20,6 +21,7 @@ export interface SettingsRow {
20
21
  format?: (value: number) => string;
21
22
  set: (value: SettingValue) => void;
22
23
  }
24
+ /** One rebindable action row rendered in the controls settings category. */
23
25
  export interface SettingsKeybindRow {
24
26
  action: string;
25
27
  label: string;
@@ -28,6 +30,7 @@ export interface SettingsKeybindRow {
28
30
  rebind: (code: string) => void;
29
31
  reset: () => void;
30
32
  }
33
+ /** A settings menu category with its rows and keybinds, ready to render. */
31
34
  export interface SettingsCategoryView {
32
35
  id: SettingCategory;
33
36
  label: string;
package/dist/social.d.ts CHANGED
@@ -24,7 +24,7 @@ export declare function AddFriendButton({ toUserId, className, children, onReque
24
24
  children?: ReactNode;
25
25
  onRequested?: (requestId: string) => void;
26
26
  onRejected?: (reason: string) => void;
27
- }): import("react").JSX.Element;
27
+ }): import("react").JSX.Element | null;
28
28
  export declare function FriendRequestsList({ className, rowClassName, acceptClassName, declineClassName, emptyState, renderRequest, }: {
29
29
  className?: string;
30
30
  rowClassName?: string;
@@ -32,7 +32,7 @@ export declare function FriendRequestsList({ className, rowClassName, acceptClas
32
32
  declineClassName?: string;
33
33
  emptyState?: ReactNode;
34
34
  renderRequest?: (request: FriendRequestEntry) => ReactNode;
35
- }): import("react").JSX.Element;
35
+ }): import("react").JSX.Element | null;
36
36
  export declare function PartyMemberRow({ member, className, dotClassName, children, }: {
37
37
  member: PartyMemberEntry;
38
38
  className?: string;
@@ -70,7 +70,7 @@ export declare function InviteToWorldButton({ toUserId, target, className, child
70
70
  children?: ReactNode;
71
71
  onInvited?: (inviteId: string) => void;
72
72
  onRejected?: (reason: string) => void;
73
- }): import("react").JSX.Element;
73
+ }): import("react").JSX.Element | null;
74
74
  export declare function WorldBrowser({ listings, onJoin, className, rowClassName, joinClassName, emptyState, renderListing, }: {
75
75
  listings: readonly SessionListing[];
76
76
  onJoin: (listing: SessionListing) => void;
package/dist/social.js CHANGED
@@ -18,9 +18,12 @@ export function FriendsList({ className, rowClassName, dotClassName, emptyState,
18
18
  }
19
19
  export function AddFriendButton({ toUserId, className, children, onRequested, onRejected, }) {
20
20
  const ctx = useGameContext();
21
- const denied = ctx.game.social.friends.canRequest(ctx.player.userId, toUserId);
21
+ const social = ctx.game.social;
22
+ if (social === undefined)
23
+ return null;
24
+ const denied = social.friends.canRequest(ctx.player.userId, toUserId);
22
25
  return (_jsx("button", { type: "button", className: className, "data-add-friend": toUserId, disabled: denied !== null, title: denied?.reason, onClick: () => {
23
- const result = ctx.game.social.friends.request(ctx.player.userId, toUserId);
26
+ const result = social.friends.request(ctx.player.userId, toUserId);
24
27
  if ("reason" in result)
25
28
  onRejected?.(result.reason);
26
29
  else
@@ -30,9 +33,12 @@ export function AddFriendButton({ toUserId, className, children, onRequested, on
30
33
  export function FriendRequestsList({ className, rowClassName, acceptClassName, declineClassName, emptyState, renderRequest, }) {
31
34
  const ctx = useGameContext();
32
35
  const requests = useFriendRequests();
36
+ const social = ctx.game.social;
37
+ if (social === undefined)
38
+ return null;
33
39
  return (_jsx("div", { className: className, "data-friend-requests": true, children: requests.length === 0
34
40
  ? emptyState ?? null
35
- : requests.map((request) => renderRequest !== undefined ? (renderRequest(request)) : (_jsxs("div", { className: rowClassName, "data-friend-request": request.requestId, children: [_jsx("span", { "data-request-from": true, children: request.fromUserId }), _jsx("button", { type: "button", className: acceptClassName, "data-accept": true, onClick: () => ctx.game.social.friends.accept(ctx.player.userId, request.requestId), children: "Accept" }), _jsx("button", { type: "button", className: declineClassName, "data-decline": true, onClick: () => ctx.game.social.friends.decline(ctx.player.userId, request.requestId), children: "Decline" })] }, request.requestId))) }));
41
+ : requests.map((request) => renderRequest !== undefined ? (renderRequest(request)) : (_jsxs("div", { className: rowClassName, "data-friend-request": request.requestId, children: [_jsx("span", { "data-request-from": true, children: request.fromUserId }), _jsx("button", { type: "button", className: acceptClassName, "data-accept": true, onClick: () => social.friends.accept(ctx.player.userId, request.requestId), children: "Accept" }), _jsx("button", { type: "button", className: declineClassName, "data-decline": true, onClick: () => social.friends.decline(ctx.player.userId, request.requestId), children: "Decline" })] }, request.requestId))) }));
36
42
  }
37
43
  export function PartyMemberRow({ member, className, dotClassName, children, }) {
38
44
  return (_jsxs("div", { className: className, "data-party-member": member.userId, "data-role": member.role, children: [_jsx(PresenceDot, { userId: member.userId, className: dotClassName }), _jsx("span", { "data-member-name": true, children: member.userId }), children] }));
@@ -46,33 +52,45 @@ export function PartyFrame({ className, rowClassName, dotClassName, emptyState,
46
52
  export function PartyInviteToast({ className, acceptClassName, declineClassName, renderInvite, }) {
47
53
  const ctx = useGameContext();
48
54
  const invites = usePartyInvites();
55
+ const social = ctx.game.social;
56
+ if (social === undefined)
57
+ return null;
49
58
  if (invites.length === 0)
50
59
  return null;
51
- return (_jsx("div", { className: className, "data-party-invites": true, children: invites.map((invite) => renderInvite !== undefined ? (renderInvite(invite)) : (_jsxs("div", { "data-party-invite": invite.inviteId, children: [_jsx("span", { "data-invite-from": true, children: invite.fromUserId }), _jsx("button", { type: "button", className: acceptClassName, "data-accept": true, onClick: () => ctx.game.social.party.accept(ctx.player.userId, invite.inviteId), children: "Join" }), _jsx("button", { type: "button", className: declineClassName, "data-decline": true, onClick: () => ctx.game.social.party.decline(ctx.player.userId, invite.inviteId), children: "Decline" })] }, invite.inviteId))) }));
60
+ return (_jsx("div", { className: className, "data-party-invites": true, children: invites.map((invite) => renderInvite !== undefined ? (renderInvite(invite)) : (_jsxs("div", { "data-party-invite": invite.inviteId, children: [_jsx("span", { "data-invite-from": true, children: invite.fromUserId }), _jsx("button", { type: "button", className: acceptClassName, "data-accept": true, onClick: () => social.party.accept(ctx.player.userId, invite.inviteId), children: "Join" }), _jsx("button", { type: "button", className: declineClassName, "data-decline": true, onClick: () => social.party.decline(ctx.player.userId, invite.inviteId), children: "Decline" })] }, invite.inviteId))) }));
52
61
  }
53
62
  export function LeavePartyButton({ className, children, }) {
54
63
  const ctx = useGameContext();
55
64
  const members = useParty();
65
+ const social = ctx.game.social;
66
+ if (social === undefined)
67
+ return null;
56
68
  if (members.length === 0)
57
69
  return null;
58
- return (_jsx("button", { type: "button", className: className, "data-leave-party": true, onClick: () => ctx.game.social.party.leave(ctx.player.userId), children: children ?? "Leave party" }));
70
+ return (_jsx("button", { type: "button", className: className, "data-leave-party": true, onClick: () => social.party.leave(ctx.player.userId), children: children ?? "Leave party" }));
59
71
  }
60
72
  export function WorldInviteToast({ className, acceptClassName, declineClassName, onAccepted, renderInvite, }) {
61
73
  const ctx = useGameContext();
62
74
  const invites = useWorldInvites();
75
+ const social = ctx.game.social;
76
+ if (social === undefined)
77
+ return null;
63
78
  if (invites.length === 0)
64
79
  return null;
65
80
  return (_jsx("div", { className: className, "data-world-invites": true, children: invites.map((invite) => renderInvite !== undefined ? (renderInvite(invite)) : (_jsxs("div", { "data-world-invite": invite.id, children: [_jsx("span", { "data-invite-from": true, children: invite.fromUserId }), _jsx("span", { "data-invite-server": true, children: invite.serverId }), _jsx("button", { type: "button", className: acceptClassName, "data-accept": true, onClick: () => {
66
- const result = ctx.game.social.worldInvites.accept(ctx.player.userId, invite.id);
81
+ const result = social.worldInvites.accept(ctx.player.userId, invite.id);
67
82
  if ("target" in result)
68
83
  onAccepted(result.target);
69
- }, children: "Join" }), _jsx("button", { type: "button", className: declineClassName, "data-decline": true, onClick: () => ctx.game.social.worldInvites.decline(ctx.player.userId, invite.id), children: "Decline" })] }, invite.id))) }));
84
+ }, children: "Join" }), _jsx("button", { type: "button", className: declineClassName, "data-decline": true, onClick: () => social.worldInvites.decline(ctx.player.userId, invite.id), children: "Decline" })] }, invite.id))) }));
70
85
  }
71
86
  export function InviteToWorldButton({ toUserId, target, className, children, onInvited, onRejected, }) {
72
87
  const ctx = useGameContext();
73
- const denied = ctx.game.social.worldInvites.canInvite(ctx.player.userId, toUserId);
88
+ const social = ctx.game.social;
89
+ if (social === undefined)
90
+ return null;
91
+ const denied = social.worldInvites.canInvite(ctx.player.userId, toUserId);
74
92
  return (_jsx("button", { type: "button", className: className, "data-invite-to-world": toUserId, disabled: denied !== null, title: denied?.reason, onClick: () => {
75
- const result = ctx.game.social.worldInvites.invite(ctx.player.userId, toUserId, target);
93
+ const result = social.worldInvites.invite(ctx.player.userId, toUserId, target);
76
94
  if ("reason" in result)
77
95
  onRejected?.(result.reason);
78
96
  else
@@ -107,10 +125,13 @@ export function QuickMatchButton({ listings, onJoin, onNoMatch, filter, classNam
107
125
  }
108
126
  export function EmoteWheel({ emotes, radius, open = true, className, emoteClassName, onPlayed, onRejected, renderEmote, }) {
109
127
  const ctx = useGameContext();
128
+ const social = ctx.game.social;
129
+ if (social === undefined)
130
+ return null;
110
131
  if (!open)
111
132
  return null;
112
133
  return (_jsx("div", { className: className, role: "menu", "data-emote-wheel": true, "data-emote-count": emotes.length, children: emotes.map((emoteId, index) => (_jsx("button", { type: "button", role: "menuitem", className: emoteClassName, "data-emote": emoteId, "data-emote-index": index, onClick: () => {
113
- const result = ctx.game.social.emotes.play(ctx.player.userId, emoteId, radius);
134
+ const result = social.emotes.play(ctx.player.userId, emoteId, radius);
114
135
  if ("reason" in result)
115
136
  onRejected?.(result.reason);
116
137
  else