Files
pad/web/src/lib
xarmian b8909befdf feat(attachments): an options panel for files (TASK-2423)
Tapping a file used to do the most destructive-adjacent thing available:
a strip tile was a bare `<a download>`, so one tap put the file in your
Downloads folder with no way to see what it was first. This is what a tap
opens instead — `AttachmentDetailsPanel`, plus the one host that owns it.

PLAN-2392 phase 2, wave B. Nothing routes into the panel yet; the strip's
tiles and the editor's chips start emitting the open event in TASK-2424,
so the panel is driven here through its host and the events bus.

Presentation is the existing `Menu` with `sheetOnMobile` — a popover on
desktop, a BottomSheet at the mobile breakpoint (DR-6). No new overlay
primitive, so ESC ordering, outside-click, placement and the sheet's focus
handling are the app's existing ones rather than second implementations.

The actions are NOT defined here: they are rendered from the shared
descriptor list (DR-5), choosing between MenuItem's anchor and button
branches on the descriptor's own `element` discriminant and never calling
`run()` on an anchor. Adding an action stays a one-descriptor change.

It opens IMMEDIATELY and completes the metadata after (DR-2, DR-10). The
event's filename / mime / size are nullable by contract, so the panel
paints what it was handed and fetches the rest itself: `ok` fills the gaps,
`missing` (404) latches an authoritative "no longer available" with every
action inert, and `transient` shows an inline error beside the row it
already knows, with a Retry that goes through `revalidateAttachmentMetadata`
— a plain refetch would replay the cached failure and look broken.

Delete is an in-app drill-down sub-view (DR-18), the item menu's shape
exactly: prompt as `role="presentation"` with an aria-describedby
back-reference, Cancel FIRST, destructive row last, and the strip's
contextual "still used in this item's content" warning carried through
(read at confirm time from the LIVE editor markdown, since the persisted
body lags). It is wired as the delete descriptor's `confirmDelete` promise
rather than as a bespoke path, so the descriptor's identity snapshot and
permission re-check across the confirmation stay in force. The unreferenced
arm stays hedged: this can only speak for the HOST's content, and the
event's `itemId` is routing, not ownership.

The host is `ItemDetail`, through a small `AttachmentPanelHost` it mounts
beside the strip. It consumes an event only when BOTH `itemId` and
`hostToken` are its own (DR-8), and supplies `mutationsEnabled` itself —
never the NodeView's (it has no mutation context) and never the timeline's
`canEdit` (which ignores `peeking` and would let a peeked pane mutate). The
host is a component rather than a block inside ItemDetail because the
addressing rule has to be testable with two hosts mounted at once, which is
what the pane host does at runtime.

Parent lifecycle (DR-14): an archived parent's attachment fetch returns a
generic 404, so archive CLOSES the panel and restore REVALIDATES it rather
than assuming the previous state holds. The strip sits outside ItemDetail's
keyed lifecycle block, so this is added, not inherited; it arrives
declaratively as `parentArchived`, following the item ItemDetail already
refetches on the SSE lifecycle events.

Long filenames and RTL are handled with logical properties throughout,
`min-width: 0` on every flex child holding the name, and the full unelided
filename in both `title` and the panel's accessible name (DR-13).

No `state_generation` and no Undo (DR-19) — Delete behaves exactly like
today's tile Delete; PLAN-2411 adds the generation token and the Undo toast
to all three entry points at once.

Also here:
- `describeAttachmentType` in the shared display helpers, built on
  `iconForAttachment` so the words and the icon beside them cannot disagree
  about what a file is.
- `liveEditorMarkdown` extracted in ItemDetail — the strip and the panel now
  read the live body through one accessor instead of two copies.

Tested through the host (20 jsdom cases): addressing with two hosts mounted,
open-with-partial-then-complete, all three metadata arms, Retry's
invalidate-before-refetch, host-supplied permission for peeked vs master,
the full confirm/cancel/failure delete paths, both warning arms, archive-
closes / restore-revalidates, item switch, and re-targeting in place. Focus
entry and return, background inertness, real placement, the sheet swap and
Enter/Space activation are browser-only and belong to phase 3d.
2026-08-04 02:44:57 +00:00
..
2026-03-26 01:52:36 +00:00