Attachment
attachmentuiSingle file or image chip. Drive look with props: state, size, orientation, media, src, removable. Not a dropzone (see file-upload).
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/attachment.json$npx shadcn@latest add https://uipkge.dev/r/react/attachment.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/attachment.json$bunx shadcn@latest add https://uipkge.dev/r/react/attachment.jsonNamed registry:
npx shadcn@latest add @uipkge-react/attachmentInstalls to:components/ui/attachment/Examples
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
size | 'default''sm''xs' | default | optional |
orientation | 'horizontal''vertical' | horizontal | optional |
title | string | — | required |
description | string | — | optional |
state | 'idle' | 'uploading' | 'processing' | 'error' | 'done' | — | optional |
media | 'file' | 'image' | 'code' | — | optional |
src | string | — | optional |
alt | string | — | optional |
removable | boolean | — | optional |
onRemove | () => void | — | optional |
npm dependencies
Files installed (3)
components/ui/attachment/attachment.tsx2.9 kB
import * as React from 'react' import { FileCode, FileText, Image as ImageIcon, Loader2, X } from 'lucide-react' import { cn } from '@/lib/utils' import { attachmentMediaVariants, attachmentVariants, type AttachmentVariants } from './attachment.variants' export interface AttachmentProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'>, AttachmentVariants { title: string description?: string state?: 'idle' | 'uploading' | 'processing' | 'error' | 'done' media?: 'file' | 'image' | 'code' src?: string alt?: string removable?: boolean onRemove?: () => void } const Attachment = React.forwardRef<HTMLDivElement, AttachmentProps>( ( { title, description, state = 'done', size = 'default', orientation = 'horizontal', media = 'file', src, alt = '', removable = false, onRemove, className, ...props }, ref, ) => { const busy = state === 'uploading' || state === 'processing' return ( <div ref={ref} data-uipkge="" data-slot="attachment" data-state={state} data-size={size} data-orientation={orientation} className={cn(attachmentVariants({ size, orientation }), className)} {...props} > <div data-slot="attachment-media" className={cn(attachmentMediaVariants({ size }))}> {src && media === 'image' && !busy ? ( <img src={src} alt={alt} className="size-full object-cover" /> ) : busy ? ( <Loader2 className="size-4 motion-safe:animate-spin" aria-hidden="true" /> ) : media === 'code' ? ( <FileCode aria-hidden="true" /> ) : media === 'image' ? ( <ImageIcon aria-hidden="true" /> ) : ( <FileText aria-hidden="true" /> )} </div> <div data-slot="attachment-content" className="min-w-0 flex-1 leading-tight"> <span data-slot="attachment-title" className={cn('block truncate font-medium', busy && 'animate-pulse')}> {title} </span> {description ? ( <span data-slot="attachment-description" className={cn( 'text-muted-foreground mt-0.5 block truncate text-xs', state === 'error' && 'text-destructive/80', )} > {description} </span> ) : null} </div> {removable ? ( <button type="button" data-slot="attachment-remove" className="text-muted-foreground hover:bg-accent hover:text-foreground relative z-10 inline-flex size-7 shrink-0 items-center justify-center rounded-md" aria-label={`Remove ${title}`} onClick={onRemove} > <X className="size-3.5" /> </button> ) : null} </div> ) }, ) Attachment.displayName = 'Attachment' export { Attachment }components/ui/attachment/attachment.variants.ts1.7 kB
import type { VariantProps } from 'class-variance-authority' import { cva } from 'class-variance-authority' /** * Variant definitions live in their own file (rather than the package * `index.ts`) so consuming Vue SFCs can import without creating a circular * dependency through the index. See card.variants.ts for the canonical * example + the SSR symptom that motivated the split. */ export const attachmentVariants = cva( 'group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 rounded-xl border bg-card text-card-foreground transition-colors data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed', { variants: { size: { default: 'gap-2 p-2 text-sm', sm: 'gap-2 p-1.5 text-xs', xs: 'gap-1.5 rounded-lg p-1 text-xs', }, orientation: { horizontal: 'min-w-40 items-center', vertical: 'w-24 flex-col', }, }, defaultVariants: { size: 'default', orientation: 'horizontal', }, }, ) export const attachmentMediaVariants = cva( "relative flex aspect-square shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-foreground group-data-[orientation=vertical]/attachment:w-full group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4", { variants: { size: { default: 'w-10', sm: 'w-8', xs: 'w-7 rounded-md [&_svg:not([class*="size-"])]:size-3.5', }, }, defaultVariants: { size: 'default', }, }, ) export type AttachmentVariants = VariantProps<typeof attachmentVariants>components/ui/attachment/index.ts0.2 kB
export { Attachment, type AttachmentProps } from './attachment' export { attachmentVariants, attachmentMediaVariants, type AttachmentVariants } from './attachment.variants'
Raw manifest:https://uipkge.dev/r/react/attachment.json