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:
+102
-27
@@ -580,14 +580,15 @@ struct BoardToolbarTests {
|
||||
|
||||
// MARK: - The card window's toolbar
|
||||
|
||||
/// The card toolbar's trio, and the two state clauses 03 states about it: Edit Body's on-state and
|
||||
/// its raw-source disable, and Add Attachment staying live in every mode.
|
||||
/// The card toolbar's six items: the trio's two state clauses (Edit Body's on-state and its
|
||||
/// raw-source disable, Add Attachment staying live in every mode), Show Sidebar, and — since the
|
||||
/// sidebar's Actions section retired (Pipeline card bcd3b323) — Delete Card and Reveal in Finder.
|
||||
@MainActor
|
||||
@Suite("Toolbar ▸ the card window")
|
||||
struct CardToolbarTests {
|
||||
|
||||
/// The three window-scoped handles a card window's toolbar reads, wired as the host wires them.
|
||||
private func makeHandles() -> (CardBodyPresentation, CardRawSourceSession, CardAttachments) {
|
||||
/// The four window-scoped handles a card window's toolbar reads, wired as the host wires them.
|
||||
private func makeHandles() -> (CardBodyPresentation, CardRawSourceSession, CardAttachments, CardWindowActions) {
|
||||
let body = CardBodyPresentation()
|
||||
let raw = CardRawSourceSession()
|
||||
raw.read = { .read("---\nschema: 1\norder: 1\n---\nbody\n") }
|
||||
@@ -595,40 +596,62 @@ struct CardToolbarTests {
|
||||
let attachments = CardAttachments()
|
||||
attachments.isEditable = true
|
||||
attachments.cardFolder = URL(filePath: "/tmp/board/lane/card")
|
||||
return (body, raw, attachments)
|
||||
let actions = CardWindowActions()
|
||||
actions.isDeletable = true
|
||||
return (body, raw, attachments, actions)
|
||||
}
|
||||
|
||||
@Test("The default set is the whole catalog — the trio plus Show Sidebar, in this file's order")
|
||||
func defaultsAreTheCatalog() {
|
||||
let (body, raw, attachments) = makeHandles()
|
||||
let specs = CardToolbar.specs(body: body, rawSource: raw, attachments: attachments)
|
||||
@Test("The default set is the trio, Show Sidebar, and Delete Card — separated by a flexible space")
|
||||
func defaultsAreSeparatedFromDeleteCard() {
|
||||
// "Card window default: Edit Body · Raw Source · Add Attachment" joined by Show Sidebar (the
|
||||
// toolbar-toggle card) and now Delete Card, HIG's "apart from the creation-side defaults"
|
||||
// spelled as a literal `.flexibleSpace` between the two clusters — `BoardToolbar.defaultItems`'
|
||||
// own leading-spacer pattern, turned trailing here.
|
||||
#expect(CardToolbar.defaultItems == [
|
||||
.cardEditBody, .cardRawSource, .cardAddAttachment, .cardShowSidebar, .flexibleSpace, .cardDeleteCard,
|
||||
])
|
||||
#expect(CardToolbar.defaultItems.filter { $0 != .flexibleSpace } == [
|
||||
.cardEditBody, .cardRawSource, .cardAddAttachment, .cardShowSidebar, .cardDeleteCard,
|
||||
])
|
||||
}
|
||||
|
||||
// "Card window default: Edit Body · Raw Source · Add Attachment … the catalog is the same
|
||||
// trio" — joined by Show Sidebar, the fourth default item the toolbar-toggle card added.
|
||||
#expect(CardToolbar.defaultItems == [.cardEditBody, .cardRawSource, .cardAddAttachment, .cardShowSidebar])
|
||||
#expect(specs.map(\.identifier) == CardToolbar.defaultItems)
|
||||
#expect(specs.map(\.label) == ["Edit Body", "Raw Source", "Add Attachment", "Show Sidebar"])
|
||||
@Test("Reveal in Finder is catalog-only — it already has a menu row with no default chord")
|
||||
func revealInFinderIsCatalogOnly() {
|
||||
#expect(!CardToolbar.defaultItems.contains(.cardRevealInFinder))
|
||||
}
|
||||
|
||||
@Test("The catalog is the six items, in this file's order, labeled off their menu titles")
|
||||
func catalogIsTheSixItems() {
|
||||
let (body, raw, attachments, actions) = makeHandles()
|
||||
let specs = CardToolbar.specs(body: body, rawSource: raw, attachments: attachments, actions: actions)
|
||||
|
||||
#expect(specs.map(\.identifier) == [
|
||||
.cardEditBody, .cardRawSource, .cardAddAttachment, .cardShowSidebar, .cardDeleteCard, .cardRevealInFinder,
|
||||
])
|
||||
#expect(specs.map(\.label) == [
|
||||
"Edit Body", "Raw Source", "Add Attachment", "Show Sidebar", "Delete Card", "Reveal in Finder",
|
||||
])
|
||||
}
|
||||
|
||||
@Test("Every item's symbol resolves on this system")
|
||||
func symbolsResolve() {
|
||||
let (body, raw, attachments) = makeHandles()
|
||||
for spec in CardToolbar.specs(body: body, rawSource: raw, attachments: attachments) {
|
||||
let (body, raw, attachments, actions) = makeHandles()
|
||||
for spec in CardToolbar.specs(body: body, rawSource: raw, attachments: attachments, actions: actions) {
|
||||
guard let symbol = spec.symbol else { continue }
|
||||
#expect(ItemSymbol.exists(symbol), "\(spec.label) draws a symbol this OS does not have")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Every item of the trio actually builds, and the toolbar is customizable")
|
||||
@Test("Every catalog item actually builds, and the toolbar is customizable")
|
||||
func everyItemBuilds() throws {
|
||||
let (body, raw, attachments) = makeHandles()
|
||||
let controller = CardToolbar.controller(body: body, rawSource: raw, attachments: attachments)
|
||||
let (body, raw, attachments, actions) = makeHandles()
|
||||
let controller = CardToolbar.controller(body: body, rawSource: raw, attachments: attachments, actions: actions)
|
||||
|
||||
#expect(controller.toolbar.allowsUserCustomization)
|
||||
#expect(controller.toolbar.allowsDisplayModeCustomization)
|
||||
#expect(controller.toolbarDefaultItemIdentifiers(controller.toolbar) == CardToolbar.defaultItems)
|
||||
|
||||
for spec in CardToolbar.specs(body: body, rawSource: raw, attachments: attachments) {
|
||||
for spec in CardToolbar.specs(body: body, rawSource: raw, attachments: attachments, actions: actions) {
|
||||
let item = try #require(controller.toolbar(
|
||||
controller.toolbar,
|
||||
itemForItemIdentifier: spec.identifier,
|
||||
@@ -641,8 +664,8 @@ struct CardToolbarTests {
|
||||
|
||||
@Test("Edit Body is a single toggle showing on-state in Edit")
|
||||
func editBodyShowsItsMode() {
|
||||
let (body, raw, attachments) = makeHandles()
|
||||
let specs = CardToolbar.specs(body: body, rawSource: raw, attachments: attachments)
|
||||
let (body, raw, attachments, actions) = makeHandles()
|
||||
let specs = CardToolbar.specs(body: body, rawSource: raw, attachments: attachments, actions: actions)
|
||||
guard let editBody = specs.spec(.cardEditBody) else {
|
||||
Issue.record("no Edit Body item")
|
||||
return
|
||||
@@ -661,8 +684,8 @@ struct CardToolbarTests {
|
||||
|
||||
@Test("Raw Source active disables Edit Body — the row's own predicate, mirrored")
|
||||
func rawSourceDisablesEditBody() {
|
||||
let (body, raw, attachments) = makeHandles()
|
||||
let specs = CardToolbar.specs(body: body, rawSource: raw, attachments: attachments)
|
||||
let (body, raw, attachments, actions) = makeHandles()
|
||||
let specs = CardToolbar.specs(body: body, rawSource: raw, attachments: attachments, actions: actions)
|
||||
guard let editBody = specs.spec(.cardEditBody), let rawSource = specs.spec(.cardRawSource) else {
|
||||
Issue.record("the card toolbar is missing an item")
|
||||
return
|
||||
@@ -687,8 +710,8 @@ struct CardToolbarTests {
|
||||
|
||||
@Test("Add Attachment stays enabled in every mode, including an open raw edit")
|
||||
func addAttachmentIsAlwaysAvailable() {
|
||||
let (body, raw, attachments) = makeHandles()
|
||||
let specs = CardToolbar.specs(body: body, rawSource: raw, attachments: attachments)
|
||||
let (body, raw, attachments, actions) = makeHandles()
|
||||
let specs = CardToolbar.specs(body: body, rawSource: raw, attachments: attachments, actions: actions)
|
||||
guard let addAttachment = specs.spec(.cardAddAttachment) else {
|
||||
Issue.record("no Add Attachment item")
|
||||
return
|
||||
@@ -719,12 +742,13 @@ struct CardToolbarTests {
|
||||
/// item here with no window-scoped handle to hold a scratch value instead).
|
||||
@Test("Show Sidebar toggles its own bit, always enabled, on-state matching the read")
|
||||
func showSidebarTogglesItsOwnBit() {
|
||||
let (body, raw, attachments) = makeHandles()
|
||||
let (body, raw, attachments, actions) = makeHandles()
|
||||
var shown = true
|
||||
let specs = CardToolbar.specs(
|
||||
body: body,
|
||||
rawSource: raw,
|
||||
attachments: attachments,
|
||||
actions: actions,
|
||||
isSidebarShown: { shown },
|
||||
setSidebarShown: { shown = $0 }
|
||||
)
|
||||
@@ -750,6 +774,57 @@ struct CardToolbarTests {
|
||||
attachments.isEditable = false
|
||||
#expect(showSidebar.isEnabled)
|
||||
}
|
||||
|
||||
/// **Delete Card is the sidebar Actions button's former write, on a push button** — gated by the
|
||||
/// same read-only predicate (`CardWindowActions.isDeletable`, wired from `!store.isReadOnly`
|
||||
/// exactly as the sidebar button's `.disabled(store.isReadOnly)` was), and firing calls the same
|
||||
/// closure the host wires from `BoardStore.deleteCard(_:)` (`CardWindowHost.configureActions`).
|
||||
@Test("Delete Card is a push button, gated by the read-only lock, that fires the wired delete")
|
||||
func deleteCardFiresTheWiredDelete() {
|
||||
let (body, raw, attachments, actions) = makeHandles()
|
||||
var deleted = 0
|
||||
actions.deleteCard = { deleted += 1 }
|
||||
let specs = CardToolbar.specs(body: body, rawSource: raw, attachments: attachments, actions: actions)
|
||||
guard let deleteCard = specs.spec(.cardDeleteCard) else {
|
||||
Issue.record("no Delete Card item")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(deleteCard.isOn == nil, "Delete Card is a push button, not a toggle")
|
||||
#expect(deleteCard.isEnabled)
|
||||
|
||||
deleteCard.activate()
|
||||
#expect(deleted == 1, "the item fires the same write the host wires from BoardStore.deleteCard")
|
||||
|
||||
// The read-only lock is the sidebar button's own predicate, carried over whole.
|
||||
actions.isDeletable = false
|
||||
#expect(!deleteCard.isEnabled)
|
||||
#expect(deleteCard.isEnabled == DeleteCardCommand.isEnabled(actions))
|
||||
}
|
||||
|
||||
/// **Reveal in Finder computes exactly what `RevealInFinderCommand`'s card-window branch does** —
|
||||
/// same function (`CardAttachments.revealURLs`), so the two surfaces can never disagree. Firing it
|
||||
/// is not exercised here, `addAttachmentIsAlwaysAvailable`'s own restraint: both open a real
|
||||
/// system surface (a panel there, Finder here), which a unit test does not drive.
|
||||
@Test("Reveal in Finder is enabled exactly when the row's own computation finds something to reveal")
|
||||
func revealInFinderMirrorsTheRowsComputation() {
|
||||
let (body, raw, attachments, actions) = makeHandles()
|
||||
let specs = CardToolbar.specs(body: body, rawSource: raw, attachments: attachments, actions: actions)
|
||||
guard let reveal = specs.spec(.cardRevealInFinder) else {
|
||||
Issue.record("no Reveal in Finder item")
|
||||
return
|
||||
}
|
||||
|
||||
// `makeHandles()` gives the section a folder and no focus — the card-folder branch.
|
||||
#expect(reveal.isEnabled)
|
||||
#expect(!CardAttachments.revealURLs(
|
||||
cardFolder: attachments.cardFolder, selectedURL: attachments.selectedURL, isSectionFocused: attachments.isFocused
|
||||
).isEmpty)
|
||||
|
||||
// A window on its way out — no folder, nothing to reveal — is the row's own disabled clause.
|
||||
attachments.cardFolder = nil
|
||||
#expect(!reveal.isEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ⌘F and the search field's two homes
|
||||
|
||||
Reference in New Issue
Block a user