Files
ipodderx-rs/web/src/theme.ts
rays 768d02c840 Catppuccin, Gruvbox, Solarized and High contrast themes
Each in light and dark. The published palettes missed AA in 19 places,
mostly Solarized and Catppuccin Latte, so each failing colour is moved
the least distance, toward black or white, that clears every pair it is
drawn in. Solarized dark's base0 had to rise to base1 to read on base02,
so its dim sits between base2 and base1 to keep three steps of type.

tests/contrast.js checks every palette against the pairs the page draws,
and a border that matches the ground it sits on. Its first run caught
Classic's links at 3.7:1 on the source list and Modern's faint at 4.3:1
on inputs; both are tuned. A failed toast moves to --panel, since the
error colours are tuned for the page's grounds, not --raise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:24:56 +00:00

63 lines
3.4 KiB
TypeScript

/* ---------------- theme ---------------- */
// A theme, and for those that come in both, light, dark or Auto, chosen in Settings and kept on
// the account, so it follows you to another browser or computer. The server writes it onto the
// page's <html> tag (data-theme, data-choice) so the page is drawn in it from the start. The
// page gets 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', 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},
catppuccin: {name: 'Catppuccin', modes: true},
gruvbox: {name: 'Gruvbox', modes: true},
solarized: {name: 'Solarized', modes: true},
contrast: {name: 'High contrast', 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 in localStorage 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'};
/// `save` for a choice made in Settings, which goes to the account; not for applying one.
function setTheme(name = theme.name, mode = theme.mode, save = false){
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;
if(save) saveTheme();
}
/// One save at a time, each sending the choice as it stands when it goes. Sent as they came,
/// several at once, a quick run through the list could reach the server out of order and
/// leave the account on a theme passed on the way.
let themeSaving = Promise.resolve();
function saveTheme(){
themeSaving = themeSaving
.then(() => api('/api/me', {method: 'PATCH', body: JSON.stringify({theme: theme.name, mode: theme.mode})}))
.catch(e => toast(`Your theme was not saved: ${e.message}`, true));
}
systemDark?.addEventListener?.('change', () => { if(theme.mode === 'auto') setTheme(); });
(() => {
const root = document.documentElement;
if(root.dataset.choice) return setTheme(root.dataset.theme, root.dataset.choice);
// Nothing on the account yet. A theme this browser kept, from before themes were kept on the
// account, goes up to it once, so nobody has to choose again.
let name: string | null = null, mode: string | null = null;
try{ name = localStorage.getItem('ipx.theme'); mode = localStorage.getItem('ipx.mode'); }catch{}
if(OLD_THEMES[name]) [name, mode] = OLD_THEMES[name];
setTheme(name ?? undefined, mode ?? undefined, !!name);
})();