diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 3ee78f7463..4a333d0d3e 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -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`; 56 color themes via `data-color-theme` (lazy-loaded from `app/public/theme-data.css`), including the Shadcn zinc-neutral theme. +Dark/light modes via `data-theme`; 57 color themes via `data-color-theme` (lazy-loaded from `app/public/theme-data.css`), including the Shadcn zinc-neutral theme. 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 30f3ac9035..da6c136c50 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -283,6 +283,8 @@ export const COLOR_THEMES = [ "monochrome", "slate", "ash", + // FNXC:DashboardTheming 2026-06-19-15:36: "air" is the source-of-truth id for the minimal, borderless, paper-like dashboard theme; keep bootstrap validators and theme options in sync with this union. + "air", "graphite", "silver", "solarized", diff --git a/packages/dashboard/app/__tests__/air-theme.test.ts b/packages/dashboard/app/__tests__/air-theme.test.ts new file mode 100644 index 0000000000..d0108db7b5 --- /dev/null +++ b/packages/dashboard/app/__tests__/air-theme.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; +import { COLOR_THEMES as CORE_COLOR_THEMES } from "@fusion/core"; +import { COLOR_THEMES as DASHBOARD_COLOR_THEMES } from "../components/themeOptions"; +import fs from "fs"; +import path from "path"; + +const themeDataPath = path.resolve(__dirname, "../public/theme-data.css"); +const dashboardIndexPath = path.resolve(__dirname, "../index.html"); +const desktopIndexPath = path.resolve(__dirname, "../../../desktop/src/renderer/index.html"); + +describe("Air color theme", () => { + const themeData = fs.readFileSync(themeDataPath, "utf-8"); + + it("defines dark and light Air theme blocks with clean font and faint borders", () => { + const darkBlock = extractSelectorBlock(themeData, '[data-color-theme="air"]'); + const lightBlock = extractSelectorBlock(themeData, '[data-color-theme="air"][data-theme="light"]'); + + expect(darkBlock).toContain('--font-primary: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif'); + expect(lightBlock).toContain('--font-primary: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif'); + expect(darkBlock).toContain("--border: color-mix(in srgb, var(--text) 8%, transparent)"); + expect(lightBlock).toContain("--border: color-mix(in srgb, var(--text) 8%, transparent)"); + expect(`${darkBlock}\n${lightBlock}`).not.toMatch(/rgba\(/); + expect(`${darkBlock}\n${lightBlock}`).not.toMatch(/#[0-9a-fA-F]{8}\b/); + }); + + it("registers Air in core, dashboard options, and both bootstrap validators", () => { + expect(CORE_COLOR_THEMES).toContain("air"); + expect(DASHBOARD_COLOR_THEMES).toContainEqual({ + value: "air", + label: "Air", + className: "theme-swatch-air", + }); + + expect(fs.readFileSync(dashboardIndexPath, "utf-8")).toContain("'air'"); + expect(fs.readFileSync(desktopIndexPath, "utf-8")).toContain('"air"'); + }); +}); + +function extractSelectorBlock(css: string, selector: string): string { + const startIdx = css.indexOf(`${selector} {`); + if (startIdx === -1) { + throw new Error(`Could not find selector block: ${selector}`); + } + + const openBraceIdx = css.indexOf("{", startIdx); + let depth = 1; + let end = openBraceIdx; + for (let i = openBraceIdx + 1; i < css.length; i++) { + if (css[i] === "{") depth++; + if (css[i] === "}") depth--; + if (depth === 0) { + end = i; + break; + } + } + + return css.slice(startIdx, end + 1); +} diff --git a/packages/dashboard/app/components/ThemeSelector.css b/packages/dashboard/app/components/ThemeSelector.css index e76cf65b41..03ff59e6b9 100644 --- a/packages/dashboard/app/components/ThemeSelector.css +++ b/packages/dashboard/app/components/ThemeSelector.css @@ -248,6 +248,13 @@ --swatch-sample-4: #d6d3d1; } +.theme-swatch-air { + --swatch-sample-1: #0f1115; + --swatch-sample-2: #151820; + --swatch-sample-3: #a8bfd8; + --swatch-sample-4: #eef1f4; +} + .theme-swatch-graphite { --swatch-sample-1: #18181b; --swatch-sample-2: #27272a; @@ -760,6 +767,13 @@ --swatch-sample-4: #768694; } +[data-theme="light"] .theme-swatch-air { + --swatch-sample-1: #fbfbfa; + --swatch-sample-2: #ffffff; + --swatch-sample-3: #536b86; + --swatch-sample-4: #26313d; +} + [data-theme="light"] .theme-swatch-graphite { --swatch-sample-1: #fafafa; --swatch-sample-2: #f4f4f5; diff --git a/packages/dashboard/app/components/themeOptions.ts b/packages/dashboard/app/components/themeOptions.ts index 06af4b5866..7f801d8153 100644 --- a/packages/dashboard/app/components/themeOptions.ts +++ b/packages/dashboard/app/components/themeOptions.ts @@ -23,6 +23,7 @@ export const COLOR_THEMES: { value: ColorTheme; label: string; className: string { value: "monochrome", label: "Mono", className: "theme-swatch-monochrome" }, { value: "slate", label: "Slate", className: "theme-swatch-slate" }, { value: "ash", label: "Ash", className: "theme-swatch-ash" }, + { value: "air", label: "Air", className: "theme-swatch-air" }, { value: "graphite", label: "Graphite", className: "theme-swatch-graphite" }, { value: "silver", label: "Silver", className: "theme-swatch-silver" }, { value: "solarized", label: "Solarized", className: "theme-swatch-solarized" }, diff --git a/packages/dashboard/app/index.html b/packages/dashboard/app/index.html index 7a7d197b96..d0a46ec21f 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', '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']; + 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']; if (!validThemes.includes(colorTheme)) { colorTheme = 'default'; } diff --git a/packages/dashboard/app/public/theme-data.css b/packages/dashboard/app/public/theme-data.css index 3620212ffb..5c21c05036 100644 --- a/packages/dashboard/app/public/theme-data.css +++ b/packages/dashboard/app/public/theme-data.css @@ -333,6 +333,107 @@ --accent-text: #fff; } +/* +FNXC:DashboardTheming 2026-06-19-15:39: +Air must be the calmest, flattest theme: near-monochrome tokens, a restrained blue-gray accent, a clean system sans stack, and faint color-mix borders. +The "remove unnecessary UI" requirement is intentionally CSS-only and scoped to [data-color-theme="air"] so shared DOM affordances remain present while borders, shadows, and decorative chrome flatten visually. +*/ +[data-color-theme="air"] { + --bg: #0f1115; + --surface: #151820; + --card: #171a22; + --card-hover: color-mix(in srgb, var(--card) 92%, var(--text) 8%); + --text: #eef1f4; + --text-muted: #9aa3ad; + --border: color-mix(in srgb, var(--text) 8%, transparent); + --surface-hover: color-mix(in srgb, var(--surface) 90%, var(--text) 10%); + --font-primary: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif; + --todo: #a5adba; + --in-progress: #b8c0cc; + --in-progress-rgb: 184, 192, 204; + --in-review: #c7ced8; + --triage: #8d96a3; + --done: #737d8a; + --color-success: #c7ced8; + --color-error: #d8a7a7; + --color-warning: #b7ad9a; + --color-info: #a8bfd8; + --cta-bg: #dbe5ef; + --cta-border: color-mix(in srgb, var(--cta-bg) 72%, transparent); + --cta-text: #111318; + --cta-bg-hover: #eef3f8; + --cta-border-hover: color-mix(in srgb, var(--cta-bg-hover) 82%, transparent); + --cta-glow: 0 0 0 transparent; + --logo-accent: var(--accent); + --accent: #a8bfd8; + --accent-text: #111318; +} + +[data-color-theme="air"][data-theme="light"] { + --bg: #fbfbfa; + --surface: #ffffff; + --card: #ffffff; + --card-hover: color-mix(in srgb, var(--card) 96%, var(--text) 4%); + --text: #1f242b; + --text-muted: #69717c; + --border: color-mix(in srgb, var(--text) 8%, transparent); + --surface-hover: color-mix(in srgb, var(--surface) 92%, var(--text) 8%); + --font-primary: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif; + --todo: #667085; + --in-progress: #4f5f73; + --in-progress-rgb: 79, 95, 115; + --in-review: #5f6f83; + --triage: #7a7368; + --done: #8a929f; + --color-success: #526273; + --color-error: #b45f5f; + --color-warning: #8a7760; + --color-info: #536b86; + --cta-bg: #26313d; + --cta-border: color-mix(in srgb, var(--cta-bg) 76%, transparent); + --cta-text: #ffffff; + --cta-bg-hover: #354253; + --cta-border-hover: color-mix(in srgb, var(--cta-bg-hover) 84%, transparent); + --cta-glow: 0 0 0 transparent; + --logo-accent: var(--accent); + --accent: #536b86; + --accent-text: #ffffff; +} + +[data-color-theme="air"] .card, +[data-color-theme="air"] .column, +[data-color-theme="air"] .panel, +[data-color-theme="air"] .modal, +[data-color-theme="air"] .task-card { + border-color: var(--border); + box-shadow: 0 0 0 transparent; + background-image: none; +} + +[data-color-theme="air"] .btn, +[data-color-theme="air"] .btn-icon, +[data-color-theme="air"] .input, +[data-color-theme="air"] input, +[data-color-theme="air"] textarea, +[data-color-theme="air"] select { + border-color: var(--border); + box-shadow: 0 0 0 transparent; +} + +[data-color-theme="air"] .btn:hover, +[data-color-theme="air"] .btn-icon:hover, +[data-color-theme="air"] .card:hover, +[data-color-theme="air"] .task-card:hover { + box-shadow: 0 0 0 transparent; +} + +[data-color-theme="air"] .badge, +[data-color-theme="air"] .pill, +[data-color-theme="air"] .tag { + border-color: var(--border); + box-shadow: 0 0 0 transparent; +} + /* GRAPHITE - Dark graphite blacks */ [data-color-theme="graphite"] { --surface-hover: color-mix(in srgb, var(--surface) 90%, var(--text) 10%); diff --git a/packages/desktop/src/renderer/index.html b/packages/desktop/src/renderer/index.html index 5355ae23e3..dda9e51ed0 100644 --- a/packages/desktop/src/renderer/index.html +++ b/packages/desktop/src/renderer/index.html @@ -22,6 +22,7 @@ "monochrome", "slate", "ash", + "air", "graphite", "silver", "solarized",