/*
* Copyright (C) 2026 Mateusz Gruszczynski @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*/
import { api } from "@rustpad/api";
import { getAvailableLanguages, getLanguage, setLanguage, t } from "@rustpad/i18n";
import { applyTheme, getTheme } from "@rustpad/theme";
import { toast } from "@rustpad/toast";
const AUTH_STATE_KEY = "rustpad:auth-state";
const THEMES = ["dark", "light"];
let initialized = false;
let saveChain = Promise.resolve();
function hasAccountSession() {
try { return localStorage.getItem(AUTH_STATE_KEY) === "1"; } catch { return false; }
}
function icon(name) {
if (name === "language") {
return '';
}
if (name === "light") {
return '';
}
return '';
}
function closeOtherPickers(except = null) {
document.querySelectorAll(".header-preference-picker[open]").forEach(details => {
if (details !== except) details.open = false;
});
}
function preferenceButton({ value, label, selected, languageCode }) {
const button = document.createElement("button");
button.type = "button";
button.className = "header-preference-option";
button.dataset.value = value;
button.setAttribute("role", "option");
button.setAttribute("aria-selected", selected ? "true" : "false");
if (languageCode) button.lang = languageCode;
const check = document.createElement("span");
check.className = "header-preference-option__check";
check.setAttribute("aria-hidden", "true");
check.textContent = selected ? "✓" : "";
const copy = document.createElement("span");
copy.className = "header-preference-option__copy";
copy.textContent = label;
button.append(check, copy);
return button;
}
function createPicker(kind) {
const details = document.createElement("details");
details.className = `header-preference-picker header-preference-picker--${kind}`;
details.dataset.preferenceKind = kind;
const summary = document.createElement("summary");
summary.className = "header-preference-trigger";
const iconWrap = document.createElement("span");
iconWrap.className = "header-preference-trigger__icon";
iconWrap.innerHTML = icon(kind === "theme" ? getTheme() : "language");
const compactValue = document.createElement("span");
compactValue.className = "header-preference-trigger__value";
compactValue.setAttribute("aria-hidden", "true");
const chevron = document.createElement("span");
chevron.className = "header-preference-trigger__chevron";
chevron.setAttribute("aria-hidden", "true");
chevron.textContent = "▾";
summary.append(iconWrap, compactValue, chevron);
const menu = document.createElement("div");
menu.className = "header-preference-menu";
menu.setAttribute("role", "listbox");
details.append(summary, menu);
details.addEventListener("toggle", () => {
if (details.open) closeOtherPickers(details);
});
return details;
}
async function persistAccountPreference(patch) {
if (!hasAccountSession()) return;
saveChain = saveChain.catch(() => undefined).then(() => api("/api/auth/profile", {
method: "POST",
body: JSON.stringify(patch),
}));
return saveChain;
}
function renderLanguagePicker(details) {
const languages = getAvailableLanguages();
const current = getLanguage();
const selected = languages.find(language => language.code === current) || languages[0];
const summary = details.querySelector(".header-preference-trigger");
const value = details.querySelector(".header-preference-trigger__value");
const menu = details.querySelector(".header-preference-menu");
const label = t("common.language", {}, "Language");
summary.setAttribute("aria-label", label);
summary.title = label;
menu.setAttribute("aria-label", label);
value.textContent = String(selected?.code || current || "en").toUpperCase();
menu.replaceChildren(...languages.map(language => preferenceButton({
value: language.code,
label: language.native_name || language.name || language.code,
selected: language.code === current,
languageCode: language.code,
})));
}
function renderThemePicker(details) {
const current = getTheme();
const summary = details.querySelector(".header-preference-trigger");
const iconWrap = details.querySelector(".header-preference-trigger__icon");
const value = details.querySelector(".header-preference-trigger__value");
const menu = details.querySelector(".header-preference-menu");
const label = t("profile.theme", {}, "Interface theme");
summary.setAttribute("aria-label", label);
summary.title = label;
menu.setAttribute("aria-label", label);
iconWrap.innerHTML = icon(current);
value.textContent = t(`theme.${current}`, {}, current === "light" ? "Light" : "Dark");
menu.replaceChildren(...THEMES.map(theme => preferenceButton({
value: theme,
label: t(`theme.${theme}`, {}, theme === "light" ? "Light" : "Dark"),
selected: theme === current,
})));
}
function renderPicker(details) {
if (details.dataset.preferenceKind === "language") renderLanguagePicker(details);
else renderThemePicker(details);
}
function renderAll() {
document.querySelectorAll(".header-preference-picker").forEach(renderPicker);
}
function bindPicker(details) {
details.querySelector(".header-preference-menu")?.addEventListener("click", async event => {
const button = event.target.closest(".header-preference-option");
if (!(button instanceof HTMLButtonElement)) return;
const next = button.dataset.value || "";
const kind = details.dataset.preferenceKind;
details.open = false;
if (kind === "language") {
const previous = getLanguage();
if (!next || next === previous) return;
await setLanguage(next);
renderAll();
try {
await persistAccountPreference({ language: next });
} catch (error) {
if (error?.status !== 401) {
await setLanguage(previous);
renderAll();
toast.danger(error.message, { title: t("profile.language.failed", {}, "Could not change language") });
}
}
return;
}
const previous = getTheme();
if (!THEMES.includes(next) || next === previous) return;
applyTheme(next);
renderAll();
try {
await persistAccountPreference({ theme: next });
} catch (error) {
if (error?.status !== 401) {
applyTheme(previous);
renderAll();
toast.danger(error.message, { title: t("profile.updateFailed", {}, "Could not update profile") });
}
}
});
}
function mount(container) {
if (!(container instanceof HTMLElement) || container.dataset.headerPreferencesMounted === "true") return;
container.dataset.headerPreferencesMounted = "true";
container.setAttribute("data-i18n-ignore", "");
const language = createPicker("language");
const theme = createPicker("theme");
container.replaceChildren(language, theme);
bindPicker(language);
bindPicker(theme);
renderPicker(language);
renderPicker(theme);
}
export function initHeaderPreferences() {
document.querySelectorAll("[data-header-preferences]").forEach(mount);
if (initialized) return;
initialized = true;
document.addEventListener("pointerdown", event => {
const target = event.target instanceof Element ? event.target : null;
if (!target?.closest(".header-preference-picker")) closeOtherPickers();
}, { passive: true });
document.addEventListener("keydown", event => {
if (event.key === "Escape") closeOtherPickers();
});
document.addEventListener("rustpad:languagechange", renderAll);
window.addEventListener("rustpad:theme-change", renderAll);
window.addEventListener("rustpad:session-change", renderAll);
window.addEventListener("rustpad:session-expired", renderAll);
}