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
+42
View File
@@ -287,6 +287,48 @@ struct ClipboardManifestTests {
)
#expect(manifest.plainText == "First\nUntitled")
}
// MARK: - pasteMenuTitle
private func manifest(kind: SelectionKind, entryCount: Int) -> ClipboardManifest {
ClipboardManifest(
copyID: "abc",
boardRoot: URL(fileURLWithPath: "/tmp/B", isDirectory: true),
kind: kind,
container: .board,
entries: (0..<entryCount).map { entry("item-\($0)") }
)
}
@Test("No app payload titles the row plain 'Paste'")
func pasteMenuTitleNoPayload() {
#expect(ClipboardManifest.pasteMenuTitle(for: nil) == "Paste")
}
@Test("An entryless manifest — unreachable in practice, but defensive — titles the row plain 'Paste'")
func pasteMenuTitleEmptyEntries() {
#expect(ClipboardManifest.pasteMenuTitle(for: manifest(kind: .card, entryCount: 0)) == "Paste")
}
@Test("One copied card titles the row 'Paste Card'")
func pasteMenuTitleSingleCard() {
#expect(ClipboardManifest.pasteMenuTitle(for: manifest(kind: .card, entryCount: 1)) == "Paste Card")
}
@Test("Several copied cards title the row 'Paste N Cards'")
func pasteMenuTitleMultipleCards() {
#expect(ClipboardManifest.pasteMenuTitle(for: manifest(kind: .card, entryCount: 3)) == "Paste 3 Cards")
}
@Test("One copied lane titles the row 'Paste Lane'")
func pasteMenuTitleSingleLane() {
#expect(ClipboardManifest.pasteMenuTitle(for: manifest(kind: .lane, entryCount: 1)) == "Paste Lane")
}
@Test("Several copied lanes title the row 'Paste N Lanes'")
func pasteMenuTitleMultipleLanes() {
#expect(ClipboardManifest.pasteMenuTitle(for: manifest(kind: .lane, entryCount: 2)) == "Paste 2 Lanes")
}
}
// MARK: - Copy and staging