A lane's context menu finds its five groups — style, clipboard, width, the fold, and the trash
LaneView.laneMenu/laneActions restructured to the owner's shape (card 22f660b0),
twinning the card menu's own reshape from earlier today (72ca222, card fe66c461):
Rename/Properties…/Style ▸ (Symbol, Color), a divider, then Copy/Cut/Paste, a
divider, then Width — Increase/Decrease/Reset, a divider, then Collapse Lane /
Expand Lane, a divider, then Send to Trash (relabeled from "Delete"). Every row
routes through existing machinery — no new commands, no new store method.
ClipboardStore's targeted copy(from:targeting:)/cut(from:targeting:) — added for
the card menu — are reused verbatim here: a new clipboardTarget computed property
wraps targetIDs (the lane's existing widen-to-selection rule) as an
ItemReferenceSet, exactly the card menu's clipboardTarget one type over. Paste
does not retarget, the card menu's own posture (a destination operation with no
per-item widening precedent). Reset Width is exactly setLaneWidth(lane.id,
units: 1) — the same call Decrease already makes at the floor — which
setLaneWidth's own remove-at-default rule already turns into an absent width
key, so no new write path is needed.
Style ▸ Symbol and ▸ Color both open the one existing style popover (the card
menu's v1 posture, unchanged), and the quick-style recents row is dropped from
this menu for symmetry with the card menu's own drop — StyleMenuItems is no
longer called from LaneView, though it and QuickStyleRow are left in place
(unused, easy to restore) exactly as the card menu's own commit chose to leave
them. Properties… is a disabled placeholder row, the owner's own word, left out
of laneActions since an always-disabled row has nothing to announce a custom
action for.
Unlike CardFaceView, LaneView's Copy/Cut/Paste enablement (copyEnabled/
cutEnabled/pasteEnabled) calls ClipboardStore's real predicates directly rather
than reducing them to selection/snapshot-free forms: this body is already
unconditionally subscribed to store.selection (isSelected) and store.snapshot
(headerInk) every pass — the struct's own "Equality gate" doc section says so —
so nothing new is subscribed, and the predicate cost is paid once per lane
(a handful) rather than once per card (hundreds), the axis the card menu's own
reduction was protecting.
Journaled on the card: Style's one-popover posture and the dropped recents row
both flagged "needs owner review" (mirroring the card menu's own flags);
DESIGN/11-command-nexus.md's Lane row is owed a rewrite, left for the main
session, same as the card menu's commit left its own Card row.
Tests: a new targeted copy/cut test proving ClipboardStore's targeted overloads
work on a lane id (writes a lane manifest, arms a lane pending-cut) — the exact
call LaneView's new Copy/Cut rows make — plus the full existing suite: 3220
tests, 3 pre-existing environmental failures (PointerLatencyTests, confirmed by
isolated rerun, unrelated to this change), all else passing.
Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -746,6 +746,38 @@ struct ClipboardTargetedCopyCutTests {
|
||||
#expect(!harness.clipboard.canCopy(from: harness.store, targeting: target))
|
||||
#expect(!harness.clipboard.canCut(from: harness.store, targeting: target))
|
||||
}
|
||||
|
||||
/// **The lane context menu's own Copy/Cut** (`LaneView`, 2026-08-09 ▸ "redesign context menu for
|
||||
/// lanes") — the identical targeted overloads the card menu's Copy/Cut already exercise above,
|
||||
/// aimed at a lane id instead of a card id for the first time. Nothing in `ClipboardStore` branches
|
||||
/// on kind at the API surface, but `SelectionGrammar.kind(of:in:)` does internally
|
||||
/// (`canCopy(from:targeting:)`'s own guard), so this is worth its own test rather than assumed by
|
||||
/// analogy: a lane target must write a **lane** manifest, and it must ignore an unrelated live
|
||||
/// card selection exactly as the card menu's targeted copy ignores an unrelated live selection.
|
||||
@Test("A targeted copy/cut of a lane writes a lane manifest, ignoring an unrelated live selection")
|
||||
func targetedCopyAndCutWorkOnALaneID() async throws {
|
||||
let harness = try makeClipboardHarness()
|
||||
defer { harness.tearDown() }
|
||||
|
||||
// The live selection names a card the target never mentions — `LaneView.clipboardTarget`'s
|
||||
// own "right-clicking something outside the selection acts on what was clicked" widening.
|
||||
harness.store.select([clipboardCard4], in: .board)
|
||||
let target = ItemReferenceSet(ids: [clipboardLane1], container: .board)
|
||||
|
||||
#expect(harness.clipboard.canCopy(from: harness.store, targeting: target))
|
||||
#expect(harness.clipboard.canCut(from: harness.store, targeting: target))
|
||||
|
||||
harness.clipboard.copy(from: harness.store, targeting: target)
|
||||
await harness.clipboard.stagingSettled()
|
||||
|
||||
let manifest = try #require(harness.clipboard.payload)
|
||||
#expect(manifest.kind == .lane)
|
||||
#expect(manifest.entries.map(\.id) == [Ident.lane1])
|
||||
|
||||
harness.clipboard.cut(from: harness.store, targeting: target)
|
||||
#expect(harness.store.transient.pendingCut.ids == [clipboardLane1])
|
||||
#expect(harness.store.transient.pendingCut.container == .board)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Availability
|
||||
|
||||
Reference in New Issue
Block a user