ข้ามไปเนื้อหา
WhereUI

ค้นหาในเอกสาร

ค้นหาชื่อ component หรือหน้าเอกสาร

Attachment

จาก shadcn

การ์ดรายการไฟล์แนบพร้อมไอคอน ชื่อไฟล์ สถานะ และปุ่มการกระทำ ใช้แสดงเอกสารประกอบคำขอกู้ทั้งก่อนและหลังอัปโหลด

สัญญากู้_น้ำใจ.pdf1.2 MB · อัปโหลดแล้ว
สลิปเงินเดือน.jpgอัปโหลดไม่สำเร็จ ไฟล์ใหญ่เกิน 5 MB
แนบเอกสารเพิ่มPDF หรือรูปภาพ ไม่เกิน 5 MB

ติดตั้ง

Terminal
pnpm dlx shadcn@latest add @whereui/attachment

ต้องผูก registry และติดตั้ง @whereui/theme นำหน้าเสมอเมื่อติดตั้งครั้งแรก

การใช้งาน

import {  Attachment, AttachmentAction, AttachmentActions, AttachmentContent,  AttachmentDescription, AttachmentMedia, AttachmentTitle,} from "@/components/ui/attachment"import { FileTextIcon, Trash2Icon } from "lucide-react"<Attachment>  <AttachmentMedia variant="icon"><FileTextIcon /></AttachmentMedia>  <AttachmentContent>    <AttachmentTitle>สัญญากู้.pdf</AttachmentTitle>    <AttachmentDescription>1.2 MB</AttachmentDescription>  </AttachmentContent>  <AttachmentActions>    <AttachmentAction aria-label="ลบไฟล์"><Trash2Icon /></AttachmentAction>  </AttachmentActions></Attachment>

ตัวอย่างเพิ่มเติม

ขนาด แนวตั้ง และสถานะอัปโหลด

สถานะ uploading / processing

บัญชีรายชื่อผู้กู้.xlsxกำลังอัปโหลด…
สัญญา_ก-2543.pdfกำลังตรวจไวรัส…

ขนาด xs / sm / default

รับแจ้งหักภาษี.pdf0.3 MB
ใบเสร็จค่าเล่าเรียน.jpg1.8 MB

แนวตั้ง (orientation=vertical)

หนังสือรับรองเงินเดือน.pdf0.9 MB

ข้อควรรู้

  • state มี idle / uploading / processing / error / done กำหนดสไตล์ให้อัตโนมัติ เช่น error ขอบแดง
  • ช่องว่างอัปโหลดใช้ state="idle" ร่วมกับ AttachmentTrigger ที่คลุมพื้นที่ทั้งใบ

ซอร์สโค้ด

components/ui/attachment.tsx
import * as React from "react"import { mergeProps } from "@base-ui/react/merge-props"import { useRender } from "@base-ui/react/use-render"import { cva, type VariantProps } from "class-variance-authority"import { cn } from "cn"import { Button } from "@/components/ui/button"const attachmentVariants = cva(  "group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-xl border bg-card text-card-foreground transition-colors focus-within:ring-1 focus-within:ring-ring/50 has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed",  {    variants: {      size: {        default:          "gap-2 text-sm has-data-[slot=attachment-content]:px-2.5 has-data-[slot=attachment-content]:py-2 has-data-[slot=attachment-media]:p-2",        sm: "gap-2.5 text-xs has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5 has-data-[slot=attachment-media]:p-1.5",        xs: "gap-1.5 rounded-lg text-xs has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1 has-data-[slot=attachment-media]:p-1",      },      orientation: {        horizontal: "min-w-40 items-center",        vertical: "w-24 flex-col has-data-[slot=attachment-content]:w-30",      },    },  })function Attachment({  className,  state = "done",  size = "default",  orientation = "horizontal",  ...props}: React.ComponentProps<"div"> &  VariantProps<typeof attachmentVariants> & {    state?: "idle" | "uploading" | "processing" | "error" | "done"  }) {  return (    <div      data-slot="attachment"      data-state={state}      data-size={size}      data-orientation={orientation}      className={cn(attachmentVariants({ size, orientation }), className)}      {...props}    />  )}const attachmentMediaVariants = cva(  "relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-foreground group-data-[orientation=vertical]/attachment:w-full group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-md group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-6 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5",  {    variants: {      variant: {        icon: "",        image:          "opacity-60 group-data-[state=done]/attachment:opacity-100 group-data-[state=idle]/attachment:opacity-100 *:[img]:aspect-square *:[img]:w-full *:[img]:object-cover",      },    },    defaultVariants: {      variant: "icon",    },  })function AttachmentMedia({  className,  variant = "icon",  ...props}: React.ComponentProps<"div"> & VariantProps<typeof attachmentMediaVariants>) {  return (    <div      data-slot="attachment-media"      data-variant={variant}      className={cn(attachmentMediaVariants({ variant }), className)}      {...props}    />  )}function AttachmentContent({  className,  ...props}: React.ComponentProps<"div">) {  return (    <div      data-slot="attachment-content"      className={cn(        "max-w-full min-w-0 flex-1 leading-tight group-data-[orientation=vertical]/attachment:px-1",        className      )}      {...props}    />  )}function AttachmentTitle({  className,  ...props}: React.ComponentProps<"span">) {  return (    <span      data-slot="attachment-title"      className={cn(        "block max-w-full min-w-0 truncate font-medium group-data-[state=processing]/attachment:shimmer group-data-[state=uploading]/attachment:shimmer",        className      )}      {...props}    />  )}function AttachmentDescription({  className,  ...props}: React.ComponentProps<"span">) {  return (    <span      data-slot="attachment-description"      className={cn(        "mt-0.5 block min-w-0 truncate text-xs text-muted-foreground group-data-[state=error]/attachment:text-destructive/80",        "max-w-full",        className      )}      {...props}    />  )}function AttachmentActions({  className,  ...props}: React.ComponentProps<"div">) {  return (    <div      data-slot="attachment-actions"      className={cn(        "relative z-20 flex shrink-0 items-center group-data-[orientation=vertical]/attachment:absolute group-data-[orientation=vertical]/attachment:top-3 group-data-[orientation=vertical]/attachment:right-3 group-data-[orientation=vertical]/attachment:gap-1",        className      )}      {...props}    />  )}function AttachmentAction({  className,  variant,  size = "icon-xs",  ...props}: React.ComponentProps<typeof Button>) {  return (    <Button      data-slot="attachment-action"      variant={variant ?? "ghost"}      size={size}      className={cn(className)}      {...props}    />  )}function AttachmentTrigger({  className,  render,  type,  ...props}: useRender.ComponentProps<"button">) {  return useRender({    defaultTagName: "button",    props: mergeProps<"button">(      {        type: render ? type : (type ?? "button"),        className: cn("absolute inset-0 z-10 outline-none", className),      },      props    ),    render,    state: {      slot: "attachment-trigger",    },  })}function AttachmentGroup({ className, ...props }: React.ComponentProps<"div">) {  return (    <div      data-slot="attachment-group"      className={cn(        "flex min-w-0 scroll-fade-x snap-x snap-mandatory scroll-px-1 scrollbar-none gap-3 overflow-x-auto overscroll-x-contain py-1 *:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start",        className      )}      {...props}    />  )}export {  Attachment,  AttachmentGroup,  AttachmentMedia,  AttachmentContent,  AttachmentTitle,  AttachmentDescription,  AttachmentActions,  AttachmentAction,  AttachmentTrigger,}