// SPDX-FileCopyrightText: 2026 Progmasoft // SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 "use client"; import { useEffect, useSyncExternalStore, useTransition } from "react"; import { selectLocale } from "@/lib/locale.actions"; import { isLocale, localeNames, supportedLocales, type Locale, } from "@/lib/localization"; interface PreferenceControlsProps { locale: Locale; labels: { dark: string; language: string; light: string; theme: string; }; } type Theme = "light" | "dark"; const themeEvent = "progmasoft-theme-change"; function readTheme(): Theme { return window.localStorage.getItem("progmasoft_theme") === "dark" ? "dark" : "light"; } /** * Applies a theme to the document at once, in the click that chose it. * * Transitions are switched off for the two frames of the change: otherwise * every button and link would animate its colours while the page around it * has already changed, which looks like flicker. */ function applyTheme(theme: Theme) { const root = document.documentElement; root.classList.add("theme-switching"); root.dataset.theme = theme; root.style.colorScheme = theme; window.requestAnimationFrame(() => window.requestAnimationFrame(() => root.classList.remove("theme-switching"), ), ); } function subscribeTheme(onStoreChange: () => void) { window.addEventListener("storage", onStoreChange); window.addEventListener(themeEvent, onStoreChange); return () => { window.removeEventListener("storage", onStoreChange); window.removeEventListener(themeEvent, onStoreChange); }; } export function PreferenceControls({ locale, labels, }: PreferenceControlsProps) { const theme = useSyncExternalStore( subscribeTheme, readTheme, () => "light", ); const [switchingLocale, startLocaleSwitch] = useTransition(); // Another tab, or another control on this page, changed the theme. useEffect(() => { if (document.documentElement.dataset.theme !== theme) { applyTheme(theme); } }, [theme]); function toggleTheme() { const selected: Theme = theme === "light" ? "dark" : "light"; applyTheme(selected); window.localStorage.setItem("progmasoft_theme", selected); window.dispatchEvent(new Event(themeEvent)); } // The server stores the preference and re-renders the current route in the // selected language, so the page changes in place: nothing is reloaded and // scroll position, theme and client state stay as they are. function chooseLocale(selected: string) { if (!isLocale(selected) || selected === locale) { return; } startLocaleSwitch(async () => { try { await selectLocale(selected); } catch { // The request did not reach the server. The query parameter asks the // proxy for the same preference with an ordinary navigation. const url = new URL(window.location.href); url.searchParams.set("lang", selected); window.location.assign(url); } }); } return (
); }