@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.
- package/CHANGELOG.md +21 -1
- package/dist/chat.d.ts +3 -3
- package/dist/chat.js +14 -5
- package/dist/chatBubbles.js +2 -1
- package/dist/components.js +1 -1
- package/dist/gameViewport.d.ts +54 -0
- package/dist/gameViewport.js +340 -0
- package/dist/hooks.d.ts +2 -0
- package/dist/hooks.js +26 -10
- package/dist/hudLayout.d.ts +21 -3
- package/dist/hudLayout.js +33 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +4 -0
- package/dist/rotateDevice.d.ts +24 -0
- package/dist/rotateDevice.js +83 -0
- package/dist/settings.d.ts +3 -0
- package/dist/social.d.ts +3 -3
- package/dist/social.js +31 -10
- package/llms.txt +111 -20
- package/package.json +2 -2
package/dist/hudLayout.d.ts
CHANGED
|
@@ -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";
|
package/dist/provider.d.ts
CHANGED
|
@@ -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
|
+
}
|
package/dist/settings.d.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
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: () =>
|
|
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: () =>
|
|
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: () =>
|
|
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 =
|
|
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: () =>
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|