A picture off the pasteboard becomes a card's file, its hero, or the board's backdrop

⌘V grows an image-data branch, below the app's own clipboard format and refused
outright while a file URL is on the pasteboard: a screenshot or a browser's Copy
Image lands as "Pasted Image.png" in the anchor card's attachments/, through the
very import path Finder file drops and ⇧⌘A take — one bracket, one Finder-style
collision ladder, one set of banners, and the same silence a drop's arrival has.
A card window's ⌘V pastes onto its own card; a focused text field still wins the
selector natively. A file-shaped flavor travels byte for byte, PNG preferred when
several are offered; TIFF and BMP are re-encoded to PNG, being interchange
encodings rather than files anyone wants in a folder.

The hero key gets the setter it was born owing: "Set as Hero" on any image row of
the attachment list, "Remove Hero" on the row that holds it, with menu-bar twins
so the context entry is nobody's only home. It writes as a restyle — one key, one
bracket, one invertible step on the window's own stack — and replaces rather than
refusing, because a card has one hero and the row that has it says Remove instead.

Edit ▸ Paste as Board Background is the same payload's other destination, taking
the existing background.image convention at its word: the picture into the board
folder as "Pasted Background.png", the colour subkey untouched, the generator's
overwrite-our-own-name rule inherited and its echo memo taught to tell the two
producers apart.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-09 02:43:53 -04:00
parent b18f7ca609
commit ca5d45156b
21 changed files with 1792 additions and 42 deletions
+27
View File
@@ -1,5 +1,6 @@
import Foundation
import Testing
import UniformTypeIdentifiers
@testable import Kanban
/// `ClipboardStore`'s own machinery the manifest, the staging lifecycle, the sweep, the
@@ -25,6 +26,12 @@ final class FakePasteboard: ClipboardPasteboard {
private(set) var text: String?
private var data: Data?
/// **What some other app put down**, by type the raw material the image branch classifies
/// (`PastedImage.flavor(hasBoardItems:types:)`). Ordered so `availableTypes()` can answer in a
/// stable order, which is what makes "our preference order wins over the pasteboard's" a claim a
/// test can actually make.
private var foreign: [(type: String, data: Data)] = []
func manifestData() -> Data? { data }
@discardableResult
@@ -32,14 +39,34 @@ final class FakePasteboard: ClipboardPasteboard {
changeCount += 1
data = manifest
self.text = text
// A real write clears the pasteboard first, so anything another app left is gone.
foreign = []
return changeCount
}
func availableTypes() -> [String] {
(data != nil ? [UTType.laneworkClipboard.identifier] : []) + foreign.map(\.type)
}
func data(forType type: String) -> Data? {
if type == UTType.laneworkClipboard.identifier { return data }
return foreign.first { $0.type == type }?.data
}
/// Another app copied: ownership moves, our type is gone, the counter advanced.
func takeOver() {
changeCount += 1
data = nil
text = nil
foreign = []
}
/// Another app put *these* flavors down a screenshot, a browser's Copy Image, a Finder copy.
/// `takeOver`'s shape with a payload: the counter advances and our own type goes, because that is
/// what `clearContents()` does to it.
func seed(_ payloads: [(type: String, data: Data)]) {
takeOver()
foreign = payloads
}
}