2026-09-11 02:46:56 +02:00
|
|
|
import { useMemo } from "react";
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
buildLocalizedHref,
|
|
|
|
|
getDirection,
|
2026-09-16 20:32:56 +02:00
|
|
|
LanguageToggle,
|
2026-09-11 02:46:56 +02:00
|
|
|
type Locale,
|
|
|
|
|
} from "../LanguageSwitcher";
|
|
|
|
|
|
|
|
|
|
import { Brand } from "./Brand";
|
|
|
|
|
import { DesktopNav } from "./DesktopNav";
|
|
|
|
|
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;
|
|
|
|
|
/** Stick the bar to the top of the viewport. */
|
|
|
|
|
sticky?: boolean;
|
|
|
|
|
className?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The SHARP-ER site navigation - Figma frame 20:236.
|
|
|
|
|
*
|
2026-09-16 20:32:56 +02:00
|
|
|
* Three slots, spread across the bar: the wordmark at the inline start, the
|
|
|
|
|
* mega-menu groups in the middle, and the language globe at the inline end.
|
|
|
|
|
* Everything is laid out with logical properties, so the Arabic bar mirrors
|
|
|
|
|
* (logo right, globe left) from `<html dir="rtl">` alone - there is no separate
|
|
|
|
|
* Arabic component, which is what variants 334:526 and 334:764 show.
|
|
|
|
|
*
|
|
|
|
|
* Below the xl breakpoint the bar collapses to wordmark + hamburger (20:220)
|
|
|
|
|
* and the whole tree moves into a full-screen drawer.
|
2026-09-11 02:46:56 +02:00
|
|
|
*
|
2026-09-16 20:32:56 +02:00
|
|
|
* EU VISIBILITY: the updated design has no funding badge in the bar - the
|
|
|
|
|
* Actions slot holds the language globe alone. The mandatory "Funded by the
|
|
|
|
|
* European Union" lockup is carried by <Footer />, which renders on every page,
|
|
|
|
|
* and by the mobile drawer; it is not dropped from the site.
|
2026-09-11 02:46:56 +02:00
|
|
|
*/
|
|
|
|
|
export function Navigation({
|
|
|
|
|
locale = "en",
|
|
|
|
|
pathname,
|
|
|
|
|
search,
|
|
|
|
|
hash,
|
|
|
|
|
items,
|
|
|
|
|
surface = "dark",
|
|
|
|
|
strings: stringOverrides,
|
|
|
|
|
homeHref,
|
|
|
|
|
showLanguageSwitcher = 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}
|
|
|
|
|
className={cx(
|
2026-09-16 20:32:56 +02:00
|
|
|
"w-full border-b border-[var(--border-subtle)]",
|
|
|
|
|
/* Translucent ground + blur, as the drafts draw the bar. */
|
|
|
|
|
"bg-[var(--surface-nav)] backdrop-blur-[14px]",
|
2026-09-11 02:46:56 +02:00
|
|
|
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",
|
2026-09-16 20:32:56 +02:00
|
|
|
/* Mobile 20:220 is a fixed 64px bar; the gutter is the site's own
|
|
|
|
|
rather than the frame's 20px, so the wordmark lines up with the
|
|
|
|
|
page content underneath it. */
|
|
|
|
|
"h-[64px] gap-md px-[var(--layout-gutter)]",
|
|
|
|
|
/* The drafts pad the bar with the same gutter every section uses, so
|
|
|
|
|
the wordmark sits directly above the page's content edge. */
|
|
|
|
|
"xl:h-auto xl:gap-lg xl:px-[var(--layout-gutter-lg)] xl:py-sm",
|
2026-09-11 02:46:56 +02:00
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<Brand
|
|
|
|
|
href={resolvedHomeHref}
|
|
|
|
|
label={strings.homeLinkLabel}
|
|
|
|
|
surface={surface}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<DesktopNav
|
|
|
|
|
items={navItems}
|
|
|
|
|
pathname={pathname}
|
|
|
|
|
strings={strings}
|
|
|
|
|
direction={direction}
|
|
|
|
|
className="hidden xl:block"
|
|
|
|
|
/>
|
|
|
|
|
|
2026-09-16 20:32:56 +02:00
|
|
|
<div className="flex items-center gap-md">
|
|
|
|
|
{/* The globe is desktop-only: the mobile bar (20:220) is wordmark +
|
|
|
|
|
hamburger, and the drawer carries the switcher instead. Visibility
|
|
|
|
|
lives on this wrapper rather than on the child, because two
|
|
|
|
|
display utilities on one element are resolved by the order
|
|
|
|
|
Tailwind emits them, not by class-attribute order. */}
|
|
|
|
|
{showLanguageSwitcher && (
|
|
|
|
|
/* Figma 555:3848 sets a 20px rule between the links and the
|
|
|
|
|
actions, then 24px to the globe. It is decorative, so it is
|
|
|
|
|
hidden from assistive technology and drawn with a border token
|
|
|
|
|
rather than a rule element. */
|
|
|
|
|
<div className="hidden items-center gap-lg xl:flex">
|
|
|
|
|
<span
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
className="h-5 w-px bg-[var(--border-default)]"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Both languages side by side, per the redesign's nav: the
|
|
|
|
|
current one chipped, the other one link. */}
|
|
|
|
|
<LanguageToggle
|
|
|
|
|
label={strings.languageMenuLabel}
|
2026-09-11 02:46:56 +02:00
|
|
|
locale={locale}
|
|
|
|
|
pathname={pathname}
|
|
|
|
|
search={search}
|
|
|
|
|
hash={hash}
|
|
|
|
|
/>
|
2026-09-16 20:32:56 +02:00
|
|
|
</div>
|
2026-09-11 02:46:56 +02:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<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} />
|
|
|
|
|
------------------------------------------------------------------------ */
|