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:
@@ -208,9 +208,25 @@ struct ShowTrashCommand: View {
|
||||
get: { store?.transient.isTrashVisible ?? false },
|
||||
set: { shown in
|
||||
guard let store else { return }
|
||||
store.transient.isTrashVisible = shown
|
||||
if !shown, store.selection.liveness == .trashed {
|
||||
store.clearSelection()
|
||||
// The re-divide is a *user-initiated structural change* — every lane compresses or
|
||||
// relaxes as the trash's one unit joins or leaves the division — so it animates in
|
||||
// the structural voice (03-board-ui.md § Motion; § Trash makes Show/Hide Trash "a
|
||||
// re-divide trigger", a lane add's behaviour exactly). It is also one of the few
|
||||
// structural changes that never touches disk, which is why it wears its own
|
||||
// transaction here instead of arriving through the reload seam like the rest.
|
||||
//
|
||||
// Reduce Motion read from AppKit rather than from `@Environment`: a menu command's
|
||||
// content is built outside any rendered hierarchy, where the environment's
|
||||
// accessibility values are not reliably populated (`Motion.prefersReducedMotion`).
|
||||
withAnimation(Motion.structural(reduced: Motion.prefersReducedMotion)) {
|
||||
store.transient.isTrashVisible = shown
|
||||
// Dropped inside the same transaction: the rows it pointed at are leaving under
|
||||
// this very animation, and a selection that cleared outside it would be the
|
||||
// highlight easing on its own — which 03 § Motion rules out ("the selection
|
||||
// highlight rides whatever transaction is active").
|
||||
if !shown, store.selection.liveness == .trashed {
|
||||
store.clearSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user