Page Header
一行的页头:倾斜的贴纸记号、20/800 的标题、圆体的数字、省略的说明,右边是这一页的工具组。
番剧
2,184 部今日放送
2026 年 10 月 6 日 周二 · 9 部page-header-demo.tsx
import { CalendarDaysIcon, LayoutGridIcon, ListIcon, RssIcon, SparklesIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { PageHeader } from "@/components/ui/page-header"
export default function PageHeaderDemo() {
return (
<div className="flex w-full flex-col gap-8">
<PageHeader
icon={SparklesIcon}
title="番剧"
meta="2,184 部"
description="按季度浏览全部番剧,或切到时间表看今天放送什么。"
tabs={
<div role="group" aria-label="视图" className="anibt-seg">
<button type="button" aria-pressed="true">
<ListIcon className="size-3.5" aria-hidden="true" />
时间表
</button>
<button type="button" aria-pressed="false">
<LayoutGridIcon className="size-3.5" aria-hidden="true" />
海报墙
</button>
</div>
}
actions={
<Button variant="secondary">
<RssIcon />
订阅 RSS
</Button>
}
headingLevel="h2"
/>
<PageHeader
icon={CalendarDaysIcon}
iconTone="time"
title="今日放送"
meta="2026 年 10 月 6 日 周二 · 9 部"
headingLevel="h2"
/>
</div>
)
}安装
npx shadcn@latest add @anibt/page-header第一次用之前:在 components.json 里登记 @anibt,再单独运行一次 npx shadcn@latest add @anibt/theme(见快速开始)。只有单独安装主题时,CLI 才会把 shadcn 的标准颜色换成白樱 / 夜樱;随组件带进来的主题只补上 AniBT 自己的变量和配方。
手动安装
- 1. 先装主题组件只读
@anibt/theme的 token 和配方,没有主题就没有样子。npx shadcn@latest add @anibt/theme - 2. 把下面的文件放进项目components/ui/page-header.tsx
import * as React from "react" import { cn } from "@/lib/utils" /** * PageHeader — the AniBT 「贴纸手账」 section head: one compact row. * * An optional section mark (a small white sticker tilted −4°, the glyph in * pink; it tips the other way under the pointer) → the title at 20/800 → a * figure in the round face (`2,184 部`) → a 12px muted description that * ellipsises beside the title and hides below `lg` → then, pushed right, the * tool group: the page's own tabs, its controls and its one candy action. * * Nothing in the title cluster wraps. Below `md` the tool group takes its own * full-width line and scrolls sideways (with a fade at the right edge) rather * than folding — so a header never grows a tall empty band. * * `compact` is the denser workbench head (18px title, 24px mark). * `headingLevel` keeps the outline honest when a shell already prints an h1. */ type PageHeaderProps = Omit<React.ComponentProps<"header">, "title"> & { /** The section mark: a lucide icon component, or any node (an emoji, an svg). */ icon?: React.ComponentType<{ className?: string }> | React.ReactNode /** `time` paints the mark blue — for a schedule, a timetable, "now". */ iconTone?: "mark" | "time" title: React.ReactNode /** The figure beside the title: `2,184 部`, `12 条`. */ meta?: React.ReactNode description?: React.ReactNode /** The page's own tabs, at the left of the tool group. */ tabs?: React.ReactNode /** Search, filters, sort. */ toolbar?: React.ReactNode /** The page's buttons, at the right end. */ actions?: React.ReactNode compact?: boolean headingLevel?: "h1" | "h2" | "h3" } function PageHeader({ icon, iconTone = "mark", title, meta, description, tabs, toolbar, actions, compact = false, headingLevel: Heading = "h1", className, children, ...props }: PageHeaderProps) { const hasTools = Boolean(tabs || toolbar || actions) return ( <header data-slot="page-header" data-compact={compact ? "true" : undefined} className={cn("flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2", compact ? "pb-2" : "pb-3", className)} {...props} > <div data-slot="page-header-title" className="group/page-header flex min-w-0 flex-1 items-center gap-3"> {icon ? <PageHeaderMark icon={icon} tone={iconTone} size={compact ? "sm" : "default"} /> : null} <Heading className={cn( "shrink-0 font-extrabold tracking-[0.02em] whitespace-nowrap text-foreground", compact ? "text-[18px] leading-7" : "text-[20px] leading-7" )} > {title} </Heading> {meta ? <span className="anibt-num shrink-0 text-[12px] whitespace-nowrap text-muted-foreground">{meta}</span> : null} {description ? ( <p className="anibt-ell hidden text-[12px] text-muted-foreground lg:block">{description}</p> ) : null} </div> {hasTools ? ( <div data-slot="page-header-tools" className={cn( "-my-1 flex w-full min-w-0 items-center gap-2 overflow-x-auto py-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", toolbar && "[mask-image:linear-gradient(to_right,black_0%,black_92%,transparent_100%)]", "md:ml-auto md:w-auto md:shrink-0 md:overflow-x-visible md:[mask-image:none]" )} > {tabs} {toolbar ? <div className="flex shrink-0 items-center gap-2">{toolbar}</div> : null} {actions ? <div className="flex shrink-0 items-center gap-2">{actions}</div> : null} </div> ) : null} {children ? <div className="w-full min-w-0">{children}</div> : null} </header> ) } /** * The section mark on its own: a 28px white sticker (24 in `sm`), tilted, the * glyph in pink (or blue with `tone="time"`). It tips the other way when the * pointer is on its header. Decorative. */ function PageHeaderMark({ icon, tone = "mark", size = "default", className, }: { icon: React.ComponentType<{ className?: string }> | React.ReactNode tone?: "mark" | "time" size?: "sm" | "default" className?: string }) { const content = isComponent(icon) ? React.createElement(icon, { className: "size-4" }) : icon return ( <span data-slot="page-header-mark" aria-hidden="true" className={cn( "anibt-rim flex shrink-0 -rotate-[4deg] items-center justify-center rounded-[6px] bg-card transition-transform duration-[400ms] ease-anibt-spring group-hover/page-header:scale-105 group-hover/page-header:rotate-[4deg] [&_svg]:size-4", tone === "time" ? "text-anibt-time" : "text-anibt-mark", size === "sm" ? "size-6" : "size-7", className )} > {content} </span> ) } function isComponent(value: unknown): value is React.ComponentType<{ className?: string }> { return ( typeof value === "function" || (typeof value === "object" && value !== null && "$$typeof" in value && !React.isValidElement(value)) ) } export { PageHeader, PageHeaderMark } export type { PageHeaderProps }
用法
用法
import { SparklesIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { PageHeader } from "@/components/ui/page-header"
<PageHeader
icon={SparklesIcon}
title="番剧"
meta="2,184 部"
description="按季度浏览全部番剧。"
actions={<Button variant="secondary">订阅 RSS</Button>}
/>从左到右:可选的小节记号(一枚倾斜 −4° 的白色贴纸,图标是粉色,指针放上去会翻向另一边)→ 20/800 的标题 → 圆体的数字 → 12 号弱色说明(和标题同一行,放不下就省略,lg 以下隐藏)→ 推到右边的工具组:这一页自己的页签 tabs、控件 toolbar、按钮 actions。
标题那一簇从不换行。md 以下,工具组单独占一行并横向滚动(右缘渐隐),而不是折成好几行——页头下面不会出现一大块空白。
iconTone="time" 把记号染成蓝色,给时间表、今日放送这类「时间」的页。
紧凑
资源
306 条 · 21 部番剧page-header-compact.tsx
import { PlusIcon, SearchIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { PageHeader } from "@/components/ui/page-header"
export default function PageHeaderCompact() {
return (
<PageHeader
compact
headingLevel="h2"
title="资源"
meta="306 条 · 21 部番剧"
className="w-full"
toolbar={
<>
<label className="anibt-field w-56">
<SearchIcon className="size-4 shrink-0" aria-hidden="true" />
<input
placeholder="搜索标题或集数"
aria-label="搜索资源"
className="min-w-0 flex-1 bg-transparent text-foreground outline-none placeholder:text-muted-foreground"
/>
</label>
<div role="group" aria-label="状态" className="anibt-seg">
<button type="button" aria-pressed="true">
全部
</button>
<button type="button" aria-pressed="false">
同步失败
</button>
</div>
</>
}
actions={
<Button>
<PlusIcon />
发布资源
</Button>
}
/>
)
}compact 是工作台里更密的页头:18 号标题、24px 记号、更小的下边距。外壳已经有 h1 时,用 headingLevel="h2" 保持标题层级正确。
属性
PageHeader
titleReactNode默认 —必填:标题
iconComponentType | ReactNode默认 —小节记号:lucide 图标组件或任意节点
iconTone"mark" | "time"默认 "mark"粉色 / 蓝色记号
metaReactNode默认 —标题旁的数字:2,184 部
descriptionReactNode默认 —一行说明,放不下就省略
tabsReactNode默认 —这一页的页签,在工具组最左
toolbarReactNode默认 —搜索、筛选、排序
actionsReactNode默认 —按钮,在最右;一页最多一个糖果主按钮
compactboolean默认 false工作台的紧凑页头
headingLevel"h1" | "h2" | "h3"默认 "h1"标题元素
childrenReactNode默认 —页头下面另起一行的内容
另外导出 PageHeaderMark,卡片头里需要同一枚记号时单独用。
