AniBTUI
✦ 组件@anibt/sonnerregistry JSON

Sonner

轻提示:浮层面板,左边 32px 的淡色类型方块,粗体标题加一行弱色说明,右端是安静的关闭键。

安装

npx shadcn@latest add @anibt/sonner

第一次用之前:在 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 sonner next-themes lucide-react
  3. 3. 把下面的文件放进项目
    components/ui/sonner.tsx
    "use client"
    
    import * as React from "react"
    import { CheckIcon, InfoIcon, Loader2Icon, TriangleAlertIcon, XIcon } from "lucide-react"
    import { useTheme } from "next-themes"
    import { Toaster as Sonner, type ToasterProps } from "sonner"
    
    /**
     * Toaster — AniBT 「贴纸手账」 toasts on sonner.
     *
     * Every toast is the floating panel: paper, a 1px edge and the pop shadow on
     * radius 14, 12px of padding. The type is a 32px tinted tile in the icon slot
     * — mint for done, blue for info, amber for a warning, red for a failure, the
     * sunk well with a spinner for work in flight — beside a 13/700 title and a
     * 12px muted line. The words still say the state on their own. The close
     * button is a quiet 24px square at the right end; an action is a small raised
     * key, a cancel is quiet.
     *
     * sonner styles a toast from attribute selectors, which outrank a utility
     * class, so the overrides here are `!important`. Its own 1px border is
     * dropped and the edge drawn in the shadow, so a toast has one line.
     *
     * Mount one `<Toaster />` near the root (shadcn's export name), then call
     * `toast.success("已复制磁力链接", { description: "…" })` from `sonner`.
     */
    const TILE = "grid size-8 place-items-center rounded-[10px] [&>svg]:size-4"
    
    const Toaster = ({ ...props }: ToasterProps) => {
      const { theme = "system" } = useTheme()
    
      return (
        <Sonner
          theme={theme as ToasterProps["theme"]}
          className="toaster group"
          closeButton
          gap={10}
          icons={{
            success: (
              <span className={`${TILE} bg-anibt-ok-wash text-anibt-ok`}>
                <CheckIcon strokeWidth={2.75} />
              </span>
            ),
            info: (
              <span className={`${TILE} bg-anibt-time-wash text-anibt-time-ink`}>
                <InfoIcon />
              </span>
            ),
            warning: (
              <span className={`${TILE} bg-anibt-warn-wash text-anibt-warn`}>
                <TriangleAlertIcon />
              </span>
            ),
            error: (
              <span className={`${TILE} bg-anibt-danger-wash text-destructive`}>
                <XIcon strokeWidth={2.75} />
              </span>
            ),
            loading: (
              <span className={`${TILE} bg-muted text-muted-foreground shadow-[inset_0_0_0_1px_var(--border)]`}>
                <Loader2Icon className="animate-spin" />
              </span>
            ),
          }}
          toastOptions={{
            classNames: {
              toast:
                "anibt-pop! border-0! gap-3! p-3! pr-11! text-[13px]! font-sans! focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid",
              title: "font-bold! text-[13px]! leading-snug! text-popover-foreground!",
              description: "text-[12px]! leading-normal! text-muted-foreground!",
              icon: "size-8! m-0! shrink-0!",
              closeButton:
                "top-1/2! size-6! rounded-[6px]! border-0! bg-transparent! text-muted-foreground! shadow-none! hover:bg-accent! hover:text-accent-foreground! focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid [&>svg]:size-3.5!",
              actionButton:
                "anibt-btn-raised! h-7! rounded-[10px]! px-3! text-[12px]! font-semibold!",
              cancelButton: "h-7! rounded-[10px]! bg-transparent! px-3! text-[12px]! font-semibold! text-anibt-ink2! hover:bg-accent! hover:text-accent-foreground!",
            },
          }}
          style={
            {
              "--normal-bg": "var(--popover)",
              "--normal-text": "var(--popover-foreground)",
              "--normal-border": "var(--border)",
              "--border-radius": "14px",
              // The close button sits at the right end, centred, instead of on the
              // top-left corner.
              "--toast-close-button-start": "auto",
              "--toast-close-button-end": "8px",
              "--toast-close-button-transform": "translateY(-50%)",
              // sonner's internal greys, pointed at the theme.
              "--gray2": "var(--muted)",
              "--gray4": "var(--border)",
              "--gray5": "var(--border)",
              "--gray11": "var(--muted-foreground)",
              "--gray12": "var(--popover-foreground)",
            } as React.CSSProperties
          }
          {...props}
        />
      )
    }
    
    export { Toaster }

用法

在应用根部放一个 <Toaster />(只放一个,否则每条提示会出现两次),然后在任何地方调用 sonner 的 toast:

app/layout.tsx
import { Toaster } from "@/components/ui/sonner"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-CN">
      <body>
        {children}
        <Toaster />
      </body>
    </html>
  )
}
components/copy-magnet.tsx
import { toast } from "sonner"

toast.success("已复制磁力链接", { description: "粘贴到下载工具即可开始" })

类型

  • toast.success:薄荷色方块和勾。
  • toast.info:蓝色方块。
  • toast.warning:琥珀色方块和警示三角。
  • toast.error:红色方块和叉。
  • toast.loading / toast.promise:下沉的灰色方块里转圈,结束后换成成功或失败。
  • toast(...):没有方块的普通提示。

颜色只是辅助,标题本身要说清楚发生了什么。

操作

action 是一个小号凸起键,cancel 是安静键,适合「撤销」这类一步可回的操作。

带撤销
toast("已删除 1 条资源", {
  description: "药屋少女的呢喃 第三季 - 12",
  action: { label: "撤销", onClick: restore },
})

主题

Toaster 从 next-themes 读取当前主题,没有 ThemeProvider 时跟随系统。面板颜色全部来自 @anibt/theme 的 token,白樱和夜樱各自正确。

属性

Toaster 接受 sonner 的全部属性(position、expand、duration、richColors……),默认打开 closeButton。