web: move the section jumps to their own strip and grow the Theme menu

The six section links leave the top bar for a thin shelf under it, each a
bordered pill at the page's normal text size, so the bar itself stays a
row of actions and the jumps still read as buttons.

The Theme menu gains the workspace's colour groups — the landing palette
now takes its hue from the accent tokens — an Auto mode that follows the
OS scheme live, and a dropdown in place of the three font chips.
This commit is contained in:
2026-09-18 11:30:12 +07:00
parent e437946ee0
commit 8259bd468d
6 changed files with 123 additions and 52 deletions
@@ -21,6 +21,12 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
useEffect(() => {
applyTheme(mode, accent, fonts);
// Auto has to follow the OS while the page is open, not just at mount.
if (mode !== 'auto') return;
const media = window.matchMedia('(prefers-color-scheme: light)');
const onChange = () => applyTheme(mode, accent, fonts);
media.addEventListener('change', onChange);
return () => media.removeEventListener('change', onChange);
}, [mode, accent, fonts]);
const setMode = useCallback((next: ThemeMode) => setModeState(next), []);
+14 -5
View File
@@ -1,7 +1,7 @@
// Theme data only — the CSS in styles/tokens.css owns the values. Adding an
// accent group is one entry here plus one 1-line block in the stylesheet.
export type ThemeMode = 'light' | 'dark';
export type ThemeMode = 'light' | 'dark' | 'auto';
export type AccentId = 'amber' | 'blue' | 'emerald' | 'rose' | 'violet';
export type FontSetId = 'studio' | 'editorial' | 'native';
@@ -23,7 +23,8 @@ export const FONT_SETS: { id: FontSetId; label: string; note: string }[] = [
{ id: 'native', label: 'Thuần Việt', note: 'Be Vietnam Pro · Space Mono' },
];
export const THEME_COLOR: Record<ThemeMode, string> = {
// Keyed by the resolved mode only: auto never paints a browser chrome colour.
export const THEME_COLOR: Record<'light' | 'dark', string> = {
light: '#ffffff',
dark: '#0a0a0a',
};
@@ -32,9 +33,17 @@ const KEY_THEME = 'rc.theme';
const KEY_ACCENT = 'rc.accent';
const KEY_FONTS = 'rc.fonts';
const MODES: ThemeMode[] = ['light', 'dark', 'auto'];
export function readTheme(): ThemeMode {
const stored = localStorage.getItem(KEY_THEME);
if (stored === 'light' || stored === 'dark') return stored;
return MODES.includes(stored as ThemeMode) ? (stored as ThemeMode) : 'auto';
}
// Auto follows the OS setting; everything downstream — the CSS, the persisted
// value — only ever sees light or dark.
export function resolveMode(mode: ThemeMode): 'light' | 'dark' {
if (mode !== 'auto') return mode;
return window.matchMedia?.('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}
@@ -50,10 +59,10 @@ export function readFonts(): FontSetId {
export function applyTheme(mode: ThemeMode, accent: AccentId, fonts: FontSetId): void {
const root = document.documentElement;
root.dataset.theme = mode;
root.dataset.theme = resolveMode(mode);
root.dataset.accent = accent;
root.dataset.fonts = fonts;
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', THEME_COLOR[mode]);
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', THEME_COLOR[resolveMode(mode)]);
localStorage.setItem(KEY_THEME, mode);
localStorage.setItem(KEY_ACCENT, accent);
localStorage.setItem(KEY_FONTS, fonts);