Implement toolbar customization for both windows
NSToolbar through the existing HostedWindowController rather than SwiftUI's toolbar — for reasons that are contract, not taste: 03's transient-search clause is a decision over the toolbar's current contents, which NSToolbar publishes and SwiftUI's API cannot answer; Undo/Redo are the system's nil-target responder-chain actions so the toolbar items validate exactly as the menu rows do (disabled on base boards, alive in m8 unchanged); and the search item hosts the real NSSearchField with explicit first-responder control. Customization is all system furniture — Customize sheet, drag rearrange, display-mode popup, overflow, autosaved per window kind. Board default: the search field alone, trailing; catalog adds New Card, New Lane, Undo, Redo, Show Trash, every action extracted from its menu command so no second predicate exists. Card default: the Edit Body / Raw Source toggles and Add Attachment, mirroring their commands' own predicates live via observation tracking. Removing the search item keeps the promise — ⌘F surfaces the same field as a transient strip under the title bar, persisting until the query clears, and an overflowed item that cannot take the keyboard falls through to the strip too. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -304,11 +304,10 @@ struct BoardCreationCommands: View {
|
||||
|
||||
var body: some View {
|
||||
Button("New Card") {
|
||||
guard let store, let target = newCardTarget else { return }
|
||||
store.transient.beginPlaceholder(inLane: target.laneID, after: target.anchorCardID)
|
||||
store?.beginNewCard()
|
||||
}
|
||||
.keyboardShortcut("n", modifiers: .command)
|
||||
.disabled(newCardTarget == nil)
|
||||
.disabled(store?.newCardTarget == nil)
|
||||
|
||||
Button("New Lane") {
|
||||
store?.createLane()
|
||||
@@ -316,17 +315,32 @@ struct BoardCreationCommands: View {
|
||||
.keyboardShortcut("n", modifiers: [.shift, .command])
|
||||
.disabled(store?.acceptsBoardMutations != true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Where ⌘N would file a card, or `nil` when it cannot — no focused board, a board that refuses
|
||||
/// writes, an inline editor holding the keyboard, or a board with no lanes.
|
||||
private var newCardTarget: NewCardTarget.Resolution? {
|
||||
guard let store, store.acceptsBoardMutations else { return nil }
|
||||
extension BoardStore {
|
||||
|
||||
/// Where ⌘N would file a card, or `nil` when it cannot — a board that refuses writes, an inline
|
||||
/// editor holding the keyboard, or a board with no lanes.
|
||||
///
|
||||
/// On the store rather than private to the menu row because **the toolbar's New Card item is the
|
||||
/// same command** (03-board-ui.md ▸ Toolbar: toolbar items mirror menu commands), and a second
|
||||
/// derivation of this rule would be a second chance to disagree with it — the same reason the row
|
||||
/// itself uses one answer for both its action and its validation.
|
||||
var newCardTarget: NewCardTarget.Resolution? {
|
||||
guard acceptsBoardMutations else { return nil }
|
||||
return NewCardTarget.resolve(
|
||||
selection: store.selection,
|
||||
lastActiveLaneID: store.transient.lastActiveLaneID,
|
||||
snapshot: store.snapshot
|
||||
selection: selection,
|
||||
lastActiveLaneID: transient.lastActiveLaneID,
|
||||
snapshot: snapshot
|
||||
)
|
||||
}
|
||||
|
||||
/// ⌘N's action — opening the new-card placeholder wherever `newCardTarget` says. Shared with the
|
||||
/// toolbar item that mirrors the row.
|
||||
func beginNewCard() {
|
||||
guard let target = newCardTarget else { return }
|
||||
transient.beginPlaceholder(inLane: target.laneID, after: target.anchorCardID)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Board Info
|
||||
|
||||
Reference in New Issue
Block a user