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:
@@ -13,7 +13,7 @@ import SwiftUI
|
||||
/// where it lands:
|
||||
///
|
||||
/// - the title as an editable field (commit on Return / focus loss, Escape abandons),
|
||||
/// - the body's Preview/Edit pairing and the raw-source outlet,
|
||||
/// - the raw-source outlet,
|
||||
/// - the sidebar's five sections, which are section *headers* here and nothing more.
|
||||
///
|
||||
/// The placeholders are structural rather than apologetic: the sidebar's inventory and its order are
|
||||
@@ -25,9 +25,28 @@ import SwiftUI
|
||||
/// The sidebar has a fixed width from `CardWindowMetrics`; the body column takes `.infinity`. That
|
||||
/// is the whole of "the window's resize flex goes to the body" — no split view, no stored divider
|
||||
/// position, nothing for a drag to disagree with.
|
||||
///
|
||||
/// ### Why the title no longer scrolls with the body
|
||||
///
|
||||
/// The body surface is a hosted `NSScrollView` (`CardBodySurface`), because ⌘F's find bar lives in
|
||||
/// one — "Edit ▸ Find (⌘F) is find-in-text here … the standard find bar" (05 ▸ Preview). A scroll
|
||||
/// view inside a scroll view is a scroll view that fights, so the column's header — the title and
|
||||
/// its created/modified line — sits above the body's scroller rather than inside it. 05 fixes the
|
||||
/// column's *order* ("Body column, top to bottom") and the columns' independent scrolling, and both
|
||||
/// still hold; which of the two things scrolls the title away was never settled, and pinning the
|
||||
/// card's name over its own body is the better reading of a window whose subtitle already follows it.
|
||||
struct CardWindowView: View {
|
||||
|
||||
let card: Card
|
||||
/// The card's folder on disk — what relative images and links in the body resolve against
|
||||
/// (05 ▸ Preview). `nil` only where a caller has no board root to build it from.
|
||||
let cardFolder: URL?
|
||||
/// This window's body-column state: which mode it is in, and the find-bar hook.
|
||||
let bodyPresentation: CardBodyPresentation
|
||||
/// Whether a checkbox may write — `false` under the board's read-only lock.
|
||||
let isEditable: Bool
|
||||
/// Commits a checkbox flip: the marker's byte offset in the body, and the state the user saw.
|
||||
let onToggleTask: (Int, Bool) -> Void
|
||||
|
||||
/// The body font's point size, read once per body evaluation: every measurement in this view —
|
||||
/// the sidebar's width, both gutters, the vertical rhythm — is derived from it, so they scale
|
||||
@@ -53,8 +72,8 @@ struct CardWindowView: View {
|
||||
|
||||
/// Title, the quiet created/modified line, then the body — 05's top-to-bottom order.
|
||||
private var bodyColumn: some View {
|
||||
ScrollView(.vertical) {
|
||||
VStack(alignment: .leading, spacing: bodyPointSize * 0.75) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: bodyPointSize * 0.5) {
|
||||
// m6-card-body: the title *field* — large and borderless, committing to frontmatter
|
||||
// on Return or focus loss, clearing to remove the `title` key, Escape abandoning to
|
||||
// the on-disk title. Read-only here; the placeholder rendering is already final.
|
||||
@@ -71,20 +90,48 @@ struct CardWindowView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
|
||||
// m6-card-body: Preview/Edit proper — a rendered preview with live task-list
|
||||
// checkboxes, and a syntax-highlighted raw editor behind ⌘E. Plain selectable text
|
||||
// until then: honest about being unrendered rather than half-rendering Markdown.
|
||||
if !card.body.isEmpty {
|
||||
Text(card.body)
|
||||
.font(.body)
|
||||
.textSelection(.enabled)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(CardWindowMetrics.gutter(bodyPointSize: bodyPointSize))
|
||||
.padding(.horizontal, CardWindowMetrics.gutter(bodyPointSize: bodyPointSize))
|
||||
.padding(.top, CardWindowMetrics.gutter(bodyPointSize: bodyPointSize))
|
||||
|
||||
if bodyPresentation.mode == .edit {
|
||||
editPlaceholderNotice
|
||||
}
|
||||
|
||||
CardBodySurface(
|
||||
body: card.body,
|
||||
mode: bodyPresentation.mode,
|
||||
cardFolder: cardFolder,
|
||||
presentation: bodyPresentation,
|
||||
isTaskToggleEnabled: isEditable,
|
||||
onToggleTask: onToggleTask
|
||||
)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
// **The opening rule, applied once** (05 ▸ Mode grammar): a card opens in Preview unless its
|
||||
// body is empty, in which case it opens straight into Edit. `openIfNeeded` is what makes it
|
||||
// "once" — a later reload that empties the file must not drag a reader into Edit.
|
||||
.task { bodyPresentation.openIfNeeded(body: card.body) }
|
||||
}
|
||||
|
||||
/// The Edit mode's honest placeholder.
|
||||
///
|
||||
/// **The mode is real; the editor is not.** 05's opening rule is not a rendering detail that can
|
||||
/// wait — it decides which surface a brand-new card lands on — so this milestone implements the
|
||||
/// *state* (`CardBodyMode`, the opening rule, the toggle) and leaves the editor itself to the
|
||||
/// Edit card. What shows meanwhile is the raw Markdown, monospaced and read-only, over a line
|
||||
/// that says so: a text view that looked editable and silently discarded keystrokes would be a
|
||||
/// worse lie than an empty pane, and one that saved would be this milestone building the thing
|
||||
/// it deliberately is not building.
|
||||
private var editPlaceholderNotice: some View {
|
||||
Text("Body editing arrives with the Edit surface — this is the raw Markdown, read-only.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, CardWindowMetrics.gutter(bodyPointSize: bodyPointSize))
|
||||
.padding(.vertical, CardWindowMetrics.previewPadding(bodyPointSize: bodyPointSize))
|
||||
.background(.background.secondary)
|
||||
}
|
||||
|
||||
/// "Created ⟨date⟩ · Modified ⟨date⟩ · by ⟨modified-by⟩", **omitting whichever keys are absent**
|
||||
|
||||
Reference in New Issue
Block a user