Print boards and cards with configurable components and named print profiles
⌘P had no story: KanbanApp removed the platform's Print row outright on 11-command-nexus.md's "No Print story in v1 (⌘P unused)" line. That line retires. File ▸ Print… now prints the board in front — lanes left to right, each lane's cards top to bottom, as a linear document rather than a picture of the strip — or, from a card window, that card. The trash is unreachable by construction: it is a sibling container of `lanes`, not a lane. The rules live in a pure layer nothing AppKit can reach. `PrintOptions` is one Codable value carrying the printing card's five bullets — which components (title, icon+labels line, rendered body, comments off by default with either reading order), page breaks, one base face and size every other size derives from, and a toggleable running head and foot. `PrintSource` is what is being printed, frozen at ⌘P so the panel's repeated relayouts and a board reloading underneath cannot disagree. `PrintDocumentBuilder` turns the pair into a block list, which is where every decision a rendered page hides becomes something a test can hold: component order, comment ordering, and page-break markers that are markers rather than whitespace. Empty is empty all the way up — a card with nothing to print consumes no page break, and a lane whose cards all dropped out takes its heading with it. A page break is a pagination fact, not a spacing one. TextKit has no page-break character, so `PrintDocumentView` splits the document into sections at its breaks and flows each into as many page-sized text containers as it needs: a container boundary *is* a sheet boundary, at any paper size with any margins. Bodies come from the app's one Markdown pass — `BodyMarkup.parse` into `BodyMarkupRenderer` — re-faced run by run so the chosen family reaches the text and fixed-pitch code keeps its own, and drawn under a forced light appearance so the card window's dynamic label colours do not print white. Options ride in the print panel's own accessory rather than a pre-flight sheet of ours, which buys the system's live preview of the real paginated document; the preview refreshes through one KVO revision counter rather than thirteen mirrored properties. Profiles persist app-side in UserDefaults, never in board files — a print profile is how this user likes to read, not what a board is (`BoardZoomStore`'s argument). A name is a profile's identity, folded case-insensitively; "Last Used" is reserved in every spelling, kept out of the stored list, and captured when an operation actually ran, so a cancelled print rewrites nothing. Both decoders are total: one unrecognized key must not cost a user every profile they saved. DESIGN/11-command-nexus.md gains the Print row and loses the sentence saying it would never have one. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -285,6 +285,10 @@ struct CardWindowHost: View {
|
||||
/// reason: two card windows on one board have two different selections, and the menu bar reaches
|
||||
/// the frontmost one through the focus system.
|
||||
@State private var attachments = CardAttachments()
|
||||
/// This window's printable subject — the card, its lane, its board and its thread, as File ▸ Print…
|
||||
/// needs them (`CardPrintSubject`). Window-scoped for `CardBodyPresentation`'s reason: two card
|
||||
/// windows are two documents, and the menu bar reaches the frontmost one through the focus system.
|
||||
@State private var cardPrint = CardPrintSubject()
|
||||
/// This window's thumbnail memory. Held here rather than in the section so it survives every
|
||||
/// snapshot the store applies — a cache that died with the view would regenerate every thumbnail
|
||||
/// on every reload (`AttachmentThumbnailCache`).
|
||||
@@ -381,6 +385,9 @@ struct CardWindowHost: View {
|
||||
// know a card window is in front at all (11-command-nexus.md scopes all three to the card
|
||||
// window).
|
||||
.focusedSceneValue(\.cardComments, session.comments)
|
||||
// File ▸ Print… (⌘P) reaches the frontmost card window the same way — the card-window scope
|
||||
// of a row the board window answers with its whole board (11-command-nexus.md).
|
||||
.focusedSceneValue(\.cardPrint, cardPrint)
|
||||
// The raw-source outlet's detailed alert, presented over this window — a validation
|
||||
// refusal on Apply, or a file that could not be opened as source. It hangs *here* rather
|
||||
// than inside the editor because the second of those fires while source mode is still
|
||||
@@ -473,6 +480,21 @@ struct CardWindowHost: View {
|
||||
// would open nothing.
|
||||
attachments.cardFolder = folder
|
||||
session.comments.cardFolder = folder
|
||||
cardPrint.cardFolder = folder
|
||||
}
|
||||
// **File ▸ Print…'s subject, re-derived from every snapshot** for the folder's and the
|
||||
// announcer's reason: a card renamed, restyled, relabelled or moved to another lane prints as
|
||||
// it is now (`CardPrintSubject`). The thread is a closure rather than a value, so ⌘P reads the
|
||||
// comments at the moment it is pressed rather than whatever the pane last saw.
|
||||
.onChange(of: placement.card, initial: true) { _, card in
|
||||
cardPrint.card = card
|
||||
cardPrint.readThread = { store.commentThread(inCard: card.id) }
|
||||
}
|
||||
.onChange(of: placement.lane.title.value, initial: true) { _, title in
|
||||
cardPrint.laneTitle = title
|
||||
}
|
||||
.onChange(of: AppModel.displayName(of: store), initial: true) { _, title in
|
||||
cardPrint.boardTitle = title
|
||||
}
|
||||
// The announcer's subject, re-derived from every snapshot for the folder's reason: a card
|
||||
// renamed mid-session is announced under its new name ("New comment on '⟨card⟩'").
|
||||
|
||||
Reference in New Issue
Block a user