= {
+ 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",
+ },
+}
+
+// ── UI font presets ────────────────────────────────────────────
+// Google Fonts use var() referencing the CSS variable set by next/font.
+// System fonts use raw font-family names.
+export const UI_FONT_PRESETS = [
+ { value: "system-ui, sans-serif", label: "System Default" },
+ { value: "var(--font-inter)", label: "Inter" },
+ { value: "var(--font-jetbrains-mono)", label: "JetBrains Mono" },
+] as const
+
+// ── Code font presets ──────────────────────────────────────────
+export const CODE_FONT_PRESETS = [
+ { value: "var(--font-jetbrains-mono)", label: "JetBrains Mono" },
+ { value: "'Fira Code', monospace", label: "Fira Code" },
+ { value: "'Source Code Pro', monospace", label: "Source Code Pro" },
+ { value: "Consolas, monospace", label: "Consolas (Windows)" },
+ { value: "Menlo, monospace", label: "Menlo (macOS)" },
+ { value: "'Courier New', monospace", label: "Courier New" },
+ { value: "monospace", label: "monospace" },
+] 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: "var(--font-jetbrains-mono)",
+ codeFont: "var(--font-jetbrains-mono)",
+ uiFontSize: 16,
+ 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"
+}
diff --git a/src/lib/appearance/use-appearance.tsx b/src/lib/appearance/use-appearance.tsx
new file mode 100644
index 0000000..70a92f4
--- /dev/null
+++ b/src/lib/appearance/use-appearance.tsx
@@ -0,0 +1,138 @@
+"use client"
+
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useState,
+} from "react"
+import type { ReactNode } from "react"
+import type { AppearanceSettings } from "./types"
+import {
+ ACCENT_PRESETS,
+ APPEARANCE_STORAGE_KEY,
+ DEFAULT_APPEARANCE,
+ DENSITY_VALUES,
+} from "./constants"
+
+interface AppearanceContextType {
+ settings: AppearanceSettings
+ update: (
+ key: K,
+ value: AppearanceSettings[K]
+ ) => void
+}
+
+const AppearanceContext = createContext({
+ settings: DEFAULT_APPEARANCE,
+ update: () => {},
+})
+
+const reducedMotionQuery =
+ typeof window !== "undefined"
+ ? window.matchMedia("(prefers-reduced-motion: reduce)")
+ : null
+
+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 applyAccentToDOM(settings: AppearanceSettings) {
+ const root = document.documentElement.style
+ const isDark = document.documentElement.classList.contains("dark")
+ const accent = ACCENT_PRESETS[settings.accentColor]
+ const v = isDark ? accent.dark : accent.light
+ root.setProperty("--primary", v)
+ root.setProperty("--ring", v)
+ root.setProperty("--sidebar-primary", v)
+}
+
+function applyToDOM(settings: AppearanceSettings) {
+ const root = document.documentElement.style
+
+ applyAccentToDOM(settings)
+
+ root.setProperty("--font-sans", settings.uiFont)
+ root.setProperty("--font-code", settings.codeFont)
+ root.setProperty("--font-size-base", `${settings.uiFontSize}px`)
+ root.setProperty("--font-size-code", `${settings.codeFontSize}px`)
+
+ 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)
+
+ const prefersReduced = reducedMotionQuery?.matches ?? false
+ 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(loadSettings)
+
+ useEffect(() => {
+ applyToDOM(settings)
+
+ // Only accent color depends on dark/light class
+ const observer = new MutationObserver(() => applyAccentToDOM(settings))
+ observer.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ["class"],
+ })
+ return () => observer.disconnect()
+ }, [settings])
+
+ 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) => {
+ if (prev[key] === value) return prev
+ const next = { ...prev, [key]: value }
+ saveSettings(next)
+ return next
+ })
+ },
+ []
+ )
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useAppearance() {
+ return useContext(AppearanceContext)
+}