diff --git a/nextjs_space/app/store/[slug]/how-it-works/how-it-works-client.tsx b/nextjs_space/app/store/[slug]/how-it-works/how-it-works-client.tsx index 3a6a6e99..5feb4df1 100644 --- a/nextjs_space/app/store/[slug]/how-it-works/how-it-works-client.tsx +++ b/nextjs_space/app/store/[slug]/how-it-works/how-it-works-client.tsx @@ -30,8 +30,8 @@ export function HowItWorksClient() { if (loading) { return ( -
-

{t("common.loading")}

+
+

{t("common.loading")}

); } diff --git a/nextjs_space/app/store/[slug]/layout.tsx b/nextjs_space/app/store/[slug]/layout.tsx index 179b11a9..574a1845 100644 --- a/nextjs_space/app/store/[slug]/layout.tsx +++ b/nextjs_space/app/store/[slug]/layout.tsx @@ -20,7 +20,7 @@ import { AutomatosWidgetWrapper } from "@/components/admin/AutomatosWidgetWrappe import { Ga4Tag } from "@/components/seo/ga4-tag"; import { parseTenantSettings } from "@/lib/tenant/tenant-settings"; import { sanitizeCss, extractGoogleFontsImports } from "@/lib/security/css-utils"; -import { hexToHsl } from "@/lib/color-utils"; +import { buildColorOverrideVars } from "@/lib/theme/tenant-tokens"; import type { Metadata } from "next"; import { buildStoreMetadata, @@ -263,20 +263,21 @@ async function renderTenantStore( pageContent: (activeTemplate?.pageContent as any) || defaults?.pageContent || {}, }; + // The palette the nav/footer override legibility check is made against — + // the same designSystem fallback order TenantThemeProvider uses. + const paletteBase = designSystem?.colors || settings.designSystem?.colors; + // Render navigation const renderNavigation = () => { // Data-driven: use section component from layout.json if (layout?.navigation) { const NavComponent = getSectionComponent(layout.navigation); if (NavComponent) { - const navOverrides = layout.navigationConfig?.colorOverrides; - const navStyle: CSSProperties | undefined = navOverrides - ? (Object.fromEntries( - Object.entries(navOverrides) - .filter(([, v]) => v && typeof v === 'string' && v.trim()) - .map(([k, v]) => [`--tenant-color-${k}`, (v as string).startsWith('#') ? hexToHsl(v as string) : v]) - ) as CSSProperties) - : undefined; + // Legibility-checked against the tenant palette (lib/theme/tenant-tokens) + const navVars = buildColorOverrideVars(layout.navigationConfig?.colorOverrides, { + base: paletteBase, + }); + const navStyle = Object.keys(navVars).length > 0 ? (navVars as CSSProperties) : undefined; return (
@@ -312,17 +313,14 @@ async function renderTenantStore( if (FooterComponent) { const isDarkFooter = layout.footer === 'FooterBrand' || layout.footer === 'FooterFull'; const darkDefaults: Record = isDarkFooter - ? { '--tenant-color-background': '220 15% 10%', '--tenant-color-text': '0 0% 100%', '--tenant-color-heading': '0 0% 100%', '--tenant-color-border': '0 0% 100%' } - : {}; - const footerOverrides = layout.footerConfig?.colorOverrides; - const overrideEntries = footerOverrides - ? Object.fromEntries( - Object.entries(footerOverrides) - .filter(([, v]) => v && typeof v === 'string' && v.trim()) - .map(([k, v]) => [`--tenant-color-${k}`, (v as string).startsWith('#') ? hexToHsl(v as string) : v]) - ) + ? { background: '220 15% 10%', text: '0 0% 100%', heading: '0 0% 100%', border: '0 0% 100%' } : {}; - const footerStyle = { ...darkDefaults, ...overrideEntries } as CSSProperties; + // Overrides layer on the dark defaults; both are legibility-checked + // against the tenant palette (lib/theme/tenant-tokens). + const footerStyle = buildColorOverrideVars(layout.footerConfig?.colorOverrides, { + defaults: darkDefaults, + base: paletteBase, + }) as CSSProperties; return (
0 ? footerStyle : undefined}> diff --git a/nextjs_space/app/store/[slug]/login/login-form.tsx b/nextjs_space/app/store/[slug]/login/login-form.tsx index d1c4f651..982097e6 100644 --- a/nextjs_space/app/store/[slug]/login/login-form.tsx +++ b/nextjs_space/app/store/[slug]/login/login-form.tsx @@ -22,7 +22,7 @@ function LoginFormInner({ businessName, logoUrl, basePath }: TenantLoginFormProp /> )}

Sign in to {businessName}

-

Welcome back! Please sign in to continue

+

Welcome back! Please sign in to continue

; default: - return ; + return ; } }; @@ -91,8 +91,8 @@ export default async function OrderConfirmationPage({ {getStatusIcon(order.paymentStatus)}

{status.title}

-

{status.message}

-

+

{status.message}

+

Order Number:{" "} {order.drGreenInvoiceNum || order.orderNumber}

@@ -149,11 +149,11 @@ export default async function OrderConfirmationPage({ return ( <>
- Subtotal + Subtotal {currency} {order.subtotal.toFixed(2)}
- Shipping + Shipping {currency} {order.shippingCost.toFixed(2)}
diff --git a/nextjs_space/app/store/[slug]/register/page.tsx b/nextjs_space/app/store/[slug]/register/page.tsx index 658a7ee5..436e855d 100644 --- a/nextjs_space/app/store/[slug]/register/page.tsx +++ b/nextjs_space/app/store/[slug]/register/page.tsx @@ -24,10 +24,10 @@ export default function RegisterRedirectPage() { }, [slug, router]); return ( -
+
-

Redirecting to eligibility check...

+

Redirecting to eligibility check...

); diff --git a/nextjs_space/app/store/[slug]/settings/page.tsx b/nextjs_space/app/store/[slug]/settings/page.tsx index fe46d6a7..c37eacb1 100644 --- a/nextjs_space/app/store/[slug]/settings/page.tsx +++ b/nextjs_space/app/store/[slug]/settings/page.tsx @@ -98,7 +98,7 @@ export default function SettingsPage() {
-

Loading...

+

Loading...

); @@ -109,21 +109,20 @@ export default function SettingsPage() { } return ( -
+
{/* Header */}
- Customer Dashboard -

+ Customer Dashboard +

Account Settings

-

Manage your account information

+

Manage your account information

{!isEditing && ( @@ -131,9 +130,9 @@ export default function SettingsPage() {
{/* Profile Information */} -
-

- +
+

+ Personal Information

@@ -151,7 +150,7 @@ export default function SettingsPage() { className="mt-1" /> ) : ( -
+
{formData.firstName || "Not set"}
)} @@ -169,7 +168,7 @@ export default function SettingsPage() { className="mt-1" /> ) : ( -
+
{formData.lastName || "Not set"}
)} @@ -179,11 +178,11 @@ export default function SettingsPage() {
-
- +
+ {user.primaryEmailAddress?.emailAddress}
-

+

Email cannot be changed

@@ -192,7 +191,7 @@ export default function SettingsPage() { {isEditing ? (
- +
) : ( -
- +
+ {formData.phone || "Not set"}
)} @@ -216,8 +215,8 @@ export default function SettingsPage() {
-
- +
+ {(user.publicMetadata?.role as string) || "PATIENT"}
@@ -247,9 +246,9 @@ export default function SettingsPage() {
{/* Address Information */} -
-

- +
+

+ Address

@@ -267,7 +266,7 @@ export default function SettingsPage() { placeholder="Street address" /> ) : ( -
+
{formData.addressLine1 || "Not set"}
)} @@ -286,7 +285,7 @@ export default function SettingsPage() { placeholder="Apartment, suite, etc." /> ) : ( -
+
{formData.addressLine2 || "Not set"}
)} @@ -305,7 +304,7 @@ export default function SettingsPage() { className="mt-1" /> ) : ( -
+
{formData.city || "Not set"}
)} @@ -323,7 +322,7 @@ export default function SettingsPage() { className="mt-1" /> ) : ( -
+
{formData.state || "Not set"}
)} @@ -341,7 +340,7 @@ export default function SettingsPage() { className="mt-1" /> ) : ( -
+
{formData.postalCode || "Not set"}
)} @@ -360,7 +359,7 @@ export default function SettingsPage() { className="mt-1" /> ) : ( -
+
{formData.country || "Not set"}
)} @@ -371,7 +370,6 @@ export default function SettingsPage() {
+ {/* Navigation & Footer Color Overrides */} @@ -214,6 +229,7 @@ export function ColoursTab({
))}
+
@@ -278,6 +294,7 @@ export function ColoursTab({
))}
+
diff --git a/nextjs_space/app/tenant-admin/branding/tabs/contrast-hint.tsx b/nextjs_space/app/tenant-admin/branding/tabs/contrast-hint.tsx new file mode 100644 index 00000000..029edbf8 --- /dev/null +++ b/nextjs_space/app/tenant-admin/branding/tabs/contrast-hint.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { AlertTriangle } from "lucide-react"; +import { + MIN_CONTRAST, + WARN_CONTRAST, + contrastRatio, + toHslChannels, +} from "@/lib/theme/tenant-tokens"; + +export interface ContrastPair { + label: string; + foreground?: string | null; + background?: string | null; +} + +interface ContrastWarning { + label: string; + ratio: number; + substituted: boolean; +} + +/** + * The pairs that fall below the readability guideline, and whether the + * storefront will substitute black or white for them (the legibility floor in + * lib/theme/tenant-tokens). Pairs with an unparseable colour are skipped. + */ +export function contrastWarnings(pairs: ContrastPair[]): ContrastWarning[] { + return pairs.flatMap(({ label, foreground, background }) => { + const fg = toHslChannels(foreground); + const bg = toHslChannels(background); + if (!fg || !bg) return []; + const ratio = contrastRatio(fg, bg); + return ratio < WARN_CONTRAST + ? [{ label, ratio, substituted: ratio < MIN_CONTRAST }] + : []; + }); +} + +/** + * The text-on-background pairs a nav, footer or section override changes. + * Colours the override leaves unset inherit from the brand palette, exactly as + * the storefront resolves them. Overrides that touch none of the relevant + * colours produce no pairs, so the brand-level warning is not repeated. + */ +export function overridePairs( + overrides: Record | undefined, + base: { textColor: string; headingColor: string; backgroundColor: string }, + scope: string, +): ContrastPair[] { + if (!overrides) return []; + const pairs: ContrastPair[] = []; + if (["background", "text", "heading"].some((key) => overrides[key])) { + const background = overrides.background || base.backgroundColor; + pairs.push( + { + label: `${scope} body text on its background`, + foreground: overrides.text || base.textColor, + background, + }, + { + label: `${scope} heading text on its background`, + foreground: overrides.heading || base.headingColor, + background, + }, + ); + } + // Cards inside the section take the override's surface; the storefront + // substitutes black/white there too when the text no longer reads. + if (overrides.surface) { + pairs.push({ + label: `${scope} body text on its card background`, + foreground: overrides.text || base.textColor, + background: overrides.surface, + }); + } + return pairs; +} + +export function ContrastHint({ pairs }: { pairs: ContrastPair[] }) { + const warnings = contrastWarnings(pairs); + if (warnings.length === 0) return null; + return ( +
    + {warnings.map((warning) => ( +
  • +
  • + ))} +
+ ); +} diff --git a/nextjs_space/app/tenant-admin/branding/tabs/section-colour-panel.tsx b/nextjs_space/app/tenant-admin/branding/tabs/section-colour-panel.tsx index 49fa3a68..d296aefc 100644 --- a/nextjs_space/app/tenant-admin/branding/tabs/section-colour-panel.tsx +++ b/nextjs_space/app/tenant-admin/branding/tabs/section-colour-panel.tsx @@ -2,6 +2,7 @@ import { X } from "lucide-react"; import { ColorPicker } from "./shared"; +import { ContrastHint, overridePairs } from "./contrast-hint"; import type { EditorFormData, SetFormData } from "./types"; /** Colour override groups — shared with ColoursTab global palette. Kept here @@ -134,6 +135,7 @@ export function SectionColourPanel({

))}
+
); } diff --git a/nextjs_space/components/consultation/consultation-form.tsx b/nextjs_space/components/consultation/consultation-form.tsx index a3542813..86114b54 100644 --- a/nextjs_space/components/consultation/consultation-form.tsx +++ b/nextjs_space/components/consultation/consultation-form.tsx @@ -213,10 +213,10 @@ export function ConsultationForm({ {/* Progress Bar */}
- + Step {currentStep} of {TOTAL_STEPS} - + {STEP_NAMES[currentStep - 1]}
diff --git a/nextjs_space/components/consultation/id-upload-form.tsx b/nextjs_space/components/consultation/id-upload-form.tsx index 16d74be6..b4361fac 100644 --- a/nextjs_space/components/consultation/id-upload-form.tsx +++ b/nextjs_space/components/consultation/id-upload-form.tsx @@ -211,10 +211,10 @@ export function IdUploadForm({ tenantSlug }: IdUploadFormProps) { {/* Progress Bar */}
- + Step {currentStep} of {TOTAL_STEPS} - + {STEP_NAMES[currentStep - 1]}
diff --git a/nextjs_space/components/consultation/steps/address-step.tsx b/nextjs_space/components/consultation/steps/address-step.tsx index 2ac8c3b8..438f10b2 100644 --- a/nextjs_space/components/consultation/steps/address-step.tsx +++ b/nextjs_space/components/consultation/steps/address-step.tsx @@ -56,10 +56,10 @@ export function AddressStep({ {/* Shipping Address Section */}
-

+

Shipping Address

-

Where should we deliver your order?

+

Where should we deliver your order?

@@ -158,15 +158,15 @@ export function AddressStep({
{/* Divider */} -
+
{/* Business Address Section */}
-

+

Business Information

-

+

Optional - Only complete if ordering for a business

diff --git a/nextjs_space/components/consultation/steps/business-info-step.tsx b/nextjs_space/components/consultation/steps/business-info-step.tsx index c54873bd..99817d95 100644 --- a/nextjs_space/components/consultation/steps/business-info-step.tsx +++ b/nextjs_space/components/consultation/steps/business-info-step.tsx @@ -34,10 +34,10 @@ export function BusinessInfoStep({ return (
-

+

Type of Business

-

+

Optional - Only complete if ordering for a business

diff --git a/nextjs_space/components/consultation/steps/contact-details-step.tsx b/nextjs_space/components/consultation/steps/contact-details-step.tsx index e7fc5e2e..eb849d5d 100644 --- a/nextjs_space/components/consultation/steps/contact-details-step.tsx +++ b/nextjs_space/components/consultation/steps/contact-details-step.tsx @@ -72,10 +72,10 @@ export function ContactDetailsStep({ return (
-

+

Contact Details

-

+

Please provide your personal information

@@ -304,7 +304,7 @@ export function ContactDetailsStep({ @@ -327,7 +327,7 @@ export function ContactDetailsStep({ diff --git a/nextjs_space/components/consultation/steps/id-upload-step.tsx b/nextjs_space/components/consultation/steps/id-upload-step.tsx index 8aee35eb..67a85ddb 100644 --- a/nextjs_space/components/consultation/steps/id-upload-step.tsx +++ b/nextjs_space/components/consultation/steps/id-upload-step.tsx @@ -66,10 +66,10 @@ export function IdUploadStep({ return (
-

+

Verify your identity

-

+

Upload a clear photo of a valid government ID (National ID, passport or driving licence). It must be your actual ID document — selfies or other photos will be rejected. @@ -110,16 +110,16 @@ export function IdUploadStep({