"use client" /** * SiteHeader — breadcrumb / top bar — WCAG 2.1 AA * * ✓
landmark for AT navigation (WCAG 1.3.1) * ✓ `useDocumentTitle` — browser tab title matches breadcrumb (WCAG 2.4.2) * ✓ Sticky at top — when stuck, the rounded breadcrumb sits on the app bg and a * bottom separator appears to anchor it; transparent at rest so the rounded * corners blend into the inset card. * ✓ Uses Inter (font-sans) — Ivy Presto is reserved for PageHeader

only * * Primary sidebar expand/collapse lives on `AppSidebar` (not here) so the * control stays with the rail it affects. ⌘B still toggles via SidebarProvider. * * Compact shell: this row is not rendered. The trail and any `trailing` chrome * portal onto the utility bar instead, which is the single chrome row in that * layout (see `components/compact-header-slot.tsx`). Routes are unaffected — * they keep passing `siteHeader` the same way. */ import * as React from "react" import { createPortal } from "react-dom" import { useLocation } from "react-router" import { PageBreadcrumbBack, PageBreadcrumbTrail, type PageBreadcrumbBackProps, type PageBreadcrumbMenuOption, type PageBreadcrumbTrailItem, } from "@/components/page-breadcrumb-trail" import { useCompactHeaderSlot } from "@/components/compact-header-slot" import { deriveBackFromBreadcrumbs, useSetUtilityBarPageChrome, } from "@/components/utility-bar-page-chrome" import { useDocumentTitle } from "@/hooks/use-document-title" import { useScrollStuck } from "@/hooks/use-scroll-stuck" import { useShellLayout } from "@/contexts/shell-layout-context" import { isCompactShell } from "@/lib/shell-layout" import { isRecordDetailChromePath } from "@/lib/record-detail-chrome" import { cn } from "@/lib/utils" export type BreadcrumbItem = PageBreadcrumbTrailItem export type BreadcrumbMenuOption = PageBreadcrumbMenuOption export type SiteHeaderBackLink = Pick /** Utility bar chrome on compact shell for detail / focus routes. Default `back`. */ export type SiteHeaderUtilityBarMode = "back" | "breadcrumb" export interface SiteHeaderProps { /** Current page title (last breadcrumb segment in trail mode). */ title?: string /** Full breadcrumb trail — each item can be a link or plain text. Title is appended automatically as the last segment. */ breadcrumbs?: BreadcrumbItem[] /** Switch among peer records on the current breadcrumb segment (detail routes). */ currentPageMenu?: BreadcrumbMenuOption[] currentPageMenuAriaLabel?: string /** * Back-icon variant — parent link only (no `title` segment in the header). * Prefer when the page `

` carries the current title (e.g. New question composer). */ back?: SiteHeaderBackLink /** * Compact utility bar chrome when `back` is set or record detail derives a * parent. `breadcrumb` shows the ancestor trail instead of the back icon. * Record detail routes with `breadcrumbs` default to `breadcrumb`. */ utilityBarMode?: SiteHeaderUtilityBarMode /** Override for `` when `back` is set and the visible H1 lives in page body. */ documentTitle?: string /** Right-aligned chrome (e.g. design-system preview mode menu). */ trailing?: React.ReactNode /** * Drop the header's own surface so whatever the page puts behind it shows * through — for pages that open on a full-bleed coloured band (the product * marketing pages), where an opaque white strip cuts the band off at the top. * * Only affects the resting state. Once the page scrolls, the stuck chrome * comes back regardless, because content passing under a transparent bar is * unreadable. */ transparent?: boolean } export function SiteHeader({ title = "Dashboard", breadcrumbs, currentPageMenu, currentPageMenuAriaLabel, back, utilityBarMode, documentTitle, trailing, transparent = false, }: SiteHeaderProps) { const isStuck = useScrollStuck() const compactHeaderSlot = useCompactHeaderSlot() const { variant: shellVariant } = useShellLayout() const compactShell = isCompactShell(shellVariant) const { pathname } = useLocation() const setUtilityBarChrome = useSetUtilityBarPageChrome() const derivedRecordBack = !back && isRecordDetailChromePath(pathname) ? deriveBackFromBreadcrumbs(breadcrumbs) : null const utilityBackHref = back?.href ?? derivedRecordBack?.href const utilityBackLabel = back?.label ?? derivedRecordBack?.label const utilityBack = utilityBackHref && utilityBackLabel ? { href: utilityBackHref, label: utilityBackLabel } : null const recordDetailTrail = !back && isRecordDetailChromePath(pathname) && Boolean(breadcrumbs?.length) const resolvedUtilityBarMode: SiteHeaderUtilityBarMode | null = back ? "back" : utilityBarMode ?? (recordDetailTrail ? "breadcrumb" : utilityBack ? "back" : null) const barCurrentPage = resolvedUtilityBarMode === "breadcrumb" && recordDetailTrail ? undefined : title const resolvedDocumentTitle = documentTitle ?? (back ? undefined : title) useDocumentTitle(resolvedDocumentTitle) const scrollTitle = documentTitle ?? title React.useLayoutEffect(() => { if (resolvedUtilityBarMode === "breadcrumb") { setUtilityBarChrome({ mode: "breadcrumb", items: breadcrumbs, currentPage: barCurrentPage, currentPageMenu: currentPageMenu, currentPageMenuAriaLabel: currentPageMenuAriaLabel, }) } else if (utilityBackHref && utilityBackLabel) { setUtilityBarChrome({ mode: "back", href: utilityBackHref, label: utilityBackLabel, scrollTitle: scrollTitle || undefined, scrollTitleMenu: currentPageMenu, scrollTitleMenuAriaLabel: currentPageMenuAriaLabel, }) } else { setUtilityBarChrome({ mode: "default" }) } return () => setUtilityBarChrome({ mode: "default" }) }, [ setUtilityBarChrome, resolvedUtilityBarMode, breadcrumbs, barCurrentPage, utilityBackHref, utilityBackLabel, scrollTitle, currentPageMenu, currentPageMenuAriaLabel, ]) const breadcrumb = back ? ( <PageBreadcrumbBack {...back} scrollTitle={scrollTitle || undefined} scrollTitleMenu={currentPageMenu} scrollTitleMenuAriaLabel={currentPageMenuAriaLabel} className="min-w-0 flex-1" /> ) : ( <PageBreadcrumbTrail variant="header" items={breadcrumbs} currentPage={title} currentPageMenu={currentPageMenu} currentPageMenuAriaLabel={currentPageMenuAriaLabel} className="min-w-0 flex-1" /> ) // Compact shell: the bar is the header. Back / breadcrumb mode owns the // leading cluster on the utility bar; trail/trailing stay off the bar. // Always skip this row on compact — even before the portal slot mounts — so // we never flash a second sticky header under the utility bar. if (compactShell) { if (resolvedUtilityBarMode || !compactHeaderSlot) return null return createPortal( <> {breadcrumb} {trailing ? ( <div className="flex shrink-0 items-center gap-1.5">{trailing}</div> ) : null} </>, compactHeaderSlot, ) } return ( <div data-site-header="" className={cn( // Sticky page chrome sits BELOW every Radix overlay (DropdownMenu / // Popover / Select / Dialog / Sheet / Tooltip all render at // z-50). Previously `z-60` here caused the school/product switcher // dropdown to open behind the breadcrumb. `z-30` keeps the header // above page content (charts, tables, scrolled rows) but below // floating overlays. // // Stuck-state chrome: subtle scrolled cue without becoming a // separate floating pill. Flush canvas has square corners; the // wrapper sticks to the top of `SidebarInset` with no radius. // When stuck we layer a faint sidebar-tinted surface, a hairline // bottom border, and a soft `shadow-sm` so the bar reads as // elevated chrome — but stays inside the page rectangle so it // doesn't compete with the table-column sticky header below. // `transition-[background-color,box-shadow]` fades the elevation // in/out alongside the bg flip. "sticky top-0 z-30 transition-[background-color,box-shadow]", isStuck ? "bg-sidebar border-b border-border shadow-sm" : "bg-transparent", )} > <header className={cn( "flex h-(--header-height) shrink-0 items-center gap-2 transition-[width,height] ease-out group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)", // Transparent only while resting. The wrapper above re-opaques on // scroll, and an opaque strip over an opaque strip is just the old bar. transparent && !isStuck ? "bg-transparent" : "bg-background", )} > <div className="flex w-full min-w-0 items-center gap-1 ps-4 pe-1.5 lg:gap-2 lg:ps-5 lg:pe-2"> {breadcrumb} {trailing ? ( <div className="flex shrink-0 items-center gap-1.5 ps-1">{trailing}</div> ) : null} </div> </header> </div> ) }