Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions apps/docs/blume.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
},
Expand Down
1 change: 1 addition & 0 deletions apps/docs/styles.css
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
13 changes: 13 additions & 0 deletions apps/docs/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
2 changes: 2 additions & 0 deletions apps/marketing/src/components/LegalLayout.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
import Layout from "../layouts/Layout.astro";
import ThemeSwitch from "./ThemeSwitch.astro";

interface Props {
title: string;
Expand Down Expand Up @@ -61,6 +62,7 @@ const { title, description, lastUpdated } = Astro.props;
>
GitHub
</a>
<ThemeSwitch />
</div>
</div>
</footer>
Expand Down
7 changes: 5 additions & 2 deletions apps/marketing/src/components/SiteFooter.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -13,16 +15,17 @@ const GH = "https://github.com/UsefulSoftwareCo/executor";
<footer class="border-t border-rule py-10">
<div class="max-w-[1200px] mx-auto px-6 sm:px-10 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-6 min-[1024px]:[.page:has(>_.page\_\_rail)_.page\_\_body_[class*='max-w-']&]:ml-0 min-[1024px]:[.page:has(>_.page\_\_rail)_.page\_\_body_.hero-inner_&]:ml-0 min-[1024px]:[.page:has(>_.page\_\_rail)_.page\_\_body_.text-center_&]:ml-0">
<div class="flex items-center gap-2.5">
<img src={MARK} alt="" width="16" height="16" class="w-4 h-4 opacity-80" />
<img src={MARK} alt="" width="16" height="16" class="logo-mono w-4 h-4 opacity-80" />
<span class="text-[13px] font-medium text-ink-2">executor</span>
<span class="text-[12px] text-ink-3 ml-2">© {new Date().getFullYear()}</span>
</div>
<div class="flex items-center gap-6 text-[13px] text-ink-2">
<div class="flex flex-wrap items-center gap-x-6 gap-y-4 text-[13px] text-ink-2">
<a href="/docs" class="hover:text-ink transition-colors">Docs</a>
<a href="/blog" class="hover:text-ink transition-colors">Blog</a>
<a href={GH} class="hover:text-ink transition-colors">GitHub</a>
<a href="/privacy" class="hover:text-ink transition-colors">Privacy</a>
<a href="/terms" class="hover:text-ink transition-colors">Terms</a>
<ThemeSwitch />
</div>
</div>
</footer>
78 changes: 78 additions & 0 deletions apps/marketing/src/components/ThemeSwitch.astro
Original file line number Diff line number Diff line change
@@ -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: '<rect x="2" y="3" width="12" height="8.5" rx="1.25"/><path d="M5.5 14h5M8 11.5V14"/>',
},
{
value: "light",
label: "Use light theme",
icon: '<circle cx="8" cy="8" r="2.75"/><path d="M8 1.5v1.25M8 13.25v1.25M1.5 8h1.25M13.25 8h1.25M3.4 3.4l.9.9M11.7 11.7l.9.9M3.4 12.6l.9-.9M11.7 4.3l.9-.9"/>',
},
{
value: "dark",
label: "Use dark theme",
icon: '<path d="M13.5 9.6A5.75 5.75 0 0 1 6.4 2.5a5.75 5.75 0 1 0 7.1 7.1Z"/>',
},
] as const;
---

<div role="group" aria-label="Theme" class="inline-flex items-center gap-0.5 rounded-full border border-rule p-0.5">
{choices.map((choice) => (
<button
type="button"
data-theme-choice={choice.value}
aria-pressed={choice.value === "system" ? "true" : "false"}
aria-label={choice.label}
title={choice.label}
class="grid size-7 cursor-pointer place-items-center rounded-full text-ink-3 transition-colors hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ink-3 aria-pressed:bg-surface-2 aria-pressed:text-ink aria-pressed:shadow-[0_0_0_1px_var(--color-rule)]"
>
<svg aria-hidden="true" class="size-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" set:html={choice.icon} />
</button>
))}
</div>

<script>
const key = "blume-theme";
type Choice = "system" | "light" | "dark";
const isExplicit = (value: string | null | undefined): value is "light" | "dark" =>
value === "light" || value === "dark";
const systemDark = matchMedia("(prefers-color-scheme: dark)");

const stored = (): Choice => {
const value = localStorage.getItem(key);
return isExplicit(value) ? value : "system";
};

const apply = () => {
const choice = stored();
document.documentElement.dataset.theme =
choice === "system" ? (systemDark.matches ? "dark" : "light") : choice;
for (const button of document.querySelectorAll<HTMLButtonElement>("[data-theme-choice]")) {
button.setAttribute("aria-pressed", String(button.dataset.themeChoice === choice));
}
};

// Delegated and registered once: the docs' client router swaps the footer
// markup on every navigation but runs this module only on the first load.
document.addEventListener("click", (event) => {
if (!(event.target instanceof Element)) return;
const button = event.target.closest<HTMLButtonElement>("[data-theme-choice]");
if (!button) return;
const choice = button.dataset.themeChoice;
if (isExplicit(choice)) localStorage.setItem(key, choice);
else localStorage.removeItem(key);
apply();
});
systemDark.addEventListener("change", apply);
window.addEventListener("storage", (event) => {
if (event.key === key) apply();
});
document.addEventListener("astro:after-swap", apply);
apply();
</script>
Loading