Repository navigation
feat(#338): auto-scroll long Dashboards while dragging a tile - #339
Merged
Merged
Conversation
Extends the #332 grip / Command-Ctrl tile move: once an active move crosses the movement threshold, holding the pointer near the top/bottom edge of the visible .dash-page viewport scrolls it continuously (bounded, edge-proportional speed; reduced-motion honored). Destination preview recomputes on every scroll frame by shifting the captured tile home rects by the scroll delta, so a stationary pointer lands on the newly revealed slot; the floating fixed card is left unadjusted. Exactly one atomic move-tile persists on release and nothing during the gesture; cancel (pointercancel / blur / Escape) changes neither order nor revision. The effective top edge sits below the sticky topbar. Edge-velocity math + the single-requestAnimationFrame-loop controller live in a new pure, injected-seam module core/dashboard-autoscroll.ts (DragAutoScrollTarget + FrameScheduler), 100/100/100/100; the DOM glue in ui/dashboard.ts builds the .dash-page adapter and injects real rAF. Both the Grid Tiles live-reflow and the flow reorder paths auto-scroll. No new bundled runtime dependency. Tests: pure edge-velocity + controller-lifecycle units (single loop, idempotent stop, no post-teardown mutation, boundary idle, reduced-motion), and dashboard integration tests (edge scroll up/down, threshold-gated non-start, destination recompute from a scroll frame alone, exactly-one commit, cancel dispatches none, read-only never scrolls, sticky-topbar offset, flow-engine scroll+reorder). CHANGELOG [Unreleased] updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDsUDSPoDYa1M1rbpgdG3f
Two drag fixes found while live-verifying #338 on a tall dashboard: 1. Flow-layout tiles (Report / 2-column / 3-column) now LIFT + float + follow the cursor during a drag, exactly like Grid Tiles — previously they stayed position:static ("dim in place"), so the new #338 auto-scroll made the grabbed tile scroll off-screen with the content. The float/lift is now shared by both engines (placeholder + FLIP reflow stays grid-only); the KPI band card gets a real box for the duration of the drag if its computed display is 'contents'. Verified live: a flow tile stays under the cursor while the page auto-scrolls thousands of px, and restores to static on release. 2. Replace the fragile "commit when the dragged tile covers >=2/3 of the DESTINATION slot's AREA" rule with MAX-OVERLAP: the move commits to whichever slot the dragged tile overlaps most, and snaps back only when it still overlaps its own origin most (or overlaps nothing). The old rule was aspect-ratio-dependent and unreachable for short tiles — a 403x120 KPI can cover at most ~0.40 of a 403x296 neighbor, so it ALWAYS snapped back and could never be moved in Grid Tiles. Max-overlap has no threshold to fight and works for any aspect ratio. Dropped OVERLAP_COMMIT_RATIO and the ratio param. Verified live: the KPI tile now commits its move (index changed) where it previously always snapped back. Pure logic stays in core/tile-reorder.ts (max-overlap, 100/100/100/100) and the DOM float/lift in ui/dashboard.ts behind the existing gesture. Tests updated: tile-reorder unit block rewritten for max-overlap incl. the short-over-tall regression; dashboard flow-drag tests assert the float/lift + cursor-follow + restore. CHANGELOG [Unreleased] reconciled. Known follow-up (pre-existing, separate): flow-layout KPI band tiles render presentational cards with no drag surface, so they can't be grabbed in flow layouts (they ARE movable in Grid Tiles). To be filed as its own issue. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDsUDSPoDYa1M1rbpgdG3f
Horizontal cursor/tile desync during a Grid Tiles move (found live): the flow-float refactor reordered beginMove so `grid.insertBefore(placeholder, card)` ran BEFORE `r0 = card.getBoundingClientRect()`. Inserting the placeholder pushes the card into the next CSS-grid cell, so r0.left captured the displaced (wrong-column) position and the floated tile sat ~one column right of the cursor (vertical fine — same row). Capture r0 + savedHeight/ savedDisplay BEFORE the placeholder insertion. Real-browser only (happy-dom ignores grid placement); added a regression test that models the displacement (the dragged card reports its home left until a placeholder occupies its slot) and asserts the float uses the pre-placeholder home left. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDsUDSPoDYa1M1rbpgdG3f
12 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Closes #338.
On a long Dashboard, moving a tile from the end to the beginning previously required several separate drops-and-scrolls, because a move could only reach destinations visible when the drag began. This adds the standard board interaction: during an active tile move, holding the pointer near the top/bottom edge of the visible Dashboard viewport scrolls it continuously, and the destination follows the newly revealed tiles.
Extends the #332 grip / ⌘-Ctrl tile move — no native HTML drag, no change to ordering semantics or schemas.
How it works
src/core/dashboard-autoscroll.ts(100/100/100/100):edgeScrollVelocity(...)— signed px/frame from the pointer's distance to the visible edge; bounded, proportional acceleration; a lower bounded cap underprefers-reduced-motion; 0 in the center and on a degenerate viewport.createDragAutoScroll(target, scheduler, opts)— a single-requestAnimationFrame-loop controller behind an injectedDragAutoScrollTarget(visibleTop/visibleBottom/scrollBy/canScrollUp/canScrollDown) andFrameSchedulerseam. At most one frame is ever pending; a stationary pointer keeps scrolling; leaving the edge zone or hitting a boundary idles the loop;stop()is idempotent and a frame already in flight can't mutate after it. A future nested scroll container can reuse the controller unchanged.src/ui/dashboard.tswireTileDrag: builds theDragAutoScrollTargetover.dash-page(effective top = page top + sticky.dash-topbarheight), injects realrequestAnimationFrame, and stops it in the existing singlecleanup()(so every exit path — pointerup / pointercancel / blur / Escape / rerender — tears it down).currentRects()helper shifts them by-(scrollTop - scrollTop0)at all four resolution sites (onMove grid+flow, onUp grid+flow) and on the per-frame recompute. The floating dragged card isposition:fixed(viewport-anchored) and is correctly left unadjusted. Persistence is unchanged: exactly one atomicmove-tileon release, nothing during the gesture; a cancel changes neither order nor revision.Both the Grid Tiles live-reflow path and the flow (Report / 2-column / 3-column) reorder path auto-scroll. No new bundled runtime dependency.
Tests
tests/unit/dashboard-autoscroll.test.ts): edge-velocity zero/direction/accel/clamp/reduced-motion/degenerate/short-viewport; controller single-loop, stationary-keeps-scrolling, leave-zone idle, boundary idle,stop()cancel + idempotent, no post-teardown mutation.tests/unit/dashboard.test.ts): edge scroll down/up, threshold-gated non-start, destination recomputes from a scroll frame alone with no new pointermove, exactly-onemove-tileafter several frames (landing on the scroll-shifted slot), Escape cancels the loop + dispatches nothing, read-only never scrolls, sticky-topbar offset shifts the top edge zone, flow-engine scroll+reorder.Reviewed by an adversarial pass (single-loop invariant, teardown on every exit, scroll-adjustment across all sites, non-vacuous tests) — no blockers/majors; the one minor it raised (untested nonzero sticky-topbar offset) is now covered by a dedicated test.
E2E (Playwright) not run locally (no engine in this sandbox) — relying on GitHub Actions.
Checklist
npm testpasses (per-file coverage gate: new core file 100/100/100/100;ui/dashboard.ts100/92.48/98.82/100, pre-existing gaps unchanged)npm run buildsucceeds (single-filedist/sql.html)src/core/, DOM glue behind an injected seam insrc/ui/CHANGELOG.md([Unreleased]) updated🤖 Generated with Claude Code
Follow-up fixes from live verification on github.demo (same PR)
Live-testing the drag on a tall dashboard surfaced three real defects, now fixed and re-verified live:
1acf43b). They wereposition:static("dim in place"), so the new auto-scroll scrolled the grabbed tile off-screen. Both engines now lift the tile to a floating follower (grid keeps its placeholder/FLIP reflow; flow has no slot grid).1acf43b). The old rule ("cover ≥2/3 of the destination slot's area") was aspect-ratio-dependent and unreachable for short tiles — a 403×120 KPI can cover at most ~0.40 of a 403×296 slot, so it always snapped back and could never be moved. Replaced with max-overlap (commit to whichever slot the tile overlaps most; snap back only when it still overlaps its origin most).core/tile-reorder.ts, 100/100/100/100.eb6a3c3). The flow-float refactor reorderedbeginMoveso the grid placeholder was inserted before the home rect was captured, displacing the card into the next CSS-grid cell → the floated tile sat ~one column right of the cursor (vertical fine). Now the rect is captured pre-placeholder; added a happy-dom regression test that models the grid displacement.Verified live (github.demo, Google-auth, SHA-matched build): auto-scroll through an 11.6k-px dashboard; flow tile stays under the cursor while scrolling; grab point 0px offset on both axes; a KPI tile commits a move (index 55→54) where the old rule snapped back.
Known follow-up (separate, pre-existing): flow-layout KPI band tiles have no drag surface — filed as #340 (KPI tiles ARE movable in Grid Tiles).