Files
archy/docker/justworks/public/assets/ds/theme.js
T
yaya 94899e69d3 feat: integrate Just Works app and native business setup
Preserve hosted website UX inside Archipelago, add isolated Business and owner-authorized native wallet setup, and include deployment handoff. Based on main with native signer import already accepted.
2026-10-08 19:39:38 +01:00

120 lines
4.2 KiB
JavaScript

/*
* Theme controller.
*
* Three preferences: "system" (default), "light", "dark". The choice is
* stored in localStorage and applied as `data-theme` on <html>; "system"
* removes the attribute so the `prefers-color-scheme` block in tokens.css
* takes over. A small inline snippet in each page's <head> applies the stored
* value before first paint, so this module only has to own the control.
*/
export const THEME_STORAGE_KEY = "jw-theme";
const THEMES = ["system", "light", "dark"];
/* Must match --jw-bg in tokens.css for each theme. */
const BROWSER_CHROME = { light: "#fafafa", dark: "#0b100f" };
const darkQuery = window.matchMedia("(prefers-color-scheme: dark)");
const listeners = new Set();
function readStored() {
try {
const stored = localStorage.getItem(THEME_STORAGE_KEY);
return THEMES.includes(stored) ? stored : "system";
} catch {
/* Private mode or blocked storage: fall back to the system preference. */
return "system";
}
}
/** The preference the user chose ("system" | "light" | "dark"). */
export function getPreference() {
return readStored();
}
/** The theme actually on screen ("light" | "dark"), resolving "system". */
export function getResolvedTheme(preference = readStored()) {
if (preference !== "system") return preference;
return darkQuery.matches ? "dark" : "light";
}
export function applyTheme(preference) {
const next = THEMES.includes(preference) ? preference : "system";
const root = document.documentElement;
if (next === "system") root.removeAttribute("data-theme");
else root.setAttribute("data-theme", next);
const meta = document.querySelector('meta[name="theme-color"]');
if (meta) meta.setAttribute("content", BROWSER_CHROME[getResolvedTheme(next)]);
try {
localStorage.setItem(THEME_STORAGE_KEY, next);
} catch {
/* Not fatal: the theme still applies for this page view. */
}
for (const listener of listeners) listener(next, getResolvedTheme(next));
return next;
}
/** Subscribe to preference changes. Returns an unsubscribe function. */
export function onThemeChange(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
/* Keep the browser chrome correct when the OS flips while set to "system". */
darkQuery.addEventListener("change", () => {
if (readStored() === "system") applyTheme("system");
});
const ICONS = {
system: '<path d="M3 4.5h12v8H3z"/><path d="M6.5 15.5h5"/>',
light: '<circle cx="9" cy="9" r="3.4"/><path d="M9 1.6v1.8M9 14.6v1.8M1.6 9h1.8M14.6 9h1.8M3.8 3.8l1.3 1.3M12.9 12.9l1.3 1.3M14.2 3.8l-1.3 1.3M5.1 12.9l-1.3 1.3"/>',
dark: '<path d="M14.5 10.4A6 6 0 0 1 7.6 3.5a6 6 0 1 0 6.9 6.9Z"/>'
};
const LABELS = { system: "Match system theme", light: "Light theme", dark: "Dark theme" };
/**
* Renders the segmented theme control into `container` and keeps its pressed
* state in sync with the current preference.
*/
export function mountThemeToggle(container) {
if (!container) return null;
container.classList.add("theme-toggle");
container.setAttribute("role", "group");
container.setAttribute("aria-label", "Color theme");
container.innerHTML = THEMES.map((theme) => `
<button type="button" data-theme-option="${theme}" title="${LABELS[theme]}" aria-label="${LABELS[theme]}">
<svg viewBox="0 0 18 18" aria-hidden="true">${ICONS[theme]}</svg>
</button>
`).join("");
const buttons = [...container.querySelectorAll("[data-theme-option]")];
function sync(preference = readStored()) {
for (const button of buttons) {
button.setAttribute("aria-pressed", String(button.dataset.themeOption === preference));
}
}
container.addEventListener("click", (event) => {
const button = event.target.closest("[data-theme-option]");
if (button) applyTheme(button.dataset.themeOption);
});
onThemeChange(sync);
sync();
return { sync };
}
/**
* The snippet that must run before first paint. Kept here so the inline copy
* in each page's <head> has a single documented source; if you change it,
* update index.html and design/index.html to match.
*/
export const PREPAINT_SNIPPET = `(function(){try{var t=localStorage.getItem("${THEME_STORAGE_KEY}");if(t==="light"||t==="dark")document.documentElement.setAttribute("data-theme",t)}catch(e){}})()`;