Remove the face carousel — one card presentation
03's resettlement reverses the pathfinder carry-over: the selection-keyed dual presentation proved undesirable, so a card has one presentation — selection changes styling, never geometry, and the masonry never reflows on click. Deleted the carousel view (page dots, glass underlay, scroll-tick monitor), the QuickLook thumbnail cache (sole consumer), the pure paging/suppression rules, and the sole-selected animation key — Motion now keys transactions on the search query and the drop proposal only. The attachment chip stays as the face's whole attachment story; viewing media is the card window's job. No carousel state had leaked beyond the view layer. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -27,8 +27,11 @@ import SwiftUI
|
||||
/// ### The card face
|
||||
///
|
||||
/// `CardFaceView`, complete as of m5: the search filter narrows what the masonry lays out and what
|
||||
/// the badge counts, the deferred cut's dim rides the face (`cutTreatment`), and the sole-selected
|
||||
/// card expands its attachment carousel in place (`CardCarousel`).
|
||||
/// the badge counts, the deferred cut's dim rides the face (`cutTreatment`), and selection changes
|
||||
/// only the face's styling — never its geometry. **A card has one presentation** (resettled
|
||||
/// 2026-07-28, reversing the pathfinder's selection-keyed carousel): the masonry never reflows on
|
||||
/// a click, and viewing an attachment's media is the card window's job, not the face's
|
||||
/// (03-board-ui.md § Card face).
|
||||
struct LaneView: View {
|
||||
|
||||
let store: BoardStore
|
||||
@@ -457,7 +460,6 @@ struct LaneView: View {
|
||||
CardFaceView(
|
||||
store: store,
|
||||
card: card,
|
||||
laneID: lane.id,
|
||||
marquee: marquee,
|
||||
drops: drops,
|
||||
openCard: openCard
|
||||
@@ -865,8 +867,7 @@ private enum CardFaceMetrics {
|
||||
static let stripeWidth: CGFloat = 4
|
||||
/// The plate's inset around its content.
|
||||
static let contentPadding: CGFloat = 10
|
||||
/// Between the icon, the title and the attachments chip — and between the title row and the
|
||||
/// carousel below it.
|
||||
/// Between the icon, the title and the attachments chip.
|
||||
static let rowSpacing: CGFloat = 6
|
||||
}
|
||||
|
||||
@@ -893,34 +894,23 @@ private enum CardFaceMetrics {
|
||||
/// meant something we can't read", and a wrong colour is worse than none. In every case the bytes
|
||||
/// on disk are untouched (`Palette`, 01-storage-format.md § Frontmatter).
|
||||
///
|
||||
/// ### The attachment carousel
|
||||
/// ### One presentation, selection styling only
|
||||
///
|
||||
/// The face is a top-aligned `VStack` and its two decorations — the accent stripe and the selection
|
||||
/// stroke — are shapes in overlays, so both stretch to whatever height the content takes. That is
|
||||
/// what lets the carousel expand *inside* this card without any of it being re-derived: the masonry
|
||||
/// already isolates column heights, so a taller card pushes only the cards below it in its own
|
||||
/// column, and every consumer of a card's size reads a **live registered frame** rather than a
|
||||
/// nominal one — the drop model's resting grid (`onGeometryChange` into `LaneDropRegistry`) and the
|
||||
/// rubber band's sweep universe (`marqueeTarget`) both re-register the moment the card grows, so
|
||||
/// neither needed a word about the expansion.
|
||||
///
|
||||
/// Who expands and when is `CardCarousel`'s, and it is the whole of what this view decides here:
|
||||
/// `soleSelectedCardID` is the narrow animation key (03-board-ui.md § Motion), `expandedCardID` is
|
||||
/// the same answer with the rubber band's suppression on top, and the difference between them is
|
||||
/// why a band never animates anything.
|
||||
/// The face is a top-aligned title row and its two decorations — the accent stripe and the
|
||||
/// selection stroke — are shapes in overlays. **A card has one presentation** (resettled
|
||||
/// 2026-07-28, reversing the pathfinder's selection-keyed carousel): selection changes only this
|
||||
/// face's styling — the selection stroke below — and never its geometry, so the masonry never
|
||||
/// reflows on a click and every consumer of a card's drawn frame (the drop model's resting grid,
|
||||
/// the rubber band's sweep universe) can trust a selection change to leave it untouched. Viewing an
|
||||
/// attachment's media is the card window's job (⌘↩ / double-click, 05-card-window.md), not the
|
||||
/// face's — the paperclip chip below is the face's whole attachment story (03-board-ui.md § Card
|
||||
/// face).
|
||||
private struct CardFaceView: View {
|
||||
|
||||
let store: BoardStore
|
||||
let card: Card
|
||||
|
||||
/// The lane this face is drawn in — the middle component of the card's folder path, which is
|
||||
/// what the carousel needs to reach `attachments/`. Passed rather than looked up: the lane
|
||||
/// rendering this face already knows, and a walk of the snapshot per card face to re-learn it
|
||||
/// would be the board's own layout asking the board where its cards are.
|
||||
let laneID: ItemID
|
||||
|
||||
/// The strip's rubber band — the registry this face registers its drawn frame into, and the
|
||||
/// session whose in-flight band suppresses the carousel (`CardCarousel.expanded`).
|
||||
/// The strip's rubber band — the registry this face registers its drawn frame into.
|
||||
let marquee: MarqueeControl
|
||||
|
||||
/// The board window's drop machinery: this face registers its measured height into the geometry
|
||||
@@ -932,10 +922,6 @@ private struct CardFaceView: View {
|
||||
/// The app-wide quick-style recents — see `LaneView`'s own note.
|
||||
@Environment(AppModel.self) private var appModel
|
||||
|
||||
/// Reduce Motion, for the carousel's expansion and its page slides (10-accessibility.md). Read
|
||||
/// from the environment and handed to `Motion`, which owns what "reduced" means.
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
|
||||
/// The plate's corner radius — shared with the accent stripe, which rounds its left corners to
|
||||
/// exactly this so the stripe reads as part of the card's edge rather than a bar laid over it.
|
||||
/// Read from `CardFaceMetrics` rather than spelled here, because the new-card placeholder has to
|
||||
@@ -947,18 +933,7 @@ private struct CardFaceView: View {
|
||||
private var stripeWidth: CGFloat { CardFaceMetrics.stripeWidth }
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: CardFaceMetrics.rowSpacing) {
|
||||
titleRow
|
||||
carousel
|
||||
}
|
||||
// **The narrow transaction** (03-board-ui.md § Motion: "animated transactions are keyed
|
||||
// narrowly — on the sole-selected card (carousel expansion) … never on broad state like the
|
||||
// selection set"). The key is the sole selection and nothing else: a multi-select's churn
|
||||
// never changes it, so multi-select churn never animates — the same bullet's other half,
|
||||
// held by construction rather than by suppression.
|
||||
//
|
||||
// The band is deliberately *not* in the key, only in what renders — see `CardCarousel`.
|
||||
.animation(Motion.carouselExpansion(reduced: reduceMotion), value: soleSelectedCardID)
|
||||
titleRow
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(CardFaceMetrics.contentPadding)
|
||||
// Constant, whether or not a stripe paints: every card's text sits on the same grid, so
|
||||
@@ -1258,7 +1233,7 @@ private struct CardFaceView: View {
|
||||
/// A value that resolves to nothing — a typo'd palette name, a malformed hex, a sequence where
|
||||
/// a scalar belongs — draws **no stripe**, and the value stays on disk exactly as written.
|
||||
/// A `Shape` rather than a sized rectangle so it takes the plate's full height whatever the
|
||||
/// content does, m5's carousel expansion included.
|
||||
/// content does — a title wrapping across its full four lines included.
|
||||
@ViewBuilder
|
||||
private var accentStripe: some View {
|
||||
if let color = Palette.color(for: card.background) {
|
||||
@@ -1270,37 +1245,6 @@ private struct CardFaceView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The attachment carousel
|
||||
|
||||
/// The paged carousel, when this card is the entire selection and has files to page through
|
||||
/// (03-board-ui.md § Card face) — below the title, inside this same plate, adding height and
|
||||
/// changing nothing above it.
|
||||
///
|
||||
/// **An untitled card gets it identically**: the placeholder is what sits above, and 03 makes
|
||||
/// titles optional at every level without qualifying anything that reads them.
|
||||
@ViewBuilder
|
||||
private var carousel: some View {
|
||||
if CardCarousel.expands(card, expanded: expandedCardID) {
|
||||
AttachmentCarousel(
|
||||
pages: CardCarousel.pages(of: card, boardRoot: store.rootURL, laneID: laneID)
|
||||
)
|
||||
.transition(Motion.carouselTransition)
|
||||
}
|
||||
}
|
||||
|
||||
/// **The animation key** — the sole-selected card, band or no band. See `CardCarousel`: keeping
|
||||
/// the marquee out of the key is what makes a band's arrival and departure change the face
|
||||
/// without easing anything, which is 03 § Motion's animation-free-by-construction rule for the
|
||||
/// marquee applied to the one surface its churn could otherwise animate.
|
||||
private var soleSelectedCardID: ItemID? {
|
||||
CardCarousel.soleSelection(store.selection)
|
||||
}
|
||||
|
||||
/// What actually expands: the key, suppressed for as long as a rubber band is in flight.
|
||||
private var expandedCardID: ItemID? {
|
||||
CardCarousel.expanded(store.selection, marqueeActive: marquee.session.isActive)
|
||||
}
|
||||
|
||||
// MARK: - Selection and rename plumbing
|
||||
|
||||
private var isSelected: Bool {
|
||||
@@ -1354,8 +1298,8 @@ private struct CardFaceView: View {
|
||||
///
|
||||
/// A newly created card is always default-styled — `BoardWriter.createCard` writes `schema`, `title`
|
||||
/// and `order` and nothing else — so "the arriving card's chrome" is exactly: the level-default
|
||||
/// symbol, the standard secondary tint, no accent stripe, no selection stroke (the commit re-selects
|
||||
/// the *lane*), and no carousel (nothing is attached yet, and it is not the sole selection).
|
||||
/// symbol, the standard secondary tint, no accent stripe, and no selection stroke (the commit
|
||||
/// re-selects the *lane*).
|
||||
private struct NewCardStubView: View {
|
||||
|
||||
let store: BoardStore
|
||||
@@ -1459,8 +1403,8 @@ private struct NewCardStubView: View {
|
||||
///
|
||||
/// **`NewCardStubView.arrivingFace`'s precedent, applied to the drag**, and for the same reason: a
|
||||
/// static rendition at the same numbers (`CardFaceMetrics`) is what makes the echo's swap invisible
|
||||
/// rather than merely un-animated. It carries no gestures, no drop target, no geometry registration
|
||||
/// and no carousel — it stands in for exactly one round trip, and every surface that reads a card's
|
||||
/// rather than merely un-animated. It carries no gestures, no drop target, and no geometry
|
||||
/// registration — it stands in for exactly one round trip, and every surface that reads a card's
|
||||
/// drawn frame (the drop zones, the rubber band) is reading the *snapshot*'s cards, which this is
|
||||
/// not one of.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user