AniBTUI
✦ 组件@anibt/alertregistry JSON

Alert

状态淡底上的提示:蓝色进行中、薄荷成功、琥珀注意、红色失败;也可以是表单里一行带点的提示。

正在同步到 4 个外站
完成后会在资源页显示每个站点的链接,大约需要两分钟。
发布成功
夜樱终电 第 6 集已经出现在最新资源和 RSS 里。

安装

npx shadcn@latest add @anibt/alert

第一次用之前:在 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
  3. 3. 把下面的文件放进项目
    components/ui/alert.tsx
    import * as React from "react"
    import { cva, type VariantProps } from "class-variance-authority"
    
    import { cn } from "@/lib/utils"
    
    /**
     * Alert — an AniBT 「贴纸手账」 notice.
     *
     * A note on the state's wash: radius 10, a 1px edge in the state's line, 13px
     * copy, and the icon (an `svg` as the first child, as in shadcn) in the
     * state's ink. One job per colour: `info` is blue (time, in progress),
     * `success` mint, `warning` amber, `destructive` red; `default` is neutral.
     * Pink never marks a notice — it is for actions.
     *
     * `inline` draws the site's row form instead: no fill, a 1px rule above and
     * below, and a 6px dot in the state's colour leading the line — for a notice
     * that is a line of a form or a list rather than a box on the page. The words
     * must say the state on their own; colour never speaks alone.
     *
     * `role="alert"` is the default; pass `role="status"` for a notice that is
     * not urgent. `AlertAction` holds a button at the top-right.
     */
    const alertVariants = cva(
      "group/alert relative grid w-full grid-cols-[minmax(0,1fr)] items-start gap-x-2.5 gap-y-0.5 text-left text-[13px] leading-[1.6] has-data-[slot=alert-action]:pr-24 [&>svg]:pointer-events-none [&>svg]:row-span-2 [&>svg]:mt-[3px] [&>svg:not([class*='size-'])]:size-4",
      {
        variants: {
          variant: {
            default: "",
            info: "",
            success: "",
            warning: "",
            destructive: "",
          },
          inline: {
            false:
              "rounded-[10px] px-3.5 py-2.5 has-[>svg]:grid-cols-[16px_minmax(0,1fr)]",
            true:
              "border-y border-border px-0 py-2 text-anibt-ink2 grid-cols-[6px_minmax(0,1fr)] before:row-span-2 before:mt-[8px] before:size-1.5 before:rounded-full before:content-[''] [&>svg]:hidden",
          },
        },
        compoundVariants: [
          { inline: false, variant: "default", class: "bg-muted text-foreground shadow-[inset_0_0_0_1px_var(--border)] [&>svg]:text-muted-foreground" },
          { inline: false, variant: "info", class: "bg-anibt-time-wash text-foreground shadow-[inset_0_0_0_1px_var(--anibt-time-line)] [&>svg]:text-anibt-time-ink *:data-[slot=alert-title]:text-anibt-time-ink" },
          { inline: false, variant: "success", class: "bg-anibt-ok-wash text-foreground shadow-[inset_0_0_0_1px_color-mix(in_srgb,var(--anibt-ok-fill)_40%,transparent)] [&>svg]:text-anibt-ok *:data-[slot=alert-title]:text-anibt-ok" },
          { inline: false, variant: "warning", class: "bg-anibt-warn-wash text-foreground shadow-[inset_0_0_0_1px_color-mix(in_srgb,var(--anibt-warn-fill)_45%,transparent)] [&>svg]:text-anibt-warn *:data-[slot=alert-title]:text-anibt-warn" },
          { inline: false, variant: "destructive", class: "bg-anibt-danger-wash text-foreground shadow-[inset_0_0_0_1px_var(--anibt-danger-line)] [&>svg]:text-destructive *:data-[slot=alert-title]:text-destructive" },
          { inline: true, variant: "default", class: "before:bg-muted-foreground" },
          { inline: true, variant: "info", class: "before:bg-anibt-time" },
          { inline: true, variant: "success", class: "before:bg-anibt-ok-fill" },
          { inline: true, variant: "warning", class: "before:bg-anibt-warn-fill" },
          { inline: true, variant: "destructive", class: "before:bg-anibt-danger-fill" },
        ],
        defaultVariants: {
          variant: "default",
          inline: false,
        },
      }
    )
    
    type AlertProps = React.ComponentProps<"div"> & VariantProps<typeof alertVariants>
    
    function Alert({ className, variant, inline, ...props }: AlertProps) {
      return (
        <div
          data-slot="alert"
          data-variant={variant ?? "default"}
          data-inline={inline ? "true" : undefined}
          role="alert"
          className={cn(alertVariants({ variant, inline }), className)}
          {...props}
        />
      )
    }
    
    /** 13/700; takes the state's ink on a tinted alert. */
    function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
      return (
        <div
          data-slot="alert-title"
          className={cn(
            "col-start-auto font-bold text-foreground group-has-[>svg]/alert:col-start-2 group-data-[inline=true]/alert:col-start-2",
            className
          )}
          {...props}
        />
      )
    }
    
    function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
      return (
        <div
          data-slot="alert-description"
          className={cn(
            "grid justify-items-start gap-1 text-anibt-ink2 group-has-[>svg]/alert:col-start-2 group-data-[inline=true]/alert:col-start-2 [&_a]:font-semibold [&_a]:text-accent-foreground [&_a]:underline-offset-4 [&_a:hover]:underline [&_p]:leading-relaxed",
            className
          )}
          {...props}
        />
      )
    }
    
    /** A button at the top-right of the notice (retry, dismiss). */
    function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
      return (
        <div
          data-slot="alert-action"
          className={cn(
            "absolute top-2 right-2 flex items-center gap-1 group-data-[inline=true]/alert:top-1.5 group-data-[inline=true]/alert:right-0",
            className
          )}
          {...props}
        />
      )
    }
    
    export { Alert, AlertTitle, AlertDescription, AlertAction, alertVariants }
    export type { AlertProps }

用法

用法
import { InfoIcon } from "lucide-react"

import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"

<Alert variant="info" role="status">
  <InfoIcon />
  <AlertTitle>正在同步到 4 个外站</AlertTitle>
  <AlertDescription>完成后会在资源页显示每个站点的链接。</AlertDescription>
</Alert>

一种颜色一种职责:info 蓝(时间、进行中)、success 薄荷、warning 琥珀、destructive 红,default 是中性的。粉色从不用来提示——粉色是操作。

第一个子元素是 svg 时(和 shadcn 一样),它会放在左边一列,染成状态的颜色;标题也用状态的颜色。AlertAction 把一个按钮放在右上角(重试、关闭)。

role 默认是 alert,会被立即播报;不紧急的提示写 role="status"。

行内

下一集预计今晚 23:30 播出。

inline 是站内表单和列表里的写法:不铺底色,上下各一条 1px 线,行首一个 6px 的状态点。点是 aria-hidden 的,文字必须自己说清状态,颜色从不单独表达意思。几条行内提示叠在一起时,给后面的加 border-t-0。

属性

Alert
variant"default" | "info" | "success" | "warning" | "destructive"默认 "default"状态
inlineboolean默认 false行内版式:无底色、上下细线、行首状态点
rolestring默认 "alert"不紧急时用 status

AlertTitle、AlertDescription、AlertAction 与 shadcn 同名,属性透传给 div。