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
+32 -2
View File
@@ -89,6 +89,9 @@ struct CardWindowHost: View {
@State private var windowController = HostedWindowController()
@State private var session = CardWindowSession()
@State private var phase: Phase = .opening
/// This window's body column the Preview/Edit mode, and the F hook a menu item reaches
/// through the focus system (`CardBodyPresentation`).
@State private var bodyPresentation = CardBodyPresentation()
private enum Phase {
case opening
@@ -149,6 +152,10 @@ struct CardWindowHost: View {
// rename retitles the window and a lane move re-subtitles it with no notification of
// our own (05-card-window.md Window).
.navigationSubtitle(windowSubtitle)
// Edit Find (F) is find-in-text in a card window (11-command-nexus.md) the menu
// item reaches the frontmost one's body surface through this, exactly as board-window
// items reach their window's store (`FocusedBoardStoreKey`).
.focusedSceneValue(\.cardBody, bodyPresentation)
.task { start() }
.onChange(of: shouldDismiss, initial: true) { _, dismisses in
guard dismisses else { return }
@@ -163,14 +170,37 @@ struct CardWindowHost: View {
@ViewBuilder
private var content: some View {
if let placement {
CardWindowView(card: placement.card)
if case let .open(store) = phase, let placement {
CardWindowView(
card: placement.card,
cardFolder: Self.cardFolder(root: store.rootURL, placement: placement),
bodyPresentation: bodyPresentation,
// "Under the read-only lock the controls disable in place an in-content mutation
// menu validation can't reach" (05 Preview). The checkbox is that control, and
// the store's own lock is the whole predicate.
isEditable: !store.isReadOnly,
onToggleTask: { offset, checked in
store.toggleTaskMarker(inCard: placement.card.id, bodyOffset: offset, checked: checked)
}
)
} else {
// Nothing to render and nothing worth animating: this window is on its way out.
Color.clear
}
}
/// `<root>/<lane>/<card>` the card's own folder, which is what its body's relative images and
/// links resolve against (05-card-window.md Preview).
///
/// Built off the store's *current* `rootURL` rather than the ref's captured one, for
/// `BoardStore.liveItem`'s reason: a mid-session folder rename moves the board, and a preview
/// resolving images against where the board used to be would quietly stop showing them.
static func cardFolder(root: URL, placement: CardPlacement) -> URL {
root
.appendingPathComponent(placement.lane.id.rawValue, isDirectory: true)
.appendingPathComponent(placement.card.id.rawValue, isDirectory: true)
}
/// Where this window's card is in this board's snapshot, or `nil` when it is not which is the
/// same condition `shouldDismiss` reads, one moment before the window goes.
private var placement: CardPlacement? {