import type { ControlSize } from "@charcuterie/tokens" import type { MouseEvent, ReactNode } from "react" import { CONTROL_SIZE_CLASS, PANEL_ITEM_SIZE_CLASS, } from "../controlStyles.ts" import { FOCUS_RING_CLASS } from "../intentStyles.ts" import { useRouterLink } from "../RouterLink/RouterLinkProvider.tsx" import { getIsRoutedHref } from "../RouterLink/routerLink.ts" import { toClassName } from "../toClassName.ts" import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.tsx" import type { NavItem } from "./navItems.ts" /** Where the row is drawn. Each paints differently; all are links. */ export type NavLinkPlacement = | "bar" | "bottom" | "panel" | "rail" | "railIcons" export type NavLinkProps = { isCurrent: boolean item: NavItem /** * Fires when the reader activates this link **in a way that keeps * them on the page** — a plain left click, Enter, Space. It is * what closes the panel behind them. * * A modified click (ctrl, meta, shift, alt) opens a new tab and * leaves this document exactly where it was, so it deliberately * does not fire: shutting the menu the reader is still working * through is the behaviour that makes people stop ctrl-clicking. */ onNavigate?: () => void placement: NavLinkPlacement size: ControlSize /** Hands the bar row's box to the measuring hook. Bar only. */ trackElement?: (element: HTMLElement | null) => void } const PLACEMENT_CLASS: Record = { bar: "inline-flex shrink-0 items-center rounded-md border border-transparent whitespace-nowrap", // A foot-of-the-screen target is a thumb target, so it takes the // 44px WCAG 2.5.5 floor rather than a control height. It is a // genuinely standalone tap target and shares a row with nothing, // which is the case // `2026-08-05-controls-share-one-height-no-per-component-touch-floor` // reserves `MIN_TOUCH_TARGET_CLASS` for. bottom: "flex min-h-(--control-min-touch-target) flex-col items-center justify-center gap-1 rounded-none px-1 py-2 text-xs", panel: "flex w-full items-center rounded-sm text-start", rail: "flex w-full items-center rounded-md", // Square, and sized on both axes from the height token — the same // rule `ICON_CONTROL_SIZE_CLASS` states, so a collapsed rail item // and an `IconButton` beside it measure the same. railIcons: "flex size-(--control-height-lg) items-center justify-center rounded-md", } /** * `-hover` in the panel, not the plain surface: the panel is drawn * on `surface-overlay`, which every dark scheme paints *lighter* * than the base tint, so the plain one reads as no change at all. * Same correction `MenuAction` carries. */ const CURRENT_CLASS: Record = { bar: "bg-intent-neutral-surface font-medium text-content-primary", bottom: "bg-intent-neutral-surface font-medium text-content-primary", panel: "bg-intent-neutral-surface-hover font-medium text-content-primary", rail: "bg-intent-neutral-surface font-medium text-content-primary", railIcons: "bg-intent-neutral-surface font-medium text-content-primary", } const RESTING_CLASS: Record = { bar: "text-content-secondary hover:bg-intent-neutral-surface hover:text-content-primary", bottom: "text-content-secondary hover:bg-intent-neutral-surface hover:text-content-primary", panel: "text-content-primary hover:bg-intent-neutral-surface-hover", rail: "text-content-secondary hover:bg-intent-neutral-surface hover:text-content-primary", railIcons: "text-content-secondary hover:bg-intent-neutral-surface hover:text-content-primary", } /** * One destination — and it is a real `` in every placement. * * That is the whole reason this exists rather than the `Menu` a * collapsed `Toolbar` opens: `MenuItem` is `{ label, onSelect }`, so * a nav folded into one becomes a row of `