mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-23 11:03:41 +00:00
e187573792
* feat(web): add reusable BottomSheet primitive (TASK-627) Introduce $lib/components/common/BottomSheet.svelte — a controlled bottom-sheet / modal component built on Svelte 5 runes with no new runtime dependencies. Features: - Mobile (< 640px): docked to bottom, full-width, rounded top corners, swipe-down-to-dismiss via pointer events (80px threshold). - Desktop (>= 640px): configurable via `desktopMode` prop — 'sheet' (default, bottom-anchored with max-width) or 'centered' (traditional centered dialog mirroring the existing .overlay/.modal pattern). - Escape key, backdrop tap, and swipe-down all trigger onclose(). - Focus trap while open; restores focus on close to the previously focused element. - Body scroll lock while open, safely restored on close/unmount. - Svelte `fly` + `fade` transitions; honors prefers-reduced-motion. - role="dialog", aria-modal="true", optional `title` wired to aria-labelledby. No consumers yet — foundation for [[IDEA-493]] (quick-actions mobile fix and inline New/Manage affordances will consume this in TASK-628 and TASK-629). Parent: IDEA-493. * fix(web): harden BottomSheet focus trap + scroll lock per Codex review - Focus trap: forward Tab now also pulls focus back when active is outside the sheet (assistive tech / programmatic focus change), mirroring the Shift+Tab branch. Without this, focus escaping the sheet broke modal isolation on subsequent Tab presses. - Scroll lock: moved to module-level counter + shared prev-overflow via acquireScrollLock(). Stacked sheets no longer clobber each other — the original body overflow is captured on the first open and restored only when the last sheet closes. Addresses both P2 comments on PR #158. * fix(web): topmost-only Escape + hydration-safe IDs in BottomSheet - Escape / Tab trap now gated to the topmost open sheet only. Added a module-level open-sheet stack (symbol tokens) so stacked instances can identify which one should handle global keyboard events. One Escape keypress no longer closes every open sheet at once. - Replaced Math.random() heading ID with $props.id() (Svelte 5.20+) so the aria-labelledby target is stable across SSR and hydration. Addresses P1 (stacked Escape) and P2 (SSR hydration mismatch) from Codex review round 2 on PR #158. * fix(web): tie BottomSheet z-index to open-stack position Convert the module-level openStack to a Svelte 5 $state array so each instance can reactively read its position in the stack. Compute backdrop + sheet z-index from that position (BASE_Z 61, two slots per stack level) and apply via inline style, replacing the fixed CSS z-index values. This keeps visual stacking aligned with the keyboard-topmost logic (pushOpenSheet / isTopmostSheet) — if a sheet renders earlier in the DOM but opens later, its visual layer now matches its logical topmost role instead of being obscured by an older DOM sibling. Addresses Codex round 3 P2 on PR #158. * fix(web): skip BottomSheet focus restore when another sheet is open In stacked-sheet scenarios, closing a non-topmost sheet would still run previouslyFocused.focus() in the focus-management effect cleanup, yanking focus out of the active dialog and onto background UI. Gate the restoration on: 1. openStack contains no tokens other than this instance's token (no other sheet is still open), AND 2. document.activeElement is not already inside a different role="dialog" ancestor. If either check fails, skip the restore — another sheet is still in control of focus. Addresses Codex round 4 P2 on PR #158. * fix(web): refine BottomSheet focus restore for stacked topmost close Round 4's blanket skip-when-others-open was too aggressive. When the topmost sheet closes while another sheet remains behind it, the previouslyFocused target typically lives inside that remaining sheet (it was the active element when this sheet opened) — restoring it is correct and keeps focus inside the remaining modal. New rule: - If no other sheets open → always restore (normal case). - If others are still open → restore only when previouslyFocused lives inside a DIFFERENT still-open dialog (not this closing one). Otherwise skip, so we don't yank focus onto background UI. Addresses Codex round 5 P2 on PR #158.
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