/* ---------------- theme ---------------- */ // A theme, and for those that come in both, light, dark or Auto, chosen in Settings and kept in a // cookie, so each device has its own: Glass on a phone, Dracula on a desktop (issue #69). The // server reads it and writes it onto the page's 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 = { adwaita: {name: 'Adwaita', modes: true}, catppuccin: {name: 'Catppuccin', modes: true}, classic: {name: 'Classic', modes: false}, dracula: {name: 'Dracula', modes: true}, flatremix: {name: 'Flat Remix', modes: true}, glass: {name: 'Glass', modes: true}, gruvbox: {name: 'Gruvbox', modes: true}, contrast: {name: 'High contrast', modes: true}, material: {name: 'Material', modes: true}, modern: {name: 'Modern', modes: true}, nordic: {name: 'Nordic', modes: true}, paper: {name: 'Paper', modes: false}, solarized: {name: 'Solarized', modes: true}, }; const MODES: Record = {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 = {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 this browser's cookie; 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; // The tab's icon in the same variant as the logo on the page. const fav = $('#favicon'); if(fav) fav.href = fav.dataset[root.dataset.mode!]!; // The top bar's colour for an iPhone before iOS 26 to paint behind the status bar, as Android's // toolbar does; the page's where the bar is see-through (Glass). iOS 26 reads the bar itself. const tc = $('meta[name=theme-color]'), bar = $('#topbar'); if(tc){ const c = bar ? getComputedStyle(bar).backgroundColor : ''; tc.content = /^rgb\(/.test(c) ? c : getComputedStyle(root).backgroundColor; } 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(); } /// Kept a year, for the whole site: the admin page is drawn in it too. function saveTheme(){ document.cookie = `ipx_theme=${theme.name}.${theme.mode}; Path=/; Max-Age=31536000; SameSite=Lax`; } // A font chosen in Settings, kept as the theme is, each browser its own (#359). The default is // what each theme sets, Lucida Grande for Classic and the system's for Glass. The reading fonts are // served by ipx from /fonts, and a browser fetches one only once it is chosen. const FONTS: Record = { theme: 'Default (set by the theme)', inter: 'Inter', system: 'System', atkinson: 'Atkinson Hyperlegible', lexend: 'Lexend', literata: 'Literata', source: 'Source Serif', dyslexic: 'OpenDyslexic', serif: "Serif (your device's)", mono: 'Monospace', }; // The reading font is an item's title and text; the same as the interface's until one is chosen. const READING_FONTS: Record = { same: 'Same as the interface', ...Object.fromEntries(Object.entries(FONTS).filter(([k]) => k !== 'theme')), }; let font = 'theme', readingFont = 'same'; function setFont(name = font, save = false){ font = FONTS[name] ? name : 'theme'; const root = document.documentElement; if(font === 'theme') delete root.dataset.font; else root.dataset.font = font; const sel = $('#sfont'); if(sel) sel.value = font; if(save) document.cookie = `ipx_font=${font}; Path=/; Max-Age=31536000; SameSite=Lax`; } function setReadingFont(name = readingFont, save = false){ readingFont = READING_FONTS[name] ? name : 'same'; const root = document.documentElement; if(readingFont === 'same') delete root.dataset.rfont; else root.dataset.rfont = readingFont; const sel = $('#srfont'); if(sel) sel.value = readingFont; if(save) document.cookie = `ipx_rfont=${readingFont}; Path=/; Max-Age=31536000; SameSite=Lax`; } setFont(document.documentElement.dataset.font); setReadingFont(document.documentElement.dataset.rfont); systemDark?.addEventListener?.('change', () => { if(theme.mode === 'auto') setTheme(); }); (() => { const root = document.documentElement; // The head script put this browser's theme from its cookie on the page. Without one, /api/me's // answer brings the account's from before (dialogs.ts). if(root.dataset.choice) return setTheme(root.dataset.theme, root.dataset.choice); // Nothing on the account either. A theme this browser kept in localStorage, from before that, // becomes its cookie, once. let name: string | null = null, mode: string | null = null; try{ name = localStorage.getItem('ipx.theme'); mode = localStorage.getItem('ipx.mode'); }catch{} if(name&&OLD_THEMES[name]) [name, mode] = OLD_THEMES[name]; setTheme(name ?? undefined, mode ?? undefined, !!name); })();