commit 2d11f2ae4e12fc340277e1f0afa12bd9b50ade8a
parent ba8d2c971791b08053675c01371963f5cca74d84
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 25 Sep 2026 22:30:32 -0400
homepage, export: the hub and the homepage render their dark base on the server (brand S2 review)
The homepage's <html> and, in hub mode only, the export app's now carry `.dark` and `data-base="dark"` in the server markup, so a reader with no JS (or before the pre-paint script) gets the site's own base instead of the light :root fallback. A site build is unchanged: its system base cannot be resolved on the server. A reader who stored Light still gets light before paint (the script) and after hydration (ThemeProvider re-asserts the STORED base; React does not patch <html>'s attributes).
homepage theme.spec: the light reader's reload is re-checked after load, and a JS-disabled test asserts data-base="dark", .dark, data-accent="signal", no data-theme-ready (the script never ran), --background #0c0a08 and the body's background.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 45 insertions(+), 3 deletions(-)
diff --git a/export/app/layout.tsx b/export/app/layout.tsx
@@ -104,13 +104,22 @@ export default async function RootLayout({
// The accent is baked onto <html> at prerender, so it is right on first
// paint before any script: `data-accent` selects the tokens.css rule, and a
// custom hex adds its fitted per-base values inline. A reader's stored pick
- // replaces `data-accent` pre-paint (ThemeScript).
+ // replaces `data-accent` pre-paint (ThemeScript). The hub's dark base is
+ // server-rendered too (`.dark` + data-base), as on the homepage; a site's
+ // "system" base cannot be resolved on the server, so it renders none and
+ // the pre-paint script sets it.
const theme = themeDefaults();
+ const serverDark = theme.defaultBase === "dark";
return (
<html
lang="en"
suppressHydrationWarning
- className={`${fontVars} h-full antialiased`}
+ className={
+ serverDark
+ ? `${fontVars} h-full antialiased dark`
+ : `${fontVars} h-full antialiased`
+ }
+ data-base={serverDark ? "dark" : undefined}
data-accent={theme.accent}
style={(theme.accentVars as React.CSSProperties | null) ?? undefined}
>
diff --git a/homepage/app/layout.tsx b/homepage/app/layout.tsx
@@ -62,10 +62,16 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
+ // Dark is rendered on the server too (`.dark` + data-base), so a reader
+ // with no JS, or before the pre-paint script, gets this site's own base
+ // rather than the light `:root` fallback. ThemeScript then applies a
+ // reader's stored base before paint, and ThemeProvider re-asserts it after
+ // hydration (which does not patch <html>'s attributes).
<html
lang="en"
suppressHydrationWarning
- className={`${fontVars} h-full antialiased`}
+ className={`${fontVars} h-full antialiased dark`}
+ data-base="dark"
data-accent={DEFAULT_ACCENT}
>
<body className="min-h-full flex flex-col bg-[var(--background)] text-[var(--foreground)] font-sans selection:bg-[var(--brand-soft)] selection:text-[var(--foreground)]">
diff --git a/homepage/e2e/theme.spec.ts b/homepage/e2e/theme.spec.ts
@@ -64,6 +64,33 @@ test("dark + Signal by default; the base toggle persists with no FOUC", async ({
expect(s.stored).toBe("light");
expect(s.base).toBe("light");
expect(s.dark).toBe(false);
+
+ // <html> is server-rendered dark now; hydration must not put it back.
+ await page.waitForLoadState("load");
+ await expect.poll(() => htmlState(page)).toMatchObject({
+ base: "light",
+ dark: false,
+ stored: "light",
+ brand: ACCENTS.signal.onLight,
+ });
+});
+
+test.describe("with JavaScript off", () => {
+ test.use({ javaScriptEnabled: false });
+
+ test("the server renders the dark base, so no script is needed for it", async ({
+ page,
+ }) => {
+ await page.goto("/");
+ const html = page.locator("html");
+ await expect(html).toHaveAttribute("data-base", "dark");
+ await expect(html).toHaveClass(/(^|\s)dark(\s|$)/);
+ await expect(html).toHaveAttribute("data-accent", "signal");
+ // The pre-paint script never ran: this is the server's markup.
+ await expect(html).not.toHaveAttribute("data-theme-ready", /.*/);
+ await expect(html).toHaveCSS("--background", BASE_GROUNDS.dark);
+ await expect(page.locator("body")).toHaveCSS("background-color", "rgb(12, 10, 8)");
+ });
});
test("every base declares a complete palette, and the grounds differ", async ({