/* ============================================================================ Footer - figma frame 140:7813 ---------------------------------------------------------------------------- ONE component for all four published variants: light EN 140:7812 | dark EN 140:7814 | light AR 140:8713 | dark AR 140:8685 `surface` picks the palette and the logo cut; direction comes from `` and every offset is a logical property, so the Arabic variants are the same markup mirrored - there is no RTL branch anywhere below. STYLING: Tailwind utilities for layout and spacing, tier-2 semantic tokens for everything that carries meaning (`text-[var(--text-primary)]`). No raw hex, no tier-1 primitive, no component stylesheet. TWO DELIBERATE DEVIATIONS FROM THE PUBLISHED FRAMES, both noted inline: 1. the two EN frames disagree on the sitemap (140:7812 carries the real IA but no column headings, 140:7814 carries headings but stale labels) - this takes the newer IA and the headings; 2. `text/tertiary` (#949492) on white is ~2.9:1, so the small labels use `--text-secondary` instead. Revert the two lines marked (a11y) for pixel-exact parity. ========================================================================= */ import { Link } from "react-router"; import type { FooterCopy, FooterLink, FooterPartner, FooterProps, FooterSocialLink, } from "./types"; import { DEFAULT_FOOTER_COPY, DEFAULT_FOOTER_PARTNERS, DEFAULT_FOOTER_SOCIAL_LINKS, FOOTER_ASSETS, FOOTER_EU_EMBLEM_SIZE, FOOTER_WORDMARK_SIZE, defaultFooterLinkGroups, } from "./content"; import { FooterSocialIcon } from "./FooterSocialIcon"; const cx = (...parts: readonly (string | false | undefined)[]): string => parts.filter(Boolean).join(" "); function definedOnly(source: Partial | undefined): Partial { if (!source) return {}; return Object.fromEntries( Object.entries(source).filter(([, value]) => value !== undefined), ) as Partial; } /** Visible focus ring on every interactive element, straight off the focus tokens. */ const FOCUS_RING = cx( "rounded-[var(--radius-control)]", "focus-visible:[outline:var(--focus-ring-width)_solid_var(--focus-ring-color)]", "focus-visible:[outline-offset:var(--focus-ring-offset)]", ); /** Only `color` - `transition-colors` would also fade `outline-color`, which makes the focus ring arrive late. */ const TRANSITION = "transition-[color] duration-[var(--motion-duration-fast)] ease-[var(--motion-ease-standard)]"; /** Body/Medium Bold links, as in figma 380:12385. */ const LINK_CLASS = cx( FOCUS_RING, TRANSITION, "body-md-bold inline-block text-[var(--text-primary)]", "hover:text-[var(--text-link)] hover:underline underline-offset-4", ); /** Label/Medium, used for the column headings and the "implemented by" label. */ const LABEL_CLASS = "label-md uppercase"; /** * Internal links go through the router so the footer navigates client-side; * anything flagged `external` stays a plain anchor and opens in a new tab. */ function FooterNavLink({ link, className }: { link: FooterLink; className: string }) { if (link.external) { return ( {link.label} ); } return ( {link.label} ); } export function Footer({ surface = "dark", locale = "en", dir, linkGroups, socialLinks = DEFAULT_FOOTER_SOCIAL_LINKS, partners = DEFAULT_FOOTER_PARTNERS, copy: copyOverrides, homeHref, className, id, }: FooterProps) { const isDark = surface === "dark"; // A CMS field that has not been filled in arrives as undefined; spreading it // would blank the default, so only defined overrides are applied. const copy: FooterCopy = { ...DEFAULT_FOOTER_COPY[locale], ...definedOnly(copyOverrides) }; const groups = linkGroups ?? defaultFooterLinkGroups(locale); const home = homeHref ?? `/${locale}`; return (
{/* ---- brand + sitemap (figma 140:7684 / 140:8686) ---------------- */}
{/* --- brand column (figma 140:7685 / 140:8687) ----------------- */}
mirrors the row. "lg:w-[345px] lg:shrink-0 lg:border-e lg:border-[var(--border-subtle)] lg:pe-2xl", )} > SHARP-ER

{copy.tagline}

{copy.description}

{/* --- EU visibility (figma 140:7688 / 238:2325) -------------- */}
{copy.euEmblemAlt}

{copy.euStatement}

{socialLinks.length > 0 && }
{/* --- sitemap columns (figma 380:12402) ------------------------ */}
{/* ---- implemented-by bar (figma 228:3062 / 140:8710) ------------- */} {partners.length > 0 && (
{/* Figma paints this label background/accent on dark and text/secondary on light; --text-accent is the readable green. */}

{copy.implementedByLabel}

    {partners.map((partner) => (
  • ))}
)}
); } /* --------------------------------------------------------------------------- Consortium logo. The three marks are shown together and never on their own (communication guideline), so this renders one mark of that row - each keeps its own designed box rather than a shared height rule. ------------------------------------------------------------------------ */ function PartnerMark({ isDark, partner }: { isDark: boolean; partner: FooterPartner }) { const image = ( {partner.name} ); if (!partner.href) return image; return ( {image} ); } /* --------------------------------------------------------------------------- Social row. Not in the published frames - the draft copy asks for it, so it closes the brand column. The negative inline-start margin cancels the icon padding so the glyphs optically align with the text above them. ------------------------------------------------------------------------ */ function SocialRow({ copy, links, }: { copy: FooterCopy; links: readonly FooterSocialLink[]; }) { return (

{copy.socialHeading}

    {links.map((link) => (
  • ))}
); } export default Footer; /* ============================================================================ Footer.stories-ish - usage, as a comment only (no Storybook in this project) ---------------------------------------------------------------------------- // 1. Dark English footer - the default, matches figma 140:7814. // already carries the direction.