+ Customer Dashboard
+
Account Settings
-
Manage your account information
+Manage your account 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) => (
+ -
+
+
+ {warning.label}: {warning.ratio.toFixed(1)}:1, below the {WARN_CONTRAST}:1
+ readability guideline.
+ {warning.substituted &&
+ " The storefront will show this text in black or white until it is fixed."}
+
+
+ ))}
+
+ );
+}
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 (
+
Personal Information
@@ -151,7 +150,7 @@ export default function SettingsPage() {
className="mt-1"
/>
) : (
- +
Email cannot be changed
-
+
+
+
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) => (
+ -
+
+
+ {warning.label}: {warning.ratio.toFixed(1)}:1, below the {WARN_CONTRAST}:1
+ readability guideline.
+ {warning.substituted &&
+ " The storefront will show this text in black or white until it is fixed."}
+
+
+ ))}
+
+ );
+}
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({
+
Address
@@ -267,7 +266,7 @@ export default function SettingsPage() {
placeholder="Street address"
/>
) : (
- -
+ {warnings.map((warning) => (
+
- + + + {warning.label}: {warning.ratio.toFixed(1)}:1, below the {WARN_CONTRAST}:1 + readability guideline. + {warning.substituted && + " The storefront will show this text in black or white until it is fixed."} + + + ))} +
+
Shipping Address
-
Where should we deliver your order?
+Where should we deliver your order?
+
Business Information
-
+
Optional - Only complete if ordering for a business