"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. */ import * as React from "react" import { PageBreadcrumbBack, PageBreadcrumbTrail, type PageBreadcrumbBackProps, type PageBreadcrumbMenuOption, type PageBreadcrumbTrailItem, } from "@/components/page-breadcrumb-trail" import { useDocumentTitle } from "@/hooks/use-document-title" import { useScrollStuck } from "@/hooks/use-scroll-stuck" import { cn } from "@/lib/utils" export type BreadcrumbItem = PageBreadcrumbTrailItem export type BreadcrumbMenuOption = PageBreadcrumbMenuOption export type SiteHeaderBackLink = Pick 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 /** 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, documentTitle, trailing, transparent = false, }: SiteHeaderProps) { const isStuck = useScrollStuck() const resolvedDocumentTitle = documentTitle ?? (back ? undefined : title) useDocumentTitle(resolvedDocumentTitle) 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. The wrapper sticks to the top of the // SidebarInset and inherits the inset's rounded top corners via // `rounded-t-xl`. 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 (matching `SidebarInset`'s // resting elevation) — but stays inside the inset 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 rounded-t-xl 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 rounded-t-xl 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"> {back ? ( <PageBreadcrumbBack {...back} className="min-w-0 flex-1" /> ) : ( <PageBreadcrumbTrail variant="header" items={breadcrumbs} currentPage={title} currentPageMenu={currentPageMenu} currentPageMenuAriaLabel={currentPageMenuAriaLabel} className="min-w-0 flex-1" /> )} {trailing ? ( <div className="flex shrink-0 items-center gap-1.5 ps-1">{trailing}</div> ) : null} </div> </header> </div> ) }