Build Preview mode rendering

The card body's resting state: swift-markdown (pinned 0.8.0, smart
typography off — Preview renders the bytes on disk) parsed into a pure
BodyMarkup model with UTF-8 source offsets, rendered on one hosted
TextKit 1 NSTextView — chosen because find-in-text is NSTextFinder,
checkbox clicks reuse AppKit character hit-testing, links are .link
attributes, and NSTextTable's automatic layout is exactly the
columns-sized-to-contents rule. The GFM subset renders per 05; HTML
stays verbatim code-styled text; relative images resolve against the
card folder while remote URLs are never fetched, drawing a quiet chip
instead. Task checkboxes are live: a click flips exactly one byte
through a fresh-read, refuse-uneditable, stamp, atomic-replace write —
the app's only offset-addressed write, so a moved target refuses as
staleTarget and what the user saw decides the direction, netting one
toggle on a double-click. Empty bodies open in Edit per CardBodyMode's
opening rule, applied once; the Edit surface itself stays an honest
read-only stub until its card. FindCommand prefers the card body's
find over board search when a card window is focused.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-28 09:59:18 -04:00
parent 7f1adf47c5
commit 6dc84176fb
15 changed files with 2608 additions and 18 deletions
+84
View File
@@ -979,6 +979,68 @@ public enum BoardWriter: Sendable {
}
}
// MARK: - Task checkboxes
/// Flips one task-list checkbox in an item's body **a single-byte edit, and the only write
/// in the app that touches the body at all** (05-card-window.md Preview: "clicking a
/// `- [ ]` / `- [x]` checkbox flips exactly that marker in the source a single-character
/// textual edit; every other byte of the body is untouched").
///
/// It is `updateIndex`'s four steps with one addition, and the addition is why it is written
/// out here rather than expressed as an `edits` closure: the flip can **refuse**, and
/// `updateIndex`'s closure cannot. Everything else is identical and deliberately so read
/// fresh from disk, refuse an uneditable frontmatter shape, edit, stamp `modified` and clear
/// `modified-by`, replace atomically. A toggle is "an ordinary user edit the standard atomic
/// write, auto-committed and undoable on git boards" (05), not a special case of anything.
///
/// ### The offset, and why it is re-checked
///
/// `bodyOffset` is a UTF-8 byte offset into the **body** (`FrontmatterDocument.body`, the text
/// after the closing delimiter) naming the character *between* the brackets the offset
/// `BodyTask.markerOffset` carried out of the parse that drew the box the user clicked.
///
/// That parse ran against a snapshot; this call reads disk. In between, an agent, a hand edit
/// or a pull may have rewritten the file the same staleness `updateIndex`'s read-fresh rule
/// exists for, except that here the *target* is a byte offset rather than a key, and a stale
/// key merely rewrites the wrong value while a stale offset would drop an `x` into the middle
/// of a sentence. So `BodyMarkup.flippingTaskMarker` re-verifies the brackets, the marker, and
/// the state the user saw before anything is written, and `.staleTarget` refuses when any of
/// the three has moved. The refusal is loud (the banner) rather than silent: the user clicked
/// a box and it did not tick.
///
/// **`checked` is what the user saw, not what they want** the flip's direction is derived
/// from it, which is what makes a double-click land on one net toggle instead of racing.
public static func toggleTaskMarker(
inItemFolder folder: URL,
bodyOffset: Int,
checked: Bool
) throws(BoardWriteError) {
var operation = WriteOperation.toggleTask(title: nil)
try checkIsDirectory(folder, describedAs: "item folder", operation: operation)
// Lanes and cards both have bodies; a board root's is its description, and no surface
// previews it. The same shape guard every other item write leans on keeps this call off it.
try checkIsUUIDShaped(folder, operation: operation)
let indexURL = folder.appendingPathComponent(BoardLoader.indexFileName)
var document = try readDocument(at: indexURL, operation: operation)
operation = operation.withTitle(document.title.value)
try checkEditable(document, at: indexURL, operation: operation)
guard let flipped = BodyMarkup.flippingTaskMarker(in: document.body, at: bodyOffset, expecting: checked) else {
throw BoardWriteError(
operation: operation,
path: indexURL.path,
reason: .staleTarget(message: "this checkbox is no longer where it was — the file changed")
)
}
document.body = flipped
document.set(FrontmatterKeys.modified, to: .date(Date()))
document.remove(FrontmatterKeys.modifiedBy)
try atomicReplace(text: document.serialized(), at: indexURL, operation: operation)
}
// MARK: - Attachments
/// The one folder this app ever creates under a card every other subfolder under
@@ -1496,6 +1558,14 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
/// never the Finder-renamed one it would have landed under.
case relocateLooseFile(filename: String)
/// A Preview task-list checkbox being ticked or unticked (05-card-window.md Preview).
///
/// Its own case rather than a fold into `.rename`'s or `.style`'s neighbourhood, on the
/// vocabulary's standing reasoning: the user clicked a checkbox, and a banner telling them the
/// app could not "restyle" or "rename" the card would name a gesture that never happened. It is
/// also the app's only *body* write, which is worth being able to see in a log at a glance.
case toggleTask(title: String?)
/// Fills in the title once the Writer has read it off the document the operation is acting
/// on identity for the six cases with no title slot at all: `createBoard`/`createLane`/
/// `createCard` are minting a file, not reading one; `importAttachment`'s "title" is the
@@ -1520,6 +1590,7 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
case .resize: .resize(title: title)
case .rename: .rename(title: title)
case .duplicateBoard: .duplicateBoard(title: title)
case .toggleTask: .toggleTask(title: title)
}
}
@@ -1548,6 +1619,7 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
case .listAttachments: "list attachments"
case .renumberChildren: "renumber children"
case let .relocateLooseFile(filename): "relocate loose file '\(filename)'"
case let .toggleTask(title): Self.phrase("toggle a checkbox in", title)
}
}
@@ -1592,6 +1664,16 @@ public struct BoardWriteError: Error, Sendable, Equatable, CustomStringConvertib
/// permissions, volume error. The destination still holds its previous bytes.
case io(message: String)
/// **The bytes the edit aimed at are not what the caller was shown.** The file read
/// cleanly and its frontmatter parsed this is not `.unreadable` but the surgical
/// target moved: the checkbox at that offset is gone, or is already in the state the
/// click would have produced (05-card-window.md Preview, `toggleTaskMarker`).
///
/// Its own reason because the app's one *offset-addressed* write is the one place where
/// "read fresh from disk" is not enough on its own: every other write names a key, and a
/// key that moved is still the same key.
case staleTarget(message: String)
public var description: String {
switch self {
case let .unreadable(message):
@@ -1600,6 +1682,8 @@ public struct BoardWriteError: Error, Sendable, Equatable, CustomStringConvertib
"frontmatter cannot be edited in place: \(shape.description)"
case let .io(message):
message
case let .staleTarget(message):
message
}
}
}