FN-6724: add Factory Mono theme

Add a monochrome Factory theme with red accents and no glow effects.

- Register Factory Mono across theme validation, typing, selector UI, and startup hydration.
- Define dark and light Factory Mono CSS tokens, swatches, red CTA accents, uppercase controls, and glow-free active states.
- Cover Factory Mono token behavior with a theme hook regression test and document the expanded theme count.
- Add a minor changeset for the published Fusion package.

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

Fusion-Task-Id: FN-6724

Fusion-Task-Lineage: 861fbcef-d1b1-4bfb-bd60-0196ce825771
This commit is contained in:
gsxdsm
2026-06-19 10:34:12 -07:00
parent c16557fb16
commit 26e5514a8e
8 changed files with 228 additions and 2 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": minor
---
Add the `factory-mono` dashboard color theme, a monochrome Factory variant with red accents and neutralized glow effects.

View File

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

@@ -281,6 +281,7 @@ export const COLOR_THEMES = [
"silver",
"solarized",
"factory",
"factory-mono",
"ayu",
"one-dark",
"nord",

View File

@@ -367,6 +367,13 @@
--swatch-sample-4: #06b6d4;
}
.theme-swatch-factory-mono {
--swatch-sample-1: #0a0a0a;
--swatch-sample-2: #1a1a1a;
--swatch-sample-3: #ef4444;
--swatch-sample-4: #ffffff;
}
.theme-swatch-ayu {
--swatch-sample-1: #0f1419;
--swatch-sample-2: #131d27;
@@ -571,6 +578,13 @@
--swatch-sample-4: #0891b2;
}
[data-theme="light"] .theme-swatch-factory-mono {
--swatch-sample-1: #f5f5f5;
--swatch-sample-2: #ffffff;
--swatch-sample-3: #dc2626;
--swatch-sample-4: #1a1a1a;
}
[data-theme="light"] .theme-swatch-ayu {
--swatch-sample-1: #fafafa;
--swatch-sample-2: #f3f3f3;

View File

@@ -42,6 +42,7 @@ const COLOR_THEMES: { value: ColorTheme; label: string; className: string }[] =
{ value: "silver", label: "Silver", className: "theme-swatch-silver" },
{ value: "solarized", label: "Solarized", className: "theme-swatch-solarized" },
{ value: "factory", label: "Factory", className: "theme-swatch-factory" },
{ value: "factory-mono", label: "Factory Mono", className: "theme-swatch-factory-mono" },
{ value: "ayu", label: "Ayu", className: "theme-swatch-ayu" },
{ value: "one-dark", label: "One Dark", className: "theme-swatch-one-dark" },
{ value: "nord", label: "Nord", className: "theme-swatch-nord" },

View File

@@ -491,6 +491,34 @@ describe("useTheme", () => {
document.head.removeChild(style);
});
it("applies factory-mono design tokens without glow effects", () => {
localStorageMock[COLOR_THEME_STORAGE_KEY] = "factory-mono";
renderHook(() => useTheme());
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");
style.textContent = baseCss + "\n" + themeDataCss;
document.head.appendChild(style);
document.documentElement.setAttribute("data-color-theme", "factory-mono");
expect(document.documentElement.getAttribute("data-color-theme")).toBe("factory-mono");
const factoryMonoBlock = themeDataCss.match(/\[data-color-theme="factory-mono"\] \{(?<body>[\s\S]*?)\n\}/)?.groups?.body;
expect(factoryMonoBlock).toBeDefined();
expect(factoryMonoBlock).toContain("--shadow-glow: none;");
expect(factoryMonoBlock).toContain("--glow-success: none;");
expect(factoryMonoBlock).toContain("--glow-warning: none;");
expect(factoryMonoBlock).toContain("--glow-danger: none;");
expect(factoryMonoBlock).toContain("--cta-glow: none;");
expect(factoryMonoBlock).not.toMatch(/--(?:shadow-glow|glow-success|glow-warning|glow-danger|cta-glow):\s*0 0/);
expect(factoryMonoBlock).toContain("--accent: #ef4444;");
expect(factoryMonoBlock).toContain("--cta-border: #ef4444;");
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', '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'];
if (!validThemes.includes(colorTheme)) {
colorTheme = 'default';
}

View File

@@ -950,6 +950,183 @@
font-weight: 700;
}
/*
FNXC:Theming 2026-06-19-00:00:
Factory Mono is a monochrome Factory variant for FN-6724: keep the industrial monospace density, uppercase controls, and 2px border system while limiting color to red accents and removing pulsing glow effects.
*/
[data-color-theme="factory-mono"] {
--bg: #0a0a0a;
--surface: #111111;
--card: #1a1a1a;
--card-hover: #222222;
--surface-hover: color-mix(in srgb, var(--surface) 90%, var(--text) 10%);
--border: #333333;
--text: #f5f5f5;
--text-muted: #a3a3a3;
--text-dim: #737373;
--todo: #d4d4d4;
--in-progress: #a3a3a3;
--in-progress-rgb: 163, 163, 163;
--in-review: #e5e5e5;
--triage: #737373;
--done: #525252;
--color-success: #e5e5e5;
--color-warning: #d4d4d4;
--color-error: #ef4444;
--color-muted: #737373;
--font-primary: "SF Mono", "JetBrains Mono", "Fira Code", Monaco, Consolas, monospace;
--font-mono: "SF Mono", "JetBrains Mono", "Fira Code", Monaco, Consolas, monospace;
--space-xs: 2px;
--space-sm: 4px;
--space-md: 8px;
--space-lg: 12px;
--space-xl: 16px;
--space-2xl: 24px;
--radius-sm: 2px;
--radius-md: 4px;
--radius-lg: 6px;
--radius-xl: 8px;
--radius: var(--radius-md);
--btn-padding: 6px 12px;
--btn-border-width: 2px;
--card-padding: 8px 10px;
--modal-padding: var(--space-md) var(--space-lg);
--header-padding: var(--space-sm) var(--space-lg);
--column-gap: var(--space-sm);
--board-padding: var(--space-md) var(--space-lg);
--shadow-sm: none;
--shadow-md: 0 1px 0 color-mix(in srgb, #000000 60%, transparent);
--shadow-lg: 0 1px 0 color-mix(in srgb, #000000 70%, transparent);
--shadow-glow: none;
--glow-success: none;
--glow-warning: none;
--glow-danger: none;
--focus-ring: 0 0 0 var(--btn-border-width) color-mix(in srgb, var(--accent) 30%, transparent);
--focus-ring-strong: 0 0 0 var(--btn-border-width) 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: transparent;
--cta-border: #ef4444;
--cta-text: #ef4444;
--cta-bg-hover: #ef4444;
--cta-border-hover: #f87171;
--cta-glow: none;
--logo-accent: #ef4444;
--color-info: #d4d4d4;
--accent: #ef4444;
--accent-text: #0a0a0a;
}
[data-color-theme="factory-mono"][data-theme="light"] {
--bg: #f5f5f5;
--surface: #ffffff;
--card: #fafafa;
--card-hover: #f0f0f0;
--surface-hover: color-mix(in srgb, var(--surface) 92%, var(--text) 8%);
--border: #d4d4d4;
--text: #1a1a1a;
--text-muted: #525252;
--text-dim: #737373;
--todo: #525252;
--in-progress: #737373;
--in-progress-rgb: 115, 115, 115;
--in-review: #404040;
--triage: #a3a3a3;
--done: #8a8a8a;
--color-success: #404040;
--color-warning: #525252;
--color-error: #dc2626;
--color-muted: #737373;
--shadow-sm: none;
--shadow-md: 0 1px 0 color-mix(in srgb, var(--text) 12%, transparent);
--shadow-lg: 0 1px 0 color-mix(in srgb, var(--text) 18%, transparent);
--shadow-glow: none;
--glow-success: none;
--glow-warning: none;
--glow-danger: none;
--focus-ring: 0 0 0 var(--btn-border-width) color-mix(in srgb, var(--accent) 25%, transparent);
--focus-ring-strong: 0 0 0 var(--btn-border-width) color-mix(in srgb, var(--accent) 38%, transparent);
--shadow: var(--shadow-lg);
--cta-bg: transparent;
--cta-border: #dc2626;
--cta-text: #dc2626;
--cta-bg-hover: #dc2626;
--cta-border-hover: #ef4444;
--cta-glow: none;
--logo-accent: #dc2626;
--color-info: #525252;
--accent: #dc2626;
--accent-text: #ffffff;
}
[data-color-theme="factory-mono"] .card.agent-active {
border-color: var(--accent);
box-shadow: none;
animation: none;
}
[data-color-theme="factory-mono"][data-theme="light"] .card.agent-active {
border-color: var(--accent);
box-shadow: none;
animation: none;
}
[data-color-theme="factory-mono"] .btn {
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
}
[data-color-theme="factory-mono"] .btn-primary,
[data-color-theme="factory-mono"] .btn-task-create {
background: transparent;
border-color: var(--accent);
color: var(--accent);
box-shadow: none;
}
[data-color-theme="factory-mono"] .btn-primary:hover,
[data-color-theme="factory-mono"] .btn-task-create:hover {
background: var(--accent);
color: var(--accent-text);
box-shadow: none;
}
[data-color-theme="factory-mono"] .card,
[data-color-theme="factory-mono"] .column {
border-width: var(--btn-border-width);
}
[data-color-theme="factory-mono"] .column-header h2 {
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 0.75rem;
}
[data-color-theme="factory-mono"] .column-count,
[data-color-theme="factory-mono"] .card-id {
font-family: var(--font-mono);
font-weight: 700;
}
/* AYU - Popular code editor theme with dark and light variants */
[data-color-theme="ayu"] {
--bg: #0f1419;