feat(maestro): redesign web UI — token system, dark/light, a11y status
ci / rust (pull_request) Failing after 3m32s
ci / rust (pull_request) Failing after 3m32s
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>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user