feat(decnet_web/theme-lab): light theme tokens + dev toggle

Adds html[data-theme="light"] block to index.css overriding the
core six tokens (bg, matrix, violet, panel, border, alert), the
matrix/violet/alert tints, and the foreground opacity ramp to a
cream-on-ink palette anchored on #dbdad6. Glows are no-op'd —
light mode trades neon haloes for hard 1px borders.

Lab page gets a Dark/Light toggle that flips
html.dataset.theme and persists to sessionStorage
(decnet_theme_lab) — intentionally tab-scoped, not user-facing.
App.tsx hydrates the same key on boot so a tab reload keeps the
dev's chosen theme. The user-facing localStorage toggle ships
later via Config.
This commit is contained in:
2026-05-09 03:23:50 -04:00
parent f3f7bff717
commit 47c57271e7
6 changed files with 191 additions and 1 deletions

View File

@@ -0,0 +1,57 @@
import React, { useEffect, useState } from 'react';
/* Dev-scoped Dark/Light theme toggle for the theme lab.
*
* Flips `document.documentElement.dataset.theme` and persists to
* **sessionStorage** intentionally — the lab is a tab-scoped
* exploration tool. Global persistence (across reloads, all users)
* is the user-facing Config toggle that ships in Task 6. */
export const THEME_SESSION_KEY = 'decnet_theme_lab';
export type Theme = 'dark' | 'light';
export function readLabTheme(): Theme {
try {
const v = sessionStorage.getItem(THEME_SESSION_KEY);
return v === 'light' ? 'light' : 'dark';
} catch {
return 'dark';
}
}
export function applyTheme(theme: Theme): void {
document.documentElement.dataset.theme = theme;
}
const ThemeToggle: React.FC = () => {
const [theme, setTheme] = useState<Theme>(() => readLabTheme());
useEffect(() => {
applyTheme(theme);
try { sessionStorage.setItem(THEME_SESSION_KEY, theme); } catch { /* ignore */ }
}, [theme]);
return (
<div className="lab-theme-toggle" role="group" aria-label="Theme">
<button
type="button"
className={`btn small ${theme === 'dark' ? '' : 'ghost'}`}
onClick={() => setTheme('dark')}
aria-pressed={theme === 'dark'}
>
DARK
</button>
<button
type="button"
className={`btn small ${theme === 'light' ? '' : 'ghost'}`}
onClick={() => setTheme('light')}
aria-pressed={theme === 'light'}
>
LIGHT
</button>
</div>
);
};
export default ThemeToggle;