From 5931e10345ec5b15fa01e44107ef71d11bf21edb Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 2 Aug 2026 19:21:50 -0700 Subject: [PATCH] FN-8730: add Midnight dashboard theme Add a persisted Midnight palette with consistent first-paint behavior across dashboard surfaces. - Register Midnight in shared theme metadata and web/Electron startup validators - Define light and dark Midnight tokens plus selector swatches - Cover selection and bootstrap validation, and document the new preset Files changed: .changeset/fn-8730-midnight-theme.md | 7 ++ docs/dashboard-guide.md | 5 +- docs/settings-reference.md | 2 +- packages/core/src/types/execution-and-ui.ts | 5 ++ .../dashboard/app/__tests__/midnight-theme.test.ts | 94 ++++++++++++++++++++++ .../dashboard/app/components/ThemeSelector.css | 14 ++++ .../components/__tests__/ThemeDropdown.test.tsx | 54 ++++++++++++- packages/dashboard/app/components/themeOptions.ts | 1 + packages/dashboard/app/index.html | 3 +- packages/dashboard/app/public/theme-data.css | 86 +++++++++++++++++++- packages/desktop/src/renderer/index.html | 2 + 11 files changed, 267 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-8730 Fusion-Task-Lineage: 8e2d81bc-37f1-4381-8985-01e83cad06bd Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-8730-midnight-theme.md | 7 ++ docs/dashboard-guide.md | 5 +- docs/settings-reference.md | 2 +- packages/core/src/types/execution-and-ui.ts | 5 + .../app/__tests__/midnight-theme.test.ts | 94 +++++++++++++++++++ .../app/components/ThemeSelector.css | 14 +++ .../__tests__/ThemeDropdown.test.tsx | 54 ++++++++++- .../dashboard/app/components/themeOptions.ts | 1 + packages/dashboard/app/index.html | 3 +- packages/dashboard/app/public/theme-data.css | 86 ++++++++++++++++- packages/desktop/src/renderer/index.html | 2 + 11 files changed, 267 insertions(+), 6 deletions(-) create mode 100644 .changeset/fn-8730-midnight-theme.md create mode 100644 packages/dashboard/app/__tests__/midnight-theme.test.ts diff --git a/.changeset/fn-8730-midnight-theme.md b/.changeset/fn-8730-midnight-theme.md new file mode 100644 index 0000000000..f21bc1d898 --- /dev/null +++ b/.changeset/fn-8730-midnight-theme.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add the Midnight deep-navy dashboard theme with light and dark modes. +category: feature +dev: Registers the persisted theme across dashboard and Electron first-paint validation. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 20474049b1..a02b14d884 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -2116,11 +2116,12 @@ Tokens in the table are stable. Renaming or removing one requires a deprecation + -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. 87 color themes via `data-color-theme` (lazy-loaded from `app/public/theme-data.css`), including Cobalt (saturated blue), Clay (warm terracotta), Moss (muted forest green), Aurora (navy/teal/violet dark surfaces with a misty blue-white light counterpart), Calm (low-stimulation slate/sage dark surfaces with a misty blue-white light counterpart), Dawn (indigo/plum dark surfaces with muted amber accents and a dawn-white light counterpart), Sage (soft green-gray surfaces with a muted olive accent and readable dark/light ramps), Factory Dark (graphite/steel dark surfaces, cool gunmetal borders, and restrained safety-orange accents with a concrete-gray light counterpart), and Factory Light (bright concrete/warm-white surfaces, steel-gray borders, a darkened safety-orange accent, and a warm-graphite dark counterpart), 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 and Shadcn Mono Red/Blue/Green/Purple/Pink/Orange/Yellow (grayscale surfaces with color-specific accents; Shadcn Mono retains its selected id), 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. 88 color themes via `data-color-theme` (lazy-loaded from `app/public/theme-data.css`), including Cobalt (saturated blue), Clay (warm terracotta), Moss (muted forest green), Aurora (navy/teal/violet dark surfaces with a misty blue-white light counterpart), Calm (low-stimulation slate/sage dark surfaces with a misty blue-white light counterpart), Dawn (indigo/plum dark surfaces with muted amber accents and a dawn-white light counterpart), Sage (soft green-gray surfaces with a muted olive accent and readable dark/light ramps), Midnight (deep-navy surfaces with restrained violet/indigo accents and a cool pale light counterpart), Factory Dark (graphite/steel dark surfaces, cool gunmetal borders, and restrained safety-orange accents with a concrete-gray light counterpart), and Factory Light (bright concrete/warm-white surfaces, steel-gray borders, a darkened safety-orange accent, and a warm-graphite dark counterpart), 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 and Shadcn Mono Red/Blue/Green/Purple/Pink/Orange/Yellow (grayscale surfaces with color-specific accents; Shadcn Mono retains its selected id), 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 color themes from **Settings → Appearance** or from the Command Center **Overview** theme card. Settings merges its selector into the current-theme row, which opens the full color-theme list; Command Center keeps the compact `ThemeDropdown` trigger. Both use the same filterable list: type a visible theme name to narrow the displayed labels and color-chip swatches, then select an option to change the theme. Filtering alone never changes the chosen theme. 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. +Choose color themes from **Settings → Appearance** or from the Command Center **Overview** theme card. Settings merges its selector into the current-theme row, which opens the full color-theme list; Command Center keeps the compact `ThemeDropdown` trigger. Both use the same filterable list: type a visible theme name to narrow the displayed labels and color-chip swatches, then select an option to change the theme. Filtering alone never changes the chosen theme. The selected color theme, including Midnight, persists in browser storage and is validated before React hydrates on both web and Electron startup. 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. - **Base tokens** (`--bg`, `--surface`, etc.) — redefine in `:root`, `[data-theme="light"]`, and every theme block. - **Semantic tokens** (`--autopilot-pulse`, `--event-error-text`, `--badge-mission-*`, `--fab-*`) — `:root` + `[data-theme="light"]` only; no per-color-theme overrides. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index 87305739ba..63d9d2f6f7 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -67,7 +67,7 @@ Defaults from `DEFAULT_GLOBAL_SETTINGS`; key scope from `GLOBAL_SETTINGS_KEYS`. |---|---|---:|---| | `anthropicAuthPreference` | `"api-key" \| "subscription"` | `"api-key"` | Which Anthropic credential wins when BOTH a raw API key and a Claude subscription OAuth login are configured. `"api-key"` keeps the historical precedence; `"subscription"` moves the raw key below the OAuth steps so a stale or revoked saved key cannot shadow a working subscription (the `401 invalid x-api-key` failure mode). Global, because credentials live in the global `~/.fusion/agent/auth.json`. Never removes a source — with one credential configured, resolution reaches it under either value. Surfaced in Settings → Authentication, where the two Anthropic cards show which credential is in use; the control appears only when both are connected. | | `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. Aurora is a built-in navy/teal/violet palette with a misty light counterpart; Calm is a low-stimulation slate/sage palette with a misty light counterpart; Dawn uses indigo/plum dark surfaces, amber accents, and dawn-white light surfaces; Factory Dark uses graphite/steel surfaces, cool gunmetal borders, and restrained safety-orange accents with a concrete-gray light counterpart; Factory Light uses bright concrete/warm-white surfaces, steel-gray borders, a darkened safety-orange accent, and a warm-graphite dark counterpart. Use `"shadcn-custom"` to show the separate custom shadcn color picker in Settings → Appearance and the Command Center theme card. | +| `colorTheme` | `ColorTheme` | `"shadcn-ember"` | Dashboard color theme preset. Aurora is a built-in navy/teal/violet palette with a misty light counterpart; Calm is a low-stimulation slate/sage palette with a misty light counterpart; Dawn uses indigo/plum dark surfaces, amber accents, and dawn-white light surfaces; Midnight uses deep-navy surfaces with restrained violet/indigo accents and a cool pale light counterpart; Factory Dark uses graphite/steel surfaces, cool gunmetal borders, and restrained safety-orange accents with a concrete-gray light counterpart; Factory Light uses bright concrete/warm-white surfaces, steel-gray borders, a darkened safety-orange accent, and a warm-graphite dark counterpart. Use `"shadcn-custom"` to show the separate 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/types/execution-and-ui.ts b/packages/core/src/types/execution-and-ui.ts index d2ae41da1e..c28028d5ed 100644 --- a/packages/core/src/types/execution-and-ui.ts +++ b/packages/core/src/types/execution-and-ui.ts @@ -144,6 +144,11 @@ export const COLOR_THEMES = [ "dawn", // FNXC:DashboardTheming 2026-07-31-21:28: Sage must stay order-synchronized across core, selector metadata, and both first-paint validators so saved preferences survive pre-hydration validation. "sage", + /* + FNXC:MidnightTheme 2026-08-03-02:05: + Midnight is a persisted selectable dashboard theme. Keep its id and order synchronized with selector metadata and both web/Electron first-paint validators so saved choices survive before hydration, while shadcn-ember remains the unset default. + */ + "midnight", // FNXC:DashboardTheming 2026-07-31-20:39: Factory Dark must stay order-synchronized across core, selector metadata, and both first-paint validators so saved preferences survive pre-hydration validation. "factory-dark", // FNXC:DashboardTheming 2026-07-31-23:51: Factory Light must stay order-synchronized across core, selector metadata, and both first-paint validators so saved preferences survive pre-hydration validation. diff --git a/packages/dashboard/app/__tests__/midnight-theme.test.ts b/packages/dashboard/app/__tests__/midnight-theme.test.ts new file mode 100644 index 0000000000..a91e028c53 --- /dev/null +++ b/packages/dashboard/app/__tests__/midnight-theme.test.ts @@ -0,0 +1,94 @@ +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 { readAppFile } from "../test/cssFixture"; + +/* +FNXC:MidnightTheme 2026-08-03-02:05: +Midnight is complete only when its persisted id, selector metadata, web and Electron first-paint registries, readable mode tokens, and global swatches agree. This contract prevents saved choices from flashing, falling back, or rendering blank previews. +*/ +describe("Midnight color theme", () => { + const themeData = readAppFile("public/theme-data.css"); + const themeSelector = readAppFile("components/ThemeSelector.css"); + const dashboardIndexHtml = readAppFile("index.html"); + const desktopIndexHtml = readAppFile("../../desktop/src/renderer/index.html"); + + it("keeps persisted, selector, and first-paint registries in exact order", () => { + const coreIds = [...CORE_COLOR_THEMES]; + const dashboardIds = DASHBOARD_COLOR_THEMES.map((theme) => theme.value); + const dashboardValidThemes = extractValidThemes(dashboardIndexHtml); + const desktopValidThemes = extractValidThemes(desktopIndexHtml); + + expect(CORE_COLOR_THEMES.filter((theme) => theme === "midnight")).toHaveLength(1); + expect(DASHBOARD_COLOR_THEMES).toContainEqual({ + value: "midnight", + label: "Midnight", + className: "theme-swatch-midnight", + }); + expect(dashboardIds).toEqual(coreIds); + expect(dashboardValidThemes).toEqual(coreIds); + expect(desktopValidThemes).toEqual(coreIds); + for (const ids of [coreIds, dashboardIds, dashboardValidThemes, desktopValidThemes]) { + expect(new Set(ids).size).toBe(ids.length); + } + expect(dashboardIndexHtml).toContain("colorTheme = 'shadcn-ember'"); + expect(desktopIndexHtml).toContain('colorTheme = "shadcn-ember"'); + }); + + it("defines complete readable dark and light Midnight token blocks", () => { + const darkBlock = extractSelectorBlock(themeData, '[data-color-theme="midnight"]'); + const lightBlock = extractSelectorBlock(themeData, '[data-color-theme="midnight"][data-theme="light"]'); + const requiredTokens = [ + "--bg:", "--surface:", "--card:", "--card-hover:", "--surface-hover:", "--border:", "--text:", + "--color-success:", "--color-warning:", "--color-error:", "--color-info:", "--cta-bg:", "--cta-text:", + "--accent:", "--accent-text:", "--focus-ring:", "--focus-ring-strong:", "--shadow-glow:", + ]; + + for (const block of [darkBlock, lightBlock]) { + for (const token of requiredTokens) expect(block).toContain(token); + } + expect(darkBlock).toContain("--bg: #0b1026;"); + expect(darkBlock).toContain("--accent: #a78bfa;"); + expect(darkBlock).toContain("--cta-text: #ffffff;"); + expect(lightBlock).toContain("--bg: #f5f6ff;"); + expect(lightBlock).toContain("--accent: #5134a3;"); + expect(lightBlock).toContain("--cta-text: #ffffff;"); + }); + + it("uses mode-specific global Midnight preview properties for an unselected swatch", () => { + const darkGlobals = extractSelectorBlock(themeData, ":root"); + const lightGlobals = extractSelectorBlock(themeData, '[data-theme="light"]'); + const darkSwatch = extractSelectorBlock(themeSelector, ".theme-swatch-midnight"); + const lightSwatch = extractSelectorBlock(themeSelector, '[data-theme="light"] .theme-swatch-midnight'); + + for (const block of [darkGlobals, lightGlobals]) { + for (const sample of [1, 2, 3, 4]) expect(block).toContain(`--midnight-swatch-sample-${sample}:`); + } + for (const block of [darkSwatch, lightSwatch]) { + for (const sample of [1, 2, 3, 4]) { + expect(block).toContain(`--swatch-sample-${sample}: var(--midnight-swatch-sample-${sample});`); + } + expect(block).not.toContain("var(--accent)"); + expect(block).not.toContain("var(--bg)"); + } + }); +}); + +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 [...match[1].matchAll(/["']([^"']+)["']/g)].map((themeMatch) => themeMatch[1]); +} + +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; + for (let index = openBraceIdx + 1; index < css.length; index++) { + if (css[index] === "{") depth++; + if (css[index] === "}") depth--; + if (depth === 0) return css.slice(startIdx, index + 1); + } + throw new Error(`Could not find closing brace for selector block: ${selector}`); +} diff --git a/packages/dashboard/app/components/ThemeSelector.css b/packages/dashboard/app/components/ThemeSelector.css index a6bb1faeb3..4aa6418b44 100644 --- a/packages/dashboard/app/components/ThemeSelector.css +++ b/packages/dashboard/app/components/ThemeSelector.css @@ -312,6 +312,13 @@ --swatch-sample-4: #72c7a1; } +.theme-swatch-midnight { + --swatch-sample-1: var(--midnight-swatch-sample-1); + --swatch-sample-2: var(--midnight-swatch-sample-2); + --swatch-sample-3: var(--midnight-swatch-sample-3); + --swatch-sample-4: var(--midnight-swatch-sample-4); +} + .theme-swatch-aurora { --swatch-sample-1: var(--aurora-swatch-sample-1); --swatch-sample-2: var(--aurora-swatch-sample-2); @@ -1181,6 +1188,13 @@ --swatch-sample-4: #397c49; } +[data-theme="light"] .theme-swatch-midnight { + --swatch-sample-1: var(--midnight-swatch-sample-1); + --swatch-sample-2: var(--midnight-swatch-sample-2); + --swatch-sample-3: var(--midnight-swatch-sample-3); + --swatch-sample-4: var(--midnight-swatch-sample-4); +} + [data-theme="light"] .theme-swatch-aurora { --swatch-sample-1: var(--aurora-swatch-sample-1); --swatch-sample-2: var(--aurora-swatch-sample-2); diff --git a/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx b/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx index f70ebf3263..665c961cf8 100644 --- a/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx +++ b/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx @@ -6,7 +6,7 @@ import { ThemeDropdown } from "../ThemeDropdown"; import { readAppFile } from "../../test/cssFixture"; // FNXC:Theme 2026-07-16-14:30: FN-8146 pins the historical Settings-grid set, including restored shadcn-mono, so a removal from COLOR_THEMES cannot make the all-themes checks pass circularly. -const EXPECTED_THEME_IDS = ['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', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', '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'] as const; +const EXPECTED_THEME_IDS = ['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', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'midnight', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', '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'] as const; function renderedThemeIds(listbox: HTMLElement) { return within(listbox).getAllByRole("option").map((option) => { @@ -136,6 +136,58 @@ describe("ThemeDropdown", () => { previewTokens.remove(); }); + it.each([ + ["compact", "compact" as const, /ocean/i], + ["current-row", "current-row" as const, /current theme system \/ ocean/i], + ])("searches, previews, and selects Midnight from the shared %s trigger", (_label, triggerVariant, triggerName) => { + Object.defineProperty(window, "innerWidth", { configurable: true, value: 390, writable: true }); + window.dispatchEvent(new Event("resize")); + document.documentElement.setAttribute("data-color-theme", "ocean"); + document.documentElement.setAttribute("data-theme", "light"); + const previewTokens = document.createElement("style"); + previewTokens.textContent = ` + :root { --midnight-swatch-sample-1: #0b1026; --midnight-swatch-sample-2: #1b2450; --midnight-swatch-sample-3: #a78bfa; --midnight-swatch-sample-4: #67d4e8; } + [data-theme="light"] { --midnight-swatch-sample-1: #f5f6ff; --midnight-swatch-sample-2: #e2e6fa; --midnight-swatch-sample-3: #5134a3; --midnight-swatch-sample-4: #16778c; } + `; + document.head.appendChild(previewTokens); + const onColorThemeChange = vi.fn(); + + const { rerender } = render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: triggerName })); + const filter = screen.getByRole("searchbox", { name: /filter color themes/i }); + fireEvent.change(filter, { target: { value: "Midnight" } }); + const midnightOption = screen.getByRole("option", { name: "Midnight" }); + expect(screen.getAllByRole("option")).toHaveLength(1); + expect(midnightOption.querySelector(".theme-swatch-midnight")).toBeTruthy(); + expect(midnightOption.querySelectorAll(".theme-option-swatch-sample")).toHaveLength(4); + for (const sample of [1, 2, 3, 4]) { + expect(getComputedStyle(document.documentElement).getPropertyValue(`--midnight-swatch-sample-${sample}`).trim()).not.toBe(""); + } + fireEvent.click(midnightOption); + expect(onColorThemeChange).toHaveBeenCalledWith("midnight"); + + rerender( + , + ); + expect(screen.getByRole("button", { name: /midnight/i })).toHaveTextContent("Midnight"); + previewTokens.remove(); + }); + it("selects themes and closes from click, escape, and outside click", () => { const onColorThemeChange = vi.fn(); render(); diff --git a/packages/dashboard/app/components/themeOptions.ts b/packages/dashboard/app/components/themeOptions.ts index bd7568fa83..9c1e98818d 100644 --- a/packages/dashboard/app/components/themeOptions.ts +++ b/packages/dashboard/app/components/themeOptions.ts @@ -79,6 +79,7 @@ export const COLOR_THEMES: { value: ColorTheme; label: string; className: string { value: "calm", label: "Calm", className: "theme-swatch-calm" }, { value: "dawn", label: "Dawn", className: "theme-swatch-dawn" }, { value: "sage", label: "Sage", className: "theme-swatch-sage" }, + { value: "midnight", label: "Midnight", className: "theme-swatch-midnight" }, { value: "factory-dark", label: "Factory Dark", className: "theme-swatch-factory-dark" }, { value: "factory-light", label: "Factory Light", className: "theme-swatch-factory-light" }, { value: "shadcn", label: "Shadcn", className: "theme-swatch-shadcn" }, diff --git a/packages/dashboard/app/index.html b/packages/dashboard/app/index.html index 1a33be4520..e8f574f69c 100644 --- a/packages/dashboard/app/index.html +++ b/packages/dashboard/app/index.html @@ -164,9 +164,10 @@ 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', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', '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']; + 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', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'midnight', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', '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-07-16-14:30: FN-8146 restores Shadcn Mono as a distinct persisted choice, so bootstrap must retain its stored id instead of remapping it to Mono Red. + // FNXC:MidnightTheme 2026-08-03-02:05: Web first paint must accept persisted Midnight before React hydrates; invalid or unset values still fall back to Shadcn Ember. if (!validThemes.includes(colorTheme)) { colorTheme = 'shadcn-ember'; } diff --git a/packages/dashboard/app/public/theme-data.css b/packages/dashboard/app/public/theme-data.css index b3b4d3ee87..e37171a692 100644 --- a/packages/dashboard/app/public/theme-data.css +++ b/packages/dashboard/app/public/theme-data.css @@ -8,7 +8,7 @@ Theme token translucency uses color-mix(in srgb, var(--token) N%, transparent) or exact literal color-mix values for glass surfaces. Raw RGB alpha color functions are banned outside var() fallbacks by FN-6489. ============================================================ */ -/* Aurora, Calm, Dawn, Sage, Factory Dark, and Factory Light swatches stay globally resolvable while a different theme is active. */ +/* Aurora, Calm, Dawn, Sage, Midnight, Factory Dark, and Factory Light swatches stay globally resolvable while a different theme is active. */ :root { --aurora-swatch-sample-1: #0a1022; --aurora-swatch-sample-2: #162044; @@ -26,6 +26,10 @@ --sage-swatch-sample-2: #27382e; --sage-swatch-sample-3: #9ab58a; --sage-swatch-sample-4: #8baaa0; + --midnight-swatch-sample-1: #0b1026; + --midnight-swatch-sample-2: #1b2450; + --midnight-swatch-sample-3: #a78bfa; + --midnight-swatch-sample-4: #67d4e8; --factory-dark-swatch-sample-1: #0d0f10; --factory-dark-swatch-sample-2: #1a1d1f; --factory-dark-swatch-sample-3: #e89532; @@ -53,6 +57,10 @@ --sage-swatch-sample-2: #e2eadf; --sage-swatch-sample-3: #55764d; --sage-swatch-sample-4: #46756b; + --midnight-swatch-sample-1: #f5f6ff; + --midnight-swatch-sample-2: #e2e6fa; + --midnight-swatch-sample-3: #5134a3; + --midnight-swatch-sample-4: #16778c; --factory-dark-swatch-sample-1: #eceef0; --factory-dark-swatch-sample-2: #d9dde0; --factory-dark-swatch-sample-3: #a95612; @@ -7428,6 +7436,82 @@ FN-8151 adds Cobalt, Clay, and Moss with complete dark and light token ramps. Ke FNXC:DashboardTheming 2026-07-20-00:00: Aurora gives operators navy/teal/violet dark surfaces and misty light surfaces. Literal palette values stay in these canonical tokens, while globally defined swatch variables keep the unselected option accurate in either mode. */ +/* +FNXC:MidnightTheme 2026-08-03-02:05: +Midnight gives operators deep-navy surfaces with restrained violet/indigo accents in dark mode and a cool pale counterpart in light mode. These complete token blocks preserve distinct semantic statuses, AA-readable text and CTA pairs, and globally resolvable swatch previews for unselected selector options. +*/ +[data-color-theme="midnight"] { + --bg: #0b1026; + --surface: #131a38; + --card: #1b2450; + --card-hover: #273164; + --surface-hover: color-mix(in srgb, var(--surface) 90%, var(--text) 10%); + --border: #3b477e; + --text: #f0f2ff; + --text-muted: #bdc5ea; + --text-dim: #8490bc; + + --todo: #98adff; + --in-progress: #67d4e8; + --in-progress-rgb: 103, 212, 232; + --in-review: #b69cff; + --triage: #f3bd66; + --done: #8b9bc7; + --color-success: #72d6ad; + --color-warning: #f3bd66; + --color-error: #fa8499; + --color-info: #76b9ff; + + --cta-bg: #6648c5; + --cta-border: #a78bfa; + --cta-text: #ffffff; + --cta-bg-hover: #795dd5; + --cta-border-hover: #c4b4ff; + --cta-glow: 0 0 8px color-mix(in srgb, var(--cta-border) 30%, transparent); + --accent: #a78bfa; + --accent-text: #171035; + --logo-accent: var(--accent); + --shadow-glow: 0 0 12px color-mix(in srgb, var(--accent) 28%, transparent); + --focus-ring: 0 0 0 2px color-mix(in srgb, var(--accent) 24%, transparent); + --focus-ring-strong: 0 0 0 2px color-mix(in srgb, var(--accent) 36%, transparent); +} + +[data-color-theme="midnight"][data-theme="light"] { + --bg: #f5f6ff; + --surface: #e9ecfa; + --card: #ffffff; + --card-hover: #dde2f6; + --surface-hover: color-mix(in srgb, var(--surface) 92%, var(--text) 8%); + --border: #b9c2e2; + --text: #172044; + --text-muted: #4e5d87; + --text-dim: #7381a9; + + --todo: #425cae; + --in-progress: #16778c; + --in-progress-rgb: 22, 119, 140; + --in-review: #6246ad; + --triage: #946015; + --done: #68789f; + --color-success: #247856; + --color-warning: #946015; + --color-error: #bd4058; + --color-info: #266eb1; + + --cta-bg: #5134a3; + --cta-border: #412887; + --cta-text: #ffffff; + --cta-bg-hover: #432989; + --cta-border-hover: #341f70; + --cta-glow: 0 0 8px color-mix(in srgb, var(--cta-border) 24%, transparent); + --accent: #5134a3; + --accent-text: #ffffff; + --logo-accent: var(--accent); + --shadow-glow: 0 0 10px color-mix(in srgb, var(--accent) 20%, transparent); + --focus-ring: 0 0 0 2px color-mix(in srgb, var(--accent) 18%, transparent); + --focus-ring-strong: 0 0 0 2px color-mix(in srgb, var(--accent) 28%, transparent); +} + [data-color-theme="aurora"] { --bg: #0a1022; --surface: #101a35; diff --git a/packages/desktop/src/renderer/index.html b/packages/desktop/src/renderer/index.html index 6d4d4594eb..ba20bc3e15 100644 --- a/packages/desktop/src/renderer/index.html +++ b/packages/desktop/src/renderer/index.html @@ -79,6 +79,7 @@ "calm", "dawn", "sage", + "midnight", "factory-dark", "factory-light", "shadcn", @@ -105,6 +106,7 @@ ]; // 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. + // FNXC:MidnightTheme 2026-08-03-02:05: Electron first paint must accept persisted Midnight before React hydrates; invalid or unset values still fall back to Shadcn Ember. if (!validThemes.includes(colorTheme)) { colorTheme = "shadcn-ember"; }