This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/web/src/theme.ts
T
naps62-yoloandClaude Opus 4.8 815ad695df
ci / rust (pull_request) Failing after 3m32s
feat(maestro): redesign web UI — token system, dark/light, a11y status
Rewrite styles.css as an OKLCH design-system: cool-slate neutrals with a
single committed signal-green for "alive" (running status, primary action,
focus, selection). Monospace becomes the UI typeface; dark is default with a
full light theme via tokens, OS-following with a persisted header toggle.

- catalog: pulsing wordmark, status-bar tally (N sessions / M live) + daemon
  chips, color-independent session status (pulsing dot for running, hollow
  ring + "exited" badge for dead), reveal-on-hover kill, three teaching
  empty states.
- attach: state pill (live/closed/error), framed dark xterm w/ green cursor.
- spawn: bottom-sheet/dialog form, mono inputs with green focus rings.
- a11y: AA contrast both themes, visible focus, prefers-reduced-motion paths,
  status never color-only.
- theme.ts: dark/light toggle + persistence; mock.ts: DEV-only review fixture
  (?mock / ?mock&spawn / ?theme=), tree-shaken from production builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:40:42 +00:00

61 lines
1.9 KiB
TypeScript

// Theme: dark (default) + light, persisted. When the user hasn't pinned a
// theme we follow the OS via CSS `prefers-color-scheme`; pinning sets an
// explicit `data-theme` on <html> that overrides the media query.
type Theme = "light" | "dark";
const KEY = "maestro-theme";
function stored(): Theme | null {
const v = localStorage.getItem(KEY);
return v === "light" || v === "dark" ? v : null;
}
function systemTheme(): Theme {
return window.matchMedia("(prefers-color-scheme: light)").matches
? "light"
: "dark";
}
/** The theme actually showing right now (pinned or system). */
export function resolvedTheme(): Theme {
return stored() ?? systemTheme();
}
/** Apply a pinned theme at boot (no-op when following the system). */
export function initTheme(): void {
// Dev-only `?theme=light|dark` override for visual review; stripped in prod.
if (import.meta.env.DEV) {
const q = new URLSearchParams(location.search).get("theme");
if (q === "light" || q === "dark") {
document.documentElement.dataset.theme = q;
return;
}
}
const pinned = stored();
if (pinned) document.documentElement.dataset.theme = pinned;
}
function setTheme(t: Theme): void {
localStorage.setItem(KEY, t);
document.documentElement.dataset.theme = t;
}
/** A header button that flips between light and dark. */
export function createThemeToggle(): HTMLButtonElement {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "btn btn-ghost btn-icon theme-toggle";
btn.innerHTML = `<span class="moon" aria-hidden="true">☾</span><span class="sun" aria-hidden="true">☀</span>`;
const sync = () => {
const next = resolvedTheme() === "dark" ? "light" : "dark";
btn.setAttribute("aria-label", `Switch to ${next} theme`);
btn.title = `Switch to ${next} theme`;
};
sync();
btn.addEventListener("click", () => {
setTheme(resolvedTheme() === "dark" ? "light" : "dark");
sync();
});
return btn;
}