theme color persist
This commit is contained in:
@@ -191,9 +191,7 @@ export function AppShell() {
|
||||
<div className="mx-auto flex w-full max-w-[1760px] gap-3 2xl:gap-4">
|
||||
<aside className="hidden w-72 shrink-0 flex-col rounded-[22px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur md:flex 2xl:w-80">
|
||||
<div>
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">MRP Codex</div>
|
||||
<h1 className="mt-2 text-xl font-extrabold text-text">Manufacturing foundation</h1>
|
||||
<p className="mt-2 text-sm text-muted">Single-tenant platform shell with branding, auth, file storage, and planning foundations.</p>
|
||||
<h1 className="text-xl font-extrabold uppercase tracking-[0.24em] text-text">CODEXIUM</h1>
|
||||
</div>
|
||||
<nav className="mt-6 space-y-2">
|
||||
{links.map((link) => (
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { ThemeProvider } from "../theme/ThemeProvider";
|
||||
import { ThemeToggle } from "../components/ThemeToggle";
|
||||
|
||||
describe("ThemeToggle", () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear();
|
||||
document.documentElement.removeAttribute("style");
|
||||
document.documentElement.classList.remove("dark");
|
||||
});
|
||||
|
||||
it("toggles the html dark class", () => {
|
||||
render(
|
||||
<ThemeProvider>
|
||||
@@ -16,5 +22,31 @@ describe("ThemeToggle", () => {
|
||||
|
||||
expect(document.documentElement.classList.contains("dark")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("hydrates persisted brand theme values on startup", async () => {
|
||||
window.localStorage.setItem(
|
||||
"mrp.theme.brand-profile",
|
||||
JSON.stringify({
|
||||
theme: {
|
||||
primaryColor: "#112233",
|
||||
accentColor: "#445566",
|
||||
surfaceColor: "#778899",
|
||||
fontFamily: "Manrope",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
render(
|
||||
<ThemeProvider>
|
||||
<div>Theme</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.documentElement.style.getPropertyValue("--color-brand")).toBe("17 34 51");
|
||||
expect(document.documentElement.style.getPropertyValue("--color-accent")).toBe("68 85 102");
|
||||
expect(document.documentElement.style.getPropertyValue("--color-surface-brand")).toBe("119 136 153");
|
||||
expect(document.documentElement.style.getPropertyValue("--font-family")).toBe("Manrope");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,14 @@ interface ThemeContextValue {
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||
const storageKey = "mrp.theme.mode";
|
||||
const brandProfileKey = "mrp.theme.brand-profile";
|
||||
|
||||
function applyThemeVariables(profile: Pick<CompanyProfileDto, "theme">) {
|
||||
document.documentElement.style.setProperty("--color-brand", hexToRgbTriplet(profile.theme.primaryColor));
|
||||
document.documentElement.style.setProperty("--color-accent", hexToRgbTriplet(profile.theme.accentColor));
|
||||
document.documentElement.style.setProperty("--color-surface-brand", hexToRgbTriplet(profile.theme.surfaceColor));
|
||||
document.documentElement.style.setProperty("--font-family", profile.theme.fontFamily);
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [mode, setMode] = useState<ThemeMode>(() => {
|
||||
@@ -20,6 +28,20 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
return stored === "dark" ? "dark" : "light";
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const storedBrandProfile = window.localStorage.getItem(brandProfileKey);
|
||||
if (!storedBrandProfile) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(storedBrandProfile) as Pick<CompanyProfileDto, "theme">;
|
||||
applyThemeVariables(parsed);
|
||||
} catch {
|
||||
window.localStorage.removeItem(brandProfileKey);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.classList.toggle("dark", mode === "dark");
|
||||
document.documentElement.style.colorScheme = mode;
|
||||
@@ -31,10 +53,8 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.documentElement.style.setProperty("--color-brand", hexToRgbTriplet(profile.theme.primaryColor));
|
||||
document.documentElement.style.setProperty("--color-accent", hexToRgbTriplet(profile.theme.accentColor));
|
||||
document.documentElement.style.setProperty("--color-surface-brand", hexToRgbTriplet(profile.theme.surfaceColor));
|
||||
document.documentElement.style.setProperty("--font-family", profile.theme.fontFamily);
|
||||
applyThemeVariables(profile);
|
||||
window.localStorage.setItem(brandProfileKey, JSON.stringify({ theme: profile.theme }));
|
||||
};
|
||||
|
||||
const value = useMemo(
|
||||
|
||||
Reference in New Issue
Block a user