@graview/embed 0.1.10 → 0.1.14

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
@@ -49,14 +49,87 @@ const handle = mount(root, { app, face }); // drawn in this commit
49
49
  ```
50
50
 
51
51
  What this asks of the framework, and what it adds: the theme scopes to the
52
- element (`themeCss(scheme, brand, { scope })`) rather than the document, every
52
+ element (`themeBaseCss(scheme, brand, { scope })`, and the scene's own rules,
53
+ `sceneCss`, drawn by the scene face beside it) rather than the document, every
53
54
  rule of it held inside that element, so nothing of the host's is restyled; the
54
55
  panes size against the picture's own box (`cqh`) rather than the viewport;
55
56
  the routed face runs on a memory router, so the host page's address is never
56
- touched; the brand's fonts are fetched by the embed rather than assumed. The
57
+ touched (unless the host's page is the app: see below); the brand's fonts are fetched by the embed rather than assumed. The
57
58
  store is in memory and starts from the seed on every mount, unless the host
58
59
  hands it one.
59
60
 
61
+ ## When the page is the app: the address bar
62
+
63
+ A host whose whole page is the app — Graview Cloud's hosted app is one —
64
+ hands the routed face the address bar, so a place, a record and the home can
65
+ be linked, reloaded and shared:
66
+
67
+ ```ts
68
+ const handle = mount(root, {
69
+ app, store,
70
+ face: innerWidth < 768 ? "pages" : "graview", // where a bare address opens
71
+ routing: "address", // the default is "memory"
72
+ basePath: "/apps/a1/", // where the app is served; "/" by default
73
+ });
74
+ ```
75
+
76
+ The host answers every address under the base with the same page. The
77
+ address then says which face is drawn and where on it, as the whole-page
78
+ Shell spells it:
79
+
80
+ | Address | Drawn |
81
+ |---|---|
82
+ | `/apps/a1/places/the-board`, `/apps/a1/tasks/t1` | the routed face, at that page |
83
+ | `/apps/a1#overview=1`, `/apps/a1#focus=t1` | the scene, at that stop (the Graview at altitude) |
84
+ | `/apps/a1` | the routed face's home, or on arrival the host's `face` |
85
+
86
+ Each page the routed face opens is pushed, and Back returns. The scene
87
+ keeps its stop in the fragment the way the Shell does: a step is pushed,
88
+ moving the furniture replaces. The face toggle pushes the address of the
89
+ face it goes to, so Back undoes it, and the page you left is where the
90
+ toggle returns you. A reload stays where it was. `faceAtAddress(options)`
91
+ is the face an address opens on, for a host that renders `<Embed>` itself.
92
+ `placesOf(app)` gives each place's `address` within the app, and
93
+ `addressOf(place, { basePath })` from `@graview/core` gives it under the
94
+ base, spelled as the face's own links are (`pathWithin` reads one back).
95
+
96
+ `routing: "memory"`, the default, is for somebody else's page: it never
97
+ writes `history` and leaves `location` as it was. A host that keeps its
98
+ own history stays on memory routing: `onNavigate(path, how)` is told each
99
+ page the routed face opens (the path within the app, and `"push"`,
100
+ `"replace"` or `"pop"`), and `handle.setPath(path)` sends it back to one
101
+ when the host's own Back arrives.
102
+
103
+ ## When the declaration changes
104
+
105
+ A chat that changes the app hands the host a new compiled app and a new
106
+ store. `handle.setApp(app, store)` swaps them under the reader (or
107
+ `setApp(app, remote)`, whose presence comes with it), and the reader stays
108
+ where they were:
109
+
110
+ ```ts
111
+ remote.onDeclaration((next) => handle.setApp(latestApp, next));
112
+ ```
113
+
114
+ The face, the place or record open on Pages, and the scene's stop and
115
+ focus are kept. What the change took away falls back to its nearest
116
+ parent: a removed record to its kind's list (or, in the scene, its kind's
117
+ group); a removed kind or place to the home; a lens gone from the scene to
118
+ its kind's group, or the home when the kind went too. Under address
119
+ routing the address stays the source of truth, and one that names
120
+ something gone is replaced, not pushed. The seat, the seats, the people,
121
+ the scheme, the brand and the notices stay; the faces are drawn again, so
122
+ an open menu, a scroll position and a half-typed field do not. `drawn()`
123
+ resolves once the new app is on the page.
124
+
125
+ A host that must remount reads the place first and hands it back:
126
+
127
+ ```ts
128
+ const at = handle.where(); // { face, path, stop, kind? }
129
+ handle.unmount();
130
+ handle = mount(root, { app, store, at });
131
+ ```
132
+
60
133
  ## What stands over what
61
134
 
62
135
  Every popover, menu and list of suggestions the embed draws — the profile,
@@ -0,0 +1,34 @@
1
+ import { type Place } from "@graview/core";
2
+ import { type MutableRefObject } from "react";
3
+ import type { EmbedFace, FrameOptions } from "./frame.js";
4
+ /**
5
+ * THE FACE AN EMBED OPENS ON (FR-106): under memory routing, the host's
6
+ * (`face`, else the one its `stop` implies); under address routing, the one
7
+ * the address names — a page past the home is the routed face, a fragment
8
+ * at the home is the scene's stop — and the host's when it names none.
9
+ * `mount` asks this itself; a host that renders `<Embed>` passes it as `face`.
10
+ */
11
+ export declare function faceAtAddress(options: Pick<FrameOptions, "routing" | "basePath"> & {
12
+ readonly face?: EmbedFace;
13
+ readonly stop?: string;
14
+ }): EmbedFace;
15
+ /** The stop the address holds for the scene on arrival, if it holds one. */
16
+ export declare function stopAtAddress(basePath: string | undefined): string | undefined;
17
+ /**
18
+ * Keeps the face in step with the address: Back and Forward move between
19
+ * the routed face and the scene, and the face toggle pushes the address of
20
+ * the face it goes to. Inside the provider; the scene's own fragment sync
21
+ * (`UrlSync`) is drawn by the scene face, so it is fetched with the scene.
22
+ */
23
+ export declare function AddressBar({ basePath, pagesWere, shown, onFace, toggle, kinds, places, }: {
24
+ readonly basePath: string | undefined;
25
+ /** The page the routed face had before a new declaration (FR-116), for the toggle to go back to. */
26
+ readonly pagesWere?: string | undefined;
27
+ readonly shown: EmbedFace;
28
+ readonly onFace: ((face: EmbedFace) => void) | undefined;
29
+ /** Where the strip's toggle is handed this bar's way of changing face. */
30
+ readonly toggle: MutableRefObject<((face: EmbedFace) => void) | undefined>;
31
+ readonly kinds: readonly string[];
32
+ readonly places: readonly Place[];
33
+ }): null;
34
+ //# sourceMappingURL=address.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../src/address.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,KAAK,EAAE,MAAM,eAAe,CAAC;AAIlE,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,OAAO,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA+B1D;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,SAAS,GAAG,UAAU,CAAC,GAAG;IAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAIpJ;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAG9E;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,SAAS,EACT,KAAK,EACL,MAAM,EACN,MAAM,EACN,KAAK,EACL,MAAM,GACP,EAAE;IACD,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,oGAAoG;IACpG,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;IACzD,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC;IAC3E,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;CACnC,QA8CA"}
@@ -0,0 +1,109 @@
1
+ import { addressOf, pathWithin } from "@graview/core";
2
+ import { aggregateId, fromUrl, toUrl, withFocus, withOverview } from "@graview/layout/view";
3
+ import { descentTarget } from "@graview/primitives/frame";
4
+ import { useNavigation } from "@graview/react/provider";
5
+ import { useEffect, useRef } from "react";
6
+ /*
7
+ * THE ADDRESS BAR, FOR A HOST WHOSE PAGE IS THE APP (FR-106).
8
+ *
9
+ * Under `routing: "address"` the address says which face is drawn and
10
+ * where on it, the way the whole-page Shell spells it:
11
+ *
12
+ * <base>/places/the-board, <base>/tasks/t1 a page: the routed face, on its router
13
+ * <base>#overview=1, <base>#focus=t1 a stop: the scene (the Graview at altitude),
14
+ * its view state in the fragment
15
+ * <base> the routed face's home — or, on arrival
16
+ * with no entry the router wrote, the host's face
17
+ *
18
+ * The scene always writes a fragment, even an empty `#`, so an address
19
+ * without one past arrival is a page. The face toggle is a step like any
20
+ * other: it pushes the address of the face it goes to, and Back undoes it.
21
+ */
22
+ /** The router's own mark on an entry it wrote: `{ idx }` (react-router), kept across a reload. */
23
+ const routerWrote = (state) => typeof state === "object" && state !== null && "idx" in state;
24
+ /** The face an address names, or null when it names none: a bare home the router did not write, or outside the base. */
25
+ function faceNamed(basePath) {
26
+ const path = pathWithin(window.location.pathname, basePath);
27
+ if (path === null)
28
+ return null;
29
+ if (path !== "/")
30
+ return "pages";
31
+ if (window.location.href.includes("#"))
32
+ return fromUrl(window.location.hash).overview ? "graview" : "scene";
33
+ return routerWrote(window.history.state) ? "pages" : null;
34
+ }
35
+ /**
36
+ * THE FACE AN EMBED OPENS ON (FR-106): under memory routing, the host's
37
+ * (`face`, else the one its `stop` implies); under address routing, the one
38
+ * the address names — a page past the home is the routed face, a fragment
39
+ * at the home is the scene's stop — and the host's when it names none.
40
+ * `mount` asks this itself; a host that renders `<Embed>` passes it as `face`.
41
+ */
42
+ export function faceAtAddress(options) {
43
+ const asked = options.face ?? (options.stop && fromUrl(options.stop).overview ? "graview" : "scene");
44
+ if (options.routing !== "address" || typeof window === "undefined")
45
+ return asked;
46
+ return faceNamed(options.basePath) ?? asked;
47
+ }
48
+ /** The stop the address holds for the scene on arrival, if it holds one. */
49
+ export function stopAtAddress(basePath) {
50
+ if (typeof window === "undefined")
51
+ return undefined;
52
+ return pathWithin(window.location.pathname, basePath) === "/" && window.location.href.includes("#") ? window.location.hash || "#" : undefined;
53
+ }
54
+ /**
55
+ * Keeps the face in step with the address: Back and Forward move between
56
+ * the routed face and the scene, and the face toggle pushes the address of
57
+ * the face it goes to. Inside the provider; the scene's own fragment sync
58
+ * (`UrlSync`) is drawn by the scene face, so it is fetched with the scene.
59
+ */
60
+ export function AddressBar({ basePath, pagesWere, shown, onFace, toggle, kinds, places, }) {
61
+ const { view, go } = useNavigation();
62
+ const latest = useRef({ shown, view, onFace });
63
+ latest.current = { shown, view, onFace };
64
+ // The page the routed face was on when the scene was asked for, to go back to by the toggle.
65
+ const pagesAt = useRef(pagesWere ?? "/");
66
+ const leaving = () => {
67
+ pagesAt.current = `${pathWithin(window.location.pathname, basePath) ?? "/"}${window.location.search}`;
68
+ };
69
+ toggle.current = (next) => {
70
+ const { shown: from, view: here, onFace: tell } = latest.current;
71
+ if (next === "pages" && from !== "pages") {
72
+ window.history.pushState(null, "", addressOf(pagesAt.current, { basePath }));
73
+ }
74
+ else if (next !== "pages" && from === "pages") {
75
+ leaving();
76
+ // The view the scene will draw, decided here so the fragment pushed is the one it lands on.
77
+ const stop = next === "graview" ? withOverview(here, true) : here.overview ? withFocus(withOverview(here, false), descentTarget(here, kinds)) : here;
78
+ go(stop);
79
+ window.history.pushState(null, "", `${addressOf("/", { basePath })}${toUrl(stop)}`);
80
+ }
81
+ tell?.(next);
82
+ };
83
+ useEffect(() => {
84
+ const onPop = () => {
85
+ if (pathWithin(window.location.pathname, basePath) === null)
86
+ return;
87
+ // Past arrival, a bare home is the routed face's.
88
+ const named = faceNamed(basePath) ?? "pages";
89
+ const { shown: from, onFace: tell } = latest.current;
90
+ if (named === "pages") {
91
+ if (from !== "pages")
92
+ tell?.("pages");
93
+ return;
94
+ }
95
+ // Between the scene and the Graview the fragment sync follows by itself; from the pages, the scene comes back where the fragment says.
96
+ if (from !== "pages")
97
+ return;
98
+ const parsed = fromUrl(window.location.hash);
99
+ const name = parsed.within?.["view"];
100
+ const place = name !== undefined && !parsed.focusId ? places.find((candidate) => candidate.as === name) : undefined;
101
+ go(place ? { ...parsed, focusId: aggregateId(place.kind) } : parsed);
102
+ tell?.(named);
103
+ };
104
+ window.addEventListener("popstate", onPop);
105
+ return () => window.removeEventListener("popstate", onPop);
106
+ }, [basePath, go, places]);
107
+ return null;
108
+ }
109
+ //# sourceMappingURL=address.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address.js","sourceRoot":"","sources":["../src/address.tsx"],"names":[],"mappings":"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"}
package/dist/embed.d.ts CHANGED
@@ -1,9 +1,10 @@
1
- import type { AnySchema, Brand, Person, Principal, Store } from "@graview/core";
1
+ import { type AnySchema, type Brand, type GraviewApp, type Person, type Principal, type Store } from "@graview/core";
2
2
  import type { StudioOffered, StudioOnApply, StudioPlace as StudioPlaceType } from "@graview/studio";
3
3
  import type { CompanionMode } from "@graview/primitives";
4
4
  import { type Notice, type NoticeHandle } from "@graview/primitives/frame";
5
5
  import { type Scheme } from "@graview/react/provider";
6
- import { AUTO_SCENE_HEIGHT, type EmbedFace, type EmbedHostContext, type FrameOptions } from "./frame.js";
6
+ import type { EmbedWhere } from "./where.js";
7
+ import { AUTO_SCENE_HEIGHT, type EmbedFace, type EmbedHostContext, type EmbedRemote, type FrameOptions } from "./frame.js";
7
8
  /**
8
9
  * FETCH A FACE BEFORE IT IS DRAWN (FR-57). A host that knows which face it
9
10
  * will open on — Graview Cloud's shell knows from the window's width before
@@ -20,7 +21,8 @@ export declare function preload(...faces: readonly EmbedFace[]): Promise<void>;
20
21
  * one element. So the theme is scoped to that element, the panes size
21
22
  * against the picture's own box, the routed face runs on a memory router,
22
23
  * the store lives in memory and starts from the seed, and the view state is
23
- * a prop rather than a URL. Everything else — the scene, the rails, the
24
+ * a prop rather than a URL — unless the host's page IS the app and it hands
25
+ * over the address bar (`routing: "address"`, FR-106). Everything else — the scene, the rails, the
24
26
  * inspector, the pages — is the framework's own, unchanged.
25
27
  */
26
28
  export { AUTO_SCENE_HEIGHT };
@@ -65,6 +67,13 @@ export interface EmbedOptions<S extends AnySchema = AnySchema> extends FrameOpti
65
67
  * `"hidden"`, which is the host's to say.
66
68
  */
67
69
  readonly companion?: CompanionMode;
70
+ /**
71
+ * WHERE TO OPEN, AS `handle.where()` SAID IT (FR-116): the face, the page
72
+ * on Pages and the scene's stop, over `face`, `path` and `stop`, settled
73
+ * in this app — what it no longer has falls back to its nearest parent. A
74
+ * host that must remount on a new declaration hands the old place back here.
75
+ */
76
+ readonly at?: EmbedWhere;
68
77
  }
69
78
  /** The studio an embed offers, for a host that keeps the declaration itself. */
70
79
  export interface EmbedStudio {
@@ -110,8 +119,28 @@ export interface EmbedProps<S extends AnySchema = AnySchema> extends EmbedOption
110
119
  export declare function faceOf(stop: string | undefined): EmbedFace;
111
120
  export declare function Embed<S extends AnySchema>(props: EmbedProps<S>): import("react").JSX.Element;
112
121
  export interface EmbedHandle {
122
+ /**
123
+ * WHERE THE READER IS (FR-116): the face, the page on Pages (or the one it
124
+ * last had) and the scene's stop — for a host that must remount to hand
125
+ * back as `mount(…, { at })`.
126
+ */
127
+ where(): EmbedWhere;
128
+ /**
129
+ * A NEW DECLARATION UNDER THE READER (FR-116): the app and its store — or a
130
+ * remote, whose presence comes with it — swapped in place. The face, the
131
+ * page on Pages and the scene's stop are kept; what the app no longer has
132
+ * falls back to its nearest parent. The seat, the seats, the people, the
133
+ * scheme, the brand asked for and the notices stay; what is drawn is drawn
134
+ * again, so an open menu, a scroll and a half-typed field do not.
135
+ */
136
+ setApp(app: GraviewApp<AnySchema>, store: Store<AnySchema> | EmbedRemote<AnySchema>): void;
113
137
  setFace(face: EmbedFace): void;
114
138
  setStop(stop: string): void;
139
+ /**
140
+ * Sends the routed face to a path within the app's own routes (FR-106): a
141
+ * host that keeps its own history hands back the path its Back arrived at.
142
+ */
143
+ setPath(path: string): void;
115
144
  setScheme(scheme: Scheme): void;
116
145
  /** Put another principal at the keyboard; the store and its history stay. */
117
146
  setSeat(principal: Principal): void;
@@ -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,9 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { EMPTY_VIEW, aggregateId, fromUrl, withFocus, withOverview } from "@graview/layout/view";
2
+ import { pathWithin } from "@graview/core";
3
+ import { EMPTY_VIEW, aggregateId, fromUrl, toUrl, withFocus, withOverview } from "@graview/layout/view";
3
4
  import { VISUALLY_HIDDEN, descentTarget, fetchFrameworkViews, frameworkViewDoors, useWidth } from "@graview/primitives/frame";
4
5
  import { createNoticeBoard } from "@graview/primitives/frame";
5
6
  import { ErrorReportContext, GraviewProvider, openingView, useNavigation } from "@graview/react/provider";
6
- import { createElement, lazy, Suspense, useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
7
+ import { AddressBar, faceAtAddress, stopAtAddress } from "./address.js";
8
+ import { createContext, createElement, lazy, Suspense, useCallback, useContext, useLayoutEffect, useMemo, useRef, useState } from "react";
7
9
  import { createRoot } from "react-dom/client";
8
10
  import { flushSync } from "react-dom";
9
11
  import { AUTO_SCENE_HEIGHT, FaceBoundary, FrameNotices, providerProps, storeOf, Strip, useErrorReport, useFrame, useIntrinsicHeight, useReady, useViews, } from "./frame.js";
@@ -82,7 +84,8 @@ const StudioPlace = lazy(() => import("@graview/studio").then((studio) => ({ def
82
84
  * one element. So the theme is scoped to that element, the panes size
83
85
  * against the picture's own box, the routed face runs on a memory router,
84
86
  * the store lives in memory and starts from the seed, and the view state is
85
- * a prop rather than a URL. Everything else — the scene, the rails, the
87
+ * a prop rather than a URL — unless the host's page IS the app and it hands
88
+ * over the address bar (`routing: "address"`, FR-106). Everything else — the scene, the rails, the
86
89
  * inspector, the pages — is the framework's own, unchanged.
87
90
  */
88
91
  export { AUTO_SCENE_HEIGHT };
@@ -113,12 +116,40 @@ export function faceOf(stop) {
113
116
  return stop && fromUrl(stop).overview ? "graview" : "scene";
114
117
  }
115
118
  export function Embed(props) {
116
- const { app, face = faceOf(props.stop), stop, toggle = true, standing = "Everything is in order", principal, heading = 2 } = props;
119
+ const [, ready] = useState(false);
120
+ const arrived = useCallback(() => ready(true), []);
121
+ // A place handed back is settled before the embed draws: until the rules for it are here, nothing stands in the element.
122
+ return props.at && !settling ? _jsx(Settling, { onReady: arrived }) : _jsx(Drawing, { ...props });
123
+ }
124
+ function Drawing(props) {
125
+ const { app, face = props.at?.face ?? faceOf(props.stop), stop, toggle = true, standing = "Everything is in order", principal, heading = 2 } = props;
117
126
  const { rootRef, scope, css, scheme, store, presence, brand, auto, height } = useFrame(props);
118
127
  const views = useViews(props, frameworkViewDoors);
119
128
  const kinds = app.schema.kinds;
120
- // The first view only: after it, where the reader goes is theirs.
121
- const initialView = useMemo(() => viewFor(face, stop, kinds, views.places(), openingView(views, app.schema)), []);
129
+ const address = props.routing === "address";
130
+ const at = useMemo(() => (props.at ? settling.settleAt(props, store.seenBy(principal ?? { kind: "human" }), views.places()) : undefined), []);
131
+ // The first view only: after it, where the reader goes is theirs. Under address routing, a stop in the fragment is where it starts.
132
+ const initialView = useMemo(() => viewFor(face, at?.stop ?? (address ? stopAtAddress(props.basePath) : undefined) ?? stop, kinds, views.places(), openingView(views, app.schema)), []);
133
+ /*
134
+ * THE PAGE ON PAGES, KEPT (FR-116): where the routed face is, or last was,
135
+ * so the scene and back is the same page, and `where()` can say it. Under
136
+ * memory routing the routed face opens on it; under address routing the
137
+ * address is where it is.
138
+ */
139
+ const pagesAt = useRef(undefined);
140
+ pagesAt.current ??= { path: at?.path ?? (address ? (face === "pages" ? (pathWithin(window.location.pathname, props.basePath) ?? "/") + window.location.search : "/") : (props.path ?? "/")), asked: props.path };
141
+ if (props.path !== pagesAt.current.asked)
142
+ pagesAt.current = { path: props.path ?? "/", asked: props.path };
143
+ const told = useRef(props.onNavigate);
144
+ told.current = props.onNavigate;
145
+ const onNavigate = useCallback((path, how) => {
146
+ pagesAt.current.path = path;
147
+ told.current?.(path, how);
148
+ }, []);
149
+ const whereabouts = useContext(Whereabouts);
150
+ if (whereabouts)
151
+ whereabouts.pages = () => pagesAt.current.path;
152
+ const toggled = useRef(undefined);
122
153
  /*
123
154
  * NARROW, THE PAGES (FR-13). Below `pagesBelow` the scene and the Graview
124
155
  * give way to the routed face, and come back when there is room: the
@@ -130,11 +161,11 @@ export function Embed(props) {
130
161
  useIntrinsicHeight(rootRef, shown, props.onIntrinsicHeight);
131
162
  const report = useErrorReport(props.onError, shown);
132
163
  const ready = useReady(props.onReady, shown);
133
- const told = useRef(props.onDrawn);
134
- told.current = props.onDrawn;
164
+ const drawnTold = useRef(props.onDrawn);
165
+ drawnTold.current = props.onDrawn;
135
166
  const drawn = useCallback((asked, drew) => {
136
167
  ready(drew);
137
- told.current?.(asked);
168
+ drawnTold.current?.(asked);
138
169
  }, [ready]);
139
170
  /*
140
171
  * THE EMBED IS ITSELF A LANDMARK.
@@ -150,7 +181,25 @@ export function Embed(props) {
150
181
  * the docs warn about, said out loud by `landmark-unique` instead of
151
182
  * silently.
152
183
  */
153
- 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, _jsxs(ErrorReportContext.Provider, { value: report, children: [_jsx(FrameNotices, { rootRef: rootRef, board: props.notices }), _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, hostActions: props.hostActions, report: report }) })) : null, _jsx(FaceBoundary, { module: shown === "pages" || shown === "picture" ? "@graview/pages" : "@graview/react", report: report, content: true, children: _jsxs(Suspense, { fallback: _jsx(Arriving, { auto: auto, scene: shown !== "pages" && shown !== "picture" }), children: [shown === "picture" ? (_jsx(PictureFace, { 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 })) : (_jsx(SceneFace, { auto: auto, rememberAs: app.name, ...(props.companion ? { companion: props.companion } : {}) })), _jsx(Drawn, { asked: face, shown: shown, onDrawn: drawn })] }) }, shown)] }) })] })] }));
184
+ 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, _jsxs(ErrorReportContext.Provider, { value: report, children: [_jsx(FrameNotices, { rootRef: rootRef, board: props.notices }), _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() }), whereabouts ? _jsx(Watch, { into: whereabouts }) : null, address ? _jsx(AddressBar, { pagesWere: at?.path, basePath: props.basePath, shown: shown, onFace: props.onFace, toggle: toggled, kinds: kinds, places: views.places() }) : null, toggle && shown !== "picture" ? (_jsx(FaceBoundary, { module: "@graview/embed", report: report, children: _jsx(EmbedStrip, { app: app, studio: props.studio, face: shown, narrow: narrow, onFace: address ? (next) => toggled.current?.(next) : props.onFace, standing: standing, seats: props.seats, principal: principal, onSeat: props.onSeat, hostActions: props.hostActions, report: report }) })) : null, _jsx(FaceBoundary, { module: shown === "pages" || shown === "picture" ? "@graview/pages" : "@graview/react", report: report, content: true, children: _jsxs(Suspense, { fallback: _jsx(Arriving, { auto: auto, scene: shown !== "pages" && shown !== "picture" }), children: [shown === "picture" ? (_jsx(PictureFace, { 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, onNavigate, ...(address ? {} : { path: pagesAt.current.path }) } })) : (_jsx(SceneFace, { address: address, auto: auto, rememberAs: app.name, scheme: scheme, scope: scope, ...(props.companion ? { companion: props.companion } : {}) })), _jsx(Drawn, { asked: face, shown: shown, onDrawn: drawn })] }) }, shown)] }) })] })] }));
185
+ }
186
+ const Whereabouts = createContext(null);
187
+ /** Keeps the scene's view where `where()` can read it. */
188
+ function Watch({ into }) {
189
+ into.view = useNavigation().view;
190
+ return null;
191
+ }
192
+ /*
193
+ * SETTLING A PLACE IN A NEW APP, fetched when one is handed over (FR-116):
194
+ * a declaration changes rarely, and a page that never swaps one never loads
195
+ * the rules for what falls back to what.
196
+ */
197
+ let settling;
198
+ const fetchSettling = () => import("./where.js").then((module) => void (settling = module));
199
+ /** Stands in for the embed while the rules for a handed-back place arrive, then draws it. */
200
+ function Settling({ onReady }) {
201
+ useLayoutEffect(() => void fetchSettling().then(onReady), [onReady]);
202
+ return null;
154
203
  }
155
204
  /** A heading at a level the host chose, heard and not seen. */
156
205
  function HeadingAt({ level, children }) {
@@ -202,7 +251,8 @@ function EmbedStrip({ app, studio, face, narrow = false, onFace, standing, seats
202
251
  fontSize: "0.875rem",
203
252
  borderWidth: 1,
204
253
  borderStyle: "solid",
205
- borderColor: candidate.pressed ? "var(--graview-accent)" : "var(--graview-edge)",
254
+ // The face you are on wears the capsule; the other is a word to press (FR-117).
255
+ borderColor: candidate.pressed ? "var(--graview-accent)" : "transparent",
206
256
  color: candidate.pressed ? "var(--graview-accent)" : "var(--graview-ink-muted)",
207
257
  background: candidate.pressed ? "var(--graview-panel)" : "transparent",
208
258
  }, children: candidate.label }, candidate.id))), scene: face === "pages"
@@ -226,16 +276,26 @@ function StudioOnTheStrip({ app, studio }) {
226
276
  return _jsx(Handed, { ...props });
227
277
  return (_jsx(Suspense, { fallback: null, children: _jsx(StudioPlace, { ...props }) }));
228
278
  }
279
+ /** The options with a new declaration in them: where the reader was stands for the path and the stop the host first gave. */
280
+ function swapped({ path: _path, stop: _stop, ...options }, swap) {
281
+ return { ...options, app: swap.app, at: swap.at, ...(swap.remote.presence ? { presence: swap.remote.presence } : {}) };
282
+ }
229
283
  /**
230
284
  * Mounts an app into an element and hands back the controls. The first
231
285
  * render is synchronous, so what comes back is already on the page.
232
286
  */
233
287
  export function mount(element, options) {
234
- const store = options.store ?? options.remote?.store ?? storeOf(options.app, options.seed);
288
+ let store = (options.store ?? options.remote?.store ?? storeOf(options.app, options.seed));
235
289
  const board = options.notices ?? createNoticeBoard();
236
290
  let setters = null;
291
+ const here = {};
292
+ let swaps = 0;
293
+ // A new declaration on its way (FR-116): `drawn()` waits for it.
294
+ let swapping = Promise.resolve();
237
295
  // Which face is asked for, which is drawn, and who is waiting for the one asked for.
238
- let asked = options.face ?? faceOf(options.stop);
296
+ // Under address routing, the face the address names (FR-106).
297
+ const opening = faceAtAddress({ ...options, ...(options.at ? { face: options.at.face } : {}) });
298
+ let asked = opening;
239
299
  let drawnFace = null;
240
300
  let waiting = [];
241
301
  const onDrawn = (face) => {
@@ -249,29 +309,60 @@ export function mount(element, options) {
249
309
  resolve();
250
310
  };
251
311
  function Host() {
252
- const [face, setFace] = useState(options.face ?? faceOf(options.stop));
312
+ const [face, setFace] = useState(opening);
253
313
  const [stop, setStop] = useState(options.stop);
314
+ const [path, setPath] = useState(options.path);
254
315
  const [scheme, setScheme] = useState(options.scheme ?? "auto");
255
316
  const [principal, setSeat] = useState(options.principal);
256
317
  const [seats, setSeats] = useState(options.seats);
257
318
  const [people, setPeople] = useState(options.people);
258
319
  const [hostContext, setHostContext] = useState(options.hostContext);
259
320
  const [brand, setBrand] = useState(options.brand);
260
- setters = { face: setFace, stop: setStop, scheme: setScheme, seat: setSeat, seats: setSeats, people: setPeople, hostContext: setHostContext, brand: setBrand };
261
- return (_jsx(Embed, { ...options, store: store, notices: board, face: face, ...(stop !== undefined ? { stop } : {}), ...(principal ? { principal } : {}), ...(seats ? { seats } : {}), ...(people ? { people } : {}), ...(hostContext ? { hostContext } : {}), ...(brand ? { brand } : {}), scheme: scheme, onFace: (next) => {
262
- asked = next;
263
- setFace(next);
264
- }, onSeat: setSeat, onDrawn: onDrawn }));
321
+ const [swap, setSwap] = useState(undefined);
322
+ setters = {
323
+ app: (next) => {
324
+ setSwap(next);
325
+ setPath(undefined);
326
+ setStop(undefined);
327
+ },
328
+ face: setFace, stop: setStop, path: setPath, scheme: setScheme, seat: setSeat, seats: setSeats, people: setPeople, hostContext: setHostContext, brand: setBrand
329
+ };
330
+ return (_jsx(Whereabouts.Provider, { value: here, children: _jsx(Embed, { ...(swap ? swapped(options, swap) : options), store: store, notices: board, face: face, ...(stop !== undefined ? { stop } : {}), ...(path !== undefined ? { path } : {}), ...(principal ? { principal } : {}), ...(seats ? { seats } : {}), ...(people ? { people } : {}), ...(hostContext ? { hostContext } : {}), ...(brand ? { brand } : {}), scheme: scheme, onFace: (next) => {
331
+ asked = next;
332
+ setFace(next);
333
+ }, onSeat: setSeat, onDrawn: onDrawn }, swap?.n ?? 0) }));
265
334
  }
335
+ const where = () => {
336
+ const focus = here.view?.focusId;
337
+ const kind = focus ? store.graph.getNode(focus)?.kind : undefined;
338
+ return { face: asked, path: here.pages?.() ?? options.path ?? "/", stop: here.view ? toUrl(here.view) : (options.stop ?? "#"), ...(kind ? { kind } : {}) };
339
+ };
266
340
  const root = createRoot(element);
267
341
  flushSync(() => root.render(_jsx(Host, {})));
268
342
  return {
269
- store: store,
343
+ get store() {
344
+ return store;
345
+ },
346
+ where,
347
+ setApp: (app, given) => {
348
+ const remote = "graph" in given ? { store: given } : given;
349
+ // The app on the page stays drawn until the rules for settling the place in the new one are here.
350
+ const swap = () => {
351
+ const at = where();
352
+ store = remote.store;
353
+ flushSync(() => setters?.app({ app, remote, at, n: ++swaps }));
354
+ };
355
+ if (settling)
356
+ swap();
357
+ else
358
+ swapping = swapping.then(fetchSettling).then(swap);
359
+ },
270
360
  setFace: (face) => {
271
361
  asked = face;
272
362
  flushSync(() => setters?.face(face));
273
363
  },
274
364
  setStop: (stop) => flushSync(() => setters?.stop(stop)),
365
+ setPath: (path) => flushSync(() => setters?.path(path)),
275
366
  setScheme: (scheme) => flushSync(() => setters?.scheme(scheme)),
276
367
  setSeat: (principal) => flushSync(() => setters?.seat(principal)),
277
368
  setSeats: (seats) => flushSync(() => setters?.seats(seats)),
@@ -279,7 +370,7 @@ export function mount(element, options) {
279
370
  setHostContext: (context) => flushSync(() => setters?.hostContext(context)),
280
371
  setBrand: (brand) => flushSync(() => setters?.brand(brand)),
281
372
  /* Drawn is the face asked for — below `pagesBelow`, drawn as the pages that stand in for it. */
282
- drawn: () => (drawnFace === asked ? Promise.resolve() : new Promise((resolve) => waiting.push(resolve))),
373
+ drawn: () => swapping.then(() => (drawnFace === asked ? undefined : new Promise((resolve) => waiting.push(resolve)))),
283
374
  notify: (notice) => {
284
375
  let said;
285
376
  flushSync(() => {