Files
pad/web
xarmian e187573792 feat(web): add reusable BottomSheet primitive (TASK-627) (#158)
* 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.
2026-04-20 07:35:23 -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
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
      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