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
+40 -4
View File
@@ -289,6 +289,11 @@ struct CardWindowHost: View {
/// needs them (`CardPrintSubject`). Window-scoped for `CardBodyPresentation`'s reason: two card
/// windows are two documents, and the menu bar reaches the frontmost one through the focus system.
@State private var cardPrint = CardPrintSubject()
/// This window's card-level actions today, just Delete Card, promoted off the sidebar's former
/// Actions section to the toolbar (`CardWindowActions`, `CardToolbar`). Window-scoped for
/// `CardBodyPresentation`'s reason: File Delete Card (`DeleteCardCommand`) reaches the
/// frontmost card window through the focus system exactly as Add Attachment does.
@State private var cardActions = CardWindowActions()
/// This window's thumbnail memory. Held here rather than in the section so it survives every
/// snapshot the store applies a cache that died with the view would regenerate every thumbnail
/// on every reload (`AttachmentThumbnailCache`).
@@ -394,6 +399,10 @@ struct CardWindowHost: View {
// File Print (P) reaches the frontmost card window the same way the card-window scope
// of a row the board window answers with its whole board (11-command-nexus.md).
.focusedSceneValue(\.cardPrint, cardPrint)
// File Delete Card reaches the frontmost card window the same way the toolbar's own
// Delete Card item reads this same handle directly, wired at install time rather than
// through the focus system (`configureWindow`).
.focusedSceneValue(\.cardWindowActions, cardActions)
// The raw-source outlet's detailed alert, presented over this window a validation
// refusal on Apply, or a file that could not be opened as source. It hangs *here* rather
// than inside the editor because the second of those fires while source mode is still
@@ -536,6 +545,9 @@ struct CardWindowHost: View {
.onChange(of: store.isReadOnly, initial: true) { _, locked in
attachments.isEditable = !locked
session.comments.isEditable = !locked
// Delete Card's own read-only gate the sidebar Actions button's `.disabled(store
// .isReadOnly)`, carried over verbatim to its toolbar and menu-row twins.
cardActions.isDeletable = !locked
}
// **The thread re-reads on every landed reload** (05 The comments column: "the pane
// reloads its thread from the same FSEvents stream").
@@ -729,6 +741,24 @@ struct CardWindowHost: View {
attachments, store: store, cardID: cardID, undo: session.undo, clipboard: appModel.clipboard
)
Self.configureComments(session.comments, store: store, cardID: cardID, on: session.undo)
Self.configureActions(cardActions, store: store, cardID: cardID)
}
/// Points this window's card-level actions at its card **the one place File Delete Card and
/// the toolbar's Delete Card item learn which card they act on**, `configureAttachments`'s pattern
/// applied to the seam Actions Delete left behind when the sidebar section retired.
///
/// The store is captured **weakly**, `configureAttachments`'s own reason: a toolbar click or menu
/// row still firing after the board window has gone should write nothing rather than resurrect a
/// released store.
///
/// `static`, and taking every collaborator as a parameter, for `configureAttachments`'s reason:
/// which card a gesture lands on is invisible in a running window until it is wrong, and this
/// shape is what lets a test drive the real wiring rather than a re-typed copy of it.
static func configureActions(_ actions: CardWindowActions, store: BoardStore, cardID: ItemID) {
actions.deleteCard = { [weak store] in
store?.deleteCard(cardID)
}
}
/// Points this window's session at **its own undo stack** the three seams the two-level model
@@ -951,13 +981,19 @@ struct CardWindowHost: View {
// The window's customizable toolbar Edit Body · Raw Source · Add Attachment, "the
// window's three committed functions" (03-board-ui.md Toolbar; 05-card-window.md
// Window), joined by Show Sidebar (below). It carries the three window-scoped handles above
// rather than a store, which is why it is installed here and not at attach: those are this
// window's, and so is it. Show Sidebar needs none of them its read and write default to
// Window), joined by Show Sidebar and, since the sidebar's Actions section retired, Delete
// Card and Reveal in Finder (below). It carries the four window-scoped handles above rather
// than a store, which is why it is installed here and not at attach: those are this window's,
// and so is it. Show Sidebar needs none of them its read and write default to
// `AppPreferences.showCardSidebar` / `.setShowCardSidebar`, the same app-wide bit every card
// window answers to, so this call leaves the two injectable parameters unnamed.
windowController.installToolbar(
CardToolbar.controller(body: bodyPresentation, rawSource: rawSource, attachments: attachments)
CardToolbar.controller(
body: bodyPresentation,
rawSource: rawSource,
attachments: attachments,
actions: cardActions
)
)
// **No title in the title bar** the card's name is shown as part of the card's body
+8
View File
@@ -236,6 +236,14 @@ struct KanbanApp: App {
SetAsHeroCommand()
RemoveHeroCommand()
AddCommentCommand()
// **Delete Card joined 2026-08-09** (05-card-window.md Actions, retired Pipeline card
// bcd3b323): the card window's own delete, promoted off the sidebar's Actions section to
// a menu row so the new toolbar item mirroring it (`CardToolbar`) has the menu twin every
// toolbar function needs. Distinctly titled from the row below "Delete" is that row's
// own singleton title (11-command-nexus.md) and deliberately chord-less: an enabled
// delete-key equivalent here would steal delete-to-line-start from this window's text
// surfaces, the reason `TrashCommands`' own was never extended to the card window.
DeleteCardCommand()
Divider()
+4 -2
View File
@@ -4513,8 +4513,10 @@ public final class BoardStore: HealHost {
_ = moveLanesToTrash(lanes)
}
/// **The card window's Actions Delete** (05-card-window.md Actions: "Delete moves the card
/// to the trash the window then dismisses itself").
/// **The card window's own delete** the sidebar's Actions Delete button until 2026-08-09
/// (Pipeline card bcd3b323), now the window's toolbar item and File Delete Card
/// (`CardWindowActions`, `CardToolbar`) moving the card to the trash; the window then dismisses
/// itself (05-card-window.md Actions, Deletion & lifecycle).
///
/// The write is `moveToTrash(_:)`, so a card deleted from its own window is byte-indistinguishable
/// from one deleted with on the board or dropped on the trash column. What differs is the same
-72
View File
@@ -256,75 +256,3 @@ struct CardStyleSection: View {
)
}
}
// MARK: - Actions
/// The sidebar's **Actions** section, at the bottom of the stack (05-card-window.md Actions):
/// **Delete** moves the card to the trash and **Reveal in Finder** the card's folder.
///
/// ### Delete writes; the window's dismissal is not its business
///
/// The button calls `BoardStore.deleteCard`, which is the delete exactly (same write op, same
/// bracket, same stamps). It does not close this window: the card's move into `.trash/` rounds back through
/// the watcher and `CardWindowHost.cardWindowFate` takes the window down, which is the same path a
/// delete from the board or from an agent already takes. Dismissing from here as well would be a
/// second rule able to disagree with the first, and 05's own wording is a sequence rather than a
/// pair ("moves the card to the trash; the window then dismisses itself").
///
/// Recovery is the board's trash column, which is why this needs no confirmation: the card is still
/// there to drag out or cut and paste back (03-board-ui.md § Trash there is no Put Back), and 03
/// reserves the alert for the purge that isn't recoverable.
///
/// ### Reveal is not edit-shaped
///
/// So it stays enabled under the read-only lock, where Delete does not inspection is a read (04
/// The trash's posture, shared by the trash row's own Reveal). What it reveals comes from
/// `CardAttachments.revealURLs`, the same rule File Reveal in Finder's card-window scope answers
/// through: this button is that rule's card-folder branch by construction, since it is the *card's*
/// action rather than the attachment list's.
struct CardActionsSection: View {
let store: BoardStore
let cardID: ItemID
/// The card's own folder `nil` only where the window has no board to build it from, which is a
/// window on its way out.
let cardFolder: URL?
private var pointSize: CGFloat { CardWindowMetrics.bodyPointSize }
private var revealURLs: [URL] {
CardAttachments.revealURLs(cardFolder: cardFolder, selectedURL: nil, isSectionFocused: false)
}
var body: some View {
VStack(alignment: .leading, spacing: CardWindowMetrics.sidebarRowSpacing(bodyPointSize: pointSize)) {
CardSidebarSectionHeader(title: "Actions")
// Delete above Reveal, which is the order 05 lists them in. Destructive styling, per 05
// the one control in this window that takes the card away. Disabled under the
// read-only lock like every other mutation (02-architecture.md's every-entry-point
// predicate); that is the attachments section's `isEditable`, read from the store
// directly because there is no handle to route it through here and nothing else in this
// section that would want one.
Button(role: .destructive) {
store.deleteCard(cardID)
} label: {
// The width is the *label's*, not the button's: a bordered button sizes to its label,
// so a frame around the button would centre a small pill in a wide row instead of
// filling it. Both rows do it, so the two are one column rather than two widths.
Text("Delete").frame(maxWidth: .infinity)
}
.tint(.red)
.disabled(store.isReadOnly)
Button {
NSWorkspace.shared.activateFileViewerSelecting(revealURLs)
} label: {
Text("Reveal in Finder").frame(maxWidth: .infinity)
}
.disabled(revealURLs.isEmpty)
}
.buttonStyle(.bordered)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
+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
),
+81
View File
@@ -0,0 +1,81 @@
import SwiftUI
// MARK: - The window's card-level actions, as a handle
/// One card window's card-level actions, reduced to what things *outside* it need today, exactly
/// one: the write that used to be the sidebar's Actions Delete (05-card-window.md Actions, retired
/// with the Actions section Pipeline card bcd3b323), now the card window's own toolbar item
/// (`CardToolbar`) and its required menu twin (`DeleteCardCommand`).
///
/// `CardAttachments`'s shape and for its own reason: published through the focus system so a menu
/// item and a toolbar item can reach the frontmost card window without a which-window-is-key
/// register, `@State` in the host so it dies with the window it belongs to.
///
/// **Deliberately not folded into `CardAttachments`.** That type is "reduced to what things outside
/// it need" about the *attachments section* specifically reused elsewhere only as the "a card
/// window is frontmost" presence signal (`ShowSidebarCommand`'s own note) and a delete has nothing
/// to do with attachments. Folding it in would leave `CardAttachments`'s own doc comment wrong about
/// what the type is for.
@MainActor
@Observable
public final class CardWindowActions {
/// Whether the delete write is offered at all `!store.isReadOnly`, the read-only lock applied
/// to the one mutation this handle starts (02-architecture.md's every-entry-point predicate,
/// `CardActionsSection`'s own posture before it retired).
public var isDeletable = false
/// Moves this window's card to the trash filled by the host with `BoardStore.deleteCard(_:)`,
/// the same write and the trash column's own drop already make (`CardActionsSection`'s former
/// doc comment, carried over verbatim: "the delete exactly same write op, same bracket, same
/// stamps"). It does not dismiss the window; the window's fate is re-derived from every snapshot,
/// exactly as it was when this lived in the sidebar.
public var deleteCard: (() -> Void)?
public init() {}
}
/// The focused card window's actions handle `FocusedCardAttachmentsKey`'s own shape, one type over.
struct FocusedCardWindowActionsKey: FocusedValueKey {
typealias Value = CardWindowActions
}
extension FocusedValues {
var cardWindowActions: CardWindowActions? {
get { self[FocusedCardWindowActionsKey.self] }
set { self[FocusedCardWindowActionsKey.self] = newValue }
}
}
// MARK: - File Delete Card
/// The card window's delete, as a menu row the required twin `CardToolbar`'s own item needs
/// ("toolbars are pure enhancement: every function they host already has a menu item + shortcut",
/// 03-board-ui.md Toolbar) now that the sidebar's Actions Delete button has retired.
///
/// **Deliberately titled "Delete Card", not "Delete"** 11-command-nexus.md's File Delete row is
/// explicit that the chord's title is a singleton ("the chord's only owner no twin menu items, no
/// shared-equivalent routing"), and that row is unchanged: it still validates against the board
/// window's own focused store and still carries . This is a second, distinctly named row, scoped to
/// the card window alone, so the two can never be mistaken for one another in the File menu.
///
/// **No default chord, on purpose** the reason the sidebar button existed in the first place and
/// the reason `TrashCommands`' own was "deliberately not extended to the card window": an enabled
/// delete-key equivalent here would steal delete-to-line-start from this window's text surfaces (the
/// body editor, the title field). `View History`'s own "no default chord" is the precedent for a
/// menu row that means something without owning a key.
struct DeleteCardCommand: View {
@FocusedValue(\.cardWindowActions) private var actions
static func isEnabled(_ actions: CardWindowActions?) -> Bool {
actions?.isDeletable == true
}
var body: some View {
Button("Delete Card") {
actions?.deleteCard?()
}
.disabled(!Self.isEnabled(actions))
}
}
+23 -21
View File
@@ -69,12 +69,14 @@ struct CardWindowView: View {
/// The board this card belongs to.
///
/// The one place in this window a whole store is handed to a view rather than a narrow seam, and
/// the sidebar is why: the Style section hosts the **shared** style editor, whose API is
/// store-shaped by design (it reads the target set's current values and writes through the one
/// `applyStyle` bracket every anchor shares), and the Actions section's Delete is the store's own
/// move-to-trash. Routing either through a closure of this window's own would be a second
/// card-styling or card-deleting path to keep in step with the first exactly what "one
/// component, one behavior" and "exactly the delete" forbid.
/// the Style section is why: it hosts the **shared** style editor, whose API is store-shaped by
/// design (it reads the target set's current values and writes through the one `applyStyle`
/// bracket every anchor shares). Routing it through a closure of this window's own would be a
/// second card-styling path to keep in step with the first exactly what "one component, one
/// behavior" forbids. Delete moved off the sidebar's own Actions section and onto the card
/// window's toolbar (`CardToolbar`, `CardWindowActions`) still the store's own move-to-trash,
/// still "exactly the delete", just reached through the host's toolbar wiring rather than this
/// view.
let store: BoardStore
/// The app-wide quick-style recents the embedded editor feeds (03-board-ui.md Styling
/// Controls) app state, not board state, which is why it arrives beside the store rather than
@@ -398,21 +400,23 @@ struct CardWindowView: View {
// MARK: - Attributes sidebar
/// The sidebar's sections, **reordered 2026-08-09** (Pipeline card 8f26b029) so **Attachments
/// sits at the bottom of the stack**: Style, Details, Actions, Attachments. Section internals are
/// untouched this is ordering only, and it is exactly the `VStack`'s child order, so keyboard
/// Tab order and VoiceOver's reading order follow it for free; A (`AddAttachmentCommand`)
/// still opens the same file panel regardless of where the section sits in the stack, since it
/// reaches the window through the focus system rather than through this view's layout.
/// The sidebar's sections: **Style, Details, Attachments** Attachments at the bottom of the
/// stack (reordered 2026-08-09, Pipeline card 8f26b029), Actions gone entirely (retired the same
/// day, Pipeline card bcd3b323): its Delete and Reveal in Finder are now the card window's own
/// toolbar items (`CardToolbar`), reachable from Customize and, for Delete, on by default. This
/// is exactly the `VStack`'s child order, so keyboard Tab order and VoiceOver's reading order
/// follow it for free; A (`AddAttachmentCommand`) still opens the same file panel regardless of
/// where Attachments sits in the stack, since it reaches the window through the focus system
/// rather than through this view's layout.
///
/// 05-card-window.md The attributes sidebar still documents the pathfinder's original order
/// (Attachments, Style, Details, Actions) an owed amendment, tracked on that card's own thread
/// rather than made here.
/// 05-card-window.md The attributes sidebar still documents the pathfinder's four-section order
/// with Actions at the bottom an owed amendment, tracked on the two cards' own threads rather
/// than made here.
///
/// One of the four is conditional, and the condition is the section's own rather than a rule
/// restated here: **Details** renders nothing when the card carries no unknown frontmatter keys
/// ("shown only when any exist"). Everything else in the stack is unconditional, so the
/// composition a user learns on one card is the composition they get on the next.
/// One of the two remaining sections is conditional, and the condition is the section's own
/// rather than a rule restated here: **Details** renders nothing when the card carries no unknown
/// frontmatter keys ("shown only when any exist"). Style and Attachments are unconditional, so
/// the composition a user learns on one card is the composition they get on the next.
///
/// The History section that once sat between Details and Actions left with app-managed git
/// (strategy/01-git-excision.md, 2026-08-08); View History (`FutureCommands.swift`) is the only
@@ -426,8 +430,6 @@ struct CardWindowView: View {
// keys and their order included, and `Card` has carried it since (`BoardModel`).
CardDetailsSection(rows: CardDetails.rows(of: card.document))
CardActionsSection(store: store, cardID: card.id, cardFolder: cardFolder)
CardAttachmentsSection(attachments: attachments, thumbnails: thumbnails)
}
.frame(maxWidth: .infinity, alignment: .leading)