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
+3 -2
View File
@@ -766,8 +766,9 @@ struct CardFaceView: View, Equatable {
/// card I right-clicked, regardless of the live selection") turns out to be what was wanted.
///
/// **Paste Image into Card is genuinely per-card** "into card" is the row's own wording, and it
/// is `ClipboardStore.pasteImage(intoCard:in:)`, the *same* method the card window's own V
/// already calls for its attachment branch. It always targets **this** card, never the widened
/// is `ClipboardStore.pasteImage(intoCard:in:)`, the *same* method the card window's own
/// attachments-header affordance calls for its picture (the V image branch it once rode retired
/// 2026-08-09 for that visible control). It always targets **this** card, never the widened
/// selection: like Open, Rename and Style's anchor, "single-card by nature" the existing
/// precedent this SCOPE asks new rows to follow for such rows. " more tbd" is left as the SCOPE
/// asks: one row today, the submenu built to grow.
+51 -17
View File
@@ -79,17 +79,24 @@ extension View {
extension View {
/// **V in a card window pastes onto that card** (04-interactions.md Clipboard, the image-data
/// and file-URL branches; 05-card-window.md Attachments) the file branch first, the picture
/// branch behind it, `pasteAction`'s own precedence one window over.
/// **V in a card window pastes a Finder-copied file onto that card** (04-interactions.md
/// Clipboard, the file-URL branch; 05-card-window.md Attachments) the board's own responder
/// shape, one window over.
///
/// The board's own responder shape, one window over and with two branches instead of three: there
/// is no board payload a card window could paste cards and lanes land on a *board* so the card
/// window answers `paste:` only for the two attachment branches, and only while one of them has
/// something to take. **One combined handler, not two `.onCommand`s for the same selector**: the
/// precedence has to be one expression for the same reason `pasteAction` is, and layering a second
/// responder over the same selector would leave the ordering to however SwiftUI happened to chain
/// them rather than to this file.
/// **The image-data branch retired from here** (re-ruled 2026-08-09 "instead of a special V
/// handler at card window level, add a control that shows up when an image is detected in
/// pasteboard"): raw image data on this window's V is no longer implicitly an attachment. It
/// falls through to whatever the focused surface does natively the body editor's own paste when
/// it can read the pasteboard, nothing at all when it cannot (`CardBodyTextView`'s capability
/// yield, unchanged; a screenshot with the editor focused is now a genuine no-op there). The
/// header's paste-image affordance is the one path left for the picture (`CardAttachments
/// .pasteImage`, wired in `CardWindowHost.configureAttachments`), and the board's own V and its
/// "Paste Image into Card" context-menu row are untouched this file changes for the *card
/// window* only.
///
/// There is no board payload a card window could paste cards and lanes land on a *board* so
/// the card window answers `paste:` only for the file branch, and only while it has something to
/// take.
///
/// **A focused text field still wins, with nothing here doing the arithmetic.** `NSTextView`
/// consumes `paste:` natively, so V in the body editor, the comment composer or an inline
@@ -101,13 +108,21 @@ extension View {
func cardWindowPaste(store: BoardStore, cardID: ItemID, clipboard: ClipboardStore) -> some View {
onCommand(
#selector(NSText.paste(_:)),
perform: Self.cardWindowPasteAction(store: store, cardID: cardID, clipboard: clipboard)
perform: CardWindowPasteRouting.action(store: store, cardID: cardID, clipboard: clipboard)
)
}
}
/// The card window's own `pasteAction` the file branch outranking the picture branch, exactly
/// as `refresh()` orders them.
private static func cardWindowPasteAction(
/// The card window's own `pasteAction` one clause, now that the picture branch answers to a
/// control instead of to this selector (`CardPasteImageAffordance`, below).
///
/// A free type rather than a `View` extension member, unlike `pasteAction` (above) staying where it
/// is: this composition is *the* regression surface for "does V still auto-attach a picture in a
/// card window", 04-interactions.md Clipboard's re-ruling, and a test needs to call it without a
/// throwaway `View` conformer standing in for one.
@MainActor
enum CardWindowPasteRouting {
static func action(
store: BoardStore,
cardID: ItemID,
clipboard: ClipboardStore
@@ -115,13 +130,32 @@ extension View {
if clipboard.canPasteFiles(intoCard: cardID, in: store) {
return { clipboard.pasteFiles(intoCard: cardID, in: store) }
}
if clipboard.canPasteImage(intoCard: cardID, in: store) {
return { clipboard.pasteImage(intoCard: cardID, in: store) }
}
return nil
}
}
// MARK: - The card window's paste-image affordance
/// **The image-data branch's replacement**: a visible control rather than a silent keyboard shortcut
/// (04-interactions.md Clipboard, re-ruled 2026-08-09). The card window no longer answers `paste:`
/// for raw image data at all (`CardWindowPasteRouting`, above); the attachments section header's
/// button is the one path left for it (`CardAttachmentsSection`), and it takes exactly the seam the
/// retired branch took `ClipboardStore.pasteImage(intoCard:in:)`, through `CardAttachments
/// .pasteImage`.
///
/// **The pure seam the control's visibility and its paste share**: `ClipboardStore
/// .canPasteImage(intoCard:in:)` is already the whole predicate the lock, the payload, and the
/// card being on the board side so this is a named alias for it rather than a second reading that
/// could drift from what pressing the button actually does. Named on its own so a test can say what
/// it is asserting ("the affordance shows") without borrowing a name that means "the paste would
/// succeed", even though today the two answers are one call.
@MainActor
enum CardPasteImageAffordance {
static func isVisible(clipboard: ClipboardStore, cardID: ItemID, in store: BoardStore) -> Bool {
clipboard.canPasteImage(intoCard: cardID, in: store)
}
}
// MARK: - Edit Paste as Board Background
/// **Edit Paste as Board Background** the pasteboard's picture into the board folder, with