FN-6814: add Shadcn Gray theme variant

Adds a neutral Shadcn Gray dashboard color theme across selection, bootstrap, documentation, and release notes.

- Add Shadcn Gray to core theme validation and dashboard theme picker options.
- Define dark and light zinc-gray theme tokens plus matching swatch previews.
- Cover the new theme with regression tests and document the expanded theme count.

Files changed:
 .changeset/shadcn-gray-variant.md                  |   5 +
 docs/dashboard-guide.md                            |   2 +-
 packages/core/src/types.ts                         |   4 +-
 .../app/__tests__/shadcn-gray-theme.test.ts        |  65 ++++++++++
 .../dashboard/app/components/ThemeSelector.css     |  15 +++
 packages/dashboard/app/components/themeOptions.ts  |   2 +
 packages/dashboard/app/index.html                  |   2 +-
 packages/dashboard/app/public/theme-data.css       | 143 ++++++++++++++++++++-
 8 files changed, 228 insertions(+), 10 deletions(-)

Fusion-Task-Id: FN-6814

Fusion-Task-Lineage: 0548ad7a-5a3c-4e39-8dab-3eb5d40d84c1
This commit is contained in:
gsxdsm
2026-06-21 03:29:28 -07:00
parent f030f73032
commit 4672203acd
8 changed files with 228 additions and 10 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": minor
---
Add a Shadcn Gray dashboard color theme with a fully neutral zinc-gray accent.

View File

@@ -1285,7 +1285,7 @@ Non-Command-Center dashboard CSS uses `--text` as the canonical primary text tok
### Theme system
Dark/light modes via `data-theme`; 66 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 and its color family: Shadcn Blue/Green/Red/Purple/Pink/Orange/Yellow, Shadcn Mono (grayscale with red accent), and Shadcn Black (pure black and white). Air is the minimal, borderless, paper-like preset with near-monochrome tokens and CSS-only chrome flattening.
Dark/light modes via `data-theme`; 67 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 and its color family: Shadcn Blue/Green/Red/Purple/Pink/Orange/Yellow, Shadcn Mono (grayscale with red accent), Shadcn Black (pure black and white), and Shadcn Gray (fully neutral zinc-gray accent). Air is the minimal, borderless, paper-like preset with near-monochrome tokens and CSS-only chrome flattening.
- **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.

View File

@@ -330,7 +330,7 @@ export const COLOR_THEMES = [
"neon-bloom",
"sepia",
"shadcn",
// FNXC:DashboardTheming 2026-06-19-16:07: FN-6756 extends the published color-theme union with shadcn-family accent variants; keep dashboard theme options, bootstrap validation, swatches, and theme-data token blocks in lockstep with this ordered list.
// FNXC:DashboardTheming 2026-06-19-16:07: FN-6756 extends the published color-theme union with shadcn-family accent variants; keep dashboard theme options, bootstrap validation, swatches, and theme-data token blocks in lockstep with this ordered list. FNXC:DashboardTheming 2026-06-20-00:00: FN-6814 adds Shadcn Gray as the fully-neutral zinc accent variant; keep it distinct from Shadcn Mono's red accent and Shadcn Black's black/white CTA while preserving the shadcn-family lockstep ordering.
"shadcn-blue",
"shadcn-green",
"shadcn-red",
@@ -339,7 +339,7 @@ export const COLOR_THEMES = [
"shadcn-orange",
"shadcn-yellow",
"shadcn-mono",
"shadcn-black",
"shadcn-black", "shadcn-gray",
] as const;
export type ColorTheme = (typeof COLOR_THEMES)[number];

View File

@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";
import { COLOR_THEMES as CORE_COLOR_THEMES } from "@fusion/core";
import { COLOR_THEMES as DASHBOARD_COLOR_THEMES } from "../components/themeOptions";
import fs from "fs";
import path from "path";
const themeDataPath = path.resolve(__dirname, "../public/theme-data.css");
const dashboardIndexPath = path.resolve(__dirname, "../index.html");
/*
FNXC:DashboardTheming 2026-06-20-00:00:
Shadcn Gray is a zinc-only accent variant. This test locks the dashboard wiring and neutral token intent without asserting the desktop bootstrap validator, which intentionally excludes all shadcn variants.
*/
describe("Shadcn Gray color theme", () => {
const themeData = fs.readFileSync(themeDataPath, "utf-8");
it("defines dark and light neutral-gray shadcn token blocks", () => {
const darkBlock = extractSelectorBlock(themeData, '[data-color-theme="shadcn-gray"]');
const lightBlock = extractSelectorBlock(themeData, '[data-color-theme="shadcn-gray"][data-theme="light"]');
expect(darkBlock).toContain("--surface-hover:");
expect(lightBlock).toContain("--surface-hover:");
expect(darkBlock).toContain("--btn-border-width: 1px;");
expect(lightBlock).toContain("--cta-glow: none;");
expect(darkBlock).toContain("--accent: #a1a1aa;");
expect(lightBlock).toContain("--accent: #52525b;");
expect(darkBlock).toContain("--color-info: #a1a1aa;");
expect(lightBlock).toContain("--color-info: #52525b;");
expect(`${darkBlock}\n${lightBlock}`).not.toContain("#ef4444");
expect(`${darkBlock}\n${lightBlock}`).not.toContain("#3b82f6");
expect(`${darkBlock}\n${lightBlock}`).not.toContain("#60a5fa");
});
it("registers Shadcn Gray in core, dashboard options, and the dashboard bootstrap validator", () => {
expect(CORE_COLOR_THEMES).toContain("shadcn-gray");
expect(DASHBOARD_COLOR_THEMES).toContainEqual({
value: "shadcn-gray",
label: "Shadcn Gray",
className: "theme-swatch-shadcn-gray",
});
expect(fs.readFileSync(dashboardIndexPath, "utf-8")).toContain("'shadcn-gray'");
});
});
function extractSelectorBlock(css: string, selector: string): string {
const startIdx = css.indexOf(`${selector} {`);
if (startIdx === -1) {
throw new Error(`Could not find selector block: ${selector}`);
}
const openBraceIdx = css.indexOf("{", startIdx);
let depth = 1;
let end = openBraceIdx;
for (let i = openBraceIdx + 1; i < css.length; i++) {
if (css[i] === "{") depth++;
if (css[i] === "}") depth--;
if (depth === 0) {
end = i;
break;
}
}
return css.slice(startIdx, end + 1);
}

View File

@@ -451,6 +451,14 @@
--swatch-sample-4: #27272a;
}
/* FNXC:DashboardTheming 2026-06-20-00:00: The Shadcn Gray swatch previews the zinc-only accent choice so users can distinguish it from red-accent Mono and black/white CTA Black before selecting it. */
.theme-swatch-shadcn-gray {
--swatch-sample-1: #09090b;
--swatch-sample-2: #18181b;
--swatch-sample-3: #a1a1aa;
--swatch-sample-4: #27272a;
}
.theme-swatch-ayu {
--swatch-sample-1: #0f1419;
--swatch-sample-2: #131d27;
@@ -732,6 +740,13 @@
--swatch-sample-4: #e4e4e7;
}
[data-theme="light"] .theme-swatch-shadcn-gray {
--swatch-sample-1: #ffffff;
--swatch-sample-2: #f4f4f5;
--swatch-sample-3: #52525b;
--swatch-sample-4: #e4e4e7;
}
[data-theme="light"] .theme-swatch-ayu {
--swatch-sample-1: #fafafa;
--swatch-sample-2: #f3f3f3;

View File

@@ -78,4 +78,6 @@ export const COLOR_THEMES: { value: ColorTheme; label: string; className: string
{ value: "shadcn-yellow", label: "Shadcn Yellow", className: "theme-swatch-shadcn-yellow" },
{ value: "shadcn-mono", label: "Shadcn Mono", className: "theme-swatch-shadcn-mono" },
{ value: "shadcn-black", label: "Shadcn Black", className: "theme-swatch-shadcn-black" },
/* FNXC:DashboardTheming 2026-06-20-00:00: Shadcn Gray is the fully-neutral zinc accent option; keep it adjacent to Shadcn Black so selectors mirror the core COLOR_THEMES order. */
{ value: "shadcn-gray", label: "Shadcn Gray", className: "theme-swatch-shadcn-gray" },
];

View File

@@ -160,7 +160,7 @@
try {
var mode = localStorage.getItem('kb-dashboard-theme-mode') || 'dark';
var colorTheme = localStorage.getItem('kb-dashboard-color-theme') || 'default';
var validThemes = ['default', 'ocean', 'forest', 'sunset', 'zen', 'berry', 'high-contrast', 'industrial', 'monochrome', 'slate', 'ash', 'air', 'graphite', 'silver', 'solarized', 'factory', 'factory-mono', 'ayu', 'one-dark', 'nord', 'dracula', 'gruvbox', 'tokyo-night', 'catppuccin-mocha', 'github-dark', 'everforest', 'rose-pine', 'kanagawa', 'night-owl', 'palenight', 'monokai-pro', 'slime', 'brutalist', 'neon-city', 'parchment', 'terminal', 'glass', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'shadcn', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', 'shadcn-black'];
var validThemes = ['default', 'ocean', 'forest', 'sunset', 'zen', 'berry', 'high-contrast', 'industrial', 'monochrome', 'slate', 'ash', 'air', 'graphite', 'silver', 'solarized', 'factory', 'factory-mono', 'ayu', 'one-dark', 'nord', 'dracula', 'gruvbox', 'tokyo-night', 'catppuccin-mocha', 'github-dark', 'everforest', 'rose-pine', 'kanagawa', 'night-owl', 'palenight', 'monokai-pro', 'slime', 'brutalist', 'neon-city', 'parchment', 'terminal', 'glass', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'shadcn', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', 'shadcn-black', 'shadcn-gray'];
if (!validThemes.includes(colorTheme)) {
colorTheme = 'default';
}

View File

@@ -2463,6 +2463,128 @@ FN-6756 adds shadcn color-family themes as exact zinc-neutral shadcn variants wi
--accent-text: #fafafa;
}
/*
FNXC:DashboardTheming 2026-06-20-00:00:
Shadcn Gray is the fully-neutral zinc variant: it keeps the shadcn surfaces and button treatment while using only zinc/gray accent, CTA, and info tokens instead of red, blue, or other chroma.
*/
[data-color-theme="shadcn-gray"] {
--bg: #09090b;
--surface: #0c0c0e;
--card: #18181b;
--card-hover: #1f1f23;
--surface-hover: color-mix(in srgb, var(--surface) 90%, var(--text) 10%);
--border: #27272a;
--text: #fafafa;
--text-muted: #a1a1aa;
--text-dim: #52525b;
--todo: #a1a1aa;
--in-progress: #71717a;
--in-progress-rgb: 113, 113, 122;
--in-review: #71717a;
--triage: #a1a1aa;
--done: #52525b;
--color-success: #71717a;
--color-warning: #a1a1aa;
--color-error: #d4d4d8;
--color-muted: #71717a;
--font-primary: "Geist", "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
--space-xs: 4px;
--space-sm: 6px;
--space-md: 10px;
--space-lg: 16px;
--space-xl: 20px;
--space-2xl: 28px;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius: var(--radius-md);
--btn-padding: 6px 12px;
--btn-border-width: 1px;
--card-padding: 10px 12px;
--modal-padding: var(--space-md) var(--space-lg);
--header-padding: var(--space-sm) var(--space-lg);
--column-gap: var(--space-md);
--board-padding: var(--space-md) var(--space-lg);
--shadow-sm: 0 1px 2px color-mix(in srgb, #000000 18%, transparent);
--shadow-md: 0 1px 3px color-mix(in srgb, #000000 22%, transparent);
--shadow-lg: 0 4px 12px color-mix(in srgb, #000000 24%, transparent);
--shadow-glow: none;
--glow-success: none;
--glow-warning: none;
--glow-danger: none;
--focus-ring: 0 0 0 calc(var(--btn-border-width) * 3) color-mix(in srgb, var(--accent) 35%, transparent);
--focus-ring-strong: 0 0 0 calc(var(--btn-border-width) * 3) color-mix(in srgb, var(--accent) 45%, transparent);
--shadow: var(--shadow-lg);
--cta-bg: #71717a;
--cta-border: #71717a;
--cta-text: #ffffff;
--cta-bg-hover: #52525b;
--cta-border-hover: #52525b;
--cta-glow: none;
--logo-accent: var(--accent);
--color-info: #a1a1aa;
--accent: #a1a1aa;
--accent-text: #18181b;
}
[data-color-theme="shadcn-gray"][data-theme="light"] {
--bg: #ffffff;
--surface: #ffffff;
--card: #ffffff;
--card-hover: #f4f4f5;
--surface-hover: color-mix(in srgb, var(--surface) 92%, var(--text) 8%);
--border: #e4e4e7;
--text: #09090b;
--text-muted: #71717a;
--text-dim: #a1a1aa;
--todo: #71717a;
--in-progress: #52525b;
--in-progress-rgb: 82, 82, 91;
--in-review: #52525b;
--triage: #71717a;
--done: #a1a1aa;
--color-success: #52525b;
--color-warning: #71717a;
--color-error: #3f3f46;
--color-muted: #71717a;
--shadow-sm: 0 1px 2px color-mix(in srgb, var(--text) 8%, transparent);
--shadow-md: 0 1px 3px color-mix(in srgb, var(--text) 10%, transparent);
--shadow-lg: 0 4px 12px color-mix(in srgb, var(--text) 12%, transparent);
--shadow-glow: none;
--glow-success: none;
--glow-warning: none;
--glow-danger: none;
--focus-ring: 0 0 0 calc(var(--btn-border-width) * 3) color-mix(in srgb, var(--accent) 18%, transparent);
--focus-ring-strong: 0 0 0 calc(var(--btn-border-width) * 3) color-mix(in srgb, var(--accent) 28%, transparent);
--shadow: var(--shadow-lg);
--cta-bg: #52525b;
--cta-border: #52525b;
--cta-text: #ffffff;
--cta-bg-hover: #3f3f46;
--cta-border-hover: #3f3f46;
--cta-glow: none;
--logo-accent: var(--accent);
--color-info: #52525b;
--accent: #52525b;
--accent-text: #ffffff;
}
[data-color-theme="shadcn-blue"] .card.agent-active,
[data-color-theme="shadcn-green"] .card.agent-active,
[data-color-theme="shadcn-red"] .card.agent-active,
@@ -2471,7 +2593,8 @@ FN-6756 adds shadcn color-family themes as exact zinc-neutral shadcn variants wi
[data-color-theme="shadcn-orange"] .card.agent-active,
[data-color-theme="shadcn-yellow"] .card.agent-active,
[data-color-theme="shadcn-mono"] .card.agent-active,
[data-color-theme="shadcn-black"] .card.agent-active {
[data-color-theme="shadcn-black"] .card.agent-active,
[data-color-theme="shadcn-gray"] .card.agent-active {
border-color: var(--accent);
box-shadow: none;
animation: none;
@@ -2485,7 +2608,8 @@ FN-6756 adds shadcn color-family themes as exact zinc-neutral shadcn variants wi
[data-color-theme="shadcn-orange"][data-theme="light"] .card.agent-active,
[data-color-theme="shadcn-yellow"][data-theme="light"] .card.agent-active,
[data-color-theme="shadcn-mono"][data-theme="light"] .card.agent-active,
[data-color-theme="shadcn-black"][data-theme="light"] .card.agent-active {
[data-color-theme="shadcn-black"][data-theme="light"] .card.agent-active,
[data-color-theme="shadcn-gray"][data-theme="light"] .card.agent-active {
border-color: var(--accent);
box-shadow: none;
animation: none;
@@ -2499,7 +2623,8 @@ FN-6756 adds shadcn color-family themes as exact zinc-neutral shadcn variants wi
[data-color-theme="shadcn-orange"] .btn,
[data-color-theme="shadcn-yellow"] .btn,
[data-color-theme="shadcn-mono"] .btn,
[data-color-theme="shadcn-black"] .btn {
[data-color-theme="shadcn-black"] .btn,
[data-color-theme="shadcn-gray"] .btn {
text-transform: none;
letter-spacing: normal;
font-weight: 500;
@@ -2522,7 +2647,9 @@ FN-6756 adds shadcn color-family themes as exact zinc-neutral shadcn variants wi
[data-color-theme="shadcn-mono"] .btn-primary,
[data-color-theme="shadcn-mono"] .btn-task-create,
[data-color-theme="shadcn-black"] .btn-primary,
[data-color-theme="shadcn-black"] .btn-task-create {
[data-color-theme="shadcn-black"] .btn-task-create,
[data-color-theme="shadcn-gray"] .btn-primary,
[data-color-theme="shadcn-gray"] .btn-task-create {
background: var(--cta-bg);
border-color: var(--cta-border);
color: var(--cta-text);
@@ -2546,7 +2673,9 @@ FN-6756 adds shadcn color-family themes as exact zinc-neutral shadcn variants wi
[data-color-theme="shadcn-mono"] .btn-primary:hover,
[data-color-theme="shadcn-mono"] .btn-task-create:hover,
[data-color-theme="shadcn-black"] .btn-primary:hover,
[data-color-theme="shadcn-black"] .btn-task-create:hover {
[data-color-theme="shadcn-black"] .btn-task-create:hover,
[data-color-theme="shadcn-gray"] .btn-primary:hover,
[data-color-theme="shadcn-gray"] .btn-task-create:hover {
background: var(--cta-bg-hover);
border-color: var(--cta-border-hover);
color: var(--cta-text);
@@ -2570,7 +2699,9 @@ FN-6756 adds shadcn color-family themes as exact zinc-neutral shadcn variants wi
[data-color-theme="shadcn-mono"] .card,
[data-color-theme="shadcn-mono"] .column,
[data-color-theme="shadcn-black"] .card,
[data-color-theme="shadcn-black"] .column {
[data-color-theme="shadcn-black"] .column,
[data-color-theme="shadcn-gray"] .card,
[data-color-theme="shadcn-gray"] .column {
border-width: var(--btn-border-width);
}