FN-6748: add Air dashboard theme
Adds a minimal, borderless Air color theme across dashboard theme selection and bootstrap validation. - Register Air in the core theme union, dashboard theme options, and browser/desktop bootstrap validators. - Add dark and light Air token blocks with flattened chrome styling and matching swatch samples. - Cover Air registration and CSS invariants with a focused dashboard test. - Document the Air theme alongside the existing Shadcn theme in dashboard theming guidance. Files changed: docs/dashboard-guide.md | 2 +- packages/core/src/types.ts | 2 + packages/dashboard/app/__tests__/air-theme.test.ts | 58 ++++++++++++ .../dashboard/app/components/ThemeSelector.css | 14 +++ packages/dashboard/app/components/themeOptions.ts | 1 + packages/dashboard/app/index.html | 2 +- packages/dashboard/app/public/theme-data.css | 101 +++++++++++++++++++++ packages/desktop/src/renderer/index.html | 1 + 8 files changed, 179 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-6748 Fusion-Task-Lineage: a0caf14f-673d-485c-8886-97e4b273b624
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
58
packages/dashboard/app/__tests__/air-theme.test.ts
Normal file
58
packages/dashboard/app/__tests__/air-theme.test.ts
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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%);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"monochrome",
|
||||
"slate",
|
||||
"ash",
|
||||
"air",
|
||||
"graphite",
|
||||
"silver",
|
||||
"solarized",
|
||||
|
||||
Reference in New Issue
Block a user