From bcaa48390bba7a28b96a0ced7b3c1bc4da9203af Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 31 Jul 2026 14:57:21 -0700 Subject: [PATCH] FN-8627: add Sage color theme Add the Sage palette across persisted dashboard and desktop theme selection paths. - Register Sage in core, dashboard bootstrap, desktop, and selector metadata. - Add dark and light Sage tokens plus independently resolvable swatches. - Cover registration, token, selector, and documentation updates. Files changed: .changeset/fn-8627-sage-theme.md | 7 ++ docs/dashboard-guide.md | 3 +- packages/core/src/types/execution-and-ui.ts | 2 + .../dashboard/app/__tests__/sage-theme.test.ts | 101 +++++++++++++++++++++ .../dashboard/app/components/ThemeSelector.css | 14 +++ .../components/__tests__/ThemeDropdown.test.tsx | 2 +- .../components/__tests__/ThemeSelector.test.tsx | 2 +- .../__tests__/CommandCenterControls.test.tsx | 2 +- packages/dashboard/app/components/themeOptions.ts | 1 + packages/dashboard/app/index.html | 2 +- packages/dashboard/app/public/theme-data.css | 86 +++++++++++++++++- packages/desktop/src/renderer/index.html | 1 + 12 files changed, 217 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-8627 Fusion-Task-Lineage: fd4353b3-1e0c-4c7e-84dd-bcad2815178c Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-8627-sage-theme.md | 7 ++ docs/dashboard-guide.md | 3 +- packages/core/src/types/execution-and-ui.ts | 2 + .../app/__tests__/sage-theme.test.ts | 101 ++++++++++++++++++ .../app/components/ThemeSelector.css | 14 +++ .../__tests__/ThemeDropdown.test.tsx | 2 +- .../__tests__/ThemeSelector.test.tsx | 2 +- .../__tests__/CommandCenterControls.test.tsx | 2 +- .../dashboard/app/components/themeOptions.ts | 1 + packages/dashboard/app/index.html | 2 +- packages/dashboard/app/public/theme-data.css | 86 ++++++++++++++- packages/desktop/src/renderer/index.html | 1 + 12 files changed, 217 insertions(+), 6 deletions(-) create mode 100644 .changeset/fn-8627-sage-theme.md create mode 100644 packages/dashboard/app/__tests__/sage-theme.test.ts diff --git a/.changeset/fn-8627-sage-theme.md b/.changeset/fn-8627-sage-theme.md new file mode 100644 index 0000000000..dc2d8c123c --- /dev/null +++ b/.changeset/fn-8627-sage-theme.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add the Sage color theme to dashboard theme controls. +category: feature +dev: Registers the sage id across core, selector, first-paint, token, and swatch registries. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index d9df8a5dee..cb3dd1e612 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -2109,9 +2109,10 @@ 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. 85 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), and Factory Dark (graphite/steel dark surfaces, cool gunmetal borders, and restrained safety-orange accents with a concrete-gray light 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. 86 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), and Factory Dark (graphite/steel dark surfaces, cool gunmetal borders, and restrained safety-orange accents with a concrete-gray light 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. diff --git a/packages/core/src/types/execution-and-ui.ts b/packages/core/src/types/execution-and-ui.ts index 541fb40ef0..7a5548fc6f 100644 --- a/packages/core/src/types/execution-and-ui.ts +++ b/packages/core/src/types/execution-and-ui.ts @@ -142,6 +142,8 @@ export const COLOR_THEMES = [ "calm", // FNXC:DashboardTheming 2026-07-20-00:00: Dawn is persisted immediately after Calm; keep this ID and order synchronized across core, selectors, and both first-paint validators so saved selections survive pre-hydration validation. "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: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", "shadcn", diff --git a/packages/dashboard/app/__tests__/sage-theme.test.ts b/packages/dashboard/app/__tests__/sage-theme.test.ts new file mode 100644 index 0000000000..bfbc3aca2f --- /dev/null +++ b/packages/dashboard/app/__tests__/sage-theme.test.ts @@ -0,0 +1,101 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +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"; + +const themeDataPath = path.resolve(__dirname, "../public/theme-data.css"); +const themeSelectorPath = path.resolve(__dirname, "../components/ThemeSelector.css"); +const dashboardIndexPath = path.resolve(__dirname, "../index.html"); +const desktopIndexPath = path.resolve(__dirname, "../../../desktop/src/renderer/index.html"); + +/* +FNXC:DashboardTheming 2026-07-31-21:28: +Sage is valid only when persistence, first-paint validators, selector metadata, tokens, and globally resolvable previews agree. This source contract catches a partial registration before a saved operator preference can flash or fall back. +*/ +describe("Sage color theme", () => { + const themeData = readFileSync(themeDataPath, "utf-8"); + const themeSelector = readFileSync(themeSelectorPath, "utf-8"); + const dashboardIndexHtml = readFileSync(dashboardIndexPath, "utf-8"); + const desktopIndexHtml = readFileSync(desktopIndexPath, "utf-8"); + + 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 === "sage")).toHaveLength(1); + expect(DASHBOARD_COLOR_THEMES).toContainEqual({ + value: "sage", + label: "Sage", + className: "theme-swatch-sage", + }); + 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 Sage token blocks", () => { + const darkBlock = extractSelectorBlock(themeData, '[data-color-theme="sage"]'); + const lightBlock = extractSelectorBlock(themeData, '[data-color-theme="sage"][data-theme="light"]'); + const requiredTokens = [ + "--bg:", "--surface:", "--card:", "--card-hover:", "--surface-hover:", "--border:", + "--text:", "--text-muted:", "--text-dim:", "--todo:", "--in-progress:", "--in-progress-rgb:", + "--in-review:", "--triage:", "--done:", "--color-success:", "--color-warning:", + "--color-error:", "--color-info:", "--cta-bg:", "--cta-border:", "--cta-text:", + "--cta-bg-hover:", "--cta-border-hover:", "--cta-glow:", "--accent:", "--accent-text:", + "--logo-accent:", "--shadow-glow:", "--focus-ring:", "--focus-ring-strong:", + ]; + + for (const block of [darkBlock, lightBlock]) { + for (const token of requiredTokens) expect(block).toContain(token); + } + expect(darkBlock).toContain("--bg: #16201a;"); + expect(darkBlock).toContain("--accent: #9ab58a;"); + expect(lightBlock).toContain("--bg: #f5f8f4;"); + expect(lightBlock).toContain("--accent: #55764d;"); + }); + + it("uses mode-specific global Sage preview properties for an unselected swatch", () => { + const darkGlobals = extractSelectorBlock(themeData, ":root"); + const lightGlobals = extractSelectorBlock(themeData, '[data-theme="light"]'); + const darkSwatch = extractSelectorBlock(themeSelector, ".theme-swatch-sage"); + const lightSwatch = extractSelectorBlock(themeSelector, '[data-theme="light"] .theme-swatch-sage'); + + for (const block of [darkGlobals, lightGlobals]) { + for (const sample of [1, 2, 3, 4]) expect(block).toContain(`--sage-swatch-sample-${sample}:`); + } + for (const block of [darkSwatch, lightSwatch]) { + for (const sample of [1, 2, 3, 4]) { + expect(block).toContain(`--swatch-sample-${sample}: var(--sage-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 0567e8f8f2..2c08f020b1 100644 --- a/packages/dashboard/app/components/ThemeSelector.css +++ b/packages/dashboard/app/components/ThemeSelector.css @@ -333,6 +333,13 @@ --swatch-sample-4: var(--dawn-swatch-sample-4); } +.theme-swatch-sage { + --swatch-sample-1: var(--sage-swatch-sample-1); + --swatch-sample-2: var(--sage-swatch-sample-2); + --swatch-sample-3: var(--sage-swatch-sample-3); + --swatch-sample-4: var(--sage-swatch-sample-4); +} + .theme-swatch-factory-dark { --swatch-sample-1: var(--factory-dark-swatch-sample-1); --swatch-sample-2: var(--factory-dark-swatch-sample-2); @@ -1188,6 +1195,13 @@ --swatch-sample-4: var(--dawn-swatch-sample-4); } +[data-theme="light"] .theme-swatch-sage { + --swatch-sample-1: var(--sage-swatch-sample-1); + --swatch-sample-2: var(--sage-swatch-sample-2); + --swatch-sample-3: var(--sage-swatch-sample-3); + --swatch-sample-4: var(--sage-swatch-sample-4); +} + [data-theme="light"] .theme-swatch-factory-dark { --swatch-sample-1: var(--factory-dark-swatch-sample-1); --swatch-sample-2: var(--factory-dark-swatch-sample-2); diff --git a/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx b/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx index 9ca0dc6de7..af0582e0b3 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', 'factory-dark', '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', 'factory-dark', '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) => { diff --git a/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx b/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx index 2b8c336c13..b049df98b8 100644 --- a/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx +++ b/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx @@ -4,7 +4,7 @@ import type { ColorTheme } from "@fusion/core"; import { ThemeSelector } from "../ThemeSelector"; // 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', 'factory-dark', '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', 'factory-dark', '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) => { diff --git a/packages/dashboard/app/components/command-center/__tests__/CommandCenterControls.test.tsx b/packages/dashboard/app/components/command-center/__tests__/CommandCenterControls.test.tsx index f7a3885da8..ea3211fdc9 100644 --- a/packages/dashboard/app/components/command-center/__tests__/CommandCenterControls.test.tsx +++ b/packages/dashboard/app/components/command-center/__tests__/CommandCenterControls.test.tsx @@ -76,7 +76,7 @@ function cssRule(css: string, selector: string) { } // 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', 'factory-dark', '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', 'factory-dark', '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) => { diff --git a/packages/dashboard/app/components/themeOptions.ts b/packages/dashboard/app/components/themeOptions.ts index 2e4206f480..b90eb98bdc 100644 --- a/packages/dashboard/app/components/themeOptions.ts +++ b/packages/dashboard/app/components/themeOptions.ts @@ -78,6 +78,7 @@ export const COLOR_THEMES: { value: ColorTheme; label: string; className: string { value: "aurora", label: "Aurora", className: "theme-swatch-aurora" }, { 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: "factory-dark", label: "Factory Dark", className: "theme-swatch-factory-dark" }, { value: "shadcn", label: "Shadcn", className: "theme-swatch-shadcn" }, { value: "shadcn-ember", label: "Shadcn Ember (Default)", className: "theme-swatch-shadcn-ember" }, diff --git a/packages/dashboard/app/index.html b/packages/dashboard/app/index.html index 10ba73437c..48db7350c0 100644 --- a/packages/dashboard/app/index.html +++ b/packages/dashboard/app/index.html @@ -164,7 +164,7 @@ 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', 'factory-dark', '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', 'factory-dark', '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. if (!validThemes.includes(colorTheme)) { diff --git a/packages/dashboard/app/public/theme-data.css b/packages/dashboard/app/public/theme-data.css index 8c8810c2af..695fe08c6b 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, and Factory Dark swatches stay globally resolvable while a different theme is active. */ +/* Aurora, Calm, Dawn, Sage, and Factory Dark swatches stay globally resolvable while a different theme is active. */ :root { --aurora-swatch-sample-1: #0a1022; --aurora-swatch-sample-2: #162044; @@ -22,6 +22,10 @@ --dawn-swatch-sample-2: #2a2248; --dawn-swatch-sample-3: #efb66a; --dawn-swatch-sample-4: #c8a4ff; + --sage-swatch-sample-1: #16201a; + --sage-swatch-sample-2: #27382e; + --sage-swatch-sample-3: #9ab58a; + --sage-swatch-sample-4: #8baaa0; --factory-dark-swatch-sample-1: #0d0f10; --factory-dark-swatch-sample-2: #1a1d1f; --factory-dark-swatch-sample-3: #e89532; @@ -41,6 +45,10 @@ --dawn-swatch-sample-2: #f1ddd7; --dawn-swatch-sample-3: #9b5618; --dawn-swatch-sample-4: #704eaa; + --sage-swatch-sample-1: #f5f8f4; + --sage-swatch-sample-2: #e2eadf; + --sage-swatch-sample-3: #55764d; + --sage-swatch-sample-4: #46756b; --factory-dark-swatch-sample-1: #eceef0; --factory-dark-swatch-sample-2: #d9dde0; --factory-dark-swatch-sample-3: #a95612; @@ -7600,6 +7608,82 @@ Dawn pairs deep indigo and plum dark surfaces with muted peach, amber, and viole --focus-ring-strong: 0 0 0 2px color-mix(in srgb, var(--accent) 36%, transparent); } +/* +FNXC:DashboardTheming 2026-07-31-21:28: +Sage uses soft green-gray surfaces with a muted olive accent, preserving readable neutral text ramps in dark and light modes. Global previews remain independent of the active theme so unselected Sage rows retain all four swatch colors. +*/ +[data-color-theme="sage"] { + --bg: #16201a; + --surface: #1e2b23; + --card: #27382e; + --card-hover: #33463a; + --surface-hover: color-mix(in srgb, var(--surface) 90%, var(--text) 10%); + --border: #4c6254; + --text: #edf3e9; + --text-muted: #bdcbbd; + --text-dim: #869a8b; + + --todo: #9bb9b2; + --in-progress: #8bbdb0; + --in-progress-rgb: 139, 189, 176; + --in-review: #b1c58b; + --triage: #d0b676; + --done: #91a198; + --color-success: #9fc48e; + --color-warning: #d0b676; + --color-error: #de8f88; + --color-info: #8bbdb0; + + --cta-bg: #9ab58a; + --cta-border: #b8cba8; + --cta-text: #16201a; + --cta-bg-hover: #aec49e; + --cta-border-hover: #d2dfc7; + --cta-glow: 0 0 8px color-mix(in srgb, var(--cta-border) 30%, transparent); + --accent: #9ab58a; + --accent-text: #16201a; + --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="sage"][data-theme="light"] { + --bg: #f5f8f4; + --surface: #eaf0e8; + --card: #ffffff; + --card-hover: #e2eadf; + --surface-hover: color-mix(in srgb, var(--surface) 92%, var(--text) 8%); + --border: #b9c9b6; + --text: #253529; + --text-muted: #526957; + --text-dim: #788f7d; + + --todo: #46756b; + --in-progress: #3f776b; + --in-progress-rgb: 63, 119, 107; + --in-review: #617d47; + --triage: #896d31; + --done: #687d70; + --color-success: #4d7a45; + --color-warning: #896d31; + --color-error: #ac4f49; + --color-info: #3f776b; + + --cta-bg: #55764d; + --cta-border: #405f3a; + --cta-text: #ffffff; + --cta-bg-hover: #46663f; + --cta-border-hover: #355330; + --cta-glow: 0 0 8px color-mix(in srgb, var(--cta-border) 24%, transparent); + --accent: #55764d; + --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); +} + /* FNXC:DashboardTheming 2026-07-31-20:39: Factory Dark is the low-ambient-light industrial sibling of Factory: graphite and steel surfaces, cool gunmetal borders, and restrained safety-orange actions. Its concrete-gray light counterpart keeps the same visual identity with readable dark text; global previews remain independent of the active theme. diff --git a/packages/desktop/src/renderer/index.html b/packages/desktop/src/renderer/index.html index 339e340125..19e4fcc273 100644 --- a/packages/desktop/src/renderer/index.html +++ b/packages/desktop/src/renderer/index.html @@ -78,6 +78,7 @@ "aurora", "calm", "dawn", + "sage", "factory-dark", "shadcn", "shadcn-ember",