
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
attachmentuiSingle file or image chip. Drive look with props: state, size, orientation, media, src, removable. Not a dropzone (see file-upload).
Also available for Vue ->$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.jsonnpx shadcn@latest add @uipkge-react/attachmentInstalls to:components/ui/attachment/| 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 |
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 }
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>
export { Attachment, type AttachmentProps } from './attachment'
export { attachmentVariants, attachmentMediaVariants, type AttachmentVariants } from './attachment.variants'
Raw manifest:https://uipkge.dev/r/react/attachment.json