Build the sole-selection attachment carousel
The card face's in-place expansion (03-board-ui.md § Card face): a card with attachments, as the sole selection, grows a paged media band below its title; every other state stays compact behind the quiet paperclip. - CardCarousel owns the pure rules: the sole-selection predicate decides by identity (a sole-selected lane matches no face, no snapshot walk), and expansion is suppressed outside the animation key while a rubber band is active — a band names a set in progress, so carousels neither flicker nor animate under it. - QuickLook thumbnails generate off-main into a per-window cache keyed to survive reselection, with the Finder-icon fallback while loading and for non-previewable types; pages ride the platform paging behavior, dots (glass underlay, solid under Reduce Transparency) click to page, and a local wheel monitor turns a discrete tick into one clamped page — precise trackpad pans fall through untouched. - The expansion animates under Motion's new carouselExpansion transaction keyed narrowly on the sole-selected card; Reduce Motion goes instant. Drop-slot math and the marquee read the expanded height for free — both re-register on every size change. 928 unit tests (24 new). m5-interactions complete. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -111,6 +111,16 @@ struct MotionCurveTests {
|
||||
#expect(Motion.laneResizeWindowDuration == 0.2)
|
||||
#expect(Motion.laneResize(reduced: false) == .snappy(duration: Motion.laneResizeWindowDuration))
|
||||
}
|
||||
|
||||
/// **The carousel expansion is the structural voice** (03-board-ui.md § Motion's first named
|
||||
/// narrow key). What the change *is* is positional — the card grows and its column-mates slide
|
||||
/// down — so it takes the general snappy spring rather than the filtering one. Pinned as an
|
||||
/// identity with `structural` rather than as a literal, because if the two ever diverge it
|
||||
/// should be a deliberate edit to one line.
|
||||
@Test func theCarouselExpansionIsTheStructuralVoice() {
|
||||
#expect(Motion.carouselExpansion(reduced: false) == Motion.structural(reduced: false))
|
||||
#expect(Motion.carouselExpansion(reduced: false) != Motion.contentReflow(reduced: false))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Reduce Motion
|
||||
@@ -125,6 +135,7 @@ struct ReduceMotionVariantTests {
|
||||
#expect(Motion.delete(reduced: true) == nil)
|
||||
#expect(Motion.laneResize(reduced: true) == nil)
|
||||
#expect(Motion.contentReflow(reduced: true) == nil)
|
||||
#expect(Motion.carouselExpansion(reduced: true) == nil)
|
||||
}
|
||||
|
||||
/// "Appear/disappear is scale + fade (cards scale from ~0.8, lanes ~0.9, combined with opacity)."
|
||||
@@ -139,4 +150,14 @@ struct ReduceMotionVariantTests {
|
||||
#expect(Motion.cardAppearance(reduced: true) == .crossfade)
|
||||
#expect(Motion.laneAppearance(reduced: true) == .crossfade)
|
||||
}
|
||||
|
||||
/// **The carousel is the one appearance with no reduced variant, because it is already the
|
||||
/// reduced form.** 03 fixes an appear/disappear scale for cards and lanes and for nothing else,
|
||||
/// and the carousel needs none: the movement is the card growing, which `carouselExpansion`
|
||||
/// animates and Reduce Motion makes instant, so the content only has to fade in over it. This
|
||||
/// asserts the parameterless accessor is deliberate rather than an omission.
|
||||
@Test func theCarouselAppearanceIsTheCrossfadeInBothVariants() {
|
||||
#expect(Motion.carouselAppearance == .crossfade)
|
||||
#expect(Motion.carouselAppearance != Motion.cardAppearance(reduced: false))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user