Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion desktop/src/features/settings/ui/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ function ThemeSettingsCard() {
<div className="mb-3 min-w-0">
<h2 className="text-sm font-semibold tracking-tight">Appearance</h2>
<p className="text-sm text-muted-foreground">
Choose a theme for Sprout. Light and dark mode is auto-detected.
Pick the theme Sprout should use. Your selection stays active until
you choose another one.
</p>
</div>

Expand Down
12 changes: 11 additions & 1 deletion desktop/src/shared/theme/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { createThemeVars, hexToHsl } from "./adaptive-theme";
import {
SYNTAX_THEMES,
isLightTheme,
type SyntaxThemeName,
extractThemeInfo,
loadThemeData,
Expand Down Expand Up @@ -85,6 +86,12 @@ function applyAccentColor(hex: string) {
root.style.setProperty("--sidebar-primary-foreground", fgHsl);
}

function applyThemeClass(themeName: SyntaxThemeName) {
const root = document.documentElement;
root.classList.remove("light", "dark");
root.classList.add(isLightTheme(themeName) ? "light" : "dark");
}

/** Apply cached CSS vars synchronously to prevent FOUC. */
function applyCachedVars(): string | null {
try {
Expand Down Expand Up @@ -146,7 +153,10 @@ export function ThemeProvider({
// Apply cached vars synchronously before first render
const [themeName, setThemeName] = useState<string>(() => {
const cached = applyCachedVars();
return cached ?? readStoredTheme(defaultTheme);
if (cached) return cached;
const storedTheme = readStoredTheme(defaultTheme);
applyThemeClass(storedTheme);
return storedTheme;
});
const [isDark, setIsDark] = useState<boolean>(() => {
return document.documentElement.classList.contains("dark");
Expand Down
40 changes: 29 additions & 11 deletions desktop/tests/e2e/profile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ test("opens settings with the keyboard shortcut and updates theme", async ({
await expect(page.getByTestId("settings-nav-appearance")).toBeVisible();
await page.getByTestId("settings-nav-appearance").click();

// Default theme is catppuccin-macchiato (dark)
// Default theme is houston (dark)
await expect
.poll(() =>
page.evaluate(() => document.documentElement.classList.contains("dark")),
Expand All @@ -306,27 +306,37 @@ test("opens settings with the keyboard shortcut and updates theme", async ({
)
.toBe(true);

await expect
.poll(() => page.evaluate(() => localStorage.getItem("sprout-theme")))
.toBe("github-light");

const primaryBeforeAccent = await page.evaluate(() =>
document.documentElement.style.getPropertyValue("--primary").trim(),
);
expect(primaryBeforeAccent).toBeTruthy();

// Accent choice should persist across later theme switches.
await page.getByTestId("accent-color-red").click();

await expect
.poll(() =>
page.evaluate(() => document.documentElement.classList.contains("dark")),
page.evaluate(() => localStorage.getItem("sprout-accent-color")),
)
.toBe(false);
.toBe("#ef4444");

// CSS variables are set on the root element (the real theming mechanism)
await expect
.poll(() =>
page.evaluate(() =>
document.documentElement.style.getPropertyValue("--background").trim(),
document.documentElement.style.getPropertyValue("--primary").trim(),
),
)
.toBeTruthy();
.not.toBe(primaryBeforeAccent);

// Theme name persists in localStorage
await expect
.poll(() => page.evaluate(() => localStorage.getItem("sprout-theme")))
.toBe("github-light");
const redAccent = await page.evaluate(() =>
document.documentElement.style.getPropertyValue("--primary").trim(),
);
expect(redAccent).toBeTruthy();

// Switch back to a dark theme — verifies light→dark transition
await page.getByTestId("theme-option-dracula").click();

await expect
Expand All @@ -339,6 +349,14 @@ test("opens settings with the keyboard shortcut and updates theme", async ({
.poll(() => page.evaluate(() => localStorage.getItem("sprout-theme")))
.toBe("dracula");

await expect
.poll(() =>
page.evaluate(() =>
document.documentElement.style.getPropertyValue("--primary").trim(),
),
)
.toBe(redAccent);

// Close settings with keyboard shortcut
await page.keyboard.press(
process.platform === "darwin" ? "Meta+," : "Control+,",
Expand Down