// Full-content-area host for plugin pages (app.page, SPEC §11.3). // // Mounted at "/p/*": matches the sub-path against declared page routes and mounts // the page bundle into the whole content area (no mail list/thread). A minimal // header keeps a way back to Mail. (Sidebar persistence is a later refinement.) import { useEffect, useMemo, useRef } from "react"; import { Link, Navigate, useLocation } from "react-router-dom"; import { useT } from "../i18n"; import { loadBundle } from "./bundleLoader"; import { usePluginContext } from "./context"; import { usePluginManifest, usePluginPages } from "./manifest"; import type { PluginUnmount } from "./types"; function PageMount({ pluginName, bundle, subpath, }: { pluginName: string; bundle: string; subpath: string; }) { const ref = useRef(null); const ctx = usePluginContext(pluginName); const props = useMemo(() => ({ subpath }), [subpath]); useEffect(() => { let cleanup: PluginUnmount | void; let alive = true; loadBundle(bundle) .then((mount) => { if (alive && ref.current) cleanup = mount(ref.current, ctx, props); }) .catch((err) => console.error(`plugin page ${pluginName} failed`, err)); return () => { alive = false; if (typeof cleanup === "function") cleanup(); }; }, [bundle, ctx, props, pluginName]); return
; } export function PluginPage() { const t = useT(); const location = useLocation(); const { data, isLoading } = usePluginManifest(); const pages = usePluginPages(); // Sub-path under "/p" (e.g. "/calendar/2026-07"); match the longest page route. const subpath = location.pathname.replace(/^\/p/, "") || "/"; const match = pages.find((p) => subpath === p.route || subpath.startsWith(`${p.route}/`)); if (isLoading || !data) { return
{t("common.loading")}
; } if (!match) return ; return (
← {t("nav.mail")} {match.nav.label}
); }