mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-24 19:32:10 +00:00
30fa9fc3dd
The inline `pad-attachment:` image NodeView opened its own hand-rolled `<dialog>`: `openImageLightbox` appended one to `document.body` and `showModal()`d it. It worked, and everything the modal contract is made of came free from the platform — top layer, inertness, focus trap, focus restore, Escape. This commit deletes it and emits on the viewer channel instead, so the `Lightbox` that `AttachmentViewerHost` mounts is the only viewer on this route, with the lease-stacked backdrop, the escape ordering and the DR-16 filter re-applied over the whole set. Shaped as a pure swap so the deletion is reviewable on its own. THE MIME IS RESOLVED BEFORE ANYTHING IS EMITTED, revising the decomposition's "keep today's positively-known gate". TASK-2431 made `Lightbox` fail closed on an unresolved MIME, so an event carrying `mime_type: null` is not "let the viewer decide" — it is a viewer that mounts and renders no image. Activation now awaits `fetchAttachmentMetadata` (a cache hit in the common case) and emits only on a positively-known allowlisted answer. That also closes a mid-phase bypass: the old gate read `knownMime` only when truthy, so a click landing before the lazy probe resolved opened the original file, and a later unsafe answer did not close it. What it costs, deliberately and temporarily, is that a `transient` probe now opens nothing — the four-branch matrix that makes the gate total is TASK-2434's. AND THE CHANNEL ENFORCES IT, because a rule only one producer follows is a convention, not an invariant — and the failure mode is silent precisely because the viewer fails closed. `notifyViewerOpen` now takes a set whose `mime_type` is non-nullable (`ViewerReadyImage` / `ViewerOpenRequest`), so a forgetful producer is a compile error rather than an image that does not open, and it refuses a set at runtime unless every entry is positively allowlisted, the way it already no-ops on a missing address. The WHOLE emission is dropped rather than the offending entry: `index` and `attachmentId` name a position in the set the producer built, and silently renumbering it would open the viewer on a different image than the one activated. `LightboxImage` is untouched — the consumer side stays nullable, because its records are live and `size_bytes` / `width` / `height` must remain optional for 3b. `events.test.ts` asserted the permissive behaviour; it now asserts the refusal, with a control so a gate that refused everything cannot pass. The await is new, so the fences are too: the gate's premises are re-checked on the far side (teardown, a uuid swap, a deletion, and a host that moved — the comment composer's address is live across an item switch), and one activation at a time. That latch is generation-stamped: it is released by the resolution's own finalizer AND by a uuid swap, which this NodeView outlives, so an unconditional release would let a superseded request unlock the one that replaced it, and a HEAD that never settles would otherwise leave the new image permanently unopenable. The `.attachment-image-lightbox` CSS goes with it; it lived in `app.css`, not the TS file, where a JS-only sweep would have left it. Tests assert the emitted PAYLOAD, not the dialog's absence — an implementation that deleted the dialog and emitted nothing satisfies "no dialog" — and a new spec drives the WHOLE route with nothing stubbed but the network: real NodeView, real bus, real `AttachmentViewerHost`, real `Lightbox`, asserting a viewer in the document, addressed to the right host, showing the un-variant original. Verified by mutation: emit-nothing, emit-unresolved-MIME, drop-the-allowlist-check at either the producer or the channel, gate the set on its first entry only, drop the one-at-a-time latch or release it unconditionally, drop any post-await fence, null the invoker, and append an overlay to `documentElement` each fail at least one test. Two of those needed cases the existing specs could not reach: every prior DR-16 test selected the node first, which pre-resolves the MIME through `canActivate()` and leaves the activation path's own check unexercised — precisely the state a body image is in when it is clicked. Claude-Session: https://claude.ai/code/session_01LmbFxQFDjcYKBLcTnor6DC
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