AniBTUI
✦ 组件@anibt/sheetregistry JSON

Sheet

从屏幕边缘滑入的面板:筛选、导航、手机侧栏。缝线头、一块滚动的正文、永远够得着的脚。

安装

npx shadcn@latest add @anibt/sheet

第一次用之前:在 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 lucide-react radix-ui
  3. 3. 把下面的文件放进项目
    components/ui/sheet.tsx
    "use client"
    
    import * as React from "react"
    import { XIcon } from "lucide-react"
    import { Dialog as SheetPrimitive } from "radix-ui"
    
    import { cn } from "@/lib/utils"
    
    /**
     * Sheet — AniBT 「贴纸手账」: the panel that slides in from an edge (filters,
     * navigation, a phone's sidebar).
     *
     * The same floating paper, scrim and close square as Dialog. Only the corners
     * away from the edge are rounded (14). `SheetHeader` is the stitched bar,
     * `SheetBody` is the one region that scrolls, and `SheetFooter` sits at the
     * bottom over a stitched top edge, so its buttons are always reachable.
     *
     * Export names and props are shadcn's (`side`, `showCloseButton`), so this
     * file can replace `components/ui/sheet.tsx` — shadcn's sidebar opens its
     * phone layout in it.
     */
    
    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 Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
      return <SheetPrimitive.Root data-slot="sheet" {...props} />
    }
    
    function SheetTrigger(props: React.ComponentProps<typeof SheetPrimitive.Trigger> & RenderCompat) {
      return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...renderAsChild(props)} />
    }
    
    function SheetClose(props: React.ComponentProps<typeof SheetPrimitive.Close> & RenderCompat) {
      return <SheetPrimitive.Close data-slot="sheet-close" {...renderAsChild(props)} />
    }
    
    function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
      return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
    }
    
    function SheetOverlay({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
      return (
        <SheetPrimitive.Overlay
          data-slot="sheet-overlay"
          className={cn(
            "fixed inset-0 z-50 bg-anibt-scrim duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
            className
          )}
          {...props}
        />
      )
    }
    
    function SheetContent({
      className,
      children,
      side = "right",
      showCloseButton = true,
      closeLabel = "关闭",
      ...props
    }: React.ComponentProps<typeof SheetPrimitive.Content> & {
      side?: "top" | "right" | "bottom" | "left"
      showCloseButton?: boolean
      /** The close button's accessible name. */
      closeLabel?: string
    }) {
      return (
        <SheetPortal>
          <SheetOverlay />
          <SheetPrimitive.Content
            data-slot="sheet-content"
            data-side={side}
            className={cn(
              "anibt-pop fixed z-50 flex flex-col overflow-y-auto overscroll-contain text-[14px] outline-none data-[state=closed]:animate-out data-[state=closed]:duration-200 data-[state=open]:animate-in data-[state=open]:duration-300 data-[state=open]:ease-anibt-out",
              side === "right" &&
                "inset-y-0 right-0 h-full w-3/4 rounded-r-none data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
              side === "left" &&
                "inset-y-0 left-0 h-full w-3/4 rounded-l-none data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
              side === "top" &&
                "inset-x-0 top-0 h-auto max-h-[80dvh] rounded-t-none data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
              side === "bottom" &&
                "inset-x-0 bottom-0 h-auto max-h-[80dvh] rounded-b-none pb-[env(safe-area-inset-bottom,0px)] data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
              className
            )}
            {...props}
          >
            {children}
            {showCloseButton ? (
              <SheetPrimitive.Close
                data-slot="sheet-close"
                className="anibt-icobtn anibt-icobtn-ghost absolute top-3 right-3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid disabled:pointer-events-none"
              >
                <XIcon className="size-4" />
                <span className="sr-only">{closeLabel}</span>
              </SheetPrimitive.Close>
            ) : null}
          </SheetPrimitive.Content>
        </SheetPortal>
      )
    }
    
    /** The 56px stitched bar at the top; its end is kept clear of the close square. */
    function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
      return (
        <div
          data-slot="sheet-header"
          className={cn(
            "anibt-stitch-under flex min-h-14 shrink-0 flex-col justify-center gap-1 px-4 py-3 pe-14 text-left",
            className
          )}
          {...props}
        />
      )
    }
    
    /** The one scrolling region of a sheet. */
    function SheetBody({ className, ...props }: React.ComponentProps<"div">) {
      return (
        <div
          data-slot="sheet-body"
          className={cn("min-h-0 flex-1 overflow-y-auto overscroll-contain p-4", className)}
          {...props}
        />
      )
    }
    
    /** Buttons at the bottom over a stitched top edge. */
    function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
      return (
        <div
          data-slot="sheet-footer"
          className={cn("anibt-stitch-over mt-auto flex shrink-0 flex-col gap-2 p-4 sm:flex-row sm:justify-end", className)}
          {...props}
        />
      )
    }
    
    /** 16/800 ink. */
    function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {
      return (
        <SheetPrimitive.Title
          data-slot="sheet-title"
          className={cn("text-[16px] leading-snug font-extrabold text-foreground", className)}
          {...props}
        />
      )
    }
    
    /** 12, muted. */
    function SheetDescription({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Description>) {
      return (
        <SheetPrimitive.Description
          data-slot="sheet-description"
          className={cn("text-[12px] leading-normal text-muted-foreground", className)}
          {...props}
        />
      )
    }
    
    export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetBody, SheetHeader, SheetFooter, SheetTitle, SheetDescription }

用法

components/filter-sheet.tsx
import { Button } from "@/components/ui/button"
import {
  Sheet,
  SheetBody,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

<Sheet>
  <SheetTrigger asChild>
    <Button variant="secondary">筛选</Button>
  </SheetTrigger>
  <SheetContent>
    <SheetHeader>
      <SheetTitle>筛选资源</SheetTitle>
      <SheetDescription>只看符合条件的发布。</SheetDescription>
    </SheetHeader>
    <SheetBody>{/* 条件 */}</SheetBody>
    <SheetFooter>
      <SheetClose asChild>
        <Button>应用</Button>
      </SheetClose>
    </SheetFooter>
  </SheetContent>
</Sheet>

结构

  • 面板和弹窗同一套浮层、遮罩和关闭方块,只把远离边缘的角做圆。
  • SheetHeader 是 56 高的缝线头:标题 16/800,说明 12 号弱色,右端给关闭按钮让位。
  • SheetBody 是唯一滚动的区域;SheetFooter 贴底,上沿是缝线,按钮在手机上竖排、宽屏右对齐。
  • 左右两侧宽 3/4,最宽 384;上下两侧最高 80% 视口,底部面板留出 Home 指示条的安全区。

方向

属性

SheetContent
side"top" | "right" | "bottom" | "left"默认 "right"从哪条边滑入
showCloseButtonboolean默认 true右上角的安静关闭方块
closeLabelstring默认 "关闭"关闭按钮的读屏名称

导出名与 shadcn 一致,另加 SheetBody。shadcn 的 sidebar 在手机上用它打开侧栏,替换后照常工作;SheetTrigger 和 SheetClose 也接受 Base UI 的 render。