46f9370 set theme-color, but Safari 26 ignores it. iOS 26 paints the strip behind the status bar
and the island in the background colour of a sticky or fixed element at the page's top edge, and
failing that the root's. The top bar was neither, and the root had no colour of its own (only
body did), so iOS used black or white: a band of another colour above the bar.
The bar is sticky now, though nothing scrolls under it, so iOS reads its colour; the root has the
page's background for screens with nothing at the top edge; Classic's metal bar gets a colour
under its gradient, the top of the metal, since iOS reads no gradient. theme-color stays, set to
the bar's colour, for iPhones before iOS 26 and for Android. Glass's bar is see-through, so iOS
frosts what is under it there, as the bar itself does.
Taken from developers' reports of iOS 26, not Apple's documentation, and not yet seen on a phone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
68 lines
3.9 KiB
TypeScript
68 lines
3.9 KiB
TypeScript
/* ---------------- 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 <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}> = {
|
|
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<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 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`;
|
|
}
|
|
systemDark?.addEventListener?.('change', () => { if(theme.mode === 'auto') setTheme(); });
|
|
(() => {
|
|
const root = document.documentElement;
|
|
// Without a cookie, the server sent the theme the account kept from before; this browser takes
|
|
// it as its own, once, so nobody has to choose again.
|
|
if(root.dataset.choice) return setTheme(root.dataset.theme, root.dataset.choice, !/(^|; )ipx_theme=/.test(document.cookie));
|
|
// 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(OLD_THEMES[name]) [name, mode] = OLD_THEMES[name];
|
|
setTheme(name ?? undefined, mode ?? undefined, !!name);
|
|
})();
|