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
+98
View File
@@ -0,0 +1,98 @@
import Observation
import SwiftUI
// MARK: - The mode
/// Which of the body column's two surfaces is showing (05-card-window.md Mode grammar).
///
/// **Two cases, not three.** The raw-source outlet swaps the *entire content area* title, body
/// and sidebar so it is a state of the window, not of the body column, and it does not belong in
/// this enum. Edit Body disabling while raw source is active (11-command-nexus.md) is that
/// window-level state's rule to enforce over this one.
public enum CardBodyMode: Equatable, Sendable {
/// The rendered, selectable preview **the resting state**.
case preview
/// The raw-Markdown editor.
case edit
/// The mode a window opens its body in: **Preview, unless the body is empty** (05 Mode
/// grammar: "a card opens in Preview unless its body is empty, which opens straight into
/// Edit with the cursor ready (a new card has nothing to preview, so during creation flows
/// title body without a mode stop)").
///
/// Whitespace is empty (`BodyMarkup.isEmpty`): a body holding one newline previews as a blank
/// page, and stopping the user at a blank preview of a blank body is precisely the ceremony
/// the rule removes.
public static func opening(body: String) -> CardBodyMode {
BodyMarkup.isEmpty(body) ? .edit : .preview
}
/// E View Edit Body's checkmark toggle. Also the whole of "Return in Preview enters Edit"
/// and "Escape in Edit returns to Preview": three gestures, one flip, so they cannot drift.
public var toggled: CardBodyMode {
self == .preview ? .edit : .preview
}
}
// MARK: - The window's body surface, as a handle
/// One card window's body column, reduced to what things *outside* it need: which mode it is in,
/// and how to put a find bar over whichever surface currently holds the keyboard.
///
/// `BoardSearchPresentation`'s shape and for its reason one per window, `@State` in the host,
/// published through the focus system so a **menu item** (Edit Find, F) can reach the frontmost
/// card window without anyone keeping a which-window-is-key register. It is deliberately not on
/// `BoardStore`: the store is the *board's*, shared by every window on it, and two card windows
/// open on two cards of one board are in two different modes.
@MainActor
@Observable
public final class CardBodyPresentation {
/// The surface showing right now. Starts in Preview and is settled by `openIfNeeded(body:)`
/// the first time the window has a body to judge.
public var mode: CardBodyMode = .preview
/// Puts the standard find bar over the focused body surface **Edit Find (F) is
/// find-in-text here** (05 Preview; 11-command-nexus.md scopes F "Board window: board
/// search; card window: find-in-text"). Filled in by the surface itself, which is the only
/// thing that holds a text view to hand the action to; `nil` until one exists, which is also
/// exactly when F has nothing to find in.
public var findInText: (() -> Void)?
/// Whether the opening rule has already run for this window.
///
/// **Once, not per snapshot.** The rule is about *opening* a card, and the body it judges
/// arrives with the first snapshot but snapshots keep arriving (a watcher reload, a lane
/// move, another window's edit). Re-running it would drag a reader back into Edit the moment
/// someone else emptied the file, and would fight a user who had just pressed E.
private var hasOpened = false
public init() {}
/// Applies the opening rule the first time it is called, and does nothing on every call after.
@discardableResult
public func openIfNeeded(body: String) -> CardBodyMode {
guard !hasOpened else { return mode }
hasOpened = true
mode = CardBodyMode.opening(body: body)
return mode
}
/// E, Return in Preview, Escape in Edit see `CardBodyMode.toggled`.
public func toggleMode() {
mode = mode.toggled
}
}
/// The focused card window's body column, beside `FocusedValues.boardSearch` see
/// `FocusedBoardStoreKey` for why window-scoped menu items reach their window this way.
struct FocusedCardBodyKey: FocusedValueKey {
typealias Value = CardBodyPresentation
}
extension FocusedValues {
var cardBody: CardBodyPresentation? {
get { self[FocusedCardBodyKey.self] }
set { self[FocusedCardBodyKey.self] = newValue }
}
}