The app learns Appearance — Auto, Light, Dark from the View menu and a toolbar pull-down
View ▸ Appearance (11-command-nexus.md): three radio-exclusive rows, app-wide, persisted, needing no window in front — the View menu's new last group. AppearanceStore owns the override's rules (absent key = Auto, lenient reads degrade to Auto, remove-at-default) with an injectable apply seam so test hosts never touch NSApp; the one real apply hands NSApp.appearance its answer in applicationDidFinishLaunching, the global side effect KanbanApp.init must not carry. The board toolbar gains its first .picker item — an NSMenuToolbarItem whose rows re-fetch their spec fresh, checkmark read at menu-open like every other menu row — and Appearance joins the search field as the second default item, centered beside it (03-board-ui.md ▸ Toolbar, ratified 2026-08-07). Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -11,16 +11,18 @@ extension NSToolbarItem.Identifier {
|
||||
static let boardShowTrash = Self("board.showTrash")
|
||||
static let boardZoomIn = Self("board.zoomIn")
|
||||
static let boardZoomOut = Self("board.zoomOut")
|
||||
static let boardAppearance = Self("board.appearance")
|
||||
}
|
||||
|
||||
// MARK: - The board window's toolbar
|
||||
|
||||
/// The board window's toolbar (03-board-ui.md ▸ Toolbar).
|
||||
///
|
||||
/// ### The default is one item, and the catalog is five more
|
||||
/// ### The default is the search field and Appearance, and the catalog is the rest
|
||||
///
|
||||
/// "**Board window default: the search field, nothing else** — trailing, the one default item; the
|
||||
/// titlebar stays clean." The flexible space ahead of it is what "trailing" means to `NSToolbar`.
|
||||
/// "**Board window default: the search field and Appearance** — both **centered**, the titlebar's
|
||||
/// view-controls cluster." The flexible space ahead of the pair is what keeps them off the leading
|
||||
/// edge before `centeredItemIdentifiers` takes over their placement.
|
||||
///
|
||||
/// "**Catalog** (available via Customize): New Card, New Lane, Zoom In, Zoom Out …, Undo, Redo …,
|
||||
/// Show Trash (toggle state matching the View menu checkmark)." Every one of them is the *same command* as its menu row
|
||||
@@ -54,8 +56,14 @@ enum BoardToolbar {
|
||||
/// than one window's — Finder's behaviour, and the reason the identifier is a constant.
|
||||
static let identifier = "dev.rzen.indie.Kanban.board"
|
||||
|
||||
/// "The search field, nothing else — trailing, the one default item."
|
||||
static let defaultItems: [NSToolbarItem.Identifier] = [.flexibleSpace, .boardSearch]
|
||||
/// "The search field and Appearance — both centered, immediately after the field
|
||||
/// (03-board-ui.md ▸ Toolbar, the view-controls cluster)."
|
||||
static let defaultItems: [NSToolbarItem.Identifier] = [.flexibleSpace, .boardSearch, .boardAppearance]
|
||||
|
||||
/// The Appearance picker's rows, in menu order — the one place index and meaning are joined, so
|
||||
/// `specs(...)`'s `selected`/`select` closures and this array can never name two different
|
||||
/// orderings of the same three choices.
|
||||
private static let appearanceOptions: [AppAppearance?] = [nil, .light, .dark]
|
||||
|
||||
/// - Parameters:
|
||||
/// - zoom: the app-wide zoom level, in the catalog order the palette shows. It is not the
|
||||
@@ -64,12 +72,15 @@ enum BoardToolbar {
|
||||
/// `UserDefaults` at build time, since `WindowToolbarController.trackValidationState` re-arms
|
||||
/// observation over each spec's `isEnabled` and a plain scalar would leave Zoom In looking live
|
||||
/// at the top rung.
|
||||
/// - appearance: the app-wide appearance override, `zoom`'s reason exactly — not the store's,
|
||||
/// and `@Observable` so the picker's checkmark, read when its menu opens, is never stale.
|
||||
/// - session: the app's drag session, for the same guard the menu rows carry
|
||||
/// (`ZoomCommands.isEnabled`).
|
||||
static func specs(
|
||||
store: BoardStore,
|
||||
search: BoardSearchPresentation,
|
||||
zoom: BoardZoomStore,
|
||||
appearance: AppearanceStore,
|
||||
session: DragSession
|
||||
) -> [ToolbarItemSpec] {
|
||||
[
|
||||
@@ -141,6 +152,31 @@ enum BoardToolbar {
|
||||
setOn: { [weak store] shown in store?.setTrashVisible(shown) }
|
||||
)
|
||||
),
|
||||
// A pull-down rather than a toggle: Auto/Light/Dark is a three-way exclusive choice, not
|
||||
// an on/off bit. The one default (and centered) catalog item beside the field
|
||||
// (`defaultItems`, `controller(...)`'s `centeredItemIdentifiers`), always enabled — an
|
||||
// appearance override needs no board state, exactly as the View-menu row needs no board
|
||||
// window (`AppearanceCommands`).
|
||||
.mirroring(
|
||||
menuTitle: "Appearance",
|
||||
identifier: .boardAppearance,
|
||||
symbol: "circle.lefthalf.filled",
|
||||
behavior: .picker(
|
||||
options: [
|
||||
(title: "Auto", symbol: nil),
|
||||
(title: "Light", symbol: nil),
|
||||
(title: "Dark", symbol: nil),
|
||||
],
|
||||
selected: { [weak appearance] in
|
||||
guard let appearance else { return nil }
|
||||
return appearanceOptions.firstIndex(of: appearance.override)
|
||||
},
|
||||
select: { [weak appearance] index in
|
||||
guard appearanceOptions.indices.contains(index) else { return }
|
||||
appearance?.setOverride(appearanceOptions[index])
|
||||
}
|
||||
)
|
||||
),
|
||||
.staticLabel(
|
||||
"Search",
|
||||
identifier: .boardSearch,
|
||||
@@ -180,18 +216,21 @@ enum BoardToolbar {
|
||||
store: BoardStore,
|
||||
search: BoardSearchPresentation,
|
||||
zoom: BoardZoomStore,
|
||||
appearance: AppearanceStore,
|
||||
session: DragSession
|
||||
) -> WindowToolbarController {
|
||||
let controller = WindowToolbarController(
|
||||
identifier: identifier,
|
||||
specs: specs(store: store, search: search, zoom: zoom, session: session),
|
||||
specs: specs(store: store, search: search, zoom: zoom, appearance: appearance, session: session),
|
||||
defaults: defaultItems
|
||||
)
|
||||
// Centered against the window, not a flexible-space sandwich (03 ▸ Toolbar's placement
|
||||
// grammar, ratified 2026-08-06): `centeredItemIdentifiers` holds as catalog items install
|
||||
// and sits outside the autosaved configuration, so it reaches machines that saved an
|
||||
// arrangement under the old trailing default. `defaultItems` is untouched.
|
||||
controller.toolbar.centeredItemIdentifiers = [.boardSearch]
|
||||
// arrangement under the old trailing default. `defaultItems` is untouched. Appearance joined
|
||||
// the cluster after search (`defaultItems`'s own order), so the two center together as one
|
||||
// group — search first, Appearance beside it — rather than as two independently-placed items.
|
||||
controller.toolbar.centeredItemIdentifiers = [.boardSearch, .boardAppearance]
|
||||
controller.onInstalledItemsChanged = { [weak search] identifiers in
|
||||
search?.isInstalledInToolbar = identifiers.contains(.boardSearch)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user