The lane strip replaces the placeholder board: window width divides
across the lanes' width units (no horizontal scroll, no minimum width,
degenerate compression accepted), a lane of n units flowing its cards
into n round-robin masonry columns via a measurement-cached Layout.
Width has two deliberately opposite controls, both landing here: the
right-edge drag (ported verbatim from the pathfinder's ColumnResize)
freezes the 1x standard at drag start, snaps between integer widths
with the asymmetric shadow-leads tick and 10pt re-entry, grows the
window one standard width per snap so siblings keep their exact
pixels, and rubber-bands at the screen's visible frame — uncapped
otherwise; the Increase/Decrease Lane Width items (new Board menu,
Cmd-Opt-arrows) are the stepper's keyboard face and re-divide the
existing window width instead, never touching the window. Width
changes write through the new .resize WriteOperation ("Couldn't
resize…" in the banner vocabulary, which grows with the surfaces by
design); malformed width values render as one unit and stay untouched
on disk. 27 new tests port the pathfinder's resize-math suite onto
the uncapped range and pin the write path's fidelity.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
94 lines
4.2 KiB
Swift
94 lines
4.2 KiB
Swift
import SwiftUI
|
||
|
||
/// One lane: a header and a vertically scrolling masonry of cards (03-board-ui.md § Lane).
|
||
///
|
||
/// **The chrome here is deliberately minimal**, and the design's real lane is a later card: the
|
||
/// leading SF Symbol, the new-card button, the drag surface, the context menu (Rename, Style…, the
|
||
/// quick-style recents row, the Width stepper, Delete), the colour accent band, inline rename and
|
||
/// the search-aware count all arrive with the lane-chrome milestone. What this view owes the
|
||
/// full-visibility layout card is the shape — a header, a body that scrolls vertically only, and a
|
||
/// masonry whose interior column count is the lane's width — and that is all it does.
|
||
struct LaneView: View {
|
||
|
||
let lane: Lane
|
||
|
||
/// Interior masonry columns — the lane's width units, or the resize session's snapped count
|
||
/// while this lane is being dragged. Passed in rather than read off `lane` so the live drag can
|
||
/// override it (see `BoardView.laneSlot`).
|
||
let columns: Int
|
||
|
||
/// Spacing between cards, and between the interior columns.
|
||
private let cardSpacing: CGFloat = 8
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 8) {
|
||
header
|
||
ScrollView(.vertical) {
|
||
// Cards stay standard width whatever the lane spans: at a slot width of
|
||
// `units × standard + (units - 1) × gap`, `MasonryLayout` divides back into exactly
|
||
// `units` columns of `standard` (03-board-ui.md § Layout — full visibility).
|
||
MasonryLayout(columns: columns, spacing: cardSpacing) {
|
||
ForEach(liveCards) { card in
|
||
CardStubView(card: card)
|
||
}
|
||
}
|
||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The header — title, or a quiet "Untitled" where there is none, plus the live card count.
|
||
///
|
||
/// Titles are optional at every level (03-board-ui.md § Card face): a missing `title` renders as
|
||
/// a secondary-styled placeholder rather than as an empty row. **Replaced wholesale by the
|
||
/// lane-chrome card**, which brings the icon, the count badge's real styling, the new-card
|
||
/// button, the drag surface and the context menu.
|
||
private var header: some View {
|
||
HStack(alignment: .firstTextBaseline, spacing: 6) {
|
||
Text(lane.title.value ?? "Untitled")
|
||
.font(.headline)
|
||
.foregroundStyle(lane.title.value == nil ? .secondary : .primary)
|
||
.lineLimit(1)
|
||
.truncationMode(.tail)
|
||
Text("\(liveCards.count)")
|
||
.font(.callout)
|
||
.foregroundStyle(.secondary)
|
||
.monospacedDigit()
|
||
Spacer(minLength: 0)
|
||
}
|
||
.padding(.horizontal, 4)
|
||
}
|
||
|
||
/// **Tombstoned cards render nowhere**, and neither do the cards of a tombstoned lane — the
|
||
/// ancestor walk is absolute (01-storage-format.md § Deletion, 02-architecture.md's effective
|
||
/// liveness). The lane half of that rule is `BoardView`'s, which never builds a `LaneView` for a
|
||
/// tombstoned lane at all.
|
||
private var liveCards: [Card] {
|
||
lane.cards.filter { !$0.isDeleted }
|
||
}
|
||
}
|
||
|
||
// MARK: - Card stub
|
||
|
||
/// A card, as a rounded plate with its title — **a stand-in, replaced by the card-face card**, which
|
||
/// brings the leading icon, the attachment chip, the selection and cut treatments, inline rename and
|
||
/// the sole-selected card's attachment carousel (03-board-ui.md § Card face).
|
||
///
|
||
/// It takes whatever width `MasonryLayout` proposes (one interior column = one standard width) and
|
||
/// sizes its own height to its content, which is what makes the masonry masonry: a taller card only
|
||
/// pushes the cards below it in its own column.
|
||
private struct CardStubView: View {
|
||
|
||
let card: Card
|
||
|
||
var body: some View {
|
||
Text(card.title.value ?? "Untitled")
|
||
.font(.body)
|
||
.foregroundStyle(card.title.value == nil ? .secondary : .primary)
|
||
.lineLimit(4)
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.padding(10)
|
||
.background(RoundedRectangle(cornerRadius: 8).fill(.background.secondary))
|
||
}
|
||
}
|