@seriousgaming/sdk 0.0.0-stage → 1.0.0-alpha.2
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/LICENSE +202 -0
- package/NOTICE +1 -0
- package/README.md +33 -2
- package/lib/build/callbacks.d.ts +998 -0
- package/lib/build/callbacks.js +32 -0
- package/lib/build/context-merge.d.ts +59 -0
- package/lib/build/context-merge.js +118 -0
- package/lib/build/document.d.ts +36 -0
- package/lib/build/document.js +8 -0
- package/lib/build/encoding.d.ts +26 -0
- package/lib/build/encoding.js +78 -0
- package/lib/build/endpoints.d.ts +421 -0
- package/lib/build/endpoints.js +46 -0
- package/lib/build/hooks.d.ts +74 -0
- package/lib/build/hooks.js +1 -0
- package/lib/build/index.d.ts +18 -0
- package/lib/build/index.js +16 -0
- package/lib/build/llm.d.ts +166 -0
- package/lib/build/llm.js +17 -0
- package/lib/build/resource-schema.d.ts +1048 -0
- package/lib/build/resource-schema.js +7 -0
- package/lib/build/scheduling.d.ts +112 -0
- package/lib/build/scheduling.js +217 -0
- package/lib/build/seed.d.ts +51 -0
- package/lib/build/seed.js +89 -0
- package/lib/build/signatures.d.ts +372 -0
- package/lib/build/signatures.js +1 -0
- package/lib/build/speech.d.ts +102 -0
- package/lib/build/speech.js +11 -0
- package/lib/build/tick-validation.d.ts +23 -0
- package/lib/build/tick-validation.js +96 -0
- package/lib/build/timer.d.ts +74 -0
- package/lib/build/timer.js +1 -0
- package/lib/build/validators.d.ts +116 -0
- package/lib/build/validators.js +1 -0
- package/lib/commands.d.ts +357 -0
- package/lib/commands.js +14 -0
- package/lib/e2e.d.ts +238 -0
- package/lib/e2e.js +248 -0
- package/lib/projection.d.ts +266 -0
- package/lib/projection.js +23 -0
- package/lib/testing/index.d.ts +13 -0
- package/lib/testing/index.js +13 -0
- package/lib/testing/logic-args.d.ts +177 -0
- package/lib/testing/logic-args.js +229 -0
- package/lib/testing/stub-services.d.ts +41 -0
- package/lib/testing/stub-services.js +176 -0
- package/lib/ui/CrossTeamDataContext.d.ts +36 -0
- package/lib/ui/CrossTeamDataContext.js +32 -0
- package/lib/ui/PresentationContext.d.ts +72 -0
- package/lib/ui/PresentationContext.js +13 -0
- package/lib/ui/PresentationWrapper.d.ts +12 -0
- package/lib/ui/PresentationWrapper.js +78 -0
- package/lib/ui/SessionUserInterfaceCallbacksContext.d.ts +3 -0
- package/lib/ui/SessionUserInterfaceCallbacksContext.js +7 -0
- package/lib/ui/SessionUserInterfaceDataContext.d.ts +103 -0
- package/lib/ui/SessionUserInterfaceDataContext.js +17 -0
- package/lib/ui/SessionUserInterfaceHookFailuresContext.d.ts +44 -0
- package/lib/ui/SessionUserInterfaceHookFailuresContext.js +15 -0
- package/lib/ui/SessionUserInterfaceHostOverlaysContext.d.ts +23 -0
- package/lib/ui/SessionUserInterfaceHostOverlaysContext.js +23 -0
- package/lib/ui/SessionUserInterfaceLocaleContext.d.ts +11 -0
- package/lib/ui/SessionUserInterfaceLocaleContext.js +13 -0
- package/lib/ui/SessionUserInterfaceMetaContext.d.ts +39 -0
- package/lib/ui/SessionUserInterfaceMetaContext.js +13 -0
- package/lib/ui/SessionUserInterfaceParticipantsContext.d.ts +12 -0
- package/lib/ui/SessionUserInterfaceParticipantsContext.js +16 -0
- package/lib/ui/SessionUserInterfaceTeamConfigurationContext.d.ts +53 -0
- package/lib/ui/SessionUserInterfaceTeamConfigurationContext.js +23 -0
- package/lib/ui/SessionUserInterfaceTeamLinksContext.d.ts +56 -0
- package/lib/ui/SessionUserInterfaceTeamLinksContext.js +12 -0
- package/lib/ui/SessionUserInterfaceTeamsContext.d.ts +13 -0
- package/lib/ui/SessionUserInterfaceTeamsContext.js +13 -0
- package/lib/ui/SessionUserInterfaceThemeContext.d.ts +9 -0
- package/lib/ui/SessionUserInterfaceThemeContext.js +7 -0
- package/lib/ui/SessionUserInterfaceThemePreferenceContext.d.ts +3 -0
- package/lib/ui/SessionUserInterfaceThemePreferenceContext.js +7 -0
- package/lib/ui/SessionUserInterfaceWalkthroughLinkContext.d.ts +42 -0
- package/lib/ui/SessionUserInterfaceWalkthroughLinkContext.js +26 -0
- package/lib/ui/SessionUserInterfaceWrapper.d.ts +8 -0
- package/lib/ui/SessionUserInterfaceWrapper.js +321 -0
- package/lib/ui/SettingsEditorCallbacksContext.d.ts +3 -0
- package/lib/ui/SettingsEditorCallbacksContext.js +7 -0
- package/lib/ui/SettingsEditorContext.d.ts +33 -0
- package/lib/ui/SettingsEditorContext.js +16 -0
- package/lib/ui/SettingsEditorLocaleContext.d.ts +13 -0
- package/lib/ui/SettingsEditorLocaleContext.js +15 -0
- package/lib/ui/SettingsEditorThemeContext.d.ts +9 -0
- package/lib/ui/SettingsEditorThemeContext.js +7 -0
- package/lib/ui/SettingsEditorThemePreferenceContext.d.ts +3 -0
- package/lib/ui/SettingsEditorThemePreferenceContext.js +7 -0
- package/lib/ui/SettingsEditorWrapper.d.ts +13 -0
- package/lib/ui/SettingsEditorWrapper.js +74 -0
- package/lib/ui/UserInterfaceCallbacksContext.d.ts +3 -0
- package/lib/ui/UserInterfaceCallbacksContext.js +7 -0
- package/lib/ui/UserInterfaceDataContext.d.ts +78 -0
- package/lib/ui/UserInterfaceDataContext.js +11 -0
- package/lib/ui/UserInterfaceLocaleContext.d.ts +13 -0
- package/lib/ui/UserInterfaceLocaleContext.js +15 -0
- package/lib/ui/UserInterfaceOwnTeamContext.d.ts +25 -0
- package/lib/ui/UserInterfaceOwnTeamContext.js +26 -0
- package/lib/ui/UserInterfaceOwnTeamJoinLinkContext.d.ts +52 -0
- package/lib/ui/UserInterfaceOwnTeamJoinLinkContext.js +13 -0
- package/lib/ui/UserInterfaceParticipantContext.d.ts +16 -0
- package/lib/ui/UserInterfaceParticipantContext.js +24 -0
- package/lib/ui/UserInterfaceParticipantsContext.d.ts +12 -0
- package/lib/ui/UserInterfaceParticipantsContext.js +13 -0
- package/lib/ui/UserInterfaceRunKindContext.d.ts +31 -0
- package/lib/ui/UserInterfaceRunKindContext.js +31 -0
- package/lib/ui/UserInterfaceTeamsContext.d.ts +12 -0
- package/lib/ui/UserInterfaceTeamsContext.js +13 -0
- package/lib/ui/UserInterfaceThemeContext.d.ts +9 -0
- package/lib/ui/UserInterfaceThemeContext.js +7 -0
- package/lib/ui/UserInterfaceThemePreferenceContext.d.ts +3 -0
- package/lib/ui/UserInterfaceThemePreferenceContext.js +7 -0
- package/lib/ui/UserInterfaceTimerContext.d.ts +10 -0
- package/lib/ui/UserInterfaceTimerContext.js +12 -0
- package/lib/ui/UserInterfaceValidationContext.d.ts +25 -0
- package/lib/ui/UserInterfaceValidationContext.js +14 -0
- package/lib/ui/UserInterfaceViewerRoleContext.d.ts +14 -0
- package/lib/ui/UserInterfaceViewerRoleContext.js +17 -0
- package/lib/ui/UserInterfaceWrapper.d.ts +8 -0
- package/lib/ui/UserInterfaceWrapper.js +219 -0
- package/lib/ui/expectedParentOrigin.d.ts +10 -0
- package/lib/ui/expectedParentOrigin.js +32 -0
- package/lib/ui/index.d.ts +50 -0
- package/lib/ui/index.js +52 -0
- package/lib/ui/internal/SurfaceGate.d.ts +19 -0
- package/lib/ui/internal/SurfaceGate.js +17 -0
- package/lib/ui/internal/SurfaceScope.d.ts +17 -0
- package/lib/ui/internal/SurfaceScope.js +16 -0
- package/lib/ui/internal/createContexts.d.ts +50 -0
- package/lib/ui/internal/createContexts.js +32 -0
- package/lib/ui/internal/document-attributes.d.ts +9 -0
- package/lib/ui/internal/document-attributes.js +87 -0
- package/lib/ui/internal/surface-timer.d.ts +3 -0
- package/lib/ui/internal/surface-timer.js +11 -0
- package/lib/ui/internal/useLatestRef.d.ts +7 -0
- package/lib/ui/internal/useLatestRef.js +11 -0
- package/lib/ui/internal/useOptimisticMerge.d.ts +19 -0
- package/lib/ui/internal/useOptimisticMerge.js +28 -0
- package/lib/ui/internal/useSurfaceControls.d.ts +52 -0
- package/lib/ui/internal/useSurfaceControls.js +19 -0
- package/lib/ui/internal/useTypedPastState.d.ts +17 -0
- package/lib/ui/internal/useTypedPastState.js +17 -0
- package/lib/ui/mutation-commit.d.ts +32 -0
- package/lib/ui/mutation-commit.js +1 -0
- package/lib/ui/mutation-outcome.d.ts +66 -0
- package/lib/ui/mutation-outcome.js +49 -0
- package/lib/ui/request-id.d.ts +18 -0
- package/lib/ui/request-id.js +44 -0
- package/lib/ui/testing.d.ts +365 -0
- package/lib/ui/testing.js +572 -0
- package/lib/ui/tick-snapshot.d.ts +28 -0
- package/lib/ui/tick-snapshot.js +34 -0
- package/lib/ui/ui-protocol.d.ts +799 -0
- package/lib/ui/ui-protocol.js +50 -0
- package/lib/ui/useAsset.d.ts +80 -0
- package/lib/ui/useAsset.js +139 -0
- package/lib/ui/useEndpointCallbacks.d.ts +31 -0
- package/lib/ui/useEndpointCallbacks.js +68 -0
- package/lib/ui/useHostChannel.d.ts +90 -0
- package/lib/ui/useHostChannel.js +338 -0
- package/lib/ui/useMutationReporter.d.ts +100 -0
- package/lib/ui/useMutationReporter.js +97 -0
- package/lib/ui/usePastState.d.ts +39 -0
- package/lib/ui/usePastState.js +63 -0
- package/lib/ui/useViewerStorage.d.ts +54 -0
- package/lib/ui/useViewerStorage.js +141 -0
- package/package.json +89 -3
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where to open the walkthrough run (§5.7).
|
|
3
|
+
*
|
|
4
|
+
* Facilitator-only, like {@link ./SessionUserInterfaceTeamLinksContext.TeamLinks},
|
|
5
|
+
* and unlike them it names no team: a walkthrough holds exactly one, so there
|
|
6
|
+
* is nothing to choose. The host sends it to the session surface and to nothing
|
|
7
|
+
* else.
|
|
8
|
+
*/
|
|
9
|
+
export type WalkthroughLink = {
|
|
10
|
+
/**
|
|
11
|
+
* The URL to open, which mounts the walkthrough's player surface.
|
|
12
|
+
*
|
|
13
|
+
* Open it in a new tab or window rather than in the session surface's own
|
|
14
|
+
* canvas: a facilitator demonstrating puts this on a projector while
|
|
15
|
+
* keeping the session in front of them.
|
|
16
|
+
*/
|
|
17
|
+
url: string;
|
|
18
|
+
};
|
|
19
|
+
export declare const SessionUserInterfaceWalkthroughLinkContext: import("react").Context<WalkthroughLink | null>;
|
|
20
|
+
/**
|
|
21
|
+
* Hook returning where to open the walkthrough, or `null` until the host has
|
|
22
|
+
* said.
|
|
23
|
+
*
|
|
24
|
+
* Every host offers a walkthrough (§5.7), so the link always arrives; `null` is
|
|
25
|
+
* only the moment before it does. Gate the control on the link, so a surface
|
|
26
|
+
* never shows a button with nowhere to go.
|
|
27
|
+
*
|
|
28
|
+
* ```tsx
|
|
29
|
+
* const walkthrough = useSessionUserInterfaceWalkthroughLink();
|
|
30
|
+
* const { resetWalkthrough } = useSessionUserInterfaceCallbacks();
|
|
31
|
+
*
|
|
32
|
+
* {walkthrough && (
|
|
33
|
+
* <>
|
|
34
|
+
* <Button href={walkthrough.url} target="_blank" rel="noreferrer">Walkthrough</Button>
|
|
35
|
+
* <Button onClick={() => void resetWalkthrough()}>Reset it</Button>
|
|
36
|
+
* </>
|
|
37
|
+
* )}
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @returns Where to open the walkthrough, or `null` before the host has said
|
|
41
|
+
*/
|
|
42
|
+
export declare const useSessionUserInterfaceWalkthroughLink: () => WalkthroughLink | null;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createDefaultedContext } from './internal/createContexts.js';
|
|
2
|
+
const { Context, useValue } = createDefaultedContext(null);
|
|
3
|
+
export const SessionUserInterfaceWalkthroughLinkContext = Context;
|
|
4
|
+
/**
|
|
5
|
+
* Hook returning where to open the walkthrough, or `null` until the host has
|
|
6
|
+
* said.
|
|
7
|
+
*
|
|
8
|
+
* Every host offers a walkthrough (§5.7), so the link always arrives; `null` is
|
|
9
|
+
* only the moment before it does. Gate the control on the link, so a surface
|
|
10
|
+
* never shows a button with nowhere to go.
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const walkthrough = useSessionUserInterfaceWalkthroughLink();
|
|
14
|
+
* const { resetWalkthrough } = useSessionUserInterfaceCallbacks();
|
|
15
|
+
*
|
|
16
|
+
* {walkthrough && (
|
|
17
|
+
* <>
|
|
18
|
+
* <Button href={walkthrough.url} target="_blank" rel="noreferrer">Walkthrough</Button>
|
|
19
|
+
* <Button onClick={() => void resetWalkthrough()}>Reset it</Button>
|
|
20
|
+
* </>
|
|
21
|
+
* )}
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @returns Where to open the walkthrough, or `null` before the host has said
|
|
25
|
+
*/
|
|
26
|
+
export const useSessionUserInterfaceWalkthroughLink = useValue;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
interface SessionUserInterfaceWrapperProps {
|
|
3
|
+
children?: ReactNode;
|
|
4
|
+
fallback?: ReactNode;
|
|
5
|
+
targetWindow?: Window;
|
|
6
|
+
}
|
|
7
|
+
declare function SessionUserInterfaceWrapper({ children, fallback, targetWindow, }: SessionUserInterfaceWrapperProps): import("react").JSX.Element;
|
|
8
|
+
export default SessionUserInterfaceWrapper;
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { applyContextUpdate } from '../build/context-merge.js';
|
|
4
|
+
import { useDocumentAttributes } from './internal/document-attributes.js';
|
|
5
|
+
import { DEFAULT_SURFACE_TIMER } from './internal/surface-timer.js';
|
|
6
|
+
import { SurfaceGate } from './internal/SurfaceGate.js';
|
|
7
|
+
import { SurfaceScope } from './internal/SurfaceScope.js';
|
|
8
|
+
import { useLatestRef } from './internal/useLatestRef.js';
|
|
9
|
+
import { useSurfaceControls } from './internal/useSurfaceControls.js';
|
|
10
|
+
import { useTypedPastState } from './internal/useTypedPastState.js';
|
|
11
|
+
import { SessionUserInterfaceCallbacksContext } from './SessionUserInterfaceCallbacksContext.js';
|
|
12
|
+
import { SessionUserInterfaceDataContext, } from './SessionUserInterfaceDataContext.js';
|
|
13
|
+
import { SessionUserInterfaceHookFailuresContext, } from './SessionUserInterfaceHookFailuresContext.js';
|
|
14
|
+
import { SessionUserInterfaceHostOverlaysContext, } from './SessionUserInterfaceHostOverlaysContext.js';
|
|
15
|
+
import { SessionUserInterfaceLocaleContext, } from './SessionUserInterfaceLocaleContext.js';
|
|
16
|
+
import { SessionUserInterfaceMetaContext, } from './SessionUserInterfaceMetaContext.js';
|
|
17
|
+
import { SessionUserInterfaceParticipantsContext } from './SessionUserInterfaceParticipantsContext.js';
|
|
18
|
+
import { DEFAULT_TEAM_CONFIGURATION, SessionUserInterfaceTeamConfigurationContext, } from './SessionUserInterfaceTeamConfigurationContext.js';
|
|
19
|
+
import { SessionUserInterfaceTeamLinksContext, } from './SessionUserInterfaceTeamLinksContext.js';
|
|
20
|
+
import { SessionUserInterfaceTeamsContext, } from './SessionUserInterfaceTeamsContext.js';
|
|
21
|
+
import { SessionUserInterfaceThemeContext, } from './SessionUserInterfaceThemeContext.js';
|
|
22
|
+
import { SessionUserInterfaceThemePreferenceContext } from './SessionUserInterfaceThemePreferenceContext.js';
|
|
23
|
+
import { SessionUserInterfaceWalkthroughLinkContext, } from './SessionUserInterfaceWalkthroughLinkContext.js';
|
|
24
|
+
import { useHostChannel } from './useHostChannel.js';
|
|
25
|
+
import { UserInterfaceRunKindContext } from './UserInterfaceRunKindContext.js';
|
|
26
|
+
/**
|
|
27
|
+
* Teams, participants and join links are pushed but do not gate first paint: a
|
|
28
|
+
* surface can render its frame around an empty roster, and holding the whole
|
|
29
|
+
* canvas back for a join link nobody has scanned yet is worse than a
|
|
30
|
+
* placeholder.
|
|
31
|
+
*
|
|
32
|
+
* `setTeamConfiguration` is the exception among the pushed values, because its
|
|
33
|
+
* default cannot be rendered honestly. A surface that paints before bounds
|
|
34
|
+
* arrive holds `max: 0`, and the obvious `teams.length < max` check then reads
|
|
35
|
+
* as 'this session is full' and disables the control it guards (#365). There is
|
|
36
|
+
* no placeholder for a bound, so the surface waits for the real one.
|
|
37
|
+
*/
|
|
38
|
+
const REQUIRED_MESSAGES = [
|
|
39
|
+
'setSessionMeta',
|
|
40
|
+
'setSessionData',
|
|
41
|
+
'setSettings',
|
|
42
|
+
'setTheme',
|
|
43
|
+
'setLocale',
|
|
44
|
+
'setAssetUrls',
|
|
45
|
+
'setTeamConfiguration',
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* Merge one team's delivered state into the session view.
|
|
49
|
+
*
|
|
50
|
+
* The revision decides what happens, and the three cases are all reachable in
|
|
51
|
+
* practice:
|
|
52
|
+
*
|
|
53
|
+
* - **In sequence** (exactly one ahead of what is held, and a first delivery
|
|
54
|
+
* for a team added mid-session counts): apply it.
|
|
55
|
+
* - **Behind or equal**: a redelivery, or a delta that raced the snapshot that
|
|
56
|
+
* already superseded it. Ignore it. Applying it would move the surface
|
|
57
|
+
* backwards, which is worse than doing nothing.
|
|
58
|
+
* - **Ahead by more than one**: an update was lost. The held state for that
|
|
59
|
+
* team is now unreliable and cannot be repaired from a delta, so the caller
|
|
60
|
+
* asks the host for a fresh snapshot.
|
|
61
|
+
*
|
|
62
|
+
* Returning `null` for the gap case keeps the decision here, where the rule is
|
|
63
|
+
* written down, rather than splitting it between this function and its caller.
|
|
64
|
+
*/
|
|
65
|
+
function mergeTeamState(previous, teamId, state) {
|
|
66
|
+
const held = previous.teams[teamId]?.revision ?? 0;
|
|
67
|
+
if (state.revision <= held)
|
|
68
|
+
return previous;
|
|
69
|
+
if (state.revision > held + 1)
|
|
70
|
+
return null;
|
|
71
|
+
return { ...previous, teams: { ...previous.teams, [teamId]: state } };
|
|
72
|
+
}
|
|
73
|
+
/** {@link mergeTeamState} for the shared store, by the same three cases. */
|
|
74
|
+
function mergeSharedState(previous, state) {
|
|
75
|
+
const held = previous.shared.revision;
|
|
76
|
+
if (state.revision <= held)
|
|
77
|
+
return previous;
|
|
78
|
+
if (state.revision > held + 1)
|
|
79
|
+
return null;
|
|
80
|
+
return { ...previous, shared: state };
|
|
81
|
+
}
|
|
82
|
+
/** An empty `deletes` is left off the wire, as a player surface leaves it off. */
|
|
83
|
+
function deletesField(deletes) {
|
|
84
|
+
return deletes?.length ? { deletes } : {};
|
|
85
|
+
}
|
|
86
|
+
const EMPTY_SESSION_DATA = { shared: { context: {}, revision: 0 }, teams: {} };
|
|
87
|
+
function SessionUserInterfaceWrapper({ children, fallback = _jsx("div", { children: "Loading..." }), targetWindow = window.parent, }) {
|
|
88
|
+
const [sessionData, setSessionDataState] = useState(EMPTY_SESSION_DATA);
|
|
89
|
+
const [settings, setSettingsState] = useState(undefined);
|
|
90
|
+
const [theme, setThemeState] = useState(undefined);
|
|
91
|
+
const [locale, setLocaleState] = useState(undefined);
|
|
92
|
+
const [participants, setParticipantsState] = useState([]);
|
|
93
|
+
const [teams, setTeamsState] = useState([]);
|
|
94
|
+
const [sessionMeta, setSessionMetaState] = useState(undefined);
|
|
95
|
+
const [teamLinks, setTeamLinksState] = useState([]);
|
|
96
|
+
// Fixed for the life of the run; 'session' is by far the common answer,
|
|
97
|
+
// so it never gates first paint.
|
|
98
|
+
const [runKind, setRunKindState] = useState('session');
|
|
99
|
+
// null until the host answers: a surface gates its control on this.
|
|
100
|
+
const [walkthroughLink, setWalkthroughLinkState] = useState(null);
|
|
101
|
+
const [hookFailures, setHookFailures] = useState([]);
|
|
102
|
+
const [teamConfiguration, setTeamConfigurationState] = useState(DEFAULT_TEAM_CONFIGURATION);
|
|
103
|
+
// Empty is the honest default and also the right answer for a host that
|
|
104
|
+
// delegates none, so this does not gate first paint.
|
|
105
|
+
const [hostOverlays, setHostOverlaysState] = useState([]);
|
|
106
|
+
/**
|
|
107
|
+
* Whether a `resyncSessionData` is outstanding. A gap usually means several
|
|
108
|
+
* deltas were lost, so without this one lost update would send a request
|
|
109
|
+
* per subsequent delta until the snapshot landed. Cleared by the snapshot
|
|
110
|
+
* arriving, which is the only thing that resolves the gap.
|
|
111
|
+
*/
|
|
112
|
+
const resyncPendingRef = useRef(false);
|
|
113
|
+
const { isInitialized, expectedParentOrigin, postMessage, sendMutation, uploadBlob, readAsset, getPastState: getPastStateRaw, getAssetUrl, assetUrls, setThemePreference, endpoints, themePreference, sessionKey, } = useHostChannel({
|
|
114
|
+
targetWindow,
|
|
115
|
+
label: 'SessionUserInterfaceWrapper',
|
|
116
|
+
requiredMessages: REQUIRED_MESSAGES,
|
|
117
|
+
onMessage: (msg, { markReceived }) => {
|
|
118
|
+
if (msg.type === 'setSessionData') {
|
|
119
|
+
setSessionDataState({
|
|
120
|
+
shared: msg.payload?.shared ?? EMPTY_SESSION_DATA.shared,
|
|
121
|
+
teams: msg.payload?.teams ?? {},
|
|
122
|
+
});
|
|
123
|
+
// A snapshot is by definition current, so any resync it was
|
|
124
|
+
// sent in answer to is now settled.
|
|
125
|
+
resyncPendingRef.current = false;
|
|
126
|
+
markReceived('setSessionData');
|
|
127
|
+
}
|
|
128
|
+
else if (msg.type === 'updateTeamState') {
|
|
129
|
+
const merged = mergeTeamState(sessionDataRef.current, msg.teamId, msg.state);
|
|
130
|
+
if (merged === null) {
|
|
131
|
+
requestResync();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
sessionDataRef.current = merged;
|
|
135
|
+
setSessionDataState(merged);
|
|
136
|
+
}
|
|
137
|
+
else if (msg.type === 'updateSharedState') {
|
|
138
|
+
const merged = mergeSharedState(sessionDataRef.current, msg.state);
|
|
139
|
+
if (merged === null) {
|
|
140
|
+
requestResync();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
sessionDataRef.current = merged;
|
|
144
|
+
setSessionDataState(merged);
|
|
145
|
+
}
|
|
146
|
+
else if (msg.type === 'setSessionMeta') {
|
|
147
|
+
setSessionMetaState(msg.payload);
|
|
148
|
+
markReceived('setSessionMeta');
|
|
149
|
+
}
|
|
150
|
+
else if (msg.type === 'setSettings') {
|
|
151
|
+
setSettingsState(msg.payload);
|
|
152
|
+
markReceived('setSettings');
|
|
153
|
+
}
|
|
154
|
+
else if (msg.type === 'setTheme') {
|
|
155
|
+
setThemeState(msg.payload);
|
|
156
|
+
markReceived('setTheme');
|
|
157
|
+
}
|
|
158
|
+
else if (msg.type === 'setLocale') {
|
|
159
|
+
setLocaleState(msg.payload);
|
|
160
|
+
markReceived('setLocale');
|
|
161
|
+
}
|
|
162
|
+
else if (msg.type === 'setTeamList') {
|
|
163
|
+
setTeamsState(msg.payload);
|
|
164
|
+
}
|
|
165
|
+
else if (msg.type === 'setTeamConfiguration') {
|
|
166
|
+
setTeamConfigurationState(msg.payload);
|
|
167
|
+
markReceived('setTeamConfiguration');
|
|
168
|
+
}
|
|
169
|
+
else if (msg.type === 'setRunKind') {
|
|
170
|
+
setRunKindState(msg.runKind);
|
|
171
|
+
}
|
|
172
|
+
else if (msg.type === 'setWalkthroughLink') {
|
|
173
|
+
setWalkthroughLinkState(msg.payload);
|
|
174
|
+
}
|
|
175
|
+
else if (msg.type === 'setTeamLinks') {
|
|
176
|
+
setTeamLinksState(msg.payload);
|
|
177
|
+
}
|
|
178
|
+
else if (msg.type === 'setAvailableOverlays') {
|
|
179
|
+
setHostOverlaysState(msg.overlays);
|
|
180
|
+
}
|
|
181
|
+
else if (msg.type === 'hookFailed') {
|
|
182
|
+
// Accumulate: a facilitator who looked away should still find
|
|
183
|
+
// out that something went wrong.
|
|
184
|
+
setHookFailures((prev) => [...prev, msg.payload]);
|
|
185
|
+
}
|
|
186
|
+
else if (msg.type === 'setParticipantList') {
|
|
187
|
+
setParticipantsState(msg.participants);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
// Mirror the latest optimistic session data so setData can merge
|
|
192
|
+
// synchronously and return a correctly-correlated promise (a state updater
|
|
193
|
+
// would run too late to capture the sendMutation promise). The delta
|
|
194
|
+
// handler reads it too, so two updates arriving before a re-render each
|
|
195
|
+
// merge onto the previous one rather than onto stale state.
|
|
196
|
+
const sessionDataRef = useLatestRef(sessionData);
|
|
197
|
+
/**
|
|
198
|
+
* Ask the host for a fresh full snapshot after a per-team revision arrived
|
|
199
|
+
* out of sequence. The surface keeps rendering what it holds meanwhile: one
|
|
200
|
+
* team's state being a beat stale is better than blanking a facilitator's
|
|
201
|
+
* screen, and the snapshot repairs it either way.
|
|
202
|
+
*/
|
|
203
|
+
const requestResync = useCallback(() => {
|
|
204
|
+
if (resyncPendingRef.current)
|
|
205
|
+
return;
|
|
206
|
+
resyncPendingRef.current = true;
|
|
207
|
+
postMessage({ type: 'resyncSessionData' });
|
|
208
|
+
}, [postMessage]);
|
|
209
|
+
const setData = useCallback((teamIdOrUpdate, updateOrOptions, maybeOptions) => {
|
|
210
|
+
const prev = sessionDataRef.current;
|
|
211
|
+
if (typeof teamIdOrUpdate !== 'string') {
|
|
212
|
+
// Shared write: one value for the session, so it merges into
|
|
213
|
+
// the shared store the surface holds once.
|
|
214
|
+
const update = teamIdOrUpdate;
|
|
215
|
+
const deletes = updateOrOptions?.deletes;
|
|
216
|
+
const next = {
|
|
217
|
+
...prev,
|
|
218
|
+
shared: {
|
|
219
|
+
...prev.shared,
|
|
220
|
+
context: applyContextUpdate(prev.shared.context, update, deletes),
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
sessionDataRef.current = next;
|
|
224
|
+
setSessionDataState(next);
|
|
225
|
+
return sendMutation({
|
|
226
|
+
type: 'updateSharedData',
|
|
227
|
+
payload: update,
|
|
228
|
+
...deletesField(deletes),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
const teamId = teamIdOrUpdate;
|
|
232
|
+
const update = updateOrOptions ?? {};
|
|
233
|
+
const deletes = maybeOptions?.deletes;
|
|
234
|
+
const teamState = prev.teams[teamId] ?? {
|
|
235
|
+
context: {},
|
|
236
|
+
completedTicks: 0,
|
|
237
|
+
timer: DEFAULT_SURFACE_TIMER,
|
|
238
|
+
revision: 0,
|
|
239
|
+
};
|
|
240
|
+
const mergedContext = applyContextUpdate(teamState.context, update, deletes);
|
|
241
|
+
const next = {
|
|
242
|
+
...prev,
|
|
243
|
+
teams: {
|
|
244
|
+
...prev.teams,
|
|
245
|
+
[teamId]: { ...teamState, context: mergedContext },
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
sessionDataRef.current = next;
|
|
249
|
+
setSessionDataState(next);
|
|
250
|
+
return sendMutation({
|
|
251
|
+
type: 'updateSessionData',
|
|
252
|
+
teamId,
|
|
253
|
+
payload: update,
|
|
254
|
+
...deletesField(deletes),
|
|
255
|
+
});
|
|
256
|
+
}, [sendMutation]);
|
|
257
|
+
const requestTick = useCallback((teamId) => sendMutation({ type: 'requestTick', teamId }), [sendMutation]);
|
|
258
|
+
const getPastState = useTypedPastState(getPastStateRaw);
|
|
259
|
+
useDocumentAttributes(locale?.locale, theme?.mode);
|
|
260
|
+
const { pauseTimer, resumeTimer, resetGame, undoTick, extendTick, openHostOverlay } = useSurfaceControls({ sendMutation });
|
|
261
|
+
// Session-wide, so it is not part of useSurfaceControls, whose messages all
|
|
262
|
+
// carry an optional teamId.
|
|
263
|
+
const setWaitingRoom = useCallback((open) => sendMutation({ type: 'setWaitingRoom', open }), [sendMutation]);
|
|
264
|
+
const resetWalkthrough = useCallback(() => sendMutation({ type: 'resetWalkthrough' }), [sendMutation]);
|
|
265
|
+
const createTeam = useCallback((name) => sendMutation({ type: 'createTeam', name }), [sendMutation]);
|
|
266
|
+
const renameTeam = useCallback((teamId, name) => sendMutation({ type: 'renameTeam', teamId, name }), [sendMutation]);
|
|
267
|
+
const renameParticipant = useCallback((participantId, name) => sendMutation({ type: 'renameParticipant', participantId, name }), [sendMutation]);
|
|
268
|
+
const moveParticipant = useCallback((participantId, teamId) => sendMutation({ type: 'moveParticipant', participantId, teamId }), [sendMutation]);
|
|
269
|
+
const deleteTeam = useCallback((teamId) => sendMutation({ type: 'deleteTeam', teamId }), [sendMutation]);
|
|
270
|
+
const hookFailuresValue = useMemo(() => ({ failures: hookFailures, clearFailures: () => setHookFailures([]) }), [hookFailures]);
|
|
271
|
+
const dataContextValue = useMemo(() => ({ sessionData, settings, setData }), [sessionData, settings, setData]);
|
|
272
|
+
const callbacksContextValue = useMemo(() => ({
|
|
273
|
+
requestTick,
|
|
274
|
+
uploadBlob,
|
|
275
|
+
getAssetUrl,
|
|
276
|
+
readAsset,
|
|
277
|
+
getPastState,
|
|
278
|
+
setThemePreference,
|
|
279
|
+
pauseTimer,
|
|
280
|
+
resumeTimer,
|
|
281
|
+
resetGame,
|
|
282
|
+
undoTick,
|
|
283
|
+
extendTick,
|
|
284
|
+
openHostOverlay,
|
|
285
|
+
setWaitingRoom,
|
|
286
|
+
resetWalkthrough,
|
|
287
|
+
createTeam,
|
|
288
|
+
renameTeam,
|
|
289
|
+
renameParticipant,
|
|
290
|
+
moveParticipant,
|
|
291
|
+
deleteTeam,
|
|
292
|
+
endpoints,
|
|
293
|
+
}), [
|
|
294
|
+
requestTick,
|
|
295
|
+
uploadBlob,
|
|
296
|
+
getAssetUrl,
|
|
297
|
+
readAsset,
|
|
298
|
+
getPastState,
|
|
299
|
+
setThemePreference,
|
|
300
|
+
pauseTimer,
|
|
301
|
+
resumeTimer,
|
|
302
|
+
resetGame,
|
|
303
|
+
undoTick,
|
|
304
|
+
extendTick,
|
|
305
|
+
openHostOverlay,
|
|
306
|
+
setWaitingRoom,
|
|
307
|
+
resetWalkthrough,
|
|
308
|
+
createTeam,
|
|
309
|
+
renameTeam,
|
|
310
|
+
renameParticipant,
|
|
311
|
+
moveParticipant,
|
|
312
|
+
deleteTeam,
|
|
313
|
+
endpoints,
|
|
314
|
+
]);
|
|
315
|
+
const storageScope = useMemo(() => ({ sessionKey, surface: 'session', viewer: 'facilitator' }), [sessionKey]);
|
|
316
|
+
return (_jsx(SurfaceGate, { expectedParentOrigin: expectedParentOrigin, ready: isInitialized &&
|
|
317
|
+
theme !== undefined &&
|
|
318
|
+
locale !== undefined &&
|
|
319
|
+
sessionMeta !== undefined, fallback: fallback, children: _jsx(SurfaceScope, { storage: storageScope, assetUrls: assetUrls, getAssetUrl: getAssetUrl, locale: locale?.locale, getPastState: getPastState, children: _jsx(SessionUserInterfaceDataContext.Provider, { value: dataContextValue, children: _jsx(SessionUserInterfaceThemeContext.Provider, { value: theme, children: _jsx(SessionUserInterfaceThemePreferenceContext.Provider, { value: themePreference, children: _jsx(SessionUserInterfaceLocaleContext.Provider, { value: locale, children: _jsx(SessionUserInterfaceMetaContext.Provider, { value: sessionMeta, children: _jsx(SessionUserInterfaceTeamsContext.Provider, { value: teams, children: _jsx(SessionUserInterfaceTeamConfigurationContext.Provider, { value: teamConfiguration, children: _jsx(UserInterfaceRunKindContext.Provider, { value: runKind, children: _jsx(SessionUserInterfaceWalkthroughLinkContext.Provider, { value: walkthroughLink, children: _jsx(SessionUserInterfaceTeamLinksContext.Provider, { value: teamLinks, children: _jsx(SessionUserInterfaceHostOverlaysContext.Provider, { value: hostOverlays, children: _jsx(SessionUserInterfaceParticipantsContext.Provider, { value: participants, children: _jsx(SessionUserInterfaceCallbacksContext.Provider, { value: callbacksContextValue, children: _jsx(SessionUserInterfaceHookFailuresContext.Provider, { value: hookFailuresValue, children: children }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) }));
|
|
320
|
+
}
|
|
321
|
+
export default SessionUserInterfaceWrapper;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createRequiredContext } from './internal/createContexts.js';
|
|
2
|
+
const { Context, useValue } = createRequiredContext({
|
|
3
|
+
hookName: 'useSettingsEditorCallbacks',
|
|
4
|
+
wrapperName: 'SettingsEditorWrapper',
|
|
5
|
+
});
|
|
6
|
+
export const SettingsEditorCallbacksContext = Context;
|
|
7
|
+
export const useSettingsEditorCallbacks = useValue;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a settings editor edits: the `setup` slot and the `scenario_editor`
|
|
3
|
+
* slot share it (§6.5).
|
|
4
|
+
*/
|
|
5
|
+
export type SettingsEditorValue<T> = {
|
|
6
|
+
/**
|
|
7
|
+
* The resolved values of the settings this editor `configures`, keyed by
|
|
8
|
+
* settings identifier, with any edits made so far merged in.
|
|
9
|
+
*/
|
|
10
|
+
settings: T;
|
|
11
|
+
/**
|
|
12
|
+
* Merge an update into the settings. Only what changed reaches the host
|
|
13
|
+
* (§8.2). Resolves once the host confirms the write; rejects with a
|
|
14
|
+
* `MutationError` when it is refused (settings validation, say) or fails.
|
|
15
|
+
*
|
|
16
|
+
* Await it: a screen that drops a refusal leaves the facilitator believing
|
|
17
|
+
* a setting took when it did not, and they will not find out until the
|
|
18
|
+
* session runs. `useMutationReporter` does the awaiting and hands you the
|
|
19
|
+
* failure to render.
|
|
20
|
+
*/
|
|
21
|
+
updateSettings: (update: Partial<T>) => Promise<void>;
|
|
22
|
+
/** Identifiers of the settings resources this editor edits. */
|
|
23
|
+
configures: string[];
|
|
24
|
+
};
|
|
25
|
+
declare const SettingsEditorContext: import("react").Context<SettingsEditorValue<unknown> | undefined>;
|
|
26
|
+
/**
|
|
27
|
+
* The settings a `setup` or `scenario_editor` slot edits, and the way to
|
|
28
|
+
* change them. Pass the generated `Settings` type from the slot's `types.ts`.
|
|
29
|
+
*
|
|
30
|
+
* @throws Error if used outside SettingsEditorWrapper
|
|
31
|
+
*/
|
|
32
|
+
declare function useSettingsEditor<T>(): SettingsEditorValue<T>;
|
|
33
|
+
export { useSettingsEditor, SettingsEditorContext };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
const SettingsEditorContext = createContext(undefined);
|
|
3
|
+
/**
|
|
4
|
+
* The settings a `setup` or `scenario_editor` slot edits, and the way to
|
|
5
|
+
* change them. Pass the generated `Settings` type from the slot's `types.ts`.
|
|
6
|
+
*
|
|
7
|
+
* @throws Error if used outside SettingsEditorWrapper
|
|
8
|
+
*/
|
|
9
|
+
function useSettingsEditor() {
|
|
10
|
+
const ctx = useContext(SettingsEditorContext);
|
|
11
|
+
if (!ctx) {
|
|
12
|
+
throw new Error('useSettingsEditor must be used inside a SettingsEditorWrapper');
|
|
13
|
+
}
|
|
14
|
+
return ctx;
|
|
15
|
+
}
|
|
16
|
+
export { useSettingsEditor, SettingsEditorContext };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type SettingsEditorLocale = {
|
|
2
|
+
locale: string;
|
|
3
|
+
};
|
|
4
|
+
export declare const SettingsEditorLocaleContext: import("react").Context<SettingsEditorLocale | undefined>;
|
|
5
|
+
/**
|
|
6
|
+
* Hook to access the current locale in a settings editor (the `setup` or
|
|
7
|
+
* `scenario_editor` slot). The host picks it from the game's supported
|
|
8
|
+
* locales.
|
|
9
|
+
*
|
|
10
|
+
* @returns Current locale object with locale string
|
|
11
|
+
* @throws Error if used outside SettingsEditorWrapper
|
|
12
|
+
*/
|
|
13
|
+
export declare const useSettingsEditorLocale: () => SettingsEditorLocale;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createRequiredContext } from './internal/createContexts.js';
|
|
2
|
+
const { Context, useValue } = createRequiredContext({
|
|
3
|
+
hookName: 'useSettingsEditorLocale',
|
|
4
|
+
wrapperName: 'SettingsEditorWrapper',
|
|
5
|
+
});
|
|
6
|
+
export const SettingsEditorLocaleContext = Context;
|
|
7
|
+
/**
|
|
8
|
+
* Hook to access the current locale in a settings editor (the `setup` or
|
|
9
|
+
* `scenario_editor` slot). The host picks it from the game's supported
|
|
10
|
+
* locales.
|
|
11
|
+
*
|
|
12
|
+
* @returns Current locale object with locale string
|
|
13
|
+
* @throws Error if used outside SettingsEditorWrapper
|
|
14
|
+
*/
|
|
15
|
+
export const useSettingsEditorLocale = useValue;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type SurfaceModeColors } from './internal/createContexts.js';
|
|
2
|
+
export type SettingsEditorModeColors = SurfaceModeColors;
|
|
3
|
+
export type SettingsEditorTheme = {
|
|
4
|
+
mode: 'dark' | 'light';
|
|
5
|
+
dark: SettingsEditorModeColors;
|
|
6
|
+
light: SettingsEditorModeColors;
|
|
7
|
+
};
|
|
8
|
+
export declare const SettingsEditorThemeContext: import("react").Context<SettingsEditorTheme | undefined>;
|
|
9
|
+
export declare const useSettingsEditorTheme: () => SettingsEditorTheme;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createRequiredContext } from './internal/createContexts.js';
|
|
2
|
+
const { Context, useValue } = createRequiredContext({
|
|
3
|
+
hookName: 'useSettingsEditorTheme',
|
|
4
|
+
wrapperName: 'SettingsEditorWrapper',
|
|
5
|
+
});
|
|
6
|
+
export const SettingsEditorThemeContext = Context;
|
|
7
|
+
export const useSettingsEditorTheme = useValue;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createRequiredContext } from './internal/createContexts.js';
|
|
2
|
+
const { Context, useValue } = createRequiredContext({
|
|
3
|
+
hookName: 'useSettingsEditorThemePreference',
|
|
4
|
+
wrapperName: 'SettingsEditorWrapper',
|
|
5
|
+
});
|
|
6
|
+
export const SettingsEditorThemePreferenceContext = Context;
|
|
7
|
+
export const useSettingsEditorThemePreference = useValue;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
interface SettingsEditorWrapperProps {
|
|
3
|
+
children?: ReactNode;
|
|
4
|
+
fallback?: ReactNode;
|
|
5
|
+
targetWindow?: Window;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The wrapper for both settings editors, the `setup` slot and the
|
|
9
|
+
* `scenario_editor` slot (§6.5). Each slot is its own bundle, so a screen
|
|
10
|
+
* knows which it is without being told: the host sends the same batch to both.
|
|
11
|
+
*/
|
|
12
|
+
declare function SettingsEditorWrapper({ children, fallback, targetWindow, }: SettingsEditorWrapperProps): import("react").JSX.Element;
|
|
13
|
+
export default SettingsEditorWrapper;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { changedFrom, deepMergeContext } from '../build/context-merge.js';
|
|
4
|
+
import { useDocumentAttributes } from './internal/document-attributes.js';
|
|
5
|
+
import { SurfaceGate } from './internal/SurfaceGate.js';
|
|
6
|
+
import { SurfaceScope } from './internal/SurfaceScope.js';
|
|
7
|
+
import { SettingsEditorCallbacksContext } from './SettingsEditorCallbacksContext.js';
|
|
8
|
+
import { SettingsEditorContext } from './SettingsEditorContext.js';
|
|
9
|
+
import { SettingsEditorLocaleContext, } from './SettingsEditorLocaleContext.js';
|
|
10
|
+
import { SettingsEditorThemeContext } from './SettingsEditorThemeContext.js';
|
|
11
|
+
import { SettingsEditorThemePreferenceContext } from './SettingsEditorThemePreferenceContext.js';
|
|
12
|
+
import { useHostChannel } from './useHostChannel.js';
|
|
13
|
+
const REQUIRED_MESSAGES = ['setSettings', 'setTheme', 'setLocale', 'setAssetUrls'];
|
|
14
|
+
/**
|
|
15
|
+
* The wrapper for both settings editors, the `setup` slot and the
|
|
16
|
+
* `scenario_editor` slot (§6.5). Each slot is its own bundle, so a screen
|
|
17
|
+
* knows which it is without being told: the host sends the same batch to both.
|
|
18
|
+
*/
|
|
19
|
+
function SettingsEditorWrapper({ children, fallback = _jsx("div", { children: "Loading..." }), targetWindow = window.parent, }) {
|
|
20
|
+
const [settings, setSettingsState] = useState(undefined);
|
|
21
|
+
const [configures, setConfigures] = useState([]);
|
|
22
|
+
const [theme, setThemeState] = useState(undefined);
|
|
23
|
+
const [locale, setLocaleState] = useState(undefined);
|
|
24
|
+
const { isInitialized, expectedParentOrigin, postMessage, sendMutation, uploadBlob, readAsset, getAssetUrl, assetUrls, setThemePreference, endpoints, themePreference, sessionKey, } = useHostChannel({
|
|
25
|
+
targetWindow,
|
|
26
|
+
label: 'SettingsEditorWrapper',
|
|
27
|
+
requiredMessages: REQUIRED_MESSAGES,
|
|
28
|
+
onMessage: (msg, { markReceived }) => {
|
|
29
|
+
if (msg.type === 'setSettings') {
|
|
30
|
+
setSettingsState(msg.payload);
|
|
31
|
+
// `setSettings` carries exactly what this editor configures
|
|
32
|
+
// (§8.3), so its keys are the list: there is no second message
|
|
33
|
+
// to drift from it.
|
|
34
|
+
setConfigures(Object.keys(msg.payload));
|
|
35
|
+
markReceived('setSettings');
|
|
36
|
+
}
|
|
37
|
+
else if (msg.type === 'setTheme') {
|
|
38
|
+
setThemeState(msg.payload);
|
|
39
|
+
markReceived('setTheme');
|
|
40
|
+
}
|
|
41
|
+
else if (msg.type === 'setLocale') {
|
|
42
|
+
setLocaleState(msg.payload);
|
|
43
|
+
markReceived('setLocale');
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
// Mirror the latest optimistic settings so updateSettings can merge
|
|
48
|
+
// synchronously and return a correctly-correlated promise (a state updater
|
|
49
|
+
// would run too late to capture the sendMutation promise).
|
|
50
|
+
const settingsRef = useRef(undefined);
|
|
51
|
+
settingsRef.current = settings;
|
|
52
|
+
const updateSettings = useCallback((update) => {
|
|
53
|
+
const current = settingsRef.current ?? {};
|
|
54
|
+
const merged = deepMergeContext(current, update);
|
|
55
|
+
// Only what changed goes to the host (§8.2): a UI that spreads the
|
|
56
|
+
// settings it was given would otherwise send every value back as
|
|
57
|
+
// a facilitator override.
|
|
58
|
+
const changed = changedFrom(update, current) ?? {};
|
|
59
|
+
settingsRef.current = merged;
|
|
60
|
+
setSettingsState(merged);
|
|
61
|
+
return sendMutation({ type: 'updateSettings', payload: changed });
|
|
62
|
+
}, [sendMutation]);
|
|
63
|
+
useDocumentAttributes(locale?.locale, theme?.mode);
|
|
64
|
+
const requestPlay = useCallback(() => postMessage({ type: 'requestPlay' }), [postMessage]);
|
|
65
|
+
const editorContextValue = useMemo(() => ({ settings, updateSettings, configures }), [settings, updateSettings, configures]);
|
|
66
|
+
const callbacksContextValue = useMemo(() => ({ uploadBlob, getAssetUrl, readAsset, setThemePreference, endpoints, requestPlay }), [uploadBlob, getAssetUrl, readAsset, setThemePreference, endpoints, requestPlay]);
|
|
67
|
+
const storageScope = useMemo(() => ({
|
|
68
|
+
sessionKey,
|
|
69
|
+
surface: 'settings-editor',
|
|
70
|
+
viewer: 'facilitator',
|
|
71
|
+
}), [sessionKey]);
|
|
72
|
+
return (_jsx(SurfaceGate, { expectedParentOrigin: expectedParentOrigin, ready: isInitialized && theme !== undefined && locale !== undefined, fallback: fallback, children: _jsx(SurfaceScope, { storage: storageScope, assetUrls: assetUrls, getAssetUrl: getAssetUrl, locale: locale?.locale, children: _jsx(SettingsEditorContext.Provider, { value: editorContextValue, children: _jsx(SettingsEditorThemeContext.Provider, { value: theme, children: _jsx(SettingsEditorThemePreferenceContext.Provider, { value: themePreference, children: _jsx(SettingsEditorLocaleContext.Provider, { value: locale, children: _jsx(SettingsEditorCallbacksContext.Provider, { value: callbacksContextValue, children: children }) }) }) }) }) }) }));
|
|
73
|
+
}
|
|
74
|
+
export default SettingsEditorWrapper;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createRequiredContext } from './internal/createContexts.js';
|
|
2
|
+
const { Context, useValue } = createRequiredContext({
|
|
3
|
+
hookName: 'useUserInterfaceCallbacks',
|
|
4
|
+
wrapperName: 'UserInterfaceWrapper',
|
|
5
|
+
});
|
|
6
|
+
export const UserInterfaceCallbacksContext = Context;
|
|
7
|
+
export const useUserInterfaceCallbacks = useValue;
|