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
This commit is contained in:
2026-08-08 22:53:10 -04:00
parent 57542177c1
commit bab456c08d
33 changed files with 1811 additions and 133 deletions
+49 -7
View File
@@ -138,10 +138,21 @@ enum DropSlotMath {
/// for that shorter list: it is a function of the board's unit total, and a lane in flight is
/// still a lane on the board (DRAG-REORDER.md § The lane strip's resting layout is arithmetic).
static func laneExtents(unitCounts: [Int], standard: CGFloat, gap: CGFloat) -> [ClosedRange<CGFloat>] {
laneExtents(
widths: unitCounts.map { LaneLayoutMath.slotWidth(units: $0, standard: standard, gap: gap) },
gap: gap)
}
/// The same extents over **drawn** widths, which is what a strip holding collapsed lanes has: a
/// slim strip's width is a fixed figure and not a multiple of the standard (03-board-ui.md § Lane
/// Collapsed lanes; `LaneLayoutMath.drawnWidths`).
///
/// The primitive, with the unit-count version above as its wrapper one walk, so a folded lane can
/// only ever move a zone, never change how zones are built.
static func laneExtents(widths: [CGFloat], gap: CGFloat) -> [ClosedRange<CGFloat>] {
var extents: [ClosedRange<CGFloat>] = []
var left = gap
for units in unitCounts {
let width = LaneLayoutMath.slotWidth(units: units, standard: standard, gap: gap)
for width in widths {
extents.append(left...(left + width))
left += width + gap
}
@@ -152,9 +163,16 @@ enum DropSlotMath {
/// widths plus the `n 1` gaps between them. This is the span the trigger regions are capped
/// at, and it is exactly the shadow run's future footprint.
static func laneRunSpan(unitCounts: [Int], standard: CGFloat, gap: CGFloat) -> CGFloat {
guard !unitCounts.isEmpty else { return 0 }
let widths = unitCounts.map { LaneLayoutMath.slotWidth(units: $0, standard: standard, gap: gap) }
return widths.reduce(0, +) + gap * CGFloat(unitCounts.count - 1)
laneRunSpan(
widths: unitCounts.map { LaneLayoutMath.slotWidth(units: $0, standard: standard, gap: gap) },
gap: gap)
}
/// The same span over **drawn** widths a dragged run that includes a collapsed lane occupies that
/// lane's slim strip, not the slot its `width` key would have bought (see `laneExtents(widths:)`).
static func laneRunSpan(widths: [CGFloat], gap: CGFloat) -> CGFloat {
guard !widths.isEmpty else { return 0 }
return widths.reduce(0, +) + gap * CGFloat(widths.count - 1)
}
/// Where a lane drag would land: an index into the ordered live lanes **with the dragged run
@@ -179,12 +197,36 @@ enum DropSlotMath {
standard: CGFloat,
gap: CGFloat,
current: Int?
) -> Int? {
laneSlot(
cursorX: cursorX,
restingWidths: restingUnits.map { LaneLayoutMath.slotWidth(units: $0, standard: standard, gap: gap) },
draggedWidths: draggedUnits.map { LaneLayoutMath.slotWidth(units: $0, standard: standard, gap: gap) },
gap: gap,
current: current
)
}
/// The same answer from **drawn** widths what the board actually asks, since a collapsed lane's
/// slim strip is neither a unit count nor a multiple of the standard (03-board-ui.md § Lane
/// Collapsed lanes).
///
/// A folded lane is an ordinary member of both lists: it occupies one zone in the resting strip like
/// any other lane (so a reorder drags *across* it in one narrow zone rather than several), and a
/// folded lane being dragged contributes its strip's width to the run's span cap, so the trigger
/// region matches the footprint the drop will actually produce.
static func laneSlot(
cursorX: CGFloat,
restingWidths: [CGFloat],
draggedWidths: [CGFloat],
gap: CGFloat,
current: Int?
) -> Int? {
slot(
cursor: cursorX,
extents: laneExtents(unitCounts: restingUnits, standard: standard, gap: gap),
extents: laneExtents(widths: restingWidths, gap: gap),
gap: gap,
draggedSpan: laneRunSpan(unitCounts: draggedUnits, standard: standard, gap: gap),
draggedSpan: laneRunSpan(widths: draggedWidths, gap: gap),
current: current
)
}