Skip to content

Release 0.6.0 — production sortable: drag defaults, live offsets, cross-container boards#11

Merged
iamv4g merged 43 commits into
mainfrom
release/0.6.0
Jul 23, 2026
Merged

Release 0.6.0 — production sortable: drag defaults, live offsets, cross-container boards#11
iamv4g merged 43 commits into
mainfrom
release/0.6.0

Conversation

@iamv4g

@iamv4g iamv4g commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Bundles the whole 0.6.0 line for dnd_kit, dnd_kit_flutter, and dnd_kit_jaspr: the integration-feedback drag-default fixes, a live sortable preview + offsets plug-in, cross-container board offsets, and the multi-container refinements found during live testing.

Drag defaults that match what users see (ADR 0024)

  • The active draggable is no longer a collision candidate: DndCollisionInput.droppableRects excludes the active id, so overId can never equal activeId and drops near the source slot commit.
  • beginDrag marks all measurements dirty, so a drag started in a scrolled viewport measures targets at their current positions.
  • Adds SortableStrategies.dropOnOver — the move commits where the isOver highlight is.
  • Adds DndRuntime.initialActiveRect (fixed for the session), so previews survive a collapsed source slot. DndDragOverlay sizes from it.
  • DndAutoScroll re-resolves collision on every tick, so overId follows content moving under a stationary pointer.

Live sortable preview + offsets, as an opt-in plug-in (ADR 0025)

  • SortableDragContext / SortableResolutionPhase: one code path for preview and commit, so the gap a UI shows equals the move it reports.
  • SortablePreview (lazy, cached per move) reports where the active item would land.
  • SortableOffsetResolver + SortableOffsets.verticalList / horizontalList / none (default none): the library reports geometry, the app decides whether to animate. Apply details.offset inside the item builder.
  • Deprecated: SortableStrategyInput.event and SortableMultiMoveInput.event (now null during preview) — use context.

Cross-container board offsets + refinements (ADR 0026)

  • SortableMultiOffsetResolver + SortableMultiOffsets.verticalLists / horizontalLists / none: a cross-container move closes the source column and opens the target column.
  • SortableMultiScope takes offsetResolver and collisionDetector, and installs its effective detector on whatever controller it is given — including one the app created — so an external controller ranks collisions with no manual wiring. DndController / DndRuntime.collisionDetector are settable.
  • Cross-container insertion tracks the pointer within the hovered card (not the grab-dependent dragged center); the gap between cards and the space below the last card resolve to the nearest card.
  • No move while the pointer is still inside the dragged item's own slot: picking an item up no longer shifts neighbours, and dropping in place no longer reorders — in both preview and commit.

Breaking behavior

  • Active item excluded from collision candidates.
  • beginDrag re-measures all droppables.
  • DndDragOverlay sizes from the drag-start rect; DndDragOverlayDetails gains a required initialActiveRect.
  • DndAutoScroll invalidates measurements each tick.

Examples & docs

  • New Flutter-only Planner demo: nested Day → Section → Item sortables in a CustomScrollView with sticky day headers, sections reordering within a day and items dragging across sections.
  • Multi-container board demos restyled (plain Material on Flutter, fixed-height scrollable columns on Jaspr); both galleries show live offsets.
  • Website recipes page for the four production patterns.
  • Full CHANGELOG entries across all three packages; ADRs 0024–0026; three exec plans moved to docs/plans/completed/.

Testing

  • dart run melos run validate green across all six packages.
  • New library coverage for phase-aware resolution, offsets, pointer-based
    insertion, the pick-up / drop-in-place no-op, and sortables inside slivers.

iamv4g added 30 commits July 22, 2026 19:50
The runtime no longer passes the active item's own droppable to the
collision detector, so overId can never equal activeId and drops near the
source slot commit instead of silently resolving to a null move.

BREAKING: custom detectors no longer receive the active id in
DndCollisionInput.droppableRects.
beginDrag invalidates all cached measurements so the first refresh uses
post-scroll positions instead of rects captured at registration.

DndAutoScrollController gains an onScrolled callback fired after each
tick; DndAutoScroll wires it to invalidate measurements and re-run
collision at the current pointer, keeping overId correct while content
scrolls under a stationary pointer.
The new strategy commits the move at the drop-over target, so the drop
lands where the isOver highlight is instead of where the active rect
center falls.

Documents that verticalList, horizontalList and grid resolve from the
rect center and can disagree with the highlight, and that isOver on
droppable and sortable-item details reflects collision, not the
committed move.
DndRuntime exposes initialActiveRect, the active rectangle measured at
drag start, which stays constant for the session. The Flutter and Jaspr
overlays lay out from it instead of the live activeRect, so collapsing
the source slot to open a placeholder gap no longer clips the preview to
zero.

DndDragOverlayDetails carries both rects: activeRect stays live for
collision-aware builders, initialActiveRect is the layout size.
Records the drag-default changes in ADR 0024 and the package changelogs:
active item excluded from collision candidates, drag-start and
auto-scroll re-measurement, SortableStrategies.dropOnOver, and
drag-start overlay sizing.

BREAKING: overId can no longer be the active id, overlays no longer
track a live source resize, and DndDragOverlayDetails requires
initialActiveRect.
Covers sortables inside a scroll view, dropping where the highlight is,
building a placeholder gap, and running nested or parallel sortables on
one controller.

Notes on the collision page that the active item is never a candidate,
and on the sortable page that dropOnOver exists for highlight-driven UIs.
Records the validation results and the remaining follow-up: the animated
placeholder API, a programmatic drag API for tests, and scroll-listener
re-measurement.
Sortable resolution no longer requires a drag end event. SortableDragContext
carries the session, the drag phase, overId, and the end event when there is
one, so the same strategy runs while a drag moves (preview) and when it ends
(commit) through one code path.

SortableStrategyInput and SortableMultiMoveInput now take context; their event
field becomes a deprecated getter that is null during preview. Both adapters
gain resolveDetails(SortableDragContext) beside moveDetailsFor, which now
delegates to it.
SortablePreview reports where the active item would land if released now,
resolved through the same strategy the drop will run so the preview and the
committed move cannot disagree. Resolution is lazy and cached per move, so
reading it from every item in a list costs one resolution.

SortableScope exposes it as SortableScopeData.preview, and sortable items
read it through SortableItemDetails.previewIndex and previewContainerId.
Multi-container scopes do not publish a preview yet.
SortableMultiScope now resolves the same preview single-container scopes
publish, using the reorder strategy of the container the dragged item came
from. Container areas register their strategy with the scope through an
owner-aware registry, because the strategy is configured per area while the
resolution happens at the scope.

SortableMultiContainerArea becomes stateful to manage that registration; its
public constructor is unchanged.
SortableOffsets.verticalList and horizontalList report how far each item a
move displaces should shift, so an application can open a placeholder gap
without recomputing collision, direction, or measurement itself. Offsets are
output only; SortableOffsets.none is the default and moves nothing.

Displaced items shift by the dragged item's extent plus the list gap, which
reproduces the post-move layout for variable-height lists. A displaced item
needs no measurement of its own, so lazy lists stay correct; a missing rect
for the dragged item yields no offsets rather than a guess.
SortableScope takes an offsetResolver, defaulting to SortableOffsets.none,
and SortableItemDetails.offset reports how far each item should move to make
room for the previewed drop. Offsets share the preview's cache, so a list
resolves them once per move however many items read them.

Applying the offset inside the item builder keeps it below the measured node
on both adapters, so it cannot change a measured rectangle; a Flutter widget
test and a Jaspr browser test pin that the rects stay put while offsets are
applied and overId does not oscillate.
Adds ADR 0025, extends the 0.6.0 changelogs, documents the offset resolver in
the Flutter README, and rewrites the website placeholder recipe around the
published offsets instead of hand-built gap logic.

Adds a performance smoke test for the per-move cost of live offsets, and a
roadmap Phase 34 entry, correcting the stale Current State section.
The sortable catalog demo now opens a placeholder gap while dragging, using
SortableOffsets.verticalList with dropOnOver so the gap and the committed move
agree. Flutter adds a toggle so both modes are visible side by side; Jaspr
animates the offset with a CSS transition.

A gallery widget test drives a real drag and asserts the displaced row shifts
up and that dropping commits the move the gap previewed.
The demo had no DndDragOverlay, so the dragged row stayed frozen in its slot
while the pointer moved — it was the only Flutter demo missing one. The live
gap made it obvious: neighbours slid aside while the dragged row did not move.

The demo now owns a controller, passes it to the scope, and renders the
dragged row in an overlay. The gallery test asserts the overlay tracks the
pointer instead of resting at the source slot.
The sortable demo dimmed the dragged row to partial opacity while its
neighbours slid over its slot, so the source appeared to sit under them. The
floating copy already lives in the overlay, so the in-list row is now hidden
while keeping its slot: the neighbours slide over one clean gap that follows
the pointer.

Corrects the recipe to hide the source rather than collapse it — the offsets
already reclaim the slot, so collapsing would reclaim it twice. The gallery
test asserts exactly one row is hidden while dragging.
The live gap already shows where the row lands, so lighting up each
hovered neighbour's border was redundant noise. The border is now
constant, which also avoids a width change that perturbs the item's
measured size mid-drag.
…le demo

Removes the soft background tint hovered neighbours got while dragging, so
the live gap is the only landing cue. Hovered rows now render exactly like
resting ones.
The overlay builder wrapped _TrackContent in a div that repeated the
14/18 padding _TrackContent already has and used a 2px border, so the
floating copy rendered a padding layer and a pixel larger than the source
and overflowed the correctly-sized overlay box. It now mirrors the in-list
row's box — no extra padding, 1px border — so the copy matches the source
size; the accent border and shadow still lift it.
Adds live sortable feedback to the 0.6.0 line: phase-aware resolution
(SortableDragContext), a live preview for single- and multi-container
scopes, and an opt-in SortableOffsetResolver for single-container gaps.
See docs/decisions/0025-sortable-live-preview-and-offsets.md.
SortableMultiOffsets.verticalLists / horizontalLists report per-item shifts
for a board move: a same-container move delegates to the single-list logic,
and a cross-container move closes the slot the dragged item leaves in its
source column and opens one at the landing index in the target column.

Adds SortableMultiOffsetResolver and SortableMultiOffsetInput, mirroring the
existing SortableMultiMoveResolver shape.
SortableMultiScope takes an offsetResolver (default SortableMultiOffsets.none)
and feeds the resolved preview move into it, so SortableItemDetails.offset
reports the shift on both same-column and cross-column drags. Offsets share
the preview cache and invalidate on the same signals.
Both board demos set SortableMultiOffsets.verticalLists and animate the
per-card offset, hiding the dragged card so the gap reads cleanly within and
across columns. Records the contract in ADR 0026, extends the 0.6.0
changelogs and the website recipe, and adds a gallery test that drives a drag
and asserts a displaced card carries an offset transform.
iamv4g added 13 commits July 23, 2026 11:05
Adds cross-container sortable offsets: SortableMultiScope.offsetResolver and
the SortableMultiOffsets built-ins open a live gap within and across columns.
See docs/decisions/0026-cross-container-sortable-offsets.md.
Adaptive cross-container insertion decided before/after from the dragged
card's translated centre, which depends on where the card was grabbed and
which column it came from. Near the over item's centre it flickered, and the
gap did not track the pointer — dropping over the lower half of a card, or
over the second card, could show no gap at all.

It now compares the pointer against the hovered card's midpoint: upper half
inserts before, lower half after. The pointer already chose the hovered card
(pointerWithin), so the whole decision runs on one stable signal. Fixes both
adapters, which share this core logic.
Hovering the gap between two cards, or the trailing space in a populated
column, resolved to the column container and appended at the end, so the
placeholder gap vanished. The multi-container collision detector now
resolves such a pointer to the nearest card, so the gap opens between the
adjacent cards; an empty column still resolves to the container.
…tector

SortableMultiScope only wires its multi-container collision detector into a
controller it creates itself. The Jaspr board demo passed its own plain
DndController, so the pointer resolved to the whole column in the gaps between
cards and drops appended at the end — the gap fix never ran because the wrong
detector was in use.

The demo's controller now carries SortableMultiContainer.collisionDetector.
Documents the requirement on SortableMultiScope.controller for both adapters,
and adds a widget test that drops into the gap between two cards and asserts a
placeholder gap opens.
…oller

DndRuntime.collisionDetector is now settable, and SortableMultiScope installs
its effective detector on whatever controller it uses — internal or
app-provided. An app can pass a plain DndController() to SortableMultiScope
without wiring SortableMultiContainer.collisionDetector itself; the scope's
collisionDetector parameter remains the customization point.

Simplifies the Jaspr board demo back to a plain controller and adds a widget
test that passes a plain external controller, drops into the gap between two
cards, and asserts a placeholder gap opens.
…spr columns

Flutter board demo drops the custom dark gradient theme for a standard
Scaffold + AppBar and Material-themed columns and cards, matching the other
demos, and replaces the stale "Experimental ... (Web Only)" header with an
accurate intro line.

Jaspr board columns now have a fixed height and scroll their own cards, like
the Flutter columns.
Adds widget tests for the layout that motivated the drag-start re-measure fix
but had no coverage: a single-container SortableScope in a SliverList, and a
multi-container SortableMultiScope with vertical sections in a CustomScrollView
(the day-list shape from the integration report). Each scrolls the viewport,
then drags and asserts the drop resolves to the post-scroll target — which
fails if measurements are not refreshed at drag start.
A CustomScrollView with sticky day headers where sections reorder within a
day and items drag across sections. Two sortable surfaces share one
controller — a per-day SortableScope for sections and one board-wide
SortableMultiScope for items — with a kind-scoped collision detector routing
section vs item drags. Items carry an editable note that survives moves.

Registered as a Flutter-only advanced demo outside the parity catalog (slivers
have no Jaspr peer); documented in examples-standard. Widget tests cover
section reorder, cross-section item moves, and note persistence.
Section cards size to their content, but the live gap is a paint-only
transform, so shifting items down to open room let the last item spill past
the card border. Reserve one dragged-item's height of real layout at the
section tail while a foreign item previews a drop there, and drop it again
otherwise. Same-section reorders stay net-zero and reserve nothing.
…slot

The active item is excluded from the droppable set, so a pointer still inside
its own (vacated) slot resolved over the nearest neighbour: neighbours shifted
the instant an item was picked up, and a press-hold-then-release with no
movement committed a swap. Resolve to no move whenever the pointer is within
the active item's original rect, which stays put for the whole drag. Covers
both the live preview and the commit.
Adds the late fixes to the 0.6.0 notes across all three packages: settable
collision detector, SortableMultiScope wiring its detector onto any controller,
pointer-based cross-container insertion, gap-to-nearest-card resolution, and the
pick-up / drop-in-place no-op.
@iamv4g iamv4g self-assigned this Jul 23, 2026
@iamv4g
iamv4g merged commit 98931d5 into main Jul 23, 2026
1 check passed
@iamv4g
iamv4g deleted the release/0.6.0 branch July 23, 2026 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant