Attachment
A file or image with metadata, upload state and actions. States idle, uploading, processing, error, done.
import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentMedia, AttachmentTitle,} from "@/components/ui/attachment";import { Spinner } from "@/components/ui/spinner";import { IconPlaceholder } from "@/edmi/icon-placeholder";
export default function Demo() { return ( <div className="flex flex-wrap items-start gap-3"> <Attachment> <AttachmentMedia> <IconPlaceholder lucide="FileTextIcon" tabler="IconFileText" hugeicons="FileIcon" phosphor="FileTextIcon" remixicon="RiFileTextLine" /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>thesis.pdf</AttachmentTitle> <AttachmentDescription>PDF, 2.4 MB</AttachmentDescription> </AttachmentContent> <AttachmentActions> <AttachmentAction aria-label="Remove"> <IconPlaceholder lucide="XIcon" tabler="IconX" hugeicons="Cancel01Icon" phosphor="XIcon" remixicon="RiCloseLine" /> </AttachmentAction> </AttachmentActions> </Attachment> <Attachment state="uploading"> <AttachmentMedia> <Spinner /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>weights.csv</AttachmentTitle> <AttachmentDescription>Uploading 64%</AttachmentDescription> </AttachmentContent> </Attachment> <Attachment state="processing"> <AttachmentMedia> <Spinner /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>report.pdf</AttachmentTitle> <AttachmentDescription>Processing</AttachmentDescription> </AttachmentContent> </Attachment> <Attachment state="error"> <AttachmentMedia> <IconPlaceholder lucide="CircleAlertIcon" tabler="IconExclamationCircle" hugeicons="AlertCircleIcon" phosphor="WarningCircleIcon" remixicon="RiErrorWarningLine" /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>backtest.xlsx</AttachmentTitle> <AttachmentDescription>Upload failed</AttachmentDescription> </AttachmentContent> <AttachmentActions> <AttachmentAction variant="outline" size="xs"> Retry </AttachmentAction> </AttachmentActions> </Attachment> <Attachment size="sm"> <AttachmentMedia> <IconPlaceholder lucide="FileTextIcon" tabler="IconFileText" hugeicons="FileIcon" phosphor="FileTextIcon" remixicon="RiFileTextLine" /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>mandate.pdf</AttachmentTitle> <AttachmentDescription>PDF, 540 KB</AttachmentDescription> </AttachmentContent> <AttachmentActions> <IconPlaceholder lucide="CheckIcon" tabler="IconCheck" hugeicons="Tick02Icon" phosphor="CheckIcon" remixicon="RiCheckLine" className="size-4 text-success-text" /> </AttachmentActions> </Attachment> </div> );}<script setup lang="ts">import { CheckIcon, AlertCircle, FileTextIcon, XIcon } from "@lucide/vue";import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentMedia, AttachmentTitle,} from "@/components/ui/attachment";import { Spinner } from "@/components/ui/spinner";</script>
<template> <div class="flex flex-wrap items-start gap-3"> <Attachment> <AttachmentMedia><FileTextIcon /></AttachmentMedia> <AttachmentContent> <AttachmentTitle>thesis.pdf</AttachmentTitle> <AttachmentDescription>PDF, 2.4 MB</AttachmentDescription> </AttachmentContent> <AttachmentActions> <AttachmentAction aria-label="Remove"><XIcon /></AttachmentAction> </AttachmentActions> </Attachment> <Attachment state="uploading"> <AttachmentMedia><Spinner /></AttachmentMedia> <AttachmentContent> <AttachmentTitle>weights.csv</AttachmentTitle> <AttachmentDescription>Uploading 64%</AttachmentDescription> </AttachmentContent> </Attachment> <Attachment state="processing"> <AttachmentMedia><Spinner /></AttachmentMedia> <AttachmentContent> <AttachmentTitle>report.pdf</AttachmentTitle> <AttachmentDescription>Processing</AttachmentDescription> </AttachmentContent> </Attachment> <Attachment state="error"> <AttachmentMedia><AlertCircle /></AttachmentMedia> <AttachmentContent> <AttachmentTitle>backtest.xlsx</AttachmentTitle> <AttachmentDescription>Upload failed</AttachmentDescription> </AttachmentContent> <AttachmentActions> <AttachmentAction variant="outline" size="xs">Retry</AttachmentAction> </AttachmentActions> </Attachment> <Attachment size="sm"> <AttachmentMedia><FileTextIcon /></AttachmentMedia> <AttachmentContent> <AttachmentTitle>mandate.pdf</AttachmentTitle> <AttachmentDescription>PDF, 540 KB</AttachmentDescription> </AttachmentContent> <AttachmentActions> <CheckIcon class="size-4 text-success-text" /> </AttachmentActions> </Attachment> </div></template><script lang="ts"> import * as Attachment from "$lib/components/ui/attachment"; import { Spinner } from "$lib/components/ui/spinner"; import IconPlaceholder from "$lib/components/icon-placeholder/icon-placeholder.svelte";</script>
<div class="flex flex-wrap items-start gap-3"> <Attachment.Root> <Attachment.Media><IconPlaceholder lucide="FileTextIcon" tabler="IconFileText" hugeicons="FileIcon" phosphor="FileTextIcon" remixicon="RiFileTextLine" /></Attachment.Media> <Attachment.Content> <Attachment.Title>thesis.pdf</Attachment.Title> <Attachment.Description>PDF, 2.4 MB</Attachment.Description> </Attachment.Content> <Attachment.Actions> <Attachment.Action aria-label="Remove"><IconPlaceholder lucide="XIcon" tabler="IconX" hugeicons="Cancel01Icon" phosphor="XIcon" remixicon="RiCloseLine" /></Attachment.Action> </Attachment.Actions> </Attachment.Root> <Attachment.Root state="uploading"> <Attachment.Media><Spinner /></Attachment.Media> <Attachment.Content> <Attachment.Title>weights.csv</Attachment.Title> <Attachment.Description>Uploading 64%</Attachment.Description> </Attachment.Content> </Attachment.Root> <Attachment.Root state="processing"> <Attachment.Media><Spinner /></Attachment.Media> <Attachment.Content> <Attachment.Title>report.pdf</Attachment.Title> <Attachment.Description>Processing</Attachment.Description> </Attachment.Content> </Attachment.Root> <Attachment.Root state="error"> <Attachment.Media><IconPlaceholder lucide="CircleAlertIcon" tabler="IconExclamationCircle" hugeicons="AlertCircleIcon" phosphor="WarningCircleIcon" remixicon="RiErrorWarningLine" /></Attachment.Media> <Attachment.Content> <Attachment.Title>backtest.xlsx</Attachment.Title> <Attachment.Description>Upload failed</Attachment.Description> </Attachment.Content> <Attachment.Actions> <Attachment.Action variant="outline" size="xs">Retry</Attachment.Action> </Attachment.Actions> </Attachment.Root> <Attachment.Root size="sm"> <Attachment.Media><IconPlaceholder lucide="FileTextIcon" tabler="IconFileText" hugeicons="FileIcon" phosphor="FileTextIcon" remixicon="RiFileTextLine" /></Attachment.Media> <Attachment.Content> <Attachment.Title>mandate.pdf</Attachment.Title> <Attachment.Description>PDF, 540 KB</Attachment.Description> </Attachment.Content> <Attachment.Actions><IconPlaceholder lucide="CheckIcon" tabler="IconCheck" hugeicons="Tick02Icon" phosphor="CheckIcon" remixicon="RiCheckLine" class="size-4 text-success-text" /></Attachment.Actions> </Attachment.Root></div>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/attachmentpnpm dlx shadcn@latest add @edmi-ui/attachmentyarn dlx shadcn@latest add @edmi-ui/attachmentbunx shadcn@latest add @edmi-ui/attachmentPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @base-ui/react class-variance-authorityTerminal window pnpm add @base-ui/react class-variance-authorityTerminal window yarn add @base-ui/react class-variance-authorityTerminal window bun add @base-ui/react class-variance-authorityAdd the Edmi components this one builds on:
button.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/attachmentpnpm dlx shadcn-vue@latest add @edmi-ui/attachmentyarn dlx shadcn-vue@latest add @edmi-ui/attachmentbunx shadcn-vue@latest add @edmi-ui/attachmentPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install reka-ui class-variance-authorityTerminal window pnpm add reka-ui class-variance-authorityTerminal window yarn add reka-ui class-variance-authorityTerminal window bun add reka-ui class-variance-authorityAdd the Edmi components this one builds on:
button.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/attachment.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/attachment.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/attachment.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/attachment.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
button.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentMedia, AttachmentTitle,} from "@/components/ui/attachment";import { Spinner } from "@/components/ui/spinner";
<div className="flex flex-wrap items-start gap-3"> <Attachment> <AttachmentMedia> <FileTextIcon /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>thesis.pdf</AttachmentTitle> <AttachmentDescription>PDF, 2.4 MB</AttachmentDescription> </AttachmentContent> <AttachmentActions> <AttachmentAction aria-label="Remove"> <XIcon /> </AttachmentAction> </AttachmentActions> </Attachment> <Attachment state="uploading"> <AttachmentMedia> <Spinner /> </AttachmentMedia> <AttachmentContent> <AttachmentTitle>weights.csv</AttachmentTitle> <AttachmentDescription>Uploading 64%</AttachmentDescription> </AttachmentContent> </Attachment> {/* … */}</div><script setup lang="ts">import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentMedia, AttachmentTitle,} from "@/components/ui/attachment";import { Spinner } from "@/components/ui/spinner";</script>
<template> <div class="flex flex-wrap items-start gap-3"> <Attachment> <AttachmentMedia><FileTextIcon /></AttachmentMedia> <AttachmentContent> <AttachmentTitle>thesis.pdf</AttachmentTitle> <AttachmentDescription>PDF, 2.4 MB</AttachmentDescription> </AttachmentContent> <AttachmentActions> <AttachmentAction aria-label="Remove"><XIcon /></AttachmentAction> </AttachmentActions> </Attachment> <Attachment state="uploading"> <AttachmentMedia><Spinner /></AttachmentMedia> <AttachmentContent> <AttachmentTitle>weights.csv</AttachmentTitle> <AttachmentDescription>Uploading 64%</AttachmentDescription> </AttachmentContent> </Attachment> <Attachment state="processing"> <AttachmentMedia><Spinner /></AttachmentMedia> <AttachmentContent> <AttachmentTitle>report.pdf</AttachmentTitle> <AttachmentDescription>Processing</AttachmentDescription> </AttachmentContent> </Attachment> <!-- … --> </div></template><script lang="ts"> import * as Attachment from "$lib/components/ui/attachment"; import { Spinner } from "$lib/components/ui/spinner";</script>
<div class="flex flex-wrap items-start gap-3"> <Attachment.Root> <Attachment.Media><FileTextIcon /></Attachment.Media> <Attachment.Content> <Attachment.Title>thesis.pdf</Attachment.Title> <Attachment.Description>PDF, 2.4 MB</Attachment.Description> </Attachment.Content> <Attachment.Actions> <Attachment.Action aria-label="Remove"><XIcon /></Attachment.Action> </Attachment.Actions> </Attachment.Root> <Attachment.Root state="uploading"> <Attachment.Media><Spinner /></Attachment.Media> <Attachment.Content> <Attachment.Title>weights.csv</Attachment.Title> <Attachment.Description>Uploading 64%</Attachment.Description> </Attachment.Content> </Attachment.Root> <Attachment.Root state="processing"> <Attachment.Media><Spinner /></Attachment.Media> <Attachment.Content> <Attachment.Title>report.pdf</Attachment.Title> <Attachment.Description>Processing</Attachment.Description> </Attachment.Content> </Attachment.Root> <!-- … --></div>Component source
Section titled “Component source”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 type * as React from "react";
import { Button } from "@/registry/edmi/ui/button";
const attachmentVariants = cva( "group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-xl border border-border bg-card text-card-foreground transition-colors focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring has-[>a,>button]:hover:bg-accent data-[state=error]:border-[color-mix(in_srgb,var(--destructive)_30%,var(--popover))] 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-[color-mix(in_srgb,var(--destructive)_10%,var(--card))] 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:bg-linear-to-r group-data-[state=processing]/attachment:from-muted-foreground group-data-[state=processing]/attachment:via-foreground group-data-[state=processing]/attachment:to-muted-foreground group-data-[state=processing]/attachment:bg-clip-text group-data-[state=processing]/attachment:text-transparent group-data-[state=processing]/attachment:animate-pulse group-data-[state=uploading]/attachment:bg-linear-to-r group-data-[state=uploading]/attachment:from-muted-foreground group-data-[state=uploading]/attachment:via-foreground group-data-[state=uploading]/attachment:to-muted-foreground group-data-[state=uploading]/attachment:bg-clip-text group-data-[state=uploading]/attachment:text-transparent group-data-[state=uploading]/attachment:animate-pulse", 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 snap-x snap-mandatory scroll-px-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden gap-3 overflow-x-auto overscroll-x-contain py-1 *:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start", className, )} {...props} /> );}
export { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentGroup, AttachmentMedia, AttachmentTitle, AttachmentTrigger,};<script setup lang="ts">import type { HTMLAttributes } from 'vue'import type { AttachmentVariants } from '.'import { cn } from '@/registry/edmi/lib/utils'import { attachmentVariants } from '.'
const props = withDefaults(defineProps<{ class?: HTMLAttributes['class'] state?: 'idle' | 'uploading' | 'processing' | 'error' | 'done' size?: AttachmentVariants['size'] orientation?: AttachmentVariants['orientation']}>(), { state: 'done', size: 'default', orientation: 'horizontal',})</script>
<template> <div data-slot="attachment" :data-state="state" :data-size="size" :data-orientation="orientation" :class="cn(attachmentVariants({ size, orientation }), props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import type { ButtonVariants } from '@/registry/edmi/ui/button'import { cn } from '@/registry/edmi/lib/utils'import { Button } from '@/registry/edmi/ui/button'
const props = withDefaults(defineProps<{ class?: HTMLAttributes['class'] variant?: ButtonVariants['variant'] size?: ButtonVariants['size']}>(), { variant: 'ghost', size: 'icon-xs',})</script>
<template> <Button data-slot="attachment-action" :variant="variant" :size="size" :class="cn(props.class)" > <slot /> </Button></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <div data-slot="attachment-actions" :class="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', props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <div data-slot="attachment-content" :class="cn('max-w-full min-w-0 flex-1 leading-tight group-data-[orientation=vertical]/attachment:px-1', props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <span data-slot="attachment-description" :class="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', props.class)" > <slot /> </span></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <div data-slot="attachment-group" :class="cn('flex min-w-0 snap-x snap-mandatory scroll-px-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden gap-3 overflow-x-auto overscroll-x-contain py-1 *:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start', props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import type { AttachmentMediaVariants } from '.'import { cn } from '@/registry/edmi/lib/utils'import { attachmentMediaVariants } from '.'
const props = withDefaults(defineProps<{ class?: HTMLAttributes['class'] variant?: AttachmentMediaVariants['variant']}>(), { variant: 'icon',})</script>
<template> <div data-slot="attachment-media" :data-variant="variant" :class="cn(attachmentMediaVariants({ variant }), props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <span data-slot="attachment-title" :class="cn('block max-w-full min-w-0 truncate font-medium group-data-[state=processing]/attachment:bg-linear-to-r group-data-[state=processing]/attachment:from-muted-foreground group-data-[state=processing]/attachment:via-foreground group-data-[state=processing]/attachment:to-muted-foreground group-data-[state=processing]/attachment:bg-clip-text group-data-[state=processing]/attachment:text-transparent group-data-[state=processing]/attachment:animate-pulse group-data-[state=uploading]/attachment:bg-linear-to-r group-data-[state=uploading]/attachment:from-muted-foreground group-data-[state=uploading]/attachment:via-foreground group-data-[state=uploading]/attachment:to-muted-foreground group-data-[state=uploading]/attachment:bg-clip-text group-data-[state=uploading]/attachment:text-transparent group-data-[state=uploading]/attachment:animate-pulse', props.class)" > <slot /> </span></template><script setup lang="ts">import type { PrimitiveProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { Primitive } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes['class']}>(), { as: 'button',})</script>
<template> <Primitive data-slot="attachment-trigger" :as="as" :as-child="asChild" :type="as === 'button' ? 'button' : undefined" :class="cn('absolute inset-0 z-10 outline-none', props.class)" > <slot /> </Primitive></template>import type { VariantProps } from "class-variance-authority";import { cva } from "class-variance-authority";
export { default as Attachment } from "./Attachment.vue";export { default as AttachmentAction } from "./AttachmentAction.vue";export { default as AttachmentActions } from "./AttachmentActions.vue";export { default as AttachmentContent } from "./AttachmentContent.vue";export { default as AttachmentDescription } from "./AttachmentDescription.vue";export { default as AttachmentGroup } from "./AttachmentGroup.vue";export { default as AttachmentMedia } from "./AttachmentMedia.vue";export { default as AttachmentTitle } from "./AttachmentTitle.vue";export { default as AttachmentTrigger } from "./AttachmentTrigger.vue";
export const attachmentVariants = cva( "group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-xl border border-border bg-card text-card-foreground transition-colors focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring has-[>a,>button]:hover:bg-accent data-[state=error]:border-[color-mix(in_srgb,var(--destructive)_30%,var(--popover))] 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", }, }, },);export type AttachmentVariants = VariantProps<typeof attachmentVariants>;
export 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-[color-mix(in_srgb,var(--destructive)_10%,var(--card))] 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", }, },);export type AttachmentMediaVariants = VariantProps< typeof attachmentMediaVariants>;<script lang="ts"> import { Button, type ButtonProps } from "$lib/registry/ui/button/index.js"; import { cn } from "$lib/utils.js";
let { ref = $bindable(null), class: className, variant = "ghost", size = "icon-xs", ...restProps }: ButtonProps = $props();</script>
<Button bind:ref data-slot="attachment-action" {variant} {size} class={cn(className)} {...restProps}/><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="attachment-actions" class={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 )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="attachment-content" class={cn( "max-w-full min-w-0 flex-1 leading-tight group-data-[orientation=vertical]/attachment:px-1", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();</script>
<span bind:this={ref} data-slot="attachment-description" class={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 )} {...restProps}> {@render children?.()}</span><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="attachment-group" class={cn( "flex min-w-0 snap-x snap-mandatory scroll-px-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden gap-3 overflow-x-auto overscroll-x-contain py-1 *:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start", className )} {...restProps}> {@render children?.()}</div><script lang="ts" module> import { tv, type VariantProps } from "tailwind-variants";
export const attachmentMediaVariants = tv({ base: "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-[color-mix(in_srgb,var(--destructive)_10%,var(--card))] 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", }, });
export type AttachmentMediaVariant = VariantProps<typeof attachmentMediaVariants>["variant"];</script>
<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, variant = "icon", ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { variant?: AttachmentMediaVariant } = $props();</script>
<div bind:this={ref} data-slot="attachment-media" data-variant={variant} class={cn(attachmentMediaVariants({ variant }), className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();</script>
<span bind:this={ref} data-slot="attachment-title" class={cn( "block max-w-full min-w-0 truncate font-medium group-data-[state=processing]/attachment:bg-linear-to-r group-data-[state=processing]/attachment:from-muted-foreground group-data-[state=processing]/attachment:via-foreground group-data-[state=processing]/attachment:to-muted-foreground group-data-[state=processing]/attachment:bg-clip-text group-data-[state=processing]/attachment:text-transparent group-data-[state=processing]/attachment:animate-pulse group-data-[state=uploading]/attachment:bg-linear-to-r group-data-[state=uploading]/attachment:from-muted-foreground group-data-[state=uploading]/attachment:via-foreground group-data-[state=uploading]/attachment:to-muted-foreground group-data-[state=uploading]/attachment:bg-clip-text group-data-[state=uploading]/attachment:text-transparent group-data-[state=uploading]/attachment:animate-pulse", className )} {...restProps}> {@render children?.()}</span><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLButtonAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, child, children, type = "button", ...restProps }: WithElementRef<HTMLButtonAttributes> & { child?: Snippet<[{ props: Record<string, unknown> }]>; } = $props();
const mergedProps = $derived({ class: cn("absolute inset-0 z-10 outline-none", className), "data-slot": "attachment-trigger", ...restProps, });</script>
{#if child} {@render child({ props: mergedProps })}{:else} <button bind:this={ref} {type} {...mergedProps}> {@render children?.()} </button>{/if}<script lang="ts" module> import { tv, type VariantProps } from "tailwind-variants";
export const attachmentVariants = tv({ base: "group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-xl border border-border bg-card text-card-foreground transition-colors focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring has-[>a,>button]:hover:bg-accent data-[state=error]:border-[color-mix(in_srgb,var(--destructive)_30%,var(--popover))] 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", }, }, defaultVariants: { size: "default", orientation: "horizontal", }, });
export type AttachmentSize = VariantProps<typeof attachmentVariants>["size"]; export type AttachmentOrientation = VariantProps<typeof attachmentVariants>["orientation"]; export type AttachmentState = "idle" | "uploading" | "processing" | "error" | "done";</script>
<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, state = "done", size = "default", orientation = "horizontal", ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { state?: AttachmentState; size?: AttachmentSize; orientation?: AttachmentOrientation; } = $props();</script>
<div bind:this={ref} data-slot="attachment" data-state={state} data-size={size} data-orientation={orientation} class={cn(attachmentVariants({ size, orientation }), className)} {...restProps}> {@render children?.()}</div>import Root from "./attachment.svelte";import Action from "./attachment-action.svelte";import Actions from "./attachment-actions.svelte";import Content from "./attachment-content.svelte";import Description from "./attachment-description.svelte";import Group from "./attachment-group.svelte";import Media from "./attachment-media.svelte";import Title from "./attachment-title.svelte";import Trigger from "./attachment-trigger.svelte";
export { Action, Action as AttachmentAction, Actions, Actions as AttachmentActions, Content, Content as AttachmentContent, Description, Description as AttachmentDescription, Group, Group as AttachmentGroup, Media, Media as AttachmentMedia, Root, // Root as Attachment, Title, Title as AttachmentTitle, Trigger, Trigger as AttachmentTrigger,};| Prop | Type | Default |
|---|---|---|
state |
idle | uploading | processing | error | done |
done |
size |
default | sm | xs |
default |
orientation |
horizontal | vertical |
horizontal |
AttachmentMedia variant |
icon | image |
icon |
Parts: Attachment, AttachmentGroup, AttachmentMedia, AttachmentContent, AttachmentTitle, AttachmentDescription, AttachmentActions, AttachmentAction, AttachmentTrigger.
✦ Edmi additions
Section titled “✦ Edmi additions”- Uploading and processing titles shimmer (gradient text); idle is dashed, error uses the destructive tint; hard 1px lip.