From 6eb4687a64185a1ba0484debea8c0b0127390998 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 19:48:28 +0000 Subject: [PATCH] docs: add multi-theme picker with seven named palettes Turn the docs-site light/dark toggle into a dropdown picker offering nine themes: the original light and dark plus japanese, arabic, indian, codex, terminal, hebrew, and georgian. Each palette redefines the existing design-system custom properties, so every page picks it up with no markup changes. theme.js builds the menu from the existing .theme-btn on load (all three pages get it for free) and keeps a next-theme cycle on window.__toggleTheme so the inline onclick still degrades gracefully without JS. Selection persists in localStorage and is applied before first paint to avoid a flash. Verified in Chromium (Playwright): all nine palettes apply the expected body background, the menu opens/closes, Escape dismisses it, and the choice survives a reload. --- docs/assets/agent-ready.css | 31 ++++++++++- docs/assets/theme.js | 107 ++++++++++++++++++++++++++++++++++-- 2 files changed, 131 insertions(+), 7 deletions(-) diff --git a/docs/assets/agent-ready.css b/docs/assets/agent-ready.css index 1f8129c..71d3025 100644 --- a/docs/assets/agent-ready.css +++ b/docs/assets/agent-ready.css @@ -28,6 +28,16 @@ :root[data-theme="dark"]{--bg:#0d1117;--panel:#131922;--panel-2:#0f141b;--text:#c9d1d9;--strong:#f0f3f6;--dim:#8b949e;--faint:#5b636d;--line:#222b36;--line-2:#2d3743;--accent:#6cb6ff;--accent-2:#56d4c0;--warn:#e3b341;--bad:#f0776a;} :root[data-theme="light"]{--bg:#f6f8fa;--panel:#fff;--panel-2:#f2f4f7;--text:#1f2328;--strong:#0b0d10;--dim:#57606a;--faint:#8a929c;--line:#d8dee4;--line-2:#c4ccd4;--accent:#0969da;--accent-2:#0a7d6f;--warn:#9a6700;--bad:#cf222e;} +/* named palettes — washi & sumi ink; teal-and-gold zellij; holi brights; + illuminated-manuscript editor; phosphor CRT; tekhelet stone; qvevri wine. */ +:root[data-theme="japanese"]{--bg:#f4efe6;--panel:#fbf8f1;--panel-2:#efe8db;--text:#2b2622;--strong:#1a1613;--dim:#6f6656;--faint:#9c917d;--line:#ddd3c1;--line-2:#cabfa8;--accent:#b7472a;--accent-2:#2e5d72;--warn:#9a6a12;--bad:#a13d2d;} +:root[data-theme="arabic"]{--bg:#10161a;--panel:#15201f;--panel-2:#101917;--text:#ddd2b8;--strong:#f5ecd6;--dim:#a99f86;--faint:#756c58;--line:#26332f;--line-2:#35443f;--accent:#29b6a6;--accent-2:#d4a537;--warn:#d4a537;--bad:#d1685a;} +:root[data-theme="indian"]{--bg:#fdf6ec;--panel:#fffdf7;--panel-2:#f8ecd8;--text:#33241a;--strong:#1f1410;--dim:#7a5c48;--faint:#a98a70;--line:#ecd9bf;--line-2:#ddc3a0;--accent:#d1477a;--accent-2:#2f8f6b;--warn:#e0871e;--bad:#c0392b;} +:root[data-theme="codex"]{--bg:#1a1626;--panel:#221d31;--panel-2:#191524;--text:#d4cde8;--strong:#f2eefc;--dim:#9a90b8;--faint:#6b6288;--line:#322a47;--line-2:#423857;--accent:#c792ea;--accent-2:#82aaff;--warn:#ffcb6b;--bad:#ff6b8a;} +:root[data-theme="terminal"]{--bg:#030805;--panel:#071009;--panel-2:#040c06;--text:#4be07a;--strong:#86ffab;--dim:#1f9c4a;--faint:#157038;--line:#0e2a17;--line-2:#163a20;--accent:#39ff8b;--accent-2:#ffb000;--warn:#ffb000;--bad:#ff5f56;} +:root[data-theme="hebrew"]{--bg:#f3f1ea;--panel:#fbfaf5;--panel-2:#eae7db;--text:#232a30;--strong:#101619;--dim:#5c6670;--faint:#8b94a0;--line:#d7d3c6;--line-2:#c3bfb0;--accent:#1b6ca8;--accent-2:#b58a2e;--warn:#b58a2e;--bad:#b3402f;} +:root[data-theme="georgian"]{--bg:#1b1012;--panel:#241619;--panel-2:#1a0f11;--text:#e6d5cf;--strong:#f8ece6;--dim:#b39089;--faint:#7d5f5a;--line:#38242a;--line-2:#4a3138;--accent:#c04a55;--accent-2:#d8a54a;--warn:#d8a54a;--bad:#d15b52;} + * { box-sizing: border-box; } html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; } body { @@ -45,9 +55,28 @@ body { .bar .brand { color: var(--strong); font-weight: 700; } .bar .brand b { color: var(--accent); } .bar .spring { flex: 1; } -.theme-btn { cursor: pointer; background: transparent; border: 1px solid var(--line-2); color: var(--dim); border-radius: 6px; padding: .3rem .55rem; font: 500 .7rem/1 var(--mono); } +.theme-btn { cursor: pointer; background: transparent; border: 1px solid var(--line-2); color: var(--dim); border-radius: 6px; padding: .3rem .55rem; font: 500 .7rem/1 var(--mono); white-space: nowrap; } .theme-btn:hover { color: var(--text); border-color: var(--accent); } +/* theme picker (menu is built by theme.js) */ +.theme-picker { position: relative; display: inline-flex; } +.theme-menu { position: absolute; right: 0; top: calc(100% + 8px); min-width: 12rem; background: var(--panel); border: 1px solid var(--line-2); border-radius: 8px; padding: .35rem; box-shadow: 0 10px 30px rgba(0,0,0,.28); z-index: 20; display: grid; gap: .1rem; } +.theme-menu[hidden] { display: none; } +.theme-opt { display: flex; align-items: center; gap: .55rem; width: 100%; text-align: left; cursor: pointer; background: transparent; border: 0; border-radius: 5px; padding: .42rem .5rem; color: var(--text); font: 500 .78rem/1 var(--mono); letter-spacing: .01em; } +.theme-opt:hover { background: var(--panel-2); color: var(--strong); } +.theme-opt[aria-selected="true"] { color: var(--accent); } +.theme-opt[aria-selected="true"]::after { content: "●"; margin-left: auto; font-size: .55rem; color: var(--accent); } +.sw { width: 13px; height: 13px; border-radius: 3px; border: 1px solid var(--line-2); flex: none; } +.sw-light { background: linear-gradient(135deg,#f6f8fa 50%,#0969da 50%); } +.sw-dark { background: linear-gradient(135deg,#0d1117 50%,#6cb6ff 50%); } +.sw-japanese { background: linear-gradient(135deg,#f4efe6 50%,#b7472a 50%); } +.sw-arabic { background: linear-gradient(135deg,#10161a 50%,#29b6a6 50%); } +.sw-indian { background: linear-gradient(135deg,#fdf6ec 50%,#d1477a 50%); } +.sw-codex { background: linear-gradient(135deg,#1a1626 50%,#c792ea 50%); } +.sw-terminal { background: linear-gradient(135deg,#030805 50%,#39ff8b 50%); } +.sw-hebrew { background: linear-gradient(135deg,#f3f1ea 50%,#1b6ca8 50%); } +.sw-georgian { background: linear-gradient(135deg,#1b1012 50%,#c04a55 50%); } + /* hero */ header.doc { padding: clamp(3rem, 8vw, 5.5rem) 0 clamp(1.6rem,4vw,2.6rem); border-bottom: 1px solid var(--line); } .eyebrow { font: 600 .72rem/1 var(--mono); letter-spacing: .22em; text-transform: uppercase; color: var(--accent); } diff --git a/docs/assets/theme.js b/docs/assets/theme.js index e11f8d9..9737d1b 100644 --- a/docs/assets/theme.js +++ b/docs/assets/theme.js @@ -1,11 +1,106 @@ (function () { var root = document.documentElement; - try { var s = localStorage.getItem("ar-theme"); if (s) root.setAttribute("data-theme", s); } catch (e) {} - window.__toggleTheme = function () { - var cur = root.getAttribute("data-theme") || + var STORE = "ar-theme"; + + // The catalogue. `id` is the data-theme value; `label` is what the menu shows. + var THEMES = [ + { id: "light", label: "Light" }, + { id: "dark", label: "Dark" }, + { id: "japanese", label: "Japanese" }, + { id: "arabic", label: "Arabic" }, + { id: "indian", label: "Indian" }, + { id: "codex", label: "Codex" }, + { id: "terminal", label: "Terminal" }, + { id: "hebrew", label: "Hebrew" }, + { id: "georgian", label: "Georgian" } + ]; + + function saved() { try { return localStorage.getItem(STORE); } catch (e) { return null; } } + function store(v) { try { localStorage.setItem(STORE, v); } catch (e) {} } + + // Apply the saved theme before first paint to avoid a flash of the default. + var s = saved(); + if (s) root.setAttribute("data-theme", s); + + function current() { + return root.getAttribute("data-theme") || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"); - var next = cur === "dark" ? "light" : "dark"; - root.setAttribute("data-theme", next); - try { localStorage.setItem("ar-theme", next); } catch (e) {} + } + function apply(id) { root.setAttribute("data-theme", id); store(id); sync(); } + + // Public API. __toggleTheme cycles to the next theme so the inline onclick in + // the markup keeps working even if the picker below never gets built. + window.__setTheme = apply; + window.__toggleTheme = function () { + var cur = current(), i = 0; + for (var k = 0; k < THEMES.length; k++) if (THEMES[k].id === cur) { i = k; break; } + apply(THEMES[(i + 1) % THEMES.length].id); }; + + var btn, menu; + + function sync() { + var cur = current(); + if (btn) btn.textContent = "theme: " + cur; + if (!menu) return; + var opts = menu.querySelectorAll("[data-theme-id]"); + for (var j = 0; j < opts.length; j++) { + opts[j].setAttribute("aria-selected", + opts[j].getAttribute("data-theme-id") === cur ? "true" : "false"); + } + } + + function open() { menu.hidden = false; btn.setAttribute("aria-expanded", "true"); + document.addEventListener("click", onDoc, true); + document.addEventListener("keydown", onKey, true); } + function close() { menu.hidden = true; btn.setAttribute("aria-expanded", "false"); + document.removeEventListener("click", onDoc, true); + document.removeEventListener("keydown", onKey, true); } + function onDoc(e) { if (!menu.contains(e.target) && !btn.contains(e.target)) close(); } + function onKey(e) { if (e.key === "Escape") { close(); btn.focus(); } } + + function build() { + btn = document.querySelector(".theme-btn"); + if (!btn) return; + + // Wrap the button so the menu can anchor to it, and take over the click. + var wrap = document.createElement("span"); + wrap.className = "theme-picker"; + btn.parentNode.insertBefore(wrap, btn); + wrap.appendChild(btn); + + btn.removeAttribute("onclick"); + btn.setAttribute("type", "button"); + btn.setAttribute("aria-haspopup", "listbox"); + btn.setAttribute("aria-expanded", "false"); + + menu = document.createElement("div"); + menu.className = "theme-menu"; + menu.setAttribute("role", "listbox"); + menu.setAttribute("aria-label", "Colour theme"); + menu.hidden = true; + + THEMES.forEach(function (t) { + var o = document.createElement("button"); + o.type = "button"; + o.className = "theme-opt"; + o.setAttribute("role", "option"); + o.setAttribute("data-theme-id", t.id); + o.innerHTML = '' + t.label; + o.addEventListener("click", function () { apply(t.id); close(); btn.focus(); }); + menu.appendChild(o); + }); + wrap.appendChild(menu); + + btn.addEventListener("click", function (e) { + e.stopPropagation(); + if (menu.hidden) open(); else close(); + }); + + sync(); + } + + if (document.readyState === "loading") + document.addEventListener("DOMContentLoaded", build); + else build(); })();