AniBTUI
✦ 组件@anibt/accordionregistry JSON

Accordion

折叠的段落:缝线分开的行,或一张张分组卡;指针下标题变粉,展开的那节箭头翻转成粉色。

在番剧页点「订阅 RSS」,挑好字幕组、语言和画质,把生成的地址填进 qBittorrent 的 RSS 下载器即可。

新的一集发布后,下载器会自己开始下载。

安装

npx shadcn@latest add @anibt/accordion

第一次用之前:在 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. 安装依赖
    npm install lucide-react radix-ui
  3. 3. 把下面的文件放进项目
    components/ui/accordion.tsx
    "use client"
    
    import * as React from "react"
    import { ChevronDownIcon } from "lucide-react"
    import { Accordion as AccordionPrimitive } from "radix-ui"
    
    import { cn } from "@/lib/utils"
    
    /**
     * Accordion — AniBT 「贴纸手账」 folding sections.
     *
     * `variant="default"`: rows parted by the dashed stitch, straight on the
     * surface they sit on. `variant="cards"`: every section is its own well
     * (1px edge, radius 10) with 8px between them; the open one takes the pink
     * edge. Triggers are 14/700 and turn pink under the pointer; the chevron
     * turns pink and flips when its section is open.
     *
     * Radix underneath (keyboard, `aria-expanded`, the height animation from
     * `tw-animate-css`). It is one section at a time and collapsible unless you
     * say otherwise: `type="multiple"` (or Base UI's `multiple`) keeps several
     * open, `collapsible={false}` keeps one always open.
     */
    type AccordionVariant = "default" | "cards"
    
    type AccordionProps = (
      | Omit<AccordionPrimitive.AccordionSingleProps, "type">
      | Omit<AccordionPrimitive.AccordionMultipleProps, "type">
    ) & {
      type?: "single" | "multiple"
      /** Base UI's spelling of `type="multiple"`. */
      multiple?: boolean
      variant?: AccordionVariant
    }
    
    function Accordion({ className, type, multiple, variant = "default", ...props }: AccordionProps) {
      const shared = {
        "data-slot": "accordion",
        "data-variant": variant,
        className: cn("group/accordion flex w-full flex-col", variant === "cards" && "gap-2", className),
      }
      if (type === "multiple" || multiple) {
        return <AccordionPrimitive.Root type="multiple" {...shared} {...(props as Omit<AccordionPrimitive.AccordionMultipleProps, "type">)} />
      }
      const single = props as Omit<AccordionPrimitive.AccordionSingleProps, "type">
      return <AccordionPrimitive.Root type="single" {...shared} {...single} collapsible={single.collapsible ?? true} />
    }
    
    function AccordionItem({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
      return (
        <AccordionPrimitive.Item
          data-slot="accordion-item"
          className={cn(
            "group-data-[variant=default]/accordion:anibt-stitch-under group-data-[variant=default]/accordion:last:bg-none",
            "group-data-[variant=cards]/accordion:anibt-well group-data-[variant=cards]/accordion:px-4 group-data-[variant=cards]/accordion:transition-shadow group-data-[variant=cards]/accordion:data-[state=open]:shadow-[0_0_0_1px_var(--anibt-wash-line)]",
            className
          )}
          {...props}
        />
      )
    }
    
    function AccordionTrigger({
      className,
      children,
      ...props
    }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
      return (
        <AccordionPrimitive.Header className="flex">
          <AccordionPrimitive.Trigger
            data-slot="accordion-trigger"
            className={cn(
              "group/accordion-trigger flex min-h-11 flex-1 items-center gap-2.5 rounded-[10px] py-2.5 text-left text-[14px] leading-[1.45] font-bold text-foreground outline-hidden transition-colors duration-150 hover:text-accent-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid disabled:pointer-events-none disabled:text-muted-foreground [&>svg:not([data-slot=accordion-chevron])]:size-4 [&>svg:not([data-slot=accordion-chevron])]:shrink-0 [&>svg:not([data-slot=accordion-chevron])]:text-muted-foreground",
              className
            )}
            {...props}
          >
            {children}
            <ChevronDownIcon
              data-slot="accordion-chevron"
              aria-hidden="true"
              className="pointer-events-none ml-auto size-4 shrink-0 text-muted-foreground transition-transform duration-300 ease-anibt-spring group-hover/accordion-trigger:text-accent-foreground group-data-[state=open]/accordion-trigger:rotate-180 group-data-[state=open]/accordion-trigger:text-anibt-mark"
            />
          </AccordionPrimitive.Trigger>
        </AccordionPrimitive.Header>
      )
    }
    
    function AccordionContent({
      className,
      children,
      ...props
    }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
      return (
        <AccordionPrimitive.Content
          data-slot="accordion-content"
          className="overflow-hidden text-[13px] text-anibt-ink2 data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down motion-reduce:animate-none"
          {...props}
        >
          <div
            className={cn(
              "pb-3.5 leading-relaxed [&_a]:font-semibold [&_a]:text-accent-foreground [&_a]:underline-offset-4 [&_a:hover]:underline [&_p:not(:last-child)]:mb-3",
              className
            )}
          >
            {children}
          </div>
        </AccordionPrimitive.Content>
      )
    }
    
    export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
    export type { AccordionProps }

用法

用法
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"

<Accordion defaultValue="rss">
  <AccordionItem value="rss">
    <AccordionTrigger>怎么用 RSS 订阅一部番?</AccordionTrigger>
    <AccordionContent>在番剧页点「订阅 RSS」……</AccordionContent>
  </AccordionItem>
</Accordion>

默认一次只开一节,并且可以全部收起——不用再写 type="single" collapsible。type="multiple"(或 Base UI 的 multiple)允许同时展开几节,collapsible={false} 让一节始终开着。

标题是 14/700,指针放上去变粉;展开的那节,右端的箭头翻转并变成粉色。标题里的图标(第一个 svg)是弱色的 16px。键盘、aria-expanded 和高度动画都来自 Radix 与 tw-animate-css。

分组卡

1080pHEVC10bitWebRip

variant="cards" 让每一节成为一个 well(1px 边、圆角 10),节与节之间 8px,展开的那节换成粉色的边。

属性

Accordion
variant"default" | "cards"默认 "default"缝线分隔的行 / 一节一张分组卡
type"single" | "multiple"默认 "single"一次开一节 / 可以开几节
multipleboolean默认 falseBase UI 的写法,等同 type=multiple
collapsibleboolean默认 truesingle 时能否全部收起
defaultValue · value · onValueChangeRadix默认 —single 时是 string,multiple 时是 string[]

AccordionItem、AccordionTrigger、AccordionContent 是 Radix 的对应部分,属性原样透传。