more fixes
- Drop the globe icon in the language switcher - Only show RightArrow on nav menuitem hover - Fix EU logo colors in light mode
This commit is contained in:
@@ -89,7 +89,6 @@ const CARD_ICON_SRC: Record<CardIconName, string> = {
|
||||
"arrow-up": "url('/assets/icons/arrow-up.svg')",
|
||||
linkedin: "url('/assets/icons/linkedin.svg')",
|
||||
mail: "url('/assets/icons/mail.svg')",
|
||||
globe: "url('/assets/icons/globe.svg')",
|
||||
};
|
||||
|
||||
export interface CardIconProps {
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
socials={[
|
||||
{ kind: "linkedin", href: "https://www.linkedin.com/in/example" },
|
||||
{ kind: "mail", href: "mailto:helen.vance@example.org", label: "Email" },
|
||||
{ kind: "website", href: "https://example.org", label: "Website" },
|
||||
]}
|
||||
/>
|
||||
</ul>
|
||||
@@ -30,14 +29,14 @@
|
||||
import { CardLink, CardMedia, CardShell, CardIcon, cx } from "./CardShell";
|
||||
import type { CardElement, CardIconName, CardImage, CardTitleElement } from "./types";
|
||||
|
||||
export type CardTeamSocialKind = "linkedin" | "mail" | "website";
|
||||
export type CardTeamSocialKind = "linkedin" | "mail";
|
||||
|
||||
export interface CardTeamSocial {
|
||||
kind: CardTeamSocialKind;
|
||||
href: string;
|
||||
/**
|
||||
* Accessible name of the link. Defaults to the platform name; pass a
|
||||
* translated string on Arabic pages ("البريد الإلكتروني", "الموقع").
|
||||
* translated string on Arabic pages ("البريد الإلكتروني").
|
||||
*/
|
||||
label?: string;
|
||||
}
|
||||
@@ -59,13 +58,11 @@ export interface CardTeamProps {
|
||||
const SOCIAL_ICON: Record<CardTeamSocialKind, CardIconName> = {
|
||||
linkedin: "linkedin",
|
||||
mail: "mail",
|
||||
website: "globe",
|
||||
};
|
||||
|
||||
const SOCIAL_LABEL: Record<CardTeamSocialKind, string> = {
|
||||
linkedin: "LinkedIn",
|
||||
mail: "Email",
|
||||
website: "Website",
|
||||
};
|
||||
|
||||
export function CardTeam({
|
||||
|
||||
@@ -33,10 +33,9 @@ export interface CardLinkTarget {
|
||||
rel?: string;
|
||||
}
|
||||
|
||||
/** The five icons the card family uses, each an asset exported from Figma. */
|
||||
/** The icons the card family uses, each an asset exported from Figma. */
|
||||
export type CardIconName =
|
||||
| "arrow-right"
|
||||
| "arrow-up"
|
||||
| "linkedin"
|
||||
| "mail"
|
||||
| "globe";
|
||||
| "mail";
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useId,
|
||||
useRef,
|
||||
useState,
|
||||
type FocusEvent as ReactFocusEvent,
|
||||
type KeyboardEvent as ReactKeyboardEvent,
|
||||
} from "react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
import "./LanguageSwitcher.css";
|
||||
import {
|
||||
buildLocalizedHref,
|
||||
getDirection,
|
||||
LOCALE_ENDONYM,
|
||||
LOCALES,
|
||||
type Locale,
|
||||
} from "./locale-path";
|
||||
|
||||
/* Reinforces the global :focus-visible rule in app.css with the same tier-2
|
||||
focus tokens, so the ring is visible even if a caller overrides `outline`. */
|
||||
const FOCUS_RING =
|
||||
"focus-visible:outline-[color:var(--focus-ring-color)] " +
|
||||
"focus-visible:outline-[length:var(--focus-ring-width)] " +
|
||||
"focus-visible:outline-offset-[var(--focus-ring-offset)]";
|
||||
|
||||
const TRANSITION =
|
||||
"transition-colors duration-[var(--motion-duration-fast)] ease-[var(--motion-ease-standard)]";
|
||||
|
||||
type OptionPosition = "first" | "last";
|
||||
|
||||
export interface LanguageMenuProps {
|
||||
/** The locale the reader is currently on. */
|
||||
locale: Locale;
|
||||
/** Current pathname, e.g. "/en/news-and-updates". */
|
||||
pathname: string;
|
||||
/** Query string to carry across the switch. Usually `location.search`. */
|
||||
search?: string;
|
||||
/** Fragment to carry across the switch. Usually `location.hash`. */
|
||||
hash?: string;
|
||||
/**
|
||||
* Accessible name of the globe, in the reader's language ("Language"). It is
|
||||
* required because the globe carries no text of its own.
|
||||
*/
|
||||
label: string;
|
||||
/** Navigate with a full document request instead of a client transition. */
|
||||
reloadDocument?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The navigation's language control: the globe (figma 478:16381) as a button
|
||||
* that opens a short list of the site's languages, each linking to the page the
|
||||
* reader is on under that locale, with the query string and fragment kept.
|
||||
*
|
||||
* Not drawn in Figma - the design shows the globe alone - so the list is built
|
||||
* from the tier-2 surface, border and elevation tokens a popover uses, and
|
||||
* hangs from the globe's inline-end edge so it opens inward in both directions.
|
||||
*
|
||||
* A disclosure rather than an ARIA menu: the entries are ordinary links and are
|
||||
* announced and followed as links. Each carries its own lang/hrefLang and its
|
||||
* label its own dir, so a screen reader switches voice for "العربية". The
|
||||
* current language is marked with aria-current and set in bold, so the state is
|
||||
* not carried by colour alone. ArrowDown/ArrowUp open the list and move through it, Escape closes it
|
||||
* and returns focus to the globe, and an outside press or focus leaving closes
|
||||
* it. Focus is never trapped.
|
||||
*/
|
||||
export function LanguageMenu({
|
||||
locale,
|
||||
pathname,
|
||||
search,
|
||||
hash,
|
||||
label,
|
||||
reloadDocument = false,
|
||||
className,
|
||||
}: LanguageMenuProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const pendingFocus = useRef<OptionPosition | null>(null);
|
||||
|
||||
const rawId = useId();
|
||||
const listId = `sharper-language-${rawId.replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
||||
|
||||
const options = useCallback(
|
||||
(): HTMLElement[] =>
|
||||
Array.from(
|
||||
rootRef.current?.querySelectorAll<HTMLElement>("[data-language-option]") ??
|
||||
[],
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const focusOption = useCallback(
|
||||
(position: OptionPosition) => {
|
||||
const all = options();
|
||||
(position === "first" ? all[0] : all[all.length - 1])?.focus();
|
||||
},
|
||||
[options],
|
||||
);
|
||||
|
||||
const close = useCallback(() => {
|
||||
pendingFocus.current = null;
|
||||
setOpen(false);
|
||||
}, []);
|
||||
|
||||
/* Outside press. pointerdown rather than click so the list is already gone
|
||||
by the time the click lands on whatever is underneath. */
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
if (!rootRef.current?.contains(event.target as Node)) setOpen(false);
|
||||
};
|
||||
document.addEventListener("pointerdown", handlePointerDown);
|
||||
return () => document.removeEventListener("pointerdown", handlePointerDown);
|
||||
}, [open]);
|
||||
|
||||
/* A keyboard request to open and step into the list can only be honoured
|
||||
once the list has rendered. */
|
||||
useEffect(() => {
|
||||
const position = pendingFocus.current;
|
||||
if (!open || position === null) return;
|
||||
pendingFocus.current = null;
|
||||
focusOption(position);
|
||||
}, [open, focusOption]);
|
||||
|
||||
const openAndFocus = (position: OptionPosition) => {
|
||||
if (open) {
|
||||
focusOption(position);
|
||||
return;
|
||||
}
|
||||
pendingFocus.current = position;
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleButtonKeyDown = (event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
openAndFocus("first");
|
||||
} else if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
openAndFocus("last");
|
||||
}
|
||||
};
|
||||
|
||||
const handleOptionKeyDown = (event: ReactKeyboardEvent<HTMLAnchorElement>) => {
|
||||
const all = options();
|
||||
const index = all.indexOf(event.currentTarget);
|
||||
const moveTo = (to: number) => all[(to + all.length) % all.length]?.focus();
|
||||
|
||||
switch (event.key) {
|
||||
case "ArrowDown":
|
||||
event.preventDefault();
|
||||
moveTo(index + 1);
|
||||
break;
|
||||
case "ArrowUp":
|
||||
event.preventDefault();
|
||||
moveTo(index - 1);
|
||||
break;
|
||||
case "Home":
|
||||
event.preventDefault();
|
||||
moveTo(0);
|
||||
break;
|
||||
case "End":
|
||||
event.preventDefault();
|
||||
moveTo(all.length - 1);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key !== "Escape" || !open) return;
|
||||
event.preventDefault();
|
||||
close();
|
||||
buttonRef.current?.focus();
|
||||
};
|
||||
|
||||
/* React's onBlur is focusout, so it fires for descendants too. */
|
||||
const handleBlur = (event: ReactFocusEvent<HTMLDivElement>) => {
|
||||
const next = event.relatedTarget as Node | null;
|
||||
if (!next || !rootRef.current?.contains(next)) close();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleBlur}
|
||||
className={["relative", className].filter(Boolean).join(" ")}
|
||||
>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
aria-expanded={open}
|
||||
aria-controls={listId}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
onClick={() => (open ? close() : setOpen(true))}
|
||||
onKeyDown={handleButtonKeyDown}
|
||||
className={[
|
||||
"inline-flex shrink-0 cursor-pointer items-center justify-center rounded-control",
|
||||
"text-[var(--text-primary)] hover:text-[var(--text-accent)]",
|
||||
"aria-expanded:text-[var(--text-accent)]",
|
||||
TRANSITION,
|
||||
FOCUS_RING,
|
||||
].join(" ")}
|
||||
>
|
||||
<span aria-hidden="true" className="sh-language-globe" />
|
||||
</button>
|
||||
|
||||
<ul
|
||||
id={listId}
|
||||
hidden={!open}
|
||||
className={[
|
||||
"absolute end-0 top-full z-[var(--layer-dropdown)] mt-sm",
|
||||
"flex min-w-[10rem] flex-col gap-2xs p-2xs",
|
||||
"rounded-control border border-[var(--border-subtle)]",
|
||||
"bg-[var(--surface-raised)] shadow-[var(--elevation-popover)]",
|
||||
].join(" ")}
|
||||
>
|
||||
{LOCALES.map((option) => {
|
||||
const current = option === locale;
|
||||
return (
|
||||
<li key={option}>
|
||||
<Link
|
||||
to={buildLocalizedHref(pathname, option, { search, hash })}
|
||||
reloadDocument={reloadDocument}
|
||||
hrefLang={option}
|
||||
lang={option}
|
||||
aria-current={current ? "true" : undefined}
|
||||
data-language-option={option}
|
||||
onClick={close}
|
||||
onKeyDown={handleOptionKeyDown}
|
||||
className={[
|
||||
current ? "body-md-bold" : "body-md",
|
||||
"block rounded-control px-sm py-xs text-start no-underline",
|
||||
"text-[var(--text-primary)] hover:bg-[var(--surface-muted)]",
|
||||
"aria-[current=true]:text-[var(--text-accent)]",
|
||||
TRANSITION,
|
||||
FOCUS_RING,
|
||||
].join(" ")}
|
||||
>
|
||||
{/* The label carries its own direction; the row does not, so
|
||||
both names line up on the page's inline-start edge instead
|
||||
of each hugging the side its own script reads from. */}
|
||||
<span dir={getDirection(option)}>{LOCALE_ENDONYM[option]}</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LanguageMenu;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
USAGE
|
||||
---------------------------------------------------------------------------
|
||||
// In the navigation bar, wired from the router's location:
|
||||
const { pathname, search, hash } = useLocation();
|
||||
<LanguageMenu
|
||||
label={strings.languageMenuLabel}
|
||||
locale={locale}
|
||||
pathname={pathname}
|
||||
search={search}
|
||||
hash={hash}
|
||||
/>
|
||||
------------------------------------------------------------------------ */
|
||||
@@ -1,35 +0,0 @@
|
||||
/* ============================================================================
|
||||
LANGUAGE SWITCHER - figma `globe` 478:16381 (navigation 20:236)
|
||||
----------------------------------------------------------------------------
|
||||
The navigation now carries the language control as a bare 24x24 globe in the
|
||||
Actions slot, replacing the "AR" / "EN" pill that used to sit next to the EU
|
||||
badge. The exported asset fills its path with a hard-coded #1D1D1B, which
|
||||
would be invisible on the dark surface, so it is applied as a mask over
|
||||
`currentColor` - the same technique as `.sh-arrow-icon` and `.sh-card-icon`.
|
||||
That keeps the exact exported geometry while the colour comes from whichever
|
||||
semantic token the bar has in force.
|
||||
|
||||
GEOMETRY, straight from Figma: a 24x24 box (`--control-icon-size`) with the
|
||||
glyph filling it edge to edge. The globe is radially symmetric, so unlike the
|
||||
arrow it needs no RTL mirroring.
|
||||
========================================================================= */
|
||||
|
||||
@layer theme, base, components, utilities;
|
||||
|
||||
@layer components {
|
||||
.sh-language-globe {
|
||||
display: block;
|
||||
flex: none;
|
||||
inline-size: var(--control-icon-size);
|
||||
block-size: var(--control-icon-size);
|
||||
background-color: currentColor;
|
||||
-webkit-mask-image: url("/assets/icons/globe-solid.svg");
|
||||
mask-image: url("/assets/icons/globe-solid.svg");
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-size: 100% 100%;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
mask-position: center;
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export interface LanguageSwitcherProps {
|
||||
* same page under that locale, preserving the rest of the path.
|
||||
*
|
||||
* The mobile drawer uses this, where a spelled-out target is clearer than an
|
||||
* icon. The desktop bar uses <LanguageMenu />, the globe with a dropdown.
|
||||
* icon.
|
||||
*/
|
||||
export function LanguageSwitcher({
|
||||
locale,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Link } from "react-router";
|
||||
|
||||
import "./LanguageSwitcher.css";
|
||||
import {
|
||||
getCounterpartLocale,
|
||||
getCounterpartPath,
|
||||
@@ -34,15 +33,11 @@ export interface LanguageToggleProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* Both languages, side by side: the globe, the current locale in an accent
|
||||
* chip, and the other locale as a link to the same page under that locale -
|
||||
* the redesign's nav control, replacing the globe-plus-dropdown on the desktop
|
||||
* bar.
|
||||
* Both languages, side by side: the current locale in an accent chip, and the
|
||||
* other locale as a link to the same page under that locale.
|
||||
*
|
||||
* One tap to switch and no state to open, which is why the draft prefers it;
|
||||
* the drawer still uses <LanguageSwitcher /> and <LanguageMenu /> remains for
|
||||
* anywhere a list of more than two languages is needed.
|
||||
*
|
||||
* the drawer still uses <LanguageSwitcher />.
|
||||
* The current language is marked with `aria-current`, not by colour alone. The
|
||||
* link carries its own lang/hrefLang/dir and is named with the target
|
||||
* language's endonym ("العربية"), so a screen reader switches voice for it
|
||||
@@ -71,8 +66,6 @@ export function LanguageToggle({
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
<span aria-hidden="true" className="sh-language-globe" />
|
||||
|
||||
<span className="flex items-center">
|
||||
<span
|
||||
aria-current="true"
|
||||
|
||||
@@ -3,8 +3,6 @@ export type {
|
||||
LanguageSwitcherProps,
|
||||
LanguageSwitcherSize,
|
||||
} from "./LanguageSwitcher";
|
||||
export { LanguageMenu } from "./LanguageMenu";
|
||||
export type { LanguageMenuProps } from "./LanguageMenu";
|
||||
export { LanguageToggle } from "./LanguageToggle";
|
||||
export type { LanguageToggleProps } from "./LanguageToggle";
|
||||
|
||||
|
||||
@@ -51,8 +51,14 @@ function MenuArrow() {
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
/* Mirrors with the page: the arrow points at the reading edge. */
|
||||
className="shrink-0 rtl:-scale-x-100"
|
||||
/* Mirrors with the page: the arrow points at the reading edge. It only
|
||||
appears when the reader hovers or focus-es the link, so a row of
|
||||
always-visible arrows does not announce every entry at once. */
|
||||
className={cx(
|
||||
"shrink-0 rtl:-scale-x-100 opacity-0",
|
||||
"transition-opacity duration-[var(--motion-duration-fast)] ease-[var(--motion-ease-standard)]",
|
||||
"group-hover:opacity-100 group-focus-visible:opacity-100",
|
||||
)}
|
||||
>
|
||||
<path d="M5 12h13M12.5 6.5L18 12l-5.5 5.5" />
|
||||
</svg>
|
||||
@@ -489,7 +495,7 @@ export function DesktopNav({
|
||||
}
|
||||
onKeyDown={(event) => handleSubLinkKeyDown(event, item.id)}
|
||||
className={cx(
|
||||
"body-sm-bold flex items-center justify-between gap-md",
|
||||
"group body-sm-bold flex items-center justify-between gap-md",
|
||||
"rounded-control px-sm py-xs no-underline",
|
||||
"text-[var(--text-primary)]",
|
||||
"hover:bg-[var(--surface-page)] hover:text-[var(--text-accent)]",
|
||||
|
||||
@@ -45,16 +45,17 @@ export interface NavigationProps {
|
||||
* The SHARP-ER site navigation - Figma frame 20:236.
|
||||
*
|
||||
* 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.
|
||||
* mega-menu groups in the middle, and the language control 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.
|
||||
* (logo right, language control 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.
|
||||
*
|
||||
* EU VISIBILITY: the updated design has no funding badge in the bar - the
|
||||
* Actions slot holds the language globe and the theme toggle. The mandatory
|
||||
* Actions slot holds the language control and the theme toggle. 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.
|
||||
@@ -122,16 +123,16 @@ export function Navigation({
|
||||
/>
|
||||
|
||||
<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
|
||||
{/* The language control 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. */
|
||||
actions, then 24px to the language cluster. 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"
|
||||
|
||||
@@ -41,7 +41,7 @@ export interface NavigationStrings {
|
||||
closeMenu: string;
|
||||
/** Accessible name of the logo link. */
|
||||
homeLinkLabel: string;
|
||||
/** Accessible name of the language globe, which carries no text of its own. */
|
||||
/** Accessible name of the in-bar language control, which carries no text of its own. */
|
||||
languageMenuLabel: string;
|
||||
/** The mandatory EU funding statement. */
|
||||
euStatement: string;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
panel right after it. Deliberately NOT an ARIA accordion widget with arrow
|
||||
key roving: there is one tab stop per question, which is what a keyboard
|
||||
user expects from a list of links-like controls, and it matches the
|
||||
disclosure the navigation already ships (DesktopNav, LanguageMenu).
|
||||
disclosure the navigation already ships (DesktopNav, the language switcher).
|
||||
|
||||
The answer is CMS richtext (markdown), so it renders through <RichText />
|
||||
rather than as a raw string.
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
is why every inline edge below is logical (ps-/pe-/start-/end-).
|
||||
========================================================================= */
|
||||
|
||||
import { Link } from "react-router";
|
||||
import { Link, useRouteLoaderData } from "react-router";
|
||||
|
||||
import { ArrowIcon, Button } from "~/components/ui/Button";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
buildWebSiteJsonLd,
|
||||
} from "~/lib/seo";
|
||||
import { safeHref } from "~/lib/safe-href";
|
||||
import { DEFAULT_THEME, type AppTheme } from "~/lib/theme";
|
||||
|
||||
import type { Route } from "./+types/home";
|
||||
import { Accordion } from "./_page-kit/accordion";
|
||||
@@ -434,6 +435,10 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
||||
const { locale, hero, approach, gallery, partners, news, faqs } = loaderData;
|
||||
const t = createTranslator(locale);
|
||||
const copy = pageCopy(locale).home;
|
||||
/* The hero badge sits on the page's own surface, so it follows the theme
|
||||
exactly as the footer's badge does: same root cookie, same emblem. */
|
||||
const root = useRouteLoaderData<{ theme?: AppTheme }>("root");
|
||||
const theme: AppTheme = root?.theme ?? DEFAULT_THEME;
|
||||
/* The three pillars are the About page's approved copy - one source, so the
|
||||
two pages can never drift apart. */
|
||||
const pillars = pageCopy(locale).about.pillars;
|
||||
@@ -495,16 +500,17 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
||||
]}
|
||||
reveal
|
||||
/* EU visibility sits at the top of the hero (the draft's badge). The
|
||||
emblem is the dark-surface export because the hero sits on the
|
||||
page's own dark surface. `emblemAlt` is left at its empty default:
|
||||
the statement is real text beside the emblem, so naming the image
|
||||
would read the attribution out twice. */
|
||||
emblem matches the page surface the hero stands on, which the theme
|
||||
cookie decides - dark emblem in dark mode, light emblem in light
|
||||
mode, like the footer's badge. `emblemAlt` is left at its empty
|
||||
default: the statement is real text beside the emblem, so naming the
|
||||
image would read the attribution out twice. */
|
||||
funderBadge={
|
||||
<EuBadge
|
||||
size="sm"
|
||||
statement={t("site.euStatement")}
|
||||
locale={locale}
|
||||
surface="dark"
|
||||
surface={theme}
|
||||
className="self-start"
|
||||
/>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user