Popover
贴在触发器旁边的小浮层:纸面、1px 外边、浮层阴影,标题 14/700,说明 12 号弱色。
popover-demo.tsx
"use client"
import { CheckIcon, CopyIcon, RssIcon } from "lucide-react"
import { useState } from "react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from "@/components/ui/popover"
const FEED = "https://anibt.net/rss/group/mmnc?res=1080p&lang=chs"
export default function PopoverDemo() {
const [copied, setCopied] = useState(false)
return (
<Popover onOpenChange={() => setCopied(false)}>
<PopoverTrigger asChild>
<Button variant="secondary">
<RssIcon />
RSS 订阅
</Button>
</PopoverTrigger>
<PopoverContent className="w-80" align="start">
<PopoverHeader>
<PopoverTitle>订阅喵萌奶茶屋</PopoverTitle>
<PopoverDescription>把链接粘贴到下载工具的 RSS 订阅里,新资源会自动下载。</PopoverDescription>
</PopoverHeader>
<div className="flex flex-wrap gap-1.5">
<Badge variant="res">1080p</Badge>
<Badge variant="lang" face="text">
简中
</Badge>
<Badge face="text">内封</Badge>
</div>
<div className="flex items-center gap-2">
<input
readOnly
aria-label="RSS 链接"
value={FEED}
className="anibt-input min-w-0 flex-1 font-mono text-[12px]"
onFocus={(event) => event.currentTarget.select()}
/>
<Button
variant="secondary"
size="icon"
aria-label={copied ? "已复制" : "复制链接"}
onClick={() => {
void navigator.clipboard?.writeText(FEED).catch(() => undefined)
setCopied(true)
}}
>
{copied ? <CheckIcon className="text-anibt-ok" /> : <CopyIcon />}
</Button>
</div>
</PopoverContent>
</Popover>
)
}安装
npx shadcn@latest add @anibt/popover第一次用之前:在 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 radix-ui - 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。
