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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user