SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
2.5 KB · 57 lines tsx
Raw Blame History
1import Link from 'next/link';2import type { ReactNode } from 'react';3import { cn } from '@/lib/format';4import { StatStrip } from './primitives';56export interface Crumb {7  label: string;8  href?: string;9}1011export function Breadcrumbs({ items, className }: { items: Crumb[]; className?: string }) {12  if (!items.length) return null;13  return (14    <nav aria-label="Breadcrumb" className={cn('mb-2 overflow-x-auto scrollbar-none text-[11px] text-subtle', className)}>15      <ol className="flex min-w-max items-center gap-1">16        {items.map((c, i) => (17          <li key={`${c.label}-${i}`} className="flex items-center gap-1">18            {i > 0 ? (19              <span aria-hidden className="text-border-strong">20                /21              </span>22            ) : null}23            {c.href && i < items.length - 1 ? (24              <Link href={c.href} className="hover:text-fg">25                {c.label}26              </Link>27            ) : (28              <span className={i === items.length - 1 ? 'text-muted' : undefined}>{c.label}</span>29            )}30          </li>31        ))}32      </ol>33    </nav>34  );35}3637/**38 * Consistent page header: breadcrumb → kicker → title → one-line context → meta row → optional KPI strip.39 */40export function PageHeader({ title, description, crumbs, actions, meta, className, compact, kicker, stats }: { title: ReactNode; description?: ReactNode; crumbs?: Crumb[]; actions?: ReactNode; meta?: ReactNode; className?: string; compact?: boolean; kicker?: ReactNode; stats?: Array<{ label: ReactNode; value: ReactNode; sub?: ReactNode; href?: string }> }) {41  return (42    <div className={cn(compact ? 'mb-4' : 'mb-6', className)}>43      {crumbs ? <Breadcrumbs items={crumbs} /> : null}44      <div className="flex flex-wrap items-end justify-between gap-x-6 gap-y-3">45        <div className="min-w-0 flex-1">46          {kicker ? <p className="t-label mb-1.5">{kicker}</p> : null}47          <h1 className={cn('text-fg', compact ? 't-title sm:text-xl' : 't-headline')}>{title}</h1>48          {description ? <p className={cn('mt-1.5 max-w-3xl text-muted', compact ? 'text-[13px] leading-relaxed' : 'text-sm leading-relaxed')}>{description}</p> : null}49          {meta ? <div className="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted">{meta}</div> : null}50        </div>51        {actions ? <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div> : null}52      </div>53      {stats?.length ? <StatStrip items={stats} className="mt-4" /> : null}54    </div>55  );56}57