FN-8454: add Aurora dashboard theme
Add the Aurora color theme across dashboard and desktop theme surfaces. - Register Aurora as a selectable global theme with matching UI styles and metadata - Load Aurora theme data in dashboard and desktop entry points - Document the theme and cover selection, token, and settings behavior Files changed: .changeset/fn-8454-aurora-theme.md | 7 ++ docs/dashboard-guide.md | 3 +- docs/settings-reference.md | 2 +- .../core/src/__tests__/global-settings.test.ts | 8 ++ packages/core/src/types/execution-and-ui.ts | 2 + .../dashboard/app/__tests__/aurora-theme.test.ts | 98 ++++++++++++++++++++++ .../dashboard/app/components/ThemeSelector.css | 14 ++++ .../components/__tests__/ThemeDropdown.test.tsx | 51 ++++++++++- .../components/__tests__/ThemeSelector.test.tsx | 2 +- .../__tests__/CommandCenterControls.test.tsx | 2 +- packages/dashboard/app/components/themeOptions.ts | 1 + .../dashboard/app/hooks/__tests__/useTheme.test.ts | 12 +++ packages/dashboard/app/index.html | 2 +- packages/dashboard/app/public/theme-data.css | 91 ++++++++++++++++++++ packages/desktop/src/renderer/index.html | 1 + 15 files changed, 290 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-8454 Fusion-Task-Lineage: cbae0854-b884-4744-a2fa-f738a71ceb32 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8454-aurora-theme.md
Normal file
7
.changeset/fn-8454-aurora-theme.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Let operators select the Aurora dashboard theme.
|
||||
category: feature
|
||||
dev: Adds persisted Aurora registry entries, first-paint validation, and dark/light palette tokens.
|
||||
@@ -1958,8 +1958,9 @@ Non-Command-Center dashboard CSS uses `--text` as the canonical primary text tok
|
||||
<!-- FNXC:DashboardTheming 2026-07-03-00:00: Fresh installs and reset-to-default use System mode so the dashboard follows the OS light/dark preference before and after hydration while keeping Shadcn Ember as the default color theme. -->
|
||||
<!-- FNXC:DashboardTheming 2026-07-16-00:00: FN-8151 adds Cobalt, Clay, and Moss; keep this inventory synchronized with the core COLOR_THEMES union and selector surfaces. -->
|
||||
<!-- FNXC:DashboardTheming 2026-07-16-14:30: FN-8146 restores historical Shadcn Mono as a persistent selector and makes the Settings current-theme row its single all-themes dropdown trigger while Command Center stays compact. -->
|
||||
<!-- FNXC:DashboardTheming 2026-07-20-00:00: Aurora is a built-in persisted palette with navy/teal/violet dark surfaces and a misty light counterpart; keep this inventory and its count synchronized with COLOR_THEMES. -->
|
||||
|
||||
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. 81 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), 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. 82 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), and Aurora (navy/teal/violet dark surfaces with a misty blue-white 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 list every selectable color theme with the same labels and color-chip swatches. The left sidebar active-item highlight and resize accent use the active theme's `--accent`, so they follow the selected Shadcn accent instead of staying fixed blue.
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ Defaults from `DEFAULT_GLOBAL_SETTINGS`; key scope from `GLOBAL_SETTINGS_KEYS`.
|
||||
| Setting | Type | Default | Description |
|
||||
|---|---|---:|---|
|
||||
| `themeMode` | `"dark" \| "light" \| "system"` | `"system"` | Dashboard theme mode. Fresh installs follow the operating system light/dark preference until the user chooses Light, Dark, or System. |
|
||||
| `colorTheme` | `ColorTheme` | `"shadcn-ember"` | Dashboard color theme preset. Use `"shadcn-custom"` to show the custom shadcn color picker in Settings → Appearance and the Command Center theme card. |
|
||||
| `colorTheme` | `ColorTheme` | `"shadcn-ember"` | Dashboard color theme preset. Aurora is a built-in navy/teal/violet palette with a misty light counterpart. Use `"shadcn-custom"` to show the separate custom shadcn color picker in Settings → Appearance and the Command Center theme card. |
|
||||
| `shadcnCustomColors` | `Record<string, string>` | `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. |
|
||||
|
||||
@@ -229,6 +229,14 @@ describe("GlobalSettingsStore", () => {
|
||||
expect(parsed.themeMode).toBe("system");
|
||||
});
|
||||
|
||||
it("round-trips the Aurora color theme through persisted settings", async () => {
|
||||
await store.init();
|
||||
await store.updateSettings({ colorTheme: "aurora" });
|
||||
|
||||
await expect(store.getSettings()).resolves.toMatchObject({ colorTheme: "aurora" });
|
||||
await expect(new GlobalSettingsStore(dir).getSettings()).resolves.toMatchObject({ colorTheme: "aurora" });
|
||||
});
|
||||
|
||||
it("persists and clears the planner clarification preference", async () => {
|
||||
await store.init();
|
||||
|
||||
|
||||
@@ -121,6 +121,8 @@ export const COLOR_THEMES = [
|
||||
"cobalt",
|
||||
"clay",
|
||||
"moss",
|
||||
// FNXC:DashboardTheming 2026-07-20-00:00: Aurora is a persisted dashboard theme; keep this ID and order synchronized with selector metadata and web/desktop first-paint validators so saved selections survive validation before React hydrates.
|
||||
"aurora",
|
||||
"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",
|
||||
|
||||
98
packages/dashboard/app/__tests__/aurora-theme.test.ts
Normal file
98
packages/dashboard/app/__tests__/aurora-theme.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
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-20-00:00:
|
||||
Aurora 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("Aurora 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 === "aurora")).toHaveLength(1);
|
||||
expect(DASHBOARD_COLOR_THEMES).toContainEqual({
|
||||
value: "aurora",
|
||||
label: "Aurora",
|
||||
className: "theme-swatch-aurora",
|
||||
});
|
||||
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 Aurora token blocks", () => {
|
||||
const darkBlock = extractSelectorBlock(themeData, '[data-color-theme="aurora"]');
|
||||
const lightBlock = extractSelectorBlock(themeData, '[data-color-theme="aurora"][data-theme="light"]');
|
||||
const requiredTokens = [
|
||||
"--bg:", "--surface:", "--card:", "--border:", "--text:", "--color-success:",
|
||||
"--color-warning:", "--color-error:", "--color-info:", "--cta-bg:", "--accent:",
|
||||
"--accent-text:", "--focus-ring:", "--shadow-glow:",
|
||||
];
|
||||
|
||||
for (const block of [darkBlock, lightBlock]) {
|
||||
for (const token of requiredTokens) expect(block).toContain(token);
|
||||
}
|
||||
expect(darkBlock).toContain("--bg: #0a1022;");
|
||||
expect(darkBlock).toContain("--accent: #65e6c7;");
|
||||
expect(lightBlock).toContain("--bg: #f4f8ff;");
|
||||
expect(lightBlock).toContain("--accent: #197d72;");
|
||||
});
|
||||
|
||||
it("uses mode-specific global Aurora preview properties for an unselected swatch", () => {
|
||||
const darkGlobals = extractSelectorBlock(themeData, ":root");
|
||||
const lightGlobals = extractSelectorBlock(themeData, '[data-theme="light"]');
|
||||
const darkSwatch = extractSelectorBlock(themeSelector, ".theme-swatch-aurora");
|
||||
const lightSwatch = extractSelectorBlock(themeSelector, '[data-theme="light"] .theme-swatch-aurora');
|
||||
|
||||
for (const block of [darkGlobals, lightGlobals]) {
|
||||
for (const sample of [1, 2, 3, 4]) expect(block).toContain(`--aurora-swatch-sample-${sample}:`);
|
||||
}
|
||||
for (const block of [darkSwatch, lightSwatch]) {
|
||||
for (const sample of [1, 2, 3, 4]) {
|
||||
expect(block).toContain(`--swatch-sample-${sample}: var(--aurora-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}`);
|
||||
}
|
||||
@@ -312,6 +312,13 @@
|
||||
--swatch-sample-4: #72c7a1;
|
||||
}
|
||||
|
||||
.theme-swatch-aurora {
|
||||
--swatch-sample-1: var(--aurora-swatch-sample-1);
|
||||
--swatch-sample-2: var(--aurora-swatch-sample-2);
|
||||
--swatch-sample-3: var(--aurora-swatch-sample-3);
|
||||
--swatch-sample-4: var(--aurora-swatch-sample-4);
|
||||
}
|
||||
|
||||
.theme-swatch-silver {
|
||||
--swatch-sample-1: #27272a;
|
||||
--swatch-sample-2: #3f3f46;
|
||||
@@ -1139,6 +1146,13 @@
|
||||
--swatch-sample-4: #397c49;
|
||||
}
|
||||
|
||||
[data-theme="light"] .theme-swatch-aurora {
|
||||
--swatch-sample-1: var(--aurora-swatch-sample-1);
|
||||
--swatch-sample-2: var(--aurora-swatch-sample-2);
|
||||
--swatch-sample-3: var(--aurora-swatch-sample-3);
|
||||
--swatch-sample-4: var(--aurora-swatch-sample-4);
|
||||
}
|
||||
|
||||
[data-theme="light"] .theme-swatch-silver {
|
||||
--swatch-sample-1: #fafafa;
|
||||
--swatch-sample-2: #f4f4f5;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { fireEvent, render, screen, within } from "@testing-library/react";
|
||||
import { ThemeDropdown } from "../ThemeDropdown";
|
||||
|
||||
// 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', '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', '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) => {
|
||||
@@ -85,6 +85,55 @@ describe("ThemeDropdown", () => {
|
||||
expect(glassSilverOption.querySelector(".theme-swatch-glass-silver")).toBeTruthy();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["compact", "compact" as const, /ocean/i],
|
||||
["current-row", "current-row" as const, /current theme dark \/ ocean/i],
|
||||
])("selects Aurora with a non-empty preview from the shared %s trigger", (_label, triggerVariant, triggerName) => {
|
||||
document.documentElement.setAttribute("data-color-theme", "ocean");
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
const previewTokens = document.createElement("style");
|
||||
previewTokens.textContent = `
|
||||
:root { --aurora-swatch-sample-1: #0a1022; --aurora-swatch-sample-2: #162044; --aurora-swatch-sample-3: #65e6c7; --aurora-swatch-sample-4: #8d8cff; }
|
||||
[data-theme="light"] { --aurora-swatch-sample-1: #f4f8ff; --aurora-swatch-sample-2: #dfeaf8; --aurora-swatch-sample-3: #197d72; --aurora-swatch-sample-4: #5652b8; }
|
||||
`;
|
||||
document.head.appendChild(previewTokens);
|
||||
const onColorThemeChange = vi.fn();
|
||||
|
||||
const { rerender } = render(
|
||||
<ThemeDropdown
|
||||
triggerVariant={triggerVariant}
|
||||
colorTheme="ocean"
|
||||
themeMode="dark"
|
||||
onColorThemeChange={onColorThemeChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: triggerName }));
|
||||
const auroraOption = screen.getByRole("option", { name: "Aurora" });
|
||||
expect(auroraOption.querySelector(".theme-swatch-aurora")).toBeTruthy();
|
||||
expect(auroraOption.querySelectorAll(".theme-option-swatch-sample")).toHaveLength(4);
|
||||
for (const sample of [1, 2, 3, 4]) {
|
||||
expect(getComputedStyle(document.documentElement).getPropertyValue(`--aurora-swatch-sample-${sample}`).trim()).not.toBe("");
|
||||
}
|
||||
fireEvent.click(auroraOption);
|
||||
expect(onColorThemeChange).toHaveBeenCalledWith("aurora");
|
||||
expect(screen.queryByRole("listbox")).toBeNull();
|
||||
|
||||
document.documentElement.setAttribute("data-theme", "light");
|
||||
rerender(
|
||||
<ThemeDropdown
|
||||
triggerVariant={triggerVariant}
|
||||
colorTheme="ocean"
|
||||
themeMode="light"
|
||||
onColorThemeChange={onColorThemeChange}
|
||||
/>,
|
||||
);
|
||||
for (const sample of [1, 2, 3, 4]) {
|
||||
expect(getComputedStyle(document.documentElement).getPropertyValue(`--aurora-swatch-sample-${sample}`).trim()).not.toBe("");
|
||||
}
|
||||
previewTokens.remove();
|
||||
});
|
||||
|
||||
it("selects themes and closes from click, escape, and outside click", () => {
|
||||
const onColorThemeChange = vi.fn();
|
||||
render(<ThemeDropdown colorTheme="default" onColorThemeChange={onColorThemeChange} />);
|
||||
|
||||
@@ -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', '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', '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) => {
|
||||
|
||||
@@ -74,7 +74,7 @@ function expectCommandCenterUseOffset(testId: string, ratio: number) {
|
||||
}
|
||||
|
||||
// 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', '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', '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) => {
|
||||
|
||||
@@ -75,6 +75,7 @@ export const COLOR_THEMES: { value: ColorTheme; label: string; className: string
|
||||
{ value: "cobalt", label: "Cobalt", className: "theme-swatch-cobalt" },
|
||||
{ value: "clay", label: "Clay", className: "theme-swatch-clay" },
|
||||
{ value: "moss", label: "Moss", className: "theme-swatch-moss" },
|
||||
{ value: "aurora", label: "Aurora", className: "theme-swatch-aurora" },
|
||||
{ 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" },
|
||||
|
||||
@@ -174,6 +174,18 @@ describe("useTheme", () => {
|
||||
expect(localStorageMock[COLOR_THEME_STORAGE_KEY]).toBe("forest");
|
||||
});
|
||||
|
||||
it("hydrates, caches, and applies Aurora from backend settings", async () => {
|
||||
mockFetchGlobalSettings.mockResolvedValue({ colorTheme: "aurora" });
|
||||
|
||||
const { result } = renderHook(() => useTheme());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.colorTheme).toBe("aurora");
|
||||
});
|
||||
expect(localStorageMock[COLOR_THEME_STORAGE_KEY]).toBe("aurora");
|
||||
expect(document.documentElement.getAttribute("data-color-theme")).toBe("aurora");
|
||||
});
|
||||
|
||||
it("hydrates dashboard font scale from backend on mount", async () => {
|
||||
mockFetchGlobalSettings.mockResolvedValue({ dashboardFontScalePct: 110 });
|
||||
|
||||
|
||||
@@ -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', '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', '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)) {
|
||||
|
||||
@@ -8,6 +8,21 @@
|
||||
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 swatches stay globally resolvable while a different theme is active. */
|
||||
:root {
|
||||
--aurora-swatch-sample-1: #0a1022;
|
||||
--aurora-swatch-sample-2: #162044;
|
||||
--aurora-swatch-sample-3: #65e6c7;
|
||||
--aurora-swatch-sample-4: #8d8cff;
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--aurora-swatch-sample-1: #f4f8ff;
|
||||
--aurora-swatch-sample-2: #dfeaf8;
|
||||
--aurora-swatch-sample-3: #197d72;
|
||||
--aurora-swatch-sample-4: #5652b8;
|
||||
}
|
||||
|
||||
/* OCEAN - Deep blues and cyans */
|
||||
[data-color-theme="ocean"] {
|
||||
--surface-hover: color-mix(in srgb, var(--surface) 90%, var(--text) 10%);
|
||||
@@ -7368,3 +7383,79 @@ FN-8151 adds Cobalt, Clay, and Moss with complete dark and light token ramps. Ke
|
||||
--accent-text: #ffffff;
|
||||
--logo-accent: var(--accent);
|
||||
}
|
||||
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
[data-color-theme="aurora"] {
|
||||
--bg: #0a1022;
|
||||
--surface: #101a35;
|
||||
--card: #162044;
|
||||
--card-hover: #202d58;
|
||||
--surface-hover: color-mix(in srgb, var(--surface) 90%, var(--text) 10%);
|
||||
--border: #2b3b6b;
|
||||
--text: #eaf5ff;
|
||||
--text-muted: #b2c5e4;
|
||||
--text-dim: #7891ba;
|
||||
|
||||
--todo: #7aa7ff;
|
||||
--in-progress: #65d8e6;
|
||||
--in-progress-rgb: 101, 216, 230;
|
||||
--in-review: #9b8cff;
|
||||
--triage: #f2bc67;
|
||||
--done: #8295b7;
|
||||
--color-success: #65e6c7;
|
||||
--color-warning: #f2bc67;
|
||||
--color-error: #fb7e96;
|
||||
--color-info: #65b9ff;
|
||||
|
||||
--cta-bg: #2a9d91;
|
||||
--cta-border: #65e6c7;
|
||||
--cta-text: #07171c;
|
||||
--cta-bg-hover: #3db9a8;
|
||||
--cta-border-hover: #98f2dc;
|
||||
--cta-glow: 0 0 8px color-mix(in srgb, var(--cta-border) 30%, transparent);
|
||||
--accent: #65e6c7;
|
||||
--accent-text: #07171c;
|
||||
--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="aurora"][data-theme="light"] {
|
||||
--bg: #f4f8ff;
|
||||
--surface: #e9f1fb;
|
||||
--card: #ffffff;
|
||||
--card-hover: #dce9f8;
|
||||
--surface-hover: color-mix(in srgb, var(--surface) 92%, var(--text) 8%);
|
||||
--border: #bdcde2;
|
||||
--text: #102644;
|
||||
--text-muted: #48617f;
|
||||
--text-dim: #7087a1;
|
||||
|
||||
--todo: #366cb7;
|
||||
--in-progress: #197d8c;
|
||||
--in-progress-rgb: 25, 125, 140;
|
||||
--in-review: #5a4fb1;
|
||||
--triage: #9a6515;
|
||||
--done: #617a98;
|
||||
--color-success: #197d72;
|
||||
--color-warning: #9a6515;
|
||||
--color-error: #bd3e59;
|
||||
--color-info: #246fae;
|
||||
|
||||
--cta-bg: #197d72;
|
||||
--cta-border: #12675e;
|
||||
--cta-text: #ffffff;
|
||||
--cta-bg-hover: #126b62;
|
||||
--cta-border-hover: #0e554e;
|
||||
--cta-glow: 0 0 8px color-mix(in srgb, var(--cta-border) 24%, transparent);
|
||||
--accent: #197d72;
|
||||
--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);
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
"cobalt",
|
||||
"clay",
|
||||
"moss",
|
||||
"aurora",
|
||||
"shadcn",
|
||||
"shadcn-ember",
|
||||
"shadcn-custom",
|
||||
|
||||
Reference in New Issue
Block a user