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
+23 -10
View File
@@ -89,7 +89,7 @@ public final class CardAttachments {
/// the very same store method a whole-window drop uses.
public func add() {
guard isEditable, cardFolder != nil else { return }
let urls = Self.chooseFiles()
let urls = AttachmentPanel.chooseFiles(message: "Choose files to attach to this card.")
guard !urls.isEmpty else { return }
// The sandbox's half, `BoardDropContext.commitFileDrop`'s rule: `start` answers false for a
@@ -172,22 +172,35 @@ public final class CardAttachments {
return [cardFolder]
}
// MARK: - The panel
}
/// The multi-select open panel behind Add Attachment **every file type**, because a card's
/// `attachments/` takes anything (01-storage-format.md § Attachments) and a filter here would be
/// this app deciding what the user may keep beside their card.
///
/// Directories are not choosable, which is the panel's own spelling of the same refusal a
/// folder drop gets (`FinderDrop`): the attachment model is flat top-level files.
private static func chooseFiles() -> [URL] {
// MARK: - The panel
/// The multi-select open panel behind every "add a file" affordance in the card window File Add
/// Attachment and the attachments header's plus (card-scoped), and the composer's and inline
/// editor's paperclips (comment-scoped, 05-card-window.md The comments column).
///
/// **Every file type**, because a card's `attachments/` takes anything (01-storage-format.md §
/// Attachments) and a filter here would be this app deciding what the user may keep beside their
/// card. Directories are not choosable, which is the panel's own spelling of the same refusal a
/// folder drop gets (`FinderDrop`): the attachment model is flat top-level files.
///
/// Shared rather than copied per surface for `BoardWriter.importFiles`' reason one layer up: the
/// paperclip is "the section header's add-affordance pattern" and a second panel that happened to
/// allow folders would make that sentence false.
@MainActor
enum AttachmentPanel {
/// - Parameter message: the panel's one line of guidance the only thing that differs between
/// the card-scoped and comment-scoped calls, because it is the only thing that *is* different.
static func chooseFiles(message: String) -> [URL] {
let panel = NSOpenPanel()
panel.canChooseFiles = true
panel.canChooseDirectories = false
panel.allowsMultipleSelection = true
panel.resolvesAliases = true
panel.prompt = "Add"
panel.message = "Choose files to attach to this card."
panel.message = message
guard panel.runModal() == .OK else { return [] }
return panel.urls
}