Avatar
模切白边的头像贴纸:人是圆的,字幕组和机构是圆角方的;没有图时是粉色淡底上的圆体首字。
喵喵绿AN星空
北Lo无
avatar-demo.tsx
import { Avatar, AvatarBadge, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
export default function AvatarDemo() {
return (
<div className="flex flex-col items-center gap-6">
<div className="flex items-end gap-4">
<Avatar size="xs">
<AvatarFallback>喵</AvatarFallback>
</Avatar>
<Avatar size="sm">
<AvatarImage src="/demo/logos/paw.svg" alt="喵萌奶茶屋" />
<AvatarFallback>喵</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage src="/demo/logos/leaf.svg" alt="绿茶字幕组" />
<AvatarFallback>绿</AvatarFallback>
</Avatar>
<Avatar size="lg">
<AvatarFallback>AN</AvatarFallback>
<AvatarBadge tone="ok" />
</Avatar>
<Avatar size="xl" shape="rounded">
<AvatarImage src="/demo/logos/star.svg" alt="星空字幕组" />
<AvatarFallback>星空</AvatarFallback>
</Avatar>
</div>
<div className="flex items-center gap-4">
<Avatar shape="rounded">
<AvatarImage src="/demo/logos/moon.svg" alt="北宇治字幕组" />
<AvatarFallback>北</AvatarFallback>
</Avatar>
<Avatar shape="rounded">
<AvatarFallback>Lo</AvatarFallback>
</Avatar>
<Avatar rim={false}>
<AvatarFallback>无</AvatarFallback>
</Avatar>
</div>
</div>
)
}安装
npx shadcn@latest add @anibt/avatar第一次用之前:在 components.json 里登记 @anibt,再单独运行一次 npx shadcn@latest add @anibt/theme(见快速开始)。只有单独安装主题时,CLI 才会把 shadcn 的标准颜色换成白樱 / 夜樱;随组件带进来的主题只补上 AniBT 自己的变量和配方。
手动安装
- 1. 先装主题组件只读
@anibt/theme的 token 和配方,没有主题就没有样子。npx shadcn@latest add @anibt/theme - 2. 安装依赖
npm install class-variance-authority radix-ui - 3. 把下面的文件放进项目components/ui/avatar.tsx
"use client" import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" import { Avatar as AvatarPrimitive } from "radix-ui" import { cn } from "@/lib/utils" /** * Avatar — an AniBT 「贴纸手账」 die-cut sticker of a face or a logo. * * Every avatar wears the sticker rim (`anibt-rim`: a 2px white edge and the * small sticker shadow) unless `rim={false}`. People are round; groups and * organisations are `shape="rounded"` (a 28% corner), which is how A tells * the two apart. The fallback is the round face on the pink wash. * * Sizes: `xs` 20 (inline beside a name), `sm` 24, `default` 32, `lg` 40, * `xl` 64 (a profile head). The names `sm` / `default` / `lg` and the * exports `AvatarBadge`, `AvatarGroup`, `AvatarGroupCount` are shadcn's, so * this file can replace `components/ui/avatar.tsx`. * * Radix underneath: the image is shown once it has loaded, the fallback * until then and whenever it fails. */ const avatarVariants = cva( "group/avatar relative inline-flex shrink-0 select-none overflow-visible align-middle data-[shape=circle]:rounded-full data-[shape=rounded]:rounded-[28%]", { variants: { size: { xs: "size-5", sm: "size-6", default: "size-8", lg: "size-10", xl: "size-16", }, rim: { true: "anibt-rim", false: "", }, }, defaultVariants: { size: "default", rim: true, }, } ) type AvatarProps = React.ComponentProps<typeof AvatarPrimitive.Root> & VariantProps<typeof avatarVariants> & { /** `circle` for people, `rounded` for groups and organisations. */ shape?: "circle" | "rounded" } function Avatar({ className, size, rim, shape = "circle", ...props }: AvatarProps) { return ( <AvatarPrimitive.Root data-slot="avatar" data-size={size ?? "default"} data-shape={shape} className={cn(avatarVariants({ size, rim }), className)} {...props} /> ) } function AvatarImage({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Image>) { return ( <AvatarPrimitive.Image data-slot="avatar-image" className={cn("aspect-square size-full rounded-[inherit] object-cover", className)} {...props} /> ) } function AvatarFallback({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) { return ( <AvatarPrimitive.Fallback data-slot="avatar-fallback" className={cn( "flex size-full items-center justify-center overflow-hidden rounded-[inherit] bg-accent font-anibt-display text-[13px] leading-none font-semibold text-accent-foreground", "group-data-[size=xs]/avatar:text-[12px] group-data-[size=sm]/avatar:text-[12px] group-data-[size=lg]/avatar:text-[15px] group-data-[size=xl]/avatar:text-[22px]", className )} {...props} /> ) } const avatarBadgeVariants = cva( "absolute -right-0.5 -bottom-0.5 z-10 inline-flex size-2.5 items-center justify-center rounded-full shadow-[0_0_0_2px_var(--anibt-sticker-edge)] select-none group-data-[size=lg]/avatar:size-3 group-data-[size=sm]/avatar:size-2 group-data-[size=xl]/avatar:right-0.5 group-data-[size=xl]/avatar:bottom-0.5 group-data-[size=xl]/avatar:size-4 group-data-[size=xs]/avatar:size-2 [&>svg]:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden group-data-[size=xs]/avatar:[&>svg]:hidden", { variants: { tone: { mark: "bg-anibt-mark text-white", ok: "bg-anibt-ok-fill text-white", time: "bg-anibt-time text-white", warn: "bg-anibt-warn-fill text-white", mute: "bg-muted-foreground text-white", }, }, defaultVariants: { tone: "mark", }, } ) /** A dot on the corner: online, live, a new item. Say the state in words nearby. */ function AvatarBadge({ className, tone, ...props }: React.ComponentProps<"span"> & VariantProps<typeof avatarBadgeVariants>) { return ( <span data-slot="avatar-badge" className={cn(avatarBadgeVariants({ tone }), className)} {...props} /> ) } /** Overlapping avatars; each rim keeps its neighbour apart. */ function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="avatar-group" className={cn("group/avatar-group flex items-center -space-x-1.5", className)} {...props} /> ) } /** The `+N` at the end of a group, as a quiet count sticker. */ function AvatarGroupCount({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="avatar-group-count" className={cn( "relative inline-flex size-8 shrink-0 items-center justify-center rounded-full bg-muted font-anibt-display text-[12px] font-semibold text-muted-foreground shadow-[0_0_0_2px_var(--anibt-sticker-edge),var(--anibt-shadow-sticker)]", "group-has-data-[size=xs]/avatar-group:size-5 group-has-data-[size=sm]/avatar-group:size-6 group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=xl]/avatar-group:size-16 [&>svg]:size-4", className )} {...props} /> ) } export { Avatar, AvatarImage, AvatarFallback, AvatarBadge, AvatarGroup, AvatarGroupCount, avatarVariants, } export type { AvatarProps }
用法
用法
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
<Avatar shape="rounded">
<AvatarImage src="/logos/nekomoe.png" alt="喵萌奶茶屋" />
<AvatarFallback>喵</AvatarFallback>
</Avatar>每个头像都贴着 2px 的模切白边和贴纸阴影(anibt-rim),rim={false} 可以去掉。图片加载完成前和加载失败时显示 AvatarFallback:粉色淡底、粉色圆体字。
只想传名字和图片地址、让组件自己决定首字,用 Entity Avatar。
形状与尺寸
shape="circle"(默认)给人,shape="rounded"(28% 圆角)给字幕组、机构。A 用形状区分两者。- 尺寸:
xs20(名字旁的小图)、sm24、default32、lg40、xl64(资料页头)。sm/default/lg与 shadcn 同名,可以直接替换components/ui/avatar.tsx。
组与角标
喵绿星北
7 个字幕组在做+3
艾Yu栞
15 人订阅+12
avatar-group.tsx
import { Avatar, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage } from "@/components/ui/avatar"
const GROUPS = [
{ name: "喵萌奶茶屋", logo: "/demo/logos/paw.svg" },
{ name: "绿茶字幕组", logo: "/demo/logos/leaf.svg" },
{ name: "星空字幕组", logo: "/demo/logos/star.svg" },
{ name: "北宇治字幕组", logo: "/demo/logos/moon.svg" },
]
export default function AvatarGroupDemo() {
return (
<div className="flex flex-col items-start gap-5">
<div className="flex items-center gap-3">
<AvatarGroup>
{GROUPS.map((group) => (
<Avatar key={group.name} shape="rounded" title={group.name}>
<AvatarImage src={group.logo} alt={group.name} />
<AvatarFallback>{group.name.slice(0, 1)}</AvatarFallback>
</Avatar>
))}
<AvatarGroupCount>+3</AvatarGroupCount>
</AvatarGroup>
<span className="text-[13px] text-anibt-ink2">7 个字幕组在做</span>
</div>
<div className="flex items-center gap-3">
<AvatarGroup>
<Avatar size="sm">
<AvatarFallback>艾</AvatarFallback>
</Avatar>
<Avatar size="sm">
<AvatarFallback>Yu</AvatarFallback>
</Avatar>
<Avatar size="sm">
<AvatarFallback>栞</AvatarFallback>
</Avatar>
<AvatarGroupCount>+12</AvatarGroupCount>
</AvatarGroup>
<span className="text-[13px] text-anibt-ink2">15 人订阅</span>
</div>
</div>
)
}AvatarGroup 让头像互相压住一点,白边自然把它们分开;AvatarGroupCount 是末尾的「+N」,一枚安静的计数贴纸。AvatarBadge 是右下角的小点(在线、直播中、有新内容),颜色只是记号,旁边要有文字说明状态。
属性
Avatar
size"xs" | "sm" | "default" | "lg" | "xl"默认 "default"20 / 24 / 32 / 40 / 64
shape"circle" | "rounded"默认 "circle"人用圆形,字幕组和机构用圆角方形
rimboolean默认 true模切白边与贴纸阴影
AvatarBadge
tone"mark" | "ok" | "time" | "warn" | "mute"默认 "mark"粉 / 薄荷 / 蓝 / 琥珀 / 灰
AvatarImage、AvatarFallback 是 Radix Avatar 的 Image / Fallback,属性原样透传;AvatarGroup、AvatarGroupCount 透传给 div。
