Saltar al contenido principal

Attachment

Muestra archivos como filas, tarjetas o chips con preview, tamaño, acciones y progreso de subida.

  • q3-board-deck.pdf

    4.6 MB

  • hero-final.png

    1.2 MB

  • source-assets.zip

    55.6 MB

1'use client';
2
3import { Attachment } from '@/components/ui/attachment';
4
5const files = [
6 { name: 'q3-board-deck.pdf', type: 'application/pdf', size: 4_820_000 },
7 { name: 'hero-final.png', type: 'image/png', size: 1_240_000 },
8 { name: 'source-assets.zip', type: 'application/zip', size: 58_300_000 },
9];
10
11export function Default() {
12 return (
13 <Attachment.List className="w-full max-w-sm">
14 {files.map((file) => (
15 <Attachment.Item key={file.name}>
16 <Attachment>
17 <Attachment.Preview type={file.type} name={file.name} />
18 <Attachment.Content>
19 <Attachment.Name>{file.name}</Attachment.Name>
20 <Attachment.Meta>
21 <Attachment.Size bytes={file.size} />
22 </Attachment.Meta>
23 </Attachment.Content>
24 <Attachment.Actions>
25 <Attachment.Download href="#" />
26 <Attachment.Remove />
27 </Attachment.Actions>
28 </Attachment>
29 </Attachment.Item>
30 ))}
31 </Attachment.List>
32 );
33}

Instalación

pnpm dlx nachui add attachment

Anatomía

1import { Attachment } from '@/components/ui/attachment';
1<Attachment>
2 <Attachment.Preview type="application/pdf" name="deck.pdf" />
3 <Attachment.Content>
4 <Attachment.Name>deck.pdf</Attachment.Name>
5 <Attachment.Meta>
6 <Attachment.Size bytes={4820000} />
7 </Attachment.Meta>
8 </Attachment.Content>
9 <Attachment.Actions>
10 <Attachment.Download href="/files/deck.pdf" />
11 <Attachment.Remove onClick={remove} />
12 </Attachment.Actions>
13</Attachment>
Attachment solo muestra. No sabe cómo llegó el archivo, así que sirve para una cola de subida, un compositor de mensajes, un hilo de comentarios o una lista de archivos que ya están en el servidor. Combínalo con File Upload cuando necesites el selector y la validación.

Composición

Attachment.List
└── Attachment.Item
└── Attachment
├── Attachment.Preview
├── Attachment.Content
│ ├── Attachment.Name
│ └── Attachment.Meta
│ └── Attachment.Size
├── Attachment.Actions
│ ├── Attachment.Download
│ └── Attachment.Remove
└── Attachment.Progress

Variantes

Filas

La variante row por defecto, una lista apilada con ícono por tipo, nombre, tamaño y acciones.
  • q3-board-deck.pdf

    4.6 MB

  • hero-final.png

    1.2 MB

  • source-assets.zip

    55.6 MB

1'use client';
2
3import { Attachment } from '@/components/ui/attachment';
4
5const files = [
6 { name: 'q3-board-deck.pdf', type: 'application/pdf', size: 4_820_000 },
7 { name: 'hero-final.png', type: 'image/png', size: 1_240_000 },
8 { name: 'source-assets.zip', type: 'application/zip', size: 58_300_000 },
9];
10
11export function Default() {
12 return (
13 <Attachment.List className="w-full max-w-sm">
14 {files.map((file) => (
15 <Attachment.Item key={file.name}>
16 <Attachment>
17 <Attachment.Preview type={file.type} name={file.name} />
18 <Attachment.Content>
19 <Attachment.Name>{file.name}</Attachment.Name>
20 <Attachment.Meta>
21 <Attachment.Size bytes={file.size} />
22 </Attachment.Meta>
23 </Attachment.Content>
24 <Attachment.Actions>
25 <Attachment.Download href="#" />
26 <Attachment.Remove />
27 </Attachment.Actions>
28 </Attachment>
29 </Attachment.Item>
30 ))}
31 </Attachment.List>
32 );
33}

Tarjetas

variant="card" para imágenes. El preview ocupa la parte superior y las acciones aparecen al pasar el cursor o al enfocar.
  • office-lobby.jpg

    office-lobby.jpg

    2 MB

  • team-offsite.jpg

    team-offsite.jpg

    2.9 MB

  • launch-day.jpg

    launch-day.jpg

    1.8 MB

1'use client';
2
3import { Attachment } from '@/components/ui/attachment';
4
5const photos = [
6 { name: 'office-lobby.jpg', size: 2_140_000, src: 'https://picsum.photos/seed/lobby/320/320' },
7 { name: 'team-offsite.jpg', size: 3_020_000, src: 'https://picsum.photos/seed/offsite/320/320' },
8 { name: 'launch-day.jpg', size: 1_870_000, src: 'https://picsum.photos/seed/launch/320/320' },
9];
10
11export function Cards() {
12 return (
13 <Attachment.List layout="grid">
14 {photos.map((photo) => (
15 <Attachment.Item key={photo.name}>
16 <Attachment variant="card">
17 <Attachment.Preview src={photo.src} name={photo.name} />
18 <Attachment.Content>
19 <Attachment.Name>{photo.name}</Attachment.Name>
20 <Attachment.Meta>
21 <Attachment.Size bytes={photo.size} />
22 </Attachment.Meta>
23 </Attachment.Content>
24 <Attachment.Actions>
25 <Attachment.Remove />
26 </Attachment.Actions>
27 </Attachment>
28 </Attachment.Item>
29 ))}
30 </Attachment.List>
31 );
32}

Chips

variant="chip" es una píldora compacta para compositores y cajas de comentarios.
  • checkout-spec.md

    18 KB
  • payment-flow.png

    392.6 KB
  • stripe-webhook.log

    93.8 KB
1'use client';
2
3import { Attachment01Icon, SentIcon } from '@hugeicons/core-free-icons';
4import { HugeiconsIcon } from '@hugeicons/react';
5import * as React from 'react';
6import { Attachment } from '@/components/ui/attachment';
7import { Button } from '@/components/ui/button';
8import { Textarea } from '@/components/ui/textarea';
9
10const initial = [
11 { id: 'spec', name: 'checkout-spec.md', type: 'text/markdown', size: 18_400 },
12 { id: 'flow', name: 'payment-flow.png', type: 'image/png', size: 402_000 },
13 { id: 'log', name: 'stripe-webhook.log', type: 'text/plain', size: 96_000 },
14];
15
16export function Chips() {
17 const [files, setFiles] = React.useState(initial);
18
19 return (
20 <div className="border-border bg-card flex w-full max-w-md flex-col gap-3 rounded-xl border p-3">
21 <Textarea
22 aria-label="Message"
23 placeholder="Write a message to the payments team"
24 autoResize
25 maxRows={5}
26 className="border-0 bg-transparent px-1 shadow-none focus-visible:ring-0 dark:bg-transparent"
27 />
28 {files.length > 0 && (
29 <Attachment.List layout="inline">
30 {files.map((file) => (
31 <Attachment.Item key={file.id}>
32 <Attachment variant="chip">
33 <Attachment.Preview type={file.type} name={file.name} />
34 <Attachment.Content>
35 <Attachment.Name>{file.name}</Attachment.Name>
36 <Attachment.Size bytes={file.size} />
37 </Attachment.Content>
38 <Attachment.Remove
39 onClick={() => setFiles((current) => current.filter((f) => f.id !== file.id))}
40 />
41 </Attachment>
42 </Attachment.Item>
43 ))}
44 </Attachment.List>
45 )}
46 <div className="flex items-center justify-between">
47 <Button type="button" variant="ghost" size="sm">
48 <HugeiconsIcon icon={Attachment01Icon} size={16} />
49 Attach
50 </Button>
51 <Button type="button" size="sm">
52 Send
53 <HugeiconsIcon icon={SentIcon} size={16} />
54 </Button>
55 </div>
56 </div>
57 );
58}

Subiendo

status define el aspecto: uploading atenúa la fila, error la delinea. Progress dibuja una barra en el borde inferior.
  • customer-export.csv

    0% of 12 MB

  • onboarding.mp4

    0% of 141.1 MB

  • contract-v3.pdf

    Upload failed, try again

1'use client';
2
3import * as React from 'react';
4import { Attachment } from '@/components/ui/attachment';
5
6const uploads = [
7 { name: 'customer-export.csv', type: 'text/csv', size: 12_600_000, speed: 9 },
8 { name: 'onboarding.mp4', type: 'video/mp4', size: 148_000_000, speed: 3 },
9 { name: 'contract-v3.pdf', type: 'application/pdf', size: 820_000, speed: 0, failed: true },
10];
11
12export function Uploading() {
13 const [progress, setProgress] = React.useState(() => uploads.map(() => 0));
14
15 React.useEffect(() => {
16 const timer = setInterval(() => {
17 setProgress((current) =>
18 current.map((value, index) => {
19 const upload = uploads[index];
20 if (!upload || upload.failed) return value;
21 return Math.min(value + upload.speed, 100);
22 }),
23 );
24 }, 300);
25 return () => clearInterval(timer);
26 }, []);
27
28 return (
29 <Attachment.List className="w-full max-w-sm">
30 {uploads.map((upload, index) => {
31 const value = progress[index] ?? 0;
32 const status = upload.failed ? 'error' : value >= 100 ? 'done' : 'uploading';
33
34 return (
35 <Attachment.Item key={upload.name}>
36 <Attachment status={status}>
37 <Attachment.Preview type={upload.type} name={upload.name} />
38 <Attachment.Content>
39 <Attachment.Name>{upload.name}</Attachment.Name>
40 <Attachment.Meta>
41 {status === 'error' && (
42 <span className="text-destructive">Upload failed, try again</span>
43 )}
44 {status === 'uploading' && (
45 <>
46 {Math.round(value)}% of <Attachment.Size bytes={upload.size} />
47 </>
48 )}
49 {status === 'done' && <Attachment.Size bytes={upload.size} />}
50 </Attachment.Meta>
51 </Attachment.Content>
52 <Attachment.Actions>
53 <Attachment.Remove
54 aria-label={status === 'uploading' ? 'Cancel upload' : 'Remove'}
55 />
56 </Attachment.Actions>
57 {status === 'uploading' && <Attachment.Progress value={value} />}
58 </Attachment>
59 </Attachment.Item>
60 );
61 })}
62 </Attachment.List>
63 );
64}

Helpers

formatFileSize(bytes, decimals?) convierte una cantidad de bytes en 1.5 MB. Attachment.Size lo llama por ti.

Referencia de API

Attachment

PropTipoPor defectoDescripción
variant"row" | "card" | "chip""row"Layout de las partes
status"idle" | "uploading" | "done" | "error""idle"Se expone como data-status y da estilo
classNamestring-Clases CSS adicionales

Attachment.Preview

PropTipoPor defectoDescripción
srcstring-URL de imagen. Renderiza una miniatura si está
typestring-Tipo MIME, elige el ícono
namestring-Nombre del archivo, usado para el ícono y el alt
altstring-Texto alternativo de la miniatura
childrenReactNode-Reemplaza la miniatura o el ícono por completo

Attachment.Size

PropTipoPor defectoDescripción
bytesnumber-Tamaño a formatear
decimalsnumber1Decimales a conservar

Attachment.Progress

PropTipoPor defectoDescripción
valuenumber-Porcentaje completado, 0 a 100

Attachment.List

PropTipoPor defectoDescripción
layout"stack" | "grid" | "inline""stack"Cómo fluyen los elementos

Attachment.Item, Attachment.Content, Attachment.Name, Attachment.Meta, Attachment.Actions, Attachment.Remove, Attachment.Download

Partes de presentación. Cada una acepta className y los atributos del elemento que renderiza. Remove es un botón y Download un ancla con el atributo download. Ambos traen un aria-label en inglés que puedes sobrescribir.
¿Encontraste algo que mejorar?

¿Notaste un error, tipografía o detalle faltante en esta página? Ayúdanos a mejorar la documentación abriendo un issue en GitHub.

Crear un Issue
Ctrl+I