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-vue@latest add https://uipkge.dev/r/vue/attachment.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/attachment.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/attachment.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/attachment.jsonNamed registry:
npx shadcn-vue@latest add @uipkge/attachmentInstalls to:app/components/ui/attachment/Examples
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
title | string | — | required |
description | string | — | optional |
state | 'idle''uploading''processing''error''done' | 'done' | optional |
size | 'default''sm''xs' | 'default' | optional |
orientation | 'horizontal''vertical' | 'horizontal' | optional |
media | 'file''image''code' | 'file' | optional |
src | string | — | optional |
alt | string | '' | optional |
removable | boolean | false | optional |
class | HTMLAttributes['class'] | — | optional |
npm dependencies
Files installed (3)
app/components/ui/attachment/Attachment.vue2.5 kB
<script setup lang="ts"> import { computed } from 'vue' import type { HTMLAttributes } from 'vue' import { FileCode, FileText, Image as ImageIcon, Loader2, X } from 'lucide-vue-next' import { cn } from '@/lib/utils' import { attachmentMediaVariants, attachmentVariants } from './attachment.variants' const props = withDefaults( defineProps<{ title: string description?: string state?: 'idle' | 'uploading' | 'processing' | 'error' | 'done' size?: 'default' | 'sm' | 'xs' orientation?: 'horizontal' | 'vertical' media?: 'file' | 'image' | 'code' src?: string alt?: string removable?: boolean class?: HTMLAttributes['class'] }>(), { state: 'done', size: 'default', orientation: 'horizontal', media: 'file', alt: '', removable: false, }, ) const emit = defineEmits<{ remove: [] }>() const busy = computed(() => props.state === 'uploading' || props.state === 'processing') </script> <template> <div data-uipkge data-slot="attachment" :data-state="state" :data-size="size" :data-orientation="orientation" :class="cn(attachmentVariants({ size, orientation }), props.class)" > <div data-slot="attachment-media" :class="cn(attachmentMediaVariants({ size }))"> <img v-if="src && media === 'image' && !busy" :src="src" :alt="alt" class="size-full object-cover" /> <Loader2 v-else-if="busy" class="size-4 motion-safe:animate-spin" aria-hidden="true" /> <FileCode v-else-if="media === 'code'" aria-hidden="true" /> <ImageIcon v-else-if="media === 'image'" aria-hidden="true" /> <FileText v-else aria-hidden="true" /> </div> <div data-slot="attachment-content" class="min-w-0 flex-1 leading-tight"> <span data-slot="attachment-title" :class="cn('block truncate font-medium', busy && 'animate-pulse')"> {{ title }} </span> <span v-if="description" data-slot="attachment-description" :class="cn('text-muted-foreground mt-0.5 block truncate text-xs', state === 'error' && 'text-destructive/80')" > {{ description }} </span> </div> <button v-if="removable" type="button" data-slot="attachment-remove" class="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}`" @click="emit('remove')" > <X class="size-3.5" /> </button> </div> </template>app/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>app/components/ui/attachment/index.ts0.2 kB
export { default as Attachment } from './Attachment.vue' export { attachmentVariants, attachmentMediaVariants, type AttachmentVariants } from './attachment.variants'
Raw manifest:https://uipkge.dev/r/vue/attachment.json