FN-6747: add shadcn dashboard theme

Add a shadcn-inspired dashboard theme across the theme picker, persisted startup validation, and public theme tokens.

- Register the Shadcn color theme in core and dashboard theme option lists.
- Add zinc-neutral dark and light CSS tokens with Geist typography, 1px borders, subtle shadows, and neutralized glow effects.
- Add theme swatches, documentation, startup allow-list support, regression coverage, and a published package changeset.

Files changed:
 .changeset/shadcn-dashboard-theme.md               |   5 +
 docs/dashboard-guide.md                            |   2 +-
 packages/core/src/types.ts                         |   1 +
 .../dashboard/app/components/ThemeSelector.css     |  14 ++
 packages/dashboard/app/components/themeOptions.ts  |   1 +
 .../dashboard/app/hooks/__tests__/useTheme.test.ts |  28 ++++
 packages/dashboard/app/index.html                  |   2 +-
 packages/dashboard/app/public/theme-data.css       | 166 +++++++++++++++++++++
 8 files changed, 217 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-6747

Fusion-Task-Lineage: cf0d6732-aa4f-457d-ae6c-e54fca0191b2
This commit is contained in:
gsxdsm
2026-06-19 16:04:29 -07:00
parent 59d3eee7f4
commit e19f7c2008
8 changed files with 217 additions and 2 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": minor
---
Add a Shadcn dashboard color theme with zinc neutral tokens, sans-serif typography, 1px borders, subtle flat shadows, and solid primary buttons.

View File

@@ -1266,7 +1266,7 @@ Non-Command-Center dashboard CSS uses `--text` as the canonical primary text tok
### Theme system
Dark/light modes via `data-theme`; 55 color themes via `data-color-theme` (lazy-loaded from `app/public/theme-data.css`).
Dark/light modes via `data-theme`; 56 color themes via `data-color-theme` (lazy-loaded from `app/public/theme-data.css`), including the Shadcn zinc-neutral theme.
- **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

@@ -327,6 +327,7 @@ export const COLOR_THEMES = [
"lavender",
"neon-bloom",
"sepia",
"shadcn",
] as const;
export type ColorTheme = (typeof COLOR_THEMES)[number];

View File

@@ -374,6 +374,13 @@
--swatch-sample-4: #ffffff;
}
.theme-swatch-shadcn {
--swatch-sample-1: #09090b;
--swatch-sample-2: #18181b;
--swatch-sample-3: #fafafa;
--swatch-sample-4: #27272a;
}
.theme-swatch-ayu {
--swatch-sample-1: #0f1419;
--swatch-sample-2: #131d27;
@@ -585,6 +592,13 @@
--swatch-sample-4: #1a1a1a;
}
[data-theme="light"] .theme-swatch-shadcn {
--swatch-sample-1: #ffffff;
--swatch-sample-2: #f4f4f5;
--swatch-sample-3: #18181b;
--swatch-sample-4: #e4e4e7;
}
[data-theme="light"] .theme-swatch-ayu {
--swatch-sample-1: #fafafa;
--swatch-sample-2: #f3f3f3;

View File

@@ -67,4 +67,5 @@ 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: "shadcn", label: "Shadcn", className: "theme-swatch-shadcn" },
];

View File

@@ -519,6 +519,34 @@ describe("useTheme", () => {
document.head.removeChild(style);
});
it("applies shadcn design tokens with neutralized glow effects", () => {
const style = document.createElement("style");
const baseCss = readFileSync(resolve(PACKAGE_ROOT, "app/styles.css"), "utf8");
const themeDataCss = readFileSync(resolve(PACKAGE_ROOT, "app/public/theme-data.css"), "utf8");
const shadcnBlock = themeDataCss.match(/\[data-color-theme="shadcn"\] \{(?<body>[\s\S]*?)\n\}/)?.groups?.body;
expect(shadcnBlock).toBeDefined();
style.textContent = `${baseCss}\n${themeDataCss}\nhtml[data-color-theme="shadcn"] {${shadcnBlock}\n}`;
document.head.appendChild(style);
localStorageMock[COLOR_THEME_STORAGE_KEY] = "shadcn";
renderHook(() => useTheme());
expect(document.documentElement.getAttribute("data-color-theme")).toBe("shadcn");
expect(shadcnBlock).toContain("--btn-border-width: 1px;");
expect(shadcnBlock).toContain("--accent: #fafafa;");
expect(shadcnBlock).toContain("--font-primary: \"Geist\"");
expect(shadcnBlock).toContain("--shadow-glow: none;");
expect(shadcnBlock).toContain("--glow-success: none;");
expect(shadcnBlock).toContain("--glow-warning: none;");
expect(shadcnBlock).toContain("--glow-danger: none;");
expect(shadcnBlock).toContain("--cta-glow: none;");
expect(shadcnBlock).not.toMatch(/--(?:shadow-glow|glow-success|glow-warning|glow-danger|cta-glow):\s*0 0/);
document.head.removeChild(style);
});
it("supports all valid theme modes", () => {
const { result } = renderHook(() => useTheme());

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', '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'];
var validThemes = ['default', 'ocean', 'forest', 'sunset', 'zen', 'berry', 'high-contrast', 'industrial', 'monochrome', 'slate', 'ash', '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'];
if (!validThemes.includes(colorTheme)) {
colorTheme = 'default';
}

View File

@@ -1127,6 +1127,172 @@ Factory Mono is a monochrome Factory variant for FN-6724: keep the industrial mo
font-weight: 700;
}
/*
FNXC:Theming 2026-06-19-15:35:
The shadcn theme for FN-6747 approximates the shadcn/ui new-york zinc aesthetic with sans-serif typography, tokenized comfortable spacing, 1px borders, subtle flat shadows, neutralized glow tokens, and a solid primary button instead of Factory-style outlined uppercase controls.
*/
[data-color-theme="shadcn"] {
--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: #60a5fa;
--in-progress: #38bdf8;
--in-progress-rgb: 56, 189, 248;
--in-review: #34d399;
--triage: #f59e0b;
--done: #71717a;
--color-success: #22c55e;
--color-warning: #f59e0b;
--color-error: #ef4444;
--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);
--transition-instant: 0.05s ease;
--transition-fast: 0.1s ease;
--transition-normal: 0.15s ease;
--transition-slow: 0.2s ease;
--cta-bg: #fafafa;
--cta-border: #fafafa;
--cta-text: #18181b;
--cta-bg-hover: #e4e4e7;
--cta-border-hover: #e4e4e7;
--cta-glow: none;
--logo-accent: var(--text);
--color-info: #38bdf8;
--accent: #fafafa;
--accent-text: #18181b;
}
[data-color-theme="shadcn"][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: #2563eb;
--in-progress: #0284c7;
--in-progress-rgb: 2, 132, 199;
--in-review: #16a34a;
--triage: #d97706;
--done: #a1a1aa;
--color-success: #16a34a;
--color-warning: #d97706;
--color-error: #dc2626;
--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: #18181b;
--cta-border: #18181b;
--cta-text: #fafafa;
--cta-bg-hover: #27272a;
--cta-border-hover: #27272a;
--cta-glow: none;
--logo-accent: var(--text);
--color-info: #0284c7;
--accent: #18181b;
--accent-text: #fafafa;
}
[data-color-theme="shadcn"] .card.agent-active {
border-color: var(--accent);
box-shadow: none;
animation: none;
}
[data-color-theme="shadcn"][data-theme="light"] .card.agent-active {
border-color: var(--accent);
box-shadow: none;
animation: none;
}
[data-color-theme="shadcn"] .btn {
text-transform: none;
letter-spacing: normal;
font-weight: 500;
}
[data-color-theme="shadcn"] .btn-primary,
[data-color-theme="shadcn"] .btn-task-create {
background: var(--cta-bg);
border-color: var(--cta-border);
color: var(--cta-text);
box-shadow: none;
}
[data-color-theme="shadcn"] .btn-primary:hover,
[data-color-theme="shadcn"] .btn-task-create:hover {
background: var(--cta-bg-hover);
border-color: var(--cta-border-hover);
color: var(--cta-text);
box-shadow: none;
}
[data-color-theme="shadcn"] .card,
[data-color-theme="shadcn"] .column {
border-width: var(--btn-border-width);
}
/* AYU - Popular code editor theme with dark and light variants */
[data-color-theme="ayu"] {
--bg: #0f1419;