72 lines
2.1 KiB
JavaScript
72 lines
2.1 KiB
JavaScript
// src/v2/components/ui/PageHeader.jsx
|
|
// Standard page title block. Every page starts with this component.
|
|
//
|
|
// Props:
|
|
// title — string (required) — main page heading
|
|
// subtitle — string — muted description below the title
|
|
// breadcrumbs — Array<{ label: string, href?: string }> — shown above the title
|
|
// children — ReactNode — action buttons rendered top-right
|
|
// className — extra classes on the wrapper
|
|
//
|
|
// Layout:
|
|
// [breadcrumbs?]
|
|
// [title] [children / actions →]
|
|
// [subtitle?]
|
|
//
|
|
// Styles: src/v2/styles/components.css (.page-header*, .breadcrumbs*)
|
|
|
|
export default function PageHeader({
|
|
title,
|
|
subtitle,
|
|
breadcrumbs,
|
|
children,
|
|
className = '',
|
|
}) {
|
|
return (
|
|
<header className={`page-header ${className}`}>
|
|
{/* Breadcrumb trail — only on detail pages */}
|
|
{breadcrumbs?.length > 0 && (
|
|
<nav aria-label="Breadcrumb" className="breadcrumbs">
|
|
{breadcrumbs.map((crumb, i) => {
|
|
const isLast = i === breadcrumbs.length - 1
|
|
return (
|
|
<span key={i} className="contents">
|
|
{i > 0 && (
|
|
<span className="breadcrumbs-sep" aria-hidden="true">
|
|
/
|
|
</span>
|
|
)}
|
|
{isLast || !crumb.href ? (
|
|
<span
|
|
className="breadcrumbs-current"
|
|
aria-current={isLast ? 'page' : undefined}
|
|
>
|
|
{crumb.label}
|
|
</span>
|
|
) : (
|
|
<a href={crumb.href}>{crumb.label}</a>
|
|
)}
|
|
</span>
|
|
)
|
|
})}
|
|
</nav>
|
|
)}
|
|
|
|
{/* Title row */}
|
|
<div className="page-header-row">
|
|
<div className="min-w-0 flex-1">
|
|
<h1 className="page-header-title">{title}</h1>
|
|
{subtitle && (
|
|
<p className="page-header-subtitle">{subtitle}</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Action slot — top-right buttons */}
|
|
{children && (
|
|
<div className="page-header-actions">{children}</div>
|
|
)}
|
|
</div>
|
|
</header>
|
|
)
|
|
}
|