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:
File diff suppressed because one or more lines are too long
@@ -25,8 +25,8 @@ Toolbars are **pure enhancement**: every function they host already has a menu i
|
||||
|
||||
## Card face
|
||||
|
||||
- Leading icon + title. The only face chip in scope is **attachments** (a quiet indicator when the card has files — the title dominates). Metadata chips (labels/assignees/due) went to the enhanced schema with their fields — out of scope.
|
||||
- **No body excerpt** (settled): the face stays title-only — the old "iterate on the card face later" item is closed with no growth.
|
||||
- Leading icon + title. Two face chips are in scope, both present-only and the same secondary-tinted, decorative styling family: **attachments** (a quiet glyph when the card has files — the title dominates) and **comments** (ruled 2026-08-09, card e729e30a — a quiet bubble glyph plus its count when the card has one or more, joining the attachments chip at the row's trailing edge). Metadata chips (labels/assignees/due) went to the enhanced schema with their fields — out of scope.
|
||||
- **No body excerpt, but not "closed with no growth"** (re-settled 2026-08-09 — that old phrasing is retired as stale): the face still never draws a preview of the card's own prose, and that stays closed for good. What *has* grown since is exposure of facts the card already carries structurally, never content: the hero banner (below) and the comments chip both draw from a fact — a named attachment, a folder count — that a sighted glance or a screen reader already had another way to learn, not from anything a body excerpt would have shown.
|
||||
- **Hero image** (ruled 2026-08-09): a card whose `hero` key names one of its own attachments (a bare filename — 01-storage-format.md § Frontmatter's card table) draws it as a **banner across the full plate width above the icon-and-title row**, aspect-fill cropped to a fixed em-scaled band (2.75 em — 36pt at the standard body, so it scales with system text size and board zoom like every other face figure). Opt-in and hand-written: **no in-app setter this version** — "Set as Hero" from the attachment row is future work. A key that is absent, malformed, or names a file that is missing, unreadable or not an image renders **exactly as no key at all** — the band has no height until a picture actually decodes, so the degrade is structural rather than a branch. Everything else the face draws is unchanged and sits outside the band: the accent stripe runs the plate's full leading edge over it, the selection and file-hover strokes ring the whole plate, the cut/drag dims cover it, and the drop model registers the plate's real height — a hero card is simply a taller card. The trash draws it too (the one-face rule). The drag replica carries the band when the picture is already decoded, which it is for any card the user can see to drag.
|
||||
- **Titles are optional at every level.** On cards and lanes, a missing `title` renders as a quiet placeholder ("Untitled", secondary styling) wherever the title would appear. On boards, the fallback is the folder name (sans extension), never "Untitled" — see 01-storage-format.md's board-naming rule; window title and welcome recents show `title` when present, folder name otherwise.
|
||||
- Attachments on the face: **the chip only — there is no face carousel** (resettled 2026-07-28, reversing the carry-over): the pathfinder's selection-keyed in-place expansion — compact unselected, media carousel when sole-selected — **proved undesirable and does not carry over**. A card has **one presentation**: selection changes styling (the selection treatment), never geometry, so the masonry never reflows on click and a card face is the same object whatever the selection state. The attachment chip above is the face's whole attachment story; viewing media is the card window's job (⌘↩ / double-click — the attachments section and QuickLook, 05-card-window.md). The earlier 2026-07-28 carousel settlements (metrics, tick paging, clamp, dots, marquee suppression, trash exclusion) are superseded with it — recorded on their Resolved cards.
|
||||
|
||||
@@ -10,7 +10,7 @@ The stance is committed in 00-vision.md: **accessibility is a requirement of "na
|
||||
|
||||
## The board through VoiceOver
|
||||
|
||||
- **Tree shape**: window → lanes (accessibility containers, in lane `order`) → cards (leaf elements, in card `order`). A lane container is labeled "⟨title⟩, lane, N cards" — the count reads the search filter like the visible badge (04-interactions.md). The lane header's collapse chevron is a labeled child ("Collapse ⟨lane⟩"). A card is **one flattened element**: label = title (or the untitled placeholder), value carries the attachment count when present, selected state via trait. Face icon and chips are decorative — folded into the element, never separately focusable: the flattened element carries the attachment count in its value, and the accessible attachment surface is the card window's keyboard-native section (below); the face itself has no media presentation (03-board-ui.md's no-carousel resettlement).
|
||||
- **Tree shape**: window → lanes (accessibility containers, in lane `order`) → cards (leaf elements, in card `order`). A lane container is labeled "⟨title⟩, lane, N cards" — the count reads the search filter like the visible badge (04-interactions.md). The lane header's collapse chevron is a labeled child ("Collapse ⟨lane⟩"). A card is **one flattened element**: label = title (or the untitled placeholder), value carries the attachment count and the comment count when present (comments joined 2026-08-09, card e729e30a — 03-board-ui.md § Card face), selected state via trait. Face icon and chips are decorative — folded into the element, never separately focusable: the flattened element carries both counts in its value, the accessible attachment surface is the card window's keyboard-native section (below), and the accessible comment surface is the card window's comments column ("Comments, N" — Comments below); the face itself has no media presentation (03-board-ui.md's no-carousel resettlement).
|
||||
- **Logical order, not masonry position** (decided): within a wide lane, VoiceOver reads cards by `order` — the interior grid columns are presentation only. This deliberately diverges from on-screen geometry (narrowed by the 2026-07-31 column-major masonry: walking down one column now *is* consecutive `order`; the divergence that remains is a geometry-sorted reading-order sweep — left-to-right, then down — which interleaves the columns); the spatial arrow-key model (04-interactions.md) remains available alongside, since board keyboard navigation keeps working with VoiceOver running.
|
||||
- **VO cursor and app selection are independent** (Finder-style): moving the VoiceOver cursor never mutates selection. VO-Space on any selectable element — card or lane header — toggles its selection (the ⌘-click analogue — a toggle, never plain click's replace, 04-interactions.md ▸ Selection; ruled 2026-07-29: a replace would silently wipe a multi-element selection, and one uniform VO-Space rule means the user never has to know the element kind to predict Space); ⌘↩ opens the card window; arrow keys and ⇧-arrows drive selection exactly as without VoiceOver. Selection state is always readable from the element (trait), and cut cards expose their dimmed pending state in the value ("cut, pending paste").
|
||||
- **Actions come from the context menu.** Context menus are the single inventory of per-item actions (Open, Rename, Delete, width stepper, …), reachable the standard way (VO-⇧-M); where SwiftUI additionally surfaces menu items as custom accessibility actions, that's free improvement, not a separate design surface. **The custom-action cut** (confirmed 2026-07-29): every plain button row of an item's context menu becomes a custom action; rows that open their own accessible surface (Style…'s popover) and non-action controls (the quick-style swatch picker) stay menu-only — the context menu remains the full inventory either way. Each action must call the same method as its menu row, so the two surfaces cannot drift.
|
||||
|
||||
Reference in New Issue
Block a user