212 lines
7.4 KiB
TypeScript
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} />
|
|
------------------------------------------------------------------------ */
|