@graview/embed 0.1.0 → 0.1.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/README.md CHANGED
@@ -28,4 +28,82 @@ element (`themeCss(scheme, brand, { scope })`) rather than the document; the
28
28
  panes size against the picture's own box (`cqh`) rather than the viewport;
29
29
  the routed face runs on a memory router, so the host page's address is never
30
30
  touched; the brand's fonts are fetched by the embed rather than assumed. The
31
- store is in memory and starts from the seed on every mount.
31
+ store is in memory and starts from the seed on every mount, unless the host
32
+ hands it one.
33
+
34
+ ## What went wrong, and how long it took
35
+
36
+ ```ts
37
+ mount(root, {
38
+ app,
39
+ onError: ({ name }, { module, face }) => beacon("embed-error", { name, module, face }),
40
+ onReady: ({ ms, face }) => beacon("embed-ready", { ms, face }),
41
+ });
42
+ ```
43
+
44
+ A view, a page, the strip or the studio that throws is contained where it
45
+ threw: it says it could not draw and offers to try again, and the rest of
46
+ the embed keeps working. The host is told the error's class (a TypeError,
47
+ a `GraphError`) and the framework module that caught it, never the message,
48
+ which may quote a record. The ready callback is told once, after the first render,
49
+ how many milliseconds it took.
50
+
51
+ ## What the host can keep
52
+
53
+ The Studio is on the strip for the seat that keeps the app, and writes
54
+ through a dev server's door or hands over files. A host whose readers
55
+ cannot save a declaration leaves it off, or keeps what it applies:
56
+
57
+ ```ts
58
+ mount(root, { app, studio: false }); // no Studio place at all
59
+ mount(root, { app, studio: { onApply: ({ app, migration, files }) => propose(app) } });
60
+ ```
61
+
62
+ Handed an onApply, the studio asks after no door and writes nothing; what the
63
+ checker passed is the host's.
64
+
65
+ `@graview/embed/pages` is the routed face alone, with the same options less
66
+ the face, the stop, the heading and the studio, and the same handle less the
67
+ faces. It takes `views`, and its pages draw the same cards, rows and record
68
+ pages the whole embed's would. A page that only ever shows the pages imports
69
+ that and does not bundle the scene, the Graview or the studio:
70
+
71
+ ```ts
72
+ import { mount } from "@graview/embed/pages";
73
+ ```
74
+
75
+ Bundled for the browser without React, the pages face alone is about 730 KB
76
+ minified (195 KB gzipped) and every face about 1.05 MB (300 KB);
77
+ `node scripts/inspect-pack.mjs` fails CI when either passes its budget
78
+ (`scripts/lib/bundle-budget.mjs`).
79
+
80
+ ## In a chat's widget
81
+
82
+ An MCP Apps frame (a ChatGPT or Claude widget) may refuse storage, is sized
83
+ from its content, and is told its theme by the chat. The embed holds there:
84
+
85
+ ```ts
86
+ const remote = await openRemote({ app, url, principal }); // @graview/ship
87
+ const handle = mount(root, {
88
+ app,
89
+ remote, // the store, and who is here, from the server
90
+ principal, // who signed in: the only seat a hosted reader has
91
+ people, // [{ id, name, kind? }]: names for the rail, offering nobody a seat
92
+ height: "auto", // the pages face as tall as its page
93
+ pagesBelow: 560, // narrower than this, the scene gives way to the pages
94
+ hostContext: { theme }, // the chat's theme, over the page's own
95
+ memory, // where the reader's settings are kept, if not the page's storage
96
+ onIntrinsicHeight: (height) => notify("ui/notifications/size-changed", { height }),
97
+ });
98
+ handle.setHostContext({ theme: "dark" }); // the chat changed theme
99
+ handle.setPeople(next); // an agent who first acted after the page opened
100
+ ```
101
+
102
+ Nothing touches `localStorage` or `sessionStorage` without a fallback, so a
103
+ frame that throws on either still mounts. `scheme: "auto"` (the default)
104
+ follows the host context, then the page's `data-theme` stamp, then the
105
+ system's preference, each as it changes. Presence a channel reports is
106
+ dropped once its last word is older than `REMOTE_PRESENCE_TTL_MS`, whether or
107
+ not the channel says the person left. A host that builds presence itself
108
+ keys each participant with `participantKey({ kind, id, session })` from
109
+ `@graview/core`, the op log's own `kind:id:session`.
package/dist/embed.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { Store, type AnySchema, type Brand, type GraviewApp, type PresenceChannel, type Principal } from "@graview/core";
2
- import { type PageComponent, type PageRegistry } from "@graview/pages";
3
- import { type Scheme, type ReactViewRegistry } from "@graview/react";
1
+ import type { AnySchema, Brand, Person, Principal, Store } from "@graview/core";
2
+ import { type StudioApplied } from "@graview/studio";
3
+ import { type Scheme } from "@graview/react";
4
+ import { type EmbedFace, type EmbedHostContext, type FrameOptions } from "./frame.js";
4
5
  /**
5
6
  * A GRAVIEW IN SOMEBODY ELSE'S PAGE.
6
7
  *
@@ -12,71 +13,36 @@ import { type Scheme, type ReactViewRegistry } from "@graview/react";
12
13
  * a prop rather than a URL. Everything else — the scene, the rails, the
13
14
  * inspector, the pages — is the framework's own, unchanged.
14
15
  */
15
- /**
16
- * The faces. `picture` is ONE NAMED LENS AND NOTHING ELSE — the place the
17
- * stop names (`#view=the-week`), drawn at full size over the kind's current
18
- * members, with no bar, no rail, no standing: a page that is about a lens
19
- * shows the lens, not an app with the lens somewhere inside it.
20
- */
21
- export type EmbedFace = "scene" | "graview" | "pages" | "picture";
22
- export interface EmbedOptions<S extends AnySchema = AnySchema> {
23
- readonly app: GraviewApp<S>;
24
- /** The graph to open with. Nothing means the empty city. */
25
- readonly seed?: {
26
- readonly nodes: readonly unknown[];
27
- readonly edges: readonly unknown[];
28
- };
29
- /** Which face to show. */
16
+ /** The scene and the Graview's height when the embed sizes itself to its content (`height: "auto"`). */
17
+ export declare const AUTO_SCENE_HEIGHT = 480;
18
+ export interface EmbedOptions<S extends AnySchema = AnySchema> extends FrameOptions<S> {
30
19
  /** Which face to open on. Omitted, the stop decides: altitude opens the Graview, anything else the scene. */
31
20
  readonly face?: EmbedFace;
32
21
  /** The scene's view state, as the fragment the app itself would put in its address bar. */
33
22
  readonly stop?: string;
34
- /** For the pages face: the path to open, within the app's own routes. */
35
- readonly path?: string;
36
- readonly principal?: Principal;
37
23
  /**
38
- * WHO ELSE IS HERE, if the host wants that: an embed broadcasts nothing
39
- * and draws nobody unless it is handed a channel — a page that puts a
40
- * graph on it is not thereby a page that tells its readers about each
41
- * other.
24
+ * THE WORKBENCH'S HEADING (FR-25): the level the embed's name is said at,
25
+ * for a reader moving by headings — `1` when the host's page is the app,
26
+ * `2` (the default) inside somebody else's article, `false` when the
27
+ * host's own heading already names it. The pages face brings its own.
42
28
  */
43
- readonly presence?: PresenceChannel;
29
+ readonly heading?: 1 | 2 | 3 | 4 | 5 | 6 | false;
44
30
  /**
45
- * The seats a reader may take, when the page wants the policy to be felt
46
- * rather than read: each is a name and a principal, shown on the strip and
47
- * pressed while it is at the keyboard. The strip, the pages and the acts
48
- * all narrow to the seat, so what a gardener may not do is struck through
49
- * the moment a gardener sits down.
31
+ * Below this width the scene and the Graview give way to the pages face,
32
+ * and come back above it. A phone's chat is no place for a map. Off by default.
50
33
  */
51
- readonly seats?: readonly {
52
- readonly label: string;
53
- readonly principal: Principal;
54
- }[];
55
- /** "auto" reads the host page: its `data-theme` stamp, else the system's preference. */
56
- readonly scheme?: Scheme | "auto";
57
- /** Defaults to the app's own brand. */
58
- readonly brand?: Brand;
59
- /** A store to share; otherwise one is made from the app and the seed. */
60
- readonly store?: Store<S>;
61
- /** Views beyond the derived defaults. */
62
- readonly views?: (schema: S) => ReactViewRegistry<S>;
63
- /** The pages face's own pages, over the derived defaults. */
64
- readonly pages?: PageRegistry<S, PageComponent<S>>;
65
- /** The face switcher and Standing, above the picture. Default on. */
66
- readonly toggle?: boolean;
67
- /** Fetch the brand's fonts. Default on; off when the host already has them. */
68
- readonly fonts?: boolean;
69
- /** The embed's height; the element's own by default. */
70
- readonly height?: number | string;
71
- /** What Standing says when nothing is wrong. */
72
- readonly standing?: string;
34
+ readonly pagesBelow?: number;
73
35
  /**
74
- * What this embed is called, for assistive technology. Several Graviews
75
- * on one page each carry the same landmarks — the relation key, the
76
- * inspector, the pages' navigation — and a landmark has to be unique by
77
- * role and name; every landmark inside is named after the embed.
36
+ * WHAT BECOMES OF THE STUDIO (FR-19). Offered by default, for the seat
37
+ * that keeps the app, writing through a dev server's door or handing
38
+ * over files. `false` leaves it off the strip: a hosted reader cannot
39
+ * save a declaration, so is not offered one to change. `{ onApply }`
40
+ * keeps it and hands the host what the checker passed, writing nothing
41
+ * itself: the host makes it a proposal, a version, a review.
78
42
  */
79
- readonly label?: string;
43
+ readonly studio?: false | {
44
+ readonly onApply: (applied: StudioApplied) => void;
45
+ };
80
46
  }
81
47
  export interface EmbedProps<S extends AnySchema = AnySchema> extends EmbedOptions<S> {
82
48
  /** Called when the strip's switcher is pressed; the host decides the face. */
@@ -86,8 +52,6 @@ export interface EmbedProps<S extends AnySchema = AnySchema> extends EmbedOption
86
52
  }
87
53
  /** The face a stop implies: a stop at altitude opens the Graview. */
88
54
  export declare function faceOf(stop: string | undefined): EmbedFace;
89
- /** The host page's scheme: an explicit `data-theme`, else the system's preference. */
90
- export declare function hostScheme(): Scheme;
91
55
  export declare function Embed<S extends AnySchema>(props: EmbedProps<S>): import("react").JSX.Element;
92
56
  export interface EmbedHandle {
93
57
  setFace(face: EmbedFace): void;
@@ -95,6 +59,12 @@ export interface EmbedHandle {
95
59
  setScheme(scheme: Scheme): void;
96
60
  /** Put another principal at the keyboard; the store and its history stay. */
97
61
  setSeat(principal: Principal): void;
62
+ /** Offer other seats, or none: a seat switcher is drawn only for two or more. */
63
+ setSeats(seats: EmbedOptions["seats"]): void;
64
+ /** Name other people: an agent who first acts after the page opened is named from the next render. */
65
+ setPeople(people: readonly Person[]): void;
66
+ /** What the host now says about its frame: the chat's theme changed, say. */
67
+ setHostContext(context: EmbedHostContext): void;
98
68
  /** Re-dress the embed: another brand, or the same brand with a different kit. */
99
69
  setBrand(brand: Brand | undefined): void;
100
70
  readonly store: Store<AnySchema>;
@@ -1 +1 @@
1
- {"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../src/embed.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../src/embed.tsx"],"names":[],"mappings":"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"}
package/dist/embed.js CHANGED
@@ -1,26 +1,26 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Store } from "@graview/core";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
2
  import { EMPTY_VIEW, aggregateId, fromUrl, withFocus, withOverview } from "@graview/layout";
4
- import { PagesApp, PlacePicture } from "@graview/pages";
5
- import { Companion, Inspector, OverviewButton, Places, Profile, ShowInstallation, Standing, descentTarget, registerDefaultViews, themeCss, useWidth } from "@graview/primitives";
3
+ import { PlacePicture } from "@graview/pages";
4
+ import { Companion, Inspector, OverviewButton, Places, ShowInstallation, VISUALLY_HIDDEN, descentTarget, useWidth } from "@graview/primitives";
6
5
  import { StudioPlace } from "@graview/studio";
7
- import { createViews, GraviewProvider, Scene, useGraview, useNavigation, useTheKeyboardLandsSomewhere, } from "@graview/react";
8
- import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
6
+ import { ErrorReportContext, GraviewProvider, Scene, useNavigation } from "@graview/react";
7
+ import { useLayoutEffect, useMemo, useRef, useState } from "react";
9
8
  import { createRoot } from "react-dom/client";
10
9
  import { flushSync } from "react-dom";
11
- import { fontsLink } from "./fonts.js";
12
- let sequence = 0;
13
- /** A store from a declaration and a seed: the app's own policy, in memory. */
14
- function storeOf(app, seed) {
15
- return new Store({
16
- schema: app.schema,
17
- mutations: app.mutations ?? [],
18
- invariants: app.invariants ?? [],
19
- ...(seed ? { snapshot: seed } : {}),
20
- ...(app.policy ? { policy: app.policy } : {}),
21
- ...(app.intelligence ? { intelligence: app.intelligence } : {}),
22
- });
23
- }
10
+ import { FaceBoundary, PagesContent, providerProps, storeOf, Strip, useErrorReport, useFrame, useIntrinsicHeight, useReady, useViews, } from "./frame.js";
11
+ /**
12
+ * A GRAVIEW IN SOMEBODY ELSE'S PAGE.
13
+ *
14
+ * The Shell assumes it owns the window: the viewport is its height, the
15
+ * document is its theme, the address bar is its view state. An embed owns
16
+ * one element. So the theme is scoped to that element, the panes size
17
+ * against the picture's own box, the routed face runs on a memory router,
18
+ * the store lives in memory and starts from the seed, and the view state is
19
+ * a prop rather than a URL. Everything else — the scene, the rails, the
20
+ * inspector, the pages — is the framework's own, unchanged.
21
+ */
22
+ /** The scene and the Graview's height when the embed sizes itself to its content (`height: "auto"`). */
23
+ export const AUTO_SCENE_HEIGHT = 480;
24
24
  /**
25
25
  * The scene's view for a face and a stop.
26
26
  *
@@ -46,73 +46,24 @@ function viewFor(face, stop, kinds, places) {
46
46
  export function faceOf(stop) {
47
47
  return stop && fromUrl(stop).overview ? "graview" : "scene";
48
48
  }
49
- /** The host page's scheme: an explicit `data-theme`, else the system's preference. */
50
- export function hostScheme() {
51
- if (typeof document === "undefined")
52
- return "light";
53
- const stamped = document.documentElement.dataset["theme"];
54
- if (stamped === "dark" || stamped === "light")
55
- return stamped;
56
- return typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
57
- }
58
49
  export function Embed(props) {
59
- const { app, seed, face = faceOf(props.stop), stop, path = "/", principal, scheme: askedScheme = "auto", brand = app.brand, toggle = true, fonts = true, height = "100%", standing = "Everything is in order", pages, label, } = props;
60
- const rootRef = useRef(null);
61
- useTheKeyboardLandsSomewhere(rootRef);
62
- const scope = useMemo(() => `graview-embed-${++sequence}`, []);
63
- /*
64
- * ONE STORE ACROSS THE SEATS. Who is at the keyboard is the provider's
65
- * business, not the store's: with the principal in these dependencies a
66
- * seat change made a fresh store from the seed, and a React host that sat
67
- * somebody else down lost every edit and the history with them.
68
- */
69
- const store = useMemo(() => props.store ?? storeOf(app, seed), [props.store, app, seed]);
70
- const views = useMemo(() => (props.views ? props.views(app.schema) : registerDefaultViews(app.schema, createViews(app.schema))), [props.views, app.schema]);
50
+ const { app, face = faceOf(props.stop), stop, toggle = true, standing = "Everything is in order", principal, heading = 2 } = props;
51
+ const { rootRef, scope, css, scheme, store, presence, brand, auto, height } = useFrame(props);
52
+ const views = useViews(props);
71
53
  const kinds = app.schema.kinds;
72
54
  // The first view only: after it, where the reader goes is theirs.
73
55
  const initialView = useMemo(() => viewFor(face, stop, kinds, views.places()), []);
74
- const scheme = askedScheme === "auto" ? hostScheme() : askedScheme;
75
- const css = useMemo(() => themeCss(scheme, brand, { scope: `.${scope}` }), [scheme, brand, scope]);
76
- // The brand's fonts, fetched once per family set, without the host's help.
77
- useEffect(() => {
78
- if (!fonts || typeof document === "undefined")
79
- return;
80
- const href = fontsLink(brand);
81
- if (!href || document.querySelector(`link[href="${href}"]`))
82
- return;
83
- const link = document.createElement("link");
84
- link.rel = "stylesheet";
85
- link.href = href;
86
- document.head.appendChild(link);
87
- }, [brand, fonts]);
88
- // Landmarks inside, named after the embed — kept so through re-renders.
89
- useLayoutEffect(() => {
90
- const root = rootRef.current;
91
- if (!root || !label)
92
- return;
93
- const name = (el) => {
94
- const own = el.getAttribute("aria-label") ?? "";
95
- if (own.startsWith(`${label} · `))
96
- return;
97
- // Named the same thing: "The pipeline · The pipeline" is one name said
98
- // twice, not a place inside a place. A region of that name IS the
99
- // embed's region, so it stops being a second landmark (two regions of
100
- // one name is axe's `landmark-unique`) and stays a named group.
101
- if (own === label) {
102
- if (el.getAttribute("role") === "region")
103
- el.setAttribute("role", "group");
104
- return;
105
- }
106
- el.setAttribute("aria-label", own ? `${label} · ${own}` : label);
107
- };
108
- // The root is the embed's own region and already wears the label; the
109
- // sweep names what is INSIDE it.
110
- const sweep = () => root.querySelectorAll("aside, nav, main, header, footer, [role=region], [role=complementary], [role=navigation]").forEach(name);
111
- sweep();
112
- const observer = new MutationObserver(sweep);
113
- observer.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ["aria-label"] });
114
- return () => observer.disconnect();
115
- }, [label]);
56
+ /*
57
+ * NARROW, THE PAGES (FR-13). Below `pagesBelow` the scene and the Graview
58
+ * give way to the routed face, and come back when there is room: the
59
+ * face the host asked for is kept, only what is drawn changes.
60
+ */
61
+ const width = useWidth(rootRef);
62
+ const narrow = props.pagesBelow !== undefined && width !== null && width < props.pagesBelow && (face === "scene" || face === "graview");
63
+ const shown = narrow ? "pages" : face;
64
+ useIntrinsicHeight(rootRef, shown, props.onIntrinsicHeight);
65
+ const report = useErrorReport(props.onError, shown);
66
+ useReady(props.onReady, shown);
116
67
  /*
117
68
  * THE EMBED IS ITSELF A LANDMARK.
118
69
  *
@@ -127,27 +78,29 @@ export function Embed(props) {
127
78
  * the docs warn about, said out loud by `landmark-unique` instead of
128
79
  * silently.
129
80
  */
130
- return (_jsxs("section", { ref: rootRef, className: scope, "aria-label": label ?? app.name, "data-graview-embed": face, style: { position: "relative", height, minHeight: 320, display: "flex", flexDirection: "column", overflow: "hidden", borderRadius: "var(--graview-radius, 12px)" }, children: [_jsx("style", { children: css }), _jsxs(GraviewProvider, { store: store, views: views, initialView: initialView, scheme: scheme, ...(brand ? { brand } : {}), ...(principal ? { principal } : {}), ...(props.seats ? { seats: props.seats } : {}), ...(props.presence ? { presence: props.presence } : {}),
131
- /* The reader's own text size and motion, on somebody else's page
132
- too: the answer lives on the browser, not on the installation. */
133
- settings: app.settings ?? [], children: [_jsx(Faces, { face: face, stop: stop, kinds: kinds, places: views.places() }), toggle && face !== "picture" ? _jsx(Strip, { app: app, face: face, onFace: props.onFace, standing: standing, seats: props.seats, principal: principal, onSeat: props.onSeat }) : null, face === "picture" ? (
134
- /*
135
- * The lens fills the frame: a one-row grid stretches it to the
136
- * height it was given, and it scrolls inside itself past that.
137
- */
138
- _jsx("div", { "data-testid": "embed-picture",
139
- // A region that may scroll has to be reachable by keyboard, and a
140
- // reachable region has to say what it is: the picture's own name.
141
- tabIndex: 0, "aria-label": views.places().find((place) => place.as === (stop ? fromUrl(stop).within?.["view"] : undefined))?.title ?? "The picture", style: {
142
- flex: "1 1 auto",
143
- minHeight: 0,
144
- display: "grid",
145
- gridTemplateRows: "minmax(0, 1fr)",
146
- overflow: "auto",
147
- background: "var(--graview-ground)",
148
- color: "var(--graview-ink)",
149
- fontFamily: "var(--graview-font-body, system-ui)",
150
- }, children: _jsx(PlacePicture, { store: store, views: views, as: (stop ? fromUrl(stop).within?.["view"] : undefined) ?? "" }) })) : face === "pages" ? (_jsx("div", { style: { flex: "1 1 auto", minHeight: 0, overflow: "auto" }, children: _jsx(PagesApp, { context: { store, embedded: true, ...(brand ? { brand } : {}), ...(principal ? { principal } : {}), ...(props.seats ? { seats: props.seats } : {}) }, ...(pages ? { registry: pages } : {}), initialPath: path }) })) : (_jsxs("div", { style: { position: "relative", flex: "1 1 auto", minHeight: 0, containerType: "size" }, children: [_jsx(Scene, { renderer: "dom" }), _jsx(OverviewButton, {}), _jsx(Companion, {}), _jsx(Inspector, { placement: "menu" })] }))] })] }));
81
+ return (_jsxs("section", { ref: rootRef, className: scope, "aria-label": props.label ?? app.name, "data-graview-embed": shown, "data-graview-scheme": scheme, ...(narrow ? { "data-graview-embed-narrow": "" } : {}), style: { position: "relative", height, minHeight: auto ? 0 : 320, display: "flex", flexDirection: "column", overflow: "hidden", borderRadius: "var(--graview-radius, 12px)" }, children: [_jsx("style", { children: css }), heading !== false && shown !== "pages" ? _jsx(HeadingAt, { level: heading, children: props.label ?? app.name }) : null, _jsx(ErrorReportContext.Provider, { value: report, children: _jsx(FaceBoundary, { module: "@graview/react", report: report, content: true, children: _jsxs(GraviewProvider, { store: store, views: views, initialView: initialView, scheme: scheme, ...providerProps(props, presence, brand), children: [_jsx(Faces, { face: shown, stop: stop, kinds: kinds, places: views.places() }), toggle && shown !== "picture" ? (_jsx(FaceBoundary, { module: "@graview/embed", report: report, children: _jsx(EmbedStrip, { app: app, studio: props.studio, face: shown, narrow: narrow, onFace: props.onFace, standing: standing, seats: props.seats, principal: principal, onSeat: props.onSeat, report: report }) })) : null, _jsx(FaceBoundary, { module: shown === "pages" || shown === "picture" ? "@graview/pages" : "@graview/react", report: report, content: true, children: shown === "picture" ? (
82
+ /*
83
+ * The lens fills the frame: a one-row grid stretches it to the
84
+ * height it was given, and it scrolls inside itself past that.
85
+ */
86
+ _jsx("div", { "data-testid": "embed-picture", "data-embed-content": "",
87
+ // A region that may scroll has to be reachable by keyboard, and a
88
+ // reachable region has to say what it is: the picture's own name.
89
+ tabIndex: 0, "aria-label": views.places().find((place) => place.as === (stop ? fromUrl(stop).within?.["view"] : undefined))?.title ?? "The picture", style: {
90
+ flex: "1 1 auto",
91
+ minHeight: 0,
92
+ display: "grid",
93
+ gridTemplateRows: "minmax(0, 1fr)",
94
+ overflow: "auto",
95
+ background: "var(--graview-ground)",
96
+ color: "var(--graview-ink)",
97
+ fontFamily: "var(--graview-font-body, system-ui)",
98
+ }, children: _jsx(PlacePicture, { store: store, views: views, as: (stop ? fromUrl(stop).within?.["view"] : undefined) ?? "" }) })) : shown === "pages" ? (_jsx(PagesContent, { store: store, views: views, presence: presence, auto: auto, brand: brand, props: props })) : (_jsxs("div", { "data-embed-content": "", style: { position: "relative", flex: auto ? `0 0 ${AUTO_SCENE_HEIGHT}px` : "1 1 auto", minHeight: 0, containerType: "size" }, children: [_jsx(Scene, { renderer: "dom" }), _jsx(OverviewButton, {}), _jsx(Companion, {}), _jsx(Inspector, { placement: "menu" })] })) }, shown)] }) }) })] }));
99
+ }
100
+ /** A heading at a level the host chose, heard and not seen. */
101
+ function HeadingAt({ level, children }) {
102
+ const Tag = `h${level}`;
103
+ return _jsx(Tag, { style: { ...VISUALLY_HIDDEN, margin: 0 }, children: children });
151
104
  }
152
105
  /** Keeps the scene's view in step with the face and stop props. */
153
106
  function Faces({ face, stop, kinds, places }) {
@@ -178,88 +131,47 @@ function Faces({ face, stop, kinds, places }) {
178
131
  }, [face, stop, kinds, view, go]);
179
132
  return null;
180
133
  }
181
- function Strip({ app, face, onFace, standing, seats, principal, onSeat, }) {
182
- const { brand } = useGraview();
183
- const sameSeat = (a, b) => a !== undefined && a.id === b.id && a.kind === b.kind;
184
- /*
185
- * COMPACT BELOW A PHONE'S WIDTH: the places and the seats as one select
186
- * each rather than a pill per name. At 360px the pills wrapped to five
187
- * rows and the strip was taller than the picture under it.
188
- */
189
- const strip = useRef(null);
190
- const width = useWidth(strip);
191
- const compact = width !== null && width < 560;
134
+ /** The strip, with the scene's faces and its own controls. */
135
+ function EmbedStrip({ app, studio, face, narrow = false, onFace, standing, seats, principal, onSeat, report, }) {
192
136
  // Two faces, not three: altitude is the scene's own control, on the
193
137
  // picture, and a third pill for it here said the same thing twice.
194
- const faces = [
195
- { id: "scene", label: "Scene", title: "The picture — rise and descend on it", pressed: face !== "pages" },
196
- { id: "pages", label: "Pages", title: "The same app as ordinary pages", pressed: face === "pages" },
197
- ];
198
- return (_jsxs("div", { ref: strip, role: "group", "aria-label": "Face", "data-testid": "embed-faces", "data-embed-strip": compact ? "compact" : "full", style: {
199
- display: "flex",
200
- alignItems: "center",
201
- // Wraps rather than clips: at a phone's width Standing takes the
202
- // next line instead of losing its last word.
203
- flexWrap: "wrap",
204
- gap: 6,
205
- padding: "8px 12px",
206
- flex: "0 0 auto",
207
- borderBottom: "1px solid var(--graview-edge)",
208
- background: "var(--graview-bar)",
209
- fontSize: "0.875rem",
210
- }, children: [_jsx("span", { style: { fontFamily: "var(--graview-font-display)", letterSpacing: "0.12em", textTransform: "uppercase", fontSize: "0.75rem", marginRight: 6 }, children: brand?.name ?? "Graview" }), faces.map((candidate) => (_jsx("button", { type: "button", "aria-pressed": candidate.pressed, "data-testid": `embed-face-${candidate.id}`, title: candidate.title, onClick: () => onFace?.(candidate.id), style: {
211
- padding: "3px 11px",
212
- borderRadius: 999,
213
- fontSize: "0.875rem",
214
- borderWidth: 1,
215
- borderStyle: "solid",
216
- borderColor: candidate.pressed ? "var(--graview-accent)" : "var(--graview-edge)",
217
- color: candidate.pressed ? "var(--graview-accent)" : "var(--graview-ink-muted)",
218
- background: candidate.pressed ? "var(--graview-panel)" : "transparent",
219
- }, children: candidate.label }, candidate.id))), face !== "pages" ? _jsx(Places, { compact: compact }) : null, face !== "pages" ? _jsx(ShowInstallation, {}) : null, face !== "pages" ? _jsx(StudioPlace, { app: app, within: "box" }) : null, _jsx(Profile, {}), seats && seats.length > 1 && compact ? (_jsx("select", { "aria-label": "Seat", "data-testid": "embed-seats", value: seats.find((seat) => sameSeat(principal, seat.principal))?.principal.id ?? "", onChange: (event) => {
220
- const seat = seats.find((candidate) => candidate.principal.id === event.target.value);
221
- if (seat)
222
- onSeat?.(seat.principal);
223
- }, style: {
224
- minHeight: 24,
225
- maxWidth: "46%",
226
- padding: "3px 8px",
227
- borderRadius: 999,
228
- fontSize: "0.875rem",
229
- borderWidth: 1,
230
- borderStyle: "solid",
231
- borderColor: "var(--graview-accent)",
232
- color: "var(--graview-accent)",
233
- background: "var(--graview-panel)",
234
- }, children: seats.map((seat) => (_jsxs("option", { value: seat.principal.id, children: ["As ", seat.label] }, seat.principal.id))) })) : seats && seats.length > 1 ? (_jsxs("div", { role: "group", "aria-label": "Seat", "data-testid": "embed-seats", style: { display: "flex", alignItems: "center", gap: 6, marginLeft: 6 }, children: [_jsx("span", { style: { fontSize: "0.75rem", letterSpacing: "0.12em", textTransform: "uppercase", color: "var(--graview-ink-faint)" }, children: "As" }), seats.map((seat) => {
235
- const pressed = sameSeat(principal, seat.principal);
236
- return (_jsx("button", { type: "button", "aria-pressed": pressed, "data-testid": `embed-seat-${seat.principal.id}`, title: `Sit down as ${seat.label}: the strip, the pages and the acts narrow to what this seat may do`, onClick: () => onSeat?.(seat.principal), style: {
237
- padding: "3px 11px",
238
- borderRadius: 999,
239
- fontSize: "0.875rem",
240
- borderWidth: 1,
241
- borderStyle: "solid",
242
- borderColor: pressed ? "var(--graview-accent)" : "var(--graview-edge)",
243
- color: pressed ? "var(--graview-accent)" : "var(--graview-ink-muted)",
244
- background: pressed ? "var(--graview-panel)" : "transparent",
245
- }, children: seat.label }, seat.principal.id));
246
- })] })) : null, _jsx("div", { style: { marginLeft: "auto", minWidth: 0 }, children: _jsx(Standing, { clean: standing }) })] }));
138
+ const faces = narrow
139
+ ? []
140
+ : [
141
+ { id: "scene", label: "Scene", title: "The picture — rise and descend on it", pressed: face !== "pages" },
142
+ { id: "pages", label: "Pages", title: "The same app as ordinary pages", pressed: face === "pages" },
143
+ ];
144
+ return (_jsx(Strip, { standing: standing, seats: seats, principal: principal, onSeat: onSeat, faces: faces.map((candidate) => (_jsx("button", { type: "button", "aria-pressed": candidate.pressed, "data-testid": `embed-face-${candidate.id}`, title: candidate.title, onClick: () => onFace?.(candidate.id), style: {
145
+ padding: "3px 11px",
146
+ borderRadius: 999,
147
+ fontSize: "0.875rem",
148
+ borderWidth: 1,
149
+ borderStyle: "solid",
150
+ borderColor: candidate.pressed ? "var(--graview-accent)" : "var(--graview-edge)",
151
+ color: candidate.pressed ? "var(--graview-accent)" : "var(--graview-ink-muted)",
152
+ background: candidate.pressed ? "var(--graview-panel)" : "transparent",
153
+ }, children: candidate.label }, candidate.id))), scene: face === "pages"
154
+ ? undefined
155
+ : (compact) => (_jsxs(_Fragment, { children: [_jsx(Places, { compact: compact }), _jsx(ShowInstallation, {}), studio !== false ? (_jsx(FaceBoundary, { module: "@graview/studio", report: report, children: _jsx(StudioPlace, { app: app, within: "box", ...(studio ? { onApply: studio.onApply } : {}) }) })) : null] })) }));
247
156
  }
248
157
  /**
249
158
  * Mounts an app into an element and hands back the controls. The first
250
159
  * render is synchronous, so what comes back is already on the page.
251
160
  */
252
161
  export function mount(element, options) {
253
- const store = options.store ?? storeOf(options.app, options.seed);
162
+ const store = options.store ?? options.remote?.store ?? storeOf(options.app, options.seed);
254
163
  let setters = null;
255
164
  function Host() {
256
165
  const [face, setFace] = useState(options.face ?? faceOf(options.stop));
257
166
  const [stop, setStop] = useState(options.stop);
258
167
  const [scheme, setScheme] = useState(options.scheme ?? "auto");
259
168
  const [principal, setSeat] = useState(options.principal);
169
+ const [seats, setSeats] = useState(options.seats);
170
+ const [people, setPeople] = useState(options.people);
171
+ const [hostContext, setHostContext] = useState(options.hostContext);
260
172
  const [brand, setBrand] = useState(options.brand);
261
- setters = { face: setFace, stop: setStop, scheme: setScheme, seat: setSeat, brand: setBrand };
262
- return (_jsx(Embed, { ...options, store: store, face: face, ...(stop !== undefined ? { stop } : {}), ...(principal ? { principal } : {}), ...(brand ? { brand } : {}), scheme: scheme, onFace: setFace, onSeat: setSeat }));
173
+ setters = { face: setFace, stop: setStop, scheme: setScheme, seat: setSeat, seats: setSeats, people: setPeople, hostContext: setHostContext, brand: setBrand };
174
+ return (_jsx(Embed, { ...options, store: store, face: face, ...(stop !== undefined ? { stop } : {}), ...(principal ? { principal } : {}), ...(seats ? { seats } : {}), ...(people ? { people } : {}), ...(hostContext ? { hostContext } : {}), ...(brand ? { brand } : {}), scheme: scheme, onFace: setFace, onSeat: setSeat }));
263
175
  }
264
176
  const root = createRoot(element);
265
177
  flushSync(() => root.render(_jsx(Host, {})));
@@ -269,6 +181,9 @@ export function mount(element, options) {
269
181
  setStop: (stop) => flushSync(() => setters?.stop(stop)),
270
182
  setScheme: (scheme) => flushSync(() => setters?.scheme(scheme)),
271
183
  setSeat: (principal) => flushSync(() => setters?.seat(principal)),
184
+ setSeats: (seats) => flushSync(() => setters?.seats(seats)),
185
+ setPeople: (people) => flushSync(() => setters?.people(people)),
186
+ setHostContext: (context) => flushSync(() => setters?.hostContext(context)),
272
187
  setBrand: (brand) => flushSync(() => setters?.brand(brand)),
273
188
  unmount: () => root.unmount(),
274
189
  };