Alert
状态淡底上的提示:蓝色进行中、薄荷成功、琥珀注意、红色失败;也可以是表单里一行带点的提示。
正在同步到 4 个外站
完成后会在资源页显示每个站点的链接,大约需要两分钟。
发布成功
夜樱终电 第 6 集已经出现在最新资源和 RSS 里。
种子里有 2 个文件名不规范
不影响发布,但外站可能识别不出集数。
动漫花园同步失败
HTTP 502:上游连续 3 次没有响应。
草稿会保存在这台设备上
换一台设备就看不到了。
alert-demo.tsx
import { CircleCheckIcon, InfoIcon, RefreshCwIcon, TriangleAlertIcon, XCircleIcon } from "lucide-react"
import { Alert, AlertAction, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"
export default function AlertDemo() {
return (
<div className="flex w-full max-w-lg flex-col gap-3">
<Alert variant="info" role="status">
<InfoIcon />
<AlertTitle>正在同步到 4 个外站</AlertTitle>
<AlertDescription>完成后会在资源页显示每个站点的链接,大约需要两分钟。</AlertDescription>
</Alert>
<Alert variant="success" role="status">
<CircleCheckIcon />
<AlertTitle>发布成功</AlertTitle>
<AlertDescription>夜樱终电 第 6 集已经出现在最新资源和 RSS 里。</AlertDescription>
</Alert>
<Alert variant="warning">
<TriangleAlertIcon />
<AlertTitle>种子里有 2 个文件名不规范</AlertTitle>
<AlertDescription>不影响发布,但外站可能识别不出集数。</AlertDescription>
</Alert>
<Alert variant="destructive">
<XCircleIcon />
<AlertTitle>动漫花园同步失败</AlertTitle>
<AlertDescription>HTTP 502:上游连续 3 次没有响应。</AlertDescription>
<AlertAction>
<Button size="sm" variant="ghost" className="text-destructive">
<RefreshCwIcon />
重试
</Button>
</AlertAction>
</Alert>
<Alert>
<AlertTitle>草稿会保存在这台设备上</AlertTitle>
<AlertDescription>换一台设备就看不到了。</AlertDescription>
</Alert>
</div>
)
}安装
npx shadcn@latest add @anibt/alert第一次用之前:在 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 - 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"。
行内
这条资源的磁力链接已失效 3 天,换一个版本可能更快。
同步失败:Nyaa
上游返回 403,检查站点设置里的 API 密钥。
下一集预计今晚 23:30 播出。
alert-inline.tsx
import { Alert, AlertAction, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"
export default function AlertInline() {
return (
<div className="flex w-full max-w-lg flex-col">
<Alert inline variant="warning">
<AlertDescription>这条资源的磁力链接已失效 3 天,换一个版本可能更快。</AlertDescription>
</Alert>
<Alert inline variant="destructive" className="border-t-0">
<AlertTitle>同步失败:Nyaa</AlertTitle>
<AlertDescription>上游返回 403,检查站点设置里的 API 密钥。</AlertDescription>
<AlertAction>
<Button size="xs" variant="secondary">
去设置
</Button>
</AlertAction>
</Alert>
<Alert inline variant="info" role="status" className="border-t-0">
<AlertDescription>下一集预计今晚 23:30 播出。</AlertDescription>
</Alert>
</div>
)
}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。
