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:
@@ -1852,13 +1852,35 @@ public enum BoardWriter: Sendable {
|
||||
try checkIsDirectory(cardFolder, describedAs: "card folder", operation: batchOperation)
|
||||
try checkIsUUIDShaped(cardFolder, operation: batchOperation)
|
||||
|
||||
let attachmentsFolder = cardFolder.appendingPathComponent(attachmentsFolderName, isDirectory: true)
|
||||
return try importFiles(sourceURLs, intoAttachmentsOf: cardFolder, batchOperation: batchOperation)
|
||||
}
|
||||
|
||||
/// **Steps 2–4 of `importAttachments`, with the shape guard left to the caller** — the copy
|
||||
/// itself, which is identical wherever an `attachments/` folder hangs.
|
||||
///
|
||||
/// It exists because a *comment* has an `attachments/` too (01-storage-format.md § Enhanced
|
||||
/// schema — "a card's anatomy one level down"), and comment attachments author in-window
|
||||
/// (05-card-window.md ▸ The comments column, ruled 2026-07-29). The alternative was a second
|
||||
/// importer beside this one, which is exactly what `CardAttachments`' own note forbids for the
|
||||
/// card level: "one import path, one set of banners, one Finder-style collision rename". The
|
||||
/// generalization is therefore the *smallest* one that keeps that true — the folder shape is what
|
||||
/// differs between a card and a comment, and it is the only thing the callers still decide.
|
||||
///
|
||||
/// Everything the batch promises is here rather than at either entry point: `attachments/` minted
|
||||
/// on first import, each source validated before it is touched, the Finder-style collision-free
|
||||
/// name, the non-atomic copy with best-effort cleanup, and the import receipt.
|
||||
static func importFiles(
|
||||
_ sourceURLs: [URL],
|
||||
intoAttachmentsOf itemFolder: URL,
|
||||
batchOperation: WriteOperation
|
||||
) throws(BoardWriteError) -> [ImportedAttachment] {
|
||||
let attachmentsFolder = itemFolder.appendingPathComponent(attachmentsFolderName, isDirectory: true)
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: attachmentsFolder, withIntermediateDirectories: true)
|
||||
} catch {
|
||||
throw BoardWriteError(
|
||||
operation: batchOperation,
|
||||
path: cardFolder.path,
|
||||
path: itemFolder.path,
|
||||
reason: .io(message: "could not create attachments folder: \(error.localizedDescription)")
|
||||
)
|
||||
}
|
||||
@@ -2325,9 +2347,24 @@ public enum BoardWriter: Sendable {
|
||||
try checkIsDirectory(cardFolder, describedAs: "card folder", operation: operation)
|
||||
try checkIsUUIDShaped(cardFolder, operation: operation)
|
||||
|
||||
guard BoardLoader.attachmentNames(in: cardFolder).contains(name) else { return nil }
|
||||
return try trashAttachment(named: name, fromItemFolder: cardFolder, operation: operation)
|
||||
}
|
||||
|
||||
let fileURL = cardFolder
|
||||
/// `removeAttachment`'s body with the shape guard left to the caller — `importFiles`' split, for
|
||||
/// its reason: a comment's authoring chips carry Remove too, "to the **system** Trash — the
|
||||
/// sidebar row's rule" (05-card-window.md ▸ The comments column), and one Trash-not-delete
|
||||
/// promise is worth more than two implementations of it.
|
||||
///
|
||||
/// The listing check, the never-hard-delete guarantee and the a-name-that-is-gone-is-not-a-failure
|
||||
/// rule all live here, so they hold at both levels without either caller restating them.
|
||||
static func trashAttachment(
|
||||
named name: String,
|
||||
fromItemFolder itemFolder: URL,
|
||||
operation: WriteOperation
|
||||
) throws(BoardWriteError) -> URL? {
|
||||
guard BoardLoader.attachmentNames(in: itemFolder).contains(name) else { return nil }
|
||||
|
||||
let fileURL = itemFolder
|
||||
.appendingPathComponent(attachmentsFolderName, isDirectory: true)
|
||||
.appendingPathComponent(name)
|
||||
var trashedURL: NSURL?
|
||||
|
||||
Reference in New Issue
Block a user