From 387fb96f7307317e809eb3b2b08386f13eeacdb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=86=8A=E5=AD=9D=E5=85=B5?= <1591788658@qq.com> Date: Thu, 2 Apr 2026 18:36:32 -0700 Subject: [PATCH 01/11] feat: add appearance settings types and constants Defines AccentColor, InterfaceDensity, TerminalScheme, CodeThemeLight, CodeThemeDark types and AppearanceSettings interface, along with all presets (accent colors, font options, density values, terminal color schemes, code theme options) and DEFAULT_APPEARANCE. Co-Authored-By: Claude Sonnet 4.6 --- src/lib/appearance/constants.ts | 352 ++++++++++++++++++++++++++++++++ src/lib/appearance/types.ts | 44 ++++ 2 files changed, 396 insertions(+) create mode 100644 src/lib/appearance/constants.ts create mode 100644 src/lib/appearance/types.ts diff --git a/src/lib/appearance/constants.ts b/src/lib/appearance/constants.ts new file mode 100644 index 0000000..203b0f2 --- /dev/null +++ b/src/lib/appearance/constants.ts @@ -0,0 +1,352 @@ +import type { + AccentColor, + AppearanceSettings, + InterfaceDensity, + TerminalScheme, +} from "./types" +import type { ITheme } from "@xterm/xterm" + +export const ACCENT_PRESETS: Record< + AccentColor, + { light: string; dark: string; label: string; dot: string } +> = { + blue: { + light: "oklch(0.55 0.2 255)", + dark: "oklch(0.7 0.18 255)", + label: "Blue", + dot: "#3b82f6", + }, + purple: { + light: "oklch(0.5 0.2 290)", + dark: "oklch(0.7 0.18 290)", + label: "Purple", + dot: "#8b5cf6", + }, + pink: { + light: "oklch(0.55 0.2 350)", + dark: "oklch(0.72 0.18 350)", + label: "Pink", + dot: "#ec4899", + }, + red: { + light: "oklch(0.55 0.22 25)", + dark: "oklch(0.7 0.19 25)", + label: "Red", + dot: "#ef4444", + }, + orange: { + light: "oklch(0.6 0.2 55)", + dark: "oklch(0.72 0.18 55)", + label: "Orange", + dot: "#f97316", + }, + yellow: { + light: "oklch(0.6 0.18 85)", + dark: "oklch(0.75 0.16 85)", + label: "Yellow", + dot: "#eab308", + }, + green: { + light: "oklch(0.55 0.18 155)", + dark: "oklch(0.7 0.16 155)", + label: "Green", + dot: "#22c55e", + }, + cyan: { + light: "oklch(0.55 0.15 200)", + dark: "oklch(0.72 0.13 200)", + label: "Cyan", + dot: "#06b6d4", + }, + graphite: { + light: "oklch(0.4 0 0)", + dark: "oklch(0.7 0 0)", + label: "Graphite", + dot: "#71717a", + }, +} + +export const UI_FONT_PRESETS = [ + { value: "system-ui", label: "System Default" }, + { + value: "'JetBrains Mono', monospace", + label: "JetBrains Mono", + }, + { value: "custom", label: "Custom..." }, +] as const + +export const CODE_FONT_PRESETS = [ + { + value: "'JetBrains Mono', monospace", + label: "JetBrains Mono", + }, + { value: "'Fira Code', monospace", label: "Fira Code" }, + { value: "'Cascadia Code', monospace", label: "Cascadia Code" }, + { + value: "'Source Code Pro', monospace", + label: "Source Code Pro", + }, + { value: "Menlo, monospace", label: "Menlo" }, + { value: "Monaco, monospace", label: "Monaco" }, + { value: "monospace", label: "monospace" }, + { value: "custom", label: "Custom..." }, +] as const + +export const DENSITY_VALUES: Record< + InterfaceDensity, + { padding: string; gap: string; lineHeight: string } +> = { + compact: { padding: "0.25rem", gap: "0.25rem", lineHeight: "1.4" }, + default: { padding: "0.5rem", gap: "0.5rem", lineHeight: "1.5" }, + spacious: { padding: "0.75rem", gap: "0.75rem", lineHeight: "1.7" }, +} + +export const TERMINAL_SCHEMES: Record = { + default: { + background: "#1a1a1a", + foreground: "#e0e0e0", + cursor: "#e0e0e0", + cursorAccent: "#1a1a1a", + selectionBackground: "#444444", + black: "#1a1a1a", + red: "#f87171", + green: "#4ade80", + yellow: "#facc15", + blue: "#60a5fa", + magenta: "#c084fc", + cyan: "#22d3ee", + white: "#e0e0e0", + brightBlack: "#737373", + brightRed: "#fca5a5", + brightGreen: "#86efac", + brightYellow: "#fde68a", + brightBlue: "#93c5fd", + brightMagenta: "#d8b4fe", + brightCyan: "#67e8f9", + brightWhite: "#ffffff", + }, + "solarized-dark": { + background: "#002b36", + foreground: "#839496", + cursor: "#839496", + cursorAccent: "#002b36", + selectionBackground: "#073642", + black: "#073642", + red: "#dc322f", + green: "#859900", + yellow: "#b58900", + blue: "#268bd2", + magenta: "#d33682", + cyan: "#2aa198", + white: "#eee8d5", + brightBlack: "#586e75", + brightRed: "#cb4b16", + brightGreen: "#586e75", + brightYellow: "#657b83", + brightBlue: "#839496", + brightMagenta: "#6c71c4", + brightCyan: "#93a1a1", + brightWhite: "#fdf6e3", + }, + "solarized-light": { + background: "#fdf6e3", + foreground: "#657b83", + cursor: "#657b83", + cursorAccent: "#fdf6e3", + selectionBackground: "#eee8d5", + black: "#073642", + red: "#dc322f", + green: "#859900", + yellow: "#b58900", + blue: "#268bd2", + magenta: "#d33682", + cyan: "#2aa198", + white: "#eee8d5", + brightBlack: "#586e75", + brightRed: "#cb4b16", + brightGreen: "#586e75", + brightYellow: "#657b83", + brightBlue: "#839496", + brightMagenta: "#6c71c4", + brightCyan: "#93a1a1", + brightWhite: "#fdf6e3", + }, + dracula: { + background: "#282a36", + foreground: "#f8f8f2", + cursor: "#f8f8f2", + cursorAccent: "#282a36", + selectionBackground: "#44475a", + black: "#21222c", + red: "#ff5555", + green: "#50fa7b", + yellow: "#f1fa8c", + blue: "#bd93f9", + magenta: "#ff79c6", + cyan: "#8be9fd", + white: "#f8f8f2", + brightBlack: "#6272a4", + brightRed: "#ff6e6e", + brightGreen: "#69ff94", + brightYellow: "#ffffa5", + brightBlue: "#d6acff", + brightMagenta: "#ff92df", + brightCyan: "#a4ffff", + brightWhite: "#ffffff", + }, + "one-dark": { + background: "#282c34", + foreground: "#abb2bf", + cursor: "#528bff", + cursorAccent: "#282c34", + selectionBackground: "#3e4451", + black: "#282c34", + red: "#e06c75", + green: "#98c379", + yellow: "#e5c07b", + blue: "#61afef", + magenta: "#c678dd", + cyan: "#56b6c2", + white: "#abb2bf", + brightBlack: "#5c6370", + brightRed: "#e06c75", + brightGreen: "#98c379", + brightYellow: "#e5c07b", + brightBlue: "#61afef", + brightMagenta: "#c678dd", + brightCyan: "#56b6c2", + brightWhite: "#ffffff", + }, + nord: { + background: "#2e3440", + foreground: "#d8dee9", + cursor: "#d8dee9", + cursorAccent: "#2e3440", + selectionBackground: "#434c5e", + black: "#3b4252", + red: "#bf616a", + green: "#a3be8c", + yellow: "#ebcb8b", + blue: "#81a1c1", + magenta: "#b48ead", + cyan: "#88c0d0", + white: "#e5e9f0", + brightBlack: "#4c566a", + brightRed: "#bf616a", + brightGreen: "#a3be8c", + brightYellow: "#ebcb8b", + brightBlue: "#81a1c1", + brightMagenta: "#b48ead", + brightCyan: "#8fbcbb", + brightWhite: "#eceff4", + }, + monokai: { + background: "#272822", + foreground: "#f8f8f2", + cursor: "#f8f8f0", + cursorAccent: "#272822", + selectionBackground: "#49483e", + black: "#272822", + red: "#f92672", + green: "#a6e22e", + yellow: "#f4bf75", + blue: "#66d9ef", + magenta: "#ae81ff", + cyan: "#a1efe4", + white: "#f8f8f2", + brightBlack: "#75715e", + brightRed: "#f92672", + brightGreen: "#a6e22e", + brightYellow: "#f4bf75", + brightBlue: "#66d9ef", + brightMagenta: "#ae81ff", + brightCyan: "#a1efe4", + brightWhite: "#f9f8f5", + }, + "github-dark": { + background: "#0d1117", + foreground: "#c9d1d9", + cursor: "#c9d1d9", + cursorAccent: "#0d1117", + selectionBackground: "#264f78", + black: "#484f58", + red: "#ff7b72", + green: "#3fb950", + yellow: "#d29922", + blue: "#58a6ff", + magenta: "#bc8cff", + cyan: "#39c5cf", + white: "#b1bac4", + brightBlack: "#6e7681", + brightRed: "#ffa198", + brightGreen: "#56d364", + brightYellow: "#e3b341", + brightBlue: "#79c0ff", + brightMagenta: "#d2a8ff", + brightCyan: "#56d4dd", + brightWhite: "#f0f6fc", + }, + "github-light": { + background: "#ffffff", + foreground: "#24292f", + cursor: "#24292f", + cursorAccent: "#ffffff", + selectionBackground: "#accef7", + black: "#24292f", + red: "#cf222e", + green: "#116329", + yellow: "#4d2d00", + blue: "#0969da", + magenta: "#8250df", + cyan: "#1b7c83", + white: "#6e7781", + brightBlack: "#57606a", + brightRed: "#a40e26", + brightGreen: "#1a7f37", + brightYellow: "#633c01", + brightBlue: "#218bff", + brightMagenta: "#a475f9", + brightCyan: "#3192aa", + brightWhite: "#8c959f", + }, +} + +export const CODE_THEME_LIGHT_OPTIONS = [ + { value: "github-light", label: "GitHub Light" }, + { value: "vitesse-light", label: "Vitesse Light" }, + { value: "min-light", label: "Min Light" }, +] as const + +export const CODE_THEME_DARK_OPTIONS = [ + { value: "github-dark", label: "GitHub Dark" }, + { value: "vitesse-dark", label: "Vitesse Dark" }, + { value: "one-dark-pro", label: "One Dark Pro" }, + { value: "dracula", label: "Dracula" }, +] as const + +export const TERMINAL_SCHEME_OPTIONS = [ + { value: "default", label: "Default" }, + { value: "solarized-dark", label: "Solarized Dark" }, + { value: "solarized-light", label: "Solarized Light" }, + { value: "dracula", label: "Dracula" }, + { value: "one-dark", label: "One Dark" }, + { value: "nord", label: "Nord" }, + { value: "monokai", label: "Monokai" }, + { value: "github-dark", label: "GitHub Dark" }, + { value: "github-light", label: "GitHub Light" }, +] as const + +export const DEFAULT_APPEARANCE: AppearanceSettings = { + accentColor: "blue", + uiFont: "'JetBrains Mono', monospace", + codeFont: "'JetBrains Mono', monospace", + uiFontSize: 14, + codeFontSize: 14, + codeThemeLight: "github-light", + codeThemeDark: "github-dark", + terminalScheme: "default", + density: "default", + reduceMotion: "system", +} + +export const APPEARANCE_STORAGE_KEY = "codeg-appearance" diff --git a/src/lib/appearance/types.ts b/src/lib/appearance/types.ts new file mode 100644 index 0000000..83b0b6c --- /dev/null +++ b/src/lib/appearance/types.ts @@ -0,0 +1,44 @@ +export type AccentColor = + | "blue" + | "purple" + | "pink" + | "red" + | "orange" + | "yellow" + | "green" + | "cyan" + | "graphite" + +export type InterfaceDensity = "compact" | "default" | "spacious" + +export type TerminalScheme = + | "default" + | "solarized-dark" + | "solarized-light" + | "dracula" + | "one-dark" + | "nord" + | "monokai" + | "github-dark" + | "github-light" + +export type CodeThemeLight = "github-light" | "vitesse-light" | "min-light" + +export type CodeThemeDark = + | "github-dark" + | "vitesse-dark" + | "one-dark-pro" + | "dracula" + +export interface AppearanceSettings { + accentColor: AccentColor + uiFont: string + codeFont: string + uiFontSize: number + codeFontSize: number + codeThemeLight: CodeThemeLight + codeThemeDark: CodeThemeDark + terminalScheme: TerminalScheme + density: InterfaceDensity + reduceMotion: "system" | "on" | "off" +} From d5102bf2f26a0efe5f9eb7387ecee60796b385a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=86=8A=E5=AD=9D=E5=85=B5?= <1591788658@qq.com> Date: Thu, 2 Apr 2026 18:38:11 -0700 Subject: [PATCH 02/11] feat(appearance): add AppearanceProvider context with CSS variable injection Co-Authored-By: Claude Sonnet 4.6 --- src/app/globals.css | 7 ++ src/app/layout.tsx | 3 +- src/lib/appearance/use-appearance.ts | 144 +++++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 src/lib/appearance/use-appearance.ts diff --git a/src/app/globals.css b/src/app/globals.css index eab6a05..3c846ac 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -48,6 +48,12 @@ --sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-border: oklch(0.922 0 0); --sidebar-ring: oklch(0.708 0 0); + --font-code: 'JetBrains Mono', monospace; + --font-size-base: 14px; + --font-size-code: 14px; + --density-padding: 0.5rem; + --density-gap: 0.5rem; + --density-line-height: 1.5; } .dark { @@ -117,6 +123,7 @@ @theme inline { --font-sans: var(--font-sans); + --font-code: var(--font-code); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index cfc1b60..0c01339 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,6 +7,7 @@ import { getMessagesForLocale } from "@/i18n/messages" import { resolveRequestLocale } from "@/i18n/resolve-request-locale" import { ThemeProvider } from "@/components/theme-provider" import { toIntlLocale } from "@/lib/i18n" +import { AppearanceProvider } from "@/lib/appearance/use-appearance" const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"], @@ -50,7 +51,7 @@ export default async function RootLayout({ enableSystem disableTransitionOnChange > - {children} + {children} diff --git a/src/lib/appearance/use-appearance.ts b/src/lib/appearance/use-appearance.ts new file mode 100644 index 0000000..52a530d --- /dev/null +++ b/src/lib/appearance/use-appearance.ts @@ -0,0 +1,144 @@ +"use client" + +import { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from "react" +import type { AppearanceSettings } from "./types" +import { + ACCENT_PRESETS, + APPEARANCE_STORAGE_KEY, + DEFAULT_APPEARANCE, + DENSITY_VALUES, +} from "./constants" +import type { ReactNode } from "react" +import { createElement } from "react" + +interface AppearanceContextType { + settings: AppearanceSettings + update: ( + key: K, + value: AppearanceSettings[K] + ) => void +} + +const AppearanceContext = createContext({ + settings: DEFAULT_APPEARANCE, + update: () => {}, +}) + +function loadSettings(): AppearanceSettings { + if (typeof window === "undefined") return DEFAULT_APPEARANCE + try { + const raw = localStorage.getItem(APPEARANCE_STORAGE_KEY) + if (!raw) return DEFAULT_APPEARANCE + return { ...DEFAULT_APPEARANCE, ...JSON.parse(raw) } + } catch { + return DEFAULT_APPEARANCE + } +} + +function saveSettings(settings: AppearanceSettings) { + localStorage.setItem(APPEARANCE_STORAGE_KEY, JSON.stringify(settings)) +} + +function applyToDOM(settings: AppearanceSettings) { + const root = document.documentElement.style + const isDark = document.documentElement.classList.contains("dark") + + // Accent color + const accent = ACCENT_PRESETS[settings.accentColor] + const accentValue = isDark ? accent.dark : accent.light + root.setProperty("--primary", accentValue) + root.setProperty("--ring", accentValue) + root.setProperty("--sidebar-primary", accentValue) + + // Fonts + root.setProperty("--font-sans", settings.uiFont) + root.setProperty("--font-code", settings.codeFont) + + // Font sizes + root.setProperty("--font-size-base", `${settings.uiFontSize}px`) + root.setProperty("--font-size-code", `${settings.codeFontSize}px`) + + // Density + const density = DENSITY_VALUES[settings.density] + root.setProperty("--density-padding", density.padding) + root.setProperty("--density-gap", density.gap) + root.setProperty("--density-line-height", density.lineHeight) + + // Reduce motion + const prefersReduced = window.matchMedia( + "(prefers-reduced-motion: reduce)" + ).matches + const shouldReduce = + settings.reduceMotion === "on" || + (settings.reduceMotion === "system" && prefersReduced) + root.setProperty("--transition-duration", shouldReduce ? "0s" : "") + root.setProperty("--animation-duration", shouldReduce ? "0s" : "") +} + +export function AppearanceProvider({ children }: { children: ReactNode }) { + const [settings, setSettings] = + useState(DEFAULT_APPEARANCE) + + // Load from localStorage on mount + useEffect(() => { + setSettings(loadSettings()) + }, []) + + // Apply CSS variables whenever settings or theme class changes + useEffect(() => { + applyToDOM(settings) + + // Re-apply when dark/light class changes (accent color depends on it) + const observer = new MutationObserver(() => applyToDOM(settings)) + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }) + return () => observer.disconnect() + }, [settings]) + + // Cross-tab sync + useEffect(() => { + const handler = (e: StorageEvent) => { + if (e.key === APPEARANCE_STORAGE_KEY && e.newValue) { + try { + setSettings({ ...DEFAULT_APPEARANCE, ...JSON.parse(e.newValue) }) + } catch { + // ignore + } + } + } + window.addEventListener("storage", handler) + return () => window.removeEventListener("storage", handler) + }, []) + + const update = useCallback( + ( + key: K, + value: AppearanceSettings[K] + ) => { + setSettings((prev) => { + const next = { ...prev, [key]: value } + saveSettings(next) + return next + }) + }, + [] + ) + + return createElement( + AppearanceContext.Provider, + { value: { settings, update } }, + children + ) +} + +export function useAppearance() { + return useContext(AppearanceContext) +} From 7bd389fcd0d52ccbe52feb4c8ecdf8a6ee785fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=86=8A=E5=AD=9D=E5=85=B5?= <1591788658@qq.com> Date: Thu, 2 Apr 2026 18:39:28 -0700 Subject: [PATCH 03/11] feat(appearance): add i18n keys for expanded appearance settings Co-Authored-By: Claude Sonnet 4.6 --- src/i18n/messages/ar.json | 47 ++++++++++++++++++++++++++-------- src/i18n/messages/de.json | 47 ++++++++++++++++++++++++++-------- src/i18n/messages/en.json | 27 +++++++++++++++++++- src/i18n/messages/es.json | 47 ++++++++++++++++++++++++++-------- src/i18n/messages/fr.json | 47 ++++++++++++++++++++++++++-------- src/i18n/messages/ja.json | 47 ++++++++++++++++++++++++++-------- src/i18n/messages/ko.json | 47 ++++++++++++++++++++++++++-------- src/i18n/messages/pt.json | 47 ++++++++++++++++++++++++++-------- src/i18n/messages/zh-CN.json | 29 +++++++++++++++++++-- src/i18n/messages/zh-TW.json | 49 +++++++++++++++++++++++++++--------- 10 files changed, 342 insertions(+), 92 deletions(-) diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index ee58d27..a217233 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -97,19 +97,44 @@ } }, "AppearanceSettings": { - "sectionTitle": "مظهر السمة", - "sectionDescription": "اختر الفاتح أو الداكن أو اتباع النظام. يتم حفظ الإعدادات تلقائيًا.", - "themeMode": "وضع السمة", - "placeholder": "اختر وضع السمة", - "system": "اتباع النظام", - "light": "فاتح", - "dark": "داكن", - "currentTheme": "السمة الفعالة الحالية: {theme}", + "sectionTitle": "Theme Appearance", + "sectionDescription": "Choose light, dark, or follow system. Settings are saved automatically.", + "themeMode": "Theme mode", + "placeholder": "Select theme mode", + "system": "Follow system", + "light": "Light", + "dark": "Dark", + "currentTheme": "Current effective theme: {theme}", "resolvedTheme": { - "light": "فاتح", - "dark": "داكن", + "light": "Light", + "dark": "Dark", "unknown": "--" - } + }, + "accentColor": "Accent color", + "fontSection": "Fonts", + "fontSectionDesc": "Customize typefaces and sizes for the interface and code.", + "uiFont": "UI font", + "codeFont": "Code font", + "uiFontSize": "UI font size", + "codeFontSize": "Code font size", + "customFontPlaceholder": "Enter font family...", + "editorSection": "Editor", + "editorSectionDesc": "Configure code syntax highlighting theme.", + "codeThemeLight": "Light theme", + "codeThemeDark": "Dark theme", + "terminalSection": "Terminal", + "terminalSectionDesc": "Choose a color scheme for the integrated terminal.", + "terminalScheme": "Color scheme", + "accessibilitySection": "Accessibility", + "accessibilitySectionDesc": "Adjust spacing and motion preferences.", + "density": "Interface density", + "densityCompact": "Compact", + "densityDefault": "Default", + "densitySpacious": "Spacious", + "reduceMotion": "Reduce motion", + "reduceMotionSystem": "Follow system", + "reduceMotionOn": "On", + "reduceMotionOff": "Off" }, "SystemSettings": { "loading": "جارٍ التحميل...", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 2820b0d..d561e84 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -97,19 +97,44 @@ } }, "AppearanceSettings": { - "sectionTitle": "Design", - "sectionDescription": "Wähle hell, dunkel oder Systemvorgabe. Einstellungen werden automatisch gespeichert.", - "themeMode": "Designmodus", - "placeholder": "Designmodus auswählen", - "system": "Systemvorgabe", - "light": "Hell", - "dark": "Dunkel", - "currentTheme": "Aktuell wirksames Design: {theme}", + "sectionTitle": "Theme Appearance", + "sectionDescription": "Choose light, dark, or follow system. Settings are saved automatically.", + "themeMode": "Theme mode", + "placeholder": "Select theme mode", + "system": "Follow system", + "light": "Light", + "dark": "Dark", + "currentTheme": "Current effective theme: {theme}", "resolvedTheme": { - "light": "Hell", - "dark": "Dunkel", + "light": "Light", + "dark": "Dark", "unknown": "--" - } + }, + "accentColor": "Accent color", + "fontSection": "Fonts", + "fontSectionDesc": "Customize typefaces and sizes for the interface and code.", + "uiFont": "UI font", + "codeFont": "Code font", + "uiFontSize": "UI font size", + "codeFontSize": "Code font size", + "customFontPlaceholder": "Enter font family...", + "editorSection": "Editor", + "editorSectionDesc": "Configure code syntax highlighting theme.", + "codeThemeLight": "Light theme", + "codeThemeDark": "Dark theme", + "terminalSection": "Terminal", + "terminalSectionDesc": "Choose a color scheme for the integrated terminal.", + "terminalScheme": "Color scheme", + "accessibilitySection": "Accessibility", + "accessibilitySectionDesc": "Adjust spacing and motion preferences.", + "density": "Interface density", + "densityCompact": "Compact", + "densityDefault": "Default", + "densitySpacious": "Spacious", + "reduceMotion": "Reduce motion", + "reduceMotionSystem": "Follow system", + "reduceMotionOn": "On", + "reduceMotionOff": "Off" }, "SystemSettings": { "loading": "Wird geladen...", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 2d4de2c..5fcf4b0 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -109,7 +109,32 @@ "light": "Light", "dark": "Dark", "unknown": "--" - } + }, + "accentColor": "Accent color", + "fontSection": "Fonts", + "fontSectionDesc": "Customize typefaces and sizes for the interface and code.", + "uiFont": "UI font", + "codeFont": "Code font", + "uiFontSize": "UI font size", + "codeFontSize": "Code font size", + "customFontPlaceholder": "Enter font family...", + "editorSection": "Editor", + "editorSectionDesc": "Configure code syntax highlighting theme.", + "codeThemeLight": "Light theme", + "codeThemeDark": "Dark theme", + "terminalSection": "Terminal", + "terminalSectionDesc": "Choose a color scheme for the integrated terminal.", + "terminalScheme": "Color scheme", + "accessibilitySection": "Accessibility", + "accessibilitySectionDesc": "Adjust spacing and motion preferences.", + "density": "Interface density", + "densityCompact": "Compact", + "densityDefault": "Default", + "densitySpacious": "Spacious", + "reduceMotion": "Reduce motion", + "reduceMotionSystem": "Follow system", + "reduceMotionOn": "On", + "reduceMotionOff": "Off" }, "SystemSettings": { "loading": "Loading...", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index eef60ef..436e1a5 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -97,19 +97,44 @@ } }, "AppearanceSettings": { - "sectionTitle": "Apariencia del tema", - "sectionDescription": "Elige claro, oscuro o seguir el sistema. La configuración se guarda automáticamente.", - "themeMode": "Modo de tema", - "placeholder": "Selecciona el modo de tema", - "system": "Seguir el sistema", - "light": "Claro", - "dark": "Oscuro", - "currentTheme": "Tema efectivo actual: {theme}", + "sectionTitle": "Theme Appearance", + "sectionDescription": "Choose light, dark, or follow system. Settings are saved automatically.", + "themeMode": "Theme mode", + "placeholder": "Select theme mode", + "system": "Follow system", + "light": "Light", + "dark": "Dark", + "currentTheme": "Current effective theme: {theme}", "resolvedTheme": { - "light": "Claro", - "dark": "Oscuro", + "light": "Light", + "dark": "Dark", "unknown": "--" - } + }, + "accentColor": "Accent color", + "fontSection": "Fonts", + "fontSectionDesc": "Customize typefaces and sizes for the interface and code.", + "uiFont": "UI font", + "codeFont": "Code font", + "uiFontSize": "UI font size", + "codeFontSize": "Code font size", + "customFontPlaceholder": "Enter font family...", + "editorSection": "Editor", + "editorSectionDesc": "Configure code syntax highlighting theme.", + "codeThemeLight": "Light theme", + "codeThemeDark": "Dark theme", + "terminalSection": "Terminal", + "terminalSectionDesc": "Choose a color scheme for the integrated terminal.", + "terminalScheme": "Color scheme", + "accessibilitySection": "Accessibility", + "accessibilitySectionDesc": "Adjust spacing and motion preferences.", + "density": "Interface density", + "densityCompact": "Compact", + "densityDefault": "Default", + "densitySpacious": "Spacious", + "reduceMotion": "Reduce motion", + "reduceMotionSystem": "Follow system", + "reduceMotionOn": "On", + "reduceMotionOff": "Off" }, "SystemSettings": { "loading": "Cargando...", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 7dd4719..c6ab22f 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -97,19 +97,44 @@ } }, "AppearanceSettings": { - "sectionTitle": "Apparence du thème", - "sectionDescription": "Choisissez clair, sombre ou suivre le système. Les paramètres sont enregistrés automatiquement.", - "themeMode": "Mode du thème", - "placeholder": "Sélectionner le mode du thème", - "system": "Suivre le système", - "light": "Clair", - "dark": "Sombre", - "currentTheme": "Thème effectif actuel : {theme}", + "sectionTitle": "Theme Appearance", + "sectionDescription": "Choose light, dark, or follow system. Settings are saved automatically.", + "themeMode": "Theme mode", + "placeholder": "Select theme mode", + "system": "Follow system", + "light": "Light", + "dark": "Dark", + "currentTheme": "Current effective theme: {theme}", "resolvedTheme": { - "light": "Clair", - "dark": "Sombre", + "light": "Light", + "dark": "Dark", "unknown": "--" - } + }, + "accentColor": "Accent color", + "fontSection": "Fonts", + "fontSectionDesc": "Customize typefaces and sizes for the interface and code.", + "uiFont": "UI font", + "codeFont": "Code font", + "uiFontSize": "UI font size", + "codeFontSize": "Code font size", + "customFontPlaceholder": "Enter font family...", + "editorSection": "Editor", + "editorSectionDesc": "Configure code syntax highlighting theme.", + "codeThemeLight": "Light theme", + "codeThemeDark": "Dark theme", + "terminalSection": "Terminal", + "terminalSectionDesc": "Choose a color scheme for the integrated terminal.", + "terminalScheme": "Color scheme", + "accessibilitySection": "Accessibility", + "accessibilitySectionDesc": "Adjust spacing and motion preferences.", + "density": "Interface density", + "densityCompact": "Compact", + "densityDefault": "Default", + "densitySpacious": "Spacious", + "reduceMotion": "Reduce motion", + "reduceMotionSystem": "Follow system", + "reduceMotionOn": "On", + "reduceMotionOff": "Off" }, "SystemSettings": { "loading": "Chargement...", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index ad65657..93cfe4f 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -97,19 +97,44 @@ } }, "AppearanceSettings": { - "sectionTitle": "テーマ外観", - "sectionDescription": "ライト、ダーク、またはシステム追従を選択できます。設定は自動保存されます。", - "themeMode": "テーマモード", - "placeholder": "テーマモードを選択", - "system": "システムに従う", - "light": "ライト", - "dark": "ダーク", - "currentTheme": "現在の有効テーマ: {theme}", + "sectionTitle": "Theme Appearance", + "sectionDescription": "Choose light, dark, or follow system. Settings are saved automatically.", + "themeMode": "Theme mode", + "placeholder": "Select theme mode", + "system": "Follow system", + "light": "Light", + "dark": "Dark", + "currentTheme": "Current effective theme: {theme}", "resolvedTheme": { - "light": "ライト", - "dark": "ダーク", + "light": "Light", + "dark": "Dark", "unknown": "--" - } + }, + "accentColor": "Accent color", + "fontSection": "Fonts", + "fontSectionDesc": "Customize typefaces and sizes for the interface and code.", + "uiFont": "UI font", + "codeFont": "Code font", + "uiFontSize": "UI font size", + "codeFontSize": "Code font size", + "customFontPlaceholder": "Enter font family...", + "editorSection": "Editor", + "editorSectionDesc": "Configure code syntax highlighting theme.", + "codeThemeLight": "Light theme", + "codeThemeDark": "Dark theme", + "terminalSection": "Terminal", + "terminalSectionDesc": "Choose a color scheme for the integrated terminal.", + "terminalScheme": "Color scheme", + "accessibilitySection": "Accessibility", + "accessibilitySectionDesc": "Adjust spacing and motion preferences.", + "density": "Interface density", + "densityCompact": "Compact", + "densityDefault": "Default", + "densitySpacious": "Spacious", + "reduceMotion": "Reduce motion", + "reduceMotionSystem": "Follow system", + "reduceMotionOn": "On", + "reduceMotionOff": "Off" }, "SystemSettings": { "loading": "読み込み中...", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 2d1f239..e0ae5d6 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -97,19 +97,44 @@ } }, "AppearanceSettings": { - "sectionTitle": "테마 모양", - "sectionDescription": "라이트, 다크 또는 시스템 설정을 선택할 수 있습니다. 설정은 자동으로 저장됩니다.", - "themeMode": "테마 모드", - "placeholder": "테마 모드 선택", - "system": "시스템 설정 따름", - "light": "라이트", - "dark": "다크", - "currentTheme": "현재 적용된 테마: {theme}", + "sectionTitle": "Theme Appearance", + "sectionDescription": "Choose light, dark, or follow system. Settings are saved automatically.", + "themeMode": "Theme mode", + "placeholder": "Select theme mode", + "system": "Follow system", + "light": "Light", + "dark": "Dark", + "currentTheme": "Current effective theme: {theme}", "resolvedTheme": { - "light": "라이트", - "dark": "다크", + "light": "Light", + "dark": "Dark", "unknown": "--" - } + }, + "accentColor": "Accent color", + "fontSection": "Fonts", + "fontSectionDesc": "Customize typefaces and sizes for the interface and code.", + "uiFont": "UI font", + "codeFont": "Code font", + "uiFontSize": "UI font size", + "codeFontSize": "Code font size", + "customFontPlaceholder": "Enter font family...", + "editorSection": "Editor", + "editorSectionDesc": "Configure code syntax highlighting theme.", + "codeThemeLight": "Light theme", + "codeThemeDark": "Dark theme", + "terminalSection": "Terminal", + "terminalSectionDesc": "Choose a color scheme for the integrated terminal.", + "terminalScheme": "Color scheme", + "accessibilitySection": "Accessibility", + "accessibilitySectionDesc": "Adjust spacing and motion preferences.", + "density": "Interface density", + "densityCompact": "Compact", + "densityDefault": "Default", + "densitySpacious": "Spacious", + "reduceMotion": "Reduce motion", + "reduceMotionSystem": "Follow system", + "reduceMotionOn": "On", + "reduceMotionOff": "Off" }, "SystemSettings": { "loading": "로딩 중...", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 343034c..43d1f4c 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -97,19 +97,44 @@ } }, "AppearanceSettings": { - "sectionTitle": "Aparência do tema", - "sectionDescription": "Escolha claro, escuro ou seguir o sistema. As configurações são salvas automaticamente.", - "themeMode": "Modo de tema", - "placeholder": "Selecionar modo de tema", - "system": "Seguir o sistema", - "light": "Claro", - "dark": "Escuro", - "currentTheme": "Tema efetivo atual: {theme}", + "sectionTitle": "Theme Appearance", + "sectionDescription": "Choose light, dark, or follow system. Settings are saved automatically.", + "themeMode": "Theme mode", + "placeholder": "Select theme mode", + "system": "Follow system", + "light": "Light", + "dark": "Dark", + "currentTheme": "Current effective theme: {theme}", "resolvedTheme": { - "light": "Claro", - "dark": "Escuro", + "light": "Light", + "dark": "Dark", "unknown": "--" - } + }, + "accentColor": "Accent color", + "fontSection": "Fonts", + "fontSectionDesc": "Customize typefaces and sizes for the interface and code.", + "uiFont": "UI font", + "codeFont": "Code font", + "uiFontSize": "UI font size", + "codeFontSize": "Code font size", + "customFontPlaceholder": "Enter font family...", + "editorSection": "Editor", + "editorSectionDesc": "Configure code syntax highlighting theme.", + "codeThemeLight": "Light theme", + "codeThemeDark": "Dark theme", + "terminalSection": "Terminal", + "terminalSectionDesc": "Choose a color scheme for the integrated terminal.", + "terminalScheme": "Color scheme", + "accessibilitySection": "Accessibility", + "accessibilitySectionDesc": "Adjust spacing and motion preferences.", + "density": "Interface density", + "densityCompact": "Compact", + "densityDefault": "Default", + "densitySpacious": "Spacious", + "reduceMotion": "Reduce motion", + "reduceMotionSystem": "Follow system", + "reduceMotionOn": "On", + "reduceMotionOff": "Off" }, "SystemSettings": { "loading": "Carregando...", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 59d7b48..b04aba7 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -108,8 +108,33 @@ "resolvedTheme": { "light": "浅色", "dark": "深色", - "unknown": "未知" - } + "unknown": "--" + }, + "accentColor": "强调色", + "fontSection": "字体", + "fontSectionDesc": "自定义界面和代码的字体及大小。", + "uiFont": "界面字体", + "codeFont": "代码字体", + "uiFontSize": "界面字号", + "codeFontSize": "代码字号", + "customFontPlaceholder": "输入字体名称...", + "editorSection": "编辑器", + "editorSectionDesc": "配置代码语法高亮主题。", + "codeThemeLight": "浅色主题", + "codeThemeDark": "深色主题", + "terminalSection": "终端", + "terminalSectionDesc": "选择内置终端的配色方案。", + "terminalScheme": "配色方案", + "accessibilitySection": "无障碍", + "accessibilitySectionDesc": "调整间距和动画偏好。", + "density": "界面密度", + "densityCompact": "紧凑", + "densityDefault": "默认", + "densitySpacious": "宽松", + "reduceMotion": "减少动画", + "reduceMotionSystem": "跟随系统", + "reduceMotionOn": "开启", + "reduceMotionOff": "关闭" }, "SystemSettings": { "loading": "加载中...", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 794746d..d196365 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -97,19 +97,44 @@ } }, "AppearanceSettings": { - "sectionTitle": "主題外觀", - "sectionDescription": "選擇淺色、深色或跟隨系統主題,設定會自動儲存。", - "themeMode": "主題模式", - "placeholder": "請選擇主題模式", - "system": "跟隨系統", - "light": "淺色", - "dark": "深色", - "currentTheme": "目前生效主題:{theme}", + "sectionTitle": "Theme Appearance", + "sectionDescription": "Choose light, dark, or follow system. Settings are saved automatically.", + "themeMode": "Theme mode", + "placeholder": "Select theme mode", + "system": "Follow system", + "light": "Light", + "dark": "Dark", + "currentTheme": "Current effective theme: {theme}", "resolvedTheme": { - "light": "淺色", - "dark": "深色", - "unknown": "未知" - } + "light": "Light", + "dark": "Dark", + "unknown": "--" + }, + "accentColor": "Accent color", + "fontSection": "Fonts", + "fontSectionDesc": "Customize typefaces and sizes for the interface and code.", + "uiFont": "UI font", + "codeFont": "Code font", + "uiFontSize": "UI font size", + "codeFontSize": "Code font size", + "customFontPlaceholder": "Enter font family...", + "editorSection": "Editor", + "editorSectionDesc": "Configure code syntax highlighting theme.", + "codeThemeLight": "Light theme", + "codeThemeDark": "Dark theme", + "terminalSection": "Terminal", + "terminalSectionDesc": "Choose a color scheme for the integrated terminal.", + "terminalScheme": "Color scheme", + "accessibilitySection": "Accessibility", + "accessibilitySectionDesc": "Adjust spacing and motion preferences.", + "density": "Interface density", + "densityCompact": "Compact", + "densityDefault": "Default", + "densitySpacious": "Spacious", + "reduceMotion": "Reduce motion", + "reduceMotionSystem": "Follow system", + "reduceMotionOn": "On", + "reduceMotionOff": "Off" }, "SystemSettings": { "loading": "載入中...", From fa913e29e4f4c7bb8456057b662041ea61467cc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=86=8A=E5=AD=9D=E5=85=B5?= <1591788658@qq.com> Date: Thu, 2 Apr 2026 18:41:32 -0700 Subject: [PATCH 04/11] feat: rewrite appearance settings UI with 5 section cards Replaces the minimal theme-only selector with a full settings panel: Theme (mode + accent color), Fonts (UI/code font + size), Editor (code highlight themes), Terminal (color scheme), and Accessibility (density + reduce motion). Consumes useAppearance hook and constants from Tasks 1-3. Co-Authored-By: Claude Sonnet 4.6 --- .../settings/appearance-settings.tsx | 384 +++++++++++++++++- 1 file changed, 381 insertions(+), 3 deletions(-) diff --git a/src/components/settings/appearance-settings.tsx b/src/components/settings/appearance-settings.tsx index 22c18e1..0b847a9 100644 --- a/src/components/settings/appearance-settings.tsx +++ b/src/components/settings/appearance-settings.tsx @@ -1,8 +1,37 @@ "use client" -import { Monitor, Moon, Sun } from "lucide-react" +import { + Check, + Monitor, + Moon, + Minus, + Plus, + Sun, + Type, + Palette, + Terminal, + Accessibility, +} from "lucide-react" import { useTranslations } from "next-intl" import { useTheme } from "next-themes" +import { useState } from "react" +import { cn } from "@/lib/utils" +import { useAppearance } from "@/lib/appearance/use-appearance" +import { + ACCENT_PRESETS, + UI_FONT_PRESETS, + CODE_FONT_PRESETS, + CODE_THEME_LIGHT_OPTIONS, + CODE_THEME_DARK_OPTIONS, + TERMINAL_SCHEME_OPTIONS, +} from "@/lib/appearance/constants" +import type { + AccentColor, + CodeThemeLight, + CodeThemeDark, + TerminalScheme, + InterfaceDensity, +} from "@/lib/appearance/types" import { Select, SelectContent, @@ -10,12 +39,51 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" +import { Input } from "@/components/ui/input" type ThemeMode = "system" | "light" | "dark" +function FontSizeInput({ + value, + onChange, + min, + max, +}: { + value: number + onChange: (v: number) => void + min: number + max: number +}) { + return ( +
+ + {value}px + +
+ ) +} + export function AppearanceSettings() { const t = useTranslations("AppearanceSettings") const { theme, resolvedTheme, setTheme } = useTheme() + const { settings, update } = useAppearance() + const [customUiFont, setCustomUiFont] = useState("") + const [customCodeFont, setCustomCodeFont] = useState("") + const resolvedThemeLabel = resolvedTheme === "dark" ? t("resolvedTheme.dark") @@ -23,26 +91,34 @@ export function AppearanceSettings() { ? t("resolvedTheme.light") : t("resolvedTheme.unknown") + const isCustomUiFont = !UI_FONT_PRESETS.some( + (p) => p.value === settings.uiFont && p.value !== "custom", + ) + const isCustomCodeFont = !CODE_FONT_PRESETS.some( + (p) => p.value === settings.codeFont && p.value !== "custom", + ) + return (
+ {/* Section 1: Theme */}

{t("sectionTitle")}

-

{t("sectionDescription")}

+ {/* Theme mode */}
{ + if (v === "custom") { + setCustomUiFont(settings.uiFont) + } else { + update("uiFont", v) + } + }} + > + + + + + {UI_FONT_PRESETS.map((p) => ( + + {p.label} + + ))} + + + {isCustomUiFont && ( + { + setCustomUiFont(e.target.value) + if (e.target.value.trim()) { + update("uiFont", e.target.value.trim()) + } + }} + /> + )} +
+ + {/* Code Font */} +
+ + + {isCustomCodeFont && ( + { + setCustomCodeFont(e.target.value) + if (e.target.value.trim()) { + update("codeFont", e.target.value.trim()) + } + }} + /> + )} +
+ + {/* Font Sizes */} +
+
+ + update("uiFontSize", v)} + /> +
+
+ + update("codeFontSize", v)} + /> +
+
+
+ + {/* Section 3: Editor */} +
+
+ +

{t("editorSection")}

+
+

+ {t("editorSectionDesc")} +

+ +
+
+ + +
+
+ + +
+
+
+ + {/* Section 4: Terminal */} +
+
+ +

{t("terminalSection")}

+
+

+ {t("terminalSectionDesc")} +

+ +
+ + +
+
+ + {/* Section 5: Accessibility */} +
+
+ +

+ {t("accessibilitySection")} +

+
+

+ {t("accessibilitySectionDesc")} +

+ + {/* Density */} +
+ + +
+ + {/* Reduce Motion */} +
+ + +
From ffff910c08b2c4f77bb3ac7e4a86d2cbda182df9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=86=8A=E5=AD=9D=E5=85=B5?= <1591788658@qq.com> Date: Thu, 2 Apr 2026 18:42:58 -0700 Subject: [PATCH 05/11] feat(appearance): apply font and density CSS variables globally Co-Authored-By: Claude Sonnet 4.6 --- src/app/globals.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/app/globals.css b/src/app/globals.css index 3c846ac..3d1d552 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -174,9 +174,18 @@ } body { @apply bg-background text-foreground; + font-size: var(--font-size-base); } } +code, +pre, +.font-mono, +[data-streamdown="code-block-body"] { + font-family: var(--font-code); + font-size: var(--font-size-code); +} + /* Streamdown code blocks: dark mode via shiki dual-theme CSS variables */ .dark [data-streamdown="code-block-body"] { background-color: var(--shiki-dark-bg, var(--sdm-bg, transparent)) !important; From ee3a3717e82a74df6eb18cbfcb22f162d06bdc4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=86=8A=E5=AD=9D=E5=85=B5?= <1591788658@qq.com> Date: Thu, 2 Apr 2026 18:43:51 -0700 Subject: [PATCH 06/11] feat(appearance): integrate terminal color scheme with appearance settings Replace hardcoded DARK_THEME/LIGHT_THEME constants and isDarkMode() with dynamic lookup via TERMINAL_SCHEMES[scheme] driven by the appearance context, so font size, font family, and color scheme all react to user settings changes. Co-Authored-By: Claude Sonnet 4.6 --- src/components/terminal/terminal-view.tsx | 96 +++++++++-------------- 1 file changed, 37 insertions(+), 59 deletions(-) diff --git a/src/components/terminal/terminal-view.tsx b/src/components/terminal/terminal-view.tsx index d5f9bfd..71106c6 100644 --- a/src/components/terminal/terminal-view.tsx +++ b/src/components/terminal/terminal-view.tsx @@ -10,58 +10,9 @@ import { } from "@/lib/api" import type { TerminalEvent } from "@/lib/types" import type { ITheme } from "@xterm/xterm" - -const DARK_THEME: ITheme = { - background: "#1a1a1a", - foreground: "#e0e0e0", - cursor: "#e0e0e0", - cursorAccent: "#1a1a1a", - selectionBackground: "#444444", - black: "#1a1a1a", - red: "#f87171", - green: "#4ade80", - yellow: "#facc15", - blue: "#60a5fa", - magenta: "#c084fc", - cyan: "#22d3ee", - white: "#e0e0e0", - brightBlack: "#737373", - brightRed: "#fca5a5", - brightGreen: "#86efac", - brightYellow: "#fde68a", - brightBlue: "#93c5fd", - brightMagenta: "#d8b4fe", - brightCyan: "#67e8f9", - brightWhite: "#ffffff", -} - -const LIGHT_THEME: ITheme = { - background: "#ffffff", - foreground: "#1a1a1a", - cursor: "#1a1a1a", - cursorAccent: "#ffffff", - selectionBackground: "#b4d5fe", - black: "#1a1a1a", - red: "#dc2626", - green: "#16a34a", - yellow: "#ca8a04", - blue: "#2563eb", - magenta: "#9333ea", - cyan: "#0891b2", - white: "#e5e5e5", - brightBlack: "#a3a3a3", - brightRed: "#ef4444", - brightGreen: "#22c55e", - brightYellow: "#eab308", - brightBlue: "#3b82f6", - brightMagenta: "#a855f7", - brightCyan: "#06b6d4", - brightWhite: "#ffffff", -} - -function isDarkMode() { - return document.documentElement.classList.contains("dark") -} +import { useAppearance } from "@/lib/appearance/use-appearance" +import { TERMINAL_SCHEMES } from "@/lib/appearance/constants" +import type { TerminalScheme } from "@/lib/appearance/types" function resolveBackgroundColor( element: HTMLElement | null | undefined @@ -77,11 +28,13 @@ function resolveBackgroundColor( return null } -function getTerminalTheme(container: HTMLDivElement | null): ITheme { - const baseTheme = isDarkMode() ? DARK_THEME : LIGHT_THEME +function getTerminalTheme( + container: HTMLDivElement | null, + scheme: TerminalScheme +): ITheme { + const baseTheme = TERMINAL_SCHEMES[scheme] const background = resolveBackgroundColor(container) if (!background) return baseTheme - return { ...baseTheme, background, @@ -106,6 +59,7 @@ export function TerminalView({ isVisible, onProcessExited, }: TerminalViewProps) { + const { settings } = useAppearance() const containerRef = useRef(null) const fitAddonRef = useRef<{ fit: () => void } | null>(null) const termRef = useRef<{ focus: () => void } | null>(null) @@ -113,6 +67,7 @@ export function TerminalView({ const isActiveRef = useRef(isActive) const isVisibleRef = useRef(isVisible) const onProcessExitedRef = useRef(onProcessExited) + const settingsRef = useRef(settings) const [loading, setLoading] = useState(true) useEffect(() => { @@ -124,6 +79,10 @@ export function TerminalView({ onProcessExitedRef.current = onProcessExited }, [onProcessExited]) + useEffect(() => { + settingsRef.current = settings + }, [settings]) + useEffect(() => { let cancelled = false let cleanup: (() => void) | undefined @@ -140,9 +99,12 @@ export function TerminalView({ const term = new Terminal({ cursorBlink: true, - fontSize: 13, - fontFamily: "Menlo, Monaco, 'Courier New', monospace", - theme: getTerminalTheme(containerRef.current), + fontSize: settingsRef.current.codeFontSize, + fontFamily: settingsRef.current.codeFont, + theme: getTerminalTheme( + containerRef.current, + settingsRef.current.terminalScheme + ), allowProposedApi: true, }) @@ -155,7 +117,10 @@ export function TerminalView({ // Watch class changes for theme switching const themeObserver = new MutationObserver(() => { - term.options.theme = getTerminalTheme(containerRef.current) + term.options.theme = getTerminalTheme( + containerRef.current, + settingsRef.current.terminalScheme + ) }) themeObserver.observe(document.documentElement, { attributes: true, @@ -292,6 +257,19 @@ export function TerminalView({ } }, [isActive, isVisible]) + // React to appearance settings changes on an existing terminal + useEffect(() => { + const term = termRef.current as any + if (!term?.options) return + term.options.fontSize = settings.codeFontSize + term.options.fontFamily = settings.codeFont + term.options.theme = getTerminalTheme( + containerRef.current, + settings.terminalScheme + ) + fitAddonRef.current?.fit() + }, [settings.codeFontSize, settings.codeFont, settings.terminalScheme]) + return (
Date: Thu, 2 Apr 2026 18:43:53 -0700 Subject: [PATCH 07/11] feat(appearance): integrate code highlight themes with appearance settings Replace hardcoded "github-light"/"github-dark" shiki themes with configurable values from the appearance context, updating caches to key by theme as well. Co-Authored-By: Claude Sonnet 4.6 --- src/components/ai-elements/code-block.tsx | 43 +++++++++++++++-------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/components/ai-elements/code-block.tsx b/src/components/ai-elements/code-block.tsx index 1837d0c..91bfc77 100644 --- a/src/components/ai-elements/code-block.tsx +++ b/src/components/ai-elements/code-block.tsx @@ -16,6 +16,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" +import { useAppearance } from "@/lib/appearance/use-appearance" import { cn } from "@/lib/utils" import { CheckIcon, CopyIcon } from "lucide-react" import { @@ -130,26 +131,34 @@ const tokensCache = new Map() // Subscribers for async token updates const subscribers = new Map void>>() -const getTokensCacheKey = (code: string, language: BundledLanguage) => { +const getTokensCacheKey = ( + code: string, + language: BundledLanguage, + lightTheme: string, + darkTheme: string +) => { const start = code.slice(0, 100) const end = code.length > 100 ? code.slice(-100) : "" - return `${language}:${code.length}:${start}:${end}` + return `${language}:${lightTheme}:${darkTheme}:${code.length}:${start}:${end}` } const getHighlighter = ( - language: BundledLanguage + language: BundledLanguage, + lightTheme: string, + darkTheme: string ): Promise> => { - const cached = highlighterCache.get(language) + const cacheKey = `${language}:${lightTheme}:${darkTheme}` + const cached = highlighterCache.get(cacheKey) if (cached) { return cached } const highlighterPromise = createHighlighter({ langs: [language], - themes: ["github-light", "github-dark"], + themes: [lightTheme as BundledTheme, darkTheme as BundledTheme], }) - highlighterCache.set(language, highlighterPromise) + highlighterCache.set(cacheKey, highlighterPromise) return highlighterPromise } @@ -173,10 +182,12 @@ const createRawTokens = (code: string): TokenizedCode => ({ export const highlightCode = ( code: string, language: BundledLanguage, + lightTheme: string, + darkTheme: string, // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks) callback?: (result: TokenizedCode) => void ): TokenizedCode | null => { - const tokensCacheKey = getTokensCacheKey(code, language) + const tokensCacheKey = getTokensCacheKey(code, language, lightTheme, darkTheme) // Return cached result if available const cached = tokensCache.get(tokensCacheKey) @@ -193,7 +204,7 @@ export const highlightCode = ( } // Start highlighting in background - fire-and-forget async pattern - getHighlighter(language) + getHighlighter(language, lightTheme, darkTheme) // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then) .then((highlighter) => { const availableLangs = highlighter.getLoadedLanguages() @@ -202,8 +213,8 @@ export const highlightCode = ( const result = highlighter.codeToTokens(code, { lang: langToUse, themes: { - dark: "github-dark", - light: "github-light", + dark: darkTheme as BundledTheme, + light: lightTheme as BundledTheme, }, }) @@ -383,13 +394,17 @@ export const CodeBlockContent = ({ language: BundledLanguage showLineNumbers?: boolean }) => { + const { settings } = useAppearance() + const lightTheme = settings.codeThemeLight + const darkTheme = settings.codeThemeDark + // Memoized raw tokens for immediate display const rawTokens = useMemo(() => createRawTokens(code), [code]) // Synchronous cached-or-raw value, recomputed when code/language changes const syncTokenized = useMemo( - () => highlightCode(code, language) ?? rawTokens, - [code, language, rawTokens] + () => highlightCode(code, language, lightTheme, darkTheme) ?? rawTokens, + [code, language, lightTheme, darkTheme, rawTokens] ) // Async highlighted result, tagged with its source code/language @@ -403,7 +418,7 @@ export const CodeBlockContent = ({ let cancelled = false // Subscribe to async highlighting result - highlightCode(code, language, (result) => { + highlightCode(code, language, lightTheme, darkTheme, (result) => { if (!cancelled) { setAsyncState({ code, language, tokenized: result }) } @@ -412,7 +427,7 @@ export const CodeBlockContent = ({ return () => { cancelled = true } - }, [code, language]) + }, [code, language, lightTheme, darkTheme]) // Use async result only if it matches current code/language const tokenized = From 5e32b878d0295dabf8690e3238bbe207ce17a1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=86=8A=E5=AD=9D=E5=85=B5?= <1591788658@qq.com> Date: Thu, 2 Apr 2026 18:50:24 -0700 Subject: [PATCH 08/11] fix(appearance): lint and prettier fixes - Fix react-hooks/set-state-in-effect by using initializer function - Fix @typescript-eslint/no-explicit-any with proper type assertion - Fix prettier formatting issues Co-Authored-By: Claude Opus 4.6 --- src/components/ai-elements/code-block.tsx | 7 ++++++- src/components/settings/appearance-settings.tsx | 10 ++++------ src/components/terminal/terminal-view.tsx | 4 +++- src/lib/appearance/use-appearance.ts | 8 +------- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/components/ai-elements/code-block.tsx b/src/components/ai-elements/code-block.tsx index 91bfc77..3c622d7 100644 --- a/src/components/ai-elements/code-block.tsx +++ b/src/components/ai-elements/code-block.tsx @@ -187,7 +187,12 @@ export const highlightCode = ( // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks) callback?: (result: TokenizedCode) => void ): TokenizedCode | null => { - const tokensCacheKey = getTokensCacheKey(code, language, lightTheme, darkTheme) + const tokensCacheKey = getTokensCacheKey( + code, + language, + lightTheme, + darkTheme + ) // Return cached result if available const cached = tokensCache.get(tokensCacheKey) diff --git a/src/components/settings/appearance-settings.tsx b/src/components/settings/appearance-settings.tsx index 0b847a9..bb59de0 100644 --- a/src/components/settings/appearance-settings.tsx +++ b/src/components/settings/appearance-settings.tsx @@ -92,10 +92,10 @@ export function AppearanceSettings() { : t("resolvedTheme.unknown") const isCustomUiFont = !UI_FONT_PRESETS.some( - (p) => p.value === settings.uiFont && p.value !== "custom", + (p) => p.value === settings.uiFont && p.value !== "custom" ) const isCustomCodeFont = !CODE_FONT_PRESETS.some( - (p) => p.value === settings.codeFont && p.value !== "custom", + (p) => p.value === settings.codeFont && p.value !== "custom" ) return ( @@ -167,7 +167,7 @@ export function AppearanceSettings() { "flex h-7 w-7 items-center justify-center rounded-full border-2 transition-transform hover:scale-110", settings.accentColor === color ? "border-foreground" - : "border-transparent", + : "border-transparent" )} onClick={() => update("accentColor", color)} > @@ -423,9 +423,7 @@ export function AppearanceSettings() { {t("densityCompact")} {t("densityDefault")} - - {t("densitySpacious")} - + {t("densitySpacious")}
diff --git a/src/components/terminal/terminal-view.tsx b/src/components/terminal/terminal-view.tsx index 71106c6..5091e68 100644 --- a/src/components/terminal/terminal-view.tsx +++ b/src/components/terminal/terminal-view.tsx @@ -259,7 +259,9 @@ export function TerminalView({ // React to appearance settings changes on an existing terminal useEffect(() => { - const term = termRef.current as any + const term = termRef.current as { + options: { fontSize: number; fontFamily: string; theme: ITheme } + } | null if (!term?.options) return term.options.fontSize = settings.codeFontSize term.options.fontFamily = settings.codeFont diff --git a/src/lib/appearance/use-appearance.ts b/src/lib/appearance/use-appearance.ts index 52a530d..2207fb0 100644 --- a/src/lib/appearance/use-appearance.ts +++ b/src/lib/appearance/use-appearance.ts @@ -82,13 +82,7 @@ function applyToDOM(settings: AppearanceSettings) { } export function AppearanceProvider({ children }: { children: ReactNode }) { - const [settings, setSettings] = - useState(DEFAULT_APPEARANCE) - - // Load from localStorage on mount - useEffect(() => { - setSettings(loadSettings()) - }, []) + const [settings, setSettings] = useState(loadSettings) // Apply CSS variables whenever settings or theme class changes useEffect(() => { From 7e69ccbce29cc42d510421530c6da5204b5e3c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=86=8A=E5=AD=9D=E5=85=B5?= <1591788658@qq.com> Date: Thu, 2 Apr 2026 18:55:34 -0700 Subject: [PATCH 09/11] fix(appearance): fix UI font, font size, and density not taking effect - Root font-family now uses var(--font-sans) instead of hardcoded Inter - Root font-size now uses var(--font-size-base) instead of hardcoded 16px - Root line-height now uses var(--density-line-height) - Default uiFontSize changed to 16 to match previous root default - Removed custom font input option from font selects Co-Authored-By: Claude Opus 4.6 --- src/app/globals.css | 9 ++- .../settings/appearance-settings.tsx | 57 ++----------------- src/lib/appearance/constants.ts | 4 +- 3 files changed, 9 insertions(+), 61 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 3d1d552..a9c7278 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -6,9 +6,9 @@ @custom-variant dark (&:is(.dark *)); :root { - font-family: Inter, Avenir, Helvetica, Arial, sans-serif; - font-size: 16px; - line-height: 24px; + font-family: var(--font-sans); + font-size: var(--font-size-base); + line-height: var(--density-line-height); font-weight: 400; font-synthesis: none; @@ -49,7 +49,7 @@ --sidebar-border: oklch(0.922 0 0); --sidebar-ring: oklch(0.708 0 0); --font-code: 'JetBrains Mono', monospace; - --font-size-base: 14px; + --font-size-base: 16px; --font-size-code: 14px; --density-padding: 0.5rem; --density-gap: 0.5rem; @@ -174,7 +174,6 @@ } body { @apply bg-background text-foreground; - font-size: var(--font-size-base); } } diff --git a/src/components/settings/appearance-settings.tsx b/src/components/settings/appearance-settings.tsx index bb59de0..8b9a013 100644 --- a/src/components/settings/appearance-settings.tsx +++ b/src/components/settings/appearance-settings.tsx @@ -14,7 +14,6 @@ import { } from "lucide-react" import { useTranslations } from "next-intl" import { useTheme } from "next-themes" -import { useState } from "react" import { cn } from "@/lib/utils" import { useAppearance } from "@/lib/appearance/use-appearance" import { @@ -39,7 +38,6 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" -import { Input } from "@/components/ui/input" type ThemeMode = "system" | "light" | "dark" @@ -81,8 +79,6 @@ export function AppearanceSettings() { const t = useTranslations("AppearanceSettings") const { theme, resolvedTheme, setTheme } = useTheme() const { settings, update } = useAppearance() - const [customUiFont, setCustomUiFont] = useState("") - const [customCodeFont, setCustomCodeFont] = useState("") const resolvedThemeLabel = resolvedTheme === "dark" @@ -91,13 +87,6 @@ export function AppearanceSettings() { ? t("resolvedTheme.light") : t("resolvedTheme.unknown") - const isCustomUiFont = !UI_FONT_PRESETS.some( - (p) => p.value === settings.uiFont && p.value !== "custom" - ) - const isCustomCodeFont = !CODE_FONT_PRESETS.some( - (p) => p.value === settings.codeFont && p.value !== "custom" - ) - return (
@@ -201,14 +190,8 @@ export function AppearanceSettings() { {t("uiFont")} - {isCustomUiFont && ( - { - setCustomUiFont(e.target.value) - if (e.target.value.trim()) { - update("uiFont", e.target.value.trim()) - } - }} - /> - )}
{/* Code Font */} @@ -242,14 +212,8 @@ export function AppearanceSettings() { {t("codeFont")} - {isCustomCodeFont && ( - { - setCustomCodeFont(e.target.value) - if (e.target.value.trim()) { - update("codeFont", e.target.value.trim()) - } - }} - /> - )}
{/* Font Sizes */} diff --git a/src/lib/appearance/constants.ts b/src/lib/appearance/constants.ts index 203b0f2..cb58972 100644 --- a/src/lib/appearance/constants.ts +++ b/src/lib/appearance/constants.ts @@ -72,7 +72,6 @@ export const UI_FONT_PRESETS = [ value: "'JetBrains Mono', monospace", label: "JetBrains Mono", }, - { value: "custom", label: "Custom..." }, ] as const export const CODE_FONT_PRESETS = [ @@ -89,7 +88,6 @@ export const CODE_FONT_PRESETS = [ { value: "Menlo, monospace", label: "Menlo" }, { value: "Monaco, monospace", label: "Monaco" }, { value: "monospace", label: "monospace" }, - { value: "custom", label: "Custom..." }, ] as const export const DENSITY_VALUES: Record< @@ -340,7 +338,7 @@ export const DEFAULT_APPEARANCE: AppearanceSettings = { accentColor: "blue", uiFont: "'JetBrains Mono', monospace", codeFont: "'JetBrains Mono', monospace", - uiFontSize: 14, + uiFontSize: 16, codeFontSize: 14, codeThemeLight: "github-light", codeThemeDark: "github-dark", From f52f52e30a936ff7112fe2ad0dbcca68ecd2b89b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=86=8A=E5=AD=9D=E5=85=B5?= <1591788658@qq.com> Date: Fri, 3 Apr 2026 10:18:51 +0800 Subject: [PATCH 10/11] =?UTF-8?q?style:=20=E4=B8=BB=E9=A2=98=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- src/app/globals.css | 3 +- src/app/layout.tsx | 10 ++-- src/components/ai-elements/code-block.tsx | 25 +++++++--- .../settings/appearance-settings.tsx | 3 +- src/lib/appearance/constants.ts | 32 ++++++------ .../{use-appearance.ts => use-appearance.tsx} | 50 +++++++++---------- 7 files changed, 70 insertions(+), 56 deletions(-) rename src/lib/appearance/{use-appearance.ts => use-appearance.tsx} (80%) diff --git a/.gitignore b/.gitignore index 2632e0b..92473db 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,5 @@ public/vs *.sw? # Agent -.claude \ No newline at end of file +.claude +/docs/superpowers \ No newline at end of file diff --git a/src/app/globals.css b/src/app/globals.css index a9c7278..5cfbb4e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -6,6 +6,7 @@ @custom-variant dark (&:is(.dark *)); :root { + --font-sans: var(--font-jetbrains-mono, monospace); font-family: var(--font-sans); font-size: var(--font-size-base); line-height: var(--density-line-height); @@ -48,7 +49,7 @@ --sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-border: oklch(0.922 0 0); --sidebar-ring: oklch(0.708 0 0); - --font-code: 'JetBrains Mono', monospace; + --font-code: var(--font-jetbrains-mono, monospace); --font-size-base: 16px; --font-size-code: 14px; --density-padding: 0.5rem; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 0c01339..ea351e6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,6 @@ import type { Metadata } from "next" import "./globals.css" -import { JetBrains_Mono } from "next/font/google" +import { Inter, JetBrains_Mono } from "next/font/google" import { NextIntlClientProvider } from "next-intl" import { AppI18nProvider } from "@/components/i18n-provider" import { getMessagesForLocale } from "@/i18n/messages" @@ -9,9 +9,13 @@ import { ThemeProvider } from "@/components/theme-provider" import { toIntlLocale } from "@/lib/i18n" import { AppearanceProvider } from "@/lib/appearance/use-appearance" +const inter = Inter({ + subsets: ["latin"], + variable: "--font-inter", +}) const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"], - variable: "--font-sans", + variable: "--font-jetbrains-mono", }) export const metadata: Metadata = { @@ -31,7 +35,7 @@ export default async function RootLayout({ return ( diff --git a/src/components/ai-elements/code-block.tsx b/src/components/ai-elements/code-block.tsx index 3c622d7..7c377c2 100644 --- a/src/components/ai-elements/code-block.tsx +++ b/src/components/ai-elements/code-block.tsx @@ -125,7 +125,7 @@ const highlighterCache = new Map< Promise> >() -// Token cache +const MAX_TOKENS_CACHE = 200 const tokensCache = new Map() // Subscribers for async token updates @@ -229,8 +229,11 @@ export const highlightCode = ( tokens: result.tokens, } - // Cache the result tokensCache.set(tokensCacheKey, tokenized) + if (tokensCache.size > MAX_TOKENS_CACHE) { + const first = tokensCache.keys().next().value + if (first !== undefined) tokensCache.delete(first) + } // Notify all subscribers const subs = subscribers.get(tokensCacheKey) @@ -412,20 +415,26 @@ export const CodeBlockContent = ({ [code, language, lightTheme, darkTheme, rawTokens] ) - // Async highlighted result, tagged with its source code/language const [asyncState, setAsyncState] = useState<{ code: string language: string + lightTheme: string + darkTheme: string tokenized: TokenizedCode } | null>(null) useEffect(() => { let cancelled = false - // Subscribe to async highlighting result highlightCode(code, language, lightTheme, darkTheme, (result) => { if (!cancelled) { - setAsyncState({ code, language, tokenized: result }) + setAsyncState({ + code, + language, + lightTheme, + darkTheme, + tokenized: result, + }) } }) @@ -434,9 +443,11 @@ export const CodeBlockContent = ({ } }, [code, language, lightTheme, darkTheme]) - // Use async result only if it matches current code/language const tokenized = - asyncState?.code === code && asyncState?.language === language + asyncState?.code === code && + asyncState?.language === language && + asyncState?.lightTheme === lightTheme && + asyncState?.darkTheme === darkTheme ? asyncState.tokenized : syncTokenized diff --git a/src/components/settings/appearance-settings.tsx b/src/components/settings/appearance-settings.tsx index 8b9a013..4928c71 100644 --- a/src/components/settings/appearance-settings.tsx +++ b/src/components/settings/appearance-settings.tsx @@ -26,6 +26,7 @@ import { } from "@/lib/appearance/constants" import type { AccentColor, + AppearanceSettings, CodeThemeLight, CodeThemeDark, TerminalScheme, @@ -387,7 +388,7 @@ export function AppearanceSettings() {