diff --git a/apps/docs/blume.config.ts b/apps/docs/blume.config.ts index bf5077cfc0..a3c6143fc0 100644 --- a/apps/docs/blume.config.ts +++ b/apps/docs/blume.config.ts @@ -112,11 +112,12 @@ export default defineConfig({ }, }), deployment: { output: "static", site: siteOrigin, base: "/docs" }, - // The marketing site is light-only and sets the same faces, so both halves of - // the public origin read as one product. + // The marketing site uses the same faces, the same colours in both modes and + // the same stored choice (the footer's theme switch), so both halves of the + // public origin read as one product. theme: { - accent: "#111111", - mode: "light", + accent: { light: "#111111", dark: "#ededed" }, + mode: "system", radius: "md", fonts: { display: "geist", body: "geist", mono: "geist-mono" }, }, diff --git a/apps/docs/styles.css b/apps/docs/styles.css index e78eeff32a..d962ba8162 100644 --- a/apps/docs/styles.css +++ b/apps/docs/styles.css @@ -1,4 +1,5 @@ @import "../marketing/src/styles/site-frame.css"; +@import "../marketing/src/styles/dark-theme.css"; /* The chrome that joins the marketing rail to Blume's own docs frame. Everything here uses the marketing tokens already defined in apps/marketing; diff --git a/apps/docs/theme.css b/apps/docs/theme.css index 2b0d3f96b7..b78495c41e 100644 --- a/apps/docs/theme.css +++ b/apps/docs/theme.css @@ -54,3 +54,16 @@ --blume-border: var(--color-rule); --blume-code-background: var(--color-surface-2); } + +/* Blume declares its own dark colours on this selector, so repeat the mapping + here; the marketing tokens it points at switch in dark-theme.css. The + accent is near-white in dark mode, so text on it takes the dark surface. */ +:root[data-theme="dark"] { + --blume-accent-foreground: var(--color-surface); + --blume-background: var(--color-surface); + --blume-foreground: var(--color-ink); + --blume-muted: var(--color-surface-2); + --blume-muted-foreground: var(--color-ink-2); + --blume-border: var(--color-rule); + --blume-code-background: var(--color-surface-2); +} diff --git a/apps/marketing/src/components/LegalLayout.astro b/apps/marketing/src/components/LegalLayout.astro index 88568e9b7e..9de1549ecc 100644 --- a/apps/marketing/src/components/LegalLayout.astro +++ b/apps/marketing/src/components/LegalLayout.astro @@ -1,5 +1,6 @@ --- import Layout from "../layouts/Layout.astro"; +import ThemeSwitch from "./ThemeSwitch.astro"; interface Props { title: string; @@ -61,6 +62,7 @@ const { title, description, lastUpdated } = Astro.props; > GitHub + diff --git a/apps/marketing/src/components/SiteFooter.astro b/apps/marketing/src/components/SiteFooter.astro index f5e0ff676a..8f1547932b 100644 --- a/apps/marketing/src/components/SiteFooter.astro +++ b/apps/marketing/src/components/SiteFooter.astro @@ -5,6 +5,8 @@ // // The brand mark resolves against the build's base, so the docs build (mounted // at /docs) serves its own copy instead of reaching for the marketing root. +import ThemeSwitch from "./ThemeSwitch.astro"; + const MARK = `${import.meta.env.BASE_URL.replace(/\/$/, "")}/favicon-192.png`; const GH = "https://github.com/UsefulSoftwareCo/executor"; @@ -13,16 +15,17 @@ const GH = "https://github.com/UsefulSoftwareCo/executor"; diff --git a/apps/marketing/src/components/ThemeSwitch.astro b/apps/marketing/src/components/ThemeSwitch.astro new file mode 100644 index 0000000000..825e866237 --- /dev/null +++ b/apps/marketing/src/components/ThemeSwitch.astro @@ -0,0 +1,78 @@ +--- +// System / light / dark. Shared through SiteFooter by the marketing site and +// the docs, so the choice is written where Blume's own theme script reads it: +// "light" or "dark" under the "blume-theme" key, and no key for the system +// setting. Each document's head script applies it before first paint. +const choices = [ + { + value: "system", + label: "Use system theme", + icon: '', + }, + { + value: "light", + label: "Use light theme", + icon: '', + }, + { + value: "dark", + label: "Use dark theme", + icon: '', + }, +] as const; +--- + +
+ {choices.map((choice) => ( + + ))} +
+ + diff --git a/apps/marketing/src/components/app-pieces.tsx b/apps/marketing/src/components/app-pieces.tsx index ecce1e0abc..256261fab5 100644 --- a/apps/marketing/src/components/app-pieces.tsx +++ b/apps/marketing/src/components/app-pieces.tsx @@ -15,6 +15,10 @@ import { DurableWorkflow } from "./workflow-durable"; type ClockProps = { readonly clock: MotionValue }; +// Motion can't interpolate theme variables, so blend two tones by progress. +const mixTones = (rest: string, active: string) => (amount: number) => + `color-mix(in oklab, var(${active}) ${amount * 100}%, var(${rest}))`; + // A short move followed by a reading pause. The count never rewinds. function useStep(clock: MotionValue, period: number) { return useTransform(clock, (time) => { @@ -34,15 +38,15 @@ function AppPiece({ readonly children: ReactNode; }) { return ( -
-
+
+
{children}

{part.title}

-

+

{part.body}

@@ -72,7 +76,7 @@ function ToolLine({ ); return ( ); @@ -81,14 +85,12 @@ function ToolLine({ function ToolPicture({ clock }: ClockProps) { const phase = useTransform(clock, (time) => (time % 4.8) / 4.8); const backgroundColor = useTransform( - phase, - [0, 0.1, 0.22, 0.38, 0.5, 1], - ["#fff", "#fff", "#ededed", "#ededed", "#fff", "#fff"], + useTransform(phase, [0, 0.1, 0.22, 0.38, 0.5, 1], [0, 0, 1, 1, 0, 0]), + mixTones("--color-tone-100", "--color-tone-93"), ); const borderColor = useTransform( - phase, - [0, 0.1, 0.22, 0.4, 0.55, 1], - ["#ddd", "#ddd", "#888", "#888", "#ddd", "#ddd"], + useTransform(phase, [0, 0.1, 0.22, 0.4, 0.55, 1], [0, 0, 1, 1, 0, 0]), + mixTones("--color-tone-87", "--color-tone-53"), ); const arrowX = useTransform(phase, [0, 0.2, 0.35, 0.5, 1], [0, 0, 3, 0, 0]); const doneOpacity = useTransform(phase, [0, 0.52, 0.62, 0.84, 1], [0, 0, 1, 1, 0]); @@ -96,22 +98,22 @@ function ToolPicture({ clock }: ClockProps) {