Menu
Public repository / 0183a48e4f75fa9dcea1bf01af89c93ddb3fe5be
AkurAI-Platform / frontend/theme.js
Latest change ed1696afdb6cc3eba686f123e0b4674d3a81081b - feat: adopt framework v0.8.0 theme system (17 themes, picker, token aliasing) by Olafur Bui
// AkurAI theme switcher — native ESM, zero deps.
//
// Themes are generated by the framework (`/themes.css`, registry `/themes.json`)
// as `:root[data-theme="…"]` token blocks. This module only chooses the active
// `data-theme` on <html>, persists it, and builds a small picker. To avoid a
// flash of the wrong theme, set the attribute as early as possible — the inline
// snippet in <head> (see `theme-init.js` contents below) runs before paint; this
// module then renders the UI.
const STORAGE_KEY = "akurai-theme";
// Default per polarity when the user has never chosen — honors the OS setting.
const DEFAULT_DARK = "akurai";
const DEFAULT_LIGHT = "akurai-light";
export function prefersLight() {
return window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches;
}
export function defaultTheme() {
return prefersLight() ? DEFAULT_LIGHT : DEFAULT_DARK;
}
export function currentTheme() {
return localStorage.getItem(STORAGE_KEY) || document.documentElement.getAttribute("data-theme") || defaultTheme();
}
export function applyTheme(slug) {
document.documentElement.setAttribute("data-theme", slug);
try {
localStorage.setItem(STORAGE_KEY, slug);
} catch {
/* storage may be unavailable (private mode); theme still applies for the session */
}
}
// Fetch the registry and build a grouped picker. `mount` is the element the
// <select> (and label) are appended to. Returns the registry array.
export async function mountPicker(mount) {
if (!mount) return [];
let themes = [];
try {
const res = await fetch("/themes.json", { headers: { accept: "application/json" } });
themes = await res.json();
} catch {
return [];
}
if (!Array.isArray(themes) || themes.length === 0) return [];
const select = document.createElement("select");
select.className = "theme-select";
select.setAttribute("aria-label", "Color theme");
// Group by family so light/dark siblings sit together.
const families = [];
const byFamily = new Map();
for (const t of themes) {
if (!byFamily.has(t.family)) {
byFamily.set(t.family, []);
families.push(t.family);
}
byFamily.get(t.family).push(t);
}
for (const family of families) {
const group = document.createElement("optgroup");
group.label = family;
for (const t of byFamily.get(family)) {
const opt = document.createElement("option");
opt.value = t.slug;
// Label already carries the name; append a polarity hint when ambiguous.
opt.textContent = t.label;
group.appendChild(opt);
}
select.appendChild(group);
}
const active = currentTheme();
select.value = active;
applyTheme(active);
select.addEventListener("change", () => applyTheme(select.value));
mount.appendChild(select);
return themes;
}
// Auto-mount: if a `[data-theme-picker]` element exists, fill it on load.
if (typeof document !== "undefined") {
const boot = () => {
const slot = document.querySelector("[data-theme-picker]");
if (slot) mountPicker(slot);
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot);
} else {
boot();
}
}