Realign the width and lane-move commands — three fresh settlements
DESIGN edits landing from the parallel session, honored: - Caret chords yield to any focused text control (04 ▸ Grammar): Move Left/Right ⌘←/⌘→ and the width pair ⌥⌘←/⌥⌘→ now disable while the board popover is open — its fields are the one non-inline text surface a board window has today; the search field and card-window fields extend the rule with their own cards. - The width pair batches over a multi-lane selection (03 ▸ Lane, the styling precedent): each selected lane steps one unit through one bracket (stepLaneWidths); floor members hold on a decrease; the context-menu stepper stays single-lane by nature. - A width write landing on 1 removes the width key (03 ▸ Lane, the remove-at-default family), every mechanism alike — writeLaneWidths is the one commit point; a hand-written width: 1 is preserved by the unchanged guard until the app itself next edits width. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -202,22 +202,35 @@ struct MoveCardCommands: View {
|
||||
/// **Never into the trash** costs nothing: the quasi-lane is not in the live lane order, so a step
|
||||
/// past the last real lane is simply off the end — which is also the disable rule at the walls,
|
||||
/// following the width stepper's floor style rather than letting the store no-op silently.
|
||||
///
|
||||
/// **Caret chords yield to any focused text control** (04-interactions.md ▸ Grammar, settled):
|
||||
/// ⌘←/⌘→ are the standard line-start/end chords, and an enabled key equivalent fires before a
|
||||
/// field ever sees the key. The inline title editors are covered by `acceptsBoardMutations`; the
|
||||
/// board popover's fields are covered by disabling while the popover is open at all — coarser than
|
||||
/// per-field focus, but the popover is a configuration surface (04's carve-out) and no lane move
|
||||
/// belongs under it. The search field (m5-search) and the card window's fields (whose windows never
|
||||
/// publish a `boardStore` in the first place) extend the same rule with their own cards.
|
||||
struct MoveLaneCommands: View {
|
||||
|
||||
@FocusedValue(\.boardStore) private var store
|
||||
@FocusedValue(\.boardInfo) private var boardInfo
|
||||
|
||||
var body: some View {
|
||||
Button("Move Left") {
|
||||
move(by: -1)
|
||||
}
|
||||
.keyboardShortcut(.leftArrow, modifiers: .command)
|
||||
.disabled(destination(-1) == nil)
|
||||
.disabled(caretChordsYield || destination(-1) == nil)
|
||||
|
||||
Button("Move Right") {
|
||||
move(by: 1)
|
||||
}
|
||||
.keyboardShortcut(.rightArrow, modifiers: .command)
|
||||
.disabled(destination(1) == nil)
|
||||
.disabled(caretChordsYield || destination(1) == nil)
|
||||
}
|
||||
|
||||
private var caretChordsYield: Bool {
|
||||
boardInfo?.isPresented == true
|
||||
}
|
||||
|
||||
/// The sole selected live lane and the display slot one step would put it in — `nil` when there
|
||||
@@ -404,48 +417,60 @@ struct BoardStyleCommand: View {
|
||||
/// 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.
|
||||
/// **They batch over a multi-lane selection** (03-board-ui.md § Lane, settled — the styling
|
||||
/// precedent): each selected lane steps one unit, one gesture, one commit
|
||||
/// (`BoardStore.stepLaneWidths`); the context-menu stepper stays single-lane by nature. A card
|
||||
/// selection, a trash-side selection and an empty one all disable them. Decrease additionally
|
||||
/// disables when **every** selected lane is at the one-unit floor — a mixed batch stays live, its
|
||||
/// floor members simply holding (the style batch's silent skip).
|
||||
///
|
||||
/// **⌥⌘←/⌥⌘→ yield to any focused text control** (04-interactions.md ▸ Grammar's caret-chords
|
||||
/// rule) — see `MoveLaneCommands`, whose ⌘←/⌘→ carry the same obligation and whose doc comment
|
||||
/// carries the mechanism.
|
||||
struct LaneWidthCommands: View {
|
||||
|
||||
@FocusedValue(\.boardStore) private var store
|
||||
@FocusedValue(\.boardInfo) private var boardInfo
|
||||
|
||||
var body: some View {
|
||||
Button("Increase Lane Width") {
|
||||
step(by: 1)
|
||||
}
|
||||
.keyboardShortcut(.rightArrow, modifiers: [.option, .command])
|
||||
.disabled(selectedLane == nil)
|
||||
.disabled(caretChordsYield || selectedLanes.isEmpty)
|
||||
|
||||
Button("Decrease Lane Width") {
|
||||
step(by: -1)
|
||||
}
|
||||
.keyboardShortcut(.leftArrow, modifiers: [.option, .command])
|
||||
.disabled(!canDecrease)
|
||||
.disabled(caretChordsYield || !canDecrease)
|
||||
}
|
||||
|
||||
/// The sole selected live lane, or `nil` — the whole of these items' validation.
|
||||
private var caretChordsYield: Bool {
|
||||
boardInfo?.isPresented == true
|
||||
}
|
||||
|
||||
/// The selected live lanes, in snapshot order — the batch, and the items' validation.
|
||||
///
|
||||
/// The lock and the open-editor rule are folded in through `acceptsBoardMutations` 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.acceptsBoardMutations else { return nil }
|
||||
private var selectedLanes: [Lane] {
|
||||
guard let store, store.acceptsBoardMutations else { return [] }
|
||||
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 }
|
||||
guard selection.liveness == .live, !selection.isEmpty else { return [] }
|
||||
return store.snapshot.lanes.filter { selection.ids.contains($0.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.
|
||||
/// `width` is ≥ 1 (03-board-ui.md § Lane), and an item whose only outcome is a no-op reads
|
||||
/// better disabled than dead — which for a batch means *some* member must have room to shrink.
|
||||
private var canDecrease: Bool {
|
||||
guard let lane = selectedLane else { return false }
|
||||
return LaneLayoutMath.displayUnits(of: lane) > 1
|
||||
selectedLanes.contains { LaneLayoutMath.displayUnits(of: $0) > 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)
|
||||
guard let store else { return }
|
||||
let lanes = selectedLanes
|
||||
guard !lanes.isEmpty else { return }
|
||||
store.stepLaneWidths(Set(lanes.map(\.id)), by: delta)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user