Skip to content

Commit a0cc65e

Browse files
committed
Add accent color theming and a manual Check for Updates button in Settings
1 parent 88f9755 commit a0cc65e

8 files changed

Lines changed: 187 additions & 5 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@ Beyond chat, Modelforge includes an **agentic mode** — the model can read/writ
5151

5252
**Customization & control**
5353
- English and Turkish UI localization.
54+
- **Theming** — light/dark/system color mode plus a choice of accent colors (default gray, blue, green, purple, orange, rose), in Settings → Appearance.
5455
- Configurable Ollama host — point at a remote server instead of localhost.
5556
- Data export/import, and one-click "copy diagnostic info" for bug reports.
56-
- Auto-updates: packaged builds check GitHub Releases for new versions.
57+
- **Updates**packaged builds check GitHub Releases for new versions automatically on launch, plus a manual "Check for updates" button in Settings (also available from the app menu).
5758

5859
## Screenshots
5960

app/src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ function registerIpcHandlers(): void {
279279
isBusy = busy;
280280
});
281281
ipcMain.handle("app:getVersion", () => app.getVersion());
282+
ipcMain.handle("app:checkForUpdates", () => checkForUpdatesManually(() => mainWindow));
282283
ipcMain.handle("app:getDiagnostics", async () => ({
283284
appVersion: app.getVersion(),
284285
electron: process.versions.electron,

app/src/preload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ contextBridge.exposeInMainWorld("api", {
8888
app: {
8989
setBusy: (busy: boolean) => ipcRenderer.invoke("app:setBusy", busy),
9090
getVersion: () => ipcRenderer.invoke("app:getVersion"),
91+
checkForUpdates: () => ipcRenderer.invoke("app:checkForUpdates"),
9192
getDiagnostics: () => ipcRenderer.invoke("app:getDiagnostics"),
9293
openLogsFolder: () => ipcRenderer.invoke("app:openLogsFolder"),
9394
},

frontend/src/components/theme-provider.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { createContext, useContext, useEffect, useState } from "react";
22

33
type Theme = "dark" | "light" | "system";
4+
export type AccentColor = "default" | "blue" | "green" | "purple" | "orange" | "rose";
5+
6+
// eslint-disable-next-line react-refresh/only-export-components -- constant shared with the accent-color picker in Settings
7+
export const ACCENT_COLORS: AccentColor[] = ["default", "blue", "green", "purple", "orange", "rose"];
48

59
type ThemeProviderProps = {
610
children: React.ReactNode;
@@ -11,9 +15,12 @@ type ThemeProviderProps = {
1115
type ThemeProviderState = {
1216
theme: Theme;
1317
setTheme: (theme: Theme) => void;
18+
accent: AccentColor;
19+
setAccent: (accent: AccentColor) => void;
1420
};
1521

1622
const ThemeProviderContext = createContext<ThemeProviderState | undefined>(undefined);
23+
const ACCENT_STORAGE_KEY = "vite-ui-accent";
1724

1825
export function ThemeProvider({
1926
children,
@@ -23,6 +30,9 @@ export function ThemeProvider({
2330
const [theme, setThemeState] = useState<Theme>(
2431
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
2532
);
33+
const [accent, setAccentState] = useState<AccentColor>(
34+
() => (localStorage.getItem(ACCENT_STORAGE_KEY) as AccentColor) || "default"
35+
);
2636

2737
useEffect(() => {
2838
const root = window.document.documentElement;
@@ -39,13 +49,24 @@ export function ThemeProvider({
3949
root.classList.add(theme);
4050
}, [theme]);
4151

52+
useEffect(() => {
53+
const root = window.document.documentElement;
54+
root.classList.remove(...ACCENT_COLORS.map((c) => `theme-${c}`));
55+
if (accent !== "default") root.classList.add(`theme-${accent}`);
56+
}, [accent]);
57+
4258
const setTheme = (theme: Theme) => {
4359
localStorage.setItem(storageKey, theme);
4460
setThemeState(theme);
4561
};
4662

63+
const setAccent = (accent: AccentColor) => {
64+
localStorage.setItem(ACCENT_STORAGE_KEY, accent);
65+
setAccentState(accent);
66+
};
67+
4768
return (
48-
<ThemeProviderContext.Provider value={{ theme, setTheme }}>
69+
<ThemeProviderContext.Provider value={{ theme, setTheme, accent, setAccent }}>
4970
{children}
5071
</ThemeProviderContext.Provider>
5172
);

frontend/src/index.css

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,66 @@
4444
--ring: oklch(0.556 0 0);
4545
}
4646

47+
/* Accent color themes — layered on top of the light/dark mode above. Only
48+
--primary/--ring (and their foreground) are tinted; backgrounds, borders,
49+
and muted surfaces stay neutral so the accent reads as a deliberate highlight
50+
color rather than tinting the whole UI. "Default" (no .theme-* class) keeps
51+
the original grayscale look. */
52+
.theme-blue {
53+
--primary: oklch(0.5 0.2 260);
54+
--primary-foreground: oklch(0.985 0 0);
55+
--ring: oklch(0.5 0.2 260);
56+
}
57+
.dark.theme-blue {
58+
--primary: oklch(0.65 0.19 260);
59+
--primary-foreground: oklch(0.145 0 0);
60+
--ring: oklch(0.65 0.19 260);
61+
}
62+
63+
.theme-green {
64+
--primary: oklch(0.5 0.16 145);
65+
--primary-foreground: oklch(0.985 0 0);
66+
--ring: oklch(0.5 0.16 145);
67+
}
68+
.dark.theme-green {
69+
--primary: oklch(0.65 0.17 145);
70+
--primary-foreground: oklch(0.145 0 0);
71+
--ring: oklch(0.65 0.17 145);
72+
}
73+
74+
.theme-purple {
75+
--primary: oklch(0.5 0.22 300);
76+
--primary-foreground: oklch(0.985 0 0);
77+
--ring: oklch(0.5 0.22 300);
78+
}
79+
.dark.theme-purple {
80+
--primary: oklch(0.65 0.2 300);
81+
--primary-foreground: oklch(0.145 0 0);
82+
--ring: oklch(0.65 0.2 300);
83+
}
84+
85+
.theme-orange {
86+
--primary: oklch(0.58 0.19 55);
87+
--primary-foreground: oklch(0.985 0 0);
88+
--ring: oklch(0.58 0.19 55);
89+
}
90+
.dark.theme-orange {
91+
--primary: oklch(0.7 0.17 55);
92+
--primary-foreground: oklch(0.145 0 0);
93+
--ring: oklch(0.7 0.17 55);
94+
}
95+
96+
.theme-rose {
97+
--primary: oklch(0.55 0.22 10);
98+
--primary-foreground: oklch(0.985 0 0);
99+
--ring: oklch(0.55 0.22 10);
100+
}
101+
.dark.theme-rose {
102+
--primary: oklch(0.65 0.19 10);
103+
--primary-foreground: oklch(0.145 0 0);
104+
--ring: oklch(0.65 0.19 10);
105+
}
106+
47107
@theme inline {
48108
--radius-sm: calc(var(--radius) - 4px);
49109
--radius-md: calc(var(--radius) - 2px);

frontend/src/lib/translations.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,15 @@ export interface Dictionary {
9090
copyDiagnosticInfo: string;
9191
copied: string;
9292
openLogsFolder: string;
93+
checkForUpdates: string;
9394
language: string;
95+
appearance: string;
96+
colorMode: string;
97+
colorModeLight: string;
98+
colorModeDark: string;
99+
colorModeSystem: string;
100+
accentColor: string;
101+
accentColorNames: { default: string; blue: string; green: string; purple: string; orange: string; rose: string };
94102
general: string;
95103
providers: string;
96104
models: string;
@@ -191,7 +199,22 @@ export const en: Dictionary = {
191199
copyDiagnosticInfo: "Copy diagnostic info",
192200
copied: "Copied",
193201
openLogsFolder: "Open logs folder",
202+
checkForUpdates: "Check for updates",
194203
language: "Language",
204+
appearance: "Appearance",
205+
colorMode: "Color mode",
206+
colorModeLight: "Light",
207+
colorModeDark: "Dark",
208+
colorModeSystem: "System",
209+
accentColor: "Accent color",
210+
accentColorNames: {
211+
default: "Default (gray)",
212+
blue: "Blue",
213+
green: "Green",
214+
purple: "Purple",
215+
orange: "Orange",
216+
rose: "Rose",
217+
},
195218
general: "General",
196219
providers: "Providers",
197220
models: "Models",
@@ -293,7 +316,22 @@ export const tr: Dictionary = {
293316
copyDiagnosticInfo: "Tanılama bilgisini kopyala",
294317
copied: "Kopyalandı",
295318
openLogsFolder: "Günlük klasörünü aç",
319+
checkForUpdates: "Güncellemeleri denetle",
296320
language: "Dil",
321+
appearance: "Görünüm",
322+
colorMode: "Renk modu",
323+
colorModeLight: "Açık",
324+
colorModeDark: "Koyu",
325+
colorModeSystem: "Sistem",
326+
accentColor: "Vurgu rengi",
327+
accentColorNames: {
328+
default: "Varsayılan (gri)",
329+
blue: "Mavi",
330+
green: "Yeşil",
331+
purple: "Mor",
332+
orange: "Turuncu",
333+
rose: "Gül kurusu",
334+
},
297335
general: "Genel",
298336
providers: "Sağlayıcılar",
299337
models: "Modeller",

frontend/src/pages/Settings.tsx

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Settings2,
1313
Bug,
1414
Copy,
15+
RefreshCw,
1516
} from "lucide-react";
1617
import { Button } from "@/components/ui/button";
1718
import { Badge } from "@/components/ui/badge";
@@ -35,6 +36,8 @@ import { OPENAI_MODELS, ANTHROPIC_MODELS, formatModelRef } from "@/lib/providers
3536
import { useSessions } from "@/lib/sessions-context";
3637
import { useI18n } from "@/lib/i18n";
3738
import type { Locale } from "@/lib/translations";
39+
import { useTheme, ACCENT_COLORS, type AccentColor } from "@/components/theme-provider";
40+
import { cn } from "@/lib/utils";
3841

3942
// Ollama pulls Hugging Face GGUF models via a "hf.co/user/repo[:quant]" model
4043
// name — accept a pasted full URL or the "huggingface.co/" host too, rather
@@ -46,12 +49,25 @@ function normalizeModelTag(input: string): string {
4649
.replace(/^huggingface\.co\//i, "hf.co/");
4750
}
4851

52+
// Static preview swatches for the accent color picker — independent of the
53+
// live CSS variables so the dot always shows the same recognizable hue
54+
// regardless of which theme is currently active.
55+
const ACCENT_SWATCHES: Record<AccentColor, string> = {
56+
default: "oklch(0.556 0 0)",
57+
blue: "oklch(0.55 0.2 260)",
58+
green: "oklch(0.55 0.16 145)",
59+
purple: "oklch(0.55 0.22 300)",
60+
orange: "oklch(0.62 0.19 55)",
61+
rose: "oklch(0.58 0.22 10)",
62+
};
63+
4964
function formatBytes(bytes: number) {
5065
return `${(bytes / 1e9).toFixed(1)} GB`;
5166
}
5267

5368
export default function Settings() {
5469
const { t, locale, setLocale } = useI18n();
70+
const { theme, setTheme, accent, setAccent } = useTheme();
5571
const [running, setRunning] = useState<boolean | null>(null);
5672
const [specs, setSpecs] = useState<SystemSpecs | null>(null);
5773
const [recommendations, setRecommendations] = useState<ModelRecommendations | null>(null);
@@ -291,6 +307,44 @@ export default function Settings() {
291307
</SettingsRow>
292308
</SettingsSection>
293309

310+
<SettingsSection title={t.appearance}>
311+
<SettingsRow label={t.colorMode}>
312+
<Select value={theme} onValueChange={(v) => setTheme(v as "light" | "dark" | "system")}>
313+
<SelectTrigger size="sm" className="w-36">
314+
<SelectValue />
315+
</SelectTrigger>
316+
<SelectContent>
317+
<SelectItem value="light">{t.colorModeLight}</SelectItem>
318+
<SelectItem value="dark">{t.colorModeDark}</SelectItem>
319+
<SelectItem value="system">{t.colorModeSystem}</SelectItem>
320+
</SelectContent>
321+
</Select>
322+
</SettingsRow>
323+
<SettingsRow label={t.accentColor} stacked>
324+
<div className="flex flex-wrap gap-2">
325+
{ACCENT_COLORS.map((c) => (
326+
<button
327+
key={c}
328+
type="button"
329+
onClick={() => setAccent(c)}
330+
aria-label={t.accentColorNames[c]}
331+
aria-pressed={accent === c}
332+
title={t.accentColorNames[c]}
333+
className={cn(
334+
"flex size-8 items-center justify-center rounded-full border-2 transition-colors",
335+
accent === c ? "border-foreground" : "border-transparent"
336+
)}
337+
>
338+
<span
339+
className="size-6 rounded-full"
340+
style={{ backgroundColor: ACCENT_SWATCHES[c] }}
341+
/>
342+
</button>
343+
))}
344+
</div>
345+
</SettingsRow>
346+
</SettingsSection>
347+
294348
<SettingsSection title={t.language}>
295349
<SettingsRow label={t.language}>
296350
<Select value={locale} onValueChange={(v) => setLocale(v as Locale)}>
@@ -346,9 +400,14 @@ export default function Settings() {
346400
</SettingsSection>
347401
)}
348402

349-
<p className="text-center text-xs text-muted-foreground">
350-
{t.appName}{appVersion ? ` v${appVersion}` : ""}
351-
</p>
403+
<div className="flex flex-col items-center gap-1.5">
404+
<p className="text-center text-xs text-muted-foreground">
405+
{t.appName}{appVersion ? ` v${appVersion}` : ""}
406+
</p>
407+
<Button size="sm" variant="outline" onClick={() => window.api.app.checkForUpdates()} className="gap-1.5">
408+
<RefreshCw className="size-3.5" /> {t.checkForUpdates}
409+
</Button>
410+
</div>
352411
</div>
353412

354413
<div>

frontend/src/types/electron.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ export interface ElectronApi {
253253
app: {
254254
setBusy: (busy: boolean) => Promise<void>;
255255
getVersion: () => Promise<string>;
256+
checkForUpdates: () => Promise<void>;
256257
getDiagnostics: () => Promise<{
257258
appVersion: string;
258259
electron: string;

0 commit comments

Comments
 (0)