AniBTUI
✦ 组件@anibt/popoverregistry JSON

Popover

贴在触发器旁边的小浮层:纸面、1px 外边、浮层阴影,标题 14/700,说明 12 号弱色。

安装

npx shadcn@latest add @anibt/popover

第一次用之前:在 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 radix-ui
  3. 3. 把下面的文件放进项目
    components/ui/popover.tsx
    "use client"
    
    import * as React from "react"
    import { Popover as PopoverPrimitive } from "radix-ui"
    
    import { cn } from "@/lib/utils"
    
    /**
     * Popover — AniBT 「贴纸手账」: a small panel anchored to its trigger.
     *
     * The floating tier (`anibt-pop`): paper, a 1px edge and the pop shadow on
     * radius 14, 16px of padding, 288 wide. `PopoverHeader` / `PopoverTitle` /
     * `PopoverDescription` give it a 14/700 title over a 12px muted line. Pickers
     * that fill it edge to edge (a command list) pass `className="p-0"`.
     *
     * Export names and props are shadcn's, so this file can replace
     * `components/ui/popover.tsx`.
     */
    
    type RenderCompat = {
      /** Base UI's `asChild`: render this element instead, with the part's props. */
      render?: React.ReactElement
      /** Base UI compatibility; ignored (Radix decides from the rendered element). */
      nativeButton?: boolean
    }
    
    /** Maps Base UI's `render` onto Radix's `asChild`. */
    function renderAsChild<P extends { asChild?: boolean; children?: React.ReactNode }>({
      render,
      nativeButton: _nativeButton,
      ...props
    }: P & RenderCompat): P {
      void _nativeButton
      if (!render) return props as P
      const children = props.children === undefined ? render : React.cloneElement(render, undefined, props.children)
      return { ...props, asChild: true, children } as P
    }
    
    function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
      return <PopoverPrimitive.Root data-slot="popover" {...props} />
    }
    
    function PopoverTrigger(props: React.ComponentProps<typeof PopoverPrimitive.Trigger> & RenderCompat) {
      return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...renderAsChild(props)} />
    }
    
    function PopoverContent({
      className,
      align = "center",
      sideOffset = 6,
      ...props
    }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
      return (
        <PopoverPrimitive.Portal>
          <PopoverPrimitive.Content
            data-slot="popover-content"
            align={align}
            sideOffset={sideOffset}
            className={cn(
              "anibt-pop z-50 flex w-72 max-w-[calc(100vw-1rem)] origin-(--radix-popover-content-transform-origin) flex-col gap-3 p-4 text-[13px] duration-150 outline-hidden",
              "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
              className
            )}
            {...props}
          />
        </PopoverPrimitive.Portal>
      )
    }
    
    function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
      return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
    }
    
    function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
      return <div data-slot="popover-header" className={cn("flex flex-col gap-1", className)} {...props} />
    }
    
    /** 14/700 ink. */
    function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
      return (
        <div
          data-slot="popover-title"
          className={cn("text-[14px] leading-snug font-bold text-foreground", className)}
          {...props}
        />
      )
    }
    
    /** 12, muted. */
    function PopoverDescription({ className, ...props }: React.ComponentProps<"p">) {
      return (
        <p
          data-slot="popover-description"
          className={cn("text-[12px] leading-normal text-muted-foreground", className)}
          {...props}
        />
      )
    }
    
    export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor, PopoverHeader, PopoverTitle, PopoverDescription }

用法

components/rss-popover.tsx
import { Button } from "@/components/ui/button"
import {
  Popover,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@/components/ui/popover"

<Popover>
  <PopoverTrigger asChild>
    <Button variant="secondary">RSS 订阅</Button>
  </PopoverTrigger>
  <PopoverContent align="start">
    <PopoverHeader>
      <PopoverTitle>订阅喵萌奶茶屋</PopoverTitle>
      <PopoverDescription>把链接粘贴到下载工具的 RSS 订阅里。</PopoverDescription>
    </PopoverHeader>
    {/* 内容 */}
  </PopoverContent>
</Popover>

材质

  • 浮层(anibt-pop),宽 288、内边 16,子元素之间 12。宽度用 className 改(w-80)。
  • 往哪边弹出由 Radix 决定,出现时从触发器方向轻轻滑入。
  • 里面放命令列表这类要铺满的内容时,传 className="p-0",见 Searchable Select。
  • 浮层不是唯一的入口:重要的操作在页面上也要能找到。

属性

PopoverContent
align"start" | "center" | "end"默认 "center"与触发器的对齐方式
sideOffsetnumber默认 6离触发器的距离
side"top" | "right" | "bottom" | "left"默认 "bottom"优先弹出的方向

导出名与 shadcn 一致:Popover、PopoverTrigger、PopoverContent、PopoverAnchor、PopoverHeader、PopoverTitle、PopoverDescription。PopoverTrigger 也接受 Base UI 的 render。