// Colour theme, appearance (light/dark/system), and desktop text size — purely // local preferences (localStorage), never sent to the server. Valid values + // defaults live in ui.js; the palettes live in style.css; a tiny copy of the // theme list runs pre-paint in the page . Applying happens at import // time, so importing this module is enough to paint the saved preference. // (Extracted verbatim from shell.js — the first slice of breaking the shell // into modules.) import { normalizeTheme, normalizeAppearance, colorScheme, normalizeTextSize, DEFAULT_TEXT_SIZE, } from "/static/ui.js"; // ---- themes ------------------------------------------------------------- const THEME_KEY = "lchat_theme"; const APPEARANCE_KEY = "lchat_appearance"; // Picker swatch colour + the light/dark surface colour (the mobile status-bar // tint). Keep keys in sync with THEMES in ui.js. export const THEME_META = { litroom: { name: "Lit Room", disc: "#ffb23e", panel: { light: "#fff8f0", dark: "#241a1e" } }, lagoon: { name: "Lagoon", disc: "#16a4a0", panel: { light: "#f3fbfa", dark: "#0c2a29" } }, sunny: { name: "Sunny", disc: "#ffc62e", panel: { light: "#fffaf0", dark: "#241d12" } }, plum: { name: "Plum", disc: "#9d5cd6", panel: { light: "#fdf8fd", dark: "#221829" } }, linen: { name: "Linen", disc: "#7d94a8", panel: { light: "#f5f1ea", dark: "#211f1b" } }, classic: { name: "Classic", disc: "#5b6cff", panel: { light: "#ffffff", dark: "#1d2026" } }, }; export const THEME_ORDER = ["litroom", "lagoon", "sunny", "plum", "linen", "classic"]; export const currentTheme = () => normalizeTheme(localStorage.getItem(THEME_KEY)); export const currentAppearance = () => normalizeAppearance(localStorage.getItem(APPEARANCE_KEY)); // Whoever hosts live-retintable surfaces (the sandbox frames) registers here; // applyTheme pings it after each change. A hook instead of an import keeps this // module dependency-light and usable from any page. let themeSink = null; export function onThemeApplied(fn) { themeSink = fn; } // Reflect the choice onto : data-theme drives the palette, color-scheme // drives light-dark(), and the meta tag keeps the status bar in step. function applyTheme(theme, appearance) { const root = document.documentElement; root.dataset.theme = theme; root.style.colorScheme = colorScheme(appearance); const dark = appearance === "dark" || (appearance !== "light" && matchMedia("(prefers-color-scheme: dark)").matches); const meta = document.querySelector('meta[name="theme-color"]'); if (meta) meta.setAttribute("content", THEME_META[theme].panel[dark ? "dark" : "light"]); // Live-push the new theme to any mounted sandbox frames so they re-tint // in place, rather than waiting to be re-displayed. if (themeSink) themeSink(); } export function setTheme(theme) { localStorage.setItem(THEME_KEY, theme); applyTheme(theme, currentAppearance()); } export function setAppearance(appearance) { localStorage.setItem(APPEARANCE_KEY, appearance); applyTheme(currentTheme(), appearance); } // Re-tint the status bar when the OS flips light/dark and we're following it. matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { if (currentAppearance() === "system") applyTheme(currentTheme(), "system"); }); applyTheme(currentTheme(), currentAppearance()); // ---- text size ------------------------------------------------------------ // Desktop-only, like the Mac iMessage slider: the picked size drives the // --msg-font-size variable (bubbles + composer). Touch devices skip all of // this — they follow the system text size via -apple-system-body in style.css. // Local preference only, same as the theme. The native Mac app sets the same // variable from its own View-menu / Settings pref instead of this key. const TEXT_SIZE_KEY = "lchat_text_size"; const isCoarse = matchMedia("(pointer: coarse)").matches; export const currentTextSize = () => normalizeTextSize(localStorage.getItem(TEXT_SIZE_KEY)); function applyTextSize(px) { if (isCoarse) return; // At the default, clear the override so the stylesheet's value (and any // future retune of it) rules; only a deliberate choice pins an inline size. if (px === DEFAULT_TEXT_SIZE) document.documentElement.style.removeProperty("--msg-font-size"); else document.documentElement.style.setProperty("--msg-font-size", px + "px"); } export function setTextSize(px) { localStorage.setItem(TEXT_SIZE_KEY, String(px)); applyTextSize(px); } applyTextSize(currentTextSize());