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>
61 lines
1.9 KiB
TypeScript
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;
|
|
}
|