Files
pad/web
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
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00

Pad Web UI

SvelteKit 2 + Svelte 5 frontend for Pad, compiled to static files and embedded into the Go binary.

Development

npm install
npm run dev          # Dev server at localhost:5173 (proxies API to localhost:7777)
npm run build        # Production build to build/
npm run check        # Type checking with svelte-check

When developing, run the Go backend separately with make dev from the project root.

Building for Production

Do not build in isolation. Always use make build from the project root — this builds the web frontend, then compiles the Go binary with the build output embedded via //go:embed.

Stack

  • Svelte 5 with runes ($state, $derived, $effect)
  • SvelteKit 2 with adapter-static (SPA mode)
  • Tiptap block editor with markdown round-trip
  • svelte-dnd-action for drag-and-drop in board/list views
  • SSE for real-time updates
  • TypeScript throughout

Structure

src/
  routes/                    SvelteKit pages
    +layout.svelte           App shell (sidebar + main)
    +page.svelte             Landing/redirect
    [workspace]/
      +page.svelte           Dashboard (collections, phases, activity)
      +layout.svelte         SSE connection per workspace
      [collection]/
        +page.svelte         Collection view (board/list)
      [collection]/[item]/
        +page.svelte         Item detail + editor
      conventions/            Purpose-built conventions page
      playbooks/              Purpose-built playbooks page
      settings/               Workspace settings
  lib/
    api/client.ts            HTTP API client
    components/
      layout/                Sidebar, navigation
      editor/                Tiptap editor, raw markdown editor
      fields/                FieldEditor, relation picker
      items/                 ItemCard, ItemDetail
      collections/           BoardView, ListView
      common/                StatusBadge, badges, modals
      search/                CommandPalette
      activity/              ActivityFeed
    stores/                  Svelte 5 reactive stores
      workspace.svelte.ts    Workspace state
      collections.svelte.ts  Collection + item state
      ui.svelte.ts           Sidebar, mobile state
    types/index.ts           TypeScript types and constants
  app.css                    Global styles and design tokens