fix: address QA feedback on site theming PR (GH #91)
- Fix gradient regression in ReportCards.tsx: use distinct color stops (--color-accent-lighter → --color-accent-light) to restore subtle gradient - Fix BrandingContext meta tag accumulation: cache ref with useRef instead of querying DOM on every render to prevent duplicate elements on remount - Add BrandingContext.test.tsx: verify CSS vars applied, theme-color meta created/updated, and no duplicate meta tags on rerender Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { createContext, useContext, useEffect, useState, useCallback } from "react";
|
import { createContext, useContext, useEffect, useRef, useState, useCallback } from "react";
|
||||||
|
|
||||||
export interface Branding {
|
export interface Branding {
|
||||||
businessName: string;
|
businessName: string;
|
||||||
@@ -27,6 +27,7 @@ export function useBranding() {
|
|||||||
|
|
||||||
export function BrandingProvider({ children }: { children: React.ReactNode }) {
|
export function BrandingProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [branding, setBranding] = useState<Branding>(DEFAULT_BRANDING);
|
const [branding, setBranding] = useState<Branding>(DEFAULT_BRANDING);
|
||||||
|
const metaThemeColorRef = useRef<HTMLMetaElement | null>(null);
|
||||||
|
|
||||||
const fetchBranding = useCallback(() => {
|
const fetchBranding = useCallback(() => {
|
||||||
fetch("/api/branding")
|
fetch("/api/branding")
|
||||||
@@ -46,13 +47,15 @@ export function BrandingProvider({ children }: { children: React.ReactNode }) {
|
|||||||
document.documentElement.style.setProperty("--color-primary", branding.primaryColor);
|
document.documentElement.style.setProperty("--color-primary", branding.primaryColor);
|
||||||
document.documentElement.style.setProperty("--color-accent", branding.accentColor);
|
document.documentElement.style.setProperty("--color-accent", branding.accentColor);
|
||||||
// Keep PWA theme-color meta tag in sync with primary color
|
// Keep PWA theme-color meta tag in sync with primary color
|
||||||
let metaThemeColor = document.querySelector<HTMLMetaElement>("meta[name='theme-color']");
|
if (!metaThemeColorRef.current) {
|
||||||
if (!metaThemeColor) {
|
metaThemeColorRef.current = document.querySelector<HTMLMetaElement>("meta[name='theme-color']");
|
||||||
metaThemeColor = document.createElement("meta");
|
if (!metaThemeColorRef.current) {
|
||||||
metaThemeColor.name = "theme-color";
|
metaThemeColorRef.current = document.createElement("meta");
|
||||||
document.head.appendChild(metaThemeColor);
|
metaThemeColorRef.current.name = "theme-color";
|
||||||
|
document.head.appendChild(metaThemeColorRef.current);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
metaThemeColor.content = branding.primaryColor;
|
metaThemeColorRef.current.content = branding.primaryColor;
|
||||||
}, [branding.primaryColor, branding.accentColor]);
|
}, [branding.primaryColor, branding.accentColor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { render, waitFor } from "@testing-library/react";
|
||||||
|
import { BrandingProvider, useBranding } from "../BrandingContext.js";
|
||||||
|
|
||||||
|
function BrandingConsumer() {
|
||||||
|
const { branding } = useBranding();
|
||||||
|
return (
|
||||||
|
<div data-testid="branding">
|
||||||
|
<span data-testid="primary">{branding.primaryColor}</span>
|
||||||
|
<span data-testid="accent">{branding.accentColor}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
document.documentElement.style.removeProperty("--color-primary");
|
||||||
|
document.documentElement.style.removeProperty("--color-accent");
|
||||||
|
// Remove any theme-color meta tags
|
||||||
|
document.querySelectorAll("meta[name='theme-color']").forEach((el) => el.remove());
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("BrandingProvider", () => {
|
||||||
|
it("applies CSS vars to document root when branding loads", async () => {
|
||||||
|
const branding = {
|
||||||
|
businessName: "Test Salon",
|
||||||
|
primaryColor: "#123456",
|
||||||
|
accentColor: "#654321",
|
||||||
|
logoBase64: null,
|
||||||
|
logoMimeType: null,
|
||||||
|
};
|
||||||
|
global.fetch = vi.fn(() =>
|
||||||
|
Promise.resolve({ ok: true, json: async () => branding } as Response)
|
||||||
|
) as unknown as typeof fetch;
|
||||||
|
|
||||||
|
render(
|
||||||
|
<BrandingProvider>
|
||||||
|
<BrandingConsumer />
|
||||||
|
</BrandingProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(document.documentElement.style.getPropertyValue("--color-primary")).toBe("#123456");
|
||||||
|
expect(document.documentElement.style.getPropertyValue("--color-accent")).toBe("#654321");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates and updates meta[name=theme-color]", async () => {
|
||||||
|
const branding = {
|
||||||
|
businessName: "Test Salon",
|
||||||
|
primaryColor: "#abcdef",
|
||||||
|
accentColor: "#fedcba",
|
||||||
|
logoBase64: null,
|
||||||
|
logoMimeType: null,
|
||||||
|
};
|
||||||
|
global.fetch = vi.fn(() =>
|
||||||
|
Promise.resolve({ ok: true, json: async () => branding } as Response)
|
||||||
|
) as unknown as typeof fetch;
|
||||||
|
|
||||||
|
render(
|
||||||
|
<BrandingProvider>
|
||||||
|
<BrandingConsumer />
|
||||||
|
</BrandingProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const meta = document.querySelector<HTMLMetaElement>("meta[name='theme-color']");
|
||||||
|
expect(meta).not.toBeNull();
|
||||||
|
expect(meta!.content).toBe("#abcdef");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not create duplicate meta[name=theme-color] tags on rerender", async () => {
|
||||||
|
const branding = {
|
||||||
|
businessName: "Test Salon",
|
||||||
|
primaryColor: "#111111",
|
||||||
|
accentColor: "#222222",
|
||||||
|
logoBase64: null,
|
||||||
|
logoMimeType: null,
|
||||||
|
};
|
||||||
|
global.fetch = vi.fn(() =>
|
||||||
|
Promise.resolve({ ok: true, json: async () => branding } as Response)
|
||||||
|
) as unknown as typeof fetch;
|
||||||
|
|
||||||
|
const { rerender } = render(
|
||||||
|
<BrandingProvider>
|
||||||
|
<BrandingConsumer />
|
||||||
|
</BrandingProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(document.querySelector("meta[name='theme-color']")).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<BrandingProvider>
|
||||||
|
<BrandingConsumer />
|
||||||
|
</BrandingProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const metas = document.querySelectorAll("meta[name='theme-color']");
|
||||||
|
expect(metas.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -76,7 +76,7 @@ function ReportCardDetail({ card, onBack }: { card: ReportCard; onBack: () => vo
|
|||||||
|
|
||||||
<div className="bg-white rounded-2xl border border-stone-200 shadow-sm overflow-hidden">
|
<div className="bg-white rounded-2xl border border-stone-200 shadow-sm overflow-hidden">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-gradient-to-r from-(--color-accent-light) to-(--color-accent-light) p-6">
|
<div className="bg-gradient-to-r from-(--color-accent-lighter) to-(--color-accent-light) p-6">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<h2 className="text-xl font-semibold text-stone-800">{card.petName}'s Grooming Report</h2>
|
<h2 className="text-xl font-semibold text-stone-800">{card.petName}'s Grooming Report</h2>
|
||||||
<button className="flex items-center gap-1.5 px-3 py-1.5 bg-white/80 text-stone-700 rounded-lg text-sm font-medium hover:bg-white">
|
<button className="flex items-center gap-1.5 px-3 py-1.5 bg-white/80 text-stone-700 rounded-lg text-sm font-medium hover:bg-white">
|
||||||
|
|||||||
Reference in New Issue
Block a user