eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1// Colour theme, appearance (light/dark/system), and desktop text size — purely
2// local preferences (localStorage), never sent to the server. Valid values +
3// defaults live in ui.js; the palettes live in style.css; a tiny copy of the
4// theme list runs pre-paint in the page <head>. Applying happens at import
5// time, so importing this module is enough to paint the saved preference.
6// (Extracted verbatim from shell.js — the first slice of breaking the shell
7// into modules.)
8import {
9 normalizeTheme,
10 normalizeAppearance,
11 colorScheme,
12 normalizeTextSize,
13 DEFAULT_TEXT_SIZE,
14} from "/static/ui.js";
15
16// ---- themes -------------------------------------------------------------
17const THEME_KEY = "lchat_theme";
18const APPEARANCE_KEY = "lchat_appearance";
19// Picker swatch colour + the light/dark surface colour (the mobile status-bar
20// tint). Keep keys in sync with THEMES in ui.js.
21export const THEME_META = {
22 litroom: { name: "Lit Room", disc: "#ffb23e", panel: { light: "#fff8f0", dark: "#241a1e" } },
23 lagoon: { name: "Lagoon", disc: "#16a4a0", panel: { light: "#f3fbfa", dark: "#0c2a29" } },
24 sunny: { name: "Sunny", disc: "#ffc62e", panel: { light: "#fffaf0", dark: "#241d12" } },
25 plum: { name: "Plum", disc: "#9d5cd6", panel: { light: "#fdf8fd", dark: "#221829" } },
26 linen: { name: "Linen", disc: "#7d94a8", panel: { light: "#f5f1ea", dark: "#211f1b" } },
27 classic: { name: "Classic", disc: "#5b6cff", panel: { light: "#ffffff", dark: "#1d2026" } },
28};
29export const THEME_ORDER = ["litroom", "lagoon", "sunny", "plum", "linen", "classic"];
30
31export const currentTheme = () => normalizeTheme(localStorage.getItem(THEME_KEY));
32export const currentAppearance = () => normalizeAppearance(localStorage.getItem(APPEARANCE_KEY));
33
34// Whoever hosts live-retintable surfaces (the sandbox frames) registers here;
35// applyTheme pings it after each change. A hook instead of an import keeps this
36// module dependency-light and usable from any page.
37let themeSink = null;
38export function onThemeApplied(fn) {
39 themeSink = fn;
40}
41
42// Reflect the choice onto <html>: data-theme drives the palette, color-scheme
43// drives light-dark(), and the meta tag keeps the status bar in step.
44function applyTheme(theme, appearance) {
45 const root = document.documentElement;
46 root.dataset.theme = theme;
47 root.style.colorScheme = colorScheme(appearance);
48 const dark =
49 appearance === "dark" ||
50 (appearance !== "light" && matchMedia("(prefers-color-scheme: dark)").matches);
51 const meta = document.querySelector('meta[name="theme-color"]');
52 if (meta) meta.setAttribute("content", THEME_META[theme].panel[dark ? "dark" : "light"]);
53 // Live-push the new theme to any mounted sandbox frames so they re-tint
54 // in place, rather than waiting to be re-displayed.
55 if (themeSink) themeSink();
56}
57export function setTheme(theme) {
58 localStorage.setItem(THEME_KEY, theme);
59 applyTheme(theme, currentAppearance());
60}
61export function setAppearance(appearance) {
62 localStorage.setItem(APPEARANCE_KEY, appearance);
63 applyTheme(currentTheme(), appearance);
64}
65// Re-tint the status bar when the OS flips light/dark and we're following it.
66matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
67 if (currentAppearance() === "system") applyTheme(currentTheme(), "system");
68});
69applyTheme(currentTheme(), currentAppearance());
70
71// ---- text size ------------------------------------------------------------
72// Desktop-only, like the Mac iMessage slider: the picked size drives the
73// --msg-font-size variable (bubbles + composer). Touch devices skip all of
74// this — they follow the system text size via -apple-system-body in style.css.
75// Local preference only, same as the theme. The native Mac app sets the same
76// variable from its own View-menu / Settings pref instead of this key.
77const TEXT_SIZE_KEY = "lchat_text_size";
78const isCoarse = matchMedia("(pointer: coarse)").matches;
79export const currentTextSize = () => normalizeTextSize(localStorage.getItem(TEXT_SIZE_KEY));
80function applyTextSize(px) {
81 if (isCoarse) return;
82 // At the default, clear the override so the stylesheet's value (and any
83 // future retune of it) rules; only a deliberate choice pins an inline size.
84 if (px === DEFAULT_TEXT_SIZE) document.documentElement.style.removeProperty("--msg-font-size");
85 else document.documentElement.style.setProperty("--msg-font-size", px + "px");
86}
87export function setTextSize(px) {
88 localStorage.setItem(TEXT_SIZE_KEY, String(px));
89 applyTextSize(px);
90}
91applyTextSize(currentTextSize());
92