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:
@@ -897,6 +897,12 @@ struct CardFaceView: View, Equatable {
|
||||
/// which, again, this very face rendering already rules out. Reduces to `!isReadOnly &&
|
||||
/// imagePayload != nil` (`pasteImageEnabled`).
|
||||
///
|
||||
/// **The Paste row's dynamic title (`pasteTitle`, 2026-08-09 ▸ card 36cce96a) rides the same
|
||||
/// channel `pasteEnabled` already opened** rather than adding one: it reads
|
||||
/// `appModel.clipboard.payload` again, the identical Observable property, and hands it to
|
||||
/// `ClipboardManifest.pasteMenuTitle(for:)` — a pure function of a value already in hand, not a
|
||||
/// second store read. No new subscription, no selection or snapshot touched.
|
||||
///
|
||||
/// Copy and Cut lean on the same style of proof: `targetIDs` is always non-empty (at minimum this
|
||||
/// card alone) and always homogeneous cards on the board side (`SelectionGrammar.mixesKinds`
|
||||
/// answers `true` only for the trash — see its own doc comment), so `canCopy`/`canCut`'s
|
||||
@@ -940,7 +946,7 @@ struct CardFaceView: View, Equatable {
|
||||
.disabled(!copyEnabled)
|
||||
Button("Cut") { appModel.clipboard.cut(from: store, targeting: clipboardTarget) }
|
||||
.disabled(!store.acceptsBoardMutations)
|
||||
Button("Paste") { appModel.clipboard.paste(into: store) }
|
||||
Button(pasteTitle) { appModel.clipboard.paste(into: store) }
|
||||
.disabled(!pasteEnabled)
|
||||
Menu("Copy Special") {
|
||||
Button("Copy Link") { copyLink() }
|
||||
@@ -1094,7 +1100,7 @@ struct CardFaceView: View, Equatable {
|
||||
.disabled(!copyEnabled)
|
||||
Button("Cut") { appModel.clipboard.cut(from: store, targeting: clipboardTarget) }
|
||||
.disabled(!store.acceptsBoardMutations)
|
||||
Button("Paste") { appModel.clipboard.paste(into: store) }
|
||||
Button(pasteTitle) { appModel.clipboard.paste(into: store) }
|
||||
.disabled(!pasteEnabled)
|
||||
Button("Send to Trash") { deleteTargets() }
|
||||
.disabled(!store.acceptsBoardMutations)
|
||||
@@ -1249,6 +1255,16 @@ struct CardFaceView: View, Equatable {
|
||||
store.acceptsBoardMutations && appModel.clipboard.payload != nil
|
||||
}
|
||||
|
||||
/// **The Paste row's title** — "Paste Card"/"Paste N Cards" and their lane twins, plain "Paste"
|
||||
/// for anything else (`ClipboardManifest.pasteMenuTitle(for:)`'s own doc comment carries the full
|
||||
/// rule). Reads exactly the same `appModel.clipboard.payload` `pasteEnabled` already reads above —
|
||||
/// the render-safety proof this file's type comment states for `pasteEnabled` covers this property
|
||||
/// too, since it is the identical Observable read, not a second one: no new subscription, no
|
||||
/// `store.selection`/`store.snapshot` touched.
|
||||
private var pasteTitle: String {
|
||||
ClipboardManifest.pasteMenuTitle(for: appModel.clipboard.payload)
|
||||
}
|
||||
|
||||
/// Paste Image into Card's enablement, `pasteEnabled`'s own reduction: `canPasteImage(intoCard:in:)`
|
||||
/// only ever answers `false` on its snapshot lookup for a card that is not a live board item, which
|
||||
/// this face rendering at all already rules out — see the type comment's "Render-safety" section.
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user