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:
5
.changeset/shadcn-dashboard-theme.md
Normal file
5
.changeset/shadcn-dashboard-theme.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
@@ -327,6 +327,7 @@ export const COLOR_THEMES = [
|
||||
"lavender",
|
||||
"neon-bloom",
|
||||
"sepia",
|
||||
"shadcn",
|
||||
] as const;
|
||||
export type ColorTheme = (typeof COLOR_THEMES)[number];
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" },
|
||||
];
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user