mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 21:39:01 +00:00
b76abdd66b
* feat(web): full ARIA-modal isolation for the mobile detail pane (TASK-2131) Follow-up to TASK-2122. The mobile full-screen detail-pane overlay had a JS focus trap + an inert list column, but the app-shell chrome behind it stayed in the a11y tree and the pane was still just an <aside>. Complete the modal: - The mobile `.item-pane` becomes role="dialog" aria-modal="true"; the desktop split stays a bare <aside> (complementary landmark, non-modal). - MobileContextBar + BottomNav (rendered in the workspace +layout, ABOVE the pane host) are marked `inert` while a mobile overlay is up, so they leave the focus order AND the screen-reader tree. A JS trap can't constrain an SR virtual cursor and aria-modal is unevenly honored, so the background chrome must physically drop out. The chrome is a layout sibling the host can't reach by prop, so PaneHost hoists "a mobile overlay is active" into a small ref-counted store (paneOverlay.svelte.ts) the layout reads — one-way writer/reader split per CONVE-1688. Ref-counted so an overlapping route-change remount can't clear the signal early. The layout carries `inert` on display:contents wrappers (cascades to the fixed chrome, adds no box). Verified in a real browser (Playwright): mobile → dialog role + aria-modal + inert descendants unfocusable; desktop → bare aside + chrome interactive. Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra * fix(web): exclude the pane's dialog role from foreign-modal guards (TASK-2131) The new mobile role="dialog" on the pane collided with three places that treat ANY [role="dialog"] as a *foreign* modal that owns its own ESC / focus — regressions the pane's <aside>-not-a-dialog invariant had been silently relying on: - paneFocus.ts PANE_EXEMPT_SURFACE_SELECTOR: any in-pane element matched closest('[role="dialog"]') → the whole pane read as an "exempt surface", killing the mobile Tab trap and confusing the focus-follows classifier. - The collection + item-page ESC guards querySelector('[role="dialog"]') → the pane matched itself → ESC was swallowed instead of closing/popping the pane on mobile. Fix: exclude the pane via [role="dialog"]:not(.item-pane) at all three sites (a genuinely nested dialog/menu opened FROM the pane still matches). Adds inExemptSurface unit coverage for the pane-not-exempt case. Caught by the independent Codex review pass. Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra * fix(web): complete mobile modal isolation — banners + print (TASK-2131) Two more members of the same [role="dialog"] collision class, from the independent Codex pass: - Print: app.css @media print hides [role="dialog"] to strip overlays. The mobile pane now matches, so printing at <=768px with the pane open dropped the whole item from the printout. Exclude via :not(.item-pane) — the pane is the content being printed, not a transient overlay. - Banners: VerifyEmailBanner + ConnectBanner rendered OUTSIDE the inert wrappers, so their controls (Resend / Connect) stayed reachable by an SR virtual cursor behind the aria-modal pane — the same gap the inert of MobileContextBar/BottomNav closes. Fold them into the top inert wrapper so ALL app-shell siblings behind the overlay leave the a11y tree; only the pane (in children()) stays interactive. 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