Skip to content

refactor(ui): one gesture core — recognizers/constants/press-hold/click-suppression (#12188 Phase 2) - #12684

Merged
lalalune merged 4 commits into
developfrom
feat/12188-gesture-core
Jul 4, 2026
Merged

lalalune merged 4 commits into
developfrom
feat/12188-gesture-core

Conversation

@lalalune

@lalalune lalalune commented Jul 4, 2026

Copy link
Copy Markdown
Member

Part of #12188 Phase 2. Builds on the merged gesture-core groundwork (#12445 recognizers/constants scaffold, #12454/#12345 push-to-talk) by finishing the centralization the earlier passes left partial: every tuned gesture value now has one definition site, and the last hand-rolled gesture copies at the call sites become adapters over the shared core.

Duplication eliminated (§C rollup — before → after)

§C item Before After
Long-press timers (450/420/200/180ms) 4 hand-rolled timers Table in gestures/constants.ts: DEFAULT_HOLD_MS 450 · COPY_HOLD_MS 420 · PUSH_TO_TALK_HOLD_MS 200. The 180ms composer timing was already unified to 200 in #12345; overlay copy-hold now uses shared usePointerPressAndHold.
5 tap-slop constants (8/10/10/10/4px) 5 scattered literals TAP_SLOP 8 · TOUCH_TAP_MOVE_SLOP 10 (the three identical 10px move-slops: copy-hold, tap-reveal, outside-sheet) · GRAPH_PAN_ENGAGE_SLOP 4
AXIS_COMMIT_SLOP 8-vs-6 2 copies AXIS_COMMIT_SLOP 8 (default) + named PAGER_AXIS_COMMIT_SLOP 6 override
axis dominance 0.8-vs-1.15 2 copies HORIZONTAL_DOMINANCE_RATIO 0.8 + named PAGER_AXIS_DOMINANCE_RATIO 1.15 override
4 flick-velocity (0.5/0.4/0.5/0.45) 4 literals DEFAULT_PULL_VELOCITY 0.5 · DEFAULT_SWIPE_VELOCITY 0.4 · PAGER_FLICK_VELOCITY 0.45; notification-pull velocity aliases DEFAULT_PULL_VELOCITY
3 rubber-band formulas (0.35/0.35/sqrt) 3 copies linear rubberBand + OVERSHOOT_RESISTANCE 0.35 (notification + pager edge); new sqrtRubberBand + SHEET_DETENT_OVERSHOOT_SCALE 6 (overlay detent)
≥6 click-suppression copies overlay/TopicGroup/Launcher/conv-item/composer×2/graph/overlay×2 useClickSuppression — RelationshipsGraphPanel migrated here (dropped a hand-rolled ref + setTimeout(…,0)); the pager/TopicGroup/conv-item already delegate (#12445). The overlay's 750ms-windowed outside-click gate is deliberately kept (it's a next-outside-click gate, not a synthesized-click swallow — see note below).
4 rAF-coalesce copies 4 loops useRafCoalescer — overlay viewport-sync migrated (dropped its hand-rolled one-rAF-per-frame loop)
long-press copy overlay:1154 hand-rolled timer + slop refs shared usePointerPressAndHold (new pointer-contract recognizer)

Centralized-constants table — deliberate unifications vs kept per-surface overrides

  • Deliberate value unification (already landed One ChatComposer core across overlay, ChatSurface, and ChatView (#12188 phase 3) #12345, documented here): the composer push-to-talk hold was 180ms and the overlay mic 200ms — the same feature with two timings — unified to one PUSH_TO_TALK_HOLD_MS = 200. No new value change is introduced in this PR.
  • Kept per-surface overrides (explicit, documented, values unchanged): PAGER_AXIS_COMMIT_SLOP (6 vs 8), PAGER_AXIS_DOMINANCE_RATIO (1.15 vs 0.8 — the pager wants horizontal to beat vertical, the pull surfaces want the widened diagonal cone), PAGER_FLICK_VELOCITY (0.45), COPY_HOLD_MS (420 vs 450), GRAPH_PAN_ENGAGE_SLOP (4), SHEET_DETENT_OVERSHOOT_SCALE (6, sqrt damping). Each carries a comment explaining the divergence.
  • A drift-gate test (gestures.test.ts → "tuned constants table") pins every default and every per-surface override plus its direction of divergence, so a future silent behavior change fails CI.

No tuned value was changed in this PR. All hooks keep their public APIs; the three gesture engines stay separate (DD2). useNotificationPull keeps its raw-TouchEvent non-passive preventDefault path.

Regression net (merged unified runners #12465) — results

Runner Result
test:launcher-e2e PASSED (launcher owns the pager driver — confirms the aliased PAGER_* constants are byte-identical)
test:chatux-gesture-e2e ALL PASSED (TopicGroup flick collapse/expand)
test:home-screen-e2e 1 failure — "hyperliquid" absent from launcherunrelated launcher-content/dedup assertion (develop's #12521 wallet-subpage hiding), not gesture math
test:chat-sheet-e2e PASSED clean on 3 of 4 runs; 1 run flaked on a Playwright click-interception timeout (infra, not a detent assertion)
chat-gesture-coverage gate (packages/app) PASSED (5/5)
jsdom gesture unit suites (8 files) 96 passed — gestures core, use-pull-gesture, use-notification-pull, useHorizontalPager ×2, chat-message tap-reveal, chat-conversation-item, mechanics-regression-gate
typecheck (packages/ui) clean except pre-existing iwer missing-dep in untouched spatial/__e2e__ (CI install resolves)
biome (12 touched source files) clean

Regression found & fixed mid-review

The chat-sheet runner initially failed 7 detent assertions (COLLAPSED→HALF flick overshooting to ~638px vs 377px). Root cause: useRafCoalescer returns a fresh object literal each render (its inner methods are stable, the wrapper is not); the overlay's viewport-sync effect depended on the whole object, so it re-ran every render and re-fired settleDragRef mid-drag, stranding the in-progress sheet gesture. Fixed by depending on the destructured stable schedule/cancel methods. The three merged adapters already destructure the stable methods, so only this new call site was affected. Runner green after the fix.

Line delta

  • Consumer call-sites (the dedup): −27 net (85 added / 112 removed)
  • Shared core (new usePointerPressAndHold + sqrtRubberBand + the documented constants table): +184
  • New unit tests (drift gate, sqrtRubberBand, pointer press-and-hold): +179

The raw call-site dedup lands negative; the module grows because this pass adds the documented tuned-value table and a reusable pointer press-and-hold recognizer that will absorb further duplication in Phase 3.

audit:app

N/A this run — shared-worktree node_modules + long dev-server boot make a full audit:app unreliable here. These are gesture-logic changes with zero visual-token / render-output changes (same tuned values, same DOM), and the e2e runners above capture rest+gesture screenshots proving the surfaces render and respond correctly.

What Phase 3 builds on

The composer/message-row merge (Phase 3) now consumes: usePointerPressAndHold and usePushToTalk (hold recognizers), useClickSuppression, TOUCH_TAP_MOVE_SLOP / the full hold + slop table, and sqrtRubberBand — so the retiring inline composers/bubbles adopt the shared gesture core instead of re-deriving it.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c642acab-7556-41a8-a4c1-50f4c601d125

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/12188-gesture-core

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lalalune
lalalune force-pushed the feat/12188-gesture-core branch 6 times, most recently from f674760 to bc12fc5 Compare July 4, 2026 04:20
@lalalune
lalalune marked this pull request as ready for review July 4, 2026 04:29

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@lalalune
lalalune force-pushed the feat/12188-gesture-core branch from bc12fc5 to 4a85a7e Compare July 4, 2026 04:33

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

Shaw added 4 commits July 4, 2026 00:34
…d-hold in the shared core (#12188)

Finish the constants centralization the gesture core (#12445) started:
gestures/constants.ts is now the single definition site for every tuned
gesture value, with named per-surface overrides (PAGER_*, COPY_HOLD_MS,
GRAPH_PAN_ENGAGE_SLOP, SHEET_DETENT_OVERSHOOT_SCALE) documenting each
deliberate divergence. No value changes.

- constants: add TOUCH_TAP_MOVE_SLOP (the three identical 10px move slops),
  GRAPH_PAN_ENGAGE_SLOP, the full hold table (DEFAULT_HOLD_MS moved in,
  COPY_HOLD_MS, PUSH_TO_TALK_HOLD_MS moved in), PAGER_AXIS_COMMIT_SLOP /
  PAGER_AXIS_DOMINANCE_RATIO / PAGER_FLICK_VELOCITY, OVERSHOOT_RESISTANCE,
  SHEET_DETENT_OVERSHOOT_SCALE.
- recognizers: add sqrtRubberBand (the chat sheet's sqrt detent damping).
- new usePointerPressAndHold: pointer-contract long-press with a move-cancel
  slop (the touch usePressAndHold cancels on any move).
- useHorizontalPager / use-notification-pull import their tuned values from
  the shared table (aliased, behavior byte-identical).
- chat-message tap-reveal slop = shared TOUCH_TAP_MOVE_SLOP.
- RelationshipsGraphPanel: hand-rolled suppress-click ref + timeout replaced
  by shared useClickSuppression; pan engage slop named.
- gestures.test.ts: sqrtRubberBand math, usePointerPressAndHold contract,
  and a tuned-constants drift-gate table (now 97 assertions across 5 suites).

Part of #12188 Phase 2 residuals after #12445/#12454.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
)

ContinuousChatOverlay drops its last three hand-rolled gesture copies:

- ThreadLine's 420ms hold-to-copy timer (local COPY_HOLD_MS /
  COPY_MOVE_CANCEL_PX + manual timer/slop refs) delegates to the shared
  usePointerPressAndHold with the centralized COPY_HOLD_MS.
- The local sqrt rubber-band function is the shared sqrtRubberBand with the
  named SHEET_DETENT_OVERSHOOT_SCALE.
- The keyboard-storm viewport sync's hand-rolled one-rAF-per-frame pattern is
  the shared useRafCoalescer (same schedule/cancel semantics).
- OUTSIDE_SHEET_TAP_SLOP aliases the shared TOUCH_TAP_MOVE_SLOP (same 10px).

The 750ms-windowed outside-click swallower is deliberately NOT migrated to
useClickSuppression: it is a document-level gate on the next outside click
after a swallowed pointerup, not a synthesized-click suppressor, and its
window is 750ms, not a macrotask.

Behavior-preserving; all tuned values unchanged. Part of #12188 Phase 2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-name marker

The chat-gesture-coverage scanner treats a literal gesture-hook name in file
content as a handler-site marker; constants.ts is a pure value table, so its
per-surface comments name the surface, not the hook.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ject (#12188)

useRafCoalescer returns a fresh object literal each render (the schedule/flush/
cancel callbacks inside are stable, the wrapper is not). The overlay's
viewport-sync effect depended on the whole object, so it re-ran every render and
re-fired settleDragRef mid-drag — stranding an in-progress sheet gesture at the
wrong detent (the chat-sheet e2e runner's COLLAPSED->HALF flick was overshooting
to a full-overscroll ~638px instead of 377px). Depend on the destructured stable
methods instead; the effect now runs once. The three merged adapters already
destructure the stable methods, so only this call site was affected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lalalune
lalalune force-pushed the feat/12188-gesture-core branch from 4a85a7e to 7c9dd82 Compare July 4, 2026 04:34
@lalalune
lalalune merged commit 42233de into develop Jul 4, 2026
18 of 32 checks passed

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@claude

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


I'll analyze this and get back to you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant