From 211b18b07390626c71fc029ab3e54fa196d8c2ee Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 30 Jun 2026 13:43:28 -0700 Subject: [PATCH] FN-7299: make Shadcn Ember the default dashboard theme Set Shadcn Ember as the default dashboard theme while keeping Shadcn Gray available as an alternate. - Add Shadcn Ember theme metadata, CSS variables, and boot-time defaults for dashboard and desktop shells. - Update persisted settings types, schema defaults, and theme selection UI to prefer Ember. - Cover Ember defaults and theme-option ordering with core, hook, and dashboard component tests. - Add a published package changeset for the operator-facing default theme change. Files changed: .changeset/fn-7299-shadcn-ember-default.md | 7 ++ .../core/src/__tests__/global-settings.test.ts | 23 +++- packages/core/src/__tests__/store-settings.test.ts | 10 +- packages/core/src/settings-schema.ts | 6 +- packages/core/src/types.ts | 4 +- .../app/__tests__/shadcn-ember-theme.test.ts | 107 ++++++++++++++++ .../app/__tests__/shadcn-gray-theme.test.ts | 2 +- .../dashboard/app/components/ThemeSelector.css | 15 +++ .../dashboard/app/components/ThemeSelector.tsx | 6 +- .../components/__tests__/ThemeDropdown.test.tsx | 20 ++- .../components/__tests__/ThemeSelector.test.tsx | 24 +++- packages/dashboard/app/components/themeOptions.ts | 7 +- .../dashboard/app/hooks/__tests__/useTheme.test.ts | 18 ++- packages/dashboard/app/hooks/useTheme.ts | 8 +- packages/dashboard/app/index.html | 10 +- packages/dashboard/app/public/theme-data.css | 137 +++++++++++++++++++++ packages/desktop/src/renderer/index.html | 42 ++++++- 17 files changed, 409 insertions(+), 37 deletions(-) Fusion-Task-Id: FN-7299 Fusion-Task-Lineage: 7e175752-b8fc-4ba9-8534-cb485f6eb18a Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7299-shadcn-ember-default.md | 7 + .../src/__tests__/global-settings.test.ts | 23 ++- .../core/src/__tests__/store-settings.test.ts | 10 +- packages/core/src/settings-schema.ts | 6 +- packages/core/src/types.ts | 4 +- .../app/__tests__/shadcn-ember-theme.test.ts | 107 ++++++++++++++ .../app/__tests__/shadcn-gray-theme.test.ts | 2 +- .../app/components/ThemeSelector.css | 15 ++ .../app/components/ThemeSelector.tsx | 6 +- .../__tests__/ThemeDropdown.test.tsx | 20 ++- .../__tests__/ThemeSelector.test.tsx | 24 ++- .../dashboard/app/components/themeOptions.ts | 7 +- .../app/hooks/__tests__/useTheme.test.ts | 18 ++- packages/dashboard/app/hooks/useTheme.ts | 8 +- packages/dashboard/app/index.html | 10 +- packages/dashboard/app/public/theme-data.css | 137 ++++++++++++++++++ packages/desktop/src/renderer/index.html | 42 +++++- 17 files changed, 409 insertions(+), 37 deletions(-) create mode 100644 .changeset/fn-7299-shadcn-ember-default.md create mode 100644 packages/dashboard/app/__tests__/shadcn-ember-theme.test.ts diff --git a/.changeset/fn-7299-shadcn-ember-default.md b/.changeset/fn-7299-shadcn-ember-default.md new file mode 100644 index 0000000000..4dd7e9b35a --- /dev/null +++ b/.changeset/fn-7299-shadcn-ember-default.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Make Shadcn Ember the default dashboard theme. +category: feature +dev: Adds the shadcn-ember color theme and updates default theme fallbacks. diff --git a/packages/core/src/__tests__/global-settings.test.ts b/packages/core/src/__tests__/global-settings.test.ts index 9773ed100c..4b6ad55f7f 100644 --- a/packages/core/src/__tests__/global-settings.test.ts +++ b/packages/core/src/__tests__/global-settings.test.ts @@ -65,7 +65,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.colorTheme).toBe("ocean"); + expect(parsed.colorTheme).toBe("shadcn-ember"); expect(parsed.ntfyEnabled).toBe(false); }); @@ -173,6 +173,23 @@ describe("GlobalSettingsStore", () => { expect(settings.defaultProvider).toBeUndefined(); }); + it("preserves explicit legacy color theme selections", async () => { + await mkdir(dir, { recursive: true }); + await writeFile( + join(dir, "settings.json"), + JSON.stringify({ colorTheme: "default" }), + ); + + await expect(new GlobalSettingsStore(dir).getSettings()).resolves.toMatchObject({ colorTheme: "default" }); + + await writeFile( + join(dir, "settings.json"), + JSON.stringify({ colorTheme: "ocean" }), + ); + + await expect(new GlobalSettingsStore(dir).getSettings()).resolves.toMatchObject({ colorTheme: "ocean" }); + }); + it("returns defaults on invalid JSON", async () => { await mkdir(dir, { recursive: true }); await writeFile(join(dir, "settings.json"), "not-json{{{"); @@ -197,7 +214,7 @@ describe("GlobalSettingsStore", () => { const updated = await store.updateSettings({ themeMode: "system" }); expect(updated.themeMode).toBe("system"); - expect(updated.colorTheme).toBe("ocean"); // unchanged default + expect(updated.colorTheme).toBe("shadcn-ember"); // unchanged default // Verify persistence const raw = await readFile(join(dir, "settings.json"), "utf-8"); @@ -849,7 +866,7 @@ describe("GlobalSettingsStore", () => { 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.colorTheme).toBe("ocean"); + expect(raw.colorTheme).toBe("shadcn-ember"); // Model fields should not be persisted expect(raw.defaultProvider).toBeUndefined(); expect(raw.defaultModelId).toBeUndefined(); diff --git a/packages/core/src/__tests__/store-settings.test.ts b/packages/core/src/__tests__/store-settings.test.ts index 2b97b8696e..203a0b1801 100644 --- a/packages/core/src/__tests__/store-settings.test.ts +++ b/packages/core/src/__tests__/store-settings.test.ts @@ -1422,7 +1422,7 @@ describe("TaskStore", () => { it("getSettings returns global defaults when no overrides exist", async () => { const settings = await harness.store().getSettings(); expect(settings.themeMode).toBe("dark"); - expect(settings.colorTheme).toBe("ocean"); + expect(settings.colorTheme).toBe("shadcn-ember"); expect(settings.maxConcurrent).toBe(2); }); @@ -1433,6 +1433,14 @@ describe("TaskStore", () => { expect(settings.colorTheme).toBe("ocean"); }); + it("preserves explicit legacy color theme selections", async () => { + await harness.store().updateGlobalSettings({ colorTheme: "default" }); + await expect(harness.store().getSettings()).resolves.toMatchObject({ colorTheme: "default" }); + + await harness.store().updateGlobalSettings({ colorTheme: "ocean" }); + await expect(harness.store().getSettings()).resolves.toMatchObject({ colorTheme: "ocean" }); + }); + it("project settings override global defaults", async () => { await harness.store().updateSettings({ maxConcurrent: 8 }); const settings = await harness.store().getSettings(); diff --git a/packages/core/src/settings-schema.ts b/packages/core/src/settings-schema.ts index 87ef8c55ed..64ea5442de 100644 --- a/packages/core/src/settings-schema.ts +++ b/packages/core/src/settings-schema.ts @@ -65,10 +65,10 @@ type ProjectSettingsSchema = Omit; export const DEFAULT_GLOBAL_SETTINGS = { themeMode: "dark", /* - FNXC:DashboardTheming 2026-06-22-18:36: - New users and unset installs should start on Ocean. Existing users who explicitly stored colorTheme "default" must remain on that legacy theme, so the id stays valid and only the absence/default seed changes to "ocean". + 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". */ - colorTheme: "ocean", + colorTheme: "shadcn-ember", shadcnCustomColors: undefined, dashboardFontScalePct: 100, /* diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 08f17136cc..fca80c13d4 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -333,6 +333,8 @@ export const COLOR_THEMES = [ "neon-bloom", "sepia", "shadcn", + // FNXC:DashboardTheming 2026-06-30-00:00: Shadcn Ember is the default for unset installs; keep it adjacent to the shadcn base so dashboard options and bootstrap validators preserve published theme order while explicit legacy ids remain valid. + "shadcn-ember", // FNXC:DashboardTheming 2026-06-20-18:20: FN-6816 adds the user-customizable shadcn variant; keep this union in lockstep with dashboard theme options, swatches, theme-data base blocks, and the shadcn custom color token list. "shadcn-custom", // FNXC:DashboardTheming 2026-06-19-16:07: FN-6756 extends the published color-theme union with shadcn-family accent variants; keep dashboard theme options, bootstrap validation, swatches, and theme-data token blocks in lockstep with this ordered list. @@ -2948,7 +2950,7 @@ export interface McpServersSettings { export interface GlobalSettings { /** Theme mode preference: dark, light, or system (follows OS). Default: "dark". */ themeMode?: ThemeMode; - /** Color theme preference for accent colors and styling. Default: "ocean"; "default" is the legacy Fusion theme id. */ + /** Color theme preference for accent colors and styling. Default: "shadcn-ember"; "default" and "ocean" remain valid explicit legacy selections. */ colorTheme?: ColorTheme; /** Token→hex override map for the customizable shadcn theme. Applied only when `colorTheme === "shadcn-custom"`; dashboard sanitizes keys and values before writing CSS custom properties. */ shadcnCustomColors?: Record; diff --git a/packages/dashboard/app/__tests__/shadcn-ember-theme.test.ts b/packages/dashboard/app/__tests__/shadcn-ember-theme.test.ts new file mode 100644 index 0000000000..fc745d5cd2 --- /dev/null +++ b/packages/dashboard/app/__tests__/shadcn-ember-theme.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it } from "vitest"; +import { COLOR_THEMES as CORE_COLOR_THEMES } from "@fusion/core"; +import { COLOR_THEMES as DASHBOARD_COLOR_THEMES } from "../components/themeOptions"; +import fs from "fs"; +import path from "path"; + +const themeDataPath = path.resolve(__dirname, "../public/theme-data.css"); +const dashboardIndexPath = path.resolve(__dirname, "../index.html"); +const desktopIndexPath = path.resolve(__dirname, "../../../desktop/src/renderer/index.html"); + +/* +FNXC:DashboardTheming 2026-06-30-00:00: +Shadcn Ember is the default color-theme contract. Tests assert source-of-truth CSS and every bootstrap validator so unset/invalid installs converge on Shadcn Ember while explicit legacy ids remain valid. +*/ +describe("Shadcn Ember color theme", () => { + const themeData = fs.readFileSync(themeDataPath, "utf-8"); + const dashboardIndex = fs.readFileSync(dashboardIndexPath, "utf-8"); + const desktopIndex = fs.readFileSync(desktopIndexPath, "utf-8"); + + it("defines dark and light shadcn structure with Ember color tokens", () => { + const darkBlock = extractSelectorBlock(themeData, '[data-color-theme="shadcn-ember"]'); + const lightBlock = extractSelectorBlock(themeData, '[data-color-theme="shadcn-ember"][data-theme="light"]'); + + expect(darkBlock).toContain("--surface-hover:"); + expect(lightBlock).toContain("--surface-hover:"); + expect(darkBlock).toContain("--bg: #09090b;"); + expect(darkBlock).toContain("--card: #18181b;"); + expect(lightBlock).toContain("--bg: #ffffff;"); + expect(lightBlock).toContain("--card-hover: #f4f4f5;"); + expect(darkBlock).toContain("--btn-border-width: 1px;"); + expect(darkBlock).toContain("--font-primary: \"Geist\","); + + expect(darkBlock).toContain("--todo: #a0a0a0;"); + expect(darkBlock).toContain("--in-progress: #b8b8b8;"); + expect(darkBlock).toContain("--color-error: #ff6b6b;"); + expect(darkBlock).toContain("--cta-bg: #d4622a;"); + expect(darkBlock).toContain("--cta-border: #e8773a;"); + expect(darkBlock).toContain("--color-info: #e8773a;"); + expect(darkBlock).toContain("--accent: #e8773a;"); + + expect(lightBlock).toContain("--todo: #404040;"); + expect(lightBlock).toContain("--in-progress: #606060;"); + expect(lightBlock).toContain("--color-error: #dc2626;"); + expect(lightBlock).toContain("--cta-bg: #c05820;"); + expect(lightBlock).toContain("--cta-border: #d4622a;"); + expect(lightBlock).toContain("--color-info: #c05820;"); + expect(lightBlock).toContain("--accent: #d4622a;"); + + expect(darkBlock).toContain("--color-warning: #f59e0b;"); + expect(lightBlock).toContain("--color-muted: #71717a;"); + }); + + it("registers the default theme in core, dashboard options, and bootstrap validators", () => { + expect(CORE_COLOR_THEMES).toContain("shadcn-ember"); + expect(DASHBOARD_COLOR_THEMES).toContainEqual({ + value: "shadcn-ember", + label: "Shadcn Ember (Default)", + className: "theme-swatch-shadcn-ember", + }); + expect(DASHBOARD_COLOR_THEMES.filter((theme) => theme.label.includes("(Default)")).map((theme) => theme.value)).toEqual([ + "shadcn-ember", + ]); + + expect(dashboardIndex).toContain("'shadcn-ember'"); + expect(dashboardIndex).toContain("|| 'shadcn-ember'"); + expect(dashboardIndex).toContain("colorTheme = 'shadcn-ember'"); + expect(desktopIndex).toContain('"shadcn-ember"'); + expect(desktopIndex).toContain('|| "shadcn-ember"'); + expect(desktopIndex).toContain('colorTheme = "shadcn-ember"'); + }); + + it("keeps dashboard and desktop bootstrap validators identical to the core color theme union", () => { + expect(DASHBOARD_COLOR_THEMES.map((theme) => theme.value)).toEqual([...CORE_COLOR_THEMES]); + expect(extractValidThemes(dashboardIndex)).toEqual([...CORE_COLOR_THEMES]); + expect(extractValidThemes(desktopIndex)).toEqual([...CORE_COLOR_THEMES]); + }); +}); + +function extractValidThemes(html: string): string[] { + const match = html.match(/var validThemes = \[([\s\S]*?)\];/); + if (!match) { + throw new Error("Could not find pre-hydration validThemes array"); + } + + return Array.from(match[1].matchAll(/["']([^"']+)["']/g), ([, theme]) => theme); +} + +function extractSelectorBlock(css: string, selector: string): string { + const startIdx = css.indexOf(`${selector} {`); + if (startIdx === -1) { + throw new Error(`Could not find selector block: ${selector}`); + } + + const openBraceIdx = css.indexOf("{", startIdx); + let depth = 1; + let end = openBraceIdx; + for (let i = openBraceIdx + 1; i < css.length; i++) { + if (css[i] === "{") depth++; + if (css[i] === "}") depth--; + if (depth === 0) { + end = i; + break; + } + } + + return css.slice(startIdx, end + 1); +} diff --git a/packages/dashboard/app/__tests__/shadcn-gray-theme.test.ts b/packages/dashboard/app/__tests__/shadcn-gray-theme.test.ts index 3b8a6a06b8..c30055a2bf 100644 --- a/packages/dashboard/app/__tests__/shadcn-gray-theme.test.ts +++ b/packages/dashboard/app/__tests__/shadcn-gray-theme.test.ts @@ -9,7 +9,7 @@ const dashboardIndexPath = path.resolve(__dirname, "../index.html"); /* FNXC:DashboardTheming 2026-06-20-00:00: -Shadcn Gray is a zinc-only accent variant. This test locks the dashboard wiring and neutral token intent without asserting the desktop bootstrap validator, which intentionally excludes all shadcn variants. +Shadcn Gray is a zinc-only accent variant. This test locks the dashboard wiring and neutral token intent; default-theme bootstrap coverage for the shadcn family lives in the Shadcn Ember contract test. */ describe("Shadcn Gray color theme", () => { const themeData = fs.readFileSync(themeDataPath, "utf-8"); diff --git a/packages/dashboard/app/components/ThemeSelector.css b/packages/dashboard/app/components/ThemeSelector.css index 65bf68ad40..90e5a12ff0 100644 --- a/packages/dashboard/app/components/ThemeSelector.css +++ b/packages/dashboard/app/components/ThemeSelector.css @@ -388,6 +388,14 @@ --swatch-sample-4: #27272a; } +/* FNXC:DashboardTheming 2026-06-30-00:00: Shadcn Ember is the default selector chip; it previews shadcn zinc surfaces with Ember's warm orange accent so users can distinguish the default from base Shadcn. */ +.theme-swatch-shadcn-ember { + --swatch-sample-1: #09090b; + --swatch-sample-2: #18181b; + --swatch-sample-3: #e8773a; + --swatch-sample-4: #27272a; +} + .theme-swatch-shadcn-custom { --swatch-sample-1: #09090b; --swatch-sample-2: #18181b; @@ -735,6 +743,13 @@ --swatch-sample-4: #e4e4e7; } +[data-theme="light"] .theme-swatch-shadcn-ember { + --swatch-sample-1: #ffffff; + --swatch-sample-2: #f4f4f5; + --swatch-sample-3: #d4622a; + --swatch-sample-4: #e4e4e7; +} + [data-theme="light"] .theme-swatch-shadcn-custom { --swatch-sample-1: #ffffff; --swatch-sample-2: #f4f4f5; diff --git a/packages/dashboard/app/components/ThemeSelector.tsx b/packages/dashboard/app/components/ThemeSelector.tsx index 3d08402a30..2630e9c555 100644 --- a/packages/dashboard/app/components/ThemeSelector.tsx +++ b/packages/dashboard/app/components/ThemeSelector.tsx @@ -42,7 +42,11 @@ export function ThemeSelector({ const { t } = useTranslation("app"); const handleReset = useCallback(() => { onThemeModeChange("dark"); - onColorThemeChange("ocean"); + /* + 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. + */ + onColorThemeChange("shadcn-ember"); onDashboardFontScaleChange(100); onShadcnCustomColorsChange({}); }, [onThemeModeChange, onColorThemeChange, onDashboardFontScaleChange, onShadcnCustomColorsChange]); diff --git a/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx b/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx index 4eec126610..a95ef71f60 100644 --- a/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx +++ b/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx @@ -10,12 +10,12 @@ describe("ThemeDropdown", () => { }); it("renders the current theme chip and opens all swatched theme options", () => { - render(); + render(); - const trigger = screen.getByRole("button", { name: /ocean/i }); + const trigger = screen.getByRole("button", { name: /shadcn ember/i }); expect(trigger.getAttribute("aria-expanded")).toBe("false"); - expect(within(trigger).getByText("Ocean (Default)")).toBeDefined(); - expect(trigger.querySelector(".theme-swatch-ocean")).toBeTruthy(); + expect(within(trigger).getByText("Shadcn Ember (Default)")).toBeDefined(); + expect(trigger.querySelector(".theme-swatch-shadcn-ember")).toBeTruthy(); fireEvent.click(trigger); @@ -29,6 +29,18 @@ describe("ThemeDropdown", () => { } }); + it("labels only Shadcn Ember as the default option", () => { + render(); + + expect(screen.getByRole("button", { name: /ocean/i }).textContent).toContain("Ocean"); + expect(screen.getByRole("button", { name: /ocean/i }).textContent).not.toContain("Default"); + + fireEvent.click(screen.getByRole("button", { name: /ocean/i })); + const defaultOptions = screen.getAllByRole("option").filter((option) => option.textContent?.includes("(Default)")); + expect(defaultOptions).toHaveLength(1); + expect(defaultOptions[0]).toHaveTextContent("Shadcn Ember (Default)"); + }); + it("selects themes and closes from click, escape, and outside click", () => { const onColorThemeChange = vi.fn(); render(); diff --git a/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx b/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx index 32d640b810..9ff4c09179 100644 --- a/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx +++ b/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx @@ -97,6 +97,24 @@ describe("ThemeSelector", () => { expect(screen.getByLabelText(`${defaultTheme.label} theme`).getAttribute("aria-pressed")).toBe("true"); }); + it("marks only Shadcn Ember as the default color theme label", () => { + render( + + ); + + expect(screen.getByLabelText("Shadcn Ember (Default) theme")).toBeDefined(); + expect(screen.getByLabelText("Ocean theme")).toBeDefined(); + expect(screen.queryByLabelText("Ocean (Default) theme")).toBeNull(); + expect(THEME_OPTIONS.filter((theme) => theme.label.includes("(Default)")).map((theme) => theme.value)).toEqual([ + "shadcn-ember", + ]); + }); + it("renders every shared swatch class from themeOptions", () => { render( { /> ); - const oceanBtn = screen.getByLabelText("Ocean (Default) theme"); + const oceanBtn = screen.getByLabelText("Ocean theme"); expect(oceanBtn.className).toContain("active"); expect(oceanBtn.getAttribute("aria-pressed")).toBe("true"); }); @@ -497,7 +515,7 @@ describe("ThemeSelector", () => { ); expect(screen.getByText(/Current theme/)).toBeDefined(); - expect(screen.getByText(/Dark \/ Ocean \(Default\)/)).toBeDefined(); + expect(screen.getByText(/Dark \/ Ocean/)).toBeDefined(); }); it("displays system theme in preview when system mode", () => { @@ -608,7 +626,7 @@ describe("ThemeSelector", () => { fireEvent.click(screen.getByLabelText("Reset to default theme")); expect(onThemeModeChange).toHaveBeenCalledWith("dark"); - expect(onColorThemeChange).toHaveBeenCalledWith("ocean"); + expect(onColorThemeChange).toHaveBeenCalledWith("shadcn-ember"); }); it("shows the shadcn custom picker only for shadcn-custom", () => { diff --git a/packages/dashboard/app/components/themeOptions.ts b/packages/dashboard/app/components/themeOptions.ts index ad57eba548..7474722a22 100644 --- a/packages/dashboard/app/components/themeOptions.ts +++ b/packages/dashboard/app/components/themeOptions.ts @@ -5,8 +5,8 @@ import type { ColorTheme, ThemeMode } from "@fusion/core"; FNXC:Theme 2026-06-19-12:00: The Settings theme grid and Command Center theme dropdown must share one source of truth for theme labels and swatch classes so color-chip affordances stay synchronized across both theme selectors. -FNXC:DashboardTheming 2026-06-22-18:36: -Ocean is the default theme label for new/unset users. The historical "default" id remains selectable as Fusion Legacy so users who already chose default are not silently moved to Ocean. +FNXC:DashboardTheming 2026-06-30-00:00: +Shadcn Ember is the default theme label for new/unset users. The historical "default" and "ocean" ids remain selectable so users who already chose them are not silently moved. */ export const THEME_MODES: { value: ThemeMode; label: string; icon: LucideIcon }[] = [ { value: "light", label: "Light", icon: Sun }, @@ -16,7 +16,7 @@ export const THEME_MODES: { value: ThemeMode; label: string; icon: LucideIcon }[ export const COLOR_THEMES: { value: ColorTheme; label: string; className: string }[] = [ { value: "default", label: "Fusion Legacy", className: "theme-swatch-default" }, - { value: "ocean", label: "Ocean (Default)", className: "theme-swatch-ocean" }, + { value: "ocean", label: "Ocean", className: "theme-swatch-ocean" }, { value: "forest", label: "Forest", className: "theme-swatch-forest" }, { value: "sunset", label: "Sunset", className: "theme-swatch-sunset" }, { value: "zen", label: "Zen", className: "theme-swatch-zen" }, @@ -72,6 +72,7 @@ export const COLOR_THEMES: { value: ColorTheme; label: string; className: string { value: "neon-bloom", label: "Neon Bloom", className: "theme-swatch-neon-bloom" }, { value: "sepia", label: "Sepia", className: "theme-swatch-sepia" }, { value: "shadcn", label: "Shadcn", className: "theme-swatch-shadcn" }, + { value: "shadcn-ember", label: "Shadcn Ember (Default)", className: "theme-swatch-shadcn-ember" }, { value: "shadcn-custom", label: "Shadcn Custom", className: "theme-swatch-shadcn-custom" }, { value: "shadcn-blue", label: "Shadcn Blue", className: "theme-swatch-shadcn-blue" }, { value: "shadcn-green", label: "Shadcn Green", className: "theme-swatch-shadcn-green" }, diff --git a/packages/dashboard/app/hooks/__tests__/useTheme.test.ts b/packages/dashboard/app/hooks/__tests__/useTheme.test.ts index b8460a94d3..4a77c035fc 100644 --- a/packages/dashboard/app/hooks/__tests__/useTheme.test.ts +++ b/packages/dashboard/app/hooks/__tests__/useTheme.test.ts @@ -104,7 +104,7 @@ describe("useTheme", () => { const { result } = renderHook(() => useTheme()); expect(result.current.themeMode).toBe("dark"); - expect(result.current.colorTheme).toBe("ocean"); + expect(result.current.colorTheme).toBe("shadcn-ember"); }); it("initializes from localStorage", () => { @@ -125,6 +125,14 @@ describe("useTheme", () => { expect(result.current.colorTheme).toBe("default"); }); + it("preserves explicit ocean color theme from localStorage", () => { + localStorageMock[COLOR_THEME_STORAGE_KEY] = "ocean"; + + const { result } = renderHook(() => useTheme()); + + expect(result.current.colorTheme).toBe("ocean"); + }); + it("hydrates themeMode from backend on mount", async () => { mockFetchGlobalSettings.mockResolvedValue({ themeMode: "light" }); @@ -143,7 +151,7 @@ describe("useTheme", () => { const { result } = renderHook(() => useTheme()); - expect(result.current.colorTheme).toBe("ocean"); + expect(result.current.colorTheme).toBe("shadcn-ember"); await waitFor(() => { expect(result.current.colorTheme).toBe("forest"); @@ -769,7 +777,7 @@ describe("useTheme", () => { const { result } = renderHook(() => useTheme()); - expect(result.current.colorTheme).toBe("ocean"); + expect(result.current.colorTheme).toBe("shadcn-ember"); }); it("clamps invalid dashboard font scale values from localStorage", () => { @@ -797,7 +805,7 @@ describe("useTheme", () => { const { result } = renderHook(() => useTheme()); expect(result.current.themeMode).toBe("dark"); - expect(result.current.colorTheme).toBe("ocean"); + expect(result.current.colorTheme).toBe("shadcn-ember"); }); describe("dynamic theme-data.css loading", () => { @@ -881,7 +889,7 @@ describe("getThemeInitScript", () => { }); expect(script).toContain("validThemes"); expect(script).toContain("if (colorTheme === 'shadcn-mono') colorTheme = 'shadcn-mono-red';"); - expect(script).toContain("colorTheme = 'ocean'"); + expect(script).toContain("colorTheme = 'shadcn-ember'"); }); it("keeps index.html inline theme validation in sync with supported themes", () => { diff --git a/packages/dashboard/app/hooks/useTheme.ts b/packages/dashboard/app/hooks/useTheme.ts index aad8683b28..3ed34212c1 100644 --- a/packages/dashboard/app/hooks/useTheme.ts +++ b/packages/dashboard/app/hooks/useTheme.ts @@ -16,7 +16,7 @@ const DEFAULT_FONT_SCALE_PCT = 100; const MIN_FONT_SCALE_PCT = 85; const MAX_FONT_SCALE_PCT = 125; const VALID_COLOR_THEMES = [...COLOR_THEMES] satisfies ColorTheme[]; -const DEFAULT_COLOR_THEME: ColorTheme = "ocean"; +const DEFAULT_COLOR_THEME: ColorTheme = "shadcn-ember"; const THEME_DATA_ID = "theme-data"; const THEME_DATA_FILENAME = "theme-data.css"; @@ -96,8 +96,8 @@ function readCachedColorTheme(): ColorTheme { // localStorage not available, use default } /* - FNXC:DashboardTheming 2026-06-22-18:36: - Missing/invalid cached theme resolves to Ocean for new installs, but an explicit cached "default" remains valid above and stays on Fusion Legacy. + FNXC:DashboardTheming 2026-06-30-00:00: + Missing/invalid cached theme resolves to Shadcn Ember for new installs, but explicit cached legacy ids such as "default" and "ocean" remain valid above and must not be migrated. */ return DEFAULT_COLOR_THEME; } @@ -467,7 +467,7 @@ export function getThemeInitScript(): string { var mode = localStorage.getItem('${THEME_MODE_STORAGE_KEY}') || 'dark'; var colorTheme = localStorage.getItem('${COLOR_THEME_STORAGE_KEY}') || '${DEFAULT_COLOR_THEME}'; var validThemes = ${JSON.stringify(VALID_COLOR_THEMES)}; - // FNXC:DashboardTheming 2026-06-22-18:36: Unset startup theme is Ocean; an explicit stored "default" remains the Fusion Legacy theme and must not be migrated. + // 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. // FNXC:DashboardTheming 2026-06-20-00:00: FN-6813 remaps the legacy mono id before bootstrap validation so persisted users keep the red mono accent. if (colorTheme === 'shadcn-mono') colorTheme = 'shadcn-mono-red'; if (!validThemes.includes(colorTheme)) { diff --git a/packages/dashboard/app/index.html b/packages/dashboard/app/index.html index 776bb45536..2ce1918364 100644 --- a/packages/dashboard/app/index.html +++ b/packages/dashboard/app/index.html @@ -159,13 +159,13 @@ (function() { try { var mode = localStorage.getItem('kb-dashboard-theme-mode') || 'dark'; - var colorTheme = localStorage.getItem('kb-dashboard-color-theme') || 'ocean'; - 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', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'shadcn', '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-22-18:36: Unset startup theme is Ocean; an explicit stored "default" remains Fusion Legacy and must not be migrated. + 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', '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. // FNXC:DashboardTheming 2026-06-20-00:00: FN-6813 remaps the legacy mono id before pre-hydration validation so persisted users keep the red mono accent. if (colorTheme === 'shadcn-mono') colorTheme = 'shadcn-mono-red'; if (!validThemes.includes(colorTheme)) { - colorTheme = 'ocean'; + colorTheme = 'shadcn-ember'; } var fontScale = Number(localStorage.getItem('kb-dashboard-font-scale-pct') || '100'); if (!Number.isFinite(fontScale)) { @@ -220,7 +220,7 @@ } } catch (e) { document.documentElement.setAttribute('data-theme', 'dark'); - document.documentElement.setAttribute('data-color-theme', 'ocean'); + document.documentElement.setAttribute('data-color-theme', 'shadcn-ember'); document.documentElement.style.fontSize = '100%'; } })(); diff --git a/packages/dashboard/app/public/theme-data.css b/packages/dashboard/app/public/theme-data.css index 1ebaecb934..b734712723 100644 --- a/packages/dashboard/app/public/theme-data.css +++ b/packages/dashboard/app/public/theme-data.css @@ -1428,6 +1428,134 @@ FN-6758 makes the default shadcn highlight/accent orange for focus rings, active --accent-text: #ffffff; } +/* +FNXC:DashboardTheming 2026-06-30-00:00: +Shadcn Ember combines shadcn/ui zinc structure with Ember's warm orange/graphite CTA, status, accent, and info colors. It is the default for unset installs while explicit legacy theme ids stay valid. +*/ +[data-color-theme="shadcn-ember"] { + --bg: #09090b; + --surface: #0c0c0e; + --card: #18181b; + --card-hover: #1f1f23; + --surface-hover: color-mix(in srgb, var(--surface) 90%, var(--text) 10%); + --border: #27272a; + + --text: #fafafa; + --text-muted: #a1a1aa; + --text-dim: #52525b; + + --todo: #a0a0a0; + --in-progress: #b8b8b8; + --in-progress-rgb: 184, 184, 184; + --in-review: #d0d0d0; + --triage: #808080; + --done: #686868; + + --color-success: #d0d0d0; + --color-warning: #f59e0b; + --color-error: #ff6b6b; + --color-muted: #71717a; + + --font-primary: "Geist", "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; + --font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace; + + --space-xs: 4px; + --space-sm: 6px; + --space-md: 10px; + --space-lg: 16px; + --space-xl: 20px; + --space-2xl: 28px; + + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-xl: 12px; + --radius: var(--radius-md); + + --btn-padding: 6px 12px; + --btn-border-width: 1px; + --card-padding: 10px 12px; + --modal-padding: var(--space-md) var(--space-lg); + --header-padding: var(--space-sm) var(--space-lg); + --column-gap: var(--space-md); + --board-padding: var(--space-md) var(--space-lg); + + --shadow-sm: 0 1px 2px color-mix(in srgb, #000000 18%, transparent); + --shadow-md: 0 1px 3px color-mix(in srgb, #000000 22%, transparent); + --shadow-lg: 0 4px 12px color-mix(in srgb, #000000 24%, transparent); + --shadow-glow: none; + --glow-success: none; + --glow-warning: none; + --glow-danger: none; + --focus-ring: 0 0 0 calc(var(--btn-border-width) * 3) color-mix(in srgb, var(--accent) 35%, transparent); + --focus-ring-strong: 0 0 0 calc(var(--btn-border-width) * 3) color-mix(in srgb, var(--accent) 45%, transparent); + --shadow: var(--shadow-lg); + + --transition-instant: 0.05s ease; + --transition-fast: 0.1s ease; + --transition-normal: 0.15s ease; + --transition-slow: 0.2s ease; + + --cta-bg: #d4622a; + --cta-border: #e8773a; + --cta-text: #fff; + --cta-bg-hover: #e8773a; + --cta-border-hover: #f09050; + --cta-glow: 0 0 8px color-mix(in srgb, var(--cta-border) 30%, transparent); + --logo-accent: #e8773a; + --color-info: #e8773a; + --accent: #e8773a; + --accent-text: #ffffff; +} + +[data-color-theme="shadcn-ember"][data-theme="light"] { + --bg: #ffffff; + --surface: #ffffff; + --card: #ffffff; + --card-hover: #f4f4f5; + --surface-hover: color-mix(in srgb, var(--surface) 92%, var(--text) 8%); + --border: #e4e4e7; + + --text: #09090b; + --text-muted: #71717a; + --text-dim: #a1a1aa; + + --todo: #404040; + --in-progress: #606060; + --in-progress-rgb: 96, 96, 96; + --in-review: #606060; + --triage: #2a2a2a; + --done: #606060; + + --color-success: #404040; + --color-warning: #d97706; + --color-error: #dc2626; + --color-muted: #71717a; + + --shadow-sm: 0 1px 2px color-mix(in srgb, var(--text) 8%, transparent); + --shadow-md: 0 1px 3px color-mix(in srgb, var(--text) 10%, transparent); + --shadow-lg: 0 4px 12px color-mix(in srgb, var(--text) 12%, transparent); + --shadow-glow: none; + --glow-success: none; + --glow-warning: none; + --glow-danger: none; + --focus-ring: 0 0 0 calc(var(--btn-border-width) * 3) color-mix(in srgb, var(--accent) 18%, transparent); + --focus-ring-strong: 0 0 0 calc(var(--btn-border-width) * 3) color-mix(in srgb, var(--accent) 28%, transparent); + --shadow: var(--shadow-lg); + + --cta-bg: #c05820; + --cta-border: #d4622a; + --cta-text: #fff; + --cta-bg-hover: #d4622a; + --cta-border-hover: #e07030; + --cta-glow: 0 0 8px color-mix(in srgb, var(--cta-bg) 30%, transparent); + --logo-accent: #d4622a; + --color-info: #c05820; + --accent: #d4622a; + --accent-text: #ffffff; +} + + /* FNXC:Theming 2026-06-20-18:30: FN-6816 keeps shadcn-custom visually identical to the base shadcn theme until sanitized user token overrides are applied inline by the dashboard theme hook. @@ -1556,6 +1684,7 @@ FN-6816 keeps shadcn-custom visually identical to the base shadcn theme until sa } [data-color-theme="shadcn"] .card.agent-active, +[data-color-theme="shadcn-ember"] .card.agent-active, [data-color-theme="shadcn-custom"] .card.agent-active { border-color: var(--accent); box-shadow: none; @@ -1563,6 +1692,7 @@ FN-6816 keeps shadcn-custom visually identical to the base shadcn theme until sa } [data-color-theme="shadcn"][data-theme="light"] .card.agent-active, +[data-color-theme="shadcn-ember"][data-theme="light"] .card.agent-active, [data-color-theme="shadcn-custom"][data-theme="light"] .card.agent-active { border-color: var(--accent); box-shadow: none; @@ -1570,6 +1700,7 @@ FN-6816 keeps shadcn-custom visually identical to the base shadcn theme until sa } [data-color-theme="shadcn"] .btn, +[data-color-theme="shadcn-ember"] .btn, [data-color-theme="shadcn-custom"] .btn { text-transform: none; letter-spacing: normal; @@ -1578,6 +1709,8 @@ FN-6816 keeps shadcn-custom visually identical to the base shadcn theme until sa [data-color-theme="shadcn"] .btn-primary, [data-color-theme="shadcn"] .btn-task-create, +[data-color-theme="shadcn-ember"] .btn-primary, +[data-color-theme="shadcn-ember"] .btn-task-create, [data-color-theme="shadcn-custom"] .btn-primary, [data-color-theme="shadcn-custom"] .btn-task-create { background: var(--cta-bg); @@ -1588,6 +1721,8 @@ FN-6816 keeps shadcn-custom visually identical to the base shadcn theme until sa [data-color-theme="shadcn"] .btn-primary:hover, [data-color-theme="shadcn"] .btn-task-create:hover, +[data-color-theme="shadcn-ember"] .btn-primary:hover, +[data-color-theme="shadcn-ember"] .btn-task-create:hover, [data-color-theme="shadcn-custom"] .btn-primary:hover, [data-color-theme="shadcn-custom"] .btn-task-create:hover { background: var(--cta-bg-hover); @@ -1598,6 +1733,8 @@ FN-6816 keeps shadcn-custom visually identical to the base shadcn theme until sa [data-color-theme="shadcn"] .card, [data-color-theme="shadcn"] .column, +[data-color-theme="shadcn-ember"] .card, +[data-color-theme="shadcn-ember"] .column, [data-color-theme="shadcn-custom"] .card, [data-color-theme="shadcn-custom"] .column { border-width: var(--btn-border-width); diff --git a/packages/desktop/src/renderer/index.html b/packages/desktop/src/renderer/index.html index dda9e51ed0..2872f5be34 100644 --- a/packages/desktop/src/renderer/index.html +++ b/packages/desktop/src/renderer/index.html @@ -9,7 +9,7 @@ (function () { try { var mode = localStorage.getItem("kb-dashboard-theme-mode") || "dark"; - var colorTheme = localStorage.getItem("kb-dashboard-color-theme") || "default"; + var colorTheme = localStorage.getItem("kb-dashboard-color-theme") || "shadcn-ember"; var validThemes = [ "default", "ocean", @@ -27,6 +27,7 @@ "silver", "solarized", "factory", + "factory-mono", "ayu", "one-dark", "nord", @@ -42,6 +43,20 @@ "palenight", "monokai-pro", "slime", + "brutalist", + "neon-city", + "parchment", + "terminal", + "glass", + "horizon", + "vitesse", + "outrun", + "snazzy", + "porple", + "espresso", + "mars", + "poimandres", + "ember", "rust", "copper", "foundry", @@ -52,10 +67,31 @@ "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: Desktop pre-hydration mirrors the full core theme union before falling back to Shadcn Ember, so every explicit legacy id stays valid on native startup. if (!validThemes.includes(colorTheme)) { - colorTheme = "default"; + colorTheme = "shadcn-ember"; } var systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches; @@ -64,7 +100,7 @@ document.documentElement.setAttribute("data-color-theme", colorTheme); } catch (error) { document.documentElement.setAttribute("data-theme", "dark"); - document.documentElement.setAttribute("data-color-theme", "default"); + document.documentElement.setAttribute("data-color-theme", "shadcn-ember"); } })();