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)") } }