From c0daacf1ba457749c2068bddf540b337a7cbc849 Mon Sep 17 00:00:00 2001 From: Hisham Aburaqibah Date: Wed, 23 Sep 2026 22:33:58 +0200 Subject: [PATCH] more fixes - Drop the globe icon in the language switcher - Only show RightArrow on nav menuitem hover - Fix EU logo colors in light mode --- apps/web/app/components/ui/Card/CardShell.tsx | 1 - apps/web/app/components/ui/Card/CardTeam.tsx | 7 +- apps/web/app/components/ui/Card/types.ts | 5 +- .../ui/LanguageSwitcher/LanguageMenu.tsx | 273 ------------------ .../ui/LanguageSwitcher/LanguageSwitcher.css | 35 --- .../ui/LanguageSwitcher/LanguageSwitcher.tsx | 2 +- .../ui/LanguageSwitcher/LanguageToggle.tsx | 13 +- .../components/ui/LanguageSwitcher/index.ts | 2 - .../components/ui/Navigation/DesktopNav.tsx | 12 +- .../components/ui/Navigation/Navigation.tsx | 23 +- .../web/app/components/ui/Navigation/types.ts | 2 +- apps/web/app/routes/_page-kit/accordion.tsx | 2 +- apps/web/app/routes/home.tsx | 18 +- apps/web/public/assets/icons/globe-solid.svg | 10 - apps/web/public/assets/icons/globe.svg | 10 - 15 files changed, 43 insertions(+), 372 deletions(-) delete mode 100644 apps/web/app/components/ui/LanguageSwitcher/LanguageMenu.tsx delete mode 100644 apps/web/app/components/ui/LanguageSwitcher/LanguageSwitcher.css delete mode 100644 apps/web/public/assets/icons/globe-solid.svg delete mode 100644 apps/web/public/assets/icons/globe.svg diff --git a/apps/web/app/components/ui/Card/CardShell.tsx b/apps/web/app/components/ui/Card/CardShell.tsx index 1d2b9ab..551b1cc 100644 --- a/apps/web/app/components/ui/Card/CardShell.tsx +++ b/apps/web/app/components/ui/Card/CardShell.tsx @@ -89,7 +89,6 @@ const CARD_ICON_SRC: Record = { "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 { diff --git a/apps/web/app/components/ui/Card/CardTeam.tsx b/apps/web/app/components/ui/Card/CardTeam.tsx index 95a72ca..7bd101c 100644 --- a/apps/web/app/components/ui/Card/CardTeam.tsx +++ b/apps/web/app/components/ui/Card/CardTeam.tsx @@ -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" }, ]} /> @@ -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 = { linkedin: "linkedin", mail: "mail", - website: "globe", }; const SOCIAL_LABEL: Record = { linkedin: "LinkedIn", mail: "Email", - website: "Website", }; export function CardTeam({ diff --git a/apps/web/app/components/ui/Card/types.ts b/apps/web/app/components/ui/Card/types.ts index 0138b1c..f980865 100644 --- a/apps/web/app/components/ui/Card/types.ts +++ b/apps/web/app/components/ui/Card/types.ts @@ -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"; diff --git a/apps/web/app/components/ui/LanguageSwitcher/LanguageMenu.tsx b/apps/web/app/components/ui/LanguageSwitcher/LanguageMenu.tsx deleted file mode 100644 index e415aad..0000000 --- a/apps/web/app/components/ui/LanguageSwitcher/LanguageMenu.tsx +++ /dev/null @@ -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(null); - const buttonRef = useRef(null); - const pendingFocus = useRef(null); - - const rawId = useId(); - const listId = `sharper-language-${rawId.replace(/[^a-zA-Z0-9_-]/g, "")}`; - - const options = useCallback( - (): HTMLElement[] => - Array.from( - rootRef.current?.querySelectorAll("[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) => { - if (event.key === "ArrowDown") { - event.preventDefault(); - openAndFocus("first"); - } else if (event.key === "ArrowUp") { - event.preventDefault(); - openAndFocus("last"); - } - }; - - const handleOptionKeyDown = (event: ReactKeyboardEvent) => { - 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) => { - 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) => { - const next = event.relatedTarget as Node | null; - if (!next || !rootRef.current?.contains(next)) close(); - }; - - return ( -
- - - -
- ); -} - -export default LanguageMenu; - -/* --------------------------------------------------------------------------- - USAGE - --------------------------------------------------------------------------- - // In the navigation bar, wired from the router's location: - const { pathname, search, hash } = useLocation(); - - ------------------------------------------------------------------------ */ diff --git a/apps/web/app/components/ui/LanguageSwitcher/LanguageSwitcher.css b/apps/web/app/components/ui/LanguageSwitcher/LanguageSwitcher.css deleted file mode 100644 index 5830f7b..0000000 --- a/apps/web/app/components/ui/LanguageSwitcher/LanguageSwitcher.css +++ /dev/null @@ -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; - } -} diff --git a/apps/web/app/components/ui/LanguageSwitcher/LanguageSwitcher.tsx b/apps/web/app/components/ui/LanguageSwitcher/LanguageSwitcher.tsx index 9d92c5f..de9efe5 100644 --- a/apps/web/app/components/ui/LanguageSwitcher/LanguageSwitcher.tsx +++ b/apps/web/app/components/ui/LanguageSwitcher/LanguageSwitcher.tsx @@ -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 , the globe with a dropdown. + * icon. */ export function LanguageSwitcher({ locale, diff --git a/apps/web/app/components/ui/LanguageSwitcher/LanguageToggle.tsx b/apps/web/app/components/ui/LanguageSwitcher/LanguageToggle.tsx index f6db3d2..6b9bf82 100644 --- a/apps/web/app/components/ui/LanguageSwitcher/LanguageToggle.tsx +++ b/apps/web/app/components/ui/LanguageSwitcher/LanguageToggle.tsx @@ -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 and remains for - * anywhere a list of more than two languages is needed. - * + * the drawer still uses . * 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(" ")} > -