import { Fragment, useCallback, useLayoutEffect, useRef, useState, type MouseEventHandler, type ReactNode } from 'react' import { cn } from './cn' import { Link } from './Link' import { WithTooltip } from './Tooltip' /** * A trail: "Documents / Quarterly plan / DOC-12". Ship's Breadcrumb * (2026-09-01), which knows nothing about what the places are. * * An item with an `href` is a link, even when it is last — a page may end its * trail on somewhere to go. The last item is where you are when it has none, * and may be `mono` (a ref, an id) or plain. The separator is a slash, muted, * never read aloud. * * A crumb that truncates shows its whole label in a tooltip on hover * (Katerina, 2026-09-01); one that fits shows nothing extra. Truncation is * re-measured when the trail resizes, so the tooltip appears and disappears * with the room the trail actually has. * * A crumb with an `href` is a plain anchor, so in a router app a click on it * reloads the page. Such an app passes `onClick` per crumb and navigates in * place there — NavItem's rule — and the `href` stays a real address so the * link can still be copied or opened in a new tab. Ship's breadcrumbs were * the last links reloading the whole app (ADOPTION S15, 2026-09-08). */ export interface Crumb { label: string href?: string /** A router app intercepts the click here. Called only on a crumb with an `href`. */ onClick?: MouseEventHandler /** Set the item in the mono face — a ref, an id. */ mono?: boolean /** * 16px, stroke 1.5, before the label — what kind of place this is, so a * container's name is not mistaken for an item's (Katerina, 2026-09-09). * Decorative: the label says where you are; the icon says what it is. */ icon?: ReactNode /** Quieter — a label that is not a place. */ muted?: boolean } export interface BreadcrumbProps { items: Crumb[] className?: string } export function Breadcrumb({ items, className }: BreadcrumbProps) { const navRef = useRef(null) const labelRefs = useRef<(HTMLElement | null)[]>([]) const [truncated, setTruncated] = useState>(new Set()) const measure = useCallback(() => { const next = new Set() labelRefs.current.forEach((el, index) => { if (el && el.scrollWidth > el.clientWidth) next.add(index) }) setTruncated((prev) => (prev.size === next.size && [...next].every((i) => prev.has(i)) ? prev : next)) }, []) useLayoutEffect(() => { measure() const nav = navRef.current // jsdom has neither layout nor ResizeObserver; without this guard a // consumer app cannot render a page with a trail in its tests. if (!nav || typeof ResizeObserver === 'undefined') return const observer = new ResizeObserver(measure) observer.observe(nav) return () => observer.disconnect() }, [measure, items]) return ( ) }