mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-24 19:32:10 +00:00
b8909befdf
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.
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