AniBTUI
✦ 组件@anibt/page-headerregistry JSON

Page Header

一行的页头:倾斜的贴纸记号、20/800 的标题、圆体的数字、省略的说明,右边是这一页的工具组。

番剧

2,184 部

今日放送

2026 年 10 月 6 日 周二 · 9 部

安装

npx shadcn@latest add @anibt/page-header

第一次用之前:在 components.json 里登记 @anibt,再单独运行一次 npx shadcn@latest add @anibt/theme(见快速开始)。只有单独安装主题时,CLI 才会把 shadcn 的标准颜色换成白樱 / 夜樱;随组件带进来的主题只补上 AniBT 自己的变量和配方。

手动安装
  1. 1. 先装主题组件只读 @anibt/theme 的 token 和配方,没有主题就没有样子。
    npx shadcn@latest add @anibt/theme
  2. 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 部番剧

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,卡片头里需要同一枚记号时单独用。