WORKFLOW-APP/components/PageHeader.tsx
SJCoetz bb97fef453 feat: initialize full ysterbaard frontend application
Add complete production-ready Ysterbaard PocketBase registry browser frontend:
- Next.js 14 App Router with TypeScript, Tailwind CSS, and TanStack React Query
- Secure server-side PocketBase API proxy to avoid exposing superuser tokens
- Read-only directors and companies registry browse pages with search, filtering, and pagination
- Client onboarding workflow for creating and managing client submissions
- Docker container configuration for easy deployment
- All supporting UI components, utility functions, and configuration files
2026-07-23 19:10:47 +02:00

70 lines
2.4 KiB
TypeScript

// components/PageHeader.tsx
// Page-level heading strip. Used on top of every screen — title +
// short subtitle, with an optional right-aligned action.
import { ReactNode } from "react";
interface PageHeaderProps {
eyebrow?: string;
title: ReactNode;
subtitle?: ReactNode;
action?: ReactNode;
}
export default function PageHeader({ eyebrow, title, subtitle, action }: PageHeaderProps) {
// Subtitle is often a fragment of multiple <span> children. Flatten
// to a single array (Children.toArray on its own does NOT flatten
// nested fragments — that bites the common
// `<><span>...</span><span>...</span></>` pattern). Drop empty
// entries, and lay them out as a flex row with separator dots so
// they don't render flush against each other.
const flatten = (node: ReactNode): ReactNode[] => {
if (node === null || node === undefined || node === false) return [];
if (Array.isArray(node)) {
return node.flatMap((n) => flatten(n as ReactNode));
}
return [node];
};
const items = flatten(subtitle);
let subtitleNode: ReactNode = null;
if (items.length === 0) {
subtitleNode = null;
} else if (items.length === 1) {
subtitleNode = <span>{items[0]}</span>;
} else {
subtitleNode = (
<span className="flex flex-wrap items-center gap-x-2 gap-y-1 text-muted">
{items.map((child, i) => (
<span key={i} className="inline-flex items-center gap-x-2">
{i > 0 ? <span aria-hidden="true" className="text-meta">·</span> : null}
<span>{child}</span>
</span>
))}
</span>
);
}
return (
<section className="border-b border-border bg-surface">
<div className="container-x py-10 md:py-14 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div className="max-w-2xl">
{eyebrow ? (
<p className="text-xs uppercase tracking-[0.08em] text-muted font-semibold mb-3">
{eyebrow}
</p>
) : null}
<h1 className="text-2xl md:text-3xl font-bold text-fg leading-tight tracking-tight">
{title}
</h1>
{subtitleNode ? (
<div className="text-base text-muted mt-3 leading-relaxed">
{subtitleNode}
</div>
) : null}
</div>
{action ? <div className="flex-shrink-0">{action}</div> : null}
</div>
</section>
);
}