Comments join attachments on the card face — a quiet bubble-and-count chip, present-only
A card whose thread holds one comment or more now draws a second trailing chip beside the paperclip: a secondary-tinted bubble glyph plus its count, shown only when the count is above zero (design ruling 2026-08-09, card e729e30a). Same styling family as the attachments chip — caption size, secondary tint, decorative and hidden outright from the accessibility tree — but this one carries a visible count rather than staying icon-only, per the ruling's own "bubble-style SF Symbol + count." It sits after the attachments chip at the row's trailing edge, in both the live title row and the drag replica. The count is a new `Card.commentCount` field the loader fills with a readdir over `comments/`'s identity-shaped children that carry their own `index.md` — `BoardLoader.commentCount(in:)`, built on the same `identityShapedChildren` predicate a trash entry's held-card count already uses. Never a parse: `.draft` and `.trash/` are excluded for free, the same dot-prefixed hidden-entry skip `CommentThread.load` documents for both, so the walk stays exactly the O(cards) shape 01-storage-format.md § Enhanced schema already commits to. Because the count rides inside the `card: Card` parameter `CardFaceView` already takes — not a new parameter of its own — drawing the chip costs nothing beyond a field read on an already-compared value: no new Observable read joins the body, and the equatable gate already covers it via `Card`'s synthesized `Equatable`. The one divergence from the comments pane's parsed count is documented rather than hidden: a comment folder whose `index.md` exists but fails to parse is a `Stray` the thread read excludes by opening and rejecting it, a cost this readdir does not pay. The face may then read one comment high until that folder is fixed or removed — the trade the ruling's "cheap directory-entry count… not a parse" asks for, over paying full parse cost on every card of every load. Every well-formed comment, and every card with no malformed one, agrees with the pane exactly. VoiceOver: `AccessibilityPhrases.cardValue` gains a `comments: Int` parameter, appended after attachments and before the cut-pending phrase — the same left-to-right order the two chips draw in, so a sighted read and a VoiceOver read never disagree about which comes first. The trashed lane row's own call site (an opaque unit with no comments to speak of) passes `comments: 0`. Docs: DESIGN/03-board-ui.md's card-face section describes both chips and retires the stale "closed with no growth" sentence, honestly recording the 2026-08-09 growth (the hero banner landed hours earlier, this chip after it) as exposure of facts the card already carries rather than a body excerpt. DESIGN/10-accessibility.md's flattened-element sentence gains the comment count. DESIGN/01-storage-format.md's Enhanced schema paragraph records the chip as shipped. WISHLIST #9 is marked shipped in place — not renumbered, since #10 and #11 are cross-referenced elsewhere. Tests: CardCommentCountListingTests (BoardLoaderTests.swift) pins the readdir against a synthetic tree — no comments/ folder, an empty one, non-identity-shaped and index-less strays excluded, .draft/.trash/ excluded for free, agreement with CommentThread.load's parsed count in the well-formed case, and the one documented divergence on a malformed index.md. AccessibilityPhrasesTests covers cardValue's new parameter alone, alongside attachments, and all three fragments together. ViewEquatableTests pins that a comment landing on a card is a gate difference. BoardRenderPerformanceTests adds a render-cost guard: one comment added to one card on a hosted 180-card board re-renders a handful of bodies, not the board. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -236,11 +236,14 @@ public struct Lane: Identifiable, Sendable, Equatable {
|
||||
public var isDeleted: Bool { !deleted.isMissing }
|
||||
}
|
||||
|
||||
/// A card: `<root>/<guid>/<guid>/index.md`, plus the *names* of its attachments. Structurally
|
||||
/// still a leaf — `comments/` (future, out-of-scope) and the attachment files' contents live
|
||||
/// alongside `index.md` on disk and are not modeled here; `attachments` is the one thing the
|
||||
/// snapshot reaches inside a card folder for, because two board-window surfaces need it before
|
||||
/// any card window exists (see its own doc comment).
|
||||
/// A card: `<root>/<guid>/<guid>/index.md`, plus the *names* of its attachments and a *count* of
|
||||
/// its comments. Structurally still a leaf — the attachment files' contents and every comment's
|
||||
/// own frontmatter and body live alongside `index.md` on disk and are not modeled here; `comments/`
|
||||
/// stays window-scoped exactly as 01-storage-format.md § Enhanced schema rules ("the board snapshot
|
||||
/// never loads comment content"), and `commentCount` does not change that — it is a readdir, not a
|
||||
/// parse. `attachments` and `commentCount` are what the snapshot reaches inside a card folder for,
|
||||
/// because board-window surfaces need them before any card window exists (see each field's own doc
|
||||
/// comment).
|
||||
public struct Card: Identifiable, Sendable, Equatable {
|
||||
public let id: ItemID
|
||||
|
||||
@@ -293,6 +296,24 @@ public struct Card: Identifiable, Sendable, Equatable {
|
||||
/// `index.md` does — no separate invalidation path to keep honest.
|
||||
public let attachments: [String]
|
||||
|
||||
/// The card's comment count — **a readdir, not a parse** (design ruling 2026-08-09, card
|
||||
/// e729e30a; WISHLIST #9's own suggested shape). Counts `comments/`'s identity-shaped children
|
||||
/// that carry a readable `index.md` (`BoardLoader.commentCount(in:)`, `identityShapedChildren`'s
|
||||
/// pattern) — the same cost class as `attachments` above, so the walk stays O(cards) exactly as
|
||||
/// 01-storage-format.md § Enhanced schema requires. It agrees with `CommentThread.load`'s parsed
|
||||
/// count in the overwhelming case; the one divergence is a comment whose `index.md` exists but
|
||||
/// fails to parse (bad YAML, non-UTF-8), which the thread read excludes as a `Stray` and this
|
||||
/// count does not pay to detect — the face may then read one comment high until that folder is
|
||||
/// fixed or removed. `.draft` and `.trash/` are excluded for free, the way they are everywhere
|
||||
/// else this thread is read: both are dot-prefixed, and the loader's directory listing skips
|
||||
/// hidden entries.
|
||||
///
|
||||
/// The board-window comments pane feeds the face's chip nothing — this field is the one and only
|
||||
/// source, so a chip and the pane it opens onto can never quietly show two different numbers for
|
||||
/// the same reason (the divergence above aside, which is a stray on disk, not a bug in either
|
||||
/// reader).
|
||||
public let commentCount: Int
|
||||
|
||||
/// The full parsed `index.md`; unknown/reserved keys ride along uninterpreted.
|
||||
public let document: FrontmatterDocument
|
||||
|
||||
|
||||
Reference in New Issue
Block a user