/* eslint-disable react-refresh/only-export-components */ import { useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Check } from "lucide-react"; import { useTheme } from "@/components/theme-provider"; import { Input } from "@/components/input"; import { Select2 } from "@/components/select2"; import { ACCENT_PRESET_COLORS, applyAccentColor, applyFontSize, applyUiFont, FONT_SIZES, UI_FONTS, } from "@/lib/theme"; import { changeAppLanguage, normalizeLanguageCode } from "@/i18n/i18n"; import { LANGUAGES } from "@/i18n/languages"; import { getUserPreferences, saveUserPreferences } from "@/api/open-tabs-api"; import type { FontSizeId, ThemeId, UiFontId } from "@/types/ui-types"; import { BUILTIN_THEMES, SYSTEM_THEME, themeLabel, useThemes, type ThemeSwatch, } from "@/look/theme-registry"; import { useChooseTheme } from "@/look/use-look-theme"; export const DEFAULT_ACCENT = "#f59145"; /** The built-in themes, with System first. Plugin themes come from useThemeOptions. */ export const THEMES: { id: ThemeId; preview: string }[] = [ { id: SYSTEM_THEME, preview: "auto" }, ...BUILTIN_THEMES.map((theme) => ({ id: theme.id, preview: theme.swatch.background, })), ]; export interface ThemeOption { id: ThemeId; label: string; /** Null for System, which previews light and dark side by side. */ swatch: ThemeSwatch | null; } /** Every theme to pick from, System first, plugin themes included. */ export function useThemeOptions(): ThemeOption[] { const { t } = useTranslation(); const themes = useThemes(); return [ { id: SYSTEM_THEME, label: t("newUi.sidebar.userProfile.themeSystem"), swatch: null, }, ...themes.map((theme) => ({ id: theme.id, label: themeLabel(theme, t), swatch: theme.swatch, })), ]; } const LIGHT_SWATCH = BUILTIN_THEMES.find( (theme) => theme.id === "light", )!.swatch; const DARK_SWATCH = BUILTIN_THEMES.find((theme) => theme.id === "dark")!.swatch; export function isHexColor(value: string): boolean { return /^#[0-9a-fA-F]{6}$/.test(value) || /^#[0-9a-fA-F]{3}$/.test(value); } type CloudPrefs = Parameters[0]; /** Saves to the account only for users who sync settings to the server. */ async function saveIfCloud(prefs: CloudPrefs): Promise { try { const current = await getUserPreferences(); if (current?.storageMode === "cloud") await saveUserPreferences(prefs); } catch { // The local copy already holds the change. } } /** * Theme, accent, font and language, applied right away and stored the same * way the profile settings store them. */ export function useAppearanceSettings() { const { theme } = useTheme(); const chooseTheme = useChooseTheme(); const [accent, setAccentState] = useState( () => localStorage.getItem("termix-accent") ?? DEFAULT_ACCENT, ); const [fontSize, setFontSizeState] = useState( () => (localStorage.getItem("termix-font-size") as FontSizeId) ?? "md", ); const [uiFont, setUiFontState] = useState( () => (localStorage.getItem("termix-ui-font") as UiFontId) ?? "jetbrains-mono", ); const [language, setLanguageState] = useState(() => normalizeLanguageCode(localStorage.getItem("i18nextLng")), ); return { theme, accent, fontSize, uiFont, language, setTheme(id: ThemeId) { chooseTheme(id); }, setAccent(value: string) { setAccentState(value); localStorage.setItem("termix-accent", value); applyAccentColor(value); void saveIfCloud({ accentColor: value }); }, setFontSize(id: FontSizeId) { setFontSizeState(id); applyFontSize(id); void saveIfCloud({ fontSize: id }); }, setUiFont(id: UiFontId) { setUiFontState(id); applyUiFont(id); }, setLanguage(code: string) { void changeAppLanguage(code) .then((next) => { setLanguageState(next); void saveIfCloud({ language: next }); }) .catch(() => {}); }, }; } function ThemePreview({ swatch }: { swatch: ThemeSwatch | null }) { if (!swatch) { return (
); } return (
); } function MiniWindow({ background, panel, text }: ThemeSwatch) { return (
); } export function ThemeGrid({ value, onChange, }: { value: ThemeId; onChange: (id: ThemeId) => void; }) { const { setThemePreview } = useTheme(); const options = useThemeOptions(); return (
{options.map(({ id, label, swatch }) => { const active = value === id; return ( ); })}
); } export function AccentPicker({ value, onChange, }: { value: string; onChange: (value: string) => void; }) { const { t } = useTranslation(); const colorInputRef = useRef(null); const [draft, setDraft] = useState(value); const [lastValue, setLastValue] = useState(value); if (value !== lastValue) { setLastValue(value); setDraft(value); } const commit = () => { const next = draft.trim(); if (isHexColor(next)) onChange(next); else setDraft(value); }; return (
{ACCENT_PRESET_COLORS.map((color) => ( ))}
); } export function FontSizePicker({ value, onChange, }: { value: FontSizeId; onChange: (id: FontSizeId) => void; }) { return (
{FONT_SIZES.map((size) => ( ))}
); } export function UiFontSelect({ value, onChange, }: { value: UiFontId; onChange: (id: UiFontId) => void; }) { return ( onChange(e.target.value as UiFontId)} className="w-full border border-border bg-background px-2.5 py-1.5 text-xs text-foreground outline-none focus:ring-1 focus:ring-ring" > {UI_FONTS.map((font) => ( ))} ); } export function LanguageSelect({ value, onChange, }: { value: string; onChange: (code: string) => void; }) { return ( onChange(e.target.value)} className="w-full border border-border bg-background px-2.5 py-1.5 text-xs text-foreground outline-none focus:ring-1 focus:ring-ring" > {LANGUAGES.map((lang) => ( ))} ); }