@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 +79 -1
- package/dist/embed.d.ts +30 -60
- package/dist/embed.d.ts.map +1 -1
- package/dist/embed.js +85 -170
- package/dist/embed.js.map +1 -1
- package/dist/frame.d.ts +255 -0
- package/dist/frame.d.ts.map +1 -0
- package/dist/frame.js +335 -0
- package/dist/frame.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/pages.d.ts +44 -0
- package/dist/pages.d.ts.map +1 -0
- package/dist/pages.js +48 -0
- package/dist/pages.js.map +1 -0
- package/package.json +12 -8
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 {
|
|
2
|
-
import { type
|
|
3
|
-
import { type Scheme
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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
|
|
29
|
+
readonly heading?: 1 | 2 | 3 | 4 | 5 | 6 | false;
|
|
44
30
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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
|
|
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
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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
|
|
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>;
|
package/dist/embed.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../src/embed.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
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 {
|
|
5
|
-
import { Companion, Inspector, OverviewButton, Places,
|
|
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 {
|
|
8
|
-
import {
|
|
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 {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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,
|
|
60
|
-
const rootRef =
|
|
61
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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":
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
};
|