diff --git a/apps/web/public/wallpapers/akane-bridge.jpg b/apps/web/public/wallpapers/akane-bridge.jpg
new file mode 100644
index 00000000000..1b66d2b985c
Binary files /dev/null and b/apps/web/public/wallpapers/akane-bridge.jpg differ
diff --git a/apps/web/public/wallpapers/akane-cliff.jpg b/apps/web/public/wallpapers/akane-cliff.jpg
new file mode 100644
index 00000000000..154b2c8cfe3
Binary files /dev/null and b/apps/web/public/wallpapers/akane-cliff.jpg differ
diff --git a/apps/web/public/wallpapers/akane-mist.jpg b/apps/web/public/wallpapers/akane-mist.jpg
new file mode 100644
index 00000000000..889e27680aa
Binary files /dev/null and b/apps/web/public/wallpapers/akane-mist.jpg differ
diff --git a/apps/web/public/wallpapers/akane-pagoda.jpg b/apps/web/public/wallpapers/akane-pagoda.jpg
new file mode 100644
index 00000000000..f263ebe3475
Binary files /dev/null and b/apps/web/public/wallpapers/akane-pagoda.jpg differ
diff --git a/apps/web/public/wallpapers/everforest.jpg b/apps/web/public/wallpapers/everforest.jpg
new file mode 100644
index 00000000000..6d75259dbb8
Binary files /dev/null and b/apps/web/public/wallpapers/everforest.jpg differ
diff --git a/apps/web/public/wallpapers/gruvbox-ferns.jpg b/apps/web/public/wallpapers/gruvbox-ferns.jpg
new file mode 100644
index 00000000000..fd318699368
Binary files /dev/null and b/apps/web/public/wallpapers/gruvbox-ferns.jpg differ
diff --git a/apps/web/public/wallpapers/pink-lakeside.png b/apps/web/public/wallpapers/pink-lakeside.png
new file mode 100644
index 00000000000..d80672889f5
Binary files /dev/null and b/apps/web/public/wallpapers/pink-lakeside.png differ
diff --git a/apps/web/public/wallpapers/ship-at-sea.jpg b/apps/web/public/wallpapers/ship-at-sea.jpg
new file mode 100644
index 00000000000..4375ee2bdd0
Binary files /dev/null and b/apps/web/public/wallpapers/ship-at-sea.jpg differ
diff --git a/apps/web/src/appSettings.ts b/apps/web/src/appSettings.ts
index 5ed218fb254..215913e9d39 100644
--- a/apps/web/src/appSettings.ts
+++ b/apps/web/src/appSettings.ts
@@ -24,6 +24,18 @@ const AppSettingsSchema = Schema.Struct({
customCodexModels: Schema.Array(Schema.String).pipe(
Schema.withConstructorDefault(() => Option.some([])),
),
+ colorThemeId: Schema.String.pipe(
+ Schema.withConstructorDefault(() => Option.some("")),
+ ),
+ backgroundImage: Schema.String.pipe(
+ Schema.withConstructorDefault(() => Option.some("")),
+ ),
+ backgroundOpacity: Schema.Number.pipe(
+ Schema.withConstructorDefault(() => Option.some(0.85)),
+ ),
+ backgroundBlur: Schema.Number.pipe(
+ Schema.withConstructorDefault(() => Option.some(0)),
+ ),
});
export type AppSettings = typeof AppSettingsSchema.Type;
export interface AppModelOption {
@@ -71,6 +83,8 @@ function normalizeAppSettings(settings: AppSettings): AppSettings {
return {
...settings,
customCodexModels: normalizeCustomModelSlugs(settings.customCodexModels, "codex"),
+ backgroundOpacity: Math.min(1.0, Math.max(0.1, settings.backgroundOpacity)),
+ backgroundBlur: Math.min(32, Math.max(0, settings.backgroundBlur)),
};
}
diff --git a/apps/web/src/components/BackgroundLayer.tsx b/apps/web/src/components/BackgroundLayer.tsx
new file mode 100644
index 00000000000..9d23463148c
--- /dev/null
+++ b/apps/web/src/components/BackgroundLayer.tsx
@@ -0,0 +1,68 @@
+import { useEffect } from "react";
+
+import { useAppSettings } from "../appSettings";
+import { getInteractiveOpacityRate, interactiveOpacity, nestedOpacity } from "../lib/colorThemes";
+import { resolveBackgroundImageUrl } from "../lib/wallpapers";
+
+export function BackgroundLayer() {
+ const { settings } = useAppSettings();
+ const { backgroundImage, backgroundOpacity, backgroundBlur, colorThemeId } = settings;
+
+ useEffect(() => {
+ const root = document.documentElement;
+ if (backgroundImage) {
+ root.setAttribute("data-has-wallpaper", "true");
+
+ const rate = getInteractiveOpacityRate(colorThemeId || undefined);
+ const overlayPct = `${Math.round(backgroundOpacity * 100)}%`;
+ const interactivePct = `${Math.round(interactiveOpacity(backgroundOpacity, rate * 0.4) * 100)}%`;
+ const nestedPct = `${Math.round(nestedOpacity(backgroundOpacity, rate) * 100)}%`;
+
+ root.style.setProperty("--wp-overlay", overlayPct);
+ root.style.setProperty("--wp-interactive", interactivePct);
+ root.style.setProperty("--wp-nested", nestedPct);
+ } else {
+ root.removeAttribute("data-has-wallpaper");
+ root.style.removeProperty("--wp-overlay");
+ root.style.removeProperty("--wp-interactive");
+ root.style.removeProperty("--wp-nested");
+ }
+ return () => {
+ root.removeAttribute("data-has-wallpaper");
+ root.style.removeProperty("--wp-overlay");
+ root.style.removeProperty("--wp-interactive");
+ root.style.removeProperty("--wp-nested");
+ };
+ }, [backgroundImage, backgroundOpacity, colorThemeId]);
+
+ if (!backgroundImage) return null;
+
+ const imageUrl = resolveBackgroundImageUrl(backgroundImage);
+ const overlayPercent = Math.round(backgroundOpacity * 100);
+
+ return (
+ <>
+ {/* Wallpaper image layer */}
+
0 ? `blur(${backgroundBlur}px)` : undefined,
+ }}
+ />
+ {/* Color overlay */}
+
+ >
+ );
+}
diff --git a/apps/web/src/hooks/useColorTheme.ts b/apps/web/src/hooks/useColorTheme.ts
new file mode 100644
index 00000000000..bd25a91f10f
--- /dev/null
+++ b/apps/web/src/hooks/useColorTheme.ts
@@ -0,0 +1,19 @@
+import { useEffect } from "react";
+
+import { useAppSettings } from "../appSettings";
+import { applyColorTheme } from "../lib/colorThemes";
+import { useTheme } from "./useTheme";
+
+export function useColorThemeEffect(): void {
+ const { resolvedTheme } = useTheme();
+ const { settings } = useAppSettings();
+ const { colorThemeId, backgroundImage } = settings;
+
+ useEffect(() => {
+ if (resolvedTheme === "dark" && colorThemeId) {
+ applyColorTheme(colorThemeId, !!backgroundImage);
+ } else {
+ applyColorTheme(null);
+ }
+ }, [resolvedTheme, colorThemeId, backgroundImage]);
+}
diff --git a/apps/web/src/index.css b/apps/web/src/index.css
index 22817916197..91a1fb53d82 100644
--- a/apps/web/src/index.css
+++ b/apps/web/src/index.css
@@ -120,6 +120,51 @@
}
}
+/*
+ * Wallpaper transparency system
+ *
+ * BackgroundLayer sets three CSS custom properties on :root:
+ * --wp-overlay raw opacity (e.g. "85%") — controls base overlay
+ * --wp-interactive interactiveOpacity result — sidebar, inputs (stays visible)
+ * --wp-nested nestedOpacity result — cards, headers (subtle tint)
+ *
+ * The overlay div (z-index: -10) uses --wp-overlay. UI panels reference the
+ * other two so interactive elements resist transparency changes.
+ */
+
+/* Fully transparent — let the overlay show through */
+:root[data-has-wallpaper] body,
+:root[data-has-wallpaper] .bg-background,
+:root[data-has-wallpaper] .bg-sidebar,
+:root[data-has-wallpaper] [data-slot="sidebar-inset"],
+:root[data-has-wallpaper] [data-slot="sidebar-inset"] > div,
+:root[data-has-wallpaper] [data-slot="sidebar-container"] {
+ background-color: transparent !important;
+}
+
+/* Interactive tier — inputs stay highly visible */
+:root[data-has-wallpaper] [data-slot="input-control"] {
+ background-color: color-mix(in srgb, var(--background) var(--wp-interactive, 97%), transparent) !important;
+}
+
+/* Chat header matches sidebar tint */
+:root[data-has-wallpaper] [data-slot="sidebar-inset"] > div > header {
+ background-color: color-mix(in srgb, var(--card) var(--wp-nested, 34%), transparent) !important;
+}
+
+/* Nested tier — sidebar, cards, panels, code blocks get a subtle tint */
+:root[data-has-wallpaper] [data-slot="sidebar-inner"] {
+ background-color: color-mix(in srgb, var(--card) var(--wp-nested, 34%), transparent) !important;
+}
+
+:root[data-has-wallpaper] .bg-card {
+ background-color: color-mix(in srgb, var(--card) var(--wp-nested, 34%), transparent) !important;
+}
+
+:root[data-has-wallpaper] .bg-muted {
+ background-color: color-mix(in srgb, var(--muted) var(--wp-nested, 34%), transparent) !important;
+}
+
body {
font-family:
"DM Sans",
diff --git a/apps/web/src/lib/colorThemes.ts b/apps/web/src/lib/colorThemes.ts
new file mode 100644
index 00000000000..a43980f71fd
--- /dev/null
+++ b/apps/web/src/lib/colorThemes.ts
@@ -0,0 +1,347 @@
+import { getAppSettingsSnapshot } from "../appSettings";
+
+export interface ThemeColors {
+ bg: string;
+ bg2: string;
+ border: string;
+ accent: string;
+ accent2: string;
+ text: string;
+ textMuted: string;
+ textDim: string;
+ danger: string;
+ green: string;
+ yellow: string;
+ magenta: string;
+}
+
+export interface ColorTheme {
+ id: string;
+ name: string;
+ colors: ThemeColors;
+ interactiveOpacityRate?: number;
+}
+
+export const COLOR_THEMES: readonly ColorTheme[] = [
+ {
+ id: "cyberpunk",
+ name: "Cyberpunk",
+ colors: {
+ bg: "#080810",
+ bg2: "#0c1018",
+ border: "#1e2d40",
+ accent: "#c8ff00",
+ accent2: "#0fc5ed",
+ text: "#d0e0f0",
+ textMuted: "#7a9ab8",
+ textDim: "#4a6580",
+ danger: "#ff2e4a",
+ green: "#44ffb1",
+ yellow: "#ffe073",
+ magenta: "#a277ff",
+ },
+ },
+ {
+ id: "josean",
+ name: "Josean",
+ interactiveOpacityRate: 0.7,
+ colors: {
+ bg: "#011423",
+ bg2: "#01101c",
+ border: "#033259",
+ accent: "#47ff9c",
+ accent2: "#0fc5ed",
+ text: "#cbe0f0",
+ textMuted: "#7a9ab8",
+ textDim: "#4a6580",
+ danger: "#e52e2e",
+ green: "#44ffb1",
+ yellow: "#ffe073",
+ magenta: "#a277ff",
+ },
+ },
+ {
+ id: "tokyo-night",
+ name: "Tokyo Night",
+ colors: {
+ bg: "#1a1b26",
+ bg2: "#16161e",
+ border: "#292e42",
+ accent: "#7aa2f7",
+ accent2: "#2ac3de",
+ text: "#c0caf5",
+ textMuted: "#7982a8",
+ textDim: "#565d80",
+ danger: "#f7768e",
+ green: "#9ece6a",
+ yellow: "#e0af68",
+ magenta: "#bb9af7",
+ },
+ },
+ {
+ id: "dracula",
+ name: "Dracula",
+ colors: {
+ bg: "#282a36",
+ bg2: "#21222c",
+ border: "#44475a",
+ accent: "#bd93f9",
+ accent2: "#8be9fd",
+ text: "#f8f8f2",
+ textMuted: "#8893b8",
+ textDim: "#626580",
+ danger: "#ff5555",
+ green: "#50fa7b",
+ yellow: "#f1fa8c",
+ magenta: "#ff79c6",
+ },
+ },
+ {
+ id: "catppuccin-mocha",
+ name: "Catppuccin Mocha",
+ colors: {
+ bg: "#1e1e2e",
+ bg2: "#181825",
+ border: "#313244",
+ accent: "#cba6f7",
+ accent2: "#89dceb",
+ text: "#cdd6f4",
+ textMuted: "#8f93a8",
+ textDim: "#626478",
+ danger: "#f38ba8",
+ green: "#a6e3a1",
+ yellow: "#f9e2af",
+ magenta: "#f5c2e7",
+ },
+ },
+ {
+ id: "gruvbox-dark",
+ name: "Gruvbox Dark",
+ colors: {
+ bg: "#282828",
+ bg2: "#1d2021",
+ border: "#3c3836",
+ accent: "#fabd2f",
+ accent2: "#83a598",
+ text: "#ebdbb2",
+ textMuted: "#a89984",
+ textDim: "#665c54",
+ danger: "#fb4934",
+ green: "#b8bb26",
+ yellow: "#fabd2f",
+ magenta: "#d3869b",
+ },
+ },
+ {
+ id: "nord",
+ name: "Nord",
+ colors: {
+ bg: "#2e3440",
+ bg2: "#272c36",
+ border: "#3b4252",
+ accent: "#88c0d0",
+ accent2: "#81a1c1",
+ text: "#eceff4",
+ textMuted: "#9aa5b4",
+ textDim: "#616e7c",
+ danger: "#bf616a",
+ green: "#a3be8c",
+ yellow: "#ebcb8b",
+ magenta: "#b48ead",
+ },
+ },
+ {
+ id: "one-dark",
+ name: "One Dark",
+ colors: {
+ bg: "#282c34",
+ bg2: "#21252b",
+ border: "#3e4452",
+ accent: "#61afef",
+ accent2: "#56b6c2",
+ text: "#abb2bf",
+ textMuted: "#7f848e",
+ textDim: "#5c6370",
+ danger: "#e06c75",
+ green: "#98c379",
+ yellow: "#e5c07b",
+ magenta: "#c678dd",
+ },
+ },
+ {
+ id: "solarized-dark",
+ name: "Solarized Dark",
+ colors: {
+ bg: "#002b36",
+ bg2: "#00252f",
+ border: "#073642",
+ accent: "#b58900",
+ accent2: "#268bd2",
+ text: "#839496",
+ textMuted: "#657b83",
+ textDim: "#586e75",
+ danger: "#dc322f",
+ green: "#859900",
+ yellow: "#b58900",
+ magenta: "#d33682",
+ },
+ },
+ {
+ id: "rose-pine",
+ name: "Rose Pine",
+ colors: {
+ bg: "#191724",
+ bg2: "#1f1d2e",
+ border: "#26233a",
+ accent: "#ebbcba",
+ accent2: "#31748f",
+ text: "#e0def4",
+ textMuted: "#908caa",
+ textDim: "#6e6a86",
+ danger: "#eb6f92",
+ green: "#9ccfd8",
+ yellow: "#f6c177",
+ magenta: "#c4a7e7",
+ },
+ },
+ {
+ id: "kanagawa",
+ name: "Kanagawa",
+ colors: {
+ bg: "#1f1f28",
+ bg2: "#16161d",
+ border: "#2a2a37",
+ accent: "#dca561",
+ accent2: "#7e9cd8",
+ text: "#dcd7ba",
+ textMuted: "#9a9a8e",
+ textDim: "#727169",
+ danger: "#e82424",
+ green: "#98bb6c",
+ yellow: "#e6c384",
+ magenta: "#957fb8",
+ },
+ },
+] as const;
+
+const CSS_VAR_KEYS = [
+ "--background",
+ "--card",
+ "--popover",
+ "--border",
+ "--primary",
+ "--ring",
+ "--secondary",
+ "--muted",
+ "--accent",
+ "--foreground",
+ "--card-foreground",
+ "--popover-foreground",
+ "--primary-foreground",
+ "--muted-foreground",
+ "--secondary-foreground",
+ "--accent-foreground",
+ "--destructive",
+ "--destructive-foreground",
+ "--success",
+ "--success-foreground",
+ "--warning",
+ "--warning-foreground",
+ "--info",
+ "--info-foreground",
+ "--input",
+] as const;
+
+function hexToAlpha(hex: string, alpha: number): string {
+ const r = parseInt(hex.slice(1, 3), 16);
+ const g = parseInt(hex.slice(3, 5), 16);
+ const b = parseInt(hex.slice(5, 7), 16);
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
+}
+
+/** Mix two hex colors. ratio 0 = pure a, 1 = pure b. */
+function mixHex(a: string, b: string, ratio: number): string {
+ const ar = parseInt(a.slice(1, 3), 16);
+ const ag = parseInt(a.slice(3, 5), 16);
+ const ab = parseInt(a.slice(5, 7), 16);
+ const br = parseInt(b.slice(1, 3), 16);
+ const bg = parseInt(b.slice(3, 5), 16);
+ const bb = parseInt(b.slice(5, 7), 16);
+ const r = Math.round(ar + (br - ar) * ratio);
+ const g = Math.round(ag + (bg - ag) * ratio);
+ const bl = Math.round(ab + (bb - ab) * ratio);
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${bl.toString(16).padStart(2, "0")}`;
+}
+
+export function getColorThemeById(id: string): ColorTheme | undefined {
+ return COLOR_THEMES.find((theme) => theme.id === id);
+}
+
+export function applyColorTheme(themeId: string | null | undefined, wallpaperActive = false): void {
+ const root = document.documentElement;
+
+ if (!themeId) {
+ for (const key of CSS_VAR_KEYS) {
+ root.style.removeProperty(key);
+ }
+ return;
+ }
+
+ const theme = getColorThemeById(themeId);
+ if (!theme) return;
+
+ const { colors } = theme;
+
+ // When wallpaper is active, boost muted/dim text toward the main text color
+ const textMuted = wallpaperActive ? mixHex(colors.textMuted, colors.text, 0.3) : colors.textMuted;
+ const textDim = wallpaperActive ? mixHex(colors.textDim, colors.text, 0.3) : colors.textDim;
+
+ root.style.setProperty("--background", colors.bg);
+ root.style.setProperty("--card", colors.bg2);
+ root.style.setProperty("--popover", colors.bg2);
+ root.style.setProperty("--border", colors.border);
+ root.style.setProperty("--primary", colors.accent);
+ root.style.setProperty("--ring", colors.accent);
+ root.style.setProperty("--secondary", hexToAlpha(colors.accent2, 0.12));
+ root.style.setProperty("--muted", hexToAlpha(colors.accent2, 0.12));
+ root.style.setProperty("--accent", hexToAlpha(colors.accent2, 0.12));
+ root.style.setProperty("--foreground", colors.text);
+ root.style.setProperty("--card-foreground", colors.text);
+ root.style.setProperty("--popover-foreground", colors.text);
+ root.style.setProperty("--primary-foreground", colors.text);
+ root.style.setProperty("--muted-foreground", textMuted);
+ root.style.setProperty("--secondary-foreground", textDim);
+ root.style.setProperty("--accent-foreground", textDim);
+ root.style.setProperty("--destructive", colors.danger);
+ root.style.setProperty("--destructive-foreground", colors.danger);
+ root.style.setProperty("--success", colors.green);
+ root.style.setProperty("--success-foreground", colors.green);
+ root.style.setProperty("--warning", colors.yellow);
+ root.style.setProperty("--warning-foreground", colors.yellow);
+ root.style.setProperty("--info", colors.magenta);
+ root.style.setProperty("--info-foreground", colors.magenta);
+ root.style.setProperty("--input", hexToAlpha(colors.border, 0.8));
+}
+
+export function getInteractiveOpacityRate(themeId: string | undefined): number {
+ if (!themeId) return 0.55;
+ return getColorThemeById(themeId)?.interactiveOpacityRate ?? 0.55;
+}
+
+/** Single-layer elements (sidebar) that sit directly over the wallpaper */
+export function interactiveOpacity(panelOpacity: number, rate = 0.55): number {
+ return 1 - (1 - panelOpacity) * rate;
+}
+
+/** Elements nested inside the panel — subtle bg2 tint, not a full opaque layer */
+export function nestedOpacity(panelOpacity: number, rate = 0.55): number {
+ return panelOpacity * rate;
+}
+
+// Eager application at module load: apply color theme if dark mode is active
+{
+ const settings = getAppSettingsSnapshot();
+ if (settings.colorThemeId && document.documentElement.classList.contains("dark")) {
+ applyColorTheme(settings.colorThemeId, !!settings.backgroundImage);
+ }
+}
diff --git a/apps/web/src/lib/wallpapers.ts b/apps/web/src/lib/wallpapers.ts
new file mode 100644
index 00000000000..aaa3f5cad79
--- /dev/null
+++ b/apps/web/src/lib/wallpapers.ts
@@ -0,0 +1,26 @@
+export interface WallpaperPreset {
+ id: string;
+ name: string;
+ file: string;
+}
+
+export const WALLPAPER_PRESETS: readonly WallpaperPreset[] = [
+ { id: "preset:ship-at-sea", name: "Ship at Sea", file: "ship-at-sea.jpg" },
+ { id: "preset:akane-pagoda", name: "Akane Pagoda", file: "akane-pagoda.jpg" },
+ { id: "preset:everforest", name: "Everforest", file: "everforest.jpg" },
+ { id: "preset:gruvbox-ferns", name: "Gruvbox Ferns", file: "gruvbox-ferns.jpg" },
+ { id: "preset:akane-cliff", name: "Akane Cliff", file: "akane-cliff.jpg" },
+ { id: "preset:akane-bridge", name: "Akane Bridge", file: "akane-bridge.jpg" },
+ { id: "preset:akane-mist", name: "Akane Mist", file: "akane-mist.jpg" },
+ { id: "preset:pink-lakeside", name: "Pink Lakeside", file: "pink-lakeside.png" },
+] as const;
+
+export function getWallpaperUrl(preset: WallpaperPreset): string {
+ return `/wallpapers/${preset.file}`;
+}
+
+export function resolveBackgroundImageUrl(value: string): string {
+ const preset = WALLPAPER_PRESETS.find((p) => p.id === value);
+ if (preset) return getWallpaperUrl(preset);
+ return value;
+}
diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx
index eb3eca9cbd6..9b5bd42ba0d 100644
--- a/apps/web/src/routes/__root.tsx
+++ b/apps/web/src/routes/__root.tsx
@@ -10,8 +10,10 @@ import { useEffect, useRef } from "react";
import { QueryClient, useQueryClient } from "@tanstack/react-query";
import { APP_DISPLAY_NAME } from "../branding";
+import { BackgroundLayer } from "../components/BackgroundLayer";
import { Button } from "../components/ui/button";
import { AnchoredToastProvider, ToastProvider, toastManager } from "../components/ui/toast";
+import { useColorThemeEffect } from "../hooks/useColorTheme";
import { serverConfigQueryOptions, serverQueryKeys } from "../lib/serverReactQuery";
import { readNativeApi } from "../nativeApi";
import { useComposerDraftStore } from "../composerDraftStore";
@@ -51,6 +53,8 @@ function RootRouteView() {
+
+
@@ -296,6 +300,11 @@ function EventRouter() {
return null;
}
+function ColorThemeManager() {
+ useColorThemeEffect();
+ return null;
+}
+
function DesktopProjectBootstrap() {
// Desktop hydration runs through EventRouter project + orchestration sync.
return null;
diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx
index 8dfeb210f28..ebe41f07af8 100644
--- a/apps/web/src/routes/_chat.settings.tsx
+++ b/apps/web/src/routes/_chat.settings.tsx
@@ -7,6 +7,8 @@ import { getModelOptions, normalizeModelSlug } from "@t3tools/shared/model";
import { MAX_CUSTOM_MODEL_LENGTH, useAppSettings } from "../appSettings";
import { isElectron } from "../env";
import { useTheme } from "../hooks/useTheme";
+import { COLOR_THEMES } from "../lib/colorThemes";
+import { WALLPAPER_PRESETS, getWallpaperUrl } from "../lib/wallpapers";
import { serverConfigQueryOptions } from "../lib/serverReactQuery";
import { ensureNativeApi } from "../nativeApi";
import { preferredTerminalEditor } from "../terminal-links";
@@ -93,6 +95,7 @@ function SettingsRouteView() {
const [customModelErrorByProvider, setCustomModelErrorByProvider] = useState<
Partial
>
>({});
+ const [customWallpaperUrl, setCustomWallpaperUrl] = useState("");
const codexBinaryPath = settings.codexBinaryPath;
const codexHomePath = settings.codexHomePath;
@@ -232,6 +235,218 @@ function SettingsRouteView() {
Active theme: {resolvedTheme}
+
+ {/* Color Theme */}
+
+
+
Color Theme
+
+ {resolvedTheme === "dark"
+ ? "Pick a color palette for dark mode."
+ : "Switch to dark mode to use color themes."}
+
+
+
+
+
+ {COLOR_THEMES.map((ct) => {
+ const selected = settings.colorThemeId === ct.id;
+ return (
+
+ );
+ })}
+
+
+
+ {/* Background Wallpaper */}
+
+
+
Background
+
+ Choose a wallpaper image displayed behind the app.
+
+
+
+
+
+ {WALLPAPER_PRESETS.map((preset) => {
+ const selected = settings.backgroundImage === preset.id;
+ return (
+
+ );
+ })}
+
+
+
+ setCustomWallpaperUrl(event.target.value)}
+ placeholder="https://example.com/wallpaper.jpg"
+ spellCheck={false}
+ className="flex-1 text-xs"
+ onKeyDown={(event) => {
+ if (event.key !== "Enter") return;
+ event.preventDefault();
+ const url = customWallpaperUrl.trim();
+ if (url) {
+ updateSettings({ backgroundImage: url });
+ setCustomWallpaperUrl("");
+ }
+ }}
+ />
+
+
+
+
+ {/* Opacity & Blur (only when wallpaper is active) */}
+ {settings.backgroundImage ? (
+
+
+
Overlay & Blur
+
+ Adjust the color overlay opacity and blur on the wallpaper.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : null}