The card window's ⌘V drops its picture branch — the attachments header now offers one instead

Raw image data on a card window's ⌘V was a keyboard shortcut with no visible
trigger; the sidebar's Attachments header now grows a quiet control — beside
the existing add affordance, present only while the pasteboard holds a
picture this card could take — that pastes it through the exact seam the
retired branch used (ClipboardStore.pasteImage(intoCard:in:), the board's
"Paste Image into Card" row's own call). The file-URL branch stays on ⌘V; a
Finder copy is still unambiguous. CardBodyTextView's paste-yield mechanism
needed no change at all — it forwards by capability, not by picture-specific
logic, so a screenshot ⌘V with the body editor focused is now a genuine
no-op there, served by the new control instead.

The pasteboard's re-read gains a fourth checkpoint — a window becoming key —
alongside menu-tracking, ⌘-down and app activation: a persistent visible
control has to read true continuously while its window is frontmost, not
only at the instant a menu or chord probes it.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-09 10:37:55 -04:00
parent 546fd2840f
commit 200fbce276
8 changed files with 343 additions and 49 deletions
+12
View File
@@ -76,6 +76,18 @@ public final class CardAttachments {
/// gesture was issued on (13-native-undo.md Rules two levels).
public var setHeroFile: ((String?) -> Void)?
/// **The header's paste-image affordance's write** filled by the host with `ClipboardStore
/// .pasteImage(intoCard:in:)`, the same call the card window's own V used before the control
/// replaced it, and the board's "Paste Image into Card" context-menu row still uses
/// (04-interactions.md Clipboard, re-ruled 2026-08-09). One import path behind three pointers
/// at it now, none of them a second implementation.
public var pasteImage: (() -> Void)?
/// **Whether the affordance shows at all** filled by the host with `CardPasteImageAffordance
/// .isVisible(clipboard:cardID:in:)`, read fresh on every view evaluation rather than cached, so
/// the control's presence and `pasteImage()`'s success can never disagree.
public var canPasteImage: (() -> Bool)?
public init() {}
// MARK: - Derived
+32 -1
View File
@@ -80,7 +80,14 @@ struct CardAttachmentsSection: View {
var body: some View {
VStack(alignment: .leading, spacing: CardWindowMetrics.attachmentRowPadding(bodyPointSize: pointSize)) {
CardSidebarSectionHeader(title: "Attachments") { addAffordance }
CardSidebarSectionHeader(title: "Attachments") {
HStack(spacing: 8) {
if attachments.canPasteImage?() == true {
pasteImageAffordance
}
addAffordance
}
}
if names.isEmpty {
emptyHint
@@ -96,6 +103,30 @@ struct CardAttachmentsSection: View {
// MARK: - Header
/// **The paste-image affordance** the V image-data branch's replacement (04-interactions.md
/// Clipboard, re-ruled 2026-08-09): present only while the pasteboard holds a picture this card
/// could take, rather than a permanent row that would spend most of its life disabled. Its
/// visibility and its tap read and call the exact same seam, `CardAttachments.canPasteImage`/
/// `.pasteImage` the header's own `CardPasteImageAffordance.isVisible` so the button can never
/// promise a paste it does not deliver.
///
/// **Present-or-absent, the hero rows' own posture** (below): a disabled picture-paste icon
/// sitting in this header the other 99% of the time a card window is open would be furniture
/// nobody reads, where "it showed up" is the whole point of the control replacing a keyboard
/// shortcut nobody could see.
private var pasteImageAffordance: some View {
Button {
attachments.pasteImage?()
} label: {
Image(systemName: "photo.badge.plus")
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.help("Paste the copied picture as an attachment")
.accessibilityLabel("Paste Image as Attachment")
}
/// The header's **quiet add affordance** "a pointer twin of File Add Attachment, no
/// separate behavior" (11-command-nexus.md), which is why it calls the same method the menu row
/// does rather than opening a panel of its own.