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:
@@ -127,6 +127,41 @@ public enum AppPreferences {
|
||||
/// `double(forKey:)` reading safe here in a way `boardZoomLevel`'s note warns it usually is not.
|
||||
public static let commentsColumnWidthKey = "commentsColumnWidth"
|
||||
|
||||
// MARK: The attributes sidebar's visibility
|
||||
|
||||
/// **View ▸ Show Sidebar** — the card window's trailing attributes sidebar (05-card-window.md ▸
|
||||
/// Composition), `showComments`'s shape and reasons exactly: one bit, app-wide, persisted across
|
||||
/// restarts, and the checkmark (and the toolbar toggle that mirrors it, `CardToolbar`) reads
|
||||
/// exactly this value so neither ever lies.
|
||||
///
|
||||
/// **Default on.** The sidebar has been part of every card window since m6; an unset key must
|
||||
/// read as the window everyone already knows rather than as a surprise collapse the first time
|
||||
/// this preference exists to read.
|
||||
public static let showCardSidebarKey = "showCardSidebar"
|
||||
|
||||
/// Read outside a view — `CardToolbar`'s toggle item, which is AppKit rather than SwiftUI and so
|
||||
/// has no `@AppStorage` of its own (`lastCardWindowSize`'s reason).
|
||||
public static var showCardSidebar: Bool {
|
||||
UserDefaults.standard.object(forKey: showCardSidebarKey) as? Bool ?? true
|
||||
}
|
||||
|
||||
/// Show/Hide Sidebar's whole behaviour — **the one write path both of its faces share**
|
||||
/// (`BoardStore.setTrashVisible`'s precedent: "a toolbar item is a menu command with a different
|
||||
/// face, never a second implementation of it"), so `ShowSidebarCommand`'s Toggle and
|
||||
/// `CardToolbar`'s item call this rather than writing `UserDefaults` each in their own words.
|
||||
///
|
||||
/// **A user-initiated structural change**, so it animates in the structural voice and goes
|
||||
/// instant under Reduce Motion — the Show Trash re-divide's own reasoning, applied to a pane
|
||||
/// leaving a window instead of a lane leaving a strip. Reduce Motion is read from AppKit rather
|
||||
/// than from `@Environment` for the same reason `setTrashVisible` reads it that way: a toolbar
|
||||
/// item's action and a menu row's both run outside any rendered hierarchy.
|
||||
@MainActor
|
||||
public static func setShowCardSidebar(_ shown: Bool) {
|
||||
withAnimation(Motion.structural(reduced: Motion.prefersReducedMotion)) {
|
||||
UserDefaults.standard.set(shown, forKey: showCardSidebarKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// The quick-style row's recently-used backgrounds — an array of palette names / hex strings,
|
||||
/// most-recent-first (03-board-ui.md § Styling ▸ Controls: "Recents are app-wide and persist
|
||||
/// app-side (user preference, never board data)"; 11-command-nexus.md files it under the
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -107,6 +107,13 @@ struct FindSteppingCommands: View {
|
||||
/// The comments pane's two rows join them (11-command-nexus.md lists Show Comments and Comments
|
||||
/// Beside Body between Edit Body and Raw Source): both are live, both are app-wide persisted bits,
|
||||
/// and both are scoped to the card window (`ShowCommentsCommand`, `CommentsBesideBodyCommand`).
|
||||
///
|
||||
/// **Show Sidebar joined at the end** (05-card-window.md ▸ Composition, the toolbar-toggle card):
|
||||
/// the trailing attributes sidebar's own visibility bit, `ShowCommentsCommand`'s shape exactly and
|
||||
/// mirroring the toolbar's own item (`CardToolbar`) the way View ▸ Show Trash mirrors its toolbar
|
||||
/// twin. Appended rather than interleaved with the comments pair, to leave the block 11 already
|
||||
/// documents (Edit Body · Show Comments · Comments Beside Body · Raw Source) in the order it names —
|
||||
/// this row is additive, not a resequencing.
|
||||
struct CardViewCommands: View {
|
||||
var body: some View {
|
||||
EditBodyCommand()
|
||||
@@ -114,5 +121,6 @@ struct CardViewCommands: View {
|
||||
CommentsBesideBodyCommand()
|
||||
RawSourceCommand()
|
||||
FutureCommand(title: "History")
|
||||
ShowSidebarCommand()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +230,16 @@ final class HostedWindowController: NSObject, NSWindowDelegate {
|
||||
window.toolbar = nil
|
||||
}
|
||||
|
||||
/// Re-validates this window's toolbar items against their current predicates, on demand.
|
||||
///
|
||||
/// `WindowToolbarController`'s own observation tracking (`trackValidationState`) only notices
|
||||
/// `@Observable` reads changing; a predicate that reads a plain `UserDefaults`-backed bit instead
|
||||
/// (`AppPreferences.showCardSidebar`, `CardToolbar`) is invisible to it. A no-op before a toolbar
|
||||
/// exists, which covers every window kind that has none.
|
||||
func revalidateToolbar() {
|
||||
toolbarController?.revalidate()
|
||||
}
|
||||
|
||||
// MARK: Title visibility
|
||||
|
||||
/// Hides this window's title from the title bar, leaving the toolbar exactly as it renders today
|
||||
|
||||
Reference in New Issue
Block a user