FN-8151: add Cobalt, Clay, and Moss themes

Add three synchronized color themes across dashboard and desktop surfaces.

- Register Cobalt, Clay, and Moss theme IDs, token palettes, and light/dark swatches.
- Keep dashboard and desktop bootstrap validators aligned with selector metadata.
- Document and test the cross-surface theme registration invariant.

Files changed:
 .changeset/fn-8151-new-themes.md                   |   7 +
 docs/dashboard-guide.md                            |   3 +-
 packages/core/src/types/execution-and-ui.ts        |   4 +
 .../app/__tests__/new-color-themes.test.ts         | 116 ++++++++++++
 .../dashboard/app/components/ThemeSelector.css     |  42 +++++
 packages/dashboard/app/components/themeOptions.ts  |   3 +
 packages/dashboard/app/index.html                  |   2 +-
 packages/dashboard/app/public/theme-data.css       | 202 +++++++++++++++++++++
 packages/desktop/src/renderer/index.html           |   3 +
 9 files changed, 380 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-8151

Fusion-Task-Lineage: d97973a7-3387-4b98-b150-8ac2e838bfa9

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-16 18:18:35 -07:00
parent 5a60643c0a
commit 07e1ceb860
9 changed files with 380 additions and 2 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: Add three new dashboard color themes: Cobalt, Clay, and Moss.
category: feature
dev: Extends ColorTheme with synchronized dark/light token blocks and selector swatches.

View File

@@ -1847,8 +1847,9 @@ Non-Command-Center dashboard CSS uses `--text` as the canonical primary text tok
<!-- FNXC:DashboardTheming 2026-06-21-00:00: FN-6840 synced the user-facing theme docs to the shipped expanded Shadcn family, the Shadcn Custom color-picker preset, and the sidebar accent behavior that follows each theme's --accent token. -->
<!-- FNXC:DashboardTheming 2026-07-01-00:00: Glass Silver is the silver/gray frosted sibling of Glass and is selectable anywhere color themes are listed, so keep the inventory count and theme description in this guide aligned with COLOR_THEMES. -->
<!-- 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. -->
Dark/light modes via `data-theme`; fresh installs default to System mode so the resolved theme follows `prefers-color-scheme` until the user explicitly chooses Light, Dark, or System. 76 color themes via `data-color-theme` (lazy-loaded from `app/public/theme-data.css`), including the Shadcn zinc-neutral theme with an orange default highlight/accent, Shadcn Custom (the same base with sanitized per-token color-picker overrides), and its color family: Shadcn Blue/Green/Red/Purple/Pink/Orange/Yellow, Shadcn Mono Red/Blue/Green/Purple/Pink/Orange/Yellow (grayscale surfaces with color-specific accents; legacy `shadcn-mono` selections migrate to Shadcn Mono Red), Shadcn Black (pure black and white), Shadcn Gray (fully neutral zinc-gray accent), and Shadcn Gray Blue (blue-gray slate neutral surfaces with a muted slate-blue accent). Air is the minimal, borderless, paper-like preset with near-monochrome tokens and CSS-only chrome flattening. Glass Silver preserves the Glass theme's frosted translucent surfaces and transparent modal overlay behavior while using silver and graphite accents instead of purple/pink.
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. 80 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 Red/Blue/Green/Purple/Pink/Orange/Yellow (grayscale surfaces with color-specific accents; legacy `shadcn-mono` selections migrate to Shadcn Mono Red), Shadcn Black (pure black and white), Shadcn Gray (fully neutral zinc-gray accent), and Shadcn Gray Blue (blue-gray slate neutral surfaces with a muted slate-blue accent). Air is the minimal, borderless, paper-like preset with near-monochrome tokens and CSS-only chrome flattening. Glass Silver preserves the Glass theme's frosted translucent surfaces and transparent modal overlay behavior while using silver and graphite accents instead of purple/pink.
Choose Shadcn variants from **Settings → Appearance** or from the Command Center **Overview** theme card; both use the same compact `ThemeDropdown`, 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.

View File

@@ -109,6 +109,10 @@ export const COLOR_THEMES = [
"lavender",
"neon-bloom",
"sepia",
// FNXC:DashboardTheming 2026-07-16-00:00: FN-8151 adds Cobalt, Clay, and Moss; keep the core union, selector metadata, both bootstrap validators, token blocks, and swatch classes in this exact order.
"cobalt",
"clay",
"moss",
"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",

View File

@@ -0,0 +1,116 @@
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");
const newThemes = [
{ value: "cobalt", label: "Cobalt" },
{ value: "clay", label: "Clay" },
{ value: "moss", label: "Moss" },
] as const;
/*
FNXC:DashboardTheming 2026-07-16-00:00:
FN-8151 requires every theme id to stay registered in the core union, selector metadata, both pre-hydration validators, token blocks, and swatch CSS. This invariant-level contract prevents a partial theme from shipping.
*/
describe("Cobalt, Clay, and Moss color themes", () => {
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 core, selector metadata, and bootstrap validators in exact order without duplicates", () => {
const coreIds = [...CORE_COLOR_THEMES];
const dashboardIds = DASHBOARD_COLOR_THEMES.map((theme) => theme.value);
const dashboardValidThemes = extractValidThemes(dashboardIndexHtml);
const desktopValidThemes = extractValidThemes(desktopIndexHtml);
for (const { value, label } of newThemes) {
expect(CORE_COLOR_THEMES).toContain(value);
expect(DASHBOARD_COLOR_THEMES).toContainEqual({
value,
label,
className: `theme-swatch-${value}`,
});
expect(dashboardValidThemes).toContain(value);
expect(desktopValidThemes).toContain(value);
}
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 dark and light token blocks and swatch samples for every new theme", () => {
const requiredTokens = [
"--bg:",
"--surface:",
"--card:",
"--cta-bg:",
"--accent:",
"--accent-text:",
"--color-info:",
];
const requiredSwatchSamples = [
"--swatch-sample-1:",
"--swatch-sample-2:",
"--swatch-sample-3:",
"--swatch-sample-4:",
];
for (const { value } of newThemes) {
const darkBlock = extractSelectorBlock(themeData, `[data-color-theme="${value}"]`);
const lightBlock = extractSelectorBlock(themeData, `[data-color-theme="${value}"][data-theme="light"]`);
const swatchBlock = extractSelectorBlock(themeSelector, `.theme-swatch-${value}`);
for (const block of [darkBlock, lightBlock]) {
for (const token of requiredTokens) {
expect(block).toContain(token);
}
}
for (const sample of requiredSwatchSamples) {
expect(swatchBlock).toContain(sample);
}
}
});
});
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}`);
}

View File

@@ -291,6 +291,27 @@
--swatch-sample-4: #e8c888;
}
.theme-swatch-cobalt {
--swatch-sample-1: #071426;
--swatch-sample-2: #102848;
--swatch-sample-3: #45a8ff;
--swatch-sample-4: #5eead4;
}
.theme-swatch-clay {
--swatch-sample-1: #201411;
--swatch-sample-2: #3b2620;
--swatch-sample-3: #ec7b4c;
--swatch-sample-4: #f2a65a;
}
.theme-swatch-moss {
--swatch-sample-1: #111b16;
--swatch-sample-2: #243326;
--swatch-sample-3: #a8c96b;
--swatch-sample-4: #72c7a1;
}
.theme-swatch-silver {
--swatch-sample-1: #27272a;
--swatch-sample-2: #3f3f46;
@@ -1095,6 +1116,27 @@
--swatch-sample-4: #a08050;
}
[data-theme="light"] .theme-swatch-cobalt {
--swatch-sample-1: #f3f8ff;
--swatch-sample-2: #e8f2ff;
--swatch-sample-3: #146fc0;
--swatch-sample-4: #12806f;
}
[data-theme="light"] .theme-swatch-clay {
--swatch-sample-1: #fbf3ea;
--swatch-sample-2: #f4e4d4;
--swatch-sample-3: #ad4d25;
--swatch-sample-4: #b55b26;
}
[data-theme="light"] .theme-swatch-moss {
--swatch-sample-1: #f2f6ee;
--swatch-sample-2: #e5eddf;
--swatch-sample-3: #557a35;
--swatch-sample-4: #397c49;
}
[data-theme="light"] .theme-swatch-silver {
--swatch-sample-1: #fafafa;
--swatch-sample-2: #f4f4f5;

View File

@@ -72,6 +72,9 @@ export const COLOR_THEMES: { value: ColorTheme; label: string; className: string
{ value: "lavender", label: "Lavender", className: "theme-swatch-lavender" },
{ value: "neon-bloom", label: "Neon Bloom", className: "theme-swatch-neon-bloom" },
{ value: "sepia", label: "Sepia", className: "theme-swatch-sepia" },
{ 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: "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" },

View File

@@ -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', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono-red', 'shadcn-mono-blue', 'shadcn-mono-green', 'shadcn-mono-purple', 'shadcn-mono-pink', 'shadcn-mono-orange', 'shadcn-mono-yellow', 'shadcn-black', 'shadcn-gray', 'shadcn-gray-blue'];
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-red', 'shadcn-mono-blue', 'shadcn-mono-green', 'shadcn-mono-purple', 'shadcn-mono-pink', 'shadcn-mono-orange', 'shadcn-mono-yellow', 'shadcn-black', 'shadcn-gray', 'shadcn-gray-blue'];
// FNXC:DashboardTheming 2026-06-30-00:00: Unset startup theme is Shadcn Ember; explicit stored legacy ids such as "default" and "ocean" remain valid and must not be migrated.
// FNXC:DashboardTheming 2026-06-20-00:00: FN-6813 remaps the legacy mono id before pre-hydration validation so persisted users keep the red mono accent.
if (colorTheme === 'shadcn-mono') colorTheme = 'shadcn-mono-red';

View File

@@ -7152,3 +7152,205 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
--accent: #9a8050;
--accent-text: #fff;
}
/*
FNXC:DashboardTheming 2026-07-16-00:00:
FN-8151 adds Cobalt, Clay, and Moss with complete dark and light token ramps. Keep these blocks synchronized with the core union, selector metadata, both bootstrap validators, and swatch classes.
*/
[data-color-theme="cobalt"] {
--bg: #071426;
--surface: #0b1d35;
--card: #102848;
--card-hover: #17355c;
--surface-hover: color-mix(in srgb, var(--surface) 90%, var(--text) 10%);
--border: #24446d;
--text: #e6f2ff;
--text-muted: #9bb9d8;
--text-dim: #6283a9;
--todo: #6eb8ff;
--in-progress: #38bdf8;
--in-progress-rgb: 56, 189, 248;
--in-review: #5eead4;
--triage: #fbbf24;
--done: #7694b5;
--color-success: #4ade80;
--color-warning: #fbbf24;
--color-error: #fb7185;
--color-info: #38bdf8;
--cta-bg: #1687e8;
--cta-border: #45a8ff;
--cta-text: #ffffff;
--cta-bg-hover: #279cf4;
--cta-border-hover: #73bdff;
--cta-glow: 0 0 8px color-mix(in srgb, var(--cta-border) 30%, transparent);
--accent: #45a8ff;
--accent-text: #ffffff;
--logo-accent: var(--accent);
}
[data-color-theme="cobalt"][data-theme="light"] {
--bg: #f3f8ff;
--surface: #e8f2ff;
--card: #ffffff;
--card-hover: #dcecff;
--surface-hover: color-mix(in srgb, var(--surface) 92%, var(--text) 8%);
--border: #b8d4f2;
--text: #10233d;
--text-muted: #4d6d91;
--text-dim: #718baa;
--todo: #1874c9;
--in-progress: #087eb6;
--in-progress-rgb: 8, 126, 182;
--in-review: #12806f;
--triage: #a16207;
--done: #6b82a0;
--color-success: #287a4b;
--color-warning: #a16207;
--color-error: #c63651;
--color-info: #087eb6;
--cta-bg: #146fc0;
--cta-border: #0f629f;
--cta-text: #ffffff;
--cta-bg-hover: #0d5fa8;
--cta-border-hover: #084d89;
--cta-glow: 0 0 8px color-mix(in srgb, var(--cta-border) 24%, transparent);
--accent: #146fc0;
--accent-text: #ffffff;
--logo-accent: var(--accent);
}
[data-color-theme="clay"] {
--bg: #201411;
--surface: #2d1c17;
--card: #3b2620;
--card-hover: #4b3128;
--surface-hover: color-mix(in srgb, var(--surface) 90%, var(--text) 10%);
--border: #684237;
--text: #f8ebe1;
--text-muted: #d1a999;
--text-dim: #986f63;
--todo: #f2a65a;
--in-progress: #fb923c;
--in-progress-rgb: 251, 146, 60;
--in-review: #f0b86b;
--triage: #facc15;
--done: #aa7b6c;
--color-success: #95c46a;
--color-warning: #facc15;
--color-error: #fb7185;
--color-info: #f2a65a;
--cta-bg: #c85b31;
--cta-border: #ec7b4c;
--cta-text: #ffffff;
--cta-bg-hover: #dc6a3d;
--cta-border-hover: #f59a70;
--cta-glow: 0 0 8px color-mix(in srgb, var(--cta-border) 30%, transparent);
--accent: #ec7b4c;
--accent-text: #ffffff;
--logo-accent: var(--accent);
}
[data-color-theme="clay"][data-theme="light"] {
--bg: #fbf3ea;
--surface: #f4e4d4;
--card: #fffaf5;
--card-hover: #f8ddc7;
--surface-hover: color-mix(in srgb, var(--surface) 92%, var(--text) 8%);
--border: #dfbca3;
--text: #3d241b;
--text-muted: #795142;
--text-dim: #9c7564;
--todo: #b55b26;
--in-progress: #c95e1a;
--in-progress-rgb: 201, 94, 26;
--in-review: #a96a24;
--triage: #9a6805;
--done: #987162;
--color-success: #527b36;
--color-warning: #9a6805;
--color-error: #bf3650;
--color-info: #ad551e;
--cta-bg: #ad4d25;
--cta-border: #933d1c;
--cta-text: #ffffff;
--cta-bg-hover: #923c1b;
--cta-border-hover: #793116;
--cta-glow: 0 0 8px color-mix(in srgb, var(--cta-border) 24%, transparent);
--accent: #ad4d25;
--accent-text: #ffffff;
--logo-accent: var(--accent);
}
[data-color-theme="moss"] {
--bg: #111b16;
--surface: #19261d;
--card: #243326;
--card-hover: #304332;
--surface-hover: color-mix(in srgb, var(--surface) 90%, var(--text) 10%);
--border: #47604b;
--text: #eaf2e5;
--text-muted: #b3c7ab;
--text-dim: #7f9a7b;
--todo: #b5c96a;
--in-progress: #8ccf75;
--in-progress-rgb: 140, 207, 117;
--in-review: #72c7a1;
--triage: #d6b85e;
--done: #82967f;
--color-success: #8ccf75;
--color-warning: #d6b85e;
--color-error: #e87872;
--color-info: #7ab8c9;
--cta-bg: #688c45;
--cta-border: #94b85e;
--cta-text: #ffffff;
--cta-bg-hover: #789e52;
--cta-border-hover: #b2cd7a;
--cta-glow: 0 0 8px color-mix(in srgb, var(--cta-border) 30%, transparent);
--accent: #a8c96b;
--accent-text: #182215;
--logo-accent: var(--accent);
}
[data-color-theme="moss"][data-theme="light"] {
--bg: #f2f6ee;
--surface: #e5eddf;
--card: #fbfdf8;
--card-hover: #d8e5d0;
--surface-hover: color-mix(in srgb, var(--surface) 92%, var(--text) 8%);
--border: #bfd0b8;
--text: #1e3021;
--text-muted: #526c55;
--text-dim: #789178;
--todo: #74883b;
--in-progress: #397c49;
--in-progress-rgb: 57, 124, 73;
--in-review: #31785e;
--triage: #96751d;
--done: #71866f;
--color-success: #397c49;
--color-warning: #96751d;
--color-error: #bc4543;
--color-info: #397589;
--cta-bg: #557a35;
--cta-border: #46692c;
--cta-text: #ffffff;
--cta-bg-hover: #45682c;
--cta-border-hover: #365422;
--cta-glow: 0 0 8px color-mix(in srgb, var(--cta-border) 24%, transparent);
--accent: #557a35;
--accent-text: #ffffff;
--logo-accent: var(--accent);
}

View File

@@ -72,6 +72,9 @@
"lavender",
"neon-bloom",
"sepia",
"cobalt",
"clay",
"moss",
"shadcn",
"shadcn-ember",
"shadcn-custom",