Files
com.gregkilmer.www/packages/ui/src/components/PageShell.astro
T
Claude 4614cd25a0 Add a dark and light mode toggle to the header
A sun and moon button at the end of the nav (and the Store's nav) swaps
the mode and remembers the choice in the browser. Until it is pressed the
site follows the device setting. A head script sets html[data-theme]
before first paint, so light styles now key off data-theme instead of a
media query, and motifs render both images with CSS showing one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
2026-10-18 19:57:05 +00:00

117 lines
3.9 KiB
Plaintext

---
/**
* Every page's frame: the document head, the page background (constellation
* dust and the family's edge seals), the site header with an optional hero,
* the page content, and the footer.
*/
import "../styles/tokens.css";
import "../styles/fonts.css";
import "../styles/base.css";
import "../styles/elements.css";
import ConstellationDust from "./ConstellationDust.astro";
import EdgeSeals from "./EdgeSeals.astro";
import SiteFooter from "./SiteFooter.astro";
import SiteHeader from "./SiteHeader.astro";
import type { NavKey, StoreNav } from "./nav.ts";
import type { PageFamily, SealName } from "./seals.ts";
interface Props {
title: string;
description?: string;
family?: PageFamily;
/** A craft page's own seal, overriding the family's. */
seal?: SealName;
current?: NavKey;
/** The Store's own header, on Store pages. */
store?: StoreNav;
/** Leave the edge seals off, for a page led by a photo. */
noSeals?: boolean;
}
const { title, description, family = "landing", seal, current, store, noSeals } = Astro.props;
const fullTitle = title === "Greg Kilmer" ? title : `${title} · Greg Kilmer`;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="dark light" />
<meta name="theme-color" content="#0E1522" />
<!-- Before first paint: the visitor's saved mode, else the device's. Without script the site stays dark. -->
<script is:inline>
(() => {
const root = document.documentElement;
const device = matchMedia("(prefers-color-scheme: light)");
const saved = () => {
try {
const t = localStorage.getItem("gk-theme");
return t === "light" || t === "dark" ? t : null;
} catch {
return null;
}
};
const apply = () => {
const t = saved() ?? (device.matches ? "light" : "dark");
root.dataset.theme = t;
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", t === "light" ? "#F4F2ED" : "#0E1522");
};
apply();
device.addEventListener("change", apply);
window.addEventListener("storage", apply);
window.gkApplyTheme = apply;
})();
</script>
<link rel="icon" href="/favicon.ico" sizes="48x48" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<title>{fullTitle}</title>
{description && <meta name="description" content={description} />}
<slot name="head" />
</head>
<body>
<a class="gk-skip" href="#main">Skip to content</a>
<div class="gk-page">
<ConstellationDust />
{!noSeals && <EdgeSeals family={family} seal={seal} />}
<SiteHeader current={current} store={store} hero={Astro.slots.has("hero")}><slot name="hero" /></SiteHeader>
<main id="main" class="gk-wrap gk-main">
<slot />
</main>
<SiteFooter />
</div>
</body>
</html>
<style>
.gk-page {
position: relative;
min-height: 100vh;
overflow-x: clip;
/* The page's nebula: violet from the top right, teal from the bottom left.
In light mode it is the dawn wash over faint star chart lines. */
background:
radial-gradient(ellipse at 100% 0%, rgba(var(--gk-violet-rgb), calc(0.28 * var(--gk-wash))), transparent 50%),
radial-gradient(ellipse at 0% 100%, rgba(var(--gk-teal-deep-rgb), calc(0.2 * var(--gk-wash))), transparent 55%),
var(--gk-chart),
var(--gk-sky);
}
.gk-main {
position: relative;
}
.gk-skip {
position: absolute;
left: 16px;
top: -60px;
z-index: 50;
padding: 10px 16px;
border-radius: 999px;
background: var(--gk-midnight);
border: 1px solid var(--gk-gold);
color: var(--gk-text);
}
.gk-skip:focus {
top: 12px;
}
</style>