web/build.mjs builds a static site, web/dist, with the icons and typeface beside the pages; Dockerfile.web serves it with Caddy, which passes /api and /media to ipx on the same origin, so cookies, live updates and Cloudflare Access work as before. ipx serves no page any more: build.rs and every include_str! of the web files are gone, and its image is built without node. What ipx did to the page as it served it is kept another way. Caddy asks ipx before serving a page (forward_auth): /api/page-check sends a browser not signed in to /login and a non-admin away from /admin, and refuses them /admin.js; /api/login-check sends someone Access vouches for past the form; a page asked for with ?token= goes to /api/signin, which sets the cookie. The theme and the admin link, once written into the page per person, are set by web/boot.js in <head> from the theme cookie and the last visit, before the first frame, and /api/me brings the version and the account's theme from before. Caddy strips the Cf-Access-* headers from every peer but TUNNEL_PEER, the address cloudflared's requests come from, which is the boundary trusted_proxies drew; ipx then trusts only Caddy's address. The browser suite runs Caddy in front of its daemon, so it tests what production would run. Not deployed: production's cutover is on #203. 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;
|
|
// 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(OLD_THEMES[name]) [name, mode] = OLD_THEMES[name];
|
|
setTheme(name ?? undefined, mode ?? undefined, !!name);
|
|
})();
|