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">
|
<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">
|
<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>
|
||||||
<div className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">MRP Codex</div>
|
<h1 className="text-xl font-extrabold uppercase tracking-[0.24em] text-text">CODEXIUM</h1>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<nav className="mt-6 space-y-2">
|
<nav className="mt-6 space-y-2">
|
||||||
{links.map((link) => (
|
{links.map((link) => (
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { ThemeProvider } from "../theme/ThemeProvider";
|
import { ThemeProvider } from "../theme/ThemeProvider";
|
||||||
import { ThemeToggle } from "../components/ThemeToggle";
|
import { ThemeToggle } from "../components/ThemeToggle";
|
||||||
|
|
||||||
describe("ThemeToggle", () => {
|
describe("ThemeToggle", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
window.localStorage.clear();
|
||||||
|
document.documentElement.removeAttribute("style");
|
||||||
|
document.documentElement.classList.remove("dark");
|
||||||
|
});
|
||||||
|
|
||||||
it("toggles the html dark class", () => {
|
it("toggles the html dark class", () => {
|
||||||
render(
|
render(
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
@@ -16,5 +22,31 @@ describe("ThemeToggle", () => {
|
|||||||
|
|
||||||
expect(document.documentElement.classList.contains("dark")).toBe(true);
|
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 ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||||
const storageKey = "mrp.theme.mode";
|
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 }) {
|
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [mode, setMode] = useState<ThemeMode>(() => {
|
const [mode, setMode] = useState<ThemeMode>(() => {
|
||||||
@@ -20,6 +28,20 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|||||||
return stored === "dark" ? "dark" : "light";
|
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(() => {
|
useEffect(() => {
|
||||||
document.documentElement.classList.toggle("dark", mode === "dark");
|
document.documentElement.classList.toggle("dark", mode === "dark");
|
||||||
document.documentElement.style.colorScheme = mode;
|
document.documentElement.style.colorScheme = mode;
|
||||||
@@ -31,10 +53,8 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.documentElement.style.setProperty("--color-brand", hexToRgbTriplet(profile.theme.primaryColor));
|
applyThemeVariables(profile);
|
||||||
document.documentElement.style.setProperty("--color-accent", hexToRgbTriplet(profile.theme.accentColor));
|
window.localStorage.setItem(brandProfileKey, JSON.stringify({ theme: profile.theme }));
|
||||||
document.documentElement.style.setProperty("--color-surface-brand", hexToRgbTriplet(profile.theme.surfaceColor));
|
|
||||||
document.documentElement.style.setProperty("--font-family", profile.theme.fontFamily);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
|
|||||||
Reference in New Issue
Block a user