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
99 lines
4.4 KiB
Swift
99 lines
4.4 KiB
Swift
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 }
|
|
}
|
|
}
|