Masonry goes column-major — cards read top-down, then across

Replaces the pathfinder-inherited round-robin deal (child i -> column i % C)
with contiguous column segments: base = n/C, the first n%C columns take one
more, and logical order runs down each column before crossing to the next.
Only the geometric mapping changes -- ranks, selection flatten, and VoiceOver
order are untouched, and MasonryPlacement stays the single placement function
both the Layout and the drop model replay.

Why: an insertion under round-robin shifted every later card across columns;
under the column-major deal later cards slide within their column and at most
one card crosses each boundary, so the drag reflow is far calmer. Drop-slot
math gets simpler too -- a column's cards are one contiguous range, a
non-final column's tail is now a genuine mid-list position, and only the last
column's tail means append.

DropSlotMathTests recomputed and extended (46 -> 50): the uneven-fill deal,
boundary positions, the shared tail/head boundary index, and a placement/
drop-model shadow-agreement check. DRAG-REORDER.md and DESIGN/10 amendments
are listed for ratification, deliberately not edited here.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-31 07:34:30 -04:00
parent 95133860e1
commit f174a524af
4 changed files with 386 additions and 142 deletions
+11 -13
View File
@@ -704,20 +704,18 @@ struct LaneView: View {
// holds identically under Reduce Motion: a transition that does not fire has no
// variant to choose between.
.transition(Motion.cardTransition(reduced: reduceMotion))
// **VoiceOver reads the masonry by `order`, not by column** 10-accessibility.md
// Logical order, not masonry position (decided): "within a wide lane,
// VoiceOver reads cards by `order` the interior grid columns are presentation
// only. This deliberately diverges from on-screen geometry."
// **VoiceOver reads the masonry by `order`, not by drawn position**
// 10-accessibility.md Logical order, not masonry position (decided).
//
// The divergence is real and it is why an explicit priority is needed at all:
// `MasonryLayout` assigns child `i` to column `i % columns`, so in a 3-unit lane
// the second card by `order` is drawn to the *right* of the first, not below it
// and an accessibility tree sorted by geometry (which is what a container does
// without this) would read the board column-major: 1, 4, 7, 2, 5, 8 , an order
// that exists nowhere in the model, on disk, or in the keyboard grammar.
// Priority descends with the slot index, so the highest reads first and the list
// is exactly `slots` the same sequence the masonry is handed and the same one
// `SelectionGrammar` flattens.
// The divergence narrowed when the masonry went column-major walking down
// one column now *is* consecutive `order` but it is still real, and it is
// why an explicit priority is needed at all: a geometry-sorted accessibility
// tree (which is what a container does without this) sweeps in reading order,
// left-to-right then down, which over a column-major grid interleaves the
// columns: 1, 4, 7, 2, 5, 8 , an order that exists nowhere in the model, on
// disk, or in the keyboard grammar. Priority descends with the slot index, so
// the highest reads first and the list is exactly `slots` the same sequence
// the masonry is handed and the same one `SelectionGrammar` flattens.
//
// The drag shadows are inert here: `DragShadow` hides itself from the tree, and
// a slot that is not an element consumes no priority.