Files
pad/web
xarmian 316e25a6ca fix(web): trap focus in BottomSheet so ESC/Tab hit the sheet, not the layer under it (BUG-2130) (#1006)
* 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
2026-07-22 17:32:31 -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

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