Build lane chrome — title bar, badge, inline rename

The lane title bar becomes real: leading SF Symbol (hand-written names
render leniently, unknown ones fall back to the level default), title
or secondary untitled placeholder, a quiet count badge that counts
exactly the cards the body renders (so the m5 search filter is
followed by construction), and a new-card button. The whole bar is
the reorder drag surface — no grip — with click-vs-movement splitting
select from drag; a pure proposal function maps the drag to an
insertion index and release commits through the Writer's same-parent
degenerate reorder, compacting and retrying when midpoint precision
runs out. Clicking never edits: inline rename is Return on the sole
selected card or Board > Rename for either kind, a third transient
editor beside the placeholder that tracks its target by UUID, commits
on focus loss, discards silently when the target vanishes, and
removes the title key on an empty commit. The new-card placeholder
renders at last — the settled Cmd-N target rule (pure, tested) files
it after the anchor card, at a selected lane's bottom, or into the
last-active lane; Return commits and re-selects the lane, Cmd-Return
also opens the card window, and a failed create discards the overlay.
New Card / New Lane / Rename land in the menus with focused-editor
and read-only validation; rename gets its own WriteOperation case in
the banner vocabulary. 59 new tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-27 08:52:24 -04:00
parent ff3ba298f0
commit b35566e0fe
21 changed files with 2855 additions and 79 deletions
+39
View File
@@ -126,6 +126,45 @@ struct RanksTests {
#expect(Ranks.insertAtHead(ofVisible: items) == 1024)
}
// MARK: - Insertion at a display position
@Test func insertionRankDispatchesOnPosition() throws {
let orders = [1024.0, 2048.0, 3072.0]
// The three cases every insertion gesture has, behind one call.
#expect(Ranks.insertionRank(amongVisible: orders, at: 0) == 0, "head: min 1024")
#expect(Ranks.insertionRank(amongVisible: orders, at: 1) == 1536, "between: the midpoint")
#expect(Ranks.insertionRank(amongVisible: orders, at: 2) == 2560)
#expect(Ranks.insertionRank(amongVisible: orders, at: 3) == 4096, "end: max + 1024")
// The result is always strictly inside the gap it names, which is what makes the display
// order the caller asked for the one it gets.
let placed = try #require(Ranks.insertionRank(amongVisible: orders, at: 1))
#expect(placed > orders[0] && placed < orders[1])
}
@Test func insertionRankIsTotalOnEdgeInputs() {
// An empty parent's first child lands at the board convention, whatever index is asked for.
#expect(Ranks.insertionRank(amongVisible: [], at: 0) == 1024)
#expect(Ranks.insertionRank(amongVisible: [], at: 7) == 1024)
// Out-of-range indices clamp to the two ends rather than trapping: an index arrives from a
// drag's geometry, and geometry can outrun a snapshot.
#expect(Ranks.insertionRank(amongVisible: [1024], at: -3) == 0)
#expect(Ranks.insertionRank(amongVisible: [1024], at: 99) == 2048)
}
@Test func insertionRankReportsAnExhaustedGapRatherThanInventingOne() {
// `nil` is the renumber trigger, not a refusal and it must fire for the duplicate-order
// tie as well as for adjacent Doubles, since neither admits a rank between.
#expect(Ranks.insertionRank(amongVisible: [1024, 1024], at: 1) == nil)
#expect(Ranks.insertionRank(amongVisible: [1024, 1024.0000000000002], at: 1) == nil)
// The ends never exhaust: append and head-insert always have room.
#expect(Ranks.insertionRank(amongVisible: [1024, 1024], at: 0) == 0)
#expect(Ranks.insertionRank(amongVisible: [1024, 1024], at: 2) == 2048)
}
// MARK: - Precision exhaustion renumber, deterministically
@Test func precisionExhaustionThenRenumberIsDeterministic() {