Build the style, details, and actions sidebar sections
The sidebar completes: the shared style editor gains a second anchor — StyleEditorLayout carries the geometry (the popover keeps its settled 268/14/7/8 untouched as the default; the sidebar packs columns to its width with no inner scroller) while every well, the batch display, the arrow grammar, and the one applyStyle bracket stay the shared component's. The card anchor is fixed, not tracking: the target is this card, and the fate walk retires the window when the card goes. Details renders every unknown frontmatter key read-only in file order — Card.document already carried them — showing the author's own bytes where the raw span is a value and the engine's rendering for block scalars and empties; reserved enhanced-schema keys are ordinary unknowns, and no keys means no section. Actions: Delete rides the same tombstone bytes as Backspace and drop-on-trash through a one-line seam, says nothing about selection, and lets the fate walk dismiss; Reveal in Finder resolves through the attachment scope so the two paths cannot disagree. History reserves its m7 slot without drawing a header no base board can honor. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - One row
|
||||
|
||||
/// One **Details** row: a frontmatter key the app does not own, and the text it carries
|
||||
/// (05-card-window.md ▸ Details).
|
||||
///
|
||||
/// `id` is the key because the effective frontmatter view has one entry per key — a key written
|
||||
/// twice reads once, at its winning occurrence (`FrontmatterDocument.parse`, the last-wins rule) —
|
||||
/// so there is nothing here for a duplicate to collide with.
|
||||
struct CardDetailRow: Identifiable, Equatable, Sendable {
|
||||
let key: String
|
||||
let value: String
|
||||
|
||||
var id: String { key }
|
||||
}
|
||||
|
||||
// MARK: - The seam
|
||||
|
||||
/// What the Details section shows, as a pure function of a parsed `index.md`
|
||||
/// (05-card-window.md ▸ Details).
|
||||
///
|
||||
/// ### Which keys
|
||||
///
|
||||
/// **Every unknown key, and only unknown keys, in file order.** The set is
|
||||
/// `FrontmatterDocument.unknownFields` unchanged — the schema's eleven names are out, and everything
|
||||
/// else is in, *including* the reserved enhanced-schema names (`labels`, `assignees`, `due`,
|
||||
/// `remote`, …), which "are ordinary unknown keys in this version and appear here like any other —
|
||||
/// no special rendering". Order is the document's, which is the file's: 01-storage-format.md
|
||||
/// preserves key order verbatim and the sidebar honors it, so a hand-written key sits where the hand
|
||||
/// that wrote it put it.
|
||||
///
|
||||
/// Nothing here reaches for a card's folder or re-reads a file: the snapshot's `Card` already
|
||||
/// carries its whole parsed document, so this section shows exactly what the last reload read, like
|
||||
/// every other surface in the window.
|
||||
///
|
||||
/// ### Which text
|
||||
///
|
||||
/// **The rawest honest form the parse can offer, and never an error.** A value written on one line
|
||||
/// renders as the author's own bytes — `rawValue`, which is the span minus the `key:` header, the
|
||||
/// surrounding whitespace and any trailing comment (a comment belongs to the line, not to the
|
||||
/// value). Quotes, hex, an ISO timestamp, a flow collection all read exactly as typed, which is the
|
||||
/// whole point of a section that exists to show what the app did *not* interpret.
|
||||
///
|
||||
/// Two shapes have no single-line source text to show, and both fall back to the engine's own
|
||||
/// reading of the value (`YAMLValue.description`):
|
||||
///
|
||||
/// - **A value spanning several lines** — a block scalar, a multi-line flow collection. Its raw span
|
||||
/// carries YAML syntax the value does not (`|`, `>-`, the continuation indent), so the bytes would
|
||||
/// be a worse answer than the text they encode.
|
||||
/// - **A value with no text at all** — `project:` with nothing after it. The rawest form is the empty
|
||||
/// string, and a row with a key and a blank beside it reads as a bug rather than as YAML's null.
|
||||
///
|
||||
/// Neither branch can throw and neither can fail: the document was parsed before a `Card` existed at
|
||||
/// all, and every reading below is total over `YAMLValue`. "Exotic YAML shapes display best-effort,
|
||||
/// never error" is that, exactly.
|
||||
enum CardDetails {
|
||||
|
||||
/// The section's rows, or `[]` when the card carries no unknown keys — which is also the whole
|
||||
/// of the section's visibility rule ("shown only when any exist"), stated where a test can hold
|
||||
/// it rather than as an `if` in a view.
|
||||
nonisolated static func rows(of document: FrontmatterDocument) -> [CardDetailRow] {
|
||||
document.unknownFields.map { CardDetailRow(key: $0.key, value: display(of: $0)) }
|
||||
}
|
||||
|
||||
/// One field's display text — see the type's doc comment for the rule and its two fallbacks.
|
||||
nonisolated static func display(of field: FrontmatterField) -> String {
|
||||
let raw = field.rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !raw.isEmpty, !raw.contains(where: \.isNewline) { return raw }
|
||||
return field.value.description.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The section
|
||||
|
||||
/// The sidebar's **Details** section: read-only key/value rows for every unknown frontmatter key
|
||||
/// (05-card-window.md ▸ Details).
|
||||
///
|
||||
/// ### There is no edit affordance, and that is the design
|
||||
///
|
||||
/// "Editing is deliberately not offered: the **raw source outlet** is the write path for frontmatter
|
||||
/// the app doesn't own." So these rows are text — selectable, copyable, and nothing else. A field
|
||||
/// here would have to guess a YAML shape for whatever the user typed, into a key whose meaning the
|
||||
/// app has no opinion about; ⌥⌘E edits the file itself, where the guess is the user's.
|
||||
///
|
||||
/// ### Absent, not empty
|
||||
///
|
||||
/// The section disappears entirely on a card with no unknown keys — which is most cards. Contrast
|
||||
/// Attachments, which keeps a hint when empty because it advertises a drop surface the user has to
|
||||
/// be able to find: there is nothing to teach here, and a permanent empty "Details" header would
|
||||
/// imply the card has details it is failing to show.
|
||||
struct CardDetailsSection: View {
|
||||
|
||||
let rows: [CardDetailRow]
|
||||
|
||||
private var pointSize: CGFloat { CardWindowMetrics.bodyPointSize }
|
||||
|
||||
var body: some View {
|
||||
if !rows.isEmpty {
|
||||
VStack(alignment: .leading, spacing: CardWindowMetrics.sidebarRowSpacing(bodyPointSize: pointSize)) {
|
||||
CardSidebarSectionHeader(title: "Details")
|
||||
ForEach(rows) { row in
|
||||
self.row(row)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
/// Key over value rather than key beside value: the sidebar is 26 characters wide, and a
|
||||
/// two-column row would give a `project:` overlay four characters to say `lanework` in. The value
|
||||
/// wraps to as many lines as it needs — the sidebar scrolls, and a truncated value in a section
|
||||
/// whose only job is to show values would be the one thing worse than no section at all.
|
||||
private func row(_ row: CardDetailRow) -> some View {
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text(row.key)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
Text(row.value)
|
||||
.font(.callout)
|
||||
.textSelection(.enabled)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel("\(row.key), \(row.value)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user