Build the trash quasi-lane
Deletion becomes a two-stage, Finder-style story. File > Delete and plain Backspace tombstone the live selection; View > Show Trash (no chord — Shift-Cmd-T stays with the system's tab bar) reveals the quasi-lane: trailing, one fixed width unit consumed only while shown, hatched dimmed header, count badge, no new-card button, exempt from resize and reorder alike. Its contents are a pure view over the snapshot — the deterministic sort (deleted newest first, folder-name ties, unparseable stamps oldest) interleaves card rows with a tombstoned lane's single entry, whose count names what Put Back returns; the ancestor walk is absolute, so an own-flag card beneath a tombstoned lane has no row and recovery is deliberately two steps. Put Back twins Delete on Cmd-Backspace with validation enabling exactly one; restore fidelity is byte-perfect because nothing ever moved. Delete Immediately confirms exactly where loss is real (every board is mode-none today; the predicate names the git carve-out for m7), Empty Trash always confirms with the true whole-board count, and dragging a tombstoned card onto a live lane restores it there — positional drops and cross-board locality arrive with m5's machinery. The banner's delete phrasing drops "move to the trash" per the naming constraint: board deletion says Delete, "Move to Trash" stays reserved for the system Trash. 47 new tests. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -173,6 +173,67 @@ struct LaneDisplayUnitsTests {
|
||||
#expect(LaneLayoutMath.displayUnits(of: wide) == 40)
|
||||
#expect(LaneLayoutMath.totalUnits(of: [wide]) == 40)
|
||||
}
|
||||
|
||||
@Test("The trash's one fixed unit joins the total only while it is shown")
|
||||
func trashUnitJoinsTheDivision() throws {
|
||||
// 03-board-ui.md § Trash: the quasi-lane "spans a fixed one width unit … consumed only
|
||||
// while shown", and Show/Hide Trash is therefore a re-divide trigger — the window is never
|
||||
// touched, the same width simply divides across one more unit.
|
||||
let loaded = try lanes(widths: ["2", nil, "3"])
|
||||
#expect(LaneLayoutMath.totalUnits(of: loaded) == 6)
|
||||
#expect(LaneLayoutMath.totalUnits(of: loaded, trashUnits: 1) == 7)
|
||||
|
||||
// Shown on a zero-lane board it is the whole division, not a second unit alongside the
|
||||
// empty board's floor of one.
|
||||
#expect(LaneLayoutMath.totalUnits(of: [], trashUnits: 1) == 1)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Hit testing
|
||||
|
||||
/// `laneIndex(atX:…)` — the strip's half of drag-to-restore (03-board-ui.md § Trash). Same lane
|
||||
/// geometry as the layout above: standard 100, gap 12, so with lanes of 1× and 2× units the slots
|
||||
/// run [12, 112), [124, 336) and everything past 348 is the trash's side of the strip.
|
||||
@Suite("LaneLayoutMath ▸ hit testing")
|
||||
struct LaneHitTestingTests {
|
||||
|
||||
private let units = [1, 2, 1]
|
||||
private func hit(_ x: CGFloat) -> Int? {
|
||||
LaneLayoutMath.laneIndex(atX: x, unitCounts: units, standard: 100, gap: 12)
|
||||
}
|
||||
|
||||
@Test("A point inside a lane's slot names that lane, width counted in units")
|
||||
func insideALaneSlot() {
|
||||
#expect(hit(12) == 0)
|
||||
#expect(hit(111.9) == 0)
|
||||
#expect(hit(124) == 1)
|
||||
// The 2× lane swallows the interior gap it spans, so its slot runs 212pt, not 200.
|
||||
#expect(hit(335.9) == 1)
|
||||
#expect(hit(348) == 2)
|
||||
#expect(hit(447.9) == 2)
|
||||
}
|
||||
|
||||
@Test("The margins, the gaps, and everything past the last lane name nothing")
|
||||
func gapsAndMarginsAreNotLanes() {
|
||||
// The outer margin, before the first lane.
|
||||
#expect(hit(0) == nil)
|
||||
#expect(hit(11.9) == nil)
|
||||
// The inter-lane gaps.
|
||||
#expect(hit(112) == nil)
|
||||
#expect(hit(123.9) == nil)
|
||||
#expect(hit(336) == nil)
|
||||
// Past the last lane — which is exactly where the trash quasi-lane sits, so a row dropped
|
||||
// back into the trash writes nothing.
|
||||
#expect(hit(448) == nil)
|
||||
#expect(hit(10_000) == nil)
|
||||
// A negative x (the pointer dragged off the leading edge) is not a lane either.
|
||||
#expect(hit(-5) == nil)
|
||||
}
|
||||
|
||||
@Test("An empty strip has no lane under any point")
|
||||
func emptyStrip() {
|
||||
#expect(LaneLayoutMath.laneIndex(atX: 50, unitCounts: [], standard: 100, gap: 12) == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The snap
|
||||
|
||||
Reference in New Issue
Block a user