The Paste row says what it is about to do — one card, one lane, or a count of either

Both context menus (`CardFaceView`'s pointer menu and VoiceOver twin, `LaneView`'s own pair)
twin a plain "Paste" row over `ClipboardStore.paste(into:)`. It now reads the clipboard's own
manifest and titles itself "Paste Card" / "Paste Lane" for a single copied item, "Paste N Cards" /
"Paste N Lanes" for several, and stays plain "Paste" for no app payload — a Finder file copy, a
screenshot, anything else the paste command still accepts but never decodes to a
`ClipboardManifest`.

`ClipboardManifest.pasteMenuTitle(for:)` is the one pure function both menus call — `kind` and
`entries.count` are the whole of it, since `SelectionKind` is singular by construction
(`SelectionGrammar.mixesKinds` refuses a mixed copy), so there is no mixed shape to compose a
plural for. `CardFaceView.pasteTitle` rides the exact Observable read `pasteEnabled` already makes
(`appModel.clipboard.payload`) — no new subscription on a builder that is not lazy.
`LaneView.pasteTitle` reads the same field directly rather than through `canPaste(into:)`, since
this view is already unconditionally subscribed to selection/snapshot and there is no reduction to
preserve.

Edit ▸ Paste on the menu bar stays plain — it is a responder attached to the platform's own Edit
menu row (`ClipboardCommands.boardClipboardCommands`), not a `Button` this app titles, and the
card's scope is context menus only.

Flagged for a follow-up: DESIGN/11-command-nexus.md's Card and Lane rows (lines 110-111) describe
Paste generically and could note the dynamic title.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-09 12:24:54 -04:00
parent 3180e565e8
commit 1817d6f0b9
4 changed files with 111 additions and 4 deletions
+14 -2
View File
@@ -664,7 +664,7 @@ struct LaneView: View, Equatable {
.disabled(!copyEnabled)
Button("Cut") { appModel.clipboard.cut(from: store, targeting: clipboardTarget) }
.disabled(!cutEnabled)
Button("Paste") { appModel.clipboard.paste(into: store) }
Button(pasteTitle) { appModel.clipboard.paste(into: store) }
.disabled(!pasteEnabled)
Divider()
@@ -706,7 +706,7 @@ struct LaneView: View, Equatable {
.disabled(!copyEnabled)
Button("Cut") { appModel.clipboard.cut(from: store, targeting: clipboardTarget) }
.disabled(!cutEnabled)
Button("Paste") { appModel.clipboard.paste(into: store) }
Button(pasteTitle) { appModel.clipboard.paste(into: store) }
.disabled(!pasteEnabled)
Button("Increase Width") { store.setLaneWidth(lane.id, units: units + 1) }
.disabled(!store.acceptsBoardMutations)
@@ -838,6 +838,18 @@ struct LaneView: View, Equatable {
appModel.clipboard.canPaste(into: store)
}
/// **The Paste row's title** `ClipboardManifest.pasteMenuTitle(for:)`'s own doc comment carries
/// the full rule ("Paste Card"/"Paste N Cards" and their lane twins, plain "Paste" otherwise),
/// shared verbatim with `CardFaceView.pasteTitle`. Reads `appModel.clipboard.payload` directly
/// rather than routing through `canPaste(into:)` as `pasteEnabled` does above: this body is already
/// unconditionally subscribed to `store.selection`/`store.snapshot` (`laneMenu`'s own
/// "Render-safety" section), so there is no render-safety reduction to preserve here and no reason
/// to read the payload a second time through a predicate that also checks board mutation and a
/// target this only ever needs the payload itself.
private var pasteTitle: String {
ClipboardManifest.pasteMenuTitle(for: appModel.clipboard.payload)
}
private var headerContent: some View {
HStack(alignment: .firstTextBaseline, spacing: BoardMetrics.laneHeaderSpacing(bodyPointSize: pointSize)) {
Image(systemName: ItemSymbol.name(lane.icon, fallback: ItemSymbol.lane))