mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-25 03:42:06 +00:00
df741172fe
`isBlockedByModal()` answers "is a viewer lease held RIGHT NOW". `Lightbox`'s
escape-stack handler calls `onClose()` synchronously, and Svelte flushes the
teardown — hence `lease.release()` — inside that call. Every `window` keydown
listener later in the SAME dispatch therefore asked against an empty stack, was
told "nothing is in front of you", and closed a second layer: one Escape closed
the viewer AND the `DockedSheet` / `BottomSheet` underneath it. The query was
right; the moment it was read was not.
Consumption is now recorded per EVENT. The viewer marks the dispatch it
consumed (`noteEscapeConsumedByViewer`) before closing, and `isBlockedByModal`
takes an optional event: a marked one blocks every later owner outright,
whatever the lease says by then. `runTopEscape(event)` forwards the driving
event to handlers so the viewer has something to mark; the stack itself never
reads it.
Keyed on the event object, NOT on `defaultPrevented` — that flag says only
"somebody handled this key", is set by controls that are not viewers, and
honouring it would change sheet behaviour with no viewer present. This marker
can only ever be set by a frontmost viewer, so on an empty lease stack it is
unreachable by construction.
DELIBERATE, NAMED BEHAVIOUR CHANGE — the FOURTH named parity exception of
PLAN-2392 phase 3a, alongside the three already recorded. `DockedSheet`,
`BottomSheet` and the `TopBar` overflow menu now decline an Escape a viewer has
already consumed. TASK-2430 shipped `DockedSheet` declining an already-
`defaultPrevented` Escape unannounced and it was reverted; this is approximately
that change made deliberately, with a stated reason, a narrower trigger and
tests. `TopBar` is not known to be broken today — its listener happens to run
before the route driver — but that is mount-order luck, not a guarantee, so it
is closed too.
EMPTY-STACK PARITY, per owner: with no viewer, the marker cannot exist, so each
touched call site reduces to exactly its previous expression. Asserted rather
than argued — `DockedSheet` and `BottomSheet` each gain an unmarked-Escape
regression beside the new blocked case, `TopBar`'s existing owner-5 e2e covers
both directions, and a `viewerBackdrop` unit test states the equivalence
directly (`isBlockedByModal(o, unmarked) === isBlockedByModal(o)`). The reverted
2430 `defaultPrevented` regression test still passes untouched.
The two `test.fail()` cases pinning BUG-2441 are now real assertions, each
extended with a second press proving the sheet keeps its own Escape rather than
going permanently deaf.
MUTATION-VERIFIED, both halves (TASK-2436's precedent):
• drop the viewer's mark → owners 3 and 4 fail: "the sheet is a LOWER layer
and must survive the press / element(s) not found", plus the new Lightbox
jsdom case ("expected spy to not be called at all, but actually been
called 1 times").
• drop the sheets' event argument → the same two e2e cases fail identically.
• drop the driver's `runTopEscape(e)` → the wiring contract fails
("expected … to match /runTopEscape\s*\(\s*e\s*\)/").
Gates: npm run check 0 errors; npm run test 1090 passed; the three viewer e2e
specs 32 passed.
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