diff --git a/.changeset/host-slot-registry.md b/.changeset/host-slot-registry.md new file mode 100644 index 0000000..43e1163 --- /dev/null +++ b/.changeset/host-slot-registry.md @@ -0,0 +1,5 @@ +--- +"sideshow": minor +--- + +Add a host-extension seam so a wrapping deployment can render its own components inside the viewer chrome without forking it. The viewer now publishes its own Solid runtime (`window.__SIDESHOW_SOLID__`) and a reactive slot registry (`window.sideshow.registerSlot(name, Component)`), renders registered components into named chrome slots (`account` in the topbar) inside its root owner so theme/context/signals are shared, and dispatches a `sideshow:ready` event once both are live. `createApp` gains an optional `headHtml` option for splicing extra `
` markup (e.g. a companion bundle loader) at request time. All inert for self-hosted deployments, which register no slots. diff --git a/server/app.ts b/server/app.ts index 0a78b9a..d964f07 100644 --- a/server/app.ts +++ b/server/app.ts @@ -104,6 +104,14 @@ export interface AppOptions { // stripped routes like /api/sessions and /s/:id?part=0; this prefix is only // used when the server/viewer generate browser-visible URLs. basePath?: BasePathHook; + // Extra HTML spliced into the viewer at request time, right after the + // base-path config script. Hosts (e.g. a SaaS wrapper) use it to load a + // companion bundle or publish additional window globals before the viewer + // boots. Kept generic — the core ships no host-specific markup; self-hosters + // simply leave it unset. The string is inserted verbatim into the HTML + // response, so it must be deployment-controlled — never built from untrusted + // or user-supplied request data without escaping. + headHtml?: string | ((request: Request) => string); // Update notice: the running version and the upgrade hint that fits this // deployment (npm install vs redeploy). Without `version`, /api/version // reports nothing and the viewer shows no notice. @@ -217,6 +225,7 @@ export function createApp({ authenticate, authToken, basePath, + headHtml, version, upgradeCommand, fetchLatestRelease, @@ -491,7 +500,8 @@ export function createApp({ text.replaceAll(LOCAL_ORIGIN, new URL(c.req.url).origin); const withViewerConfig = (text: string, request: Request) => { - const script = ``; + const extra = typeof headHtml === "function" ? (headHtml(request) ?? "") : (headHtml ?? ""); + const script = `${extra}`; const headClose = text.lastIndexOf(""); return headClose >= 0 ? `${text.slice(0, headClose)}${script}${text.slice(headClose)}` diff --git a/viewer/src/App.tsx b/viewer/src/App.tsx index 76a5d30..0449274 100644 --- a/viewer/src/App.tsx +++ b/viewer/src/App.tsx @@ -5,6 +5,7 @@ import { Card, cardEls, frameForSource } from "./Card.tsx"; import { applyFrameHeight } from "./SandboxedPart.tsx"; import { renderNotes } from "./notes.ts"; import { SessionTimeline } from "./SessionTimeline.tsx"; +import { Slot } from "./host.tsx"; import { activeTheme, initTheme, setTheme, themeOptions } from "./theme.ts"; import { checkVersion, @@ -121,6 +122,13 @@ export default function App() {