mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 21:39:01 +00:00
316e25a6ca
* fix(web): trap focus in BottomSheet so ESC/Tab hit the sheet, not the layer under it (BUG-2130) BottomSheet is a role="dialog" aria-modal mobile sheet but, unlike the native-<dialog> Modal.svelte, it never moved focus into itself or trapped Tab. Two consequences, app-wide (most visible over the mobile split-pane): - ESC closed the wrong layer: focus stayed on the trigger outside the sheet, so a window-level ESC handler underneath (e.g. the collection page's pane-close) fired first and closed THAT instead of the sheet. - Tab escaped the sheet into the obscured content behind it. Fix in the shared component, mirroring Modal.svelte's behavior: - Move focus onto the panel (tabindex=-1) on open; restore focus to the trigger on close and on teardown-while-open. - Trap Tab/Shift+Tab within the sheet, reusing the pane's already-tested trap math (paneFocusables + nextTrapTarget from paneFocus.ts) so the two focus traps can't drift. The focus effect reads only `open`/`sheetEl` and writes the non-reactive `previouslyFocused`, so it can't self-invalidate (CONVE-1688). Surgical over a native-<dialog> rebuild: 11 consumers make the blast radius large, and the bug is scoped to the shared component. Converging BottomSheet onto the Modal primitive is a separate, larger refactor. Adds BottomSheet.svelte.test.ts (focus-in, Tab/Shift+Tab wrap, Escape, backdrop, focus-restore). Verified: full web suite (471) green, svelte-check clean, Codex CLEAN, and a real mobile-browser drive (focus-in, Tab + Shift+Tab trapped, ESC closes only the sheet with the item pane surviving, focus restored to the trigger). Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra * fix(web): only the frontmost BottomSheet handles Escape/Tab (nested sheets) Codex PR review caught an adjacent facet of the same layer-isolation bug: every open BottomSheet registers a window-level Escape/Tab handler, so when one sheet opens another (Quick Actions sheet → the mobile emoji picker's sheet, both role="dialog" BottomSheets, the inner DOM-nested in the outer), a single Escape fired both handlers and closed BOTH layers. Gate each sheet's handler on being the frontmost (innermost) open sheet: a nested child sheet renders inside our content, so a sheet that CONTAINS another open `.bs-sheet` is not frontmost and stays out. Order-independent by design — a defaultPrevented/stopPropagation check can't work here because the outer sheet's window listener is registered first and fires before the inner's. Verified at runtime (mobile): open Quick Actions → New quick action → the emoji-picker button opens a nested sheet; one Escape now closes only the picker (Quick Actions survives), a second closes Quick Actions. Adds a nested-sheet unit test. Full web suite 472 green, svelte-check clean. Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra * fix(web): generalize BottomSheet frontmost gate to sibling sheets too Follow-up to the nested-sheet fix: replace the descendant-only guard with a document-wide frontmost check so the "only the topmost sheet handles Escape/Tab" rule also holds for sibling sheets (two open overlays where neither DOM-contains the other). A sheet that contains a deeper open sheet is never frontmost; among the remaining leaf sheets the last in document order paints on top at the shared z-index, so it wins. Recomputed per keydown, so order-independent. Two full-screen overlays can't both be reached by the user today (opening one covers every other trigger), so this hardens a currently-unreachable topology rather than fixing a live repro — but it makes the invariant total and closes the Codex review's remaining finding. The single-sheet path short-circuits to frontmost=true, so the verified primary behavior is unchanged (re-verified at runtime: single-sheet focus-in/trap/Escape/restore + nested one-layer-per-Esc both still green). Adds a sibling-topology unit test. Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra
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