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:
@@ -29,6 +29,9 @@ struct BoardWindowHost: View {
|
||||
@Environment(AppModel.self) private var appModel
|
||||
@Environment(\.openWindow) private var openWindow
|
||||
@Environment(\.dismissWindow) private var dismissWindow
|
||||
/// The transient search strip's arrival and departure has a reduced variant like every other
|
||||
/// appearance in the app (10-accessibility.md; `Motion.transientSearchTransition`).
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
|
||||
/// The window's own controller — `@State` so it outlives body evaluations and so SwiftUI keeps it
|
||||
/// alive for exactly as long as this window exists.
|
||||
@@ -86,6 +89,15 @@ struct BoardWindowHost: View {
|
||||
case let .open(store):
|
||||
VStack(spacing: 0) {
|
||||
BannerStripView(rows: store.bannerRows) { store.banners.dismiss($0) }
|
||||
// **⌘F's fallback**, and only that: the search field's home is the toolbar item
|
||||
// (`BoardToolbar`), and this strip exists for the window where the user has taken
|
||||
// that item out — "with the field removed from the toolbar, invoking it surfaces the
|
||||
// field transiently until the search clears" (03-board-ui.md ▸ Toolbar). It sits
|
||||
// directly under the title bar, where the item it stands in for would be.
|
||||
if boardSearch.isTransient {
|
||||
BoardSearchBar(store: store, presentation: boardSearch)
|
||||
.transition(Motion.transientSearchTransition(reduced: reduceMotion))
|
||||
}
|
||||
// The window is handed to the board as a closure, not a value: `WindowAccessor`
|
||||
// attaches after this body first runs, and the lane-resize drag needs the *live*
|
||||
// window to grow at its right edge (03-board-ui.md § Lane).
|
||||
@@ -99,21 +111,14 @@ struct BoardWindowHost: View {
|
||||
search: boardSearch
|
||||
)
|
||||
}
|
||||
// **The board window's toolbar: the search field, nothing else** (03-board-ui.md ▸
|
||||
// Toolbar, "trailing, the one default item; the titlebar stays clean"). It is a toolbar
|
||||
// rather than a strip inside the content because that is where 03 puts it, and it hosts
|
||||
// an `NSSearchField` rather than `.searchable` for the reasons `BoardSearchField`
|
||||
// records — explicit first-responder control, and stock key behaviour.
|
||||
//
|
||||
// m6-toolbar: the rest of 03's toolbar story is the customization card's — the
|
||||
// Customize palette, the New Card / New Lane / Undo / Redo / Show Trash catalog, and
|
||||
// with it ⌘F's transient surfacing of a *removed* field. That work replaces this
|
||||
// declaration with an identified, customizable toolbar; the item itself does not move.
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
BoardSearchField(store: store, presentation: boardSearch)
|
||||
.frame(width: 220)
|
||||
}
|
||||
// The transient strip's two dismissal inputs (`BoardSearchPresentation
|
||||
// .transientPersists`): it stays while a query is filtering the board or while the field
|
||||
// holds the keyboard, and goes when neither is true.
|
||||
.onChange(of: store.searchQuery) { _, query in
|
||||
boardSearch.dismissTransientIfCleared(query: query)
|
||||
}
|
||||
.onChange(of: boardSearch.isFocused) { _, _ in
|
||||
boardSearch.dismissTransientIfCleared(query: store.searchQuery)
|
||||
}
|
||||
// "The board in front", for the menu items that act on it (`LaneWidthCommands`), and
|
||||
// beside it the window's own popover flag, which is what File ▸ Board Info toggles, its
|
||||
@@ -267,6 +272,12 @@ struct BoardWindowHost: View {
|
||||
windowController.installTitlebarAccessory(
|
||||
boardInfoTitlebarAccessory(store: store, recents: appModel.styleRecents, presentation: boardInfo)
|
||||
)
|
||||
|
||||
// The board's customizable toolbar (03-board-ui.md ▸ Toolbar) — installed here for the
|
||||
// accessory's reason exactly: it carries the store, and it is a board window's, not every
|
||||
// hosted window's. Its search item is the search field's home, and it is what tells
|
||||
// `boardSearch` whether that home still exists.
|
||||
windowController.installToolbar(BoardToolbar.controller(store: store, search: boardSearch))
|
||||
}
|
||||
|
||||
// MARK: - Closing
|
||||
|
||||
Reference in New Issue
Block a user