From b20a25c526138118ba8238bc48a9fe4c4abba235 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 21 Jun 2026 11:01:52 -0700 Subject: [PATCH] FN-6815: add Shadcn Gray Blue theme Add a slate-neutral Shadcn Gray Blue dashboard theme variant.\n\n- Register shadcn-gray-blue across core theme validation, startup bootstrap, and selector options.\n- Add dark and light slate blue-gray design tokens, swatches, and shared shadcn styling coverage.\n- Extend theme tests, dashboard docs, and the published package changeset for the new variant.\n\nFiles changed:\n .changeset/shadcn-gray-blue-theme.md | 5 +\n docs/dashboard-guide.md | 2 +-\n packages/core/src/types.ts | 2 +\n .../dashboard/app/components/ThemeSelector.css | 15 +++\n .../components/WorkflowOptionalStepsDropdown.css | 3 +-\n packages/dashboard/app/components/themeOptions.ts | 2 +\n .../dashboard/app/hooks/__tests__/useTheme.test.ts | 12 ++\n packages/dashboard/app/index.html | 2 +-\n packages/dashboard/app/public/theme-data.css | 143 ++++++++++++++++++++-\n 9 files changed, 177 insertions(+), 9 deletions(-) Fusion-Task-Id: FN-6815 Fusion-Task-Lineage: 71cca9b3-12ab-4b91-93c5-e3d2498501ce --- .changeset/shadcn-gray-blue-theme.md | 5 + docs/dashboard-guide.md | 2 +- packages/core/src/types.ts | 2 + .../app/components/ThemeSelector.css | 15 ++ .../WorkflowOptionalStepsDropdown.css | 3 +- .../dashboard/app/components/themeOptions.ts | 2 + .../app/hooks/__tests__/useTheme.test.ts | 12 ++ packages/dashboard/app/index.html | 2 +- packages/dashboard/app/public/theme-data.css | 143 +++++++++++++++++- 9 files changed, 177 insertions(+), 9 deletions(-) create mode 100644 .changeset/shadcn-gray-blue-theme.md diff --git a/.changeset/shadcn-gray-blue-theme.md b/.changeset/shadcn-gray-blue-theme.md new file mode 100644 index 0000000000..2393b7bec0 --- /dev/null +++ b/.changeset/shadcn-gray-blue-theme.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": minor +--- + +Add the `shadcn-gray-blue` dashboard color theme with slate blue-gray surfaces and a muted slate-blue accent. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 7e9602bbb5..a66caab865 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -1291,7 +1291,7 @@ Non-Command-Center dashboard CSS uses `--text` as the canonical primary text tok ### Theme system -Dark/light modes via `data-theme`; 73 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 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), 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. +Dark/light modes via `data-theme`; 74 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 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. - **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. diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index f2094f69fa..04adcb5926 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -332,6 +332,7 @@ export const COLOR_THEMES = [ "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-20-00:00: FN-6813 renames the grayscale-base mono theme to shadcn-mono-red and adds the remaining mono accent variants; keep Shadcn Gray adjacent to Shadcn Black so the color-family order stays stable with FN-6814. + // FNXC:DashboardTheming 2026-06-21-00:00: FN-6815 adds shadcn-gray-blue as the slate-neutral blue-gray sibling; keep it adjacent to Shadcn Gray so the published union mirrors dashboard option order. "shadcn-blue", "shadcn-green", "shadcn-red", @@ -348,6 +349,7 @@ export const COLOR_THEMES = [ "shadcn-mono-yellow", "shadcn-black", "shadcn-gray", + "shadcn-gray-blue", ] as const; export type ColorTheme = (typeof COLOR_THEMES)[number]; diff --git a/packages/dashboard/app/components/ThemeSelector.css b/packages/dashboard/app/components/ThemeSelector.css index be444d668f..1063dcb0c0 100644 --- a/packages/dashboard/app/components/ThemeSelector.css +++ b/packages/dashboard/app/components/ThemeSelector.css @@ -502,6 +502,14 @@ --swatch-sample-4: #27272a; } +/* FNXC:DashboardTheming 2026-06-21-00:00: FN-6815 previews Shadcn Gray Blue with slate surfaces and a muted slate-blue accent so the selector chip communicates the blue-gray neutral ramp, not just another blue accent. */ +.theme-swatch-shadcn-gray-blue { + --swatch-sample-1: #020617; + --swatch-sample-2: #0f172a; + --swatch-sample-3: #64748b; + --swatch-sample-4: #1e293b; +} + .theme-swatch-ayu { --swatch-sample-1: #0f1419; --swatch-sample-2: #131d27; @@ -833,6 +841,13 @@ --swatch-sample-4: #e4e4e7; } +[data-theme="light"] .theme-swatch-shadcn-gray-blue { + --swatch-sample-1: #ffffff; + --swatch-sample-2: #f1f5f9; + --swatch-sample-3: #475569; + --swatch-sample-4: #e2e8f0; +} + [data-theme="light"] .theme-swatch-ayu { --swatch-sample-1: #fafafa; --swatch-sample-2: #f3f3f3; diff --git a/packages/dashboard/app/components/WorkflowOptionalStepsDropdown.css b/packages/dashboard/app/components/WorkflowOptionalStepsDropdown.css index a733ddda74..a0aafa3839 100644 --- a/packages/dashboard/app/components/WorkflowOptionalStepsDropdown.css +++ b/packages/dashboard/app/components/WorkflowOptionalStepsDropdown.css @@ -51,7 +51,8 @@ .wf-optional-steps-dropdown-option:hover, .wf-optional-steps-dropdown-option.is-active { - background: var(--surface-hover, rgba(127, 127, 127, 0.12)); + /* FNXC:DashboardTheming 2026-06-21-00:00: Status/theme-token tests ban raw rgba fallbacks for --surface-hover; use the shared color-mix fallback so every color theme, including newly added shadcn variants, keeps tokenized hover surfaces. */ + background: var(--surface-hover, color-mix(in srgb, var(--surface) 90%, var(--text) 10%)); } .wf-optional-steps-dropdown-option:focus-visible { diff --git a/packages/dashboard/app/components/themeOptions.ts b/packages/dashboard/app/components/themeOptions.ts index b3fe0d73a0..fdd1ac1458 100644 --- a/packages/dashboard/app/components/themeOptions.ts +++ b/packages/dashboard/app/components/themeOptions.ts @@ -86,4 +86,6 @@ export const COLOR_THEMES: { value: ColorTheme; label: string; className: string { 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" }, + /* FNXC:DashboardTheming 2026-06-21-00:00: FN-6815 exposes the slate-neutral Shadcn Gray Blue option next to Shadcn Gray so users can pick a blue-tinted gray surface ramp rather than only a blue accent. */ + { value: "shadcn-gray-blue", label: "Shadcn Gray Blue", className: "theme-swatch-shadcn-gray-blue" }, ]; diff --git a/packages/dashboard/app/hooks/__tests__/useTheme.test.ts b/packages/dashboard/app/hooks/__tests__/useTheme.test.ts index 4c7df026bb..eb264e981c 100644 --- a/packages/dashboard/app/hooks/__tests__/useTheme.test.ts +++ b/packages/dashboard/app/hooks/__tests__/useTheme.test.ts @@ -560,6 +560,7 @@ describe("useTheme", () => { { id: "shadcn-mono-red", accent: "#ef4444" }, { id: "shadcn-mono-blue", accent: "#3b82f6" }, { id: "shadcn-black", accent: "#fafafa" }, + { id: "shadcn-gray-blue", accent: "#64748b", card: "#0f172a" }, ] as const; style.textContent = `${baseCss}\n${themeDataCss}`; document.head.appendChild(style); @@ -580,6 +581,9 @@ describe("useTheme", () => { expect(document.documentElement.getAttribute("data-color-theme")).toBe(variant.id); expect(block).toContain("--btn-border-width: 1px;"); expect(block).toContain(`--accent: ${variant.accent};`); + if ("card" in variant) { + expect(block).toContain(`--card: ${variant.card};`); + } expect(block).toContain("--shadow-glow: none;"); expect(block).toContain("--cta-glow: none;"); expect(block).not.toMatch(/--(?:shadow-glow|glow-success|glow-warning|glow-danger|cta-glow):\s*0 0/); @@ -589,6 +593,9 @@ describe("useTheme", () => { expect(resolvedStyle.getPropertyValue("--shadow-glow").trim()).toBe("none"); expect(resolvedStyle.getPropertyValue("--cta-glow").trim()).toBe("none"); expect(resolvedStyle.getPropertyValue("--btn-border-width").trim()).toBe("1px"); + if ("card" in variant) { + expect(resolvedStyle.getPropertyValue("--card").trim()).toBe(variant.card); + } document.head.removeChild(variantStyle); } @@ -601,6 +608,8 @@ describe("useTheme", () => { ?.groups?.body; const blackBlock = themeDataCss.match(/\[data-color-theme="shadcn-black"\] \{(?[\s\S]*?)\n\}/) ?.groups?.body; + const grayBlueBlock = themeDataCss.match(/\[data-color-theme="shadcn-gray-blue"\] \{(?[\s\S]*?)\n\}/) + ?.groups?.body; expect(blueBlock).toContain("--todo: #60a5fa;"); expect(monoRedBlock).toContain("--todo: #a1a1aa;"); expect(monoRedBlock).toContain("--in-progress: #71717a;"); @@ -611,6 +620,9 @@ describe("useTheme", () => { expect(blackBlock).toContain("--todo: #d4d4d8;"); expect(blackBlock).toContain("--in-progress: #a1a1aa;"); expect(blackBlock).not.toContain("--todo: #60a5fa;"); + expect(grayBlueBlock).toContain("--card: #0f172a;"); + expect(grayBlueBlock).not.toContain("--card: #18181b;"); + expect(grayBlueBlock).toContain("--accent: #64748b;"); document.head.removeChild(style); }); diff --git a/packages/dashboard/app/index.html b/packages/dashboard/app/index.html index 6926a9fe59..fba912c99c 100644 --- a/packages/dashboard/app/index.html +++ b/packages/dashboard/app/index.html @@ -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-red', 'shadcn-mono-blue', 'shadcn-mono-green', 'shadcn-mono-purple', 'shadcn-mono-pink', 'shadcn-mono-orange', 'shadcn-mono-yellow', 'shadcn-black', 'shadcn-gray']; + 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-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-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'; if (!validThemes.includes(colorTheme)) { diff --git a/packages/dashboard/app/public/theme-data.css b/packages/dashboard/app/public/theme-data.css index 9f6920025b..4d38297bab 100644 --- a/packages/dashboard/app/public/theme-data.css +++ b/packages/dashboard/app/public/theme-data.css @@ -3311,6 +3311,128 @@ Shadcn Gray is the fully-neutral zinc variant: it keeps the shadcn surfaces and --accent-text: #ffffff; } +/* +FNXC:Theming 2026-06-21-00:00: +FN-6815 adds Shadcn Gray Blue as the slate-neutral shadcn variant: the whole surface ramp moves from zinc to blue-gray slate while CTA/accent tokens use muted slate-blue and the shadcn flat-shadow/no-glow treatment stays intact. +*/ +[data-color-theme="shadcn-gray-blue"] { + --bg: #020617; + --surface: #0b1220; + --card: #0f172a; + --card-hover: #1e293b; + --surface-hover: color-mix(in srgb, var(--surface) 90%, var(--text) 10%); + --border: #1e293b; + + --text: #f8fafc; + --text-muted: #94a3b8; + --text-dim: #475569; + + --todo: #60a5fa; + --in-progress: #38bdf8; + --in-progress-rgb: 56, 189, 248; + --in-review: #34d399; + --triage: #f59e0b; + --done: #64748b; + + --color-success: #22c55e; + --color-warning: #f59e0b; + --color-error: #ef4444; + --color-muted: #64748b; + + --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: #64748b; + --cta-border: #64748b; + --cta-text: #ffffff; + --cta-bg-hover: #475569; + --cta-border-hover: #475569; + --cta-glow: none; + --logo-accent: var(--accent); + --color-info: #64748b; + --accent: #64748b; + --accent-text: #ffffff; +} + +[data-color-theme="shadcn-gray-blue"][data-theme="light"] { + --bg: #ffffff; + --surface: #ffffff; + --card: #ffffff; + --card-hover: #f1f5f9; + --surface-hover: color-mix(in srgb, var(--surface) 92%, var(--text) 8%); + --border: #e2e8f0; + + --text: #0f172a; + --text-muted: #64748b; + --text-dim: #94a3b8; + + --todo: #2563eb; + --in-progress: #0284c7; + --in-progress-rgb: 2, 132, 199; + --in-review: #16a34a; + --triage: #d97706; + --done: #94a3b8; + + --color-success: #16a34a; + --color-warning: #d97706; + --color-error: #dc2626; + --color-muted: #64748b; + + --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: #475569; + --cta-border: #475569; + --cta-text: #ffffff; + --cta-bg-hover: #334155; + --cta-border-hover: #334155; + --cta-glow: none; + --logo-accent: var(--accent); + --color-info: #475569; + --accent: #475569; + --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, @@ -3326,7 +3448,8 @@ Shadcn Gray is the fully-neutral zinc variant: it keeps the shadcn surfaces and [data-color-theme="shadcn-mono-orange"] .card.agent-active, [data-color-theme="shadcn-mono-yellow"] .card.agent-active, [data-color-theme="shadcn-black"] .card.agent-active, -[data-color-theme="shadcn-gray"] .card.agent-active { +[data-color-theme="shadcn-gray"] .card.agent-active, +[data-color-theme="shadcn-gray-blue"] .card.agent-active { border-color: var(--accent); box-shadow: none; animation: none; @@ -3347,7 +3470,8 @@ Shadcn Gray is the fully-neutral zinc variant: it keeps the shadcn surfaces and [data-color-theme="shadcn-mono-orange"][data-theme="light"] .card.agent-active, [data-color-theme="shadcn-mono-yellow"][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 { +[data-color-theme="shadcn-gray"][data-theme="light"] .card.agent-active, +[data-color-theme="shadcn-gray-blue"][data-theme="light"] .card.agent-active { border-color: var(--accent); box-shadow: none; animation: none; @@ -3368,7 +3492,8 @@ Shadcn Gray is the fully-neutral zinc variant: it keeps the shadcn surfaces and [data-color-theme="shadcn-mono-orange"] .btn, [data-color-theme="shadcn-mono-yellow"] .btn, [data-color-theme="shadcn-black"] .btn, -[data-color-theme="shadcn-gray"] .btn { +[data-color-theme="shadcn-gray"] .btn, +[data-color-theme="shadcn-gray-blue"] .btn { text-transform: none; letter-spacing: normal; font-weight: 500; @@ -3405,7 +3530,9 @@ Shadcn Gray is the fully-neutral zinc variant: it keeps the shadcn surfaces and [data-color-theme="shadcn-black"] .btn-primary, [data-color-theme="shadcn-black"] .btn-task-create, [data-color-theme="shadcn-gray"] .btn-primary, -[data-color-theme="shadcn-gray"] .btn-task-create { +[data-color-theme="shadcn-gray"] .btn-task-create, +[data-color-theme="shadcn-gray-blue"] .btn-primary, +[data-color-theme="shadcn-gray-blue"] .btn-task-create { background: var(--cta-bg); border-color: var(--cta-border); color: var(--cta-text); @@ -3443,7 +3570,9 @@ Shadcn Gray is the fully-neutral zinc variant: it keeps the shadcn surfaces and [data-color-theme="shadcn-black"] .btn-primary: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 { +[data-color-theme="shadcn-gray"] .btn-task-create:hover, +[data-color-theme="shadcn-gray-blue"] .btn-primary:hover, +[data-color-theme="shadcn-gray-blue"] .btn-task-create:hover { background: var(--cta-bg-hover); border-color: var(--cta-border-hover); color: var(--cta-text); @@ -3481,7 +3610,9 @@ Shadcn Gray is the fully-neutral zinc variant: it keeps the shadcn surfaces and [data-color-theme="shadcn-black"] .card, [data-color-theme="shadcn-black"] .column, [data-color-theme="shadcn-gray"] .card, -[data-color-theme="shadcn-gray"] .column { +[data-color-theme="shadcn-gray"] .column, +[data-color-theme="shadcn-gray-blue"] .card, +[data-color-theme="shadcn-gray-blue"] .column { border-width: var(--btn-border-width); }