AniBTUI
✦ 组件@anibt/avatarregistry JSON

Avatar

模切白边的头像贴纸:人是圆的,字幕组和机构是圆角方的;没有图时是粉色淡底上的圆体首字。

喵喵绿AN星空
北Lo无

安装

npx shadcn@latest add @anibt/avatar

第一次用之前:在 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 class-variance-authority radix-ui
  3. 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 用形状区分两者。
  • 尺寸:xs 20(名字旁的小图)、sm 24、default 32、lg 40、xl 64(资料页头)。sm / default / lg 与 shadcn 同名,可以直接替换 components/ui/avatar.tsx。

组与角标

喵绿星北
+3
7 个字幕组在做
艾Yu栞
+12
15 人订阅

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。