import AppKit // MARK: - Identifiers extension NSToolbarItem.Identifier { static let cardEditBody = Self("card.editBody") static let cardRawSource = Self("card.rawSource") static let cardAddAttachment = Self("card.addAttachment") } // MARK: - The card window's toolbar /// The card window's toolbar (03-board-ui.md ▸ Toolbar; 05-card-window.md ▸ Window). /// /// "**Card window default: Edit Body · Raw Source · Add Attachment** — the window's three committed /// functions, all discoverable from its toolbar; the catalog is the same trio." So the default set /// *is* the catalog here, and Customize offers rearrangement and removal rather than a choice of /// items — which is exactly what a window with three functions should offer. /// /// ### The three items are the three menu rows, predicates included /// /// - **Edit Body** is "a single toggle button (on-state in Edit — mirroring the View ▸ Edit Body /// checkmark)", and it disables while source mode is active. That clause is not restated here: the /// predicate is `EditBodyCommand.isEnabled(body:rawSource:)`, the menu row's own, handed to the /// item verbatim. The pathfinder's segmented Preview|Edit is retired, so this is one button. /// - **Raw Source** is "likewise a toggle showing on-state", and its two directions are not /// symmetric: on enters, off *applies* (`CardRawSourceSession.applyAndLeave`). A refused Apply /// leaves source mode open, and the item's on-state simply fails to clear — the same way the menu /// row's checkmark does, because both read `isActive`. /// - **Add Attachment** "stays enabled in every mode — attachment operations never touch /// `index.md`, so they're safe alongside a raw edit". Its predicate is likewise the row's own /// (`AddAttachmentCommand.isEnabled`), which is scope plus the read-only lock and says nothing /// about the body's mode. /// /// Labels are the menu titles minus a trailing ellipsis, so File ▸ Add Attachment… labels as **Add /// Attachment** (03's own example). @MainActor enum CardToolbar { static let identifier = "dev.rzen.indie.Kanban.card" /// "The catalog is the same trio" — so the defaults are the catalog, in 03's order. static let defaultItems: [NSToolbarItem.Identifier] = [ .cardEditBody, .cardRawSource, .cardAddAttachment, ] static func specs( body: CardBodyPresentation, rawSource: CardRawSourceSession, attachments: CardAttachments ) -> [ToolbarItemSpec] { [ .mirroring( menuTitle: "Edit Body", identifier: .cardEditBody, symbol: "square.and.pencil", behavior: .toggle( isEnabled: { [weak body, weak rawSource] in EditBodyCommand.isEnabled(body: body, rawSource: rawSource) }, isOn: { [weak body] in body?.mode == .edit }, setOn: { [weak body] isOn in body?.setMode(isOn ? .edit : .preview) } ) ), .mirroring( menuTitle: "Raw Source", identifier: .cardRawSource, symbol: "doc.plaintext", behavior: .toggle( isEnabled: { [weak rawSource] in rawSource != nil }, isOn: { [weak rawSource] in rawSource?.isActive == true }, setOn: { [weak rawSource] isOn in guard let rawSource else { return } if isOn { rawSource.enter() } else { rawSource.applyAndLeave() } } ) ), .mirroring( menuTitle: "Add Attachment…", identifier: .cardAddAttachment, symbol: "paperclip", behavior: .button( isEnabled: { [weak attachments] in AddAttachmentCommand.isEnabled(attachments) }, perform: { [weak attachments] in attachments?.add() } ) ), ] } static func controller( body: CardBodyPresentation, rawSource: CardRawSourceSession, attachments: CardAttachments ) -> WindowToolbarController { WindowToolbarController( identifier: identifier, specs: specs(body: body, rawSource: rawSource, attachments: attachments), defaults: defaultItems ) } }