Files
sharper.ly/apps/web/app/components/ui/Navigation/Navigation.tsx
T
2026-09-11 02:46:56 +02:00

212 lines
7.4 KiB
TypeScript

import { useMemo } from "react";
import {
buildLocalizedHref,
getDirection,
LanguageSwitcher,
type Locale,
} from "../LanguageSwitcher";
import { Brand } from "./Brand";
import { DesktopNav } from "./DesktopNav";
import { EuBadge } from "./EuBadge";
import { MobileNav } from "./MobileNav";
import { getDefaultNavItems, getNavigationStrings } from "./nav-items";
import { cx } from "./styles";
import type { NavItem, NavigationStrings, NavSurface } from "./types";
export interface NavigationProps {
/** Reading locale. Drives the copy, the default hrefs and the arrow keys. */
locale?: Locale;
/** Current pathname, e.g. "/ar/contact". Used for aria-current="page". */
pathname: string;
/** Query string, carried across a language switch. */
search?: string;
/** Fragment, carried across a language switch. */
hash?: string;
/**
* The navigation tree. The app shell feeds this from the Strapi
* site-settings single type; omitting it renders the typed default IA.
*/
items?: readonly NavItem[];
/** Which tier-2 surface the bar paints. `:root` is dark, so dark is default. */
surface?: NavSurface;
/** Override any of the navigation's own strings (e.g. from the CMS). */
strings?: Partial<NavigationStrings>;
/** Where the logo links. Defaults to the locale home, "/en" or "/ar". */
homeHref?: string;
showLanguageSwitcher?: boolean;
/**
* Show the EU badge in the compact bar below xl as well. ON by default:
* donor visibility is non-negotiable, and the Figma mobile frame (20:220),
* which is logo + hamburger only, would leave the badge out of the header on
* every phone, tablet and 1280px-narrow laptop. Below xl the lockup renders
* at a reduced size rather than being dropped. Pass `false` only for a
* surface where the funding statement is guaranteed elsewhere on screen.
*/
showEuBadgeOnMobileBar?: boolean;
/** Stick the bar to the top of the viewport. */
sticky?: boolean;
className?: string;
}
/**
* The SHARP-ER site navigation - Figma frame 20:236.
*
* Logo at the inline start, the mandatory "Funded by the European Union" badge
* at the inline end, and the mega-menu groups between them. Everything is laid
* out with logical properties, so the Arabic bar mirrors (logo right, EU badge
* left) from `<html dir="rtl">` alone - there is no separate Arabic component.
*
* Below the xl breakpoint the bar collapses to logo + hamburger and the whole
* tree moves into a full-screen drawer.
*/
export function Navigation({
locale = "en",
pathname,
search,
hash,
items,
surface = "dark",
strings: stringOverrides,
homeHref,
showLanguageSwitcher = true,
showEuBadgeOnMobileBar = true,
sticky = true,
className,
}: NavigationProps) {
const navItems = useMemo(
() => items ?? getDefaultNavItems(locale),
[items, locale],
);
const strings = useMemo(
() => getNavigationStrings(locale, stringOverrides),
[locale, stringOverrides],
);
const resolvedHomeHref = homeHref ?? buildLocalizedHref("/", locale);
const direction = getDirection(locale);
return (
<header
data-surface={surface}
/* overflow-x-clip contains the mega panel's full-bleed backdrop without
clipping the panel itself, which overflows vertically. */
className={cx(
"w-full overflow-x-clip border-b border-[var(--border-subtle)]",
sticky && "sticky top-0 z-[var(--layer-header)]",
className,
)}
>
<div
className={cx(
"mx-auto flex w-full max-w-[var(--layout-content-max)] items-center justify-between",
/* The compact bar carries wordmark + EU lockup + hamburger on a
320px screen, so it runs a tighter gap than the desktop bar. */
"min-h-[64px] gap-xs px-[var(--layout-gutter)]",
"xl:min-h-[72px] xl:gap-lg xl:px-2xl",
)}
>
<Brand
href={resolvedHomeHref}
label={strings.homeLinkLabel}
surface={surface}
/>
<DesktopNav
items={navItems}
pathname={pathname}
strings={strings}
direction={direction}
className="hidden xl:block"
/>
<div className="flex items-center gap-xs xl:gap-md">
{/* Desktop-only cluster. Visibility lives on this wrapper rather than
on the children: two display utilities on one element are resolved
by the order Tailwind emits them, not by class-attribute order. */}
<div className="hidden items-center gap-md xl:flex">
{showLanguageSwitcher && (
<LanguageSwitcher
locale={locale}
pathname={pathname}
search={search}
hash={hash}
/>
)}
{!showEuBadgeOnMobileBar && (
<EuBadge
statement={strings.euStatement}
emblemAlt={strings.euEmblemAlt}
locale={locale}
surface={surface}
/>
)}
</div>
{showEuBadgeOnMobileBar && (
<EuBadge
statement={strings.euStatement}
emblemAlt={strings.euEmblemAlt}
locale={locale}
surface={surface}
/* Below xl the lockup is scaled down - emblem 36x24 instead of
48x32, statement wrapped in a narrower box - so it still fits
beside the wordmark and the hamburger at 320px. It is scaled,
never hidden: the statement stays at Label/Medium, i.e. the
same size it is everywhere else, so it remains legible. */
className="max-xl:[&>img]:h-[24px] max-xl:[&>img]:w-[36px] max-xl:[&>span]:w-[80px]"
/>
)}
<MobileNav
items={navItems}
pathname={pathname}
search={search}
hash={hash}
locale={locale}
strings={strings}
surface={surface}
homeHref={resolvedHomeHref}
showLanguageSwitcher={showLanguageSwitcher}
/>
</div>
</div>
</header>
);
}
export default Navigation;
/* ---------------------------------------------------------------------------
USAGE
---------------------------------------------------------------------------
// Dark bar on the dark home page, default IA, English:
<Navigation locale="en" pathname="/en" />
// Arabic. The bar mirrors from <html dir="rtl"> - no extra props, no
// separate component - and the arrow keys mirror with it.
<Navigation locale="ar" pathname="/ar/contact" />
// Light bar on an interior page:
<Navigation locale="en" pathname="/en/about" surface="light" />
// Fed from Strapi site-settings, with the CMS owning the labels and hrefs:
<Navigation
locale={locale}
pathname={pathname}
items={siteSettings.navigation}
strings={{ euStatement: siteSettings.euFundingStatement }}
/>
// Wired from a route, using the router's own location so a language switch
// keeps the query string and fragment:
const { pathname, search, hash } = useLocation();
<Navigation locale={locale} pathname={pathname} search={search} hash={hash} />
// The EU badge is in the compact bar by default. Opt out only where the
// funding statement is guaranteed to be visible elsewhere on the screen:
<Navigation locale="en" pathname="/en" showEuBadgeOnMobileBar={false} />
------------------------------------------------------------------------ */