From afde6b7857e04c1c9497f9db4cd881781444215e Mon Sep 17 00:00:00 2001 From: Scrubs McBarkley Date: Sat, 21 Mar 2026 23:50:43 +0000 Subject: [PATCH 1/2] feat: unify site theming via CSS custom properties (GH #91) Replace all hardcoded brand color hex values with CSS custom properties so BrandingContext drives both the customer portal and staff site. - index.css: add derived accent/primary vars using color-mix() (--color-accent-hover, --color-accent-dark, --color-accent-light, --color-accent-lighter, --color-primary-dark); fix focus ring styles to use var(--color-primary) instead of hardcoded hex - BrandingContext.tsx: also update in sync with primaryColor so PWA theme-color tracks branding at runtime - portal/sections: replace bg-[#8b7355], text-[#6b5a42], bg-[#f0ebe4], bg-[#faf5ef], hover:bg-[#7a6549] etc. with Tailwind v4 CSS var utilities (bg-(--color-accent), text-(--color-accent-dark), etc.) - pages: replace inline style "#4f8a6f"/"#3d7a5f" with var(--color-primary) / var(--color-primary-dark) across Appointments, Book, Clients, GroupBooking, Invoices, Reports, Services, Staff, and DevSessionIndicator Closes #91 Co-Authored-By: Paperclip --- apps/web/src/BrandingContext.tsx | 8 +++++ .../src/components/DevSessionIndicator.tsx | 2 +- apps/web/src/index.css | 9 +++-- apps/web/src/pages/Appointments.tsx | 6 ++-- apps/web/src/pages/Book.tsx | 16 ++++----- apps/web/src/pages/Clients.tsx | 6 ++-- apps/web/src/pages/GroupBooking.tsx | 4 +-- apps/web/src/pages/Invoices.tsx | 4 +-- apps/web/src/pages/Reports.tsx | 2 +- apps/web/src/pages/Services.tsx | 4 +-- apps/web/src/pages/Staff.tsx | 4 +-- .../src/portal/sections/AccountSettings.tsx | 12 +++---- apps/web/src/portal/sections/Appointments.tsx | 34 +++++++++---------- .../src/portal/sections/BillingPayments.tsx | 22 ++++++------ .../web/src/portal/sections/Communication.tsx | 12 +++---- apps/web/src/portal/sections/Dashboard.tsx | 16 ++++----- apps/web/src/portal/sections/PetProfiles.tsx | 14 ++++---- apps/web/src/portal/sections/ReportCards.tsx | 16 ++++----- 18 files changed, 102 insertions(+), 89 deletions(-) diff --git a/apps/web/src/BrandingContext.tsx b/apps/web/src/BrandingContext.tsx index 00a761c..ec5fad3 100644 --- a/apps/web/src/BrandingContext.tsx +++ b/apps/web/src/BrandingContext.tsx @@ -45,6 +45,14 @@ export function BrandingProvider({ children }: { children: React.ReactNode }) { useEffect(() => { document.documentElement.style.setProperty("--color-primary", branding.primaryColor); document.documentElement.style.setProperty("--color-accent", branding.accentColor); + // Keep PWA theme-color meta tag in sync with primary color + let metaThemeColor = document.querySelector("meta[name='theme-color']"); + if (!metaThemeColor) { + metaThemeColor = document.createElement("meta"); + metaThemeColor.name = "theme-color"; + document.head.appendChild(metaThemeColor); + } + metaThemeColor.content = branding.primaryColor; }, [branding.primaryColor, branding.accentColor]); return ( diff --git a/apps/web/src/components/DevSessionIndicator.tsx b/apps/web/src/components/DevSessionIndicator.tsx index 993698d..ee66891 100644 --- a/apps/web/src/components/DevSessionIndicator.tsx +++ b/apps/web/src/components/DevSessionIndicator.tsx @@ -29,7 +29,7 @@ export function DevSessionIndicator() { @@ -374,7 +374,7 @@ export function AppointmentsPage() {
{saving ? "Saving…" diff --git a/apps/web/src/pages/Book.tsx b/apps/web/src/pages/Book.tsx index ecfd5bb..0e0710d 100644 --- a/apps/web/src/pages/Book.tsx +++ b/apps/web/src/pages/Book.tsx @@ -66,8 +66,8 @@ function StepIndicator({ step }: { step: number }) { padding: "0.5rem 0.25rem", fontSize: 12, fontWeight: active ? 700 : 400, - color: active ? "#4f8a6f" : done ? "#4f8a6f" : "#9ca3af", - borderBottom: `3px solid ${active ? "#4f8a6f" : done ? "#4f8a6f" : "#e5e7eb"}`, + color: active ? "var(--color-primary)" : done ? "var(--color-primary)" : "#9ca3af", + borderBottom: `3px solid ${active ? "var(--color-primary)" : done ? "var(--color-primary)" : "#e5e7eb"}`, }} >
-
+
{fmtPrice(svc.basePriceCents)}
{fmtDuration(svc.durationMinutes)}
@@ -349,8 +349,8 @@ export function BookPage() { style={{ padding: "0.4rem 0.85rem", borderRadius: 6, - border: `2px solid ${selectedSlot === slot ? "#4f8a6f" : "#d1d5db"}`, - background: selectedSlot === slot ? "#4f8a6f" : "#fff", + border: `2px solid ${selectedSlot === slot ? "var(--color-primary)" : "#d1d5db"}`, + background: selectedSlot === slot ? "var(--color-primary)" : "#fff", color: selectedSlot === slot ? "#fff" : "#374151", fontSize: 13, fontWeight: 500, diff --git a/apps/web/src/pages/Clients.tsx b/apps/web/src/pages/Clients.tsx index c5354d9..ba11584 100644 --- a/apps/web/src/pages/Clients.tsx +++ b/apps/web/src/pages/Clients.tsx @@ -367,7 +367,7 @@ export function ClientsPage() {

Clients

@@ -622,7 +622,7 @@ export function ClientsPage() { {clientFormError &&

{clientFormError}

}
- @@ -761,7 +761,7 @@ export function ClientsPage() { {logFormError &&

{logFormError}

}
- diff --git a/apps/web/src/pages/GroupBooking.tsx b/apps/web/src/pages/GroupBooking.tsx index 445530a..5cf8d39 100644 --- a/apps/web/src/pages/GroupBooking.tsx +++ b/apps/web/src/pages/GroupBooking.tsx @@ -287,7 +287,7 @@ function NewGroupBookingForm({ @@ -471,7 +471,7 @@ export function GroupBookingPage() { diff --git a/apps/web/src/pages/Invoices.tsx b/apps/web/src/pages/Invoices.tsx index d1c3457..5039dd3 100644 --- a/apps/web/src/pages/Invoices.tsx +++ b/apps/web/src/pages/Invoices.tsx @@ -129,7 +129,7 @@ function CreateFromAppointmentForm({ @@ -540,7 +540,7 @@ export function InvoicesPage() { diff --git a/apps/web/src/pages/Reports.tsx b/apps/web/src/pages/Reports.tsx index f7b3ceb..a3515ce 100644 --- a/apps/web/src/pages/Reports.tsx +++ b/apps/web/src/pages/Reports.tsx @@ -270,7 +270,7 @@ export function ReportsPage() { -
diff --git a/apps/web/src/pages/Services.tsx b/apps/web/src/pages/Services.tsx index eb952b1..65cf380 100644 --- a/apps/web/src/pages/Services.tsx +++ b/apps/web/src/pages/Services.tsx @@ -119,7 +119,7 @@ export function ServicesPage() {

Services

@@ -232,7 +232,7 @@ export function ServicesPage() { diff --git a/apps/web/src/pages/Staff.tsx b/apps/web/src/pages/Staff.tsx index 5e9b594..aac23a7 100644 --- a/apps/web/src/pages/Staff.tsx +++ b/apps/web/src/pages/Staff.tsx @@ -78,7 +78,7 @@ export function StaffPage() {

Staff

-
@@ -145,7 +145,7 @@ export function StaffPage() {
{formError &&

{formError}

}
- diff --git a/apps/web/src/portal/sections/AccountSettings.tsx b/apps/web/src/portal/sections/AccountSettings.tsx index 670e879..2377023 100644 --- a/apps/web/src/portal/sections/AccountSettings.tsx +++ b/apps/web/src/portal/sections/AccountSettings.tsx @@ -22,7 +22,7 @@ export function AccountSettings({ readOnly }: Props) { key={id} onClick={() => setTab(id)} className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium ${ - tab === id ? "bg-[#f0ebe4] text-[#6b5a42]" : "text-stone-500 hover:bg-stone-50" + tab === id ? "bg-(--color-accent-light) text-(--color-accent-dark)" : "text-stone-500 hover:bg-stone-50" }`} > @@ -69,7 +69,7 @@ function PersonalInfo({ readOnly }: { readOnly: boolean }) {
))} {!readOnly && ( - )} @@ -103,7 +103,7 @@ function PasswordChange({ readOnly }: { readOnly: boolean }) {
-
@@ -116,7 +116,7 @@ function ManagePets({ readOnly }: { readOnly: boolean }) {
{PETS.map(pet => (
-
+
{pet.photo}
@@ -136,7 +136,7 @@ function ManagePets({ readOnly }: { readOnly: boolean }) {
))} {!readOnly && ( - @@ -165,7 +165,7 @@ function Agreements() { {new Date(agr.dateSigned).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })} - + ))} diff --git a/apps/web/src/portal/sections/Appointments.tsx b/apps/web/src/portal/sections/Appointments.tsx index 7d1aa70..fdb9281 100644 --- a/apps/web/src/portal/sections/Appointments.tsx +++ b/apps/web/src/portal/sections/Appointments.tsx @@ -30,13 +30,13 @@ export function AppointmentsSection({ readOnly }: Props) {
@@ -44,7 +44,7 @@ export function AppointmentsSection({ readOnly }: Props) { {!readOnly && (
@@ -218,7 +218,7 @@ function BookingFlow({ onClose, readOnly }: { onClose: () => void; readOnly: boo key={pet.id} onClick={() => { setSelectedPet(pet); setStep(2); }} className={`w-full flex items-center gap-3 p-3 rounded-xl border text-left transition-colors ${ - selectedPet?.id === pet.id ? "border-[#8b7355] bg-[#faf5ef]" : "border-stone-200 hover:border-stone-300" + selectedPet?.id === pet.id ? "border-(--color-accent) bg-(--color-accent-lighter)" : "border-stone-200 hover:border-stone-300" }`} > {pet.photo} @@ -246,7 +246,7 @@ function BookingFlow({ onClose, readOnly }: { onClose: () => void; readOnly: boo ); }} className={`w-full flex items-center justify-between p-3 rounded-xl border text-left ${ - selectedServices.find(s => s.id === svc.id) ? "border-[#8b7355] bg-[#faf5ef]" : "border-stone-200 hover:border-stone-300" + selectedServices.find(s => s.id === svc.id) ? "border-(--color-accent) bg-(--color-accent-lighter)" : "border-stone-200 hover:border-stone-300" }`} >
@@ -273,7 +273,7 @@ function BookingFlow({ onClose, readOnly }: { onClose: () => void; readOnly: boo ); }} className={`w-full flex items-center justify-between p-2.5 rounded-lg border text-left text-sm ${ - selectedAddOns.find(s => s.id === svc.id) ? "border-[#8b7355] bg-[#faf5ef]" : "border-stone-200 hover:border-stone-300" + selectedAddOns.find(s => s.id === svc.id) ? "border-(--color-accent) bg-(--color-accent-lighter)" : "border-stone-200 hover:border-stone-300" }`} >
@@ -291,7 +291,7 @@ function BookingFlow({ onClose, readOnly }: { onClose: () => void; readOnly: boo @@ -307,7 +307,7 @@ function BookingFlow({ onClose, readOnly }: { onClose: () => void; readOnly: boo @@ -426,7 +426,7 @@ function BookingFlow({ onClose, readOnly }: { onClose: () => void; readOnly: boo diff --git a/apps/web/src/portal/sections/BillingPayments.tsx b/apps/web/src/portal/sections/BillingPayments.tsx index be750f9..63d1240 100644 --- a/apps/web/src/portal/sections/BillingPayments.tsx +++ b/apps/web/src/portal/sections/BillingPayments.tsx @@ -38,7 +38,7 @@ export function BillingPayments({ readOnly }: Props) { > Add Tip -
@@ -57,7 +57,7 @@ export function BillingPayments({ readOnly }: Props) { key={id} onClick={() => setTab(id)} className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium ${ - tab === id ? "bg-[#f0ebe4] text-[#6b5a42]" : "text-stone-500 hover:bg-stone-50" + tab === id ? "bg-(--color-accent-light) text-(--color-accent-dark)" : "text-stone-500 hover:bg-stone-50" }`} > @@ -134,7 +134,7 @@ export function BillingPayments({ readOnly }: Props) {
))} {!readOnly && ( - @@ -145,8 +145,8 @@ export function BillingPayments({ readOnly }: Props) {
-
- +
+

Autopay

@@ -156,7 +156,7 @@ export function BillingPayments({ readOnly }: Props) { {!readOnly ? ( @@ -174,7 +174,7 @@ export function BillingPayments({ readOnly }: Props) { {PREPAID_PACKAGES.map(pkg => (
- +

{pkg.name}

@@ -184,7 +184,7 @@ export function BillingPayments({ readOnly }: Props) {
@@ -218,7 +218,7 @@ function TipModal({ onClose }: { onClose: () => void }) { key={pct} onClick={() => { setTipPercent(pct); setCustomTip(""); }} className={`flex-1 py-2 rounded-lg text-sm font-medium border ${ - tipPercent === pct ? "border-[#8b7355] bg-[#faf5ef] text-[#6b5a42]" : "border-stone-200 text-stone-600" + tipPercent === pct ? "border-(--color-accent) bg-(--color-accent-lighter) text-(--color-accent-dark)" : "border-stone-200 text-stone-600" }`} > {pct}% @@ -227,7 +227,7 @@ function TipModal({ onClose }: { onClose: () => void }) { - +
diff --git a/apps/web/src/portal/sections/Communication.tsx b/apps/web/src/portal/sections/Communication.tsx index d7fa4c9..a965054 100644 --- a/apps/web/src/portal/sections/Communication.tsx +++ b/apps/web/src/portal/sections/Communication.tsx @@ -16,7 +16,7 @@ export function Communication({ readOnly }: Props) { @@ -177,7 +177,7 @@ function NotificationPreferences({ readOnly }: { readOnly: boolean }) { onClick={() => toggle(cat.key, ch.key)} disabled={readOnly} className={`w-10 h-5 rounded-full transition-colors inline-block ${ - prefs[cat.key][ch.key] ? "bg-[#8b7355]" : "bg-stone-300" + prefs[cat.key][ch.key] ? "bg-(--color-accent)" : "bg-stone-300" } ${readOnly ? "cursor-not-allowed opacity-60" : ""}`} >
-
+
Next Appointment
@@ -71,7 +71,7 @@ export function Dashboard({ onNavigate, readOnly }: Props) {
-
{daysUntil(nextAppt.date)}
+
{daysUntil(nextAppt.date)}
days away
@@ -103,7 +103,7 @@ export function Dashboard({ onNavigate, readOnly }: Props) { className="bg-white rounded-2xl border border-stone-200 p-4 shadow-sm text-left hover:border-stone-300 transition-colors" >
-
+
{pet.photo}
@@ -128,14 +128,14 @@ export function Dashboard({ onNavigate, readOnly }: Props) { {/* Loyalty Card */}
-
+
Loyalty Rewards

{LOYALTY.points} pts

@@ -161,7 +161,7 @@ export function Dashboard({ onNavigate, readOnly }: Props) { {!readOnly && ( @@ -176,7 +176,7 @@ export function Dashboard({ onNavigate, readOnly }: Props) {
{recentEvents.map(evt => (
-
+
{evt.text} {formatDate(evt.date)}
@@ -184,7 +184,7 @@ export function Dashboard({ onNavigate, readOnly }: Props) {
diff --git a/apps/web/src/portal/sections/PetProfiles.tsx b/apps/web/src/portal/sections/PetProfiles.tsx index 5034be8..3f10d20 100644 --- a/apps/web/src/portal/sections/PetProfiles.tsx +++ b/apps/web/src/portal/sections/PetProfiles.tsx @@ -30,7 +30,7 @@ export function PetProfiles({ readOnly }: Props) { key={p.id} onClick={() => { setSelectedPetId(p.id); setActiveTab("info"); }} className={`flex items-center gap-3 px-4 py-3 rounded-xl border transition-colors ${ - p.id === selectedPetId ? "border-[#8b7355] bg-[#faf5ef]" : "border-stone-200 bg-white hover:border-stone-300" + p.id === selectedPetId ? "border-(--color-accent) bg-(--color-accent-lighter)" : "border-stone-200 bg-white hover:border-stone-300" }`} > {p.photo} @@ -45,7 +45,7 @@ export function PetProfiles({ readOnly }: Props) { {/* Profile Header */}
-
+
{pet.photo}
@@ -74,7 +74,7 @@ export function PetProfiles({ readOnly }: Props) { key={id} onClick={() => setActiveTab(id)} className={`flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium whitespace-nowrap ${ - activeTab === id ? "bg-[#f0ebe4] text-[#6b5a42]" : "text-stone-500 hover:text-stone-700" + activeTab === id ? "bg-(--color-accent-light) text-(--color-accent-dark)" : "text-stone-500 hover:text-stone-700" }`} > @@ -114,7 +114,7 @@ function BasicInfoTab({ pet, readOnly }: { pet: Pet; readOnly: boolean }) { {!readOnly && ( - )} @@ -148,7 +148,7 @@ function GroomingTab({ pet, readOnly }: { pet: Pet; readOnly: boolean }) { {!readOnly && ( - )} @@ -189,7 +189,7 @@ function VaccinationsTab({ pet, readOnly }: { pet: Pet; readOnly: boolean }) { {vax.documentUploaded ? ( Uploaded ) : !readOnly ? ( - @@ -226,7 +226,7 @@ function HistoryTab({ petHistory }: { petHistory: typeof PAST_APPOINTMENTS }) { {new Date(appt.date).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })} {appt.reportCardId && ( - Report → + Report → )}
)) diff --git a/apps/web/src/portal/sections/ReportCards.tsx b/apps/web/src/portal/sections/ReportCards.tsx index dacfe60..88172ec 100644 --- a/apps/web/src/portal/sections/ReportCards.tsx +++ b/apps/web/src/portal/sections/ReportCards.tsx @@ -33,7 +33,7 @@ export function ReportCards() { className="w-full bg-white rounded-2xl border border-stone-200 p-5 shadow-sm text-left hover:border-stone-300 transition-colors" >
-
+
@@ -70,13 +70,13 @@ function ReportCardDetail({ card, onBack }: { card: ReportCard; onBack: () => vo return (
-
{/* Header */} -
+

{card.petName}'s Grooming Report

{card.beforeDescription}

-
-

After

-
+
+

After

+
Photo placeholder

{card.afterDescription}

@@ -148,7 +148,7 @@ function ReportCardDetail({ card, onBack }: { card: ReportCard; onBack: () => vo )} {/* Groomer's Note */} -
+

A Note from {card.groomerName}

"{card.groomerNote}"

@@ -161,7 +161,7 @@ function ReportCardDetail({ card, onBack }: { card: ReportCard; onBack: () => vo {new Date(card.nextRecommendedDate).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}

-
-- 2.52.0 From a15585a8e6960cbed794bbffadb8e6d6dbdc1471 Mon Sep 17 00:00:00 2001 From: Scrubs McBarkley Date: Sun, 22 Mar 2026 00:12:57 +0000 Subject: [PATCH 2/2] fix: address QA feedback on site theming PR (GH #91) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- apps/web/src/BrandingContext.tsx | 17 +-- .../src/__tests__/BrandingContext.test.tsx | 106 ++++++++++++++++++ apps/web/src/portal/sections/ReportCards.tsx | 2 +- 3 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/__tests__/BrandingContext.test.tsx diff --git a/apps/web/src/BrandingContext.tsx b/apps/web/src/BrandingContext.tsx index ec5fad3..1420e02 100644 --- a/apps/web/src/BrandingContext.tsx +++ b/apps/web/src/BrandingContext.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useEffect, useState, useCallback } from "react"; +import { createContext, useContext, useEffect, useRef, useState, useCallback } from "react"; export interface Branding { businessName: string; @@ -27,6 +27,7 @@ export function useBranding() { export function BrandingProvider({ children }: { children: React.ReactNode }) { const [branding, setBranding] = useState(DEFAULT_BRANDING); + const metaThemeColorRef = useRef(null); const fetchBranding = useCallback(() => { 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-accent", branding.accentColor); // Keep PWA theme-color meta tag in sync with primary color - let metaThemeColor = document.querySelector("meta[name='theme-color']"); - if (!metaThemeColor) { - metaThemeColor = document.createElement("meta"); - metaThemeColor.name = "theme-color"; - document.head.appendChild(metaThemeColor); + if (!metaThemeColorRef.current) { + metaThemeColorRef.current = document.querySelector("meta[name='theme-color']"); + if (!metaThemeColorRef.current) { + metaThemeColorRef.current = document.createElement("meta"); + metaThemeColorRef.current.name = "theme-color"; + document.head.appendChild(metaThemeColorRef.current); + } } - metaThemeColor.content = branding.primaryColor; + metaThemeColorRef.current.content = branding.primaryColor; }, [branding.primaryColor, branding.accentColor]); return ( diff --git a/apps/web/src/__tests__/BrandingContext.test.tsx b/apps/web/src/__tests__/BrandingContext.test.tsx new file mode 100644 index 0000000..2e7816c --- /dev/null +++ b/apps/web/src/__tests__/BrandingContext.test.tsx @@ -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 ( +
+ {branding.primaryColor} + {branding.accentColor} +
+ ); +} + +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( + + + + ); + + 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( + + + + ); + + await waitFor(() => { + const meta = document.querySelector("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( + + + + ); + + await waitFor(() => { + expect(document.querySelector("meta[name='theme-color']")).not.toBeNull(); + }); + + rerender( + + + + ); + + await waitFor(() => { + const metas = document.querySelectorAll("meta[name='theme-color']"); + expect(metas.length).toBe(1); + }); + }); +}); diff --git a/apps/web/src/portal/sections/ReportCards.tsx b/apps/web/src/portal/sections/ReportCards.tsx index 88172ec..8336285 100644 --- a/apps/web/src/portal/sections/ReportCards.tsx +++ b/apps/web/src/portal/sections/ReportCards.tsx @@ -76,7 +76,7 @@ function ReportCardDetail({ card, onBack }: { card: ReportCard; onBack: () => vo
{/* Header */} -
+

{card.petName}'s Grooming Report