blob: 81cd691401b5fdf7f225ccf60024ac0795f54564 [file]
// packages/ui/src/primitives/section-header.tsx
//
// Convergence round 5: the ONE "section title (+ count / subtitle) +
// trailing action" header. Before this, three dialects were hand-rolled:
// skills' span-row (label + N 个), daily-review's h4 with a colored
// accent bar, and permission's h4 + small + trailing button.
//
// Styled with Tailwind utilities (portable, like DialogHeader/StatTile);
// call sites keep their wrapper classes for page-pinned CSS placement.
import type { ReactNode } from 'react';
import { cn } from '../utils.js';
export interface SectionHeaderProps {
title: ReactNode;
/** `id` on the title element, for a section's `aria-labelledby` link. */
titleId?: string;
/** Muted trailing count (e.g. "3 个" / "2 份"). */
count?: ReactNode;
/** One quiet line under the title (permission's layer explainer). */
subtitle?: ReactNode;
/** Right-aligned action cluster (button / filter pills). */
action?: ReactNode;
/** Leading accent bar (daily-review's section marker). */
accent?: boolean;
/** Heading level for the title, or a non-heading `span`. `h3` is for a
* section that sits directly under a page `h2` (关于 privacy, 数据 config). */
as?: 'h3' | 'h4' | 'span';
className?: string;
}
export function SectionHeader({
title,
titleId,
count,
subtitle,
action,
accent = false,
as: Tag = 'span',
className,
}: SectionHeaderProps) {
return (
<div
className={cn('flex min-w-0 items-center justify-between gap-3', className)}
data-slot="section-header"
>
<div className="min-w-0">
<Tag
id={titleId}
className={cn(
'm-0 inline-flex items-center gap-2 text-[length:var(--font-size-ui)] font-semibold text-foreground',
accent &&
'before:inline-block before:h-3 before:w-[3px] before:rounded-[var(--radius-pill)] before:bg-[oklch(from_var(--status-running)_l_c_h_/_0.85)] before:content-[""]',
)}
data-slot="section-header-title"
>
{title}
{count != null && (
<small
className="text-[length:var(--font-size-caption)] font-normal text-[color:var(--muted-foreground)] [font-variant-numeric:tabular-nums]"
data-slot="section-header-count"
>
{count}
</small>
)}
</Tag>
{subtitle != null && (
<small
className="mt-0.5 block text-[length:var(--font-size-caption)] font-normal text-[color:var(--foreground-secondary)]"
data-slot="section-header-subtitle"
>
{subtitle}
</small>
)}
</div>
{action != null && (
<div className="flex shrink-0 items-center gap-2" data-slot="section-header-action">
{action}
</div>
)}
</div>
);
}