Build the Raw Source outlet

The escape hatch: View > Raw Source (opt-cmd-E) unmounts the whole
content area for the literal on-disk index.md in a plain monospaced
editor with Cancel/Apply. Raw source is window-level state, not a third
body mode — entry rides setMode(.preview), which flushes the Edit
session by construction, then reads the file fresh; exit reveals
Preview, and an empty body after Apply does not reopen Edit (openIfNeeded
already ran). Apply validates the proposed bytes through the loader's
own card checks — parseDocument's strict UTF-8/BOM rejection, schema,
order — deliberately skipping the uneditable-shape refusal, since a
flow-mapping card is exactly what the hatch repairs; invalid bytes
alert in place with the loader's own error and no bracket opens. The
write is byte-for-byte with no modified stamp and no modified-by clear,
per 01's explicit carve-out — the verbatim contract outranks stamping —
and identical bytes write nothing. Escape cancels, cmd-Return applies,
toggle-off applies too, and cmd-E disables while raw is active via a
testable predicate. Tombstoned targets refuse as vanished: a foreign
delete is never reverted by a stale buffer.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-28 11:25:25 -04:00
parent e989c1f26e
commit 40c0a75c24
12 changed files with 1733 additions and 65 deletions
+31 -11
View File
@@ -9,11 +9,11 @@ import SwiftUI
///
/// The *shell*: the two columns, their scrolling, the sidebar's fixed width, and the read-only
/// renderings of what the loader already knows the card's title and its created/modified line
/// over the body column's two live surfaces (Preview and Edit, `CardBodySurface`). Everything that
/// over the body column's two live surfaces (Preview and Edit, `CardBodySurface`), with the
/// raw-source outlet swapping the pair of columns out entirely when it is active. Everything that
/// reads or writes beyond that is later work and is marked where it lands:
///
/// - the title as an editable field (commit on Return / focus loss, Escape abandons),
/// - 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
@@ -47,6 +47,9 @@ struct CardWindowView: View {
/// it, Preview renders it, and `adopt(diskBody:)` below is where the snapshot gets a say
/// which is exactly the point at which dirty-buffer-wins is decided.
let bodySession: CardBodyEditSession
/// This window's raw-source outlet. While it is active the two columns are gone entirely see
/// `body`.
let rawSource: CardRawSourceSession
/// 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.
@@ -57,18 +60,35 @@ struct CardWindowView: View {
/// together when the system text size changes.
private var bodyPointSize: CGFloat { CardWindowMetrics.bodyPointSize }
/// The two columns **or the raw-source editor in place of both of them**.
///
/// A swap rather than an overlay, which is 05 Raw source outlet's own word for it ("swaps the
/// **entire content area title, body, and sidebar ** for the literal on-disk `index.md`") and
/// what the rule underneath it requires: the same frontmatter is being edited as raw text, so a
/// sidebar still offering to restyle the card, or a title field still writing to `title`, would
/// be two editors racing for one file. Unmounting them is the only version of "they can't fight"
/// that cannot be got wrong later.
///
/// The cost is one thing and it is accepted: the body editor's scroll position and selection do
/// not survive a round trip through source mode, because its text view genuinely goes away. What
/// does survive is the buffer, which is the Edit session's, not the view's and it was flushed
/// to disk on the way in regardless.
var body: some View {
HStack(spacing: 0) {
bodyColumn
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
if rawSource.isActive {
CardRawSourceView(session: rawSource, presentation: bodyPresentation)
} else {
HStack(spacing: 0) {
bodyColumn
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
Divider()
Divider()
sidebar
// Fixed, and the one place it comes from.
.frame(width: CardWindowMetrics.sidebarWidth(bodyPointSize: bodyPointSize))
.frame(maxHeight: .infinity, alignment: .top)
.background(.background.secondary)
sidebar
// Fixed, and the one place it comes from.
.frame(width: CardWindowMetrics.sidebarWidth(bodyPointSize: bodyPointSize))
.frame(maxHeight: .infinity, alignment: .top)
.background(.background.secondary)
}
}
}