Comments, phase 2 — the pane, the composer, and the inline session
The card window recomposes into three componentized panes (body, comments, attributes) with two mounts — beside or body-over-comments at ~3:2 — behind View ▸ Comments Beside Body. View ▸ Show Comments is one persisted app-wide bit, no content-derived auto-show; File ▸ Add Comment flips it on and focuses the composer. The thread renders author lines, edited markers, card-subset Markdown bodies, and read-only Quick Look chips under a count header with the sort- direction control. The composer edits comments/.draft/ on the slow cadence (blur, close, quit, ~30s interval), Escape only moves focus, ⌘↩ posts. Inline edit is a body-edit session in miniature: 700ms debounce, Save/⌘↩ commits, Cancel and Escape revert to session-start bytes, close flushes. File drops within either authoring surface carve out of the window-wide card default into that surface's attachments/; paperclips cover the no-drag path. Close flush runs inline flush, then draft save, then the comments/.trash purge; open sweeps crash residue. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -3,8 +3,22 @@ import UniformTypeIdentifiers
|
||||
|
||||
// MARK: - CardWindowView
|
||||
|
||||
/// The card window's content: **two full-height, independently scrolling columns** — a wide body
|
||||
/// column leading, a narrow attributes sidebar trailing (05-card-window.md ▸ Composition).
|
||||
/// The card window's content: **three componentized panes** — a wide body pane leading, the comments
|
||||
/// pane in the middle when it is shown, and the narrow attributes sidebar trailing
|
||||
/// (05-card-window.md ▸ Composition, re-composed 2026-07-29).
|
||||
///
|
||||
/// ### The composition arranges; the panes do not know about each other
|
||||
///
|
||||
/// > each an independent component with its own scroll, arranged by the window's layout rather than
|
||||
/// > wired to each other; componentization is the rule, so the comments pane mounts beside the body
|
||||
/// > or below it (the layout option) without either pane knowing which.
|
||||
///
|
||||
/// That is enforced here by there being nothing to enforce: `CardCommentsPane` takes no layout
|
||||
/// parameter and the body column takes none either. This view puts one of them in a frame; the
|
||||
/// arithmetic behind the frame is `CommentsMount`, which is pure and therefore checkable.
|
||||
///
|
||||
/// The sidebar is unchanged by any of it — it is a third pane, it has always been fixed-width, and
|
||||
/// the resize flex still goes to the body and never to the two fixed panes.
|
||||
///
|
||||
/// ### What this milestone builds, and what it deliberately does not
|
||||
///
|
||||
@@ -71,6 +85,10 @@ struct CardWindowView: View {
|
||||
/// This window's attachments section: the listing, the selection, and the two writes it starts
|
||||
/// (05 ▸ Attachments).
|
||||
let attachments: CardAttachments
|
||||
/// This window's comments pane: the thread, the composer's draft buffer, and the one open inline
|
||||
/// edit session (05 ▸ The comments column). It lives on the window's *session* so the close flush
|
||||
/// can reach it, which is why it arrives here rather than being made here.
|
||||
let comments: CardComments
|
||||
/// This window's thumbnail memory, held by the host so it outlives a snapshot.
|
||||
let thumbnails: AttachmentThumbnailCache
|
||||
/// The whole-window file drop (05 ▸ Attachments: "the drop surface remains the **whole
|
||||
@@ -79,11 +97,24 @@ struct CardWindowView: View {
|
||||
/// Commits a checkbox flip: the marker's byte offset in the body, and the state the user saw.
|
||||
let onToggleTask: (Int, Bool) -> Void
|
||||
|
||||
/// **View ▸ Show Comments** and **View ▸ Comments Beside Body** — app-wide, persisted, read here
|
||||
/// rather than passed in (05 ▸ The comments column; ▸ Composition).
|
||||
///
|
||||
/// `@AppStorage` because the two bits genuinely are app-wide: every open card window obeys the
|
||||
/// same pair, so a window that took them as parameters would need something above it keeping
|
||||
/// every window in step with a value that has exactly one instance. It is also what makes the
|
||||
/// menu rows' checkmarks and these panes provably the same bit (`ShowCommentsCommand`).
|
||||
@AppStorage(AppPreferences.showCommentsKey) private var showComments = true
|
||||
@AppStorage(AppPreferences.commentsBesideBodyKey) private var commentsBesideBody = true
|
||||
|
||||
/// 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
|
||||
/// together when the system text size changes.
|
||||
private var bodyPointSize: CGFloat { CardWindowMetrics.bodyPointSize }
|
||||
|
||||
/// Where the comments pane sits, when it is shown at all.
|
||||
private var mount: CommentsMount { CommentsMount(besideBody: commentsBesideBody) }
|
||||
|
||||
/// 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
|
||||
@@ -111,10 +142,14 @@ struct CardWindowView: View {
|
||||
@ViewBuilder
|
||||
private var content: some View {
|
||||
if rawSource.isActive {
|
||||
// **All three panes**, comments included: "Raw Source still swaps the entire content area
|
||||
// — all panes, comments included; the raw outlet's rule is unchanged" (05 ▸ The comments
|
||||
// column). The swap encloses the whole composition below rather than any one pane, which
|
||||
// is what keeps that true as the composition grows.
|
||||
CardRawSourceView(session: rawSource, presentation: bodyPresentation)
|
||||
} else {
|
||||
HStack(spacing: 0) {
|
||||
bodyColumn
|
||||
contentPanes
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
|
||||
Divider()
|
||||
@@ -128,6 +163,59 @@ struct CardWindowView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// The body pane and the comments pane, in whichever of the two mounts is current — or the body
|
||||
/// pane alone, when Show Comments is off.
|
||||
///
|
||||
/// **The thread stays visible through body Edit in either mount**, and needs no rule of its own:
|
||||
/// Edit swaps the content of the body pane (`CardBodySurface`), which is *inside* the body column
|
||||
/// here, so nothing about the composition changes when the mode flips. That is the sidebar's own
|
||||
/// precedent, which 05 names when it states the rule.
|
||||
@ViewBuilder
|
||||
private var contentPanes: some View {
|
||||
if showComments {
|
||||
switch mount {
|
||||
case .beside:
|
||||
HStack(spacing: 0) {
|
||||
bodyColumn
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
|
||||
Divider()
|
||||
|
||||
commentsPane
|
||||
// Fixed, like the sidebar: "resize flex always goes to the body, never the
|
||||
// fixed panes" (05 ▸ Composition).
|
||||
.frame(width: CardWindowMetrics.commentsColumnWidth(bodyPointSize: bodyPointSize))
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
|
||||
case .stacked:
|
||||
// The ≈3:2 split needs a height to divide, and a `GeometryReader` is the only way to
|
||||
// have one — `layoutPriority` and flexible frames express *preferences*, and this is
|
||||
// a ratio the design fixes. The body takes its share; the comments pane takes the
|
||||
// remainder, so the divider between them can never leave a gap or overlap.
|
||||
GeometryReader { proxy in
|
||||
VStack(spacing: 0) {
|
||||
bodyColumn
|
||||
.frame(height: mount.bodyHeight(in: proxy.size.height))
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
|
||||
Divider()
|
||||
|
||||
commentsPane
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bodyColumn
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
}
|
||||
}
|
||||
|
||||
private var commentsPane: some View {
|
||||
CardCommentsPane(comments: comments, cardFolder: cardFolder, thumbnails: thumbnails)
|
||||
}
|
||||
|
||||
// MARK: - Body column
|
||||
|
||||
/// Title, the quiet created/modified line, then the body — 05's top-to-bottom order.
|
||||
|
||||
Reference in New Issue
Block a user