diff --git a/.changeset/fn-7458-system-theme-default.md b/.changeset/fn-7458-system-theme-default.md new file mode 100644 index 0000000000..51e782c9fe --- /dev/null +++ b/.changeset/fn-7458-system-theme-default.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Default fresh startup and theme reset to System mode. +category: fix +dev: Fresh global settings, pre-hydration scripts, and Appearance reset now keep Shadcn Ember while following OS light/dark preference. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 6ae7c2d196..2b95d774dc 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -1620,8 +1620,9 @@ Non-Command-Center dashboard CSS uses `--text` as the canonical primary text tok + -Dark/light modes via `data-theme`; 76 color themes via `data-color-theme` (lazy-loaded from `app/public/theme-data.css`), including the Shadcn zinc-neutral theme with an orange default highlight/accent, Shadcn Custom (the same base with sanitized per-token color-picker overrides), and its color family: Shadcn Blue/Green/Red/Purple/Pink/Orange/Yellow, Shadcn Mono Red/Blue/Green/Purple/Pink/Orange/Yellow (grayscale surfaces with color-specific accents; legacy `shadcn-mono` selections migrate to Shadcn Mono Red), Shadcn Black (pure black and white), Shadcn Gray (fully neutral zinc-gray accent), and Shadcn Gray Blue (blue-gray slate neutral surfaces with a muted slate-blue accent). Air is the minimal, borderless, paper-like preset with near-monochrome tokens and CSS-only chrome flattening. Glass Silver preserves the Glass theme's frosted translucent surfaces and transparent modal overlay behavior while using silver and graphite accents instead of purple/pink. +Dark/light modes via `data-theme`; fresh installs default to System mode so the resolved theme follows `prefers-color-scheme` until the user explicitly chooses Light, Dark, or System. 76 color themes via `data-color-theme` (lazy-loaded from `app/public/theme-data.css`), including the Shadcn zinc-neutral theme with an orange default highlight/accent, Shadcn Custom (the same base with sanitized per-token color-picker overrides), and its color family: Shadcn Blue/Green/Red/Purple/Pink/Orange/Yellow, Shadcn Mono Red/Blue/Green/Purple/Pink/Orange/Yellow (grayscale surfaces with color-specific accents; legacy `shadcn-mono` selections migrate to Shadcn Mono Red), Shadcn Black (pure black and white), Shadcn Gray (fully neutral zinc-gray accent), and Shadcn Gray Blue (blue-gray slate neutral surfaces with a muted slate-blue accent). Air is the minimal, borderless, paper-like preset with near-monochrome tokens and CSS-only chrome flattening. Glass Silver preserves the Glass theme's frosted translucent surfaces and transparent modal overlay behavior while using silver and graphite accents instead of purple/pink. Choose Shadcn variants from **Settings → Appearance** or from the Command Center **Overview** theme card; both selectors use the same `themeOptions.ts` labels and color-chip swatches. The left sidebar active-item highlight and resize accent use the active theme's `--accent`, so they follow the selected Shadcn accent instead of staying fixed blue. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index fe146700cb..33ef7436ad 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -48,8 +48,8 @@ Defaults from `DEFAULT_GLOBAL_SETTINGS`; key scope from `GLOBAL_SETTINGS_KEYS`. | Setting | Type | Default | Description | |---|---|---:|---| -| `themeMode` | `"dark" \| "light" \| "system"` | `"dark"` | Dashboard theme mode. | -| `colorTheme` | `ColorTheme` | `"default"` | Dashboard color theme preset. Use `"shadcn-custom"` to show the custom shadcn color picker in Settings → Appearance and the Command Center theme card. | +| `themeMode` | `"dark" \| "light" \| "system"` | `"system"` | Dashboard theme mode. Fresh installs follow the operating system light/dark preference until the user chooses Light, Dark, or System. | +| `colorTheme` | `ColorTheme` | `"shadcn-ember"` | Dashboard color theme preset. Use `"shadcn-custom"` to show the custom shadcn color picker in Settings → Appearance and the Command Center theme card. | | `shadcnCustomColors` | `Record` | `undefined` | Optional shadcn design-token override map for `"shadcn-custom"` only. Keys are CSS token names such as `--accent`, `--bg`, `--surface`, `--card`, `--border`, `--text`, `--text-muted`, workflow status tokens, and `--color-success`/`--color-warning`/`--color-error`; values must be sanitized `#RGB` or `#RRGGBB` hex colors. Missing or invalid entries fall back to the `shadcn-custom` base defaults and are not applied to other themes. | | `language` | `"en" \| "zh-CN" \| "zh-TW" \| "fr" \| "es" \| "ko"` | `undefined` | UI language for the dashboard and TUI. When unset, the dashboard detects from localStorage → browser language and the CLI from `--lang` flag → environment locale, falling back to `en`. Validated at the store write boundary (`validateLocale`); invalid values are dropped. Reset to auto-detect via the dashboard's "Auto" language option or `fn settings set language auto` (clears the persisted key). | | `dashboardFontScalePct` | `number` | `100` | Dashboard font scale percentage used by Appearance settings. Valid range: `85` to `125`; applied pre-hydration via document root font-size so board typography (column headers/counts, task cards, and quick-entry text) scales with the setting from first paint. | diff --git a/packages/core/src/__tests__/global-settings.test.ts b/packages/core/src/__tests__/global-settings.test.ts index 4b6ad55f7f..0b841a3920 100644 --- a/packages/core/src/__tests__/global-settings.test.ts +++ b/packages/core/src/__tests__/global-settings.test.ts @@ -64,7 +64,7 @@ describe("GlobalSettingsStore", () => { const raw = await readFile(join(dir, "settings.json"), "utf-8"); const parsed = JSON.parse(raw); - expect(parsed.themeMode).toBe("dark"); + expect(parsed.themeMode).toBe("system"); expect(parsed.colorTheme).toBe("shadcn-ember"); expect(parsed.ntfyEnabled).toBe(false); }); @@ -86,6 +86,13 @@ describe("GlobalSettingsStore", () => { expect(settings.themeMode).toBe("light"); }); + it.each(["dark", "light", "system"] as const)("preserves explicit %s theme mode choices", async (themeMode) => { + await store.init(); + await store.updateSettings({ themeMode }); + + await expect(store.getSettings()).resolves.toMatchObject({ themeMode }); + }); + it("adopts the legacy ~/.pi/kb directory when ~/.fusion does not exist", async () => { const homeDir = makeTmpDir(); process.env.HOME = homeDir; @@ -233,7 +240,7 @@ describe("GlobalSettingsStore", () => { expect(settings.defaultModelId).toBe("claude-sonnet-4-5"); expect(settings.ntfyEnabled).toBe(true); expect(settings.ntfyTopic).toBe("my-topic"); - expect(settings.themeMode).toBe("dark"); // preserved default + expect(settings.themeMode).toBe("system"); // preserved default }); it("does not overwrite an existing invalid settings file with defaults", async () => { @@ -486,7 +493,7 @@ describe("GlobalSettingsStore", () => { const result = await store.readRaw(); expect(result.futureField).toBe("hello"); - expect(result.themeMode).toBe("dark"); + expect(result.themeMode).toBe("system"); }); it("readRaw returns empty object for missing file", async () => { @@ -555,7 +562,7 @@ describe("GlobalSettingsStore", () => { // First call populates cache const first = await store.getSettings(); - expect(first.themeMode).toBe("dark"); + expect(first.themeMode).toBe("system"); // Update settings await store.updateSettings({ themeMode: "light" }); @@ -865,7 +872,7 @@ describe("GlobalSettingsStore", () => { // Settings file should exist with defaults but no model fields persisted const raw = JSON.parse(await readFile(join(dir, "settings.json"), "utf-8")); // Only default theme fields should be present - expect(raw.themeMode).toBe("dark"); + expect(raw.themeMode).toBe("system"); expect(raw.colorTheme).toBe("shadcn-ember"); // Model fields should not be persisted expect(raw.defaultProvider).toBeUndefined(); diff --git a/packages/core/src/__tests__/store-settings.test.ts b/packages/core/src/__tests__/store-settings.test.ts index 203a0b1801..37d94f430d 100644 --- a/packages/core/src/__tests__/store-settings.test.ts +++ b/packages/core/src/__tests__/store-settings.test.ts @@ -1421,7 +1421,7 @@ describe("TaskStore", () => { describe("global/project settings merging", () => { it("getSettings returns global defaults when no overrides exist", async () => { const settings = await harness.store().getSettings(); - expect(settings.themeMode).toBe("dark"); + expect(settings.themeMode).toBe("system"); expect(settings.colorTheme).toBe("shadcn-ember"); expect(settings.maxConcurrent).toBe(2); }); @@ -1468,7 +1468,7 @@ describe("TaskStore", () => { const settings = await harness.store().getSettings(); expect(settings.maxConcurrent).toBe(5); // themeMode should still be the global default, not "light" - expect(settings.themeMode).toBe("dark"); + expect(settings.themeMode).toBe("system"); // Verify the project config doesn't contain themeMode const configRaw = await readFile(join(harness.rootDir(), ".fusion", "config.json"), "utf-8"); @@ -1766,7 +1766,7 @@ describe("TaskStore", () => { expect(settings.maxWorktrees).toBe(4); // default expect(settings.pollIntervalMs).toBe(15000); // default // Global settings should still be present - expect(settings.themeMode).toBe("dark"); // global default + expect(settings.themeMode).toBe("system"); // global default }); it("includes global settings merged with project settings", async () => { diff --git a/packages/core/src/settings-schema.ts b/packages/core/src/settings-schema.ts index 67b8f90a54..6bb6ce397b 100644 --- a/packages/core/src/settings-schema.ts +++ b/packages/core/src/settings-schema.ts @@ -63,7 +63,11 @@ type ProjectSettingsSchema = Omit; /** Default values for global (user-level) settings. */ export const DEFAULT_GLOBAL_SETTINGS = { - themeMode: "dark", + /* + FNXC:DashboardTheming 2026-07-03-00:00: + Fresh installs must follow the operating system theme until the user explicitly chooses Light, Dark, or System. Keep this global default aligned with dashboard and desktop pre-hydration fallbacks. + */ + themeMode: "system", /* FNXC:DashboardTheming 2026-06-30-00:00: New users and unset installs should start on Shadcn Ember. Existing users who explicitly stored colorTheme "default", "ocean", or another valid theme must remain on that selection, so the ids stay valid and only the absence/default seed changes to "shadcn-ember". diff --git a/packages/dashboard/app/__tests__/index-html-theme-link.test.ts b/packages/dashboard/app/__tests__/index-html-theme-link.test.ts index 2598ddc166..e740db91ff 100644 --- a/packages/dashboard/app/__tests__/index-html-theme-link.test.ts +++ b/packages/dashboard/app/__tests__/index-html-theme-link.test.ts @@ -25,6 +25,24 @@ describe("index.html theme-data boot contract", () => { expect(script).toContain("style.fontSize"); }); + it("defaults web pre-hydration to system theme mode", () => { + expect(script).toContain("localStorage.getItem('kb-dashboard-theme-mode') || 'system'"); + expect(script).toContain("var validModes = ['dark', 'light', 'system']"); + expect(script).toContain("mode = 'system'"); + expect(script).not.toContain("localStorage.getItem('kb-dashboard-theme-mode') || 'dark'"); + expect(script).toContain("fallbackSystemDark ? 'dark' : 'light'"); + }); + + it("keeps desktop renderer pre-hydration aligned with system default", () => { + const desktopHtml = readFileSync(resolve(PACKAGE_ROOT, "../desktop/src/renderer/index.html"), "utf8"); + + expect(desktopHtml).toContain('localStorage.getItem("kb-dashboard-theme-mode") || "system"'); + expect(desktopHtml).toContain('var validModes = ["dark", "light", "system"]'); + expect(desktopHtml).toContain('mode = "system"'); + expect(desktopHtml).not.toContain('localStorage.getItem("kb-dashboard-theme-mode") || "dark"'); + expect(desktopHtml).toContain('fallbackSystemDark ? "dark" : "light"'); + }); + it("keeps themeDataUrl resolution branches", () => { expect(script).toContain("http://"); expect(script).toContain("https://"); diff --git a/packages/dashboard/app/components/SettingsModal.tsx b/packages/dashboard/app/components/SettingsModal.tsx index 5c1c58bd68..3a4be98c4c 100644 --- a/packages/dashboard/app/components/SettingsModal.tsx +++ b/packages/dashboard/app/components/SettingsModal.tsx @@ -679,8 +679,12 @@ export function SettingsModal({ addToast, projectId, initialSection, - themeMode = "dark", - colorTheme = "ocean", + /* + FNXC:DashboardTheming 2026-07-03-00:00: + Settings/onboarding surfaces that render before App threads persisted appearance settings should mirror fresh startup defaults: System mode with Shadcn Ember. + */ + themeMode = "system", + colorTheme = "shadcn-ember", onThemeModeChange, onColorThemeChange, dashboardFontScalePct = 100, diff --git a/packages/dashboard/app/components/ThemeSelector.tsx b/packages/dashboard/app/components/ThemeSelector.tsx index 2630e9c555..055d0e0ae2 100644 --- a/packages/dashboard/app/components/ThemeSelector.tsx +++ b/packages/dashboard/app/components/ThemeSelector.tsx @@ -41,10 +41,10 @@ export function ThemeSelector({ }: ThemeSelectorProps) { const { t } = useTranslation("app"); const handleReset = useCallback(() => { - onThemeModeChange("dark"); + onThemeModeChange("system"); /* - FNXC:DashboardTheming 2026-06-30-00:00: - Reset to defaults must match the new unset-install fallback so Settings restores Shadcn Ember without migrating explicit Ocean or legacy theme choices. + FNXC:DashboardTheming 2026-07-03-00:00: + Reset to defaults must match fresh-install behavior: System mode follows the OS preference while Shadcn Ember stays the default color theme, without migrating explicit Ocean or legacy theme choices. */ onColorThemeChange("shadcn-ember"); onDashboardFontScaleChange(100); diff --git a/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx b/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx index 69ea515655..8c93ad3933 100644 --- a/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx +++ b/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx @@ -120,13 +120,18 @@ describe("ThemeDropdown", () => { render( , ); - fireEvent.click(screen.getByRole("button", { name: /light/i })); + const modeGroup = screen.getByRole("radiogroup", { name: /theme mode/i }); + expect(within(modeGroup).getByRole("button", { name: /light/i })).toHaveAttribute("aria-pressed", "false"); + expect(within(modeGroup).getByRole("button", { name: /dark/i })).toHaveAttribute("aria-pressed", "false"); + expect(within(modeGroup).getByRole("button", { name: /system/i })).toHaveAttribute("aria-pressed", "true"); + + fireEvent.click(within(modeGroup).getByRole("button", { name: /light/i })); expect(onThemeModeChange).toHaveBeenCalledWith("light"); }); diff --git a/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx b/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx index 86fa9e83c6..e5c028a93e 100644 --- a/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx +++ b/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx @@ -641,7 +641,7 @@ describe("ThemeSelector", () => { ); fireEvent.click(screen.getByLabelText("Reset to default theme")); - expect(onThemeModeChange).toHaveBeenCalledWith("dark"); + expect(onThemeModeChange).toHaveBeenCalledWith("system"); expect(onColorThemeChange).toHaveBeenCalledWith("shadcn-ember"); }); diff --git a/packages/dashboard/app/hooks/__tests__/useTheme.test.ts b/packages/dashboard/app/hooks/__tests__/useTheme.test.ts index 290c317ae4..da005925c5 100644 --- a/packages/dashboard/app/hooks/__tests__/useTheme.test.ts +++ b/packages/dashboard/app/hooks/__tests__/useTheme.test.ts @@ -100,11 +100,26 @@ describe("useTheme", () => { document.querySelectorAll('link[id="theme-data"]').forEach((link) => link.remove()); }); - it("initializes with default values when localStorage is empty", () => { + it("initializes with system theme and Shadcn Ember color defaults when localStorage is empty", () => { + currentSystemDark = false; + const { result } = renderHook(() => useTheme()); - expect(result.current.themeMode).toBe("dark"); + expect(result.current.themeMode).toBe("system"); + expect(result.current.resolvedThemeMode).toBe("light"); expect(result.current.colorTheme).toBe("shadcn-ember"); + expect(document.documentElement.getAttribute("data-theme")).toBe("light"); + expect(document.documentElement.getAttribute("data-color-theme")).toBe("shadcn-ember"); + }); + + it("resolves fresh system theme to dark when the OS preference is dark", () => { + currentSystemDark = true; + + const { result } = renderHook(() => useTheme()); + + expect(result.current.themeMode).toBe("system"); + expect(result.current.resolvedThemeMode).toBe("dark"); + expect(document.documentElement.getAttribute("data-theme")).toBe("dark"); }); it("initializes from localStorage", () => { @@ -138,7 +153,7 @@ describe("useTheme", () => { const { result } = renderHook(() => useTheme()); - expect(result.current.themeMode).toBe("dark"); + expect(result.current.themeMode).toBe("system"); await waitFor(() => { expect(result.current.themeMode).toBe("light"); @@ -756,11 +771,14 @@ describe("useTheme", () => { }); it("ignores invalid theme mode in localStorage", () => { + currentSystemDark = false; localStorageMock[THEME_MODE_STORAGE_KEY] = "invalid"; const { result } = renderHook(() => useTheme()); - expect(result.current.themeMode).toBe("dark"); + expect(result.current.themeMode).toBe("system"); + expect(result.current.resolvedThemeMode).toBe("light"); + expect(document.documentElement.getAttribute("data-theme")).toBe("light"); }); it("remaps legacy shadcn mono color theme from localStorage", () => { @@ -819,7 +837,7 @@ describe("useTheme", () => { const { result } = renderHook(() => useTheme()); - expect(result.current.themeMode).toBe("dark"); + expect(result.current.themeMode).toBe("system"); expect(result.current.colorTheme).toBe("shadcn-ember"); }); @@ -877,6 +895,31 @@ describe("useTheme", () => { }); describe("getThemeInitScript", () => { + let scriptSystemDark = true; + + beforeEach(() => { + vi.unstubAllGlobals(); + scriptSystemDark = true; + localStorage.clear(); + document.documentElement.removeAttribute("data-theme"); + document.documentElement.removeAttribute("data-color-theme"); + document.documentElement.style.fontSize = ""; + document.querySelectorAll('link[id="theme-data"]').forEach((link) => link.remove()); + const themeDataLink = document.createElement("link"); + themeDataLink.id = "theme-data"; + themeDataLink.rel = "stylesheet"; + themeDataLink.href = "/theme-data.css"; + document.head.appendChild(themeDataLink); + vi.stubGlobal("matchMedia", (query: string) => ({ + matches: query === "(prefers-color-scheme: dark)" ? scriptSystemDark : false, + media: query, + onchange: null, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => true, + })); + }); + it("returns a script string", () => { const script = getThemeInitScript(); @@ -922,6 +965,64 @@ describe("getThemeInitScript", () => { expect(script).toContain("prefers-color-scheme"); expect(script).toContain("systemDark"); expect(script).toContain("effectiveMode"); + expect(script).toContain("mode = localStorage.getItem('kb-dashboard-theme-mode') || 'system'"); + }); + + it("pre-hydration script resolves fresh startup from light OS preference", () => { + scriptSystemDark = false; + + window.eval(getThemeInitScript()); + + expect(document.documentElement.getAttribute("data-theme")).toBe("light"); + expect(document.documentElement.getAttribute("data-color-theme")).toBe("shadcn-ember"); + }); + + it("pre-hydration script resolves fresh startup from dark OS preference", () => { + scriptSystemDark = true; + + window.eval(getThemeInitScript()); + + expect(document.documentElement.getAttribute("data-theme")).toBe("dark"); + expect(document.documentElement.getAttribute("data-color-theme")).toBe("shadcn-ember"); + }); + + it("pre-hydration script preserves explicit cached dark and light modes", () => { + scriptSystemDark = false; + localStorage.setItem(THEME_MODE_STORAGE_KEY, "dark"); + window.eval(getThemeInitScript()); + expect(document.documentElement.getAttribute("data-theme")).toBe("dark"); + + scriptSystemDark = true; + localStorage.setItem(THEME_MODE_STORAGE_KEY, "light"); + window.eval(getThemeInitScript()); + expect(document.documentElement.getAttribute("data-theme")).toBe("light"); + }); + + it("pre-hydration script treats invalid cached theme mode as system", () => { + scriptSystemDark = false; + localStorage.setItem(THEME_MODE_STORAGE_KEY, "sepia-but-mode"); + + window.eval(getThemeInitScript()); + + expect(document.documentElement.getAttribute("data-theme")).toBe("light"); + }); + + it("pre-hydration script catch fallback follows system preference", () => { + vi.stubGlobal("localStorage", { + getItem: () => { + throw new Error("localStorage disabled"); + }, + setItem: () => {}, + removeItem: () => {}, + }); + scriptSystemDark = false; + + window.eval(getThemeInitScript()); + expect(document.documentElement.getAttribute("data-theme")).toBe("light"); + + scriptSystemDark = true; + window.eval(getThemeInitScript()); + expect(document.documentElement.getAttribute("data-theme")).toBe("dark"); }); it("pre-hydration script applies only sanitized shadcn-custom overrides", () => { diff --git a/packages/dashboard/app/hooks/useTheme.ts b/packages/dashboard/app/hooks/useTheme.ts index 3ed34212c1..09bbc03b3e 100644 --- a/packages/dashboard/app/hooks/useTheme.ts +++ b/packages/dashboard/app/hooks/useTheme.ts @@ -71,7 +71,7 @@ function isValidThemeMode(value: unknown): value is ThemeMode { } function readCachedThemeMode(): ThemeMode { - if (!isBrowser) return "dark"; + if (!isBrowser) return "system"; try { const saved = localStorage.getItem(THEME_MODE_STORAGE_KEY); if (isValidThemeMode(saved)) { @@ -80,7 +80,11 @@ function readCachedThemeMode(): ThemeMode { } catch { // localStorage not available, use default } - return "dark"; + /* + FNXC:DashboardTheming 2026-07-03-00:00: + Missing or invalid browser cache must behave like a fresh install: System mode is selected and the resolved data-theme follows prefers-color-scheme before backend hydration. + */ + return "system"; } function readCachedColorTheme(): ColorTheme { @@ -464,7 +468,11 @@ export function getThemeInitScript(): string { return ` (function() { try { - var mode = localStorage.getItem('${THEME_MODE_STORAGE_KEY}') || 'dark'; + var mode = localStorage.getItem('${THEME_MODE_STORAGE_KEY}') || 'system'; + var validModes = ['dark', 'light', 'system']; + if (!validModes.includes(mode)) { + mode = 'system'; + } var colorTheme = localStorage.getItem('${COLOR_THEME_STORAGE_KEY}') || '${DEFAULT_COLOR_THEME}'; var validThemes = ${JSON.stringify(VALID_COLOR_THEMES)}; // FNXC:DashboardTheming 2026-06-30-00:00: Unset startup theme is Shadcn Ember; explicit stored legacy ids such as "default" and "ocean" remain valid and must not be migrated. @@ -478,6 +486,7 @@ export function getThemeInitScript(): string { fontScale = ${DEFAULT_FONT_SCALE_PCT}; } fontScale = Math.min(${MAX_FONT_SCALE_PCT}, Math.max(${MIN_FONT_SCALE_PCT}, Math.round(fontScale))); + // FNXC:DashboardTheming 2026-07-03-00:00: Pre-hydration defaults to System mode so fresh startup matches the OS preference without waiting for React or backend settings hydration. var systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches; var effectiveMode = mode === 'system' ? (systemDark ? 'dark' : 'light') : mode; document.documentElement.setAttribute('data-theme', effectiveMode); @@ -526,7 +535,11 @@ export function getThemeInitScript(): string { } } } catch (e) { - document.documentElement.setAttribute('data-theme', 'dark'); + var fallbackSystemDark = false; + try { + fallbackSystemDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + } catch (matchMediaError) {} + document.documentElement.setAttribute('data-theme', fallbackSystemDark ? 'dark' : 'light'); document.documentElement.setAttribute('data-color-theme', '${DEFAULT_COLOR_THEME}'); document.documentElement.style.fontSize = '${DEFAULT_FONT_SCALE_PCT}%'; } diff --git a/packages/dashboard/app/index.html b/packages/dashboard/app/index.html index 06365b177a..db5545af86 100644 --- a/packages/dashboard/app/index.html +++ b/packages/dashboard/app/index.html @@ -158,7 +158,11 @@ // Theme initialization - runs before React to prevent flash (function() { try { - var mode = localStorage.getItem('kb-dashboard-theme-mode') || 'dark'; + var mode = localStorage.getItem('kb-dashboard-theme-mode') || 'system'; + var validModes = ['dark', 'light', 'system']; + if (!validModes.includes(mode)) { + mode = 'system'; + } var colorTheme = localStorage.getItem('kb-dashboard-color-theme') || 'shadcn-ember'; var validThemes = ['default', 'ocean', 'forest', 'sunset', 'zen', 'berry', 'high-contrast', 'industrial', 'monochrome', 'slate', 'ash', 'air', 'graphite', 'silver', 'solarized', 'factory', 'factory-mono', 'ayu', 'one-dark', 'nord', 'dracula', 'gruvbox', 'tokyo-night', 'catppuccin-mocha', 'github-dark', 'everforest', 'rose-pine', 'kanagawa', 'night-owl', 'palenight', 'monokai-pro', 'slime', 'brutalist', 'neon-city', 'parchment', 'terminal', 'glass', 'glass-silver', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono-red', 'shadcn-mono-blue', 'shadcn-mono-green', 'shadcn-mono-purple', 'shadcn-mono-pink', 'shadcn-mono-orange', 'shadcn-mono-yellow', 'shadcn-black', 'shadcn-gray', 'shadcn-gray-blue']; // FNXC:DashboardTheming 2026-06-30-00:00: Unset startup theme is Shadcn Ember; explicit stored legacy ids such as "default" and "ocean" remain valid and must not be migrated. @@ -172,6 +176,7 @@ fontScale = 100; } fontScale = Math.min(125, Math.max(85, Math.round(fontScale))); + // FNXC:DashboardTheming 2026-07-03-00:00: Fresh web startup uses System mode by default so first paint follows prefers-color-scheme until the user stores an explicit Light/Dark/System choice. var systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches; var effectiveMode = mode === 'system' ? (systemDark ? 'dark' : 'light') : mode; document.documentElement.setAttribute('data-theme', effectiveMode); @@ -219,7 +224,11 @@ } } } catch (e) { - document.documentElement.setAttribute('data-theme', 'dark'); + var fallbackSystemDark = false; + try { + fallbackSystemDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + } catch (matchMediaError) {} + document.documentElement.setAttribute('data-theme', fallbackSystemDark ? 'dark' : 'light'); document.documentElement.setAttribute('data-color-theme', 'shadcn-ember'); document.documentElement.style.fontSize = '100%'; } diff --git a/packages/desktop/src/renderer/index.html b/packages/desktop/src/renderer/index.html index 855a6b5495..5af021e101 100644 --- a/packages/desktop/src/renderer/index.html +++ b/packages/desktop/src/renderer/index.html @@ -8,7 +8,11 @@ // Theme initialization - runs before React to prevent flash (function () { try { - var mode = localStorage.getItem("kb-dashboard-theme-mode") || "dark"; + var mode = localStorage.getItem("kb-dashboard-theme-mode") || "system"; + var validModes = ["dark", "light", "system"]; + if (!validModes.includes(mode)) { + mode = "system"; + } var colorTheme = localStorage.getItem("kb-dashboard-color-theme") || "shadcn-ember"; var validThemes = [ "default", @@ -95,12 +99,17 @@ colorTheme = "shadcn-ember"; } + // FNXC:DashboardTheming 2026-07-03-00:00: Desktop shell startup mirrors dashboard startup: default System mode resolves from prefers-color-scheme before React hydration. var systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches; var effectiveMode = mode === "system" ? (systemDark ? "dark" : "light") : mode; document.documentElement.setAttribute("data-theme", effectiveMode); document.documentElement.setAttribute("data-color-theme", colorTheme); } catch (error) { - document.documentElement.setAttribute("data-theme", "dark"); + var fallbackSystemDark = false; + try { + fallbackSystemDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + } catch (matchMediaError) {} + document.documentElement.setAttribute("data-theme", fallbackSystemDark ? "dark" : "light"); document.documentElement.setAttribute("data-color-theme", "shadcn-ember"); } })();