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:
@@ -69,6 +69,10 @@ struct LaneView: View {
|
||||
/// business knowing about `WindowGroup` keys.
|
||||
let openCard: (ItemID) -> Void
|
||||
|
||||
/// Reduce Motion, for the card transition below (10-accessibility.md). Read from the environment
|
||||
/// and handed to `Motion`, which owns what "reduced" means.
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
|
||||
/// Spacing between cards, and between the interior columns.
|
||||
private let cardSpacing: CGFloat = 8
|
||||
|
||||
@@ -305,12 +309,21 @@ struct LaneView: View {
|
||||
// `units` columns of `standard` (03-board-ui.md § Layout — full visibility).
|
||||
MasonryLayout(columns: columns, spacing: cardSpacing) {
|
||||
ForEach(slots) { slot in
|
||||
switch slot {
|
||||
case let .card(card):
|
||||
CardFaceView(store: store, card: card, openCard: openCard)
|
||||
case .placeholder:
|
||||
NewCardStubView(store: store, openCard: openCard)
|
||||
Group {
|
||||
switch slot {
|
||||
case let .card(card):
|
||||
CardFaceView(store: store, card: card, openCard: openCard)
|
||||
case .placeholder:
|
||||
NewCardStubView(store: store, openCard: openCard)
|
||||
}
|
||||
}
|
||||
// "Appear/disappear is scale + fade (cards scale from ~0.8 …)"
|
||||
// (03-board-ui.md § Motion), which is how a create, a delete, a Put Back and
|
||||
// (m5) a search filter's leavers all reach the masonry. The placeholder wears it
|
||||
// too: it is the card, one round trip early. Whether any of it *performs* is
|
||||
// decided upstream — at the reload for the real cards (`Motion.reloadAnimates`),
|
||||
// at the gesture for the placeholder, which touches no disk.
|
||||
.transition(Motion.cardTransition(reduced: reduceMotion))
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
|
||||
Reference in New Issue
Block a user