FN-7299: make Shadcn Ember the default dashboard theme
Set Shadcn Ember as the default dashboard theme while keeping Shadcn Gray available as an alternate. - Add Shadcn Ember theme metadata, CSS variables, and boot-time defaults for dashboard and desktop shells. - Update persisted settings types, schema defaults, and theme selection UI to prefer Ember. - Cover Ember defaults and theme-option ordering with core, hook, and dashboard component tests. - Add a published package changeset for the operator-facing default theme change. Files changed: .changeset/fn-7299-shadcn-ember-default.md | 7 ++ .../core/src/__tests__/global-settings.test.ts | 23 +++- packages/core/src/__tests__/store-settings.test.ts | 10 +- packages/core/src/settings-schema.ts | 6 +- packages/core/src/types.ts | 4 +- .../app/__tests__/shadcn-ember-theme.test.ts | 107 ++++++++++++++++ .../app/__tests__/shadcn-gray-theme.test.ts | 2 +- .../dashboard/app/components/ThemeSelector.css | 15 +++ .../dashboard/app/components/ThemeSelector.tsx | 6 +- .../components/__tests__/ThemeDropdown.test.tsx | 20 ++- .../components/__tests__/ThemeSelector.test.tsx | 24 +++- packages/dashboard/app/components/themeOptions.ts | 7 +- .../dashboard/app/hooks/__tests__/useTheme.test.ts | 18 ++- packages/dashboard/app/hooks/useTheme.ts | 8 +- packages/dashboard/app/index.html | 10 +- packages/dashboard/app/public/theme-data.css | 137 +++++++++++++++++++++ packages/desktop/src/renderer/index.html | 42 ++++++- 17 files changed, 409 insertions(+), 37 deletions(-) Fusion-Task-Id: FN-7299 Fusion-Task-Lineage: 7e175752-b8fc-4ba9-8534-cb485f6eb18a Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7299-shadcn-ember-default.md
Normal file
7
.changeset/fn-7299-shadcn-ember-default.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Make Shadcn Ember the default dashboard theme.
|
||||
category: feature
|
||||
dev: Adds the shadcn-ember color theme and updates default theme fallbacks.
|
||||
@@ -65,7 +65,7 @@ describe("GlobalSettingsStore", () => {
|
||||
const raw = await readFile(join(dir, "settings.json"), "utf-8");
|
||||
const parsed = JSON.parse(raw);
|
||||
expect(parsed.themeMode).toBe("dark");
|
||||
expect(parsed.colorTheme).toBe("ocean");
|
||||
expect(parsed.colorTheme).toBe("shadcn-ember");
|
||||
expect(parsed.ntfyEnabled).toBe(false);
|
||||
});
|
||||
|
||||
@@ -173,6 +173,23 @@ describe("GlobalSettingsStore", () => {
|
||||
expect(settings.defaultProvider).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves explicit legacy color theme selections", async () => {
|
||||
await mkdir(dir, { recursive: true });
|
||||
await writeFile(
|
||||
join(dir, "settings.json"),
|
||||
JSON.stringify({ colorTheme: "default" }),
|
||||
);
|
||||
|
||||
await expect(new GlobalSettingsStore(dir).getSettings()).resolves.toMatchObject({ colorTheme: "default" });
|
||||
|
||||
await writeFile(
|
||||
join(dir, "settings.json"),
|
||||
JSON.stringify({ colorTheme: "ocean" }),
|
||||
);
|
||||
|
||||
await expect(new GlobalSettingsStore(dir).getSettings()).resolves.toMatchObject({ colorTheme: "ocean" });
|
||||
});
|
||||
|
||||
it("returns defaults on invalid JSON", async () => {
|
||||
await mkdir(dir, { recursive: true });
|
||||
await writeFile(join(dir, "settings.json"), "not-json{{{");
|
||||
@@ -197,7 +214,7 @@ describe("GlobalSettingsStore", () => {
|
||||
const updated = await store.updateSettings({ themeMode: "system" });
|
||||
|
||||
expect(updated.themeMode).toBe("system");
|
||||
expect(updated.colorTheme).toBe("ocean"); // unchanged default
|
||||
expect(updated.colorTheme).toBe("shadcn-ember"); // unchanged default
|
||||
|
||||
// Verify persistence
|
||||
const raw = await readFile(join(dir, "settings.json"), "utf-8");
|
||||
@@ -849,7 +866,7 @@ describe("GlobalSettingsStore", () => {
|
||||
const raw = JSON.parse(await readFile(join(dir, "settings.json"), "utf-8"));
|
||||
// Only default theme fields should be present
|
||||
expect(raw.themeMode).toBe("dark");
|
||||
expect(raw.colorTheme).toBe("ocean");
|
||||
expect(raw.colorTheme).toBe("shadcn-ember");
|
||||
// Model fields should not be persisted
|
||||
expect(raw.defaultProvider).toBeUndefined();
|
||||
expect(raw.defaultModelId).toBeUndefined();
|
||||
|
||||
@@ -1422,7 +1422,7 @@ describe("TaskStore", () => {
|
||||
it("getSettings returns global defaults when no overrides exist", async () => {
|
||||
const settings = await harness.store().getSettings();
|
||||
expect(settings.themeMode).toBe("dark");
|
||||
expect(settings.colorTheme).toBe("ocean");
|
||||
expect(settings.colorTheme).toBe("shadcn-ember");
|
||||
expect(settings.maxConcurrent).toBe(2);
|
||||
});
|
||||
|
||||
@@ -1433,6 +1433,14 @@ describe("TaskStore", () => {
|
||||
expect(settings.colorTheme).toBe("ocean");
|
||||
});
|
||||
|
||||
it("preserves explicit legacy color theme selections", async () => {
|
||||
await harness.store().updateGlobalSettings({ colorTheme: "default" });
|
||||
await expect(harness.store().getSettings()).resolves.toMatchObject({ colorTheme: "default" });
|
||||
|
||||
await harness.store().updateGlobalSettings({ colorTheme: "ocean" });
|
||||
await expect(harness.store().getSettings()).resolves.toMatchObject({ colorTheme: "ocean" });
|
||||
});
|
||||
|
||||
it("project settings override global defaults", async () => {
|
||||
await harness.store().updateSettings({ maxConcurrent: 8 });
|
||||
const settings = await harness.store().getSettings();
|
||||
|
||||
@@ -65,10 +65,10 @@ type ProjectSettingsSchema = Omit<ProjectSettings, MovedProjectSettingsKey>;
|
||||
export const DEFAULT_GLOBAL_SETTINGS = {
|
||||
themeMode: "dark",
|
||||
/*
|
||||
FNXC:DashboardTheming 2026-06-22-18:36:
|
||||
New users and unset installs should start on Ocean. Existing users who explicitly stored colorTheme "default" must remain on that legacy theme, so the id stays valid and only the absence/default seed changes to "ocean".
|
||||
FNXC:DashboardTheming 2026-06-30-00:00:
|
||||
New users and unset installs should start on Shadcn Ember. Existing users who explicitly stored colorTheme "default", "ocean", or another valid theme must remain on that selection, so the ids stay valid and only the absence/default seed changes to "shadcn-ember".
|
||||
*/
|
||||
colorTheme: "ocean",
|
||||
colorTheme: "shadcn-ember",
|
||||
shadcnCustomColors: undefined,
|
||||
dashboardFontScalePct: 100,
|
||||
/*
|
||||
|
||||
@@ -333,6 +333,8 @@ export const COLOR_THEMES = [
|
||||
"neon-bloom",
|
||||
"sepia",
|
||||
"shadcn",
|
||||
// FNXC:DashboardTheming 2026-06-30-00:00: Shadcn Ember is the default for unset installs; keep it adjacent to the shadcn base so dashboard options and bootstrap validators preserve published theme order while explicit legacy ids remain valid.
|
||||
"shadcn-ember",
|
||||
// FNXC:DashboardTheming 2026-06-20-18:20: FN-6816 adds the user-customizable shadcn variant; keep this union in lockstep with dashboard theme options, swatches, theme-data base blocks, and the shadcn custom color token list.
|
||||
"shadcn-custom",
|
||||
// FNXC:DashboardTheming 2026-06-19-16:07: FN-6756 extends the published color-theme union with shadcn-family accent variants; keep dashboard theme options, bootstrap validation, swatches, and theme-data token blocks in lockstep with this ordered list.
|
||||
@@ -2948,7 +2950,7 @@ export interface McpServersSettings {
|
||||
export interface GlobalSettings {
|
||||
/** Theme mode preference: dark, light, or system (follows OS). Default: "dark". */
|
||||
themeMode?: ThemeMode;
|
||||
/** Color theme preference for accent colors and styling. Default: "ocean"; "default" is the legacy Fusion theme id. */
|
||||
/** Color theme preference for accent colors and styling. Default: "shadcn-ember"; "default" and "ocean" remain valid explicit legacy selections. */
|
||||
colorTheme?: ColorTheme;
|
||||
/** Token→hex override map for the customizable shadcn theme. Applied only when `colorTheme === "shadcn-custom"`; dashboard sanitizes keys and values before writing CSS custom properties. */
|
||||
shadcnCustomColors?: Record<string, string>;
|
||||
|
||||
107
packages/dashboard/app/__tests__/shadcn-ember-theme.test.ts
Normal file
107
packages/dashboard/app/__tests__/shadcn-ember-theme.test.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
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");
|
||||
|
||||
/*
|
||||
FNXC:DashboardTheming 2026-06-30-00:00:
|
||||
Shadcn Ember is the default color-theme contract. Tests assert source-of-truth CSS and every bootstrap validator so unset/invalid installs converge on Shadcn Ember while explicit legacy ids remain valid.
|
||||
*/
|
||||
describe("Shadcn Ember color theme", () => {
|
||||
const themeData = fs.readFileSync(themeDataPath, "utf-8");
|
||||
const dashboardIndex = fs.readFileSync(dashboardIndexPath, "utf-8");
|
||||
const desktopIndex = fs.readFileSync(desktopIndexPath, "utf-8");
|
||||
|
||||
it("defines dark and light shadcn structure with Ember color tokens", () => {
|
||||
const darkBlock = extractSelectorBlock(themeData, '[data-color-theme="shadcn-ember"]');
|
||||
const lightBlock = extractSelectorBlock(themeData, '[data-color-theme="shadcn-ember"][data-theme="light"]');
|
||||
|
||||
expect(darkBlock).toContain("--surface-hover:");
|
||||
expect(lightBlock).toContain("--surface-hover:");
|
||||
expect(darkBlock).toContain("--bg: #09090b;");
|
||||
expect(darkBlock).toContain("--card: #18181b;");
|
||||
expect(lightBlock).toContain("--bg: #ffffff;");
|
||||
expect(lightBlock).toContain("--card-hover: #f4f4f5;");
|
||||
expect(darkBlock).toContain("--btn-border-width: 1px;");
|
||||
expect(darkBlock).toContain("--font-primary: \"Geist\",");
|
||||
|
||||
expect(darkBlock).toContain("--todo: #a0a0a0;");
|
||||
expect(darkBlock).toContain("--in-progress: #b8b8b8;");
|
||||
expect(darkBlock).toContain("--color-error: #ff6b6b;");
|
||||
expect(darkBlock).toContain("--cta-bg: #d4622a;");
|
||||
expect(darkBlock).toContain("--cta-border: #e8773a;");
|
||||
expect(darkBlock).toContain("--color-info: #e8773a;");
|
||||
expect(darkBlock).toContain("--accent: #e8773a;");
|
||||
|
||||
expect(lightBlock).toContain("--todo: #404040;");
|
||||
expect(lightBlock).toContain("--in-progress: #606060;");
|
||||
expect(lightBlock).toContain("--color-error: #dc2626;");
|
||||
expect(lightBlock).toContain("--cta-bg: #c05820;");
|
||||
expect(lightBlock).toContain("--cta-border: #d4622a;");
|
||||
expect(lightBlock).toContain("--color-info: #c05820;");
|
||||
expect(lightBlock).toContain("--accent: #d4622a;");
|
||||
|
||||
expect(darkBlock).toContain("--color-warning: #f59e0b;");
|
||||
expect(lightBlock).toContain("--color-muted: #71717a;");
|
||||
});
|
||||
|
||||
it("registers the default theme in core, dashboard options, and bootstrap validators", () => {
|
||||
expect(CORE_COLOR_THEMES).toContain("shadcn-ember");
|
||||
expect(DASHBOARD_COLOR_THEMES).toContainEqual({
|
||||
value: "shadcn-ember",
|
||||
label: "Shadcn Ember (Default)",
|
||||
className: "theme-swatch-shadcn-ember",
|
||||
});
|
||||
expect(DASHBOARD_COLOR_THEMES.filter((theme) => theme.label.includes("(Default)")).map((theme) => theme.value)).toEqual([
|
||||
"shadcn-ember",
|
||||
]);
|
||||
|
||||
expect(dashboardIndex).toContain("'shadcn-ember'");
|
||||
expect(dashboardIndex).toContain("|| 'shadcn-ember'");
|
||||
expect(dashboardIndex).toContain("colorTheme = 'shadcn-ember'");
|
||||
expect(desktopIndex).toContain('"shadcn-ember"');
|
||||
expect(desktopIndex).toContain('|| "shadcn-ember"');
|
||||
expect(desktopIndex).toContain('colorTheme = "shadcn-ember"');
|
||||
});
|
||||
|
||||
it("keeps dashboard and desktop bootstrap validators identical to the core color theme union", () => {
|
||||
expect(DASHBOARD_COLOR_THEMES.map((theme) => theme.value)).toEqual([...CORE_COLOR_THEMES]);
|
||||
expect(extractValidThemes(dashboardIndex)).toEqual([...CORE_COLOR_THEMES]);
|
||||
expect(extractValidThemes(desktopIndex)).toEqual([...CORE_COLOR_THEMES]);
|
||||
});
|
||||
});
|
||||
|
||||
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 Array.from(match[1].matchAll(/["']([^"']+)["']/g), ([, theme]) => theme);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ const dashboardIndexPath = path.resolve(__dirname, "../index.html");
|
||||
|
||||
/*
|
||||
FNXC:DashboardTheming 2026-06-20-00:00:
|
||||
Shadcn Gray is a zinc-only accent variant. This test locks the dashboard wiring and neutral token intent without asserting the desktop bootstrap validator, which intentionally excludes all shadcn variants.
|
||||
Shadcn Gray is a zinc-only accent variant. This test locks the dashboard wiring and neutral token intent; default-theme bootstrap coverage for the shadcn family lives in the Shadcn Ember contract test.
|
||||
*/
|
||||
describe("Shadcn Gray color theme", () => {
|
||||
const themeData = fs.readFileSync(themeDataPath, "utf-8");
|
||||
|
||||
@@ -388,6 +388,14 @@
|
||||
--swatch-sample-4: #27272a;
|
||||
}
|
||||
|
||||
/* FNXC:DashboardTheming 2026-06-30-00:00: Shadcn Ember is the default selector chip; it previews shadcn zinc surfaces with Ember's warm orange accent so users can distinguish the default from base Shadcn. */
|
||||
.theme-swatch-shadcn-ember {
|
||||
--swatch-sample-1: #09090b;
|
||||
--swatch-sample-2: #18181b;
|
||||
--swatch-sample-3: #e8773a;
|
||||
--swatch-sample-4: #27272a;
|
||||
}
|
||||
|
||||
.theme-swatch-shadcn-custom {
|
||||
--swatch-sample-1: #09090b;
|
||||
--swatch-sample-2: #18181b;
|
||||
@@ -735,6 +743,13 @@
|
||||
--swatch-sample-4: #e4e4e7;
|
||||
}
|
||||
|
||||
[data-theme="light"] .theme-swatch-shadcn-ember {
|
||||
--swatch-sample-1: #ffffff;
|
||||
--swatch-sample-2: #f4f4f5;
|
||||
--swatch-sample-3: #d4622a;
|
||||
--swatch-sample-4: #e4e4e7;
|
||||
}
|
||||
|
||||
[data-theme="light"] .theme-swatch-shadcn-custom {
|
||||
--swatch-sample-1: #ffffff;
|
||||
--swatch-sample-2: #f4f4f5;
|
||||
|
||||
@@ -42,7 +42,11 @@ export function ThemeSelector({
|
||||
const { t } = useTranslation("app");
|
||||
const handleReset = useCallback(() => {
|
||||
onThemeModeChange("dark");
|
||||
onColorThemeChange("ocean");
|
||||
/*
|
||||
FNXC:DashboardTheming 2026-06-30-00:00:
|
||||
Reset to defaults must match the new unset-install fallback so Settings restores Shadcn Ember without migrating explicit Ocean or legacy theme choices.
|
||||
*/
|
||||
onColorThemeChange("shadcn-ember");
|
||||
onDashboardFontScaleChange(100);
|
||||
onShadcnCustomColorsChange({});
|
||||
}, [onThemeModeChange, onColorThemeChange, onDashboardFontScaleChange, onShadcnCustomColorsChange]);
|
||||
|
||||
@@ -10,12 +10,12 @@ describe("ThemeDropdown", () => {
|
||||
});
|
||||
|
||||
it("renders the current theme chip and opens all swatched theme options", () => {
|
||||
render(<ThemeDropdown colorTheme="ocean" onColorThemeChange={vi.fn()} />);
|
||||
render(<ThemeDropdown colorTheme="shadcn-ember" onColorThemeChange={vi.fn()} />);
|
||||
|
||||
const trigger = screen.getByRole("button", { name: /ocean/i });
|
||||
const trigger = screen.getByRole("button", { name: /shadcn ember/i });
|
||||
expect(trigger.getAttribute("aria-expanded")).toBe("false");
|
||||
expect(within(trigger).getByText("Ocean (Default)")).toBeDefined();
|
||||
expect(trigger.querySelector(".theme-swatch-ocean")).toBeTruthy();
|
||||
expect(within(trigger).getByText("Shadcn Ember (Default)")).toBeDefined();
|
||||
expect(trigger.querySelector(".theme-swatch-shadcn-ember")).toBeTruthy();
|
||||
|
||||
fireEvent.click(trigger);
|
||||
|
||||
@@ -29,6 +29,18 @@ describe("ThemeDropdown", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("labels only Shadcn Ember as the default option", () => {
|
||||
render(<ThemeDropdown colorTheme="ocean" onColorThemeChange={vi.fn()} />);
|
||||
|
||||
expect(screen.getByRole("button", { name: /ocean/i }).textContent).toContain("Ocean");
|
||||
expect(screen.getByRole("button", { name: /ocean/i }).textContent).not.toContain("Default");
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /ocean/i }));
|
||||
const defaultOptions = screen.getAllByRole("option").filter((option) => option.textContent?.includes("(Default)"));
|
||||
expect(defaultOptions).toHaveLength(1);
|
||||
expect(defaultOptions[0]).toHaveTextContent("Shadcn Ember (Default)");
|
||||
});
|
||||
|
||||
it("selects themes and closes from click, escape, and outside click", () => {
|
||||
const onColorThemeChange = vi.fn();
|
||||
render(<ThemeDropdown colorTheme="default" onColorThemeChange={onColorThemeChange} />);
|
||||
|
||||
@@ -97,6 +97,24 @@ describe("ThemeSelector", () => {
|
||||
expect(screen.getByLabelText(`${defaultTheme.label} theme`).getAttribute("aria-pressed")).toBe("true");
|
||||
});
|
||||
|
||||
it("marks only Shadcn Ember as the default color theme label", () => {
|
||||
render(
|
||||
<ThemeSelector
|
||||
themeMode="dark"
|
||||
colorTheme="shadcn-ember"
|
||||
onThemeModeChange={vi.fn()}
|
||||
onColorThemeChange={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText("Shadcn Ember (Default) theme")).toBeDefined();
|
||||
expect(screen.getByLabelText("Ocean theme")).toBeDefined();
|
||||
expect(screen.queryByLabelText("Ocean (Default) theme")).toBeNull();
|
||||
expect(THEME_OPTIONS.filter((theme) => theme.label.includes("(Default)")).map((theme) => theme.value)).toEqual([
|
||||
"shadcn-ember",
|
||||
]);
|
||||
});
|
||||
|
||||
it("renders every shared swatch class from themeOptions", () => {
|
||||
render(
|
||||
<ThemeSelector
|
||||
@@ -124,7 +142,7 @@ describe("ThemeSelector", () => {
|
||||
/>
|
||||
);
|
||||
|
||||
const oceanBtn = screen.getByLabelText("Ocean (Default) theme");
|
||||
const oceanBtn = screen.getByLabelText("Ocean theme");
|
||||
expect(oceanBtn.className).toContain("active");
|
||||
expect(oceanBtn.getAttribute("aria-pressed")).toBe("true");
|
||||
});
|
||||
@@ -497,7 +515,7 @@ describe("ThemeSelector", () => {
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Current theme/)).toBeDefined();
|
||||
expect(screen.getByText(/Dark \/ Ocean \(Default\)/)).toBeDefined();
|
||||
expect(screen.getByText(/Dark \/ Ocean/)).toBeDefined();
|
||||
});
|
||||
|
||||
it("displays system theme in preview when system mode", () => {
|
||||
@@ -608,7 +626,7 @@ describe("ThemeSelector", () => {
|
||||
|
||||
fireEvent.click(screen.getByLabelText("Reset to default theme"));
|
||||
expect(onThemeModeChange).toHaveBeenCalledWith("dark");
|
||||
expect(onColorThemeChange).toHaveBeenCalledWith("ocean");
|
||||
expect(onColorThemeChange).toHaveBeenCalledWith("shadcn-ember");
|
||||
});
|
||||
|
||||
it("shows the shadcn custom picker only for shadcn-custom", () => {
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { ColorTheme, ThemeMode } from "@fusion/core";
|
||||
FNXC:Theme 2026-06-19-12:00:
|
||||
The Settings theme grid and Command Center theme dropdown must share one source of truth for theme labels and swatch classes so color-chip affordances stay synchronized across both theme selectors.
|
||||
|
||||
FNXC:DashboardTheming 2026-06-22-18:36:
|
||||
Ocean is the default theme label for new/unset users. The historical "default" id remains selectable as Fusion Legacy so users who already chose default are not silently moved to Ocean.
|
||||
FNXC:DashboardTheming 2026-06-30-00:00:
|
||||
Shadcn Ember is the default theme label for new/unset users. The historical "default" and "ocean" ids remain selectable so users who already chose them are not silently moved.
|
||||
*/
|
||||
export const THEME_MODES: { value: ThemeMode; label: string; icon: LucideIcon }[] = [
|
||||
{ value: "light", label: "Light", icon: Sun },
|
||||
@@ -16,7 +16,7 @@ export const THEME_MODES: { value: ThemeMode; label: string; icon: LucideIcon }[
|
||||
|
||||
export const COLOR_THEMES: { value: ColorTheme; label: string; className: string }[] = [
|
||||
{ value: "default", label: "Fusion Legacy", className: "theme-swatch-default" },
|
||||
{ value: "ocean", label: "Ocean (Default)", className: "theme-swatch-ocean" },
|
||||
{ value: "ocean", label: "Ocean", className: "theme-swatch-ocean" },
|
||||
{ value: "forest", label: "Forest", className: "theme-swatch-forest" },
|
||||
{ value: "sunset", label: "Sunset", className: "theme-swatch-sunset" },
|
||||
{ value: "zen", label: "Zen", className: "theme-swatch-zen" },
|
||||
@@ -72,6 +72,7 @@ export const COLOR_THEMES: { value: ColorTheme; label: string; className: string
|
||||
{ value: "neon-bloom", label: "Neon Bloom", className: "theme-swatch-neon-bloom" },
|
||||
{ value: "sepia", label: "Sepia", className: "theme-swatch-sepia" },
|
||||
{ value: "shadcn", label: "Shadcn", className: "theme-swatch-shadcn" },
|
||||
{ value: "shadcn-ember", label: "Shadcn Ember (Default)", className: "theme-swatch-shadcn-ember" },
|
||||
{ value: "shadcn-custom", label: "Shadcn Custom", className: "theme-swatch-shadcn-custom" },
|
||||
{ value: "shadcn-blue", label: "Shadcn Blue", className: "theme-swatch-shadcn-blue" },
|
||||
{ value: "shadcn-green", label: "Shadcn Green", className: "theme-swatch-shadcn-green" },
|
||||
|
||||
@@ -104,7 +104,7 @@ describe("useTheme", () => {
|
||||
const { result } = renderHook(() => useTheme());
|
||||
|
||||
expect(result.current.themeMode).toBe("dark");
|
||||
expect(result.current.colorTheme).toBe("ocean");
|
||||
expect(result.current.colorTheme).toBe("shadcn-ember");
|
||||
});
|
||||
|
||||
it("initializes from localStorage", () => {
|
||||
@@ -125,6 +125,14 @@ describe("useTheme", () => {
|
||||
expect(result.current.colorTheme).toBe("default");
|
||||
});
|
||||
|
||||
it("preserves explicit ocean color theme from localStorage", () => {
|
||||
localStorageMock[COLOR_THEME_STORAGE_KEY] = "ocean";
|
||||
|
||||
const { result } = renderHook(() => useTheme());
|
||||
|
||||
expect(result.current.colorTheme).toBe("ocean");
|
||||
});
|
||||
|
||||
it("hydrates themeMode from backend on mount", async () => {
|
||||
mockFetchGlobalSettings.mockResolvedValue({ themeMode: "light" });
|
||||
|
||||
@@ -143,7 +151,7 @@ describe("useTheme", () => {
|
||||
|
||||
const { result } = renderHook(() => useTheme());
|
||||
|
||||
expect(result.current.colorTheme).toBe("ocean");
|
||||
expect(result.current.colorTheme).toBe("shadcn-ember");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.colorTheme).toBe("forest");
|
||||
@@ -769,7 +777,7 @@ describe("useTheme", () => {
|
||||
|
||||
const { result } = renderHook(() => useTheme());
|
||||
|
||||
expect(result.current.colorTheme).toBe("ocean");
|
||||
expect(result.current.colorTheme).toBe("shadcn-ember");
|
||||
});
|
||||
|
||||
it("clamps invalid dashboard font scale values from localStorage", () => {
|
||||
@@ -797,7 +805,7 @@ describe("useTheme", () => {
|
||||
const { result } = renderHook(() => useTheme());
|
||||
|
||||
expect(result.current.themeMode).toBe("dark");
|
||||
expect(result.current.colorTheme).toBe("ocean");
|
||||
expect(result.current.colorTheme).toBe("shadcn-ember");
|
||||
});
|
||||
|
||||
describe("dynamic theme-data.css loading", () => {
|
||||
@@ -881,7 +889,7 @@ describe("getThemeInitScript", () => {
|
||||
});
|
||||
expect(script).toContain("validThemes");
|
||||
expect(script).toContain("if (colorTheme === 'shadcn-mono') colorTheme = 'shadcn-mono-red';");
|
||||
expect(script).toContain("colorTheme = 'ocean'");
|
||||
expect(script).toContain("colorTheme = 'shadcn-ember'");
|
||||
});
|
||||
|
||||
it("keeps index.html inline theme validation in sync with supported themes", () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ const DEFAULT_FONT_SCALE_PCT = 100;
|
||||
const MIN_FONT_SCALE_PCT = 85;
|
||||
const MAX_FONT_SCALE_PCT = 125;
|
||||
const VALID_COLOR_THEMES = [...COLOR_THEMES] satisfies ColorTheme[];
|
||||
const DEFAULT_COLOR_THEME: ColorTheme = "ocean";
|
||||
const DEFAULT_COLOR_THEME: ColorTheme = "shadcn-ember";
|
||||
const THEME_DATA_ID = "theme-data";
|
||||
const THEME_DATA_FILENAME = "theme-data.css";
|
||||
|
||||
@@ -96,8 +96,8 @@ function readCachedColorTheme(): ColorTheme {
|
||||
// localStorage not available, use default
|
||||
}
|
||||
/*
|
||||
FNXC:DashboardTheming 2026-06-22-18:36:
|
||||
Missing/invalid cached theme resolves to Ocean for new installs, but an explicit cached "default" remains valid above and stays on Fusion Legacy.
|
||||
FNXC:DashboardTheming 2026-06-30-00:00:
|
||||
Missing/invalid cached theme resolves to Shadcn Ember for new installs, but explicit cached legacy ids such as "default" and "ocean" remain valid above and must not be migrated.
|
||||
*/
|
||||
return DEFAULT_COLOR_THEME;
|
||||
}
|
||||
@@ -467,7 +467,7 @@ export function getThemeInitScript(): string {
|
||||
var mode = localStorage.getItem('${THEME_MODE_STORAGE_KEY}') || 'dark';
|
||||
var colorTheme = localStorage.getItem('${COLOR_THEME_STORAGE_KEY}') || '${DEFAULT_COLOR_THEME}';
|
||||
var validThemes = ${JSON.stringify(VALID_COLOR_THEMES)};
|
||||
// FNXC:DashboardTheming 2026-06-22-18:36: Unset startup theme is Ocean; an explicit stored "default" remains the Fusion Legacy theme and must not be migrated.
|
||||
// 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-06-20-00:00: FN-6813 remaps the legacy mono id before bootstrap validation so persisted users keep the red mono accent.
|
||||
if (colorTheme === 'shadcn-mono') colorTheme = 'shadcn-mono-red';
|
||||
if (!validThemes.includes(colorTheme)) {
|
||||
|
||||
@@ -159,13 +159,13 @@
|
||||
(function() {
|
||||
try {
|
||||
var mode = localStorage.getItem('kb-dashboard-theme-mode') || 'dark';
|
||||
var colorTheme = localStorage.getItem('kb-dashboard-color-theme') || 'ocean';
|
||||
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', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', '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-22-18:36: Unset startup theme is Ocean; an explicit stored "default" remains Fusion Legacy and must not be migrated.
|
||||
var colorTheme = localStorage.getItem('kb-dashboard-color-theme') || 'shadcn-ember';
|
||||
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', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', '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-06-20-00:00: FN-6813 remaps the legacy mono id before pre-hydration validation so persisted users keep the red mono accent.
|
||||
if (colorTheme === 'shadcn-mono') colorTheme = 'shadcn-mono-red';
|
||||
if (!validThemes.includes(colorTheme)) {
|
||||
colorTheme = 'ocean';
|
||||
colorTheme = 'shadcn-ember';
|
||||
}
|
||||
var fontScale = Number(localStorage.getItem('kb-dashboard-font-scale-pct') || '100');
|
||||
if (!Number.isFinite(fontScale)) {
|
||||
@@ -220,7 +220,7 @@
|
||||
}
|
||||
} catch (e) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
document.documentElement.setAttribute('data-color-theme', 'ocean');
|
||||
document.documentElement.setAttribute('data-color-theme', 'shadcn-ember');
|
||||
document.documentElement.style.fontSize = '100%';
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -1428,6 +1428,134 @@ FN-6758 makes the default shadcn highlight/accent orange for focus rings, active
|
||||
--accent-text: #ffffff;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:DashboardTheming 2026-06-30-00:00:
|
||||
Shadcn Ember combines shadcn/ui zinc structure with Ember's warm orange/graphite CTA, status, accent, and info colors. It is the default for unset installs while explicit legacy theme ids stay valid.
|
||||
*/
|
||||
[data-color-theme="shadcn-ember"] {
|
||||
--bg: #09090b;
|
||||
--surface: #0c0c0e;
|
||||
--card: #18181b;
|
||||
--card-hover: #1f1f23;
|
||||
--surface-hover: color-mix(in srgb, var(--surface) 90%, var(--text) 10%);
|
||||
--border: #27272a;
|
||||
|
||||
--text: #fafafa;
|
||||
--text-muted: #a1a1aa;
|
||||
--text-dim: #52525b;
|
||||
|
||||
--todo: #a0a0a0;
|
||||
--in-progress: #b8b8b8;
|
||||
--in-progress-rgb: 184, 184, 184;
|
||||
--in-review: #d0d0d0;
|
||||
--triage: #808080;
|
||||
--done: #686868;
|
||||
|
||||
--color-success: #d0d0d0;
|
||||
--color-warning: #f59e0b;
|
||||
--color-error: #ff6b6b;
|
||||
--color-muted: #71717a;
|
||||
|
||||
--font-primary: "Geist", "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
--font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
|
||||
|
||||
--space-xs: 4px;
|
||||
--space-sm: 6px;
|
||||
--space-md: 10px;
|
||||
--space-lg: 16px;
|
||||
--space-xl: 20px;
|
||||
--space-2xl: 28px;
|
||||
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 8px;
|
||||
--radius-xl: 12px;
|
||||
--radius: var(--radius-md);
|
||||
|
||||
--btn-padding: 6px 12px;
|
||||
--btn-border-width: 1px;
|
||||
--card-padding: 10px 12px;
|
||||
--modal-padding: var(--space-md) var(--space-lg);
|
||||
--header-padding: var(--space-sm) var(--space-lg);
|
||||
--column-gap: var(--space-md);
|
||||
--board-padding: var(--space-md) var(--space-lg);
|
||||
|
||||
--shadow-sm: 0 1px 2px color-mix(in srgb, #000000 18%, transparent);
|
||||
--shadow-md: 0 1px 3px color-mix(in srgb, #000000 22%, transparent);
|
||||
--shadow-lg: 0 4px 12px color-mix(in srgb, #000000 24%, transparent);
|
||||
--shadow-glow: none;
|
||||
--glow-success: none;
|
||||
--glow-warning: none;
|
||||
--glow-danger: none;
|
||||
--focus-ring: 0 0 0 calc(var(--btn-border-width) * 3) color-mix(in srgb, var(--accent) 35%, transparent);
|
||||
--focus-ring-strong: 0 0 0 calc(var(--btn-border-width) * 3) 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: #d4622a;
|
||||
--cta-border: #e8773a;
|
||||
--cta-text: #fff;
|
||||
--cta-bg-hover: #e8773a;
|
||||
--cta-border-hover: #f09050;
|
||||
--cta-glow: 0 0 8px color-mix(in srgb, var(--cta-border) 30%, transparent);
|
||||
--logo-accent: #e8773a;
|
||||
--color-info: #e8773a;
|
||||
--accent: #e8773a;
|
||||
--accent-text: #ffffff;
|
||||
}
|
||||
|
||||
[data-color-theme="shadcn-ember"][data-theme="light"] {
|
||||
--bg: #ffffff;
|
||||
--surface: #ffffff;
|
||||
--card: #ffffff;
|
||||
--card-hover: #f4f4f5;
|
||||
--surface-hover: color-mix(in srgb, var(--surface) 92%, var(--text) 8%);
|
||||
--border: #e4e4e7;
|
||||
|
||||
--text: #09090b;
|
||||
--text-muted: #71717a;
|
||||
--text-dim: #a1a1aa;
|
||||
|
||||
--todo: #404040;
|
||||
--in-progress: #606060;
|
||||
--in-progress-rgb: 96, 96, 96;
|
||||
--in-review: #606060;
|
||||
--triage: #2a2a2a;
|
||||
--done: #606060;
|
||||
|
||||
--color-success: #404040;
|
||||
--color-warning: #d97706;
|
||||
--color-error: #dc2626;
|
||||
--color-muted: #71717a;
|
||||
|
||||
--shadow-sm: 0 1px 2px color-mix(in srgb, var(--text) 8%, transparent);
|
||||
--shadow-md: 0 1px 3px color-mix(in srgb, var(--text) 10%, transparent);
|
||||
--shadow-lg: 0 4px 12px color-mix(in srgb, var(--text) 12%, transparent);
|
||||
--shadow-glow: none;
|
||||
--glow-success: none;
|
||||
--glow-warning: none;
|
||||
--glow-danger: none;
|
||||
--focus-ring: 0 0 0 calc(var(--btn-border-width) * 3) color-mix(in srgb, var(--accent) 18%, transparent);
|
||||
--focus-ring-strong: 0 0 0 calc(var(--btn-border-width) * 3) color-mix(in srgb, var(--accent) 28%, transparent);
|
||||
--shadow: var(--shadow-lg);
|
||||
|
||||
--cta-bg: #c05820;
|
||||
--cta-border: #d4622a;
|
||||
--cta-text: #fff;
|
||||
--cta-bg-hover: #d4622a;
|
||||
--cta-border-hover: #e07030;
|
||||
--cta-glow: 0 0 8px color-mix(in srgb, var(--cta-bg) 30%, transparent);
|
||||
--logo-accent: #d4622a;
|
||||
--color-info: #c05820;
|
||||
--accent: #d4622a;
|
||||
--accent-text: #ffffff;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
FNXC:Theming 2026-06-20-18:30:
|
||||
FN-6816 keeps shadcn-custom visually identical to the base shadcn theme until sanitized user token overrides are applied inline by the dashboard theme hook.
|
||||
@@ -1556,6 +1684,7 @@ FN-6816 keeps shadcn-custom visually identical to the base shadcn theme until sa
|
||||
}
|
||||
|
||||
[data-color-theme="shadcn"] .card.agent-active,
|
||||
[data-color-theme="shadcn-ember"] .card.agent-active,
|
||||
[data-color-theme="shadcn-custom"] .card.agent-active {
|
||||
border-color: var(--accent);
|
||||
box-shadow: none;
|
||||
@@ -1563,6 +1692,7 @@ FN-6816 keeps shadcn-custom visually identical to the base shadcn theme until sa
|
||||
}
|
||||
|
||||
[data-color-theme="shadcn"][data-theme="light"] .card.agent-active,
|
||||
[data-color-theme="shadcn-ember"][data-theme="light"] .card.agent-active,
|
||||
[data-color-theme="shadcn-custom"][data-theme="light"] .card.agent-active {
|
||||
border-color: var(--accent);
|
||||
box-shadow: none;
|
||||
@@ -1570,6 +1700,7 @@ FN-6816 keeps shadcn-custom visually identical to the base shadcn theme until sa
|
||||
}
|
||||
|
||||
[data-color-theme="shadcn"] .btn,
|
||||
[data-color-theme="shadcn-ember"] .btn,
|
||||
[data-color-theme="shadcn-custom"] .btn {
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
@@ -1578,6 +1709,8 @@ FN-6816 keeps shadcn-custom visually identical to the base shadcn theme until sa
|
||||
|
||||
[data-color-theme="shadcn"] .btn-primary,
|
||||
[data-color-theme="shadcn"] .btn-task-create,
|
||||
[data-color-theme="shadcn-ember"] .btn-primary,
|
||||
[data-color-theme="shadcn-ember"] .btn-task-create,
|
||||
[data-color-theme="shadcn-custom"] .btn-primary,
|
||||
[data-color-theme="shadcn-custom"] .btn-task-create {
|
||||
background: var(--cta-bg);
|
||||
@@ -1588,6 +1721,8 @@ FN-6816 keeps shadcn-custom visually identical to the base shadcn theme until sa
|
||||
|
||||
[data-color-theme="shadcn"] .btn-primary:hover,
|
||||
[data-color-theme="shadcn"] .btn-task-create:hover,
|
||||
[data-color-theme="shadcn-ember"] .btn-primary:hover,
|
||||
[data-color-theme="shadcn-ember"] .btn-task-create:hover,
|
||||
[data-color-theme="shadcn-custom"] .btn-primary:hover,
|
||||
[data-color-theme="shadcn-custom"] .btn-task-create:hover {
|
||||
background: var(--cta-bg-hover);
|
||||
@@ -1598,6 +1733,8 @@ FN-6816 keeps shadcn-custom visually identical to the base shadcn theme until sa
|
||||
|
||||
[data-color-theme="shadcn"] .card,
|
||||
[data-color-theme="shadcn"] .column,
|
||||
[data-color-theme="shadcn-ember"] .card,
|
||||
[data-color-theme="shadcn-ember"] .column,
|
||||
[data-color-theme="shadcn-custom"] .card,
|
||||
[data-color-theme="shadcn-custom"] .column {
|
||||
border-width: var(--btn-border-width);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
(function () {
|
||||
try {
|
||||
var mode = localStorage.getItem("kb-dashboard-theme-mode") || "dark";
|
||||
var colorTheme = localStorage.getItem("kb-dashboard-color-theme") || "default";
|
||||
var colorTheme = localStorage.getItem("kb-dashboard-color-theme") || "shadcn-ember";
|
||||
var validThemes = [
|
||||
"default",
|
||||
"ocean",
|
||||
@@ -27,6 +27,7 @@
|
||||
"silver",
|
||||
"solarized",
|
||||
"factory",
|
||||
"factory-mono",
|
||||
"ayu",
|
||||
"one-dark",
|
||||
"nord",
|
||||
@@ -42,6 +43,20 @@
|
||||
"palenight",
|
||||
"monokai-pro",
|
||||
"slime",
|
||||
"brutalist",
|
||||
"neon-city",
|
||||
"parchment",
|
||||
"terminal",
|
||||
"glass",
|
||||
"horizon",
|
||||
"vitesse",
|
||||
"outrun",
|
||||
"snazzy",
|
||||
"porple",
|
||||
"espresso",
|
||||
"mars",
|
||||
"poimandres",
|
||||
"ember",
|
||||
"rust",
|
||||
"copper",
|
||||
"foundry",
|
||||
@@ -52,10 +67,31 @@
|
||||
"lavender",
|
||||
"neon-bloom",
|
||||
"sepia",
|
||||
"shadcn",
|
||||
"shadcn-ember",
|
||||
"shadcn-custom",
|
||||
"shadcn-blue",
|
||||
"shadcn-green",
|
||||
"shadcn-red",
|
||||
"shadcn-purple",
|
||||
"shadcn-pink",
|
||||
"shadcn-orange",
|
||||
"shadcn-yellow",
|
||||
"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: Desktop pre-hydration mirrors the full core theme union before falling back to Shadcn Ember, so every explicit legacy id stays valid on native startup.
|
||||
if (!validThemes.includes(colorTheme)) {
|
||||
colorTheme = "default";
|
||||
colorTheme = "shadcn-ember";
|
||||
}
|
||||
|
||||
var systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
@@ -64,7 +100,7 @@
|
||||
document.documentElement.setAttribute("data-color-theme", colorTheme);
|
||||
} catch (error) {
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
document.documentElement.setAttribute("data-color-theme", "default");
|
||||
document.documentElement.setAttribute("data-color-theme", "shadcn-ember");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user