Card
AniBT 的面板:纸面、1px 外边、面板阴影、圆角 14,带缝线的头和脚;面板里的分组用 well。
我的订阅
喵萌奶茶屋 · 简繁内封 · 1080p
3
药屋少女的呢喃 第三季
1080p第 12 集
葬送的芙莉莲 第二季
1080p第 9 集
间谍过家家 第三季
1080p第 4 集
card-demo.tsx
import { Badge, StatusBadge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
const SHOWS = [
{ title: "药屋少女的呢喃 第三季", episode: "第 12 集", status: <StatusBadge tone="ok">已更新</StatusBadge> },
{ title: "葬送的芙莉莲 第二季", episode: "第 9 集", status: <StatusBadge tone="run">搬运中</StatusBadge> },
{ title: "间谍过家家 第三季", episode: "第 4 集", status: <StatusBadge tone="warn">延期</StatusBadge> },
]
export default function CardDemo() {
return (
<Card className="w-full max-w-md">
<CardHeader stitched>
<CardTitle>我的订阅</CardTitle>
<CardDescription>喵萌奶茶屋 · 简繁内封 · 1080p</CardDescription>
<CardAction>
<Badge variant="count">3</Badge>
</CardAction>
</CardHeader>
<CardContent className="flex flex-col">
{SHOWS.map((show) => (
<div key={show.title} className="anibt-stitch-under flex items-center gap-3 py-2.5 last:bg-none">
<div className="min-w-0 flex-1">
<div className="anibt-ell text-[14px] font-bold">{show.title}</div>
<div className="mt-1 flex items-center gap-1.5">
<Badge variant="res">1080p</Badge>
<span className="anibt-num text-[12px] text-muted-foreground">{show.episode}</span>
</div>
</div>
{show.status}
</div>
))}
</CardContent>
<CardFooter stitched className="justify-end">
<Button variant="ghost">管理</Button>
<Button>添加订阅</Button>
</CardFooter>
</Card>
)
}安装
npx shadcn@latest add @anibt/card第一次用之前:在 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/card.tsx
import * as React from "react" import { cn } from "@/lib/utils" /** * Card — an AniBT 「贴纸手账」 panel. * * `variant="panel"` (default) is paper on the ground under a 1px border edge * and the panel shadow, radius 14 — the `anibt-sheet` recipe. `variant="well"` * is a group inside a panel: the edge only, radius 10, no lift. * * The box is A's: 16px of padding and 12px between header, body and footer; * `density="compact"` tightens it to 12 / 8. Vertical padding and the gap are * the `--card-py` / `--card-gap` variables, so a caller's `py-0` / `gap-0` * still wins through `cn`. * * `CardHeader stitched` / `CardFooter stitched` part the head or the foot * from the body with A's dashed stitch, edge to edge. */ function Card({ className, variant = "panel", density = "default", ...props }: React.ComponentProps<"div"> & { variant?: "panel" | "well" density?: "default" | "compact" }) { return ( <div data-slot="card" data-variant={variant} data-density={density} className={cn( "group/card flex flex-col gap-(--card-gap) py-(--card-py) [--card-gap:0.75rem] [--card-py:1rem] data-[density=compact]:[--card-gap:0.5rem] data-[density=compact]:[--card-py:0.75rem]", variant === "well" ? "anibt-well" : "anibt-sheet", className )} {...props} /> ) } function CardHeader({ className, stitched = false, ...props }: React.ComponentProps<"div"> & { stitched?: boolean }) { return ( <div data-slot="card-header" data-stitched={stitched || undefined} className={cn( "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1 px-4 group-data-[density=compact]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto]", stitched && "anibt-stitch-under pb-3 group-data-[density=compact]/card:pb-2", className )} {...props} /> ) } /** A panel title: 16/800. */ function CardTitle({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="card-title" className={cn("text-[16px] leading-[1.4] font-extrabold text-card-foreground", className)} {...props} /> ) } /** The line under a title: 12, muted. */ function CardDescription({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="card-description" className={cn("text-[12px] leading-normal text-muted-foreground", className)} {...props} /> ) } /** The tools at the right end of a header row. */ function CardAction({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="card-action" className={cn("col-start-2 row-span-2 row-start-1 flex items-center gap-2 self-start justify-self-end", className)} {...props} /> ) } function CardContent({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="card-content" className={cn("px-4 text-[14px] group-data-[density=compact]/card:px-3", className)} {...props} /> ) } function CardFooter({ className, stitched = false, ...props }: React.ComponentProps<"div"> & { stitched?: boolean }) { return ( <div data-slot="card-footer" data-stitched={stitched || undefined} className={cn( "flex items-center gap-2 px-4 group-data-[density=compact]/card:px-3", stitched && "anibt-stitch-over pt-3 group-data-[density=compact]/card:pt-2", className )} {...props} /> ) } export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }
用法
components/subscription-card.tsx
import { Button } from "@/components/ui/button"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
export function SubscriptionCard() {
return (
<Card>
<CardHeader stitched>
<CardTitle>我的订阅</CardTitle>
<CardDescription>喵萌奶茶屋 · 简繁内封</CardDescription>
<CardAction>…</CardAction>
</CardHeader>
<CardContent>…</CardContent>
<CardFooter stitched className="justify-end">
<Button>保存</Button>
</CardFooter>
</Card>
)
}CardHeader stitched 在头下面缝一条虚线,CardFooter stitched 在脚上面缝一条,都从卡片一边通到另一边。标题是 16/800,说明是 12 号弱字,CardAction 放在头的右端。
分组
订阅设置
面板里的分组用 well:只留 1px 边,不再抬起。
字幕
简中繁中
画质与格式
1080pMKV
card-well.tsx
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
export default function CardWell() {
return (
<Card className="w-full max-w-lg">
<CardHeader stitched>
<CardTitle>订阅设置</CardTitle>
<CardDescription>面板里的分组用 well:只留 1px 边,不再抬起。</CardDescription>
</CardHeader>
<CardContent className="grid gap-3 sm:grid-cols-2">
<Card variant="well" density="compact">
<CardHeader>
<CardTitle className="text-[14px]">字幕</CardTitle>
<CardAction>
<Button size="xs" variant="ghost">
编辑
</Button>
</CardAction>
</CardHeader>
<CardContent className="flex flex-wrap gap-1.5">
<Badge variant="lang" face="text">
简中
</Badge>
<Badge variant="lang" face="text">
繁中
</Badge>
</CardContent>
</Card>
<Card variant="well" density="compact">
<CardHeader>
<CardTitle className="text-[14px]">画质与格式</CardTitle>
<CardAction>
<Button size="xs" variant="ghost">
编辑
</Button>
</CardAction>
</CardHeader>
<CardContent className="flex flex-wrap gap-1.5">
<Badge variant="res">1080p</Badge>
<Badge>MKV</Badge>
</CardContent>
</Card>
</CardContent>
</Card>
)
}面板里的面板用 variant="well":只留 1px 边,圆角 10,不再抬起。不要在面板里再叠一层带阴影的面板。
密度
默认 16 / 12
页面里的面板
24
紧凑 12 / 8
后台、工作台的密集卡
24
card-density.tsx
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
export default function CardDensity() {
return (
<div className="grid w-full max-w-xl gap-4 sm:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>默认 16 / 12</CardTitle>
<CardDescription>页面里的面板</CardDescription>
</CardHeader>
<CardContent>
<Badge variant="count-quiet">24</Badge>
</CardContent>
</Card>
<Card density="compact">
<CardHeader>
<CardTitle>紧凑 12 / 8</CardTitle>
<CardDescription>后台、工作台的密集卡</CardDescription>
</CardHeader>
<CardContent>
<Badge variant="count-quiet">24</Badge>
</CardContent>
</Card>
</div>
)
}默认的盒子是 16px 内边距、12px 的块间距;density="compact" 收紧到 12 / 8,给后台和工作台的密集卡用。垂直内边距和间距是 --card-py / --card-gap 变量,调用处的 py-0、gap-0 依然能覆盖。
属性
Card
variant"panel" | "well"默认 "panel"panel 是带阴影的面板(anibt-sheet);well 是面板里的分组(anibt-well)
density"default" | "compact"默认 "default"16 / 12 或 12 / 8 的内边距与间距
CardHeader · CardFooter
stitchedboolean默认 false头的下边 / 脚的上边缝一条虚线
CardTitle、CardDescription、CardAction、CardContent 没有额外属性,其余属性都透传给 div。
