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
+30 -2
View File
@@ -474,6 +474,23 @@ struct CardWindowHost: View {
.onChange(of: placement.card.attachments, initial: true) { _, names in
attachments.names = names
}
// **The hero is the snapshot's too**, and republished the same way, so the row that
// offers "Remove Hero" is the row the card face is actually banding from (05
// Attachments; 03-board-ui.md § Card face Hero image). A malformed key reads as no
// hero, which is exactly what the face draws.
.onChange(of: placement.card.hero.value, initial: true) { _, hero in
attachments.hero = hero
}
// **V in this window pastes a picture onto this card** (04-interactions.md Clipboard's
// image-data branch). Here rather than inside `CardWindowView` because the availability
// is the clipboard's observable reading and this is where a store, a card id and the
// app-wide clipboard are all in scope at once the same join `configureAttachments`
// makes for the other two writes.
.cardWindowImagePaste(
store: store,
cardID: placement.card.id,
clipboard: appModel.clipboard
)
.onChange(of: Self.cardFolder(root: store.rootURL, placement: placement), initial: true) { _, folder in
// Re-derived from the store's *current* root, `cardFolder`'s rule: a mid-session
// folder rename moves the board, and rows resolving against where it used to be
@@ -693,7 +710,7 @@ struct CardWindowHost: View {
session.rawSourceIsActive = { [rawSource] in rawSource.isActive }
session.rawSourceApply = { [rawSource] in rawSource.applyAndLeave() }
session.rawSourceCancel = { [rawSource] in rawSource.cancel() }
Self.configureAttachments(attachments, store: store, cardID: cardID)
Self.configureAttachments(attachments, store: store, cardID: cardID, undo: session.undo)
Self.configureComments(session.comments, store: store, cardID: cardID, on: session.undo)
}
@@ -811,13 +828,24 @@ struct CardWindowHost: View {
/// `static`, and taking every collaborator as a parameter, for `configureRawSource`'s reason:
/// the target resolution is invisible in a running window until it is wrong, and this shape is
/// what lets a test drive the real wiring rather than a re-typed copy of it.
static func configureAttachments(_ attachments: CardAttachments, store: BoardStore, cardID: ItemID) {
static func configureAttachments(
_ attachments: CardAttachments,
store: BoardStore,
cardID: ItemID,
undo: CardWindowUndo
) {
attachments.importFiles = { [weak store] urls in
store?.importAttachments(urls, toCard: cardID)
}
attachments.removeFile = { [weak store] name in
store?.removeAttachment(named: name, fromCard: cardID)
}
// **The window's own stack**, like every other gesture issued in this window
// (13-native-undo.md Rules two levels): Set as Hero is a restyle, and a restyle made here
// folds into this session's close step rather than landing on the board's.
attachments.setHeroFile = { [weak store] name in
store?.setHero(name, onCard: cardID, on: undo)
}
}
/// Points the raw-source outlet at its card the outlet's three seams (05-card-window.md Raw