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:
5
.changeset/factory-mono-theme.md
Normal file
5
.changeset/factory-mono-theme.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
@@ -281,6 +281,7 @@ export const COLOR_THEMES = [
|
||||
"silver",
|
||||
"solarized",
|
||||
"factory",
|
||||
"factory-mono",
|
||||
"ayu",
|
||||
"one-dark",
|
||||
"nord",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user