FN-214: add Cozy Cartoon dashboard theme

Add a Cozy Cartoon theme with synchronized dashboard and desktop styling, controls, documentation, and regression coverage.

- Add the pastel light and cozy-night dark palettes with theme selector previews.
- Apply rounded, oversized controls while preserving compact action-row sizing.
- Document the theme and update theme types, metadata, and tests.

Files changed:
 .changeset/fn-214-cozy-cartoon-theme.md            |   6 +
 docs/dashboard-guide.md                            |   5 +-
 docs/settings-reference.md                         |   3 +-
 packages/core/src/types/ui/execution-and-ui.ts     |   2 +
 .../app/__tests__/cozy-cartoon-theme.test.ts       | 294 +++++++++++++++++++++
 .../quick-entry-action-row-height-parity.test.tsx  |   1 +
 .../quick-entry-workflow-trigger-height.test.tsx   |   1 +
 .../dashboard/app/components/ThemeSelector.css     |  16 ++
 .../components/__tests__/ThemeDropdown.test.tsx    |   2 +-
 .../components/__tests__/ThemeSelector.test.tsx    |   2 +-
 .../__tests__/CommandCenterControls.test.tsx       |  11 +-
 packages/dashboard/app/components/themeOptions.ts  |   1 +
 packages/dashboard/app/index.html                  |   2 +-
 packages/dashboard/app/public/theme-data.css       |  97 +++++++
 packages/dashboard/app/styles.css                  |   9 +
 packages/desktop/src/renderer/index.html           |   1 +
 16 files changed, 446 insertions(+), 7 deletions(-)

Fusion-Task-Id: FN-214

Fusion-Task-Lineage: f56cbadd-bbf0-4eb0-83b5-89d3fefd6f47

Co-authored-by: Fusion <noreply@runfusion.ai>
This commit is contained in:
Fusion Agent
2026-08-28 08:06:35 +00:00
parent 532fabc1ac
commit eb299666c2
16 changed files with 446 additions and 7 deletions

View File

@@ -0,0 +1,6 @@
---
"@runfusion/fusion": minor
---
summary: Add the Cozy Cartoon color theme with a pastel light palette and oversized rounded buttons.
category: feature

View File

@@ -2323,12 +2323,13 @@ Tokens in the table are stable. Renaming or removing one requires a deprecation
<!-- FNXC:DashboardTheming 2026-08-18-05:45: Velvet is a persisted plum/burgundy palette with rose-gold dark-mode accents and a warm blush-white light counterpart; keep its inventory entry and count synchronized with COLOR_THEMES. -->
<!-- FNXC:DashboardTheming 2026-08-23-01:51: Iceberg is a persisted navy-slate palette with a periwinkle-blue accent and pale blue-gray light counterpart; keep its id/order synchronized across core, selector metadata, both first-paint validators, token blocks, swatch CSS, and this inventory. -->
<!-- FNXC:DashboardTheming 2026-08-27-04:23: Flexoki is a persisted warm inky palette with muted ink-blue and violet dark accents and a cream paper light counterpart; keep its inventory entry and count synchronized with COLOR_THEMES. -->
<!-- FNXC:DashboardTheming 2026-08-28-07:45: Cozy Cartoon is a persisted cream pastel light palette with coral, mint, sky, and lavender accents, oversized rounded buttons, and a warm cozy-night dark counterpart; keep its inventory entry and count synchronized with COLOR_THEMES. -->
<!-- FNXC:DashboardTheming 2026-08-20-00:33: Medieval is a persisted palette with readable local Pixelify Sans for ordinary UI, deliberate mono code/terminal exceptions, CSS paper texture, and a shared wood-framed generic modal; keep this operator inventory synchronized with COLOR_THEMES. -->
<!-- FNXC:DashboardTheming 2026-07-21-12:20: FN-8471 keeps the Settings current-theme row and Command Center compact trigger on one filterable shared list so operators can discover any rendered theme name without changing the persisted selection until they choose an option. -->
Dark/light modes via `data-theme`; fresh installs default to System mode so the resolved theme follows `prefers-color-scheme` until the user explicitly chooses Light, Dark, or System. 92 color themes via `data-color-theme` (lazy-loaded from `app/public/theme-data.css`), including Cobalt (saturated blue), Clay (warm terracotta), Moss (muted forest green), Aurora (navy/teal/violet dark surfaces with a misty blue-white light counterpart), Calm (low-stimulation slate/sage dark surfaces with a misty blue-white light counterpart), Dawn (indigo/plum dark surfaces with muted amber accents and a dawn-white light counterpart), Sage (soft green-gray surfaces with a muted olive accent and readable dark/light ramps), Midnight (deep-navy surfaces with restrained violet/indigo accents and a cool pale light counterpart), Velvet (deep plum/burgundy dark surfaces with rose-gold accents and a warm blush-white light counterpart), Iceberg (navy-slate dark surfaces with a periwinkle-blue accent and a pale blue-gray light counterpart), Flexoki (warm inky near-black dark surfaces with muted ink-blue and violet accents and a cream paper light counterpart), Medieval (readable local Pixelify Sans for ordinary UI, deliberate mono code/terminal exceptions, CSS paper texture, and wood-framed generic modals), Factory Dark (graphite/steel dark surfaces, cool gunmetal borders, and restrained safety-orange accents with a concrete-gray light counterpart), and Factory Light (bright concrete/warm-white surfaces, steel-gray borders, a darkened safety-orange accent, and a warm-graphite dark counterpart), the Shadcn zinc-neutral theme with an orange default highlight/accent, Shadcn Custom (the same base with sanitized per-token color-picker overrides), and its color family: Shadcn Blue/Green/Red/Purple/Pink/Orange/Yellow, Shadcn Mono and Shadcn Mono Red/Blue/Green/Purple/Pink/Orange/Yellow (grayscale surfaces with color-specific accents; Shadcn Mono retains its selected id), 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. Glass Silver preserves the Glass theme's frosted translucent surfaces and transparent modal overlay behavior while using silver and graphite accents instead of purple/pink.
Dark/light modes via `data-theme`; fresh installs default to System mode so the resolved theme follows `prefers-color-scheme` until the user explicitly chooses Light, Dark, or System. 93 color themes via `data-color-theme` (lazy-loaded from `app/public/theme-data.css`), including Cobalt (saturated blue), Clay (warm terracotta), Moss (muted forest green), Aurora (navy/teal/violet dark surfaces with a misty blue-white light counterpart), Calm (low-stimulation slate/sage dark surfaces with a misty blue-white light counterpart), Dawn (indigo/plum dark surfaces with muted amber accents and a dawn-white light counterpart), Sage (soft green-gray surfaces with a muted olive accent and readable dark/light ramps), Midnight (deep-navy surfaces with restrained violet/indigo accents and a cool pale light counterpart), Velvet (deep plum/burgundy dark surfaces with rose-gold accents and a warm blush-white light counterpart), Iceberg (navy-slate dark surfaces with a periwinkle-blue accent and a pale blue-gray light counterpart), Flexoki (warm inky near-black dark surfaces with muted ink-blue and violet accents and a cream paper light counterpart), Cozy Cartoon (cream pastel light surfaces with soft coral, mint, sky, and lavender accents, oversized rounded buttons, and a warm cozy-night dark counterpart), Medieval (readable local Pixelify Sans for ordinary UI, deliberate mono code/terminal exceptions, CSS paper texture, and wood-framed generic modals), Factory Dark (graphite/steel dark surfaces, cool gunmetal borders, and restrained safety-orange accents with a concrete-gray light counterpart), and Factory Light (bright concrete/warm-white surfaces, steel-gray borders, a darkened safety-orange accent, and a warm-graphite dark counterpart), the Shadcn zinc-neutral theme with an orange default highlight/accent, Shadcn Custom (the same base with sanitized per-token color-picker overrides), and its color family: Shadcn Blue/Green/Red/Purple/Pink/Orange/Yellow, Shadcn Mono and Shadcn Mono Red/Blue/Green/Purple/Pink/Orange/Yellow (grayscale surfaces with color-specific accents; Shadcn Mono retains its selected id), 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. Glass Silver preserves the Glass theme's frosted translucent surfaces and transparent modal overlay behavior while using silver and graphite accents instead of purple/pink.
Choose color themes from **Settings → Appearance** or from the Command Center **Overview** theme card. Settings merges its selector into the current-theme row, which opens the full color-theme list; Command Center keeps the compact `ThemeDropdown` trigger. Both use the same filterable list: type a visible theme name to narrow the displayed labels and color-chip swatches, then select an option to change the theme. Filtering alone never changes the chosen theme. The selected color theme, including Midnight, Velvet, Iceberg, Flexoki, and Medieval, persists in browser storage and is validated before React hydrates on both web and Electron startup. The left sidebar active-item highlight and resize accent use the active theme's `--accent`, so they follow the selected Shadcn accent instead of staying fixed blue.
Choose color themes from **Settings → Appearance** or from the Command Center **Overview** theme card. Settings merges its selector into the current-theme row, which opens the full color-theme list; Command Center keeps the compact `ThemeDropdown` trigger. Both use the same filterable list: type a visible theme name to narrow the displayed labels and color-chip swatches, then select an option to change the theme. Filtering alone never changes the chosen theme. The selected color theme, including Midnight, Velvet, Iceberg, Flexoki, Cozy Cartoon, and Medieval, persists in browser storage and is validated before React hydrates on both web and Electron startup. The left sidebar active-item highlight and resize accent use the active theme's `--accent`, so they follow the selected Shadcn accent instead of staying fixed blue.
- **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

@@ -73,12 +73,13 @@ Defaults from `DEFAULT_GLOBAL_SETTINGS`; key scope from `GLOBAL_SETTINGS_KEYS`.
<!-- FNXC:DashboardTheming 2026-08-23-01:51: Iceberg is a persisted navy-slate palette with a periwinkle-blue accent and pale blue-gray light counterpart; keep its id/order synchronized across core, selector metadata, both first-paint validators, token blocks, swatch CSS, and settings documentation. -->
<!-- FNXC:DashboardTheming 2026-08-27-04:23: Flexoki is a persisted warm inky palette with muted ink-blue and violet dark accents and a cream paper light counterpart; keep its settings description synchronized with the selector metadata. -->
<!-- FNXC:DashboardTheming 2026-08-28-07:45: Cozy Cartoon publishes its cream pastel flagship palette, cozy-night dark counterpart, and larger rounded button geometry as one persisted theme choice. -->
| Setting | Type | Default | Description |
|---|---|---:|---|
| <a id="anthropicauthpreference"></a>`anthropicAuthPreference` | `"api-key" \| "subscription"` | `"api-key"` | Which Anthropic credential wins when BOTH a raw API key and a Claude subscription OAuth login are configured. `"api-key"` keeps the historical precedence; `"subscription"` moves the raw key below the OAuth steps so a stale or revoked saved key cannot shadow a working subscription (the `401 invalid x-api-key` failure mode). Global, because credentials live in the global `~/.fusion/agent/auth.json`. Never removes a source — with one credential configured, resolution reaches it under either value. Surfaced in Settings → Authentication, where the two Anthropic cards show which credential is in use; the control appears only when both are connected. |
| `themeMode` | `"dark" \| "light" \| "system"` | `"system"` | Dashboard theme mode. Fresh installs follow the operating system light/dark preference until the user chooses Light, Dark, or System. |
| `colorTheme` | `ColorTheme` | `"shadcn-ember"` | Dashboard color theme preset. Medieval adds CSS-generated paper texture, a wood-framed generic modal treatment, and the locally bundled Pixelify Sans ordinary UI font with no CDN/runtime font request; code and terminal surfaces remain monospace. Aurora is a built-in navy/teal/violet palette with a misty light counterpart; Calm is a low-stimulation slate/sage palette with a misty light counterpart; Dawn uses indigo/plum dark surfaces, amber accents, and dawn-white light surfaces; Midnight uses deep-navy surfaces with restrained violet/indigo accents and a cool pale light counterpart; Velvet uses deep plum/burgundy dark surfaces with rose-gold accents and a warm blush-white light counterpart; Iceberg uses navy-slate dark surfaces with a periwinkle-blue accent and a pale blue-gray light counterpart; Flexoki uses warm inky near-black dark surfaces with muted ink-blue and violet accents and a cream paper light counterpart; Factory Dark uses graphite/steel surfaces, cool gunmetal borders, and restrained safety-orange accents with a concrete-gray light counterpart; Factory Light uses bright concrete/warm-white surfaces, steel-gray borders, a darkened safety-orange accent, and a warm-graphite dark counterpart. Use `"shadcn-custom"` to show the separate custom shadcn color picker in Settings → Appearance and the Command Center theme card. |
| `colorTheme` | `ColorTheme` | `"shadcn-ember"` | Dashboard color theme preset. Medieval adds CSS-generated paper texture, a wood-framed generic modal treatment, and the locally bundled Pixelify Sans ordinary UI font with no CDN/runtime font request; code and terminal surfaces remain monospace. Aurora is a built-in navy/teal/violet palette with a misty light counterpart; Calm is a low-stimulation slate/sage palette with a misty light counterpart; Dawn uses indigo/plum dark surfaces, amber accents, and dawn-white light surfaces; Midnight uses deep-navy surfaces with restrained violet/indigo accents and a cool pale light counterpart; Velvet uses deep plum/burgundy dark surfaces with rose-gold accents and a warm blush-white light counterpart; Iceberg uses navy-slate dark surfaces with a periwinkle-blue accent and a pale blue-gray light counterpart; Flexoki uses warm inky near-black dark surfaces with muted ink-blue and violet accents and a cream paper light counterpart; Cozy Cartoon uses a cream pastel light palette with soft coral, mint, sky, and lavender accents, a warm cozy-night dark counterpart, and larger rounded button geometry; Factory Dark uses graphite/steel surfaces, cool gunmetal borders, and restrained safety-orange accents with a concrete-gray light counterpart; Factory Light uses bright concrete/warm-white surfaces, steel-gray borders, a darkened safety-orange accent, and a warm-graphite dark counterpart. Use `"shadcn-custom"` to show the separate custom shadcn color picker in Settings → Appearance and the Command Center theme card. |
| `shadcnCustomColors` | `Record<string, string>` | `undefined` | Optional shadcn design-token override map for `"shadcn-custom"` only. Keys are CSS token names such as `--accent`, `--bg`, `--surface`, `--card`, `--border`, `--text`, `--text-muted`, workflow status tokens, and `--color-success`/`--color-warning`/`--color-error`; values must be sanitized `#RGB` or `#RRGGBB` hex colors. Missing or invalid entries fall back to the `shadcn-custom` base defaults and are not applied to other themes. |
| `language` | `"en" \| "zh-CN" \| "zh-TW" \| "fr" \| "es" \| "ko" \| "pt-BR"` | `undefined` | UI language for the dashboard and TUI. When unset, the dashboard detects from localStorage → browser language and the CLI from `--lang` flag → environment locale, falling back to `en`. Validated at the store write boundary (`validateLocale`); invalid values are dropped. Reset to auto-detect via the dashboard's "Auto" language option or `fn settings set language auto` (clears the persisted key). |
| `dashboardFontScalePct` | `number` | `100` | Dashboard font scale percentage used by Appearance settings. Valid range: `85` to `125`; applied pre-hydration via document root font-size so board typography (column headers/counts, task cards, and quick-entry text) scales with the setting from first paint. |

View File

@@ -160,6 +160,8 @@ export const COLOR_THEMES = [
"iceberg",
// FNXC:DashboardTheming 2026-08-27-04:23: Flexoki is persisted immediately after Iceberg; keep this ID and order synchronized across core, selector metadata, both first-paint validators, token blocks, and swatch CSS so saved selections survive pre-hydration validation.
"flexoki",
// FNXC:DashboardTheming 2026-08-28-07:45: Cozy Cartoon must stay order-synchronized across core, selector metadata, both first-paint validators, token blocks, and swatch CSS so persisted selections survive startup without a fallback flash.
"cozy-cartoon",
// FNXC:DashboardTheming 2026-07-31-20:39: Factory Dark must stay order-synchronized across core, selector metadata, and both first-paint validators so saved preferences survive pre-hydration validation.
"factory-dark",
// FNXC:DashboardTheming 2026-07-31-23:51: Factory Light must stay order-synchronized across core, selector metadata, and both first-paint validators so saved preferences survive pre-hydration validation.

View File

@@ -0,0 +1,294 @@
import { createElement as h } from "react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, within } from "@testing-library/react";
import { COLOR_THEMES as CORE_COLOR_THEMES, type ColorTheme } from "@fusion/core";
import { fetchGlobalSettings, updateGlobalSettings } from "../api";
import { resolveColorTheme } from "../components/ThemeDropdown";
import { ThemeSelector } from "../components/ThemeSelector";
import { COLOR_THEMES as DASHBOARD_COLOR_THEMES } from "../components/themeOptions";
import { useTheme } from "../hooks/useTheme";
import { loadStylesCss, loadThemeDataCss, readAppFile } from "../test/cssFixture";
vi.mock("../api", async (importOriginal) => {
const { createDashboardApiMock } = await import("../test/mockApi");
return createDashboardApiMock(() => importOriginal<typeof import("../api")>(), {
fetchGlobalSettings: vi.fn(),
updateGlobalSettings: vi.fn(),
});
});
const mockFetchGlobalSettings = vi.mocked(fetchGlobalSettings);
const mockUpdateGlobalSettings = vi.mocked(updateGlobalSettings);
/*
FNXC:DashboardTheming 2026-08-28-07:45:
Cozy Cartoon is complete only when persistence, both selector hosts, web and Electron first paint, mode-specific pastel tokens and previews, and its oversized ordinary-button contract agree. Compact controls, mobile touch floors, and the desktop ViewHeader remain shared invariants rather than theme-specific geometry.
*/
describe("Cozy Cartoon color theme", () => {
const themeData = loadThemeDataCss();
const themeSelectorCss = readAppFile("components/ThemeSelector.css");
const styles = loadStylesCss();
const cssNoComments = styles.replace(/\/\*[\s\S]*?\*\//g, "");
const dashboardIndexHtml = readAppFile("index.html");
const desktopIndexHtml = readAppFile("../../desktop/src/renderer/index.html");
const settingsReference = readAppFile("../../../docs/settings-reference.md");
const dashboardGuide = readAppFile("../../../docs/dashboard-guide.md");
let stylesheet: HTMLStyleElement | undefined;
beforeEach(() => {
localStorage.clear();
document.documentElement.removeAttribute("data-theme");
document.documentElement.removeAttribute("data-color-theme");
document.documentElement.style.cssText = "";
mockFetchGlobalSettings.mockReset();
mockFetchGlobalSettings.mockImplementation(() => new Promise(() => {}));
mockUpdateGlobalSettings.mockReset();
mockUpdateGlobalSettings.mockResolvedValue({});
stylesheet = document.createElement("style");
stylesheet.textContent = `${themeData}\n${styles}`;
document.head.appendChild(stylesheet);
});
afterEach(() => {
stylesheet?.remove();
vi.clearAllMocks();
});
it("keeps persisted, selector, and first-paint registries in exact order", () => {
const coreIds = [...CORE_COLOR_THEMES];
const dashboardIds = DASHBOARD_COLOR_THEMES.map((theme) => theme.value);
const dashboardValidThemes = extractValidThemes(dashboardIndexHtml);
const desktopValidThemes = extractValidThemes(desktopIndexHtml);
expect(CORE_COLOR_THEMES.filter((theme) => theme === "cozy-cartoon")).toHaveLength(1);
expect(DASHBOARD_COLOR_THEMES.filter((theme) => theme.value === "cozy-cartoon")).toEqual([{
value: "cozy-cartoon",
label: "Cozy Cartoon",
className: "theme-swatch-cozy-cartoon",
}]);
expect(dashboardIds).toEqual(coreIds);
expect(dashboardValidThemes).toEqual(coreIds);
expect(desktopValidThemes).toEqual(coreIds);
for (const ids of [coreIds, dashboardIds, dashboardValidThemes, desktopValidThemes]) {
expect(new Set(ids).size).toBe(ids.length);
expect(ids.filter((id) => id === "cozy-cartoon")).toHaveLength(1);
expect(ids[ids.indexOf("flexoki") + 1]).toBe("cozy-cartoon");
}
expect(dashboardIndexHtml).toContain("colorTheme = 'shadcn-ember'");
expect(desktopIndexHtml).toContain('colorTheme = "shadcn-ember"');
});
it("keeps both selector hosts on the shared ThemeDropdown registry", () => {
const dropdown = readAppFile("components/ThemeDropdown.tsx");
const commandCenter = readAppFile("components/command-center/CommandCenterControls.tsx");
const settingsSelector = readAppFile("components/ThemeSelector.tsx");
expect(dropdown).toContain('from "./themeOptions"');
expect(dropdown).toContain("COLOR_THEMES");
expect(dropdown).toContain("export function resolveColorTheme");
expect(dropdown).toContain("COLOR_THEMES.find");
expect(commandCenter).toContain('from "../ThemeDropdown"');
expect(commandCenter).toContain("<ThemeDropdown");
expect(commandCenter).not.toContain("themeOptions");
expect(commandCenter).not.toContain("COLOR_THEMES");
expect(commandCenter).not.toContain("cozy-cartoon");
expect(settingsSelector).toContain("<ThemeDropdown");
expect(settingsSelector).not.toContain("COLOR_THEMES");
});
it("falls back unknown ids and resolves Cozy Cartoon from the canonical registry", () => {
expect(resolveColorTheme("not-a-theme" as ColorTheme)).toBe(DASHBOARD_COLOR_THEMES[0]);
expect(resolveColorTheme("cozy-cartoon")).toEqual({
value: "cozy-cartoon",
label: "Cozy Cartoon",
className: "theme-swatch-cozy-cartoon",
});
});
it.each(["dark", "light"] as const)("selects and persists Cozy Cartoon through Settings in %s mode", (themeMode) => {
localStorage.setItem("kb-dashboard-theme-mode", themeMode);
render(h(CozyCartoonThemeFixture));
fireEvent.click(screen.getByRole("button", { name: /^current theme/i }));
const listbox = screen.getByRole("listbox", { name: "Color theme" });
fireEvent.change(screen.getByRole("searchbox", { name: /filter color themes/i }), { target: { value: "cozy" } });
fireEvent.click(within(listbox).getByRole("option", { name: "Cozy Cartoon" }));
expect(document.documentElement).toHaveAttribute("data-theme", themeMode);
expect(document.documentElement).toHaveAttribute("data-color-theme", "cozy-cartoon");
expect(localStorage.getItem("kb-dashboard-color-theme")).toBe("cozy-cartoon");
});
it("defines complete readable token blocks with pinned WCAG contrast", () => {
const darkBlock = extractSelectorBlock(themeData, '[data-color-theme="cozy-cartoon"]');
const lightBlock = extractSelectorBlock(themeData, '[data-color-theme="cozy-cartoon"][data-theme="light"]');
const requiredTokens = [
"--bg:", "--surface:", "--card:", "--card-hover:", "--surface-hover:", "--border:",
"--text:", "--text-muted:", "--text-dim:", "--todo:", "--in-progress:", "--in-progress-rgb:",
"--in-review:", "--triage:", "--done:", "--color-success:", "--color-warning:",
"--color-error:", "--color-info:", "--cta-bg:", "--cta-border:", "--cta-text:",
"--cta-bg-hover:", "--cta-border-hover:", "--cta-glow:", "--accent:", "--accent-text:",
"--logo-accent:", "--shadow-glow:", "--focus-ring:", "--focus-ring-strong:",
];
for (const block of [darkBlock, lightBlock]) {
for (const token of requiredTokens) expect(block).toContain(token);
expect(block).not.toMatch(/rgba?\(/);
for (const [foreground, background] of [
["--text", "--bg"],
["--text", "--card"],
["--text-muted", "--bg"],
["--cta-text", "--cta-bg"],
["--accent-text", "--accent"],
] as const) {
expect(contrastRatio(tokenHex(block, foreground), tokenHex(block, background))).toBeGreaterThanOrEqual(4.5);
}
expect(contrastRatio(tokenHex(block, "--text-dim"), tokenHex(block, "--bg"))).toBeGreaterThanOrEqual(3);
}
expect(darkBlock).toContain("--bg: #241d24;");
expect(darkBlock).toContain("--accent: #f4a7a3;");
expect(lightBlock).toContain("--bg: #fff7ef;");
expect(lightBlock).toContain("--accent: #e07a6a;");
});
it("keeps the Cozy button rule top-level and inherits the existing mobile touch floors", () => {
const idx = cssNoComments.indexOf('[data-color-theme="cozy-cartoon"]');
expect(idx).not.toBe(-1);
const before = cssNoComments.slice(0, idx);
expect((before.match(/\{/g)?.length ?? 0) - (before.match(/\}/g)?.length ?? 0)).toBe(0);
const mobileSelectorIdx = cssNoComments.indexOf(".btn:not(.btn-icon):not(.btn-badge):not(.btn-sm):not(.btn--sm)", idx);
const mobileMediaIdx = cssNoComments.lastIndexOf("@media (max-width: 768px)", mobileSelectorIdx);
expect(mobileMediaIdx).not.toBe(-1);
const mobileBlock = extractAtRuleBlock(cssNoComments, mobileMediaIdx);
expect(extractSelectorBlock(mobileBlock, ".btn:not(.btn-icon):not(.btn-badge):not(.btn-sm):not(.btn--sm)")).toContain("min-height: 36px");
const iconRule = extractSelectorBlock(mobileBlock, ".btn-icon");
expect(iconRule).toContain("min-width: 36px");
expect(iconRule).toContain("min-height: 36px");
});
it("enlarges token geometry without overriding ViewHeader height contracts", () => {
const rootBlock = extractSelectorBlock(styles, ":root");
const themeBlock = extractSelectorBlock(themeData, '[data-color-theme="cozy-cartoon"]');
const scopedButton = extractSelectorBlock(styles, '[data-color-theme="cozy-cartoon"] .btn:not(.btn-sm):not(.btn--sm)');
const declaredProperties = [...scopedButton.matchAll(/^\s*([a-z-]+)\s*:/gm)].map((match) => match[1]);
expect(declaredProperties).toEqual(["font-size", "font-weight"]);
expect(scopedButton).not.toMatch(/min-height/);
expect(scopedButton).not.toMatch(/(?:^|\n)\s*height\s*:/);
for (const token of ["--btn-padding", "--btn-border-width", "--radius-md", "--radius-lg", "--icon-size-md"] as const) {
expect(cssTokenValue(themeBlock, token)).not.toBe(cssTokenValue(rootBlock, token));
}
expect(themeData).not.toContain("--quick-entry-action-row-height-desktop");
expect(themeData).not.toContain("--quick-entry-action-row-height-mobile");
expect(themeBlock).not.toContain("--icon-size-sm");
});
it("excludes compact controls by selector shape without changing shared button rules", () => {
const cozySelectors = [...cssNoComments.matchAll(/([^{}]*\[data-color-theme="cozy-cartoon"\][^{}]*)\{/g)]
.map((match) => match[1].trim());
expect(cozySelectors).toEqual(['[data-color-theme="cozy-cartoon"] .btn:not(.btn-sm):not(.btn--sm)']);
const selector = cozySelectors[0];
expect(selector).toContain(":not(.btn-sm)");
expect(selector).toContain(":not(.btn--sm)");
const targetedCompound = selector.replace(/:not\([^)]*\)/g, "");
for (const compactTarget of [".btn-sm", ".btn--sm", ".quick-entry", ".view-header"]) {
expect(targetedCompound).not.toContain(compactTarget);
}
expect(extractSelectorBlock(styles, ".btn-sm")).toContain("padding: 4px 10px");
expect(extractSelectorBlock(styles, ".btn")).toContain("padding: var(--btn-padding)");
});
it("uses globally resolvable Cozy Cartoon swatches in both modes", () => {
const darkGlobals = extractSelectorBlock(themeData, ":root");
const lightGlobals = extractSelectorBlock(themeData, '[data-theme="light"]');
const darkSwatch = extractSelectorBlock(themeSelectorCss, ".theme-swatch-cozy-cartoon");
const lightSwatch = extractSelectorBlock(themeSelectorCss, '[data-theme="light"] .theme-swatch-cozy-cartoon');
for (const block of [darkGlobals, lightGlobals]) {
for (const sample of [1, 2, 3, 4]) expect(block).toContain(`--cozy-cartoon-swatch-sample-${sample}:`);
}
for (const block of [darkSwatch, lightSwatch]) {
for (const sample of [1, 2, 3, 4]) {
expect(block).toContain(`--swatch-sample-${sample}: var(--cozy-cartoon-swatch-sample-${sample});`);
}
expect(block).not.toContain("var(--accent)");
expect(block).not.toContain("var(--bg)");
}
});
it("publishes Cozy Cartoon in the operator theme catalogs", () => {
const colorThemeRow = settingsReference.split("\n").find((line) => line.startsWith("| `colorTheme` |"));
expect(colorThemeRow).toContain("Cozy Cartoon");
expect(dashboardGuide).toContain("Cozy Cartoon");
expect(dashboardGuide).toContain("93 color themes");
});
});
function CozyCartoonThemeFixture() {
const theme = useTheme();
return h(ThemeSelector, {
themeMode: theme.themeMode,
colorTheme: theme.colorTheme as ColorTheme,
dashboardFontScalePct: theme.dashboardFontScalePct,
resolvedThemeMode: theme.resolvedThemeMode,
shadcnCustomColors: theme.shadcnCustomColors,
onThemeModeChange: theme.setThemeMode,
onColorThemeChange: theme.setColorTheme,
onDashboardFontScaleChange: theme.setDashboardFontScalePct,
onShadcnCustomColorsChange: theme.setShadcnCustomColors,
});
}
function extractValidThemes(html: string): string[] {
const match = html.match(/var validThemes = \[([\s\S]*?)\];/);
if (!match) throw new Error("Could not find pre-hydration validThemes array");
return [...match[1].matchAll(/["']([^"']+)["']/g)].map((themeMatch) => themeMatch[1]);
}
function extractSelectorBlock(css: string, selector: string): string {
const startIdx = css.indexOf(`${selector} {`);
if (startIdx === -1) throw new Error(`Could not find selector block: ${selector}`);
return extractBraceBlock(css, startIdx);
}
function extractAtRuleBlock(css: string, startIdx: number): string {
return extractBraceBlock(css, startIdx);
}
function extractBraceBlock(css: string, startIdx: number): string {
const openBraceIdx = css.indexOf("{", startIdx);
if (openBraceIdx === -1) throw new Error("Could not find opening brace");
let depth = 1;
for (let index = openBraceIdx + 1; index < css.length; index++) {
if (css[index] === "{") depth++;
if (css[index] === "}") depth--;
if (depth === 0) return css.slice(startIdx, index + 1);
}
throw new Error("Could not find closing brace");
}
function cssTokenValue(block: string, token: string): string {
const match = block.match(new RegExp(`${token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*:\\s*([^;]+);`));
if (!match) throw new Error(`Could not find token: ${token}`);
return match[1].trim();
}
function tokenHex(block: string, token: string): string {
const value = cssTokenValue(block, token);
if (!/^#[0-9a-f]{6}$/i.test(value)) throw new Error(`${token} is not a six-digit hex color: ${value}`);
return value;
}
function contrastRatio(foreground: string, background: string): number {
const lighter = Math.max(relativeLuminance(foreground), relativeLuminance(background));
const darker = Math.min(relativeLuminance(foreground), relativeLuminance(background));
return (lighter + 0.05) / (darker + 0.05);
}
function relativeLuminance(hex: string): number {
const channels = [1, 3, 5].map((offset) => Number.parseInt(hex.slice(offset, offset + 2), 16) / 255);
const [red, green, blue] = channels.map((channel) => channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4);
return 0.2126 * red + 0.7152 * green + 0.0722 * blue;
}

View File

@@ -85,6 +85,7 @@ vi.mock("lucide-react", () => ({
Lightbulb: () => null,
ListTree: () => null,
Sparkles: () => null,
ShieldCheck: () => null,
Save: () => null,
X: () => null,
ChevronDown: () => null,

View File

@@ -75,6 +75,7 @@ vi.mock("lucide-react", () => ({
Lightbulb: () => null,
ListTree: () => null,
Sparkles: () => null,
ShieldCheck: () => null,
Save: () => null,
X: () => null,
ChevronDown: () => null,

View File

@@ -342,6 +342,14 @@
--swatch-sample-4: var(--flexoki-swatch-sample-4);
}
/* FNXC:DashboardTheming 2026-08-28-07:45: Cozy Cartoon maps only its global mode-specific cream, coral, and lavender samples so the pastel preview remains accurate before selection and stays synchronized with the persisted theme registry. */
.theme-swatch-cozy-cartoon {
--swatch-sample-1: var(--cozy-cartoon-swatch-sample-1);
--swatch-sample-2: var(--cozy-cartoon-swatch-sample-2);
--swatch-sample-3: var(--cozy-cartoon-swatch-sample-3);
--swatch-sample-4: var(--cozy-cartoon-swatch-sample-4);
}
.theme-swatch-aurora {
--swatch-sample-1: var(--aurora-swatch-sample-1);
--swatch-sample-2: var(--aurora-swatch-sample-2);
@@ -1246,6 +1254,14 @@
--swatch-sample-4: var(--flexoki-swatch-sample-4);
}
/* FNXC:DashboardTheming 2026-08-28-07:45: The light selector chip resolves the Cozy Cartoon flagship pastel samples globally instead of borrowing active-theme colors. */
[data-theme="light"] .theme-swatch-cozy-cartoon {
--swatch-sample-1: var(--cozy-cartoon-swatch-sample-1);
--swatch-sample-2: var(--cozy-cartoon-swatch-sample-2);
--swatch-sample-3: var(--cozy-cartoon-swatch-sample-3);
--swatch-sample-4: var(--cozy-cartoon-swatch-sample-4);
}
[data-theme="light"] .theme-swatch-aurora {
--swatch-sample-1: var(--aurora-swatch-sample-1);
--swatch-sample-2: var(--aurora-swatch-sample-2);

View File

@@ -7,7 +7,7 @@ import { readAppFile } from "../../test/cssFixture";
// FNXC:Theme 2026-07-16-14:30: FN-8146 pins the historical Settings-grid set, including restored shadcn-mono, so a removal from COLOR_THEMES cannot make the all-themes checks pass circularly.
// FNXC:DashboardTheming 2026-08-23-01:51: Iceberg must remain after Velvet so the Command Center theme trigger proves the shared persisted registry and global swatch contract render together.
const EXPECTED_THEME_IDS = ['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', 'medieval', 'terminal', 'glass', 'glass-silver', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'midnight', 'velvet', 'iceberg', 'flexoki', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', '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'] as const;
const EXPECTED_THEME_IDS = ['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', 'medieval', 'terminal', 'glass', 'glass-silver', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'midnight', 'velvet', 'iceberg', 'flexoki', 'cozy-cartoon', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', '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'] as const;
function renderedThemeIds(listbox: HTMLElement) {
return within(listbox).getAllByRole("option").map((option) => {

View File

@@ -6,7 +6,7 @@ import { ThemeSelector } from "../ThemeSelector";
// FNXC:Theme 2026-07-16-14:30: FN-8146 pins the historical Settings-grid set, including restored shadcn-mono, so a removal from COLOR_THEMES cannot make the all-themes checks pass circularly.
// FNXC:Theme 2026-08-16-05:07: FN-8730 (5931e10345) added the Midnight preset; the census gains "midnight" between sage and factory-dark to match COLOR_THEMES order.
// FNXC:DashboardTheming 2026-08-23-01:51: Iceberg must remain after Velvet so the Settings selector proves the shared persisted registry and global swatch contract render together.
const EXPECTED_THEME_IDS = ['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', 'medieval', 'terminal', 'glass', 'glass-silver', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'midnight', 'velvet', 'iceberg', 'flexoki', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', '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'] as const;
const EXPECTED_THEME_IDS = ['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', 'medieval', 'terminal', 'glass', 'glass-silver', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'midnight', 'velvet', 'iceberg', 'flexoki', 'cozy-cartoon', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', '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'] as const;
function renderedThemeIds(listbox: HTMLElement) {
return within(listbox).getAllByRole("option").map((option) => {

View File

@@ -78,7 +78,7 @@ function cssRule(css: string, selector: string) {
// FNXC:Theme 2026-07-16-14:30: FN-8146 pins the historical Settings-grid set, including restored shadcn-mono, so a removal from COLOR_THEMES cannot make the all-themes checks pass circularly.
// FNXC:Theme 2026-08-16-05:07: FN-8730 (5931e10345) added the Midnight preset; the census gains "midnight" between sage and factory-dark to match COLOR_THEMES order.
// FNXC:DashboardTheming 2026-08-23-01:51: Iceberg must remain after Velvet so the Command Center controls prove the shared persisted registry and global swatch contract render together.
const EXPECTED_THEME_IDS = ['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', 'medieval', 'terminal', 'glass', 'glass-silver', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'midnight', 'velvet', 'iceberg', 'flexoki', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', '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'] as const;
const EXPECTED_THEME_IDS = ['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', 'medieval', 'terminal', 'glass', 'glass-silver', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'midnight', 'velvet', 'iceberg', 'flexoki', 'cozy-cartoon', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', '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'] as const;
function renderedThemeIds(listbox: HTMLElement) {
return within(listbox).getAllByRole("option").map((option) => {
@@ -124,6 +124,15 @@ describe("CommandCenterControls concurrency markers", () => {
expect(onColorThemeChange).toHaveBeenCalledWith("medieval");
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
fireEvent.click(trigger);
fireEvent.change(screen.getByRole("searchbox", { name: /filter color themes/i }), { target: { value: "cozy" } });
const cozyOption = screen.getByRole("option", { name: "Cozy Cartoon" });
expect(cozyOption.querySelector(".theme-swatch-cozy-cartoon")).toBeTruthy();
fireEvent.click(cozyOption);
expect(onColorThemeChange).toHaveBeenCalledWith("cozy-cartoon");
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
});
// FNXC:GlobalConcurrencyControls 2026-07-15-12:00: FN-8007 requires dashboard markers to use the exact native-thumb coordinate system when the expanded range max exceeds the persisted cap.

View File

@@ -86,6 +86,7 @@ export const COLOR_THEMES: { value: ColorTheme; label: string; className: string
{ value: "iceberg", label: "Iceberg", className: "theme-swatch-iceberg" },
// FNXC:DashboardTheming 2026-08-27-04:23: Flexoki follows Iceberg in this shared selector source so Settings and Command Center preserve the persisted registry order and swatch mapping.
{ value: "flexoki", label: "Flexoki", className: "theme-swatch-flexoki" },
{ value: "cozy-cartoon", label: "Cozy Cartoon", className: "theme-swatch-cozy-cartoon" },
{ value: "factory-dark", label: "Factory Dark", className: "theme-swatch-factory-dark" },
{ value: "factory-light", label: "Factory Light", className: "theme-swatch-factory-light" },
{ value: "shadcn", label: "Shadcn", className: "theme-swatch-shadcn" },

View File

@@ -172,7 +172,7 @@
var colorTheme = localStorage.getItem('kb-dashboard-color-theme') || 'shadcn-ember';
// FNXC:DashboardTheming 2026-08-23-01:51: Iceberg must remain after Velvet in this first-paint validator, core, selector metadata, Electron validation, token blocks, and swatch CSS so persisted selections do not flash or fall back.
// FNXC:DashboardTheming 2026-08-27-04:23: Flexoki must stay immediately after Iceberg here and across the persisted registry, selector, Electron validator, token blocks, and swatches so a saved selection applies before hydration.
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', 'medieval', 'terminal', 'glass', 'glass-silver', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'midnight', 'velvet', 'iceberg', 'flexoki', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', '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'];
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', 'medieval', 'terminal', 'glass', 'glass-silver', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'midnight', 'velvet', 'iceberg', 'flexoki', 'cozy-cartoon', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', '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-30-00:00: Unset startup theme is Shadcn Ember; explicit stored legacy ids such as "default" and "ocean" remain valid and must not be migrated.
// FNXC:DashboardTheming 2026-07-16-14:30: FN-8146 restores Shadcn Mono as a distinct persisted choice, so bootstrap must retain its stored id instead of remapping it to Mono Red.
// FNXC:DashboardTheming 2026-08-19-19:23: Web first paint must accept persisted Medieval before React hydrates; invalid or unset values still fall back to Shadcn Ember.

View File

@@ -44,6 +44,11 @@
--flexoki-swatch-sample-2: #282726;
--flexoki-swatch-sample-3: #4385be;
--flexoki-swatch-sample-4: #8b7ec8;
/* FNXC:DashboardTheming 2026-08-28-07:45: Cozy Cartoon previews stay globally resolvable in both modes so its pastel chip remains accurate before selection; keep them synchronized with the persisted theme order and oversized-button theme contract. */
--cozy-cartoon-swatch-sample-1: #241d24;
--cozy-cartoon-swatch-sample-2: #3a2e39;
--cozy-cartoon-swatch-sample-3: #f4a7a3;
--cozy-cartoon-swatch-sample-4: #c9b3f0;
--medieval-swatch-sample-1: #21170f;
--medieval-swatch-sample-2: #392417;
--medieval-swatch-sample-3: #d5ad67;
@@ -91,6 +96,11 @@
--flexoki-swatch-sample-2: #e6e4d9;
--flexoki-swatch-sample-3: #205ea6;
--flexoki-swatch-sample-4: #5e409d;
/* FNXC:DashboardTheming 2026-08-28-07:45: Light-mode Cozy Cartoon previews expose the flagship cream, coral, and lavender palette globally rather than borrowing whichever theme is active. */
--cozy-cartoon-swatch-sample-1: #fff7ef;
--cozy-cartoon-swatch-sample-2: #f0d9c6;
--cozy-cartoon-swatch-sample-3: #e07a6a;
--cozy-cartoon-swatch-sample-4: #8a6fd0;
--medieval-swatch-sample-1: #f4ead2;
--medieval-swatch-sample-2: #e1ceb0;
--medieval-swatch-sample-3: #765125;
@@ -7864,6 +7874,93 @@ Flexoki is a persisted warm inky palette with a cream paper light counterpart. K
--focus-ring-strong: 0 0 0 2px color-mix(in srgb, var(--accent) 28%, transparent);
}
/*
FNXC:DashboardTheming 2026-08-28-07:45:
Cozy Cartoon pairs a flagship light pastel palette with a warm cozy-night dark counterpart and oversized rounded controls. Keep its id, ordered registries, token blocks, and swatch mappings synchronized so saved selections paint correctly before hydration.
*/
[data-color-theme="cozy-cartoon"] {
--bg: #241d24;
--surface: #2e2530;
--card: #3a2e39;
--card-hover: #453548;
--surface-hover: color-mix(in srgb, var(--surface) 90%, var(--text) 10%);
--border: #4c3d4c;
--text: #f6e8e0;
--text-muted: #cdb8b0;
--text-dim: #a08d88;
--todo: #f2cf8f;
--in-progress: #8fc9f2;
--in-progress-rgb: 143, 201, 242;
--in-review: #c9b3f0;
--triage: #f2b19c;
--done: #9c8f97;
--color-success: #a8d8b0;
--color-warning: #f2d59a;
--color-error: #f2a1a1;
--color-info: #9fd8d2;
--cta-bg: #f4a7a3;
--cta-border: #f9c2bf;
--cta-text: #2b1a1c;
--cta-bg-hover: #f9bcb8;
--cta-border-hover: #ffd6d3;
--cta-glow: 0 0 10px color-mix(in srgb, var(--cta-border) 40%, transparent);
--accent: #f4a7a3;
--accent-text: #2b1a1c;
--logo-accent: var(--accent);
--shadow-glow: 0 0 14px color-mix(in srgb, var(--accent) 30%, transparent);
--focus-ring: 0 0 0 3px color-mix(in srgb, var(--accent) 28%, transparent);
--focus-ring-strong: 0 0 0 3px color-mix(in srgb, var(--accent) 42%, transparent);
--radius-sm: 10px;
--radius-md: 14px;
--radius-lg: 20px;
--radius-xl: 26px;
--radius-pill: 999px;
--btn-padding: 12px 22px;
--btn-border-width: 2px;
--icon-size-md: 18px;
--cozy-cartoon-btn-font-size: 15px;
--cozy-cartoon-btn-font-weight: 600;
}
[data-color-theme="cozy-cartoon"][data-theme="light"] {
--bg: #fff7ef;
--surface: #fffdf9;
--card: #ffffff;
--card-hover: #fdeadd;
--surface-hover: color-mix(in srgb, var(--surface) 92%, var(--text) 8%);
--border: #f0d9c6;
--text: #43312b;
--text-muted: #7a655c;
--text-dim: #9a857b;
--todo: #e8a33d;
--in-progress: #4a9fd4;
--in-progress-rgb: 74, 159, 212;
--in-review: #8a6fd0;
--triage: #e0705f;
--done: #9a857b;
--color-success: #4f9d69;
--color-warning: #c98a1e;
--color-error: #d1544c;
--color-info: #3f97a8;
--cta-bg: #ffb3ab;
--cta-border: #f79a90;
--cta-text: #4a2326;
--cta-bg-hover: #ffc2b8;
--cta-border-hover: #f7a9a0;
--cta-glow: 0 0 10px color-mix(in srgb, var(--cta-border) 45%, transparent);
--accent: #e07a6a;
--accent-text: #3a1f1c;
--logo-accent: var(--accent);
--shadow-glow: 0 0 12px color-mix(in srgb, var(--accent) 22%, transparent);
--focus-ring: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
--focus-ring-strong: 0 0 0 3px color-mix(in srgb, var(--accent) 34%, transparent);
}
[data-color-theme="aurora"] {
--bg: #0a1022;
--surface: #101a35;

View File

@@ -1343,6 +1343,15 @@ Operator feedback requires one readable pixel face for ordinary prose, links, da
font-family: var(--font-mono) !important;
}
/*
FNXC:DashboardTheming 2026-08-28-07:45:
The shared .btn rule hardcodes its font size, so Cozy Cartoon needs this single token-backed typography override to make ordinary buttons visibly larger. The compact exclusions preserve every .btn-sm/.btn--sm control, including the height-pinned Quick Add action row, while omitting height and min-height lets the desktop ViewHeader clamp keep trimming enlarged button padding without stretching headers.
*/
[data-color-theme="cozy-cartoon"] .btn:not(.btn-sm):not(.btn--sm) {
font-size: var(--cozy-cartoon-btn-font-size);
font-weight: var(--cozy-cartoon-btn-font-weight);
}
.modal {
position: relative;
background: var(--surface);

View File

@@ -86,6 +86,7 @@
"iceberg",
// FNXC:DashboardTheming 2026-08-27-04:23: Flexoki remains after Iceberg in Electron first-paint validation and must stay synchronized with core, selector metadata, web validation, token blocks, and swatch CSS.
"flexoki",
"cozy-cartoon",
"factory-dark",
"factory-light",
"shadcn",