Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/components/shell/IframeHost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,25 @@ export function IframeHost({
bridge.dispose();
bridgeRef.current = null;
};
}, [app, app.url, app.trust, webId, workspacePod, scope, allowWrite, project, podFetch, onResize, onOpen]);
// `project` is intentionally NOT a dep: a project switch must NOT tear down
// and recreate the bridge (its proactive welcome would go out at the
// un-negotiated host version and a v1 app drops it). It's pushed to the live
// bridge via setProject below, exactly like theme.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [app, app.url, app.trust, webId, workspacePod, scope, allowWrite, podFetch, onResize, onOpen]);

// Push shell theme changes to the live bridge so the embedded app's chrome
// tracks the shell without re-mounting the iframe.
useEffect(() => {
bridgeRef.current?.setTheme(theme);
}, [theme]);

// Push project switches to the live bridge so an embedded project-scoped app
// (Projects) follows the shell's switcher — again without re-mounting the frame.
useEffect(() => {
bridgeRef.current?.setProject(project ? { id: project.id, name: project.name } : null);
}, [project]);

if (!app.url) {
return (
<div className="grid h-full place-items-center p-8 text-sm text-muted-foreground">
Expand Down
11 changes: 11 additions & 0 deletions src/lib/shell/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ export interface BridgeOptions {
export interface Bridge {
/** Push a new color mode to the app (re-broadcasts welcome). Idempotent. */
setTheme(theme: BridgeTheme): void;
/** Push a project switch to the app (re-broadcasts welcome). Idempotent.
* Mirrors {@link setTheme}: the frame stays mounted and the negotiated
* protocol version is preserved, so the welcome actually reaches the app —
* unlike tearing down and recreating the bridge, whose proactive welcome
* goes out at the un-negotiated host version and a v1 app drops it. */
setProject(project: BridgeIdentity["project"]): void;
dispose(): void;
}

Expand Down Expand Up @@ -392,6 +398,11 @@ export function createBridge(opts: BridgeOptions): Bridge {
currentTheme = theme;
welcome();
},
setProject(project: BridgeIdentity["project"]) {
if ((currentIdentity.project?.id ?? null) === (project?.id ?? null)) return;
currentIdentity = { ...currentIdentity, project };
welcome();
},
dispose() {
window.removeEventListener("message", onMessage);
unsubscribe();
Expand Down
11 changes: 11 additions & 0 deletions src/lib/shell/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ const NOTES_URL = siblingUrl(process.env.NEXT_PUBLIC_APP_NOTES_URL, 3120, "/note
const CONTACTS_URL = siblingUrl(process.env.NEXT_PUBLIC_APP_CONTACTS_URL, 3130, "/contacts");
const CALENDAR_URL = siblingUrl(process.env.NEXT_PUBLIC_APP_CALENDAR_URL, 3140, "/calendar");
const PHOTOS_URL = siblingUrl(process.env.NEXT_PUBLIC_APP_PHOTOS_URL, 3150, "/photos");
// Projects serves its app surface at the origin root (no sub-route).
const PROJECTS_URL = siblingUrl(process.env.NEXT_PUBLIC_APP_PROJECTS_URL, 3160, "");
// Slides (mind-slides-v0) is embedded at its `/studio` route. It brokers like the
// rest of the suite (its `src/lib/solid/broker.ts` does the v1 handshake), so
// embedded it uses the shell's SSO + pod — decks save to `{shellPod}mind-slides/
Expand Down Expand Up @@ -245,6 +247,15 @@ function builtinApps(): HostedApp[] {
trust: "first-party",
widgets: [RECENT_WIDGET],
},
{
key: "projects",
label: "Projects",
icon: "📋",
enabled: true,
url: PROJECTS_URL,
embed: "iframe",
trust: "first-party",
},
{
key: "notes",
label: "Notes",
Expand Down
Loading