diff --git a/docs/components.md b/docs/components.md index c84829b..4e118ef 100644 --- a/docs/components.md +++ b/docs/components.md @@ -76,6 +76,6 @@ import PageShell from "@gk/ui/components/PageShell.astro"; ``` -The site is dark by default and turns light when the device is set to light (`prefers-color-scheme`). Colors come from the tokens in `tokens.css`, which has the light values too, so a component that uses `var(--gk-…)` needs nothing more. For a translucent color use a channel token, as in `rgba(var(--gk-gold-rgb), 0.35)`, never a hardcoded rgba. In SVG markup use `style="fill:var(--gk-gold)"`. +The site follows the device's light or dark setting until the visitor presses the sun and moon toggle in the header (`ThemeToggle`), which saves their choice in the browser. A script in PageShell's head sets `` or `"dark"` before the page paints; without script the site stays dark. Style light mode with `:root[data-theme="light"]` (in a component, `:global(:root[data-theme="light"]) .x`), not a `prefers-color-scheme` media query, so the toggle can override it. Colors come from the tokens in `tokens.css`, which has the light values too, so a component that uses `var(--gk-…)` needs nothing more. For a translucent color use a channel token, as in `rgba(var(--gk-gold-rgb), 0.35)`, never a hardcoded rgba. In SVG markup use `style="fill:var(--gk-gold)"`. -Motif images come in a light recolor and, for seals, a gold foil version for each mode. Show a motif with `Motif` (or `Seal`), which picks the right file for the device; a plain `` stays dark. Pass `foil` only to a seal standing alone at 120px or more and half opacity or more. After changing a motif SVG, run `node packages/ui/scripts/light-motifs.mjs` to rewrite the `light/`, `foil/` and `foil-light/` versions. +Motif images come in a light recolor and, for seals, a gold foil version for each mode. Show a motif with `Motif` (or `Seal`), which renders both files and shows the one for the current mode; a plain `` stays dark. Pass `foil` only to a seal standing alone at 120px or more and half opacity or more. After changing a motif SVG, run `node packages/ui/scripts/light-motifs.mjs` to rewrite the `light/`, `foil/` and `foil-light/` versions. diff --git a/packages/ui/src/assets/icons/moon.svg b/packages/ui/src/assets/icons/moon.svg new file mode 100644 index 0000000..09a915e --- /dev/null +++ b/packages/ui/src/assets/icons/moon.svg @@ -0,0 +1 @@ + diff --git a/packages/ui/src/assets/icons/sun.svg b/packages/ui/src/assets/icons/sun.svg new file mode 100644 index 0000000..a62d8a5 --- /dev/null +++ b/packages/ui/src/assets/icons/sun.svg @@ -0,0 +1 @@ + diff --git a/packages/ui/src/components/Motif.astro b/packages/ui/src/components/Motif.astro index 470933c..c9ccba0 100644 --- a/packages/ui/src/components/Motif.astro +++ b/packages/ui/src/components/Motif.astro @@ -1,7 +1,9 @@ --- /** - * A motif image that follows the device's light or dark setting: the dark - * original, or its light recolor. Pass `foil` for a large standalone seal. + * A motif image that follows the site's light or dark mode: the dark + * original, or its light recolor. Both are in the page and CSS shows one + * (base.css); the hidden one is lazy, so it never loads. Pass `foil` for a + * large standalone seal. */ import type { HTMLAttributes } from "astro/types"; import { motifSources } from "./seals.ts"; @@ -10,7 +12,16 @@ interface Props extends Omit, "src"> { src: string; foil?: boolean; } -const { src, foil, ...rest } = Astro.props; +const { src, foil, class: className, loading = "lazy", ...rest } = Astro.props; const v = motifSources(src, { foil }); --- - +{ + v.light === v.dark ? ( + + ) : ( + <> + + + + ) +} diff --git a/packages/ui/src/components/PageShell.astro b/packages/ui/src/components/PageShell.astro index 692ccb4..b443f13 100644 --- a/packages/ui/src/components/PageShell.astro +++ b/packages/ui/src/components/PageShell.astro @@ -36,8 +36,31 @@ const fullTitle = title === "Greg Kilmer" ? title : `${title} · Greg Kilmer`; - - + + + diff --git a/packages/ui/src/components/SiteHeader.astro b/packages/ui/src/components/SiteHeader.astro index b68df70..dd62fdf 100644 --- a/packages/ui/src/components/SiteHeader.astro +++ b/packages/ui/src/components/SiteHeader.astro @@ -10,6 +10,7 @@ import monogram from "../assets/brand/gk-monogram.svg?url"; import Divider from "./Divider.astro"; import Motif from "./Motif.astro"; import Star from "./Star.astro"; +import ThemeToggle from "./ThemeToggle.astro"; import { main, tail, world, type NavKey, type StoreNav } from "./nav.ts"; import { filamentBanner } from "./seals.ts"; @@ -43,10 +44,11 @@ const decs = [60, 30, 0, -30].map((d, i) => ({ at: (i / 4) * 100, label: `${d > a Patiun LLC workshop - {tail.map((l) =>
  • {l.label}
  • )} +
  • ) @@ -206,6 +209,10 @@ const decs = [60, 30, 0, -30].map((d, i) => ({ at: (i / 4) * 100, label: `${d > font-size: 14px; font-weight: 500; } + /* The Store's longer links fit one row at desktop width. */ + .store-links { + column-gap: 14px; + } .links a { display: inline-flex; align-items: center; @@ -224,7 +231,8 @@ const decs = [60, 30, 0, -30].map((d, i) => ({ at: (i / 4) * 100, label: `${d > .links a[aria-current] { color: var(--gk-teal-bright); } - .sep { + .sep, + .mode { display: flex; } .mw { @@ -288,17 +296,15 @@ const decs = [60, 30, 0, -30].map((d, i) => ({ at: (i / 4) * 100, label: `${d > .atlas { display: none; } - @media (prefers-color-scheme: light) { - .atlas { - display: block; - position: absolute; - inset: 0; - pointer-events: none; - font-size: 9px; - letter-spacing: 0.1em; - color: var(--gk-muted); - font-variant-numeric: tabular-nums; - } + :global(:root[data-theme="light"]) .atlas { + display: block; + position: absolute; + inset: 0; + pointer-events: none; + font-size: 9px; + letter-spacing: 0.1em; + color: var(--gk-muted); + font-variant-numeric: tabular-nums; } .ra { position: absolute; diff --git a/packages/ui/src/components/StarChartArt.astro b/packages/ui/src/components/StarChartArt.astro index 22b6e4a..c28873a 100644 --- a/packages/ui/src/components/StarChartArt.astro +++ b/packages/ui/src/components/StarChartArt.astro @@ -38,15 +38,13 @@ const { seal = "transmutation", ratio = "4 / 3", src, alt = "", class: className var(--gk-sky); } /* Light mode: a morning sky tile, a touch cooler than the page. */ - @media (prefers-color-scheme: light) { - .gk-art { - background: - radial-gradient(ellipse 70% 90% at 100% 0%, rgba(106, 47, 224, 0.16), transparent 70%), - radial-gradient(ellipse 55% 80% at 0% 70%, rgba(15, 140, 132, 0.14), transparent 70%), - repeating-linear-gradient(0deg, rgba(19, 28, 43, 0.06) 0 1px, transparent 1px 40px), - repeating-linear-gradient(90deg, rgba(19, 28, 43, 0.06) 0 1px, transparent 1px 40px), - #e9e6ee; - } + :global(:root[data-theme="light"]) .gk-art { + background: + radial-gradient(ellipse 70% 90% at 100% 0%, rgba(106, 47, 224, 0.16), transparent 70%), + radial-gradient(ellipse 55% 80% at 0% 70%, rgba(15, 140, 132, 0.14), transparent 70%), + repeating-linear-gradient(0deg, rgba(19, 28, 43, 0.06) 0 1px, transparent 1px 40px), + repeating-linear-gradient(90deg, rgba(19, 28, 43, 0.06) 0 1px, transparent 1px 40px), + #e9e6ee; } .mat { position: absolute; diff --git a/packages/ui/src/components/ThemeToggle.astro b/packages/ui/src/components/ThemeToggle.astro new file mode 100644 index 0000000..3d373ca --- /dev/null +++ b/packages/ui/src/components/ThemeToggle.astro @@ -0,0 +1,64 @@ +--- +/** + * Swaps between dark and light mode. Shows the moon in dark mode and the sun + * in light. Until it is pressed the site follows the device setting; a press + * saves the choice in this browser (PageShell's head script reads it back). + */ +import Icon from "./Icon.astro"; +--- + + + + + diff --git a/packages/ui/src/styles/base.css b/packages/ui/src/styles/base.css index f1fa981..2a83893 100644 --- a/packages/ui/src/styles/base.css +++ b/packages/ui/src/styles/base.css @@ -93,7 +93,8 @@ p a:not([class]):hover { } } -/* A motif's picture adds no box: its image lays out as if it stood alone. */ -picture.gk-motif { - display: contents; +/* A motif has a dark and a light image; show the one for the mode. */ +:root:not([data-theme="light"]) .gk-motif-light, +:root[data-theme="light"] .gk-motif-dark { + display: none !important; } diff --git a/packages/ui/src/styles/elements.css b/packages/ui/src/styles/elements.css index fb6c8a7..bf485aa 100644 --- a/packages/ui/src/styles/elements.css +++ b/packages/ui/src/styles/elements.css @@ -42,17 +42,15 @@ box-shadow: 0 0 0 4px rgba(var(--gk-teal-rgb), 0.16); } /* On Starlight the deep teal carries white text (5.8:1); the lighter teal is too faint. */ -@media (prefers-color-scheme: light) { - .gk-pri, - .gk-pri:hover { - color: #fff; - } - .gk-pri { - background: var(--gk-teal-bright); - } - .gk-pri:hover { - background: #085e58; - } +:root[data-theme="light"] .gk-pri, +:root[data-theme="light"] .gk-pri:hover { + color: #fff; +} +:root[data-theme="light"] .gk-pri { + background: var(--gk-teal-bright); +} +:root[data-theme="light"] .gk-pri:hover { + background: #085e58; } .gk-sec { background: transparent; diff --git a/packages/ui/src/styles/tokens.css b/packages/ui/src/styles/tokens.css index aa4f36d..8961fd2 100644 --- a/packages/ui/src/styles/tokens.css +++ b/packages/ui/src/styles/tokens.css @@ -1,7 +1,8 @@ /* - * Brand tokens. Dark is the default (Greg's decision, 2026-10-11); a device set - * to light gets the light values below (picked 2026-10-18 on the light motif - * board: Dawn chart sky, deep gold line seals). + * Brand tokens. Dark is the default (Greg's decision, 2026-10-11); the light + * values below (picked 2026-10-18 on the light motif board: Dawn chart sky, + * deep gold line seals) apply when . PageShell sets + * that from the device setting, or from the visitor's choice on the toggle. */ :root { color-scheme: dark; @@ -74,49 +75,47 @@ --gk-ease: 0.15s; } -@media (prefers-color-scheme: light) { - :root { - color-scheme: light; +:root[data-theme="light"] { + color-scheme: light; - --gk-sky: #f4f2ed; - --gk-midnight: #ffffff; - --gk-lines: #d9d2c2; - --gk-lines-strong: #b8ad97; - --gk-starlight: #131c2b; - --gk-text: #131c2b; - --gk-soft: #33465a; - --gk-muted: #52646f; - --gk-placeholder: #5e6e78; - /* Teal graphics; teal text, links and the primary button use the deeper teal-bright. */ - --gk-teal: #0f8c84; - --gk-teal-bright: #0a726b; - --gk-gold: #8a6516; - --gk-gold-bright: #6f5112; - --gk-gold-line: rgba(138, 101, 22, 0.55); - --gk-gold-tint: rgba(138, 101, 22, 0.1); - --gk-violet: #6a2fe0; - --gk-danger: #b3261e; + --gk-sky: #f4f2ed; + --gk-midnight: #ffffff; + --gk-lines: #d9d2c2; + --gk-lines-strong: #b8ad97; + --gk-starlight: #131c2b; + --gk-text: #131c2b; + --gk-soft: #33465a; + --gk-muted: #52646f; + --gk-placeholder: #5e6e78; + /* Teal graphics; teal text, links and the primary button use the deeper teal-bright. */ + --gk-teal: #0f8c84; + --gk-teal-bright: #0a726b; + --gk-gold: #8a6516; + --gk-gold-bright: #6f5112; + --gk-gold-line: rgba(138, 101, 22, 0.55); + --gk-gold-tint: rgba(138, 101, 22, 0.1); + --gk-violet: #6a2fe0; + --gk-danger: #b3261e; - --gk-orn-link: #0f8c84; - --gk-orn-dot: #131c2b; - --gk-orn-dim: #a9b3b9; + --gk-orn-link: #0f8c84; + --gk-orn-dot: #131c2b; + --gk-orn-dim: #a9b3b9; - --gk-gold-rgb: 138, 101, 22; - --gk-gold-bright-rgb: 111, 81, 18; - --gk-star-rgb: 19, 28, 43; - --gk-text-rgb: 19, 28, 43; - --gk-teal-rgb: 10, 114, 107; - --gk-teal-deep-rgb: 15, 140, 132; - --gk-sky-rgb: 244, 242, 237; - --gk-midnight-rgb: 255, 255, 255; - --gk-dust-accent-rgb: 106, 47, 224; - --gk-wash: 0.5; - --gk-chart: - repeating-linear-gradient(0deg, rgba(19, 28, 43, 0.032) 0 1px, transparent 1px 44px), - repeating-linear-gradient(90deg, rgba(19, 28, 43, 0.032) 0 1px, transparent 1px 44px); + --gk-gold-rgb: 138, 101, 22; + --gk-gold-bright-rgb: 111, 81, 18; + --gk-star-rgb: 19, 28, 43; + --gk-text-rgb: 19, 28, 43; + --gk-teal-rgb: 10, 114, 107; + --gk-teal-deep-rgb: 15, 140, 132; + --gk-sky-rgb: 244, 242, 237; + --gk-midnight-rgb: 255, 255, 255; + --gk-dust-accent-rgb: 106, 47, 224; + --gk-wash: 0.5; + --gk-chart: + repeating-linear-gradient(0deg, rgba(19, 28, 43, 0.032) 0 1px, transparent 1px 44px), + repeating-linear-gradient(90deg, rgba(19, 28, 43, 0.032) 0 1px, transparent 1px 44px); - --gk-filament: linear-gradient(90deg, transparent, #8a6516 25%, #0f8c84 75%, transparent); - } + --gk-filament: linear-gradient(90deg, transparent, #8a6516 25%, #0f8c84 75%, transparent); } @media (max-width: 640px) { diff --git a/services/site/src/components/blog/LeadPost.astro b/services/site/src/components/blog/LeadPost.astro index ea8fee2..f33cfbf 100644 --- a/services/site/src/components/blog/LeadPost.astro +++ b/services/site/src/components/blog/LeadPost.astro @@ -120,11 +120,9 @@ const now = nowStatuses(s, post); filter: drop-shadow(0 0 16px rgba(var(--gk-gold-rgb), 0.35)); } /* Glow is for the night sky; on Starlight the foil stands on its own. */ - @media (prefers-color-scheme: light) { - .art :global(.seal) { - opacity: 1; - filter: none; - } + :global(:root[data-theme="light"]) .art :global(.seal) { + opacity: 1; + filter: none; } .cover { position: absolute; diff --git a/services/site/src/pages/404.astro b/services/site/src/pages/404.astro index fd492d6..8a1d1e5 100644 --- a/services/site/src/pages/404.astro +++ b/services/site/src/pages/404.astro @@ -83,11 +83,9 @@ const groups = [ filter: drop-shadow(0 0 22px rgba(var(--gk-teal-rgb), 0.35)); } /* Glow is for the night sky; on Starlight the foil stands on its own. */ - @media (prefers-color-scheme: light) { - .lost :global(.seal) { - opacity: 1; - filter: none; - } + :global(:root[data-theme="light"]) .lost :global(.seal) { + opacity: 1; + filter: none; } .eyebrow { font-size: 11px; diff --git a/services/site/src/pages/inquiries/index.astro b/services/site/src/pages/inquiries/index.astro index c7741a3..69e7d5e 100644 --- a/services/site/src/pages/inquiries/index.astro +++ b/services/site/src/pages/inquiries/index.astro @@ -222,11 +222,9 @@ const turnstileKey = turnstileSiteKey(); opacity: 0.9; } /* Glow is for the night sky; on Starlight the foil stands on its own. */ - @media (prefers-color-scheme: light) { - .intro :global(.seal) { - opacity: 1; - filter: none; - } + :global(:root[data-theme="light"]) .intro :global(.seal) { + opacity: 1; + filter: none; } .eyebrow { font-size: 11px; diff --git a/services/site/src/pages/inquiries/sent.astro b/services/site/src/pages/inquiries/sent.astro index 0c83846..be34f1d 100644 --- a/services/site/src/pages/inquiries/sent.astro +++ b/services/site/src/pages/inquiries/sent.astro @@ -107,11 +107,9 @@ const steps = [ filter: drop-shadow(0 0 18px rgba(var(--gk-gold-rgb), 0.35)); } /* Glow is for the night sky; on Starlight the foil stands on its own. */ - @media (prefers-color-scheme: light) { - .intro :global(.seal) { - opacity: 1; - filter: none; - } + :global(:root[data-theme="light"]) .intro :global(.seal) { + opacity: 1; + filter: none; } .eyebrow { font-size: 11px;