A card window toolbar control shows and hides the trailing sidebar — View ▸ Show Sidebar joins Edit Body, Raw Source and Add Attachment

The card window's trailing attributes sidebar has always been unconditional
since m6; this adds View ▸ Show Sidebar (a checkmark toggle, ShowComments'
shape) and a matching toolbar item on the customizable card toolbar
(NSToolbar/WindowToolbarController), a fourth default beside Edit Body,
Raw Source and Add Attachment. sidebar.right for the trailing pane; one
shared animated write path (AppPreferences.setShowCardSidebar) both faces
call, structural-voice reflow with a trailing slide-and-fade transition
(Motion.cardSidebarTransition), Reduce Motion respected throughout.

CardWindowMetrics.minimumSize gains a sidebar: Bool = true parameter so a
hidden sidebar shrinks the window's floor, composing with the existing
commentsColumn parameter. The toolbar item's read/write are injectable
closures (defaulted to the real UserDefaults-backed pair) so its plumbing
is testable without touching the developer's own preferences domain.
WindowToolbarController's observation tracking only sees @Observable
reads, so a small HostedWindowController.revalidateToolbar() plus an
onChange nudge keeps the toolbar button's on-state in step with the
View-menu row's write.

Scope held narrowly to visibility, per the card: no sidebar section
reordering, no action-moving.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-09 09:14:51 -04:00
parent 55663e855a
commit d905e73960
11 changed files with 301 additions and 28 deletions
+22 -5
View File
@@ -302,6 +302,12 @@ struct CardWindowHost: View {
/// threading the pair through a view that would then have to publish them back up.
@AppStorage(AppPreferences.showCommentsKey) private var showComments = true
@AppStorage(AppPreferences.commentsBesideBodyKey) private var commentsBesideBody = true
/// The sidebar's own visibility bit, read here for `showComments`' one reason: the window's
/// **minimum size** depends on it too (`minimumSize`). The toolbar item reads
/// `AppPreferences.showCardSidebar` directly (`CardToolbar`, an AppKit seam with no `@AppStorage`
/// of its own), and `CardWindowView` reads this same key again itself three readers of one
/// `UserDefaults` key, `showComments`' own arrangement.
@AppStorage(AppPreferences.showCardSidebarKey) private var showSidebar = true
private enum Phase {
case opening
@@ -426,15 +432,23 @@ struct CardWindowHost: View {
isClosePending = false
windowController.closeAfterFlush()
}
// **The toolbar's Show Sidebar item, nudged fresh** `WindowToolbarController`'s own
// observation tracking only sees `@Observable` reads (its header explains why: menu
// commands and AppKit have no `@Environment` to poll), and `AppPreferences.showCardSidebar`
// is a plain `UserDefaults` read, not one. The toolbar's own click already revalidates
// itself (`toggleFired`); this is the same nudge for the View-menu row's write, which is
// the only other place this bit changes.
.onChange(of: showSidebar) { _, _ in windowController.revalidateToolbar() }
.onDisappear { finish() }
}
/// **The minimum grows only while the comments pane is beside the body** (05-card-window.md
/// Composition) which is the whole reason the stacked mount exists, so a narrow display keeps
/// the minimum it always had.
/// **The minimum grows only while the comments pane is beside the body, and shrinks while the
/// sidebar is hidden** (05-card-window.md Composition) the stacked mount's own reason, and
/// View Show Sidebar's mirror of it: a window with neither costs the width of neither.
private var minimumSize: CGSize {
CardWindowMetrics.minimumSize(
bodyPointSize: CardWindowMetrics.bodyPointSize,
sidebar: showSidebar,
commentsColumn: showComments && commentsBesideBody
)
}
@@ -915,8 +929,11 @@ 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). 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.
// 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
// `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)
)