/**
* The header "Appearance" control: an icon button that opens a popover for
* choosing the base mode (System / Dark / Light) and the theme/accent. The
* accent grid lists every theme in the registry — built-in system themes plus
* any imported user themes — and offers Import / Export of theme files. Reads
* and writes the live theme via `useTheme()`.
* @module gui/components/ThemePicker
*/
import { useEffect, useRef, useState } from "react";
import { useIntl, defineMessages } from "react-intl";
import { useTheme } from "../theme/ThemeProvider.jsx";
import { parseThemeFile, serializeTheme } from "../theme/themeFile.js";
import { PaletteIcon, SunIcon, MoonIcon, MonitorIcon } from "./icons.jsx";
import { jsxDEV as _jsxDEV, Fragment as _Fragment } from "react/jsx-dev-runtime";
const msgs = defineMessages({
appearance: {
id: "theme.appearance",
defaultMessage: "Appearance"
},
mode: {
id: "theme.mode",
defaultMessage: "Mode"
},
system: {
id: "theme.system",
defaultMessage: "System"
},
dark: {
id: "theme.dark",
defaultMessage: "Dark"
},
light: {
id: "theme.light",
defaultMessage: "Light"
},
accent: {
id: "theme.accent",
defaultMessage: "Accent"
},
importTheme: {
id: "theme.import",
defaultMessage: "Import theme…"
},
exportTheme: {
id: "theme.export",
defaultMessage: "Export"
},
removeTheme: {
id: "theme.remove",
defaultMessage: "Remove {name}"
}
});
const MODES = [{
id: "system",
label: msgs.system,
Icon: MonitorIcon
}, {
id: "dark",
label: msgs.dark,
Icon: MoonIcon
}, {
id: "light",
label: msgs.light,
Icon: SunIcon
}];
/**
* @returns {JSX.Element} The header appearance (theme) picker.
*/
export default function ThemePicker() {
const intl = useIntl();
const {
mode,
setMode,
accent,
setAccent,
themes,
userThemeIds,
addUserTheme,
removeUserTheme
} = useTheme();
const [open, setOpen] = useState(false);
const [error, setError] = useState("");
const fileRef = useRef(null);
// Close on Escape (matches the links menu).
useEffect(() => {
if (!open) return undefined;
const onKey = e => {
if (e.key === "Escape") setOpen(false);
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open]);
async function onImport(e) {
const file = e.target.files?.[0];
e.target.value = ""; // allow re-picking the same file
if (!file) return;
const res = parseThemeFile(await file.text());
if (!res.ok) {
setError(res.error);
return;
}
setError("");
addUserTheme(res.theme);
setAccent(res.theme.id);
}
function onExport() {
const theme = themes.find(t => t.id === accent) || themes[0];
if (!theme) return;
const blob = new Blob([serializeTheme(theme)], {
type: "application/json"
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${theme.id}-theme.json`;
a.click();
URL.revokeObjectURL(url);
}
return /*#__PURE__*/_jsxDEV("div", {
className: "theme-menu",
children: [/*#__PURE__*/_jsxDEV("button", {
className: `theme-trigger${open ? " on" : ""}`,
onClick: () => setOpen(o => !o),
title: intl.formatMessage(msgs.appearance),
"aria-label": intl.formatMessage(msgs.appearance),
"aria-haspopup": "dialog",
"aria-expanded": open,
children: [/*#__PURE__*/_jsxDEV(PaletteIcon, {}, void 0, false), /*#__PURE__*/_jsxDEV("span", {
className: "ctl-label",
children: intl.formatMessage(msgs.appearance)
}, void 0, false)]
}, void 0, true), open && /*#__PURE__*/_jsxDEV(_Fragment, {
children: [/*#__PURE__*/_jsxDEV("div", {
className: "theme-scrim",
onClick: () => setOpen(false),
"aria-hidden": "true"
}, void 0, false), /*#__PURE__*/_jsxDEV("div", {
className: "theme-pop",
role: "dialog",
"aria-label": intl.formatMessage(msgs.appearance),
children: [/*#__PURE__*/_jsxDEV("div", {
className: "theme-section",
children: [/*#__PURE__*/_jsxDEV("div", {
className: "theme-section-label",
id: "theme-mode-label",
children: intl.formatMessage(msgs.mode)
}, void 0, false), /*#__PURE__*/_jsxDEV("div", {
className: "theme-segmented",
role: "radiogroup",
"aria-labelledby": "theme-mode-label",
children: MODES.map(({
id,
label,
Icon
}) => /*#__PURE__*/_jsxDEV("button", {
type: "button",
role: "radio",
"aria-checked": mode === id,
className: `theme-seg${mode === id ? " on" : ""}`,
onClick: () => setMode(id),
children: [/*#__PURE__*/_jsxDEV(Icon, {}, void 0, false), /*#__PURE__*/_jsxDEV("span", {
children: intl.formatMessage(label)
}, void 0, false)]
}, id, true))
}, void 0, false)]
}, void 0, true), /*#__PURE__*/_jsxDEV("div", {
className: "theme-section",
children: [/*#__PURE__*/_jsxDEV("div", {
className: "theme-section-label",
id: "theme-accent-label",
children: intl.formatMessage(msgs.accent)
}, void 0, false), /*#__PURE__*/_jsxDEV("div", {
className: "theme-swatches",
role: "radiogroup",
"aria-labelledby": "theme-accent-label",
children: themes.map(t => /*#__PURE__*/_jsxDEV("span", {
className: "theme-swatch-wrap",
children: [/*#__PURE__*/_jsxDEV("button", {
type: "button",
role: "radio",
"aria-checked": accent === t.id,
className: `theme-swatch${accent === t.id ? " on" : ""}`,
style: {
"--sw": t.swatch
},
onClick: () => setAccent(t.id),
title: t.label,
"aria-label": t.label
}, void 0, false), userThemeIds.includes(t.id) && /*#__PURE__*/_jsxDEV("button", {
type: "button",
className: "theme-swatch-del",
onClick: () => removeUserTheme(t.id),
title: intl.formatMessage(msgs.removeTheme, {
name: t.label
}),
"aria-label": intl.formatMessage(msgs.removeTheme, {
name: t.label
}),
children: "×"
}, void 0, false)]
}, t.id, true))
}, void 0, false), /*#__PURE__*/_jsxDEV("div", {
className: "theme-file-actions",
children: [/*#__PURE__*/_jsxDEV("button", {
type: "button",
className: "theme-file-btn",
onClick: () => fileRef.current?.click(),
children: intl.formatMessage(msgs.importTheme)
}, void 0, false), /*#__PURE__*/_jsxDEV("button", {
type: "button",
className: "theme-file-btn",
onClick: onExport,
children: intl.formatMessage(msgs.exportTheme)
}, void 0, false), /*#__PURE__*/_jsxDEV("input", {
ref: fileRef,
type: "file",
accept: ".json,application/json",
hidden: true,
onChange: onImport
}, void 0, false)]
}, void 0, true), error && /*#__PURE__*/_jsxDEV("div", {
className: "theme-file-error",
role: "alert",
children: error
}, void 0, false)]
}, void 0, true)]
}, void 0, true)]
}, void 0, true)]
}, void 0, true);
}