Implement the motion language

One named home for every curve and duration — the two-voice split 03
fixes (snappy structural: drag reflow 0.18, delete 0.25, lane resize
0.2; smooth content-reflow 0.28), the scale-and-fade appear/disappear
transitions (cards 0.8, lanes 0.9), and a Reduce Motion variant on
every accessor (animations go instant, transitions go crossfade). A
post-migration grep holds the invariant: zero motion literals outside
Motion.swift. The animate-vs-snap split lands where Lanework's
one-way flow puts it: the store's reload seam. An app-mediated echo
applies its snapshot inside the structural transaction; foreign and
reconciling reloads — and every bracket-ending wholesale reload —
assign bare, because live-reload is the board becoming what's on
disk, not an event to perform. A window that merged foreign events
into an app-mediated span animates, deliberately: the ratified merge
rule makes it indistinguishable from a pure echo, and a test pins
that reading so a future mixed case fails loudly. The lane-reorder
reflow keys on the drop proposal alone (pointer tracking stays 1:1),
the resize session freezes its Reduce Motion answer at drag start so
the unit tick and the window resize can never disagree, and the m5
drag / m5 search / m7 undo voices are named seams waiting for their
call sites. 13 new tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-27 15:59:59 -04:00
parent 3be38fcb47
commit 5ebf9fb90c
8 changed files with 548 additions and 37 deletions
+9
View File
@@ -114,6 +114,10 @@ struct TrashLaneView: View {
/// session has.
let dragSession: TrashDragSession
/// Reduce Motion, for the row transition below 10-accessibility.md names the trash
/// specifically ("and trash animations all get reduced variants").
@Environment(\.accessibilityReduceMotion) private var reduceMotion
/// The lane plate's corner radius matched to `LaneView`'s so the column reads as a sibling of
/// the lanes rather than as a different kind of object.
private let cornerRadius: CGFloat = 10
@@ -205,6 +209,11 @@ struct TrashLaneView: View {
drag: drag,
dragSession: dragSession
)
// A row is a tombstoned item, so it arrives and leaves in the card's dialect
// a delete files one in, a Put Back or a purge takes one out, and both halves of
// that pair should read alike from either side of the strip. The transaction is
// the reload's, like the lanes' (`Motion.reloadAnimates`).
.transition(Motion.cardTransition(reduced: reduceMotion))
}
}
.frame(maxWidth: .infinity, alignment: .topLeading)