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:
@@ -17,11 +17,18 @@ import UniformTypeIdentifiers
|
||||
/// parameter and the body column takes none either. This view puts one of them in a frame; the
|
||||
/// arithmetic behind the frame is `CommentsMount`, which is pure and therefore checkable.
|
||||
///
|
||||
/// The sidebar is unchanged by any of it — it is a third pane, it has always been fixed-width, and
|
||||
/// the resize flex still goes to the body and never to the sidebar. **The comments column is no
|
||||
/// longer in that second category** (ruled 2026-08-09): its divider is user-draggable in the beside
|
||||
/// mount, so the flex the body and the comments pane split is now the user's to move, within the two
|
||||
/// floors `CardWindowMetrics.clampedCommentsColumnWidth` keeps either side from crossing.
|
||||
/// The sidebar is unchanged by any of it — it is a third pane, it has always been fixed-width when
|
||||
/// shown, and the resize flex still goes to the body and never to the sidebar. **The comments column
|
||||
/// is no longer in that second category** (ruled 2026-08-09): its divider is user-draggable in the
|
||||
/// beside mount, so the flex the body and the comments pane split is now the user's to move, within
|
||||
/// the two floors `CardWindowMetrics.clampedCommentsColumnWidth` keeps either side from crossing.
|
||||
///
|
||||
/// **Whether it shows at all is its own bit** (View ▸ Show Sidebar, extended for the toolbar-toggle
|
||||
/// card): app-wide, persisted `showSidebar` below, mirrored by the card window's toolbar
|
||||
/// (`CardToolbar`). Hidden, the pane and its divider are simply absent from the `HStack` and the body
|
||||
/// column takes the width back through its own `.infinity` frame — no third state, no collapsed
|
||||
/// sliver, the comments pane's own "costs the window no width while it is not mounted" rule applied
|
||||
/// to the sidebar's visibility instead of the comments pane's mount.
|
||||
///
|
||||
/// ### What this milestone builds, and what it deliberately does not
|
||||
///
|
||||
@@ -117,6 +124,18 @@ struct CardWindowView: View {
|
||||
@AppStorage(AppPreferences.showCommentsKey) private var showComments = true
|
||||
@AppStorage(AppPreferences.commentsBesideBodyKey) private var commentsBesideBody = true
|
||||
|
||||
/// **View ▸ Show Sidebar** — the trailing attributes sidebar's own visibility bit, `showComments`'s
|
||||
/// neighbour and shape exactly (05-card-window.md ▸ Composition, extended for the toolbar toggle):
|
||||
/// app-wide, persisted, read here rather than passed in so this pane and the toolbar item that
|
||||
/// mirrors it (`CardToolbar`) and the View-menu row that mirrors it too (`ShowSidebarCommand`) are
|
||||
/// provably the same bit.
|
||||
@AppStorage(AppPreferences.showCardSidebarKey) private var showSidebar = true
|
||||
|
||||
/// Reduce Motion, read from the environment because this is a view — `Motion`'s own split between
|
||||
/// callers that have one and callers (a menu command, a toolbar item) that reach `Motion
|
||||
/// .prefersReducedMotion` instead.
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
|
||||
/// **The comments column's user-set width**, in points — the divider's memory (05-card-window.md
|
||||
/// ▸ The comments column, extended 2026-08-09). `@AppStorage` for the same reason its two
|
||||
/// neighbours above are: every open card window's divider answers to one figure. `0` is the
|
||||
@@ -203,14 +222,25 @@ struct CardWindowView: View {
|
||||
contentPanes
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
|
||||
Divider()
|
||||
// **View ▸ Show Sidebar** (`ShowSidebarCommand`, `CardToolbar`): the whole trailing
|
||||
// column, divider included, comes and goes with the one bit — the body/comments
|
||||
// columns above take the width back through their own `.infinity` frames, exactly as
|
||||
// they do while the pane is merely narrow. A structural reflow, the Show Trash
|
||||
// re-divide's own voice (`Motion.structural`), with the pane itself sliding off the
|
||||
// trailing edge it lives on (`Motion.cardSidebarTransition`) rather than just
|
||||
// vanishing.
|
||||
if showSidebar {
|
||||
Divider()
|
||||
|
||||
sidebar
|
||||
// Fixed, and the one place it comes from.
|
||||
.frame(width: CardWindowMetrics.sidebarWidth(bodyPointSize: bodyPointSize))
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
.background(.background.secondary)
|
||||
sidebar
|
||||
// Fixed, and the one place it comes from.
|
||||
.frame(width: CardWindowMetrics.sidebarWidth(bodyPointSize: bodyPointSize))
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
.background(.background.secondary)
|
||||
.transition(Motion.cardSidebarTransition(reduced: reduceMotion))
|
||||
}
|
||||
}
|
||||
.animation(Motion.structural(reduced: reduceMotion), value: showSidebar)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,6 +427,44 @@ struct CardWindowView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - View ▸ Show Sidebar
|
||||
|
||||
/// View ▸ Show Sidebar (checkmark toggle, **no default chord**) — the trailing attributes sidebar's
|
||||
/// visibility (05-card-window.md ▸ Composition, extended for the toolbar toggle).
|
||||
///
|
||||
/// `ShowCommentsCommand`'s shape and reasons exactly: one bit, app-wide and persisted, the checkmark
|
||||
/// reading exactly that bit so the row never lies, and it mirrors the toolbar's own Show Sidebar item
|
||||
/// (`CardToolbar`) the way View ▸ Show Trash mirrors its toolbar twin — a menu command with a second
|
||||
/// face, never two implementations of one.
|
||||
///
|
||||
/// Validation is **scope and nothing else**, `ShowCommentsCommand`'s own posture: with no card window
|
||||
/// in front there is no focused value and the row disables, and the read-only lock plays no part —
|
||||
/// showing or hiding a pane is not a mutation. `\.cardAttachments` is read only as the "a card window
|
||||
/// is frontmost" signal `AddAttachmentCommand` already uses; the sidebar this toggles shows attachments
|
||||
/// among its other sections, but the row does not otherwise touch that handle.
|
||||
struct ShowSidebarCommand: View {
|
||||
|
||||
@FocusedValue(\.cardAttachments) private var attachments
|
||||
@AppStorage(AppPreferences.showCardSidebarKey) private var isShown = true
|
||||
|
||||
static func isEnabled(_ attachments: CardAttachments?) -> Bool {
|
||||
attachments != nil
|
||||
}
|
||||
|
||||
/// The toggle's binding, `ShowTrashCommand.isVisible`'s own shape: the setter routes through
|
||||
/// `AppPreferences.setShowCardSidebar` — the one write path this bit's two faces share — rather
|
||||
/// than writing `$isShown` directly, which is what makes the animated reflow the toolbar item
|
||||
/// gets true of this row too.
|
||||
private var isVisible: Binding<Bool> {
|
||||
Binding(get: { isShown }, set: { AppPreferences.setShowCardSidebar($0) })
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Toggle("Show Sidebar", isOn: isVisible)
|
||||
.disabled(!Self.isEnabled(attachments))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The window-wide drop
|
||||
|
||||
/// Attaches the whole-window file drop, or nothing at all.
|
||||
|
||||
Reference in New Issue
Block a user