/* ============================================================================ 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. ONE DELIBERATE DEVIATION FROM THE PUBLISHED FRAMES: the two EN frames disagree on the sitemap. 140:7812 carries the real IA but no column headings; 140:7814 carries headings but labels for pages that do not exist (Partners, Blog, Photo Gallery, SUQs, Pilot Initiatives, Research). This takes the newer IA - built from ROUTE_SLUGS, the same source the router is built from - and the headings. Linking the frame's labels verbatim is what previously shipped five dead links. Column headings take `--text-tertiary`, as the frames do: that token is already re-pointed to a readable step on the light surface (semantic.css), so it needs no a11y override here. ========================================================================= */ import { Link } from "react-router"; import type { FooterCopy, FooterLink, FooterPartner, FooterProps, FooterSocialLink, } from "./types"; import { DEFAULT_FOOTER_COPY, DEFAULT_FOOTER_PARTNERS, defaultFooterSocialLinks, FOOTER_ASSETS, FOOTER_EU_EMBLEM_SIZE, FOOTER_WORDMARK_SIZE, defaultFooterLinkGroups, } from "./content"; import "./Footer.css"; import { EuBadge } from "../EuBadge"; 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/Large Bold links - 18/28, as the footer frame sets them. */ const LINK_CLASS = cx( FOCUS_RING, /* Colour AND the 4px nudge, both eased, both in ./Footer.css - see the note there on why this is not a `translate-x` utility. */ "sh-footer-link", "body-md-bold inline-block no-underline text-[var(--text-primary)]", "hover:text-[var(--text-accent)]", ); /** Label/Medium - the "implemented by" label (figma 228:3035). */ const LABEL_CLASS = "label-md uppercase"; /** The drafts set the column headings as a small uppercase label. `--text-tertiary` is already remapped to a readable step on the light surface, so the same token is correct on both. */ const GROUP_HEADING_CLASS = "label-md uppercase text-[var(--text-tertiary)]"; /** * 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, 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); /* `[]` from a caller hides the row deliberately; `undefined` means "nobody said", so the project's own confirmed channels stand in. */ const social = socialLinks ?? defaultFooterSocialLinks(locale); const home = homeHref ?? `/${locale}`; return (
{/* ---- brand column + sitemap --------------------------------- */}
SHARP-ER {/* One line: the full name and the dates the project runs. */}

{copy.brandLine}

{/* The draft draws each channel as a bordered button with its glyph and a written label, not as a bare icon. */} {social.length > 0 && ( <>

{copy.socialHeading}

    {social.map((link) => (
  • ))}
)} {/* EU visibility, through the shared lockup the hero uses. */}
{/* ---- implemented by ------------------------------------------ */} {partners.length > 0 && (

{copy.implementedByLabel}

{/* The draft prints the three names in bordered chips because it cannot embed artwork. The real marks are shown instead - they are contractual visibility and must not be substituted. */}
    {partners.map((partner) => (
  • ))}
)} {/* ---- the funding disclaimer closes the footer ----------------- */}

{copy.disclaimer}

); } /* --------------------------------------------------------------------------- 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} ); } /* --------------------------------------------------------------------------- One social channel, drawn as the draft's bordered button: the glyph, then the written label ("Follow us on Facebook"), so the control names itself instead of relying on an icon alone. ------------------------------------------------------------------------ */ function SocialButton({ link }: { link: FooterSocialLink }) { return ( {link.label} ); } 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.