Claude: changes for issue #64 #67

Merged
Grandiras merged 1 commit from claude/issue-64 into main 2026-07-29 18:18:33 +00:00
Collaborator

Closes #64.

Closes #64.
Stop the touch drag paying for the library's desktop assumptions
All checks were successful
CI / build (pull_request) Successful in 37s
2489ec4ac1
Dragging a card on a phone was sluggish because two SortableJS options
that are cheap on a desktop are bound to the raw input stream on touch.

`scroll` and `emptyInsertThreshold` both get their coordinates from
`dragover` when a native HTML5 drag is available. On touch there is no
native drag, so the library hangs the same handlers off `pointermove` and
`touchmove` instead — and a current phone samples touches 120–240 times a
second. `scroll` opens every call with `document.elementFromPoint`, which
flushes style and layout for the whole board and then hit-tests it;
`emptyInsertThreshold` puts every empty Sortable into a document-level
handler that measures each one, and the pagination bar contributes one
permanently empty Sortable per column. That asymmetry is why the same
drag was smooth on a desktop and crawled on a phone.

- `emptyInsertThreshold: 0` on the drop tiles. It cost them a 20px halo;
  they are 64px tall while a drag is on.
- `scroll: false` everywhere, with the capability reimplemented in
  sortable-interop.js as `dragScroll`. It reads nothing from the DOM
  while the finger is moving: the containers are measured when the drag
  starts, the move handler only records a coordinate, and a rAF loop
  compares the two. It listens for `dragover` as well, because a mouse
  drag fires no pointer move at all while it is in flight.
- `will-change: transform` on `.card-dragging`. The clone's transform is
  rewritten every frame, and off its own layer that repaints two soft
  shadows every frame with it.

Traced over a drag on an emulated Pixel 7 at 10x CPU throttle, four
columns of fourteen cards, median of seven runs: script 1442ms -> 979ms,
style recalc 292ms -> 180ms, paint 361ms -> 214ms, total attributed
main-thread work 4086ms -> 2990ms. `elementFromPoint` alone went from
238ms to 90ms of a 3.1s drag.

e2e pins the two options and the promotion, and mobile.spec.js drives a
real CDP touch drag to pin the scrolling that is now ours rather than the
library's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
Collaborator

🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 Reviewing this PR — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/166); the reply will link the full session transcript. 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/pr-67/run-166.md) — updating every ~5 minutes while the task runs.
claude-reviewer left a comment

Verdict: approve — no blocking issues found.

Solid, well-scoped perf fix. Replacing SortableJS's built-in scroll (bound to pointermove/touchmove on touch devices, opening with a synchronous elementFromPoint) with a hand-rolled dragScroll that measures once and only reads a cached coordinate per rAF frame is the right shape of fix for the described 120–240Hz touch-sample cost. emptyInsertThreshold: 0 and will-change: transform on .card-dragging are small, well-justified, scoped-to-drag-only changes.

Walked through dragScroll's bookkeeping carefully:

  • start/stop correctly add/remove the same bound listener references (this.onMove, this.onResize), so no leak across repeated drags, and stop() is called from both onEnd and disposeAll.
  • The "already at the end, let the board take the frame instead" logic (if (el.scrollTop !== before) return; vs. falling through) matches the comment's description.
  • dragover is correctly included alongside pointer/touch events since a native HTML5 drag (mouse) fires no pointer-move while in flight.
  • Zones are re-measured only on resize and after a board-scroll frame, consistent with "nothing reads the DOM mid-drag except the measured coordinate."

One minor, non-blocking observation: the generic zone bounding check (x < r.left || x > r.right || y < r.top - band || y > r.bottom + band) reuses the same shape for both the vertical (column) and horizontal (board) zones, so the board's band (derived from its width) is applied as vertical slop rather than a horizontal one — harmless since it's just a permissive containment pre-filter and the actual scroll direction/amount is computed separately, but worth a one-line comment if this is revisited.

Tests are appropriately split between a fast board.spec.js options check and a real CDP-driven touch-drag scroll test in mobile.spec.js, matching the project's existing pattern for touch-only interop paths.


⏱ 1m 9s · 2 turns · tokens: 51.5k in / 5.7k out (+94.8k cache-read)
📜 Full session transcript

✅ **Verdict: approve** — no blocking issues found. Solid, well-scoped perf fix. Replacing SortableJS's built-in `scroll` (bound to `pointermove`/`touchmove` on touch devices, opening with a synchronous `elementFromPoint`) with a hand-rolled `dragScroll` that measures once and only reads a cached coordinate per rAF frame is the right shape of fix for the described 120–240Hz touch-sample cost. `emptyInsertThreshold: 0` and `will-change: transform` on `.card-dragging` are small, well-justified, scoped-to-drag-only changes. Walked through `dragScroll`'s bookkeeping carefully: - `start`/`stop` correctly add/remove the same bound listener references (`this.onMove`, `this.onResize`), so no leak across repeated drags, and `stop()` is called from both `onEnd` and `disposeAll`. - The "already at the end, let the board take the frame instead" logic (`if (el.scrollTop !== before) return;` vs. falling through) matches the comment's description. - `dragover` is correctly included alongside pointer/touch events since a native HTML5 drag (mouse) fires no pointer-move while in flight. - Zones are re-measured only on resize and after a board-scroll frame, consistent with "nothing reads the DOM mid-drag except the measured coordinate." One minor, non-blocking observation: the generic zone bounding check (`x < r.left || x > r.right || y < r.top - band || y > r.bottom + band`) reuses the same shape for both the vertical (column) and horizontal (board) zones, so the board's `band` (derived from its width) is applied as vertical slop rather than a horizontal one — harmless since it's just a permissive containment pre-filter and the actual scroll direction/amount is computed separately, but worth a one-line comment if this is revisited. Tests are appropriately split between a fast `board.spec.js` options check and a real CDP-driven touch-drag scroll test in `mobile.spec.js`, matching the project's existing pattern for touch-only interop paths. --- ⏱ 1m 9s · 2 turns · tokens: 51.5k in / 5.7k out (+94.8k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/pr-67/run-166.md) <!-- claude-verdict:approve --> <!-- claude-reviewed-head:2489ec4ac1d742d5ed7011035728ce7e42721a70 -->
Grandiras deleted branch claude/issue-64 2026-07-29 18:18:33 +00:00
Grandiras referenced this pull request from a commit 2026-07-29 18:18:33 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
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
Grandiras/Ponente!67
No description provided.