import SwiftUI // MARK: - View ▸ Show Comments /// View ▸ Show Comments (checkmark toggle, **no default chord**) — 11-command-nexus.md; /// 05-card-window.md ▸ The comments column. /// /// ### One bit, app-wide, persisted — and the checkmark *is* the bit /// /// > **View ▸ Show Comments** is a checkmark toggle à la Show Trash, and its choice is **app-wide and /// > persisted across restarts**. One bit, no content-derived auto-show: checked, every card window /// > carries the pane (a comment-less card shows the empty thread and the composer — the invitation /// > is the point); unchecked, threads and drafts are out of sight until the user says otherwise, the /// > Show Trash bargain. The checkmark reads the bit — the menu never lies. /// /// `@AppStorage` is that sentence with no machinery under it: the row's `isOn` reads the very default /// every card window's pane reads, so there is no per-window mirror to keep in step and no auto-show /// rule that could disagree with the tick. "Deleting the last comment never closes the pane" needs no /// code at all for the same reason — nothing but this row writes the key. /// /// **No App Group.** The suite is `UserDefaults.standard`, which the sandbox already scopes to this /// one app (`AppPreferences`); the group suite the 2026-07-29 ruling named went with the App Group /// itself when the edition split collapsed. /// /// Validation is **scope and nothing else**: with no card window in front there is no `cardComments` /// focused value and the row disables. The read-only lock is deliberately not part of it — showing a /// pane is not a mutation, exactly as entering Edit is not (`EditBodyCommand`). struct ShowCommentsCommand: View { @FocusedValue(\.cardComments) private var comments @AppStorage(AppPreferences.showCommentsKey) private var isShown = true /// The row's validation, as a value a test can hold — `EditBodyCommand.isEnabled`'s shape, for /// its reason: a menu item's `.disabled` is otherwise only observable by driving the menu bar. static func isEnabled(_ comments: CardComments?) -> Bool { comments != nil } var body: some View { Toggle("Show Comments", isOn: $isShown) .disabled(!Self.isEnabled(comments)) } } // MARK: - View ▸ Comments Beside Body /// View ▸ Comments Beside Body (checkmark toggle, **no default chord**) — "checked = side-by-side /// (default), unchecked = body over comments; app-wide, persisted" (11-command-nexus.md; /// 05-card-window.md ▸ Composition). /// /// `ShowCommentsCommand`'s shape exactly, and for its reasons — one persisted bit, the checkmark /// reading it, scope-only validation. The two rows sit together because they are the pane's two /// user-facing facts and 11 lists them adjacent. /// /// It stays enabled while Show Comments is off. The row is a *layout* preference, not a second /// visibility switch, and a user arranging their window before turning the pane on is doing something /// perfectly ordinary — the alternative (disabling it) would also make the checkmark lie about a bit /// that is still stored and still applies the moment the pane appears. struct CommentsBesideBodyCommand: View { @FocusedValue(\.cardComments) private var comments @AppStorage(AppPreferences.commentsBesideBodyKey) private var isBeside = true static func isEnabled(_ comments: CardComments?) -> Bool { comments != nil } var body: some View { Toggle("Comments Beside Body", isOn: $isBeside) .disabled(!Self.isEnabled(comments)) } } // MARK: - File ▸ Add Comment /// File ▸ Add Comment (**no default chord**) — card window, all tiers (11-command-nexus.md; /// 05-card-window.md ▸ The comments column). /// /// ### One gesture, two effects, in the one order that works /// /// > **File ▸ Add Comment** flips the bit on when it's off (the gesture *is* the user choosing to see /// > comments — same persistence) and focuses the composer in one gesture. /// /// The write comes first and the focus request second, because the composer does not exist to be /// focused until the pane is mounted. The request is a counter on the window's handle rather than a /// call into a text view (`CardComments.focusComposerRequests`), so the pane picks it up on the /// update after the one that mounted it — which is the only ordering that survives the pane arriving /// in the same frame. /// /// **Flipping the bit is persisted like any other flip of it.** That is the ruling, stated in the /// design as "the same user choice": a user who reaches for Add Comment has said they want to see /// comments, and a visibility that reverted at the next window would make the row a one-shot. /// /// Validation is scope. The read-only lock is not part of it, `EditBodyCommand`'s rule: this focuses /// a text surface, and 02-architecture.md keeps editor buffers alive under the lock (only their saves /// suspend), so a locked board can still be typed into and its text copied out. What the lock does /// disable is the composer's Post button and its paperclip, in place, where they are. struct AddCommentCommand: View { @FocusedValue(\.cardComments) private var comments @AppStorage(AppPreferences.showCommentsKey) private var isShown = true static func isEnabled(_ comments: CardComments?) -> Bool { comments != nil } /// What the row does to the two pieces of state it touches, as a pure pair a test can drive: the /// bit's new value, and whether a focus request is owed. /// /// It is extracted for the reason every menu-row rule in this codebase is: "turns it on when it /// is off, and focuses either way" is one sentence with two clauses, and the clause that /// regresses silently is the second one — a row that only focused when it had just turned the /// pane on would look correct in the demo and be wrong every time after. static func effect(isShown: Bool) -> (isShown: Bool, focusesComposer: Bool) { (isShown: true, focusesComposer: true) } var body: some View { Button("Add Comment") { let effect = Self.effect(isShown: isShown) isShown = effect.isShown guard effect.focusesComposer else { return } comments?.focusComposer() } .disabled(!Self.isEnabled(comments)) } }