f9f284cac9da38fa7e48b86889c2e8f01488cbff
12
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
eb950ee881 |
Remove the lane header's "+" new-card control
The lane header no longer draws a new-card button. Every other creation path is untouched: File > New Card (Cmd-N), Return on a selected lane, and double-click on lane empty space. - LaneView: drop the button and its overlay slot, leaving the collapse chevron as the header's one piece of trailing chrome. - NewCardTarget: drop the button's click-names-target carve-out over the Cmd-N target rule (Return on a selected lane is now the only direct-target path left). - AccessibilityPhrases: drop the button's spoken label. - BoardMetrics: drop its width reserve; laneHeaderTrailingReserve now reduces to the collapse chevron's own reserve. - DESIGN/03, 04, 10, 11: update the lane title-bar inventory, the Cmd-N override clause and search-clearing mechanism list, the accessibility tree-shape sentence, and the pointer-grammar row that described the button. - Tests: drop the two pinned accessibility-label tests and the newCardButtonReserve assertions; retarget the truncation-headroom test at the now-solo laneHeaderTrailingReserve; fix stale doc-quote comments and the manual accessibility-verification checklist. Full KanbanTests suite: 2807 tests, 482 suites, all passing. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy |
||
|
|
bab456c08d |
Collapsible lanes — frontmatter-backed slim strips outside the width division
A lane folds to a fixed slim vertical strip carrying its glyph, its card-count badge and its title turned on its side, and the strip is deliberately not part of the window's division: the expanded lanes' units divide what is left once each folded strip's fixed width has come off the top, so folding a lane is a re-divide trigger of the Show/Hide Trash family — the window never moves and the siblings grow into what the lane gave up. The state is a first-class lane frontmatter key, `collapsed: true`, and document state exactly as `width` is: the files are the board, so an agent folds a lane by writing one key. Absent means expanded, expanding removes the key rather than writing `false` (the remove-at-default family beside a one-unit `width`, the empty rename's `title` and the None well's `background`), and the lane's `width` rides along untouched so expanding restores the lane the user had. The read is `width`'s leniency one type over — a boolean scalar or a quoted boolean word reads as itself, everything else has no reading at all and renders as expanded, bytes preserved either way. Toggling is the header's always-visible collapse chevron, the lane context menu's single Collapse Lane / Expand Lane row, and a plain click anywhere on the strip; a modified click on the strip stays the ordinary selection grammar, so a folded lane is still selectable by pointer. The title reads bottom-up and is justified to the top of the room below the strip's chrome (owner ruling 2026-08-08), truncating against the strip's own height. While folded the lane draws no cards at all, which is what makes every exclusion true by construction rather than by a guard per gesture: no card face means no marquee target and no navigation frame, and no registered grid means the masonry's drop zones have nothing to resolve against. What did need code is the half that names absolute destinations — the option-arrow jumps and the arrow seed scan past a folded lane, the lane domain's down-arrow is inert on one, and New Card skips it (a selection inside one falls through to the last-active lane, the stale selection's rule). A drop on the strip appends at the lane's end, cards and Finder files alike, with an accent edge standing in for the shadow the strip has no masonry to open; there is no hover-to-auto- expand yet. Lane reorder works on the strip, and a dragged folded lane carries its fold, so its shadow and its replica are the strip rather than its units. The write is `writeLaneWidths` clause for clause — one `updateIndex` bracket, the same stamp behaviour, the same three do-nothing paths — with two new `WriteOperation` cases and two new undo verbs rather than one of each, because a banner or an Edit-menu row that said "resize" after Collapse Lane would name a control the user never touched. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy |
||
|
|
cdba512512 |
The seams unbind — the provider is always native, and the git stack compiles dead
Step 4 of strategy/01-git-excision.md, the entangled one: AppModel's makeHistoryProvider collapses to the native provider (the seam stays injectable per the reversibility posture), the session's git state and its wiring go (wireGitUndo, wireBranchSwitching, the card-session staging threading), BoardStore sheds commitSeam and the identity-history ranker (the loader's nil-safe rung now tops out at birth date — today's no-git behavior), SessionSettleGate keeps the gate and inherits the path utility it borrowed, BoardRegistry drops the persisted operation stamp (decode-safe), and the git banner family leaves BannerCenter with its announcer and accessibility phrases. One missed harvest tie severed (the narrator's root subject is its own now). Nothing outside Kanban/Git/ references the stack — proven by sweep. 2,855 tests green. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy |
||
|
|
cfee4a4b41 |
The board learns to zoom — eight rungs on one ruler, and Actual Size is the untouched board
View ▸ Zoom In / Zoom Out / Actual Size (⌘+ / ⌘− / ⌘0): 75%–200% in eight rungs, app-wide and persisted (the Show Comments precedent) — a viewing comfort, not a property of any one board. The level travels as BoardZoomContext in the environment, injected on BoardView alone so the banner strip, search bar, sheets and popovers stay at the system size; the environment is also what carries it through CardFaceView's equality gate, which compares nothing that moves with the level. Every BoardMetrics figure follows zoom.bodyPointSize — card and lane chrome, drag replicas and the count badge, the resize handle, the trash column — and the drop registry carries the ruler for event-time reads, with the autoscroller's three reaches turning font-derived (reachSide named as the stripGap it always equalled). Lanes still divide the window; zoom never moves the window or its floor. The toolbar gains a catalog-only Zoom In/Out pair mirroring the menu rows' predicate; zoom holds shut mid-drag (frozen geometry), each rung announces itself to VoiceOver, and the render suite pins both invariants: a rung repaints every face, a no-op Actual Size repaints nothing. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy |
||
|
|
0d8ecdb78b |
An unopenable repository fails loudly — the standing row, the paused surface, the honest heal
Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy |
||
|
|
31fee00c73 |
The decision surface — a refused open becomes a live repair, in place
Phase 3 of the decision surface, completing the card (01 ▸ Malformed input, settled 2026-07-31). An attended open's fail-fast walk transforms the loading window's content into one aggregated surface — never a sheet, never a chain: defects grouped by class, each class stated once with its files listed (Reveal in Finder + Open in Editor per row), a class-level default preselected, per-item override behind a disclosure. Only honest choices: YAML and malformed-schema get Editor + Re-check (Skip below the root); newer-than-app gets Skip alone and blocks the board at the root; the two root repairs — minted index, schema: 1 stamp — are defaults. Repair and Open applies fixes in one store-less write bracket and re-walks: clean proceeds, remainder re-aggregates into the same surface. Cancel and ⌘W retire to welcome's row; restored opens never see the surface at all (OpenOrigin rides the PendingOpen carrier). Skips are per-open consent that rides the session — the store retains the skip set and every reload passes it — and the opened board posts a warning-tone notice naming what was left out, each item's Reveal riding the banner strip's new reveal control. On Pro boards the repair bracket binds its own EchoLedger, heal-marks everything, and the store adopts it before the committer starts, so repairs land as one separate commit authored Lanework Integrity — pinned end to end. Also fixed en route: a retired loading window left its close interception installed and returned false from windowShouldClose forever, blocking quit. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97 |
||
|
|
0933ac1b01 |
Every open passes through a real loading window — the walk moves off the main actor
Phase 2 of the decision surface: the pre-snapshot loading state ruled 2026-07-29 (02 § Launch and window lifecycle), built. The board window appears immediately at its saved frame, titled with the registry record's cached name, its content a centered spinner behind an injectable ~200ms grace — no skeletons, and the first snapshot snaps in place. The tree walk runs off-main via BoardStoreRegistry.acquireOffMain (per-board single-flight keyed by file identity — concurrent opens of one root share a walk, restoration of many boards is genuinely parallel), landing in BoardStore's new designated init(rootURL:loaded:); the self-walking init survives as a convenience for its ~470 callers. ⌘W during the walk is real: configureWindow split into a loading half (frame restore, frame tracking, close interception — installed before the walk) and a store half (toolbar, widget, hideTitle, undo — installed at the snap), and the walk lives in an explicitly held BoardOpenWalk so the user's close and SwiftUI's teardown end in one cancel(). Cancellation is discard-on-completion: nil from acquireOffMain means nothing was built, nothing retained, and no open-now flag was ever set. Failure keeps today's sequence exactly: record the launch failure, welcome's row carries it, the window retires. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97 |
||
|
|
9588f7b1f0 |
Comments, phase 3 — search, the thread find, announcements, and a11y
Board search reaches comment bodies through a search-owned transient
index: the first live-query keystroke sweeps comments/*/index.md
off-actor (.draft and comments/.trash excluded), keystrokes re-filter
in memory, the index discards on clear — the snapshot stays O(cards).
⌘F routes by focus: the comments pane gets an app-owned find bar
spanning the whole rendered thread (next/prev cross rows with
wraparound); body and composer keep NSTextFinder; Find Next/Previous
graduate from FutureCommands. Foreign comment changes speak
path-shaped beside the announcer's ladder ("New comment on 'X'",
plural folds), narrowed by EchoLedger receipts consumed through
CommentPath.classify — and that read fixed a latent footprint bug
where a comment receipt resolved against the card's attachment
listing, read .absent, and classified the user's own write as
foreign. The pane completes its a11y story: flattened comment
elements with Edit/Delete/Reveal custom actions (un-flattening
during inline edit), phrase-table vocabulary, labeled composer and
sort control, and an audit over the open pane on a comment-seeded
fixture (runnable only where automation permission exists).
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
|
||
|
|
f7c8088783 |
Lanes delete into the trash — rendering, grammar, drag, clipboard, a11y
Phase 2 completes the lanes-in-trash card. TrashEntry merges the trash's two kinds by rank in exactly ONE place (ItemPath.resolve's own merge deleted in favor of it — the three-merge-points finding shrinks instead of growing). TrashLaneRowView renders the opaque row — tertiary plate, level-default lane glyph never the lane's own icon, title + card count, no accents, no expansion; the column badge counts rendered rows. Selection grammar: kind-homogeneous trash selections — ranges skip the other kind, ⇧-extension stops at the kind boundary, plain arrows walk the merged order, marquee stays card-only (now load-bearing: rows register frames for arrows), Select All card-scoped; successor-on-purge crosses kinds like navigation as the interim for open Gap 7b5cbc90. Drag: TrashDrop accepts lane sessions (drop on shown trash deletes), restoreLanes routes a trash-sourced strip drop as an arrival-ranked within-board move with an undo step. Clipboard: ⌘X/⌘V lane restore via opaque lane subjects; fixed boardRoot(ofLaneFolder:) returning .trash as the root — a same-board restore looked like an import and would have reminted the lane it was restoring (pinned by test). A11y: row = one flattened "title, deleted lane, N cards" element with Delete/Reveal actions; BoardDiff crossings read lanes as deleted/restored, shown-trash churn digested at row level. Agent guide stays v7 — the literal already teaches lanes-trash-by-move and kind stamping; drift-guard pins those lines. README trash paragraph notes lanes. Both schemes 1893 tests / 322 suites green. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
5880838e66 |
Build the EchoLedger - per-file write provenance for announcements
User-ruled 2026-07-29: the ledger builds now in base, pre-release (DESIGN/02 - Components - EchoLedger; DESIGN/10 - Live board announcements). Receipts drop inside BoardWriter's four disk primitives (atomic replace, folder move, removal, attachment copy) into a @TaskLocal ledger that BoardStore.performWrite binds for the bracket's duration - no call-site bookkeeping, and performWholesale deliberately binds nothing per 02's bracket exemption. Classification is a pure function of two snapshots: an item whose folder, index.md bytes, or attachment listing differs is an observed change; disk matching the receipt is app-mediated (receipt consumed), no receipt or mismatch is foreign. Byte-identical foreign overwrites classify app-mediated (unobservable, accepted); a foreign edit over a fresh app write classifies foreign. The announcer now consumes per-file facts on every reload origin - the WatchOrigin gate is gone (ReloadFacts.origin removed outright; nothing read it after the gate fell). Reconciling sweeps announce their receipt-less findings as foreign, closing both interim holes (debounce-window absorption, reconcile silence). The vanishing-focus rung gates on the ledger too: "deleted externally" would be a lie about an app-mediated delete, and the subject's own verdict decides. Divergence flagged: attachment imports hash the landed file right after FileManager.copyItem rather than during the copy (the bytes do not stream through the app); an unreadable read-back records nothing, the direction that biases toward foreign. 30 ledger tests added, announcer suite reworked to the ruling. 1638 green on both schemes. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
c339b4cecf |
Implement live accessibility announcements
The board speaks when files change under the user, per DESIGN/10 § Live board announcements. BoardDiff is the pure snapshot summarizer (identity sets for cards/lanes added/edited/moved/deleted — ids, not tallies, so pro-m1's semantic commit engine can build on it; edited = rendered content only, moved beats edited, implied events don't steal the subject). BoardAnnouncer is the decision seam: focusOutcome computes the vanishing-focus sentence and the walk-up-then-sideways recovery (next lane by order, else previous, board container only when none remain, never the trash); speech(for:) is the one-sentence precedence ladder — raised condition > bracket completion > cleared condition > vanished focus > digest — foreign-only for the last two rungs, so app-mediated echoes stay silent. BoardStore.land assembles ReloadFacts and posts exactly one sentence per reload through the injectable announce outlet (AccessibilityAnnouncer, medium priority, never interrupting). Selection recovery layers on top of ItemReferenceSet re-resolution — survivors veto, the emptied selection lands on the vanished item's lane and re-arms ⌘N's active-lane memory. performWholesale(announcing:) arms a completion phrase consumed by the closing reload — nil on every base bracket today; pro-m1 fills git phrasings. Locks raised outside the reload path (vanished root, unwritable location) announce through the same ladder, and the banner strip is a labeled "Board status" container whose row labels are the announced sentences (AccessibilityPhrases.bannerLabel — one string for eye and ear). Announcements classify at reload granularity (WatchOrigin) as a deliberate interim: DESIGN/02's EchoLedger (per-file classification, the announcer's specified input, git-free) was scheduled with the auto-committer that the edition split moved to pro-m1 — filed on the Redesign board for a ruling. 1533 unit tests green, both schemes build. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
273c182ef4 |
Build the VoiceOver tree and actions
The board window's accessibility tree per DESIGN/10: lanes are containers labeled "<title>, lane, N cards" (filter-aware count = renderedCards, the badge's own collection); cards are one flattened element each — label = title or the untitled placeholder, value = attachment count + "cut, pending paste", selection via trait; face icon, stripe, and paperclip are decorative and hidden. Masonry never leaks into traversal: slots carry order-keyed accessibilitySortPriority, so a wide lane reads by card order, not column-major. Lane titles carry the heading trait for the rotor. VO-Space is the ⌘-click analogue routed through the existing BoardStore.click funnel (SelectionGrammar stays the single answer for toggle and container-boundary rules) — cards and lane headers both. Context-menu rows double as custom accessibility actions, each calling the same private method as its menu row so the surfaces cannot drift; trash cards expose Delete and Reveal in Finder and never Open. The trash column is pinned last via sort priority 0, its label/value re-routed through the new AccessibilityPhrases seam; toggling trash visibility posts a one-line announcement from the store seam (both command faces). The invisible lane-resize drag strip leaves the tree — the stepper and menu items are the accessible width path. AccessibilityPhrases is the pure vocabulary seam (labels, values, plural folding shared with TrashModel.phrase), pinned by its own test suite. Both schemes build; 1466 unit tests green. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |