Files
pad/web
xarmian 4e81480d0a fix(editor): block-drag global listeners defer to a frontmost viewer (BUG-2453) (#1373)
`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
2026-09-14 17:15:07 -04: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
    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