Attachment
A file's extension is its picture; a ring counts it in and fills to a dot when it's safe.
Report an issue with AttachmentExample
With the second round of marks, as promised.
Waiting
Uploading
Checking
Done
Reverse, uploading
Reverse, checking
Reverse, done
Reverse, failed
Enclosure
Failed
Translated enclosure
Install
Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base and button along.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/attachment.jsonUsage
"use client"import * as React from "react"import { Attachment, AttachmentList } from "@/components/ui/attachment"import { Button } from "@/components/ui/button"export default function Example() {const [files, setFiles] = React.useState([{ name: "Halden brief, second round.pdf", size: "2.4 MB" },{ name: "Stamp sizes.png", size: "3.6 MB" },])// ponytail: a pretend upload; each press sends another quarter of the file.const [sent, setSent] = React.useState(0.25)const done = sent >= 1return (<div className="grid gap-(--ot-space-8)"><AttachmentList className="max-w-[40rem]">{files.map((f) => (<Attachment key={f.name} {...f} onRemove={() => setFiles((all) => all.filter((x) => x !== f))} />))}</AttachmentList><div className="grid max-w-[40rem] justify-items-start gap-(--ot-space-5)"><AttachmentList variant="reverse" className="w-full"><Attachment name="Timetable grid.svg" size="0.8 MB" /><Attachmentname="Ferry flag, one colour.eps"state={done ? "done" : "uploading"}progress={Math.min(sent, 1)}status={done ? "5.2 MB" : `${(5.2 * sent).toFixed(1)} of 5.2 MB`}/></AttachmentList><Button variant="bracket" disabled={done} onClick={() => setSent((p) => p + 0.25)}>Send the next part</Button></div><div className="grid max-w-[40rem] gap-(--ot-space-3)"><p>With the second round of marks, as promised.</p><AttachmentList variant="enclosure"><Attachment name="Halden brief, second round.pdf" size="2.4 MB" /><Attachment name="Stamp sizes.png" size="3.6 MB" /></AttachmentList></div></div>)}Contract
- Underneath: native.
- Anatomy:
<ul class="ot-attachments" data-variant="reverse | enclosure">of<li class="ot-attach" data-state="idle | uploading | processing | done | error">holding.ot-attach-ext(data-ext),.ot-attach-body(.ot-attach-name,.ot-attach-meta), andsvg.ot-attach-ring(.ot-attach-track,.ot-attach-arc,.ot-attach-fill). Script sets--p(0–1). Enclosure setsdata-count,data-enclosure-labelfromenclosureLabel(default"Encl.") andaria-label="Enclosures"(overridable through the native prop). - A file's extension is its picture, set wide and thin. A ring counts it in and goes pencil while it's checked; when it's safe the ring closes and gives way to a full stop (
--ot-dot), which lands with spiccato: a ring round a dot would read as a chosen radio. Failed, the ring turns crimson and the line says what happened, with a bracket Remove. Narrow (below 30rem), Remove steps down under the name so the name keeps the width.removeLabelgives the button its word (default"Remove"); its accessible name is that word followed by the file name. - Reverse, after "the uncreative", where part of a word is reversed out of an ink block. No ring: the extension is set heavy and tight, and a block (
.ot-attach-ext::after, the same letters in paper) prints across it as--pgrows, so each letter turns from ink on paper to paper on ink as the file arrives. Checking, the block is pencil; safe, it is ink; failed, it stops in crimson where the connection dropped. Right to left it prints from the right. Forced colours: the block isCanvasTextwith the letters inCanvas. - Enclosure, after the business letter's enclosure line (the Weingart letter). The list is one sentence: "Encl. (2)", then each file's name, yours in italic, its size or status in parentheses, a semicolon between and a full stop at the end. The extension and a safe file's ring are dropped; a small ring stays inline while a file is on its way. Meant for what has been sent, under a message. The name and parenthesised status wrap to the available measure, including long unbroken names; no content is hidden to fit.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
name | string | The file name. Its extension is set wide and thin as the picture. | |
size | string | The size as it should read, in tabular figures, such as 2.4 MB. | |
status | string | Says what is happening or what went wrong in place of the size. | |
state | "idle" | "uploading" | "processing" | "done" | "error" | "done" | The ring's state: it turns while processing, fills to a dot when done, and turns crimson on error. |
progress | number | How far an upload has got, 0 to 1. | |
onRemove | () => void | Shows a bracket Remove button labelled with the file's name. | |
removeLabel | string | "Remove" | The Remove button's word. Its accessible name adds the file name after this label. |
AttachmentList | ul props | The list the files sit in. | |
AttachmentList enclosureLabel | string | "Encl." | The word before the enclosure count, read by CSS from data-enclosure-label. Give aria-label on the list to translate its accessible name (Enclosures by default). |
AttachmentList variant | "default" | "reverse" | "enclosure" | "default" | reverse: no ring; the extension is reversed out of an ink block that prints across its letters as progress grows, pencil while checking, ink when safe, crimson where a failed upload stopped. enclosure: a letter's enclosure line, "Encl. (2)" then each file run on in one sentence, its size in parentheses; a ring shows only while a file is on its way. The list is labelled Enclosures. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Attachment | "the uncreative" | The extension is the picture |
| Reverse attachment | "the uncreative" | The extension reversed out of ink, printed across as it arrives |
| Enclosure attachment | Weingart letter; the letter's enclosure line | "Encl. (2)", the files run on in one sentence |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Attachment | Count in | The ring fills, closes, and gives way to a full stop that lands |
| Reverse attachment | The ink block runs across the extension as the file arrives |