Themes: Dracula, Material, Adwaita, Flat Remix, Paper, Nordic, each light, dark or Auto

The theme picker lists Modern (the old Dark and Light), Classic and six new
palettes, from Dracula's spec (with Alucard), Material 3's baseline scheme,
libadwaita's CSS variables, Flat Remix's _colors.scss, Paper and Nord. A second
setting picks Light, Dark or Auto where a theme has both; Classic and Paper do
not, so it is hidden for them.

The page gets data-mode, light or dark, and Auto is worked out in theme.ts from
the system, so each palette is written once instead of again under a media
query. Every new palette clears WCAG AA for text on its backgrounds. An old
ipx.theme of dark, light or auto carries over as Modern.

Closes #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 12:53:25 +00:00
parent 26802d2b23
commit e2969bcee8
6 changed files with 294 additions and 51 deletions

40
web/src/theme.ts Normal file
View File

@@ -0,0 +1,40 @@
/* ---------------- theme ---------------- */
// A theme, and for those that come in both, light, dark or Auto, chosen in Settings and kept in
// ipx.theme and ipx.mode. The page gets data-theme and data-mode, light or dark, which is all
// the CSS reads: Auto is worked out here, from the system, so no palette is written twice.
const THEMES: Record<string, {name: string, modes: boolean}> = {
modern: {name: 'Modern', modes: true},
classic: {name: 'Classic, the 2004 Mac app', modes: false},
dracula: {name: 'Dracula', modes: true},
material: {name: 'Material', modes: true},
adwaita: {name: 'Adwaita', modes: true},
flatremix: {name: 'Flat Remix', modes: true},
paper: {name: 'Paper', modes: false},
nordic: {name: 'Nordic', modes: true},
};
const MODES: Record<string, string> = {auto: 'Auto (matches your system)', light: 'Light', dark: 'Dark'};
// Before themes came in light and dark, ipx.theme held one of these.
const OLD_THEMES: Record<string, [string, string]> = {dark: ['modern', 'dark'], light: ['modern', 'light'], auto: ['modern', 'auto']};
const systemDark = window.matchMedia?.('(prefers-color-scheme: dark)');
const theme = {name: 'modern', mode: 'dark'};
function setTheme(name = theme.name, mode = theme.mode){
theme.name = THEMES[name] ? name : 'modern';
theme.mode = MODES[mode] ? mode : 'dark';
const both = THEMES[theme.name].modes;
const root = document.documentElement;
root.dataset.theme = theme.name;
// A theme with one palette has it whatever the mode; both of those are light.
root.dataset.mode = !both ? 'light'
: theme.mode === 'auto' ? (systemDark && !systemDark.matches ? 'light' : 'dark') : theme.mode;
const sel = $('#stheme'); if(sel) sel.value = theme.name;
const ms = $('#smode'); if(ms) ms.value = theme.mode;
const mf = $('#smodefield'); if(mf) mf.hidden = !both;
try{ localStorage.setItem('ipx.theme', theme.name); localStorage.setItem('ipx.mode', theme.mode); }catch{}
}
systemDark?.addEventListener?.('change', () => { if(theme.mode === 'auto') setTheme(); });
try{
let name = localStorage.getItem('ipx.theme'), mode = localStorage.getItem('ipx.mode');
if(OLD_THEMES[name]) [name, mode] = OLD_THEMES[name];
setTheme(name ?? undefined, mode ?? undefined);
}catch{ setTheme(); }