/** * @file AppearanceSettings.tsx * @description Settings → Appearance (`/settings/appearance`), grouped so * one card holds one subject: **Text & fonts** (`#fonts`) — the interface * language (`#language`), the text size (`lib/fontSize.ts`, `#text-size`, * issue #79), the terminal font and its size (`lib/terminalFont.ts`, * `#terminal-font`, with a preview line), and — local mode only — whether a * pasted image reaches a `claude` pane natively (`lib/nativePaste.ts`, * `#native-paste`, this browser only); **Sessions** (`#sessions-view`) — * the per-user Chat UI default (`ChatUiSettings`, `#chat-ui`) and whether the * session view's tabs hold every workspace's sessions (`lib/workspaceTabs.ts`, * `#workspace-tabs`), and whether a Markdown file opens as Preview (the * default) or Raw (`lib/markdownView.ts`, `#markdown-view`); then the * Dark / Light / System mode switch and one named-theme picker per mode, and the * corner pet (`PetSettings`, deep-linked as `#pet`) — the old General and Pet * pages redirect here. Theme state is in `hooks/useTheme.ts`, palettes in * `index.css`. Each card's swatch re-scopes the theme's CSS variables on its * own element (`dark` class or `data-mode`, plus `data-theme`), so the preview * is painted by the same tokens as the app — no hex is duplicated here. * **Reset to defaults** (after a confirm) returns to Dark with both stock themes. * **Run setup again** reopens the first-run wizard (`components/onboarding`). * @author Nguyễn Ngọc Trí Vĩ */ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Monitor, Moon, RotateCcw, Sun } from "lucide-react"; import { SettingsRow, SettingsSection, SettingsSegmented, SettingsSwitchRow } from "./SettingsRow"; import { PetSettings } from "./PetSettings"; import { ChatUiSettings } from "./ChatUiSettings"; import { Select } from "../Select"; import { THEMES, useTheme, type Theme, type ThemeMode } from "../../hooks/useTheme"; import { confirmDialog } from "../../lib/confirm"; import { resetOnboarding } from "../../lib/userPrefs"; import { toast } from "../../lib/toast"; import { TERMINAL_FONTS, readTerminalFont, writeTerminalFont, type TerminalFont, } from "../../lib/terminalFont"; import { readAllTabs, writeAllTabs } from "../../lib/workspaceTabs"; import { readMarkdownPreview, writeMarkdownPreview } from "../../lib/markdownView"; import { readNativePaste, writeNativePaste } from "../../lib/nativePaste"; import { usePermissions } from "../AuthGate"; import { TERMINAL_FONT_SIZES, UI_FONT_SIZES, readTerminalFontSize, readUiFontSize, writeTerminalFontSize, writeUiFontSize, type UiFontSize, } from "../../lib/fontSize"; export const THEME_LABELS: Record = { darcula: "Darcula", "apple-dark": "Apple Dark", "apple-light": "Apple Light", canvas: "Canvas", "orca-dark": "Orca Dark", "orca-light": "Orca Light", }; const MODES: { mode: ThemeMode; Icon: typeof Moon }[] = [ { mode: "dark", Icon: Moon }, { mode: "light", Icon: Sun }, { mode: "system", Icon: Monitor }, ]; export function Swatch({ mode, name }: { mode: Theme; name: string }) { return (
const{" "} x{" "} ={" "} "ok"
); } export function AppearanceSettings() { const { t, i18n } = useTranslation("settings"); const { mode, theme, themeFor, setMode, setThemeFor } = useTheme(); const [termFont, setTermFont] = useState(readTerminalFont); const [termSize, setTermSize] = useState(readTerminalFontSize); const [uiSize, setUiSize] = useState(readUiFontSize); const [allTabs, setAllTabs] = useState(readAllTabs); const [mdPreview, setMdPreview] = useState(readMarkdownPreview); const [nativePaste, setNativePaste] = useState(readNativePaste); const { local } = usePermissions(); const pickTermFont = (font: TerminalFont) => { writeTerminalFont(font); setTermFont(font); }; const reset = async () => { const ok = await confirmDialog({ title: t("appearance.resetTitle"), message: t("appearance.resetBody"), confirmLabel: t("appearance.reset"), }); if (!ok) return; setMode("dark"); setThemeFor("dark", "default"); setThemeFor("light", "default"); toast.success(t("appearance.resetDone")); }; return (
void i18n.changeLanguage(v)} options={[ { value: "en" as const, label: t("language.names.en") }, { value: "vi" as const, label: t("language.names.vi") }, ]} /> } /> { const size = Number(v) as UiFontSize; writeUiFontSize(size); setUiSize(size); }} options={UI_FONT_SIZES.map((size) => ({ value: String(size), label: t(`textSize.names.${size}`), }))} /> } /> label={t("terminalFont.title")} value={termFont} onChange={pickTermFont} options={(Object.keys(TERMINAL_FONTS) as TerminalFont[]).map((f) => ({ value: f, label: t(`terminalFont.names.${f}`), }))} /> } /> { writeTerminalFontSize(Number(v)); setTermSize(Number(v)); }} options={TERMINAL_FONT_SIZES.map((size) => ({ value: String(size), label: `${size}px`, }))} /> } />

❯ Tiếng Việt: ấ ế ố ồ ủ ữ — const x = {"{ ok: true }"}; 0O il1|

{local && ( { writeNativePaste(on); setNativePaste(on); }} /> )}
{ writeAllTabs(on); setAllTabs(on); }} /> { writeMarkdownPreview(v === "preview"); setMdPreview(v === "preview"); }} options={[ { value: "preview" as const, label: t("markdownView.preview") }, { value: "raw" as const, label: t("markdownView.raw") }, ]} /> } />

{t("appearance.theme")}

({ value: m, label: ( <> {t(`appearance.modes.${m}`)} ), }))} /> } /> {(["dark", "light"] as const).map((group) => (

{t(`appearance.${group}Themes`)} {theme === group && ( {t("appearance.active")} )}

{THEMES[group].map((name) => { const selected = themeFor[group] === name; const label = THEME_LABELS[name] ?? t("appearance.default"); return ( ); })}
))}

{t("appearance.pet")}

{t("appearance.petDescription")}

{t("onboarding:rerun.button")} } />
); }