XI Conversation

Star on GitHubFeedback
XI ConversationNative HTMLot-attach

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 Attachment

Example

  • Halden brief, second round.pdf 2.4 MB
  • Stamp sizes.png 3.6 MB
  • Timetable grid.svg 0.8 MB
  • Ferry flag, one colour.eps 1.3 of 5.2 MB

With the second round of marks, as promised.

  • Halden brief, second round.pdf 2.4 MB
  • Stamp sizes.png 3.6 MB
Waiting
  • Stamps.png Waiting
Uploading
  • Stamps.png 1.4 of 3.6 MB
Checking
  • Stamps.png Checking
Done
  • Stamps.png 3.6 MB
Reverse, uploading
  • Stamps.png 1.4 of 3.6 MB
Reverse, checking
  • Stamps.png Checking
Reverse, done
  • Stamps.png 3.6 MB
Reverse, failed
  • Marks, final.ai The connection dropped.
Enclosure
  • Brief.pdf 2.4 MB
  • Stamps.png 1.4 of 3.6 MB
Failed
  • Marks, final.ai The connection dropped.
Translated enclosure
  • Brief.pdf 2.4 MB

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.json

Usage

attachment-example.tsx
"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), and svg.ot-attach-ring (.ot-attach-track, .ot-attach-arc, .ot-attach-fill). Script sets --p (0–1). Enclosure sets data-count, data-enclosure-label from enclosureLabel (default "Encl.") and aria-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. removeLabel gives 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 --p grows, 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 is CanvasText with the letters in Canvas.
  • 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

Props of Attachment
PropTypeDefaultWhat it does
namestringThe file name. Its extension is set wide and thin as the picture.
sizestringThe size as it should read, in tabular figures, such as 2.4 MB.
statusstringSays 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.
progressnumberHow far an upload has got, 0 to 1.
onRemove() => voidShows a bracket Remove button labelled with the file's name.
removeLabelstring"Remove"The Remove button's word. Its accessible name adds the file name after this label.
AttachmentListul propsThe list the files sit in.
AttachmentList enclosureLabelstring"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

Where its move comes from
ComponentReferenceMove
Attachment"the uncreative"The extension is the picture
Reverse attachment"the uncreative"The extension reversed out of ink, printed across as it arrives
Enclosure attachmentWeingart letter; the letter's enclosure line"Encl. (2)", the files run on in one sentence

How it moves

How it moves
ComponentArticulationWhat moves
AttachmentCount inThe ring fills, closes, and gives way to a full stop that lands
Reverse attachmentPrintThe ink block runs across the extension as the file arrives