Comments, phase 2 — the pane, the composer, and the inline session

The card window recomposes into three componentized panes (body,
comments, attributes) with two mounts — beside or body-over-comments
at ~3:2 — behind View ▸ Comments Beside Body. View ▸ Show Comments is
one persisted app-wide bit, no content-derived auto-show; File ▸ Add
Comment flips it on and focuses the composer. The thread renders
author lines, edited markers, card-subset Markdown bodies, and
read-only Quick Look chips under a count header with the sort-
direction control. The composer edits comments/.draft/ on the slow
cadence (blur, close, quit, ~30s interval), Escape only moves focus,
⌘↩ posts. Inline edit is a body-edit session in miniature: 700ms
debounce, Save/⌘↩ commits, Cancel and Escape revert to session-start
bytes, close flushes. File drops within either authoring surface
carve out of the window-wide card default into that surface's
attachments/; paperclips cover the no-drag path. Close flush runs
inline flush, then draft save, then the comments/.trash purge;
open sweeps crash residue.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-30 20:19:52 -04:00
parent f68ac3668e
commit fe3ffac48e
27 changed files with 4496 additions and 24 deletions
+47
View File
@@ -60,6 +60,53 @@ public enum AppPreferences {
UserDefaults.standard.set(NSStringFromSize(size), forKey: lastCardWindowSizeKey)
}
// MARK: The comments pane's three bits
/// **View Show Comments** "a checkmark toggle à la Show Trash, and its choice is **app-wide
/// and persisted across restarts**" (05-card-window.md The comments column, re-ruled
/// 2026-07-29; 11-command-nexus.md).
///
/// **One bit, and no content-derived auto-show**: checked, every card window carries the pane
/// a comment-less card shows the empty thread and the composer, because the invitation is the
/// point; unchecked, threads and drafts are out of sight until the user says otherwise. The
/// checkmark reads exactly this value, so the menu never lies, and deleting the last comment
/// never closes the pane because nothing but this bit does.
///
/// **Default on.** 05 does not spell a default, and the two candidate readings pull in opposite
/// directions the Show Trash bargain (a secondary surface, default off) against "the invitation
/// is the point" (a pane whose empty state is its whole argument). The invitation wins: a
/// comments feature nobody sees until they find a View-menu row is a feature that is not there,
/// and the user who does not want it turns it off once, forever, which is what the persistence is
/// for.
public static let showCommentsKey = "showComments"
public static var showComments: Bool {
UserDefaults.standard.object(forKey: showCommentsKey) as? Bool ?? true
}
/// **View Comments Beside Body** "checked = side-by-side (default), unchecked = body over
/// comments; app-wide, persisted" (11-command-nexus.md; 05 Composition).
///
/// Default **on**, which 05 does state: "side-by-side is the default".
public static let commentsBesideBodyKey = "commentsBesideBody"
public static var commentsBesideBody: Bool {
UserDefaults.standard.object(forKey: commentsBesideBodyKey) as? Bool ?? true
}
/// The comments header's **sort-direction control** "chronological ascending by default,
/// flippable to newest-first (app-wide, persisted)" (05 The comments column; 11 files it under
/// Configuration controls).
///
/// Stored as "newest first" rather than as a direction so the default is `false` and the plain
/// `bool(forKey:)` reading is the right one the one preference here that does not need to tell
/// "off" from "never set".
public static let commentsNewestFirstKey = "commentsNewestFirst"
public static var commentsNewestFirst: Bool {
UserDefaults.standard.bool(forKey: commentsNewestFirstKey)
}
/// 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