mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-25 22:10:25 +00:00
4e81480d0a
`block-drag-handle.ts` installs four WINDOW-level listeners and none of them asked whether anything was in front of the editor. Background inertness stops NEW interaction with the editor; it does not stop a gesture that already owns the pointer. So a block drag in flight when the attachment viewer opened kept running against an editor that was `inert` and invisible behind the viewer's backdrop — and the drop still landed. This is the deferred half of TASK-2430, which taught seven global owners to stand down and closed with "Deferred: svelte-dnd-action + editor block-drag global gestures." FOUR OWNERS, NOT THE TWO THE BUG NAMES. It names the mouse pair; `touchmove` and `touchend` are installed on the same line with the identical hole, so a mouse-only fix would have left every touch device unguarded. Each handler consults the shared arbitration helper — `isBlockedByModal`, never a raw lease query — with `editorView.dom` as the owner, the surface asking to act rather than `event.target`, which during a straddling gesture is whatever the pointer happens to be over. ABORT, NOT EARLY-RETURN, the choice `ItemGraph.abortPan` already makes for its captured pan. An early return leaves `dragging` latched, the block at 20% opacity, the ghost under the cursor and `pointerEvents: none` on the editor — a half-drag frozen onto a surface that is now behind a viewer, with nothing left to deliver the event that would end it. The teardown reuses `cancelDrag`, which already existed and had NO callers; unlike `endDrag` it never calls `executeMove`, so aborting cannot commit the reorder. The gates sit above the branch structure, not inside the `dragging` arm: `mouseup` and `touchend` each also open the block menu on a press that never moved, and a guard placed in the drag arm would still pop a menu onto the inert editor beneath the viewer. `isBlockedByModal` returns false on an empty lease stack, so with no viewer open every path is untouched. Each of the four owners has an inert-while-frontmost leg and the contract's empty-stack regression, and the empty-stack legs assert what ships TODAY — the trap TASK-2430 shipped and reverted twice, where a test labelled a regression actually specified a change. Codex round 2 closed two more, both in the abort's own teardown: `abortDrag` left `activeBlock` populated where `endDrag` nulls it, and the comment beside it claimed to clear two pending flags when `cancelDrag` already owned one of them. The first is consistency rather than a defect closed — no path was found that acts on a stale `activeBlock` — and the comment now says so instead of implying a bug was fixed. Six mutants, each killed by its own legs: removing any of the four gates; replacing the abort with a bare early return; and pointing the owner at a viewer-contained node. The reorder itself is proven suppressed, on both `mouseup` and `touchend`, by legs that drag the SECOND block. That detail is load-bearing: jsdom gives every element a zero `getBoundingClientRect`, so `dropPosAtY` scores all candidates alike and keeps the first — for a drag of block 1 that is the slot the node is already in, so the drop runs and changes nothing and a doc-equality assertion passes either way. An earlier revision of this change asserted exactly that and was green with the gate deleted. Dragging block 2 makes the first candidate position 0, the paragraphs swap, and the assertion has something to see; each reorder leg is paired with an empty-stack leg proving the drag still reorders with no viewer, so a future harness break cannot turn it back into a vacuous pass. Claude-Session: https://claude.ai/code/session_019EmDd3Wy8b3cPG1TZBSzBu
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
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