The sidebar's Actions section retires — Delete and Reveal in Finder move to the card window's toolbar

Actions is gone from the trailing sidebar. Its two rows land on the card
window's toolbar instead: Delete Card is a new default item (trash SF
Symbol), and Reveal in Finder joins the customizable catalog. The sidebar's
final order is now Style, Details, Attachments — no fourth section.

Delete Card is a push button gated by the same read-only predicate the
sidebar button carried, and it fires the identical write
(BoardStore.deleteCard(_:)) — same bracket, same stamps, no confirmation,
matching the delete flow exactly: recovery is the board's trash lane, so
there is nothing here for an alert to guard. It sits behind a trailing
flexibleSpace in the default set, apart from the four creation/view items
ahead of it, the HIG separation Mail.app's own toolbar Delete models —
one-click, no-confirm, recoverable by trash.

Reveal in Finder is catalog-only: it already had a menu-bar twin with no
default chord (File ▸ Reveal in Finder / RevealInFinderCommand), so nothing
was unreachable before this — the toolbar item is Customize's shortcut to
the same computation (CardAttachments.revealURLs), not a new path.

Delete Card needed a menu-row twin of its own before it could sit on the
toolbar at all ("toolbars are pure enhancement: every function they host
already has a menu item + shortcut" — 03-board-ui.md ▸ Toolbar). File ▸
Delete Card is that row: distinctly titled from the board-scope File ▸
Delete (whose title 11-command-nexus.md calls out as the ⌘⌫ chord's
singleton), and deliberately chord-less — an enabled delete-key equivalent
in the card window would steal delete-to-line-start from its text surfaces,
the same reason the board's own ⌘⌫ was never extended here in the first
place. A new small handle, CardWindowActions, carries the wiring through the
focus system the way CardAttachments and CardPrintSubject already do for
their own single-purpose seams — kept separate from CardAttachments on
purpose, since a delete has nothing to do with the attachments section that
type is scoped to.

CardToolbarTests grows the BoardToolbarTests split (defaults vs. catalog,
now that they differ) plus two new suites: Delete Card firing the wired
write under the lock, and Reveal in Finder's enablement mirroring the menu
row's own card-window computation.

05-card-window.md's Actions section and 11-command-nexus.md's File-menu
inventory are now stale; both amendments are owed and tracked on the card's
own thread rather than made here.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-09 10:37:55 -04:00
parent 2a8fef258d
commit dfc6057f0d
9 changed files with 343 additions and 137 deletions
+82 -9
View File
@@ -7,6 +7,8 @@ extension NSToolbarItem.Identifier {
static let cardRawSource = Self("card.rawSource")
static let cardAddAttachment = Self("card.addAttachment")
static let cardShowSidebar = Self("card.showSidebar")
static let cardDeleteCard = Self("card.deleteCard")
static let cardRevealInFinder = Self("card.revealInFinder")
}
// MARK: - The card window's toolbar
@@ -14,14 +16,19 @@ extension NSToolbarItem.Identifier {
/// 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. **Show Sidebar joined
/// them** (05-card-window.md Composition, the toggle added beside the toolbar-customization work):
/// a fourth default item, the trailing sidebar's own show/hide control, the `Show Trash` precedent
/// applied to the card window's one collapsible pane.
/// functions, all discoverable from its toolbar." **Show Sidebar joined them** (05-card-window.md
/// Composition, the toggle added beside the toolbar-customization work): a fourth default item, the
/// trailing sidebar's own show/hide control, the `Show Trash` precedent applied to the card window's
/// one collapsible pane. **Delete Card joined them too** (05 Actions, retired Pipeline card
/// bcd3b323): the sidebar's Actions section is gone, and its destructive Delete moved here as the
/// fifth default item, trailing a `.flexibleSpace` so it sits apart from the four creation-and-view
/// items ahead of it HIG's "destructive actions read as separate from the everyday cluster",
/// `Mail.app`'s own toolbar Delete being the nearest system precedent for a one-click, no-confirm,
/// recoverable-by-trash button. **Reveal in Finder joined the catalog** the same day, catalog-only
/// it already has a menu-bar twin with no default chord (`RevealInFinderCommand`), so nothing was
/// unreachable before this; the toolbar item is Customize's shortcut to it, not its only path.
///
/// ### The four items are the four menu rows, predicates included
/// ### The six items are six 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
@@ -41,6 +48,16 @@ extension NSToolbarItem.Identifier {
/// app-wide, persisted bit `CardWindowView` renders from and every card window's toolbar answers
/// to alike. Always enabled, `ShowCommentsCommand`'s own posture: showing or hiding a pane is not a
/// mutation, so the read-only lock has no say in it.
/// - **Delete Card** is a push button carrying `trash`, mirroring **File Delete Card**
/// (`DeleteCardCommand`) a new, distinctly titled row (never "Delete": 11-command-nexus.md files
/// that title as the board-scope row's own singleton). Its predicate is the sidebar button's own
/// read-only check (`CardWindowActions.isDeletable`), and firing it calls `BoardStore.deleteCard(_:)`
/// directly the same write, same bracket, same stamps and the sidebar button always used. **No
/// confirmation**, matching the delete flow it replaces exactly: recovery is the board's trash lane,
/// so the alert 03 reserves is for the permanent purge, not this move.
/// - **Reveal in Finder** mirrors the existing card-window branch of `RevealInFinderCommand` same
/// computation (`CardAttachments.revealURLs`), same `NSWorkspace` call, so the toolbar item and the
/// menu row can never disagree about what "the selected attachment, or the card's folder" means.
///
/// Labels are the menu titles minus a trailing ellipsis, so File Add Attachment labels as **Add
/// Attachment** (03's own example).
@@ -49,16 +66,24 @@ enum CardToolbar {
static let identifier = "dev.rzen.indie.Kanban.card"
/// "The catalog is the same trio" plus Show Sidebar so the defaults are the whole catalog, in
/// this file's order.
/// The trio plus Show Sidebar plus Delete Card the whole catalog except Reveal in Finder, which
/// is catalog-only (already reachable off its own menu row, no default chord). The trailing
/// `.flexibleSpace` is what "placed apart from the creation-side defaults" means in code: Delete
/// Card is still a default, just not adjacent to the four items ahead of it.
static let defaultItems: [NSToolbarItem.Identifier] = [
.cardEditBody,
.cardRawSource,
.cardAddAttachment,
.cardShowSidebar,
.flexibleSpace,
.cardDeleteCard,
]
/// - Parameters:
/// - actions: Delete Card's window-scoped handle (`CardWindowActions`) the same object
/// `DeleteCardCommand` reads through the focus system, handed here directly since the toolbar
/// is installed once at window-open time rather than reactively (`CardWindowHost
/// .configureWindow`).
/// - isSidebarShown: Show Sidebar's read, defaulted to the real bit
/// (`AppPreferences.showCardSidebar`). Injectable so a test can drive the item without
/// touching the developer's own `UserDefaults.standard` domain `StyleRecents`' own caution,
@@ -69,6 +94,7 @@ enum CardToolbar {
body: CardBodyPresentation,
rawSource: CardRawSourceSession,
attachments: CardAttachments,
actions: CardWindowActions,
isSidebarShown: @escaping () -> Bool = { AppPreferences.showCardSidebar },
setSidebarShown: @escaping (Bool) -> Void = { AppPreferences.setShowCardSidebar($0) }
) -> [ToolbarItemSpec] {
@@ -124,13 +150,59 @@ enum CardToolbar {
symbol: "sidebar.right",
behavior: .toggle(isEnabled: { true }, isOn: isSidebarShown, setOn: setSidebarShown)
),
// **The sidebar's former Actions Delete** (05 Actions, retired) same write, same
// no-confirmation posture: recovery is the board's trash lane, so there is nothing here
// for an alert to guard. Placed last, after the trailing `.flexibleSpace` in
// `defaultItems`, so it never sits adjacent to the four creation/view items ahead of it.
.mirroring(
menuTitle: "Delete Card",
identifier: .cardDeleteCard,
symbol: "trash",
behavior: .button(
isEnabled: { [weak actions] in DeleteCardCommand.isEnabled(actions) },
perform: { [weak actions] in actions?.deleteCard?() }
)
),
// **The sidebar's former Actions Reveal in Finder** (05 Actions, retired) catalog
// only, since it already has a menu-bar twin with no default chord
// (`RevealInFinderCommand`); Customize is this button's whole reason to exist, not its
// only path. The computation is that row's card-window branch, verbatim, so the two
// surfaces can never disagree about what "the selected attachment, or the card's folder"
// means.
.mirroring(
menuTitle: "Reveal in Finder",
identifier: .cardRevealInFinder,
symbol: "folder",
behavior: .button(
isEnabled: { [weak attachments] in
guard let attachments else { return false }
return !Self.revealURLs(attachments).isEmpty
},
perform: { [weak attachments] in
guard let attachments else { return }
NSWorkspace.shared.activateFileViewerSelecting(Self.revealURLs(attachments))
}
)
),
]
}
/// `RevealInFinderCommand`'s own card-window branch, read here rather than re-derived the
/// selected attachment's file when the section has the keyboard, the card's folder otherwise
/// (`CardAttachments.revealURLs`'s own rule).
private static func revealURLs(_ attachments: CardAttachments) -> [URL] {
CardAttachments.revealURLs(
cardFolder: attachments.cardFolder,
selectedURL: attachments.selectedURL,
isSectionFocused: attachments.isFocused
)
}
static func controller(
body: CardBodyPresentation,
rawSource: CardRawSourceSession,
attachments: CardAttachments,
actions: CardWindowActions,
isSidebarShown: @escaping () -> Bool = { AppPreferences.showCardSidebar },
setSidebarShown: @escaping (Bool) -> Void = { AppPreferences.setShowCardSidebar($0) }
) -> WindowToolbarController {
@@ -140,6 +212,7 @@ enum CardToolbar {
body: body,
rawSource: rawSource,
attachments: attachments,
actions: actions,
isSidebarShown: isSidebarShown,
setSidebarShown: setSidebarShown
),