GESTURES: native macOS/iOS gestures everywhere — audit + one shared gesture layer #399

Closed
opened 2026-09-29 04:41:45 +00:00 by kayg · 10 comments
Owner

Request (owner, 2026-09-29)

"I also want a gesture review across the UI in the sense that I notice that gestures (trackpad gestures, mobile swipes) work only in certain parts of the UI. I really want our web app to feel like a native macOS / iOS app."

Deliverable 1: gesture audit (docs/research/gesture-audit.md)

For every mode and surface (Calendar views, Notes, Files list/grid, Photos timeline and viewer, Search, Settings sheets and sections, previews/popovers, the composer, menus, sidebars, Mail when it lands), record which of these work today, which should work (following the macOS and iOS HIG and the native apps' behaviour: Calendar, Files/Finder, Photos, Notes, Mail), and which are missing or inconsistent:

  • Trackpad (macOS): two-finger horizontal swipe for back/forward in navigation and between days/weeks/months in Calendar; pinch to zoom (Photos timeline density, Calendar week/day zoom, image viewer); two-finger swipe on list rows for swipe actions; force-click/long-press preview; momentum and rubber-band overscroll; smooth scroll snapping.
  • Touch (iOS): edge-swipe back in every pushed view (Settings sub-sections, note → list, file → folder); swipe down to dismiss sheets and previews (with the spring from #291); swipe actions on rows (leading/trailing: e.g. complete task, delete, copy link, per DESIGN deep-link rule); horizontal swipe between days/weeks/months; pinch in Photos and the image viewer; long-press context menus with haptic-style feedback where the platform allows; pull to refresh only where it means something.
  • Keyboard equivalents for every gesture (accessibility), and reduced-motion behaviour.
  • Conflicts: browser back/forward swipe vs in-app swipes (use overscroll-behavior and history integration so the browser gesture and the app agree), text selection vs swipe, vertical scroll vs horizontal swipe thresholds.

Deliverable 2: one shared gesture layer

One primitive in packages/ui (pointer events, velocity-based thresholds, rubber band, the #291 spring, cancellation) used by every surface; no per-screen gesture code. Then implement the missing gestures from the audit in priority order: navigation back/forward, sheet dismiss, day/week/month swipes, row swipe actions, pinch zoom.

Proof

Playwright touch and trackpad-wheel emulation tests per gesture; frame strips for the dismiss/swipe animations; and Claude checks the feel on the macOS VM (trackpad through the cua-driver) and in iOS Safari simulation. Production-build evidence at 390/820/1440.

## Request (owner, 2026-09-29) "I also want a gesture review across the UI in the sense that I notice that gestures (trackpad gestures, mobile swipes) work only in certain parts of the UI. I really want our web app to feel like a native macOS / iOS app." ## Deliverable 1: gesture audit (docs/research/gesture-audit.md) For every mode and surface (Calendar views, Notes, Files list/grid, Photos timeline and viewer, Search, Settings sheets and sections, previews/popovers, the composer, menus, sidebars, Mail when it lands), record which of these work today, which should work (following the macOS and iOS HIG and the native apps' behaviour: Calendar, Files/Finder, Photos, Notes, Mail), and which are missing or inconsistent: - **Trackpad (macOS):** two-finger horizontal swipe for back/forward in navigation and between days/weeks/months in Calendar; pinch to zoom (Photos timeline density, Calendar week/day zoom, image viewer); two-finger swipe on list rows for swipe actions; force-click/long-press preview; momentum and rubber-band overscroll; smooth scroll snapping. - **Touch (iOS):** edge-swipe back in every pushed view (Settings sub-sections, note → list, file → folder); swipe down to dismiss sheets and previews (with the spring from #291); swipe actions on rows (leading/trailing: e.g. complete task, delete, copy link, per DESIGN deep-link rule); horizontal swipe between days/weeks/months; pinch in Photos and the image viewer; long-press context menus with haptic-style feedback where the platform allows; pull to refresh only where it means something. - Keyboard equivalents for every gesture (accessibility), and reduced-motion behaviour. - Conflicts: browser back/forward swipe vs in-app swipes (use `overscroll-behavior` and history integration so the browser gesture and the app agree), text selection vs swipe, vertical scroll vs horizontal swipe thresholds. ## Deliverable 2: one shared gesture layer One primitive in packages/ui (pointer events, velocity-based thresholds, rubber band, the #291 spring, cancellation) used by every surface; no per-screen gesture code. Then implement the missing gestures from the audit in priority order: navigation back/forward, sheet dismiss, day/week/month swipes, row swipe actions, pinch zoom. ## Proof Playwright touch and trackpad-wheel emulation tests per gesture; frame strips for the dismiss/swipe animations; and Claude checks the feel on the macOS VM (trackpad through the cua-driver) and in iOS Safari simulation. Production-build evidence at 390/820/1440.
Author
Owner

Started on job/gestures at base 00015e652a. I am auditing current gesture behavior and browser constraints before implementing the shared layer.

Started on job/gestures at base 00015e652a153485d9e7adda1c5ee61bc17300db. I am auditing current gesture behavior and browser constraints before implementing the shared layer.
Author
Owner

Audit finding at e19f3c66: FileCollection and QuickLook already use the app-local swipe action; OverlaySurface and the phone sidebar already use the #291 spring. Day/Week and Photos already own continuous scroll or pinch. The missing Calendar gap is discrete paging in Month/Year/Agenda; browser history must remain route-based. The audit records these owners and input conflicts in docs/research/gesture-audit.md.

Audit finding at e19f3c66: FileCollection and QuickLook already use the app-local swipe action; OverlaySurface and the phone sidebar already use the #291 spring. Day/Week and Photos already own continuous scroll or pinch. The missing Calendar gap is discrete paging in Month/Year/Agenda; browser history must remain route-based. The audit records these owners and input conflicts in docs/research/gesture-audit.md.
Author
Owner

Implemented shared swipe ownership at 4abf116c and Calendar Month/Year/Agenda paging at f5881f59. Finding: root phone routes need the existing 20 px sidebar edge; pushed routes need back. At 03054c66 the edge uses browser history for an in-app predecessor and a stable parent for cold deep links. Component tests cover touch axis ownership, wheel bursts, control exclusion, and parent resolution.

Implemented shared swipe ownership at 4abf116c and Calendar Month/Year/Agenda paging at f5881f59. Finding: root phone routes need the existing 20 px sidebar edge; pushed routes need back. At 03054c66 the edge uses browser history for an in-app predecessor and a stable parent for cold deep links. Component tests cover touch axis ownership, wheel bursts, control exclusion, and parent resolution.
Author
Owner

Finding at c5a72117: the new Calendar page swipe target guard excluded text fields but still allowed a gesture starting on a button or link. The guard now excludes interactive controls and nested gesture owners. Focused gesture tests pass (5/5), and web type checking reports 0 errors and 0 warnings. A production Playwright touch/wheel probe is being added for Month, Year and Agenda at 390/820/1440 px in both themes.

Finding at c5a72117: the new Calendar page swipe target guard excluded text fields but still allowed a gesture starting on a button or link. The guard now excludes interactive controls and nested gesture owners. Focused gesture tests pass (5/5), and web type checking reports 0 errors and 0 warnings. A production Playwright touch/wheel probe is being added for Month, Year and Agenda at 390/820/1440 px in both themes.
Author
Owner

Finding at 3a7a9e4f: the shared swipe action replaced its tracked pointer when a second touch began. This could commit a row or page swipe during a pinch. It now cancels the pending swipe and waits for all fingers to lift before re-arming. A regression test covers the two-finger sequence and a subsequent fresh swipe; the focused suite passes 6/6.

Finding at 3a7a9e4f: the shared swipe action replaced its tracked pointer when a second touch began. This could commit a row or page swipe during a pinch. It now cancels the pending swipe and waits for all fingers to lift before re-arming. A regression test covers the two-finger sequence and a subsequent fresh swipe; the focused suite passes 6/6.
Author
Owner

Production browser findings after the dev merge:

  • Calendar Year: a second horizontal wheel packet could advance to 2028 after the first packet navigated to 2027. The route change remounted the gesture action, losing its wheel burst state. Commit f8121a13 retains a burst boundary by owner key across remounts; the Calendar pager uses a 700 ms boundary for trackpad inertia. The browser probe now sends consecutive packets and confirms one page change.
  • Settings cold deep link: the 20 px edge-back zone was behind the pushed Settings sheet, so touch never reached it. Commit 74468044 lifts that strip above the overlay surface. The production probe confirms the strip receives the touch and returns to /settings.
  • Photos density: Chromium's generic synthesized pinch touched the timeline day header in a one-photo fixture. The test now places both touch points inside the tile. The real timeline changed from Medium to Large, and Ctrl-wheel restored Medium.

The gesture-only production probe passes. The full run produced 54 screenshots across 390/820/1440 px and light/dark. Final gates and artifact upload are in progress.

Production browser findings after the `dev` merge: - Calendar Year: a second horizontal wheel packet could advance to 2028 after the first packet navigated to 2027. The route change remounted the gesture action, losing its wheel burst state. Commit `f8121a13` retains a burst boundary by owner key across remounts; the Calendar pager uses a 700 ms boundary for trackpad inertia. The browser probe now sends consecutive packets and confirms one page change. - Settings cold deep link: the 20 px edge-back zone was behind the pushed Settings sheet, so touch never reached it. Commit `74468044` lifts that strip above the overlay surface. The production probe confirms the strip receives the touch and returns to `/settings`. - Photos density: Chromium's generic synthesized pinch touched the timeline day header in a one-photo fixture. The test now places both touch points inside the tile. The real timeline changed from Medium to Large, and Ctrl-wheel restored Medium. The gesture-only production probe passes. The full run produced 54 screenshots across 390/820/1440 px and light/dark. Final gates and artifact upload are in progress.
Author
Owner

Production review artifacts are attached (not committed):

The full production gesture probe passed against the merged worktree server. Web check: svelte-check found 0 errors and 0 warnings. Web tests: Test Files 124 passed (124), Tests 797 passed (797). Rust gates and the adversarial round are still running.

Production review artifacts are attached (not committed): - [54 screenshots at 390/820/1440 px in light and dark, plus seven spring frames](https://git.kayg.org/attachments/f3dc1dcf-919c-46e1-989c-9530b8b24ac1) - [Sheet edge-drag and spring-release frame strip](https://git.kayg.org/attachments/0a88a4b2-d46c-44d2-91c7-17cdef5d8c11) The full production gesture probe passed against the merged worktree server. Web check: `svelte-check found 0 errors and 0 warnings`. Web tests: `Test Files 124 passed (124)`, `Tests 797 passed (797)`. Rust gates and the adversarial round are still running.
Author
Owner

Direct screenshot previews for review: Calendar Month at 390 px, light, Files at 820 px, dark, Photos at 1440 px, light. The earlier ZIP contains all 54 viewport/theme captures.

Direct screenshot previews for review: [Calendar Month at 390 px, light](https://git.kayg.org/attachments/f18c9d7e-82d1-4223-a0af-da5d8ce3f454), [Files at 820 px, dark](https://git.kayg.org/attachments/8ca7cf9f-def9-4bd1-a75f-93b671102533), [Photos at 1440 px, light](https://git.kayg.org/attachments/81ed2fdb-d6c0-428d-b387-78e4bbcc93f8). The earlier ZIP contains all 54 viewport/theme captures.
Author
Owner

Rust gate status on the shared build host: cargo fmt --check passed (exit 0, empty output). The full cargo clippy --all-targets -- -D warnings run was terminated with exit 143 while checking dependencies; its last lines were:

    Checking calternal-path v0.0.1 (/home/kayg/Developer/calternal-wt/gestures/crates/calternal-path)
    Checking hyper v1.11.1
   Compiling rand_chacha v0.3.1

It reported no diagnostic before termination. The queued full cargo test has acquired the target lock and is compiling. I will post its result or its remaining state in the final report.

Rust gate status on the shared build host: `cargo fmt --check` passed (exit 0, empty output). The full `cargo clippy --all-targets -- -D warnings` run was terminated with exit 143 while checking dependencies; its last lines were: ``` Checking calternal-path v0.0.1 (/home/kayg/Developer/calternal-wt/gestures/crates/calternal-path) Checking hyper v1.11.1 Compiling rand_chacha v0.3.1 ``` It reported no diagnostic before termination. The queued full `cargo test` has acquired the target lock and is compiling. I will post its result or its remaining state in the final report.
Author
Owner

#399 gesture job report

Branch: job/gestures (merged dev once before final gates). Head: 2ec58efd. No push, deploy, or merge into dev.

Built

  • Audited gestures across the shell, Calendar, Notes, Files, Photos, Search, Settings, previews, menus, composer, and future Mail in docs/research/gesture-audit.md with Apple HIG and browser references.
  • Moved the shared swipe/long-press/pull/sheet decision layer to packages/ui/src/gestures.ts; added wheel normalization, axis ownership, cross-remount wheel burst suppression, and common pinch geometry. Kept keyboard controls and reduced-motion behavior in the owning surfaces.
  • Added Calendar Month/Year/Agenda touch and wheel paging while preserving Day/Week native grid scroll; added pushed-view edge back, shared sheet dismissal decisions, Files row trackpad actions, and shared pinch metrics in Calendar, Photos, and ImageView.
  • Added production browser probes, 54 width/theme screenshots, and a seven-frame sidebar spring strip. The production gesture probe passed.

Primary files: packages/ui/src/gestures.ts, packages/ui/src/components/{OverlaySurface.svelte,calendar/TimeGrid.svelte,files/FileCollection.svelte,viewer/ImageView.svelte}, apps/web/src/lib/{gestures.ts,navigation.ts,components/app-sidebar.svelte,photos/PhotoTimeline.svelte}, apps/web/src/routes/calendar/[view]/[date]/+page.svelte, apps/web/e2e/{gestures-399.mjs,gesture-frames-399.mjs}, docs/research/gesture-audit.md and related tests.

Evidence

Gate output (verbatim summaries)

cargo fmt --check: exit 0, no output.

bun run check:

$ node scripts/check-type-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
Text sizes use shared role tokens.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/gestures/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

bun run test:

 Test Files  124 passed (124)
      Tests  797 passed (797)
   Start at  09:25:11
   Duration  237.97s (transform 61%, environment 17%, import 11%, tests 8%, setup 3%)

Production gesture probe:

PASS edge back
PASS Files wheel
PASS Files touch
PASS viewer pinch
PASS gesture touch/wheel paging, edge back, Files row swipe, Photos and viewer pinch, and screenshots at 390/820/1440 in both themes: /home/kayg/Developer/calternal-wt/gestures/artifacts/gestures-399

Production spring strip:

PASS touch sheet spring frame strip: /home/kayg/Developer/calternal-wt/gestures/artifacts/gestures-399/frames/sheet-edge-spring-strip.png

cargo clippy --all-targets -- -D warnings: exit 143 (host terminated during dependency checking; no diagnostic):

    Checking calternal-path v0.0.1 (/home/kayg/Developer/calternal-wt/gestures/crates/calternal-path)
    Checking hyper v1.11.1
   Compiling rand_chacha v0.3.1

cargo test: exit 143 (stopped during cold dependency compilation because the projected duration exceeded this job's four-hour limit; no tests ran):

   Compiling webauthn-attestation-ca v0.5.5
   Compiling curve25519-dalek-derive v0.1.1
   Compiling derive_more v0.99.20
   Compiling selectors v0.25.0
   Compiling curve25519-dalek v4.1.3

cargo clean:

     Removed 9021 files, 5.5GiB total

Adversarial API-only round ended with exit 143 before completion. Its last output was:

!! block reminder spam create 20: SLOW 9.9s status 201
!! block reminder spam create 26: SLOW 10.9s status 201
!! block reminder spam create 28: SLOW 7.8s status 201
!! block reminder spam create 40: SLOW 5.9s status 201
!! block reminder spam create 63: SLOW 5.6s status 201

No non-SLOW finding appeared before termination. The round is incomplete.

Decisions

  • Calendar uses a 700 ms keyed wheel burst boundary so trackpad inertia cannot page twice across a route remount. The default shared gesture window is unchanged.
  • The 20 px edge-back strip sits one layer above pushed-view sheets, leaving the visible Back control available.
  • Force Touch has no portable web input contract; long-press/context menu supplies preview actions. No pull-to-refresh was added where local data already updates without a server refresh.

Known gaps

  • The audit lists further candidates, including touch preview dismissal and Search palette pull-down dismissal. Full per-surface unification of pinch tracking and forward navigation remain for follow-up. Mail is not built.
  • Full Rust Clippy and workspace tests remain unverified due the terminations above. The browser evidence covers Chromium only; macOS trackpad through cua-driver and iOS Safari simulation remain for Claude to review.
# #399 gesture job report Branch: `job/gestures` (merged `dev` once before final gates). Head: `2ec58efd`. No push, deploy, or merge into `dev`. ## Built - Audited gestures across the shell, Calendar, Notes, Files, Photos, Search, Settings, previews, menus, composer, and future Mail in `docs/research/gesture-audit.md` with Apple HIG and browser references. - Moved the shared swipe/long-press/pull/sheet decision layer to `packages/ui/src/gestures.ts`; added wheel normalization, axis ownership, cross-remount wheel burst suppression, and common pinch geometry. Kept keyboard controls and reduced-motion behavior in the owning surfaces. - Added Calendar Month/Year/Agenda touch and wheel paging while preserving Day/Week native grid scroll; added pushed-view edge back, shared sheet dismissal decisions, Files row trackpad actions, and shared pinch metrics in Calendar, Photos, and ImageView. - Added production browser probes, 54 width/theme screenshots, and a seven-frame sidebar spring strip. The production gesture probe passed. Primary files: `packages/ui/src/gestures.ts`, `packages/ui/src/components/{OverlaySurface.svelte,calendar/TimeGrid.svelte,files/FileCollection.svelte,viewer/ImageView.svelte}`, `apps/web/src/lib/{gestures.ts,navigation.ts,components/app-sidebar.svelte,photos/PhotoTimeline.svelte}`, `apps/web/src/routes/calendar/[view]/[date]/+page.svelte`, `apps/web/e2e/{gestures-399.mjs,gesture-frames-399.mjs}`, `docs/research/gesture-audit.md` and related tests. ## Evidence - [All 54 screenshots and spring frames](https://git.kayg.org/attachments/f3dc1dcf-919c-46e1-989c-9530b8b24ac1) - [Sheet edge-drag and spring-release strip](https://git.kayg.org/attachments/0a88a4b2-d46c-44d2-91c7-17cdef5d8c11) - Direct previews: [phone light](https://git.kayg.org/attachments/f18c9d7e-82d1-4223-a0af-da5d8ce3f454), [tablet dark](https://git.kayg.org/attachments/8ca7cf9f-def9-4bd1-a75f-93b671102533), [desktop light](https://git.kayg.org/attachments/81ed2fdb-d6c0-428d-b387-78e4bbcc93f8). ## Gate output (verbatim summaries) `cargo fmt --check`: exit 0, no output. `bun run check`: ``` $ node scripts/check-type-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json Text sizes use shared role tokens. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/gestures/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` `bun run test`: ``` Test Files 124 passed (124) Tests 797 passed (797) Start at 09:25:11 Duration 237.97s (transform 61%, environment 17%, import 11%, tests 8%, setup 3%) ``` Production gesture probe: ``` PASS edge back PASS Files wheel PASS Files touch PASS viewer pinch PASS gesture touch/wheel paging, edge back, Files row swipe, Photos and viewer pinch, and screenshots at 390/820/1440 in both themes: /home/kayg/Developer/calternal-wt/gestures/artifacts/gestures-399 ``` Production spring strip: ``` PASS touch sheet spring frame strip: /home/kayg/Developer/calternal-wt/gestures/artifacts/gestures-399/frames/sheet-edge-spring-strip.png ``` `cargo clippy --all-targets -- -D warnings`: exit 143 (host terminated during dependency checking; no diagnostic): ``` Checking calternal-path v0.0.1 (/home/kayg/Developer/calternal-wt/gestures/crates/calternal-path) Checking hyper v1.11.1 Compiling rand_chacha v0.3.1 ``` `cargo test`: exit 143 (stopped during cold dependency compilation because the projected duration exceeded this job's four-hour limit; no tests ran): ``` Compiling webauthn-attestation-ca v0.5.5 Compiling curve25519-dalek-derive v0.1.1 Compiling derive_more v0.99.20 Compiling selectors v0.25.0 Compiling curve25519-dalek v4.1.3 ``` `cargo clean`: ``` Removed 9021 files, 5.5GiB total ``` Adversarial API-only round ended with exit 143 before completion. Its last output was: ``` !! block reminder spam create 20: SLOW 9.9s status 201 !! block reminder spam create 26: SLOW 10.9s status 201 !! block reminder spam create 28: SLOW 7.8s status 201 !! block reminder spam create 40: SLOW 5.9s status 201 !! block reminder spam create 63: SLOW 5.6s status 201 ``` No non-SLOW finding appeared before termination. The round is incomplete. ## Decisions - Calendar uses a 700 ms keyed wheel burst boundary so trackpad inertia cannot page twice across a route remount. The default shared gesture window is unchanged. - The 20 px edge-back strip sits one layer above pushed-view sheets, leaving the visible Back control available. - Force Touch has no portable web input contract; long-press/context menu supplies preview actions. No pull-to-refresh was added where local data already updates without a server refresh. ## Known gaps - The audit lists further candidates, including touch preview dismissal and Search palette pull-down dismissal. Full per-surface unification of pinch tracking and forward navigation remain for follow-up. Mail is not built. - Full Rust Clippy and workspace tests remain unverified due the terminations above. The browser evidence covers Chromium only; macOS trackpad through cua-driver and iOS Safari simulation remain for Claude to review.
kayg closed this issue 2026-09-29 08:29:21 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#399
No description provided.