Files
lanework/Kanban/UI/Board/BoardCommands.swift
T
rzen ff3ba298f0 Build the full-visibility lane layout
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
2026-07-27 08:16:14 -04:00

79 lines
3.5 KiB
Swift

import SwiftUI
// MARK: - The focused board
/// The frontmost board window's store, published into the focus system by `BoardWindowHost` so menu
/// items can act on "the board in front" without the app model keeping a which-window-is-key
/// register of its own.
///
/// `focusedSceneValue` rather than `focusedValue`: the value is the *window's*, not any particular
/// control's, so it stays available whatever inside the board has keyboard focus — which is what a
/// menu item validating against the selection needs.
struct FocusedBoardStoreKey: FocusedValueKey {
typealias Value = BoardStore
}
extension FocusedValues {
var boardStore: BoardStore? {
get { self[FocusedBoardStoreKey.self] }
set { self[FocusedBoardStoreKey.self] = newValue }
}
}
// MARK: - Lane width items
/// Increase / Decrease Lane Width — **the width stepper's keyboard face** (03-board-ui.md § Lane,
/// 11-command-nexus.md), and therefore the *re-divide* mechanism: each step re-divides the existing
/// window width across the new unit total, compressing the siblings. They never touch the window's
/// size — window-growing behaviour belongs to the right-edge drag alone — and they are uncapped, so
/// widths beyond what the screen can fit stay reachable here even though the drag hard-stops.
///
/// **Validation is the sole-selected-lane rule.** Both items are enabled only when the focused
/// board's selection resolves to exactly one live lane; a card selection, a multi-selection, a
/// trash-side selection and an empty one all disable them. Decrease additionally disables at one
/// unit, which is the floor. Nothing selects a lane yet — the lane-chrome card wires the header
/// click (04-interactions.md ▸ Selection) — so these validate-disable in today's build, which is
/// expected rather than broken.
struct LaneWidthCommands: View {
@FocusedValue(\.boardStore) private var store
var body: some View {
Button("Increase Lane Width") {
step(by: 1)
}
.keyboardShortcut(.rightArrow, modifiers: [.option, .command])
.disabled(selectedLane == nil)
Button("Decrease Lane Width") {
step(by: -1)
}
.keyboardShortcut(.leftArrow, modifiers: [.option, .command])
.disabled(!canDecrease)
}
/// The sole selected live lane, or `nil` — the whole of these items' validation.
///
/// A read-only board disables every mutating command (02-architecture.md § The lock's scope), so
/// the lock is folded in here rather than left for the write to refuse: an item that is going to
/// fail should not look available.
private var selectedLane: Lane? {
guard let store, !store.isReadOnly else { return nil }
let selection = store.selection
guard selection.liveness == .live, selection.ids.count == 1, let id = selection.ids.first else { return nil }
return store.snapshot.lanes.first { $0.id == id && !$0.isDeleted }
}
/// A one-unit lane cannot shrink: `width` is ≥ 1 (03-board-ui.md § Lane), and an item whose only
/// outcome is a no-op reads better disabled than dead.
private var canDecrease: Bool {
guard let lane = selectedLane else { return false }
return LaneLayoutMath.displayUnits(of: lane) > 1
}
private func step(by delta: Int) {
guard let store, let lane = selectedLane else { return }
store.setLaneWidth(lane.id, units: LaneLayoutMath.displayUnits(of: lane) + delta)
}
}