diff --git a/.changeset/host-hide-brand.md b/.changeset/host-hide-brand.md new file mode 100644 index 0000000..755e69c --- /dev/null +++ b/.changeset/host-hide-brand.md @@ -0,0 +1,5 @@ +--- +"sideshow": minor +--- + +Add a `hideBrand` flag to the embed host contract. When set, the engine omits its own "sideshow" wordmark (the sidebar/header home-link brand) so a host that supplies its own branding — e.g. a workspace picker atop the sidebar and a wordmark in the footer — isn't doubled up. Self-hosted leaves it unset and shows the wordmark as before. diff --git a/e2e/embed-hide-brand.spec.ts b/e2e/embed-hide-brand.spec.ts new file mode 100644 index 0000000..c03c0b9 --- /dev/null +++ b/e2e/embed-hide-brand.spec.ts @@ -0,0 +1,55 @@ +// End-to-end proof of the `hideBrand` host flag: an embedder that supplies its own +// branding can suppress the engine's "sideshow" wordmark. With the flag off +// (self-hosted default) the wordmark renders as before, so parity holds. +// +// Same harness as embed-main-slot.spec.ts. +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { expect, publish, test } from "./fixtures.ts"; + +const embedDir = fileURLToPath(new URL("../viewer/dist-embed", import.meta.url)); + +function contentType(path: string): string { + if (path.endsWith(".js") || path.endsWith(".mjs")) return "text/javascript"; + if (path.endsWith(".wasm")) return "application/wasm"; + if (path.endsWith(".css")) return "text/css"; + return "application/octet-stream"; +} + +const embedHtml = (hideBrand: boolean) => ` +
+ +`; + +async function mount(page: import("@playwright/test").Page, serverUrl: string, hideBrand: boolean) { + page.on("pageerror", (e) => console.error("[pageerror]", e.message)); + const path = `/__embedtest-brand-${hideBrand ? "off" : "on"}`; + await page.route(`**${path}`, (route) => + route.fulfill({ contentType: "text/html", body: embedHtml(hideBrand) }), + ); + await page.route("**/__embed/**", (route) => { + const name = new URL(route.request().url()).pathname.replace("/__embed/", ""); + route.fulfill({ contentType: contentType(name), body: readFileSync(`${embedDir}/${name}`) }); + }); + await page.goto(`${serverUrl}${path}`); +} + +test("hideBrand: true suppresses the engine wordmark", async ({ page, server }) => { + await publish(server.url, { html: "card
", title: "Seeded", agent: "e2e" }, ""); + await mount(page, server.url, true); + await expect(page.locator("aside")).toBeVisible(); + await expect(page.locator(".brand")).toHaveCount(0); +}); + +test("hideBrand off (self-hosted default): the wordmark renders", async ({ page, server }) => { + await publish(server.url, { html: "card
", title: "Seeded", agent: "e2e" }, ""); + await mount(page, server.url, false); + await expect(page.locator("aside .brand")).toBeVisible(); +}); diff --git a/viewer/embed.d.ts b/viewer/embed.d.ts index 687bc35..9065536 100644 --- a/viewer/embed.d.ts +++ b/viewer/embed.d.ts @@ -38,6 +38,13 @@ export interface SideshowHost { * true; self-hosted drives the same flag via a window global. Defaults to off. */ screenshots?: boolean; + /** + * Omit the engine's own "sideshow" wordmark (the sidebar/header home-link brand) + * when the host provides its own branding/header — e.g. a cloud with a workspace + * picker atop the sidebar and its own wordmark in the footer. Self-hosted leaves + * this unset and shows the wordmark. Defaults to off. + */ + hideBrand?: boolean; /** * The engine calls this with the fully-resolved palette on initial mount, on * every live theme switch, and on an OS light/dark flip — symmetric with diff --git a/viewer/src/App.tsx b/viewer/src/App.tsx index 22c82bd..5f2abe7 100644 --- a/viewer/src/App.tsx +++ b/viewer/src/App.tsx @@ -189,11 +189,17 @@ export default function App() { ☰ -