92 lines
2.9 KiB
TypeScript
92 lines
2.9 KiB
TypeScript
|
|
/* PageHeader — bordered page-title block.
|
||
|
|
*
|
||
|
|
* Sits below <AppNavigation /> at the top of any page. Owns the page
|
||
|
|
* title, description, breadcrumbs, and a right-side actions slot.
|
||
|
|
*
|
||
|
|
* Two brand props:
|
||
|
|
* • accent — replaces the bottom border with a 2px Wulf-blue rule,
|
||
|
|
* echoing the standards-guide blue header band.
|
||
|
|
* • watermark — renders the W mark behind the title at very low
|
||
|
|
* opacity, giving the page a quiet brand signature.
|
||
|
|
*
|
||
|
|
* Re-exported from components/navigation/app-navigation.tsx for
|
||
|
|
* back-compat; new code can import from here directly. */
|
||
|
|
|
||
|
|
import Link from 'next/link';
|
||
|
|
import { cn } from '@/lib/utils';
|
||
|
|
import { WulfMark } from '@/components/branding/wulf-mark';
|
||
|
|
|
||
|
|
export interface BreadcrumbItem {
|
||
|
|
label: string;
|
||
|
|
href?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface PageHeaderProps {
|
||
|
|
title: string;
|
||
|
|
description?: string;
|
||
|
|
breadcrumbs?: BreadcrumbItem[];
|
||
|
|
actions?: React.ReactNode;
|
||
|
|
/** Replace the standard border-b with a 2px Wulf-blue rule. */
|
||
|
|
accent?: boolean;
|
||
|
|
/** Render the W mark watermark behind the title at ~4% opacity. */
|
||
|
|
watermark?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function PageHeader({
|
||
|
|
title,
|
||
|
|
description,
|
||
|
|
breadcrumbs,
|
||
|
|
actions,
|
||
|
|
accent = false,
|
||
|
|
watermark = false,
|
||
|
|
}: PageHeaderProps) {
|
||
|
|
return (
|
||
|
|
<div className={cn(accent ? 'rule-brand' : 'border-b')}>
|
||
|
|
<div className={cn('container mx-auto px-6 py-4', watermark && 'has-mark-watermark')}>
|
||
|
|
{watermark && (
|
||
|
|
<span className="mark-watermark" aria-hidden="true">
|
||
|
|
<WulfMark variant="mark" alt="" />
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{breadcrumbs && breadcrumbs.length > 0 && (
|
||
|
|
<nav
|
||
|
|
aria-label="Breadcrumb"
|
||
|
|
className="flex items-center space-x-2 text-sm text-muted-foreground mb-2"
|
||
|
|
>
|
||
|
|
{breadcrumbs.map((crumb, index) => (
|
||
|
|
<div key={`${crumb.label}-${index}`} className="flex items-center">
|
||
|
|
{index > 0 && <span className="mx-2" aria-hidden="true">/</span>}
|
||
|
|
{crumb.href ? (
|
||
|
|
<Link
|
||
|
|
href={crumb.href}
|
||
|
|
className="hover:text-foreground transition-colors"
|
||
|
|
>
|
||
|
|
{crumb.label}
|
||
|
|
</Link>
|
||
|
|
) : (
|
||
|
|
<span className="text-foreground" aria-current="page">
|
||
|
|
{crumb.label}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</nav>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="flex items-center justify-between gap-4">
|
||
|
|
<div className="min-w-0">
|
||
|
|
<h1 className="text-2xl font-bold tracking-tight">{title}</h1>
|
||
|
|
{description && (
|
||
|
|
<p className="text-muted-foreground mt-1">{description}</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
{actions && (
|
||
|
|
<div className="flex items-center gap-2 shrink-0">{actions}</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|