Files
fusion/packages/dashboard/app/components/ThemeSelector.tsx
gsxdsm 6206af8ce9 FN-8146: restore persistent theme selection
Restore the full Settings theme picker while preserving Shadcn Mono selections across reloads.

- Merge the Settings selector into the current-theme row with shared dropdown controls.
- Restore Shadcn Mono across theme metadata, token styles, and dashboard/desktop bootstrap validation.
- Scope Mono light swatches correctly and cover restored options and persistence with tests.

Files changed:
 .changeset/FN-8146-theme-dropdown-current-row.md   |  7 +++
 docs/dashboard-guide.md                            |  5 ++-
 packages/core/src/types/execution-and-ui.ts        |  1 +
 .../dashboard/app/components/ThemeDropdown.css     | 52 ++++++++++++++++++++++
 .../dashboard/app/components/ThemeDropdown.tsx     | 42 ++++++++++++++---
 .../dashboard/app/components/ThemeSelector.css     | 42 +----------------
 .../dashboard/app/components/ThemeSelector.tsx     | 45 +++++--------------
 .../components/__tests__/ThemeDropdown.test.tsx    | 46 ++++++++++++++++---
 .../components/__tests__/ThemeSelector.test.tsx    | 29 ++++++++----
 .../__tests__/CommandCenterControls.test.tsx       | 15 ++++++-
 packages/dashboard/app/components/themeOptions.ts  |  2 +
 .../dashboard/app/hooks/__tests__/useTheme.test.ts |  8 ++--
 packages/dashboard/app/hooks/useTheme.ts           |  8 ++--
 packages/dashboard/app/index.html                  |  5 +--
 packages/dashboard/app/public/theme-data.css       | 16 ++++++-
 packages/desktop/src/renderer/index.html           |  1 +
 16 files changed, 213 insertions(+), 111 deletions(-)

Fusion-Task-Id: FN-8146

Fusion-Task-Lineage: 32ec7f21-ee6b-4459-8e7b-f5a5435c4994

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-17 00:00:46 -07:00

109 lines
4.0 KiB
TypeScript

import "./ThemeSelector.css";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import type { ThemeMode, ColorTheme } from "@fusion/core";
import { THEME_MODES } from "./themeOptions";
import { ThemeDropdown } from "./ThemeDropdown";
interface ThemeSelectorProps {
themeMode: ThemeMode;
colorTheme: ColorTheme;
dashboardFontScalePct?: number;
shadcnCustomColors?: Record<string, string>;
resolvedThemeMode?: "dark" | "light";
onThemeModeChange: (mode: ThemeMode) => void;
onColorThemeChange: (theme: ColorTheme) => void;
onDashboardFontScaleChange?: (scalePct: number) => void;
onShadcnCustomColorsChange?: (colors: Record<string, string>) => void;
}
const FONT_SCALE_OPTIONS = [
{ value: 90, label: "Small" },
{ value: 100, label: "Default" },
{ value: 110, label: "Large" },
{ value: 120, label: "Largest" },
] as const;
/**
* ThemeSelector component for choosing light/dark/system mode and color theme
*/
export function ThemeSelector({
themeMode,
colorTheme,
dashboardFontScalePct = 100,
shadcnCustomColors = {},
resolvedThemeMode = themeMode === "light" ? "light" : "dark",
onThemeModeChange,
onColorThemeChange,
onDashboardFontScaleChange = () => {},
onShadcnCustomColorsChange = () => {},
}: ThemeSelectorProps) {
const { t } = useTranslation("app");
const handleReset = useCallback(() => {
onThemeModeChange("system");
/*
FNXC:DashboardTheming 2026-07-03-00:00:
Reset to defaults must match fresh-install behavior: System mode follows the OS preference while Shadcn Ember stays the default color theme, without migrating explicit Ocean or legacy theme choices.
*/
onColorThemeChange("shadcn-ember");
onDashboardFontScaleChange(100);
onShadcnCustomColorsChange({});
}, [onThemeModeChange, onColorThemeChange, onDashboardFontScaleChange, onShadcnCustomColorsChange]);
return (
<div className="theme-selector">
{/* Theme Mode Toggle */}
<div className="theme-mode-toggle" role="radiogroup" aria-label={t("theme.modeLabel", "Theme mode")}>
{THEME_MODES.map(({ value, label, icon: Icon }) => (
<button
key={value}
className={`theme-mode-btn${themeMode === value ? " active" : ""}`}
onClick={() => onThemeModeChange(value)}
aria-pressed={themeMode === value}
aria-label={t(`theme.${value}Mode`, `${label} mode`)}
title={t(`theme.${value}Mode`, `${label} mode`)}
>
<Icon size={16} />
<span>{t(`theme.${value}`, label)}</span>
</button>
))}
</div>
{/* FNXC:Theme 2026-07-16-14:30: FN-8146 makes the current-theme row the sole Settings color-theme trigger, replacing the separate static preview and standalone dropdown without adding a second mode control. */}
<ThemeDropdown
triggerVariant="current-row"
colorTheme={colorTheme}
themeMode={themeMode}
onColorThemeChange={onColorThemeChange}
shadcnCustomColors={shadcnCustomColors}
resolvedThemeMode={resolvedThemeMode}
onShadcnCustomColorsChange={onShadcnCustomColorsChange}
/>
<div className="theme-section-title">{t("theme.fontSize", "Font Size")}</div>
<div className="theme-font-size-toggle" role="radiogroup" aria-label={t("theme.fontSizeLabel", "Dashboard font size")}>
{FONT_SCALE_OPTIONS.map(({ value, label }) => (
<button
key={value}
className={`theme-font-size-btn${dashboardFontScalePct === value ? " active" : ""}`}
onClick={() => onDashboardFontScaleChange(value)}
aria-pressed={dashboardFontScalePct === value}
>
<span>{t(`theme.fontSize.${label}`, label)}</span>
</button>
))}
</div>
{/* Reset Button */}
<button
className="theme-reset-btn"
onClick={handleReset}
aria-label={t("theme.resetLabel", "Reset to default theme")}
>
<span>{t("theme.resetButton", "Reset to defaults")}</span>
</button>
</div>
);
}