Files
pad/web
xarmian e4c2ff0a03 fix(web): simplify BottomSheet to fix broken mobile interactions (#161)
* fix(web): simplify BottomSheet to fix broken mobile interactions

The original BottomSheet layered on several advanced behaviors — portal
to body, module-level $state open-stack, focus trap, swipe-to-dismiss,
reactive z-index, stacked-sheet Escape routing — and something in that
stack broke click dispatch on Android Chromium for every button inside
the sheet except the existing quick-action rows.

Root cause investigation: every click handler I wrote in this session
failed to fire on Android (close button, backdrop, footer rows, even
an unrelated debug banner's dismiss), while the existing shipped UI
(CreateCollectionModal etc.) continued to work fine. That narrowed
the problem to something structural in the new components rather than
any specific CSS / event wiring.

Fix: rewrite BottomSheet as a ~100-line clone of the working
CreateCollectionModal pattern — plain {#if open} + overlay +
stopPropagation on the inner panel, nothing more. Mobile-first
CSS docks the sheet to the bottom of the viewport; a single
@media (min-width: 640px) rule centers it as a traditional modal on
desktop. No portal, no module-scope $state, no <svelte:window>, no
focus trap, no swipe gesture.

Tradeoffs deliberately accepted for now:
- Swipe-to-dismiss is gone. Backdrop tap + close button are the
  dismissal paths; the viewport-scoped overlay makes this fine.
- No focus trap. Every other modal in the app already ships without
  one, so this matches existing behavior.
- No stacked-sheet Escape prioritization. Single-sheet usage only.

Can be re-layered carefully later if any of those features are
actually needed, but only one feature at a time with mobile testing
between each.

* fix(web): restore Escape dismissal + ARIA dialog semantics on BottomSheet

Addresses both P2 comments from Codex on PR #161.

- Escape key closes the sheet. Added a svelte:window onkeydown that
  early-returns when !open, matching the pattern used elsewhere in
  the app. This is the keyboard dismissal path for desktop and users
  with hardware keyboards on mobile — and the only keyboard path
  when title is omitted (no close button rendered).
- Restored role=\"dialog\", aria-modal=\"true\", and aria-labelledby
  (pointing at the visible title heading when one is set, falling
  back to aria-label=\"Dialog\" otherwise). Without these, assistive
  tech wouldn't announce modal context and users could continue
  navigating background content.

Stable per-instance heading id uses \$props.id() (SSR-safe), bound to
a top-level const per the Svelte 5 placement rule.

Notably NOT reintroduced: focus trap, portal, module-scope stack,
swipe gesture, reactive z-index. Those were the culprits for the
Android click-dispatch regression and stay out of the simplified
implementation.
2026-04-20 12:27:52 -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