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:
@@ -127,6 +127,23 @@ public enum AppPreferences {
|
||||
/// stale-from-a-future-build value indistinguishable from a legal one downstream.
|
||||
public static let boardZoomLevelKey = "boardZoomLevel"
|
||||
|
||||
// MARK: The print profiles
|
||||
|
||||
/// **The named print profiles** (11-command-nexus.md ▸ File ▸ Print…; the printing card's closing
|
||||
/// line, "these configurations probably good to persist (as named print profiles) and reused").
|
||||
/// Read and written by `PrintProfileStore`, which owns their rules; the key is declared here with
|
||||
/// its neighbours for `WindowID`'s reason.
|
||||
///
|
||||
/// The value is JSON `Data`, not a plist tree — see that type's persistence note. It is app-side
|
||||
/// and never board data: a print profile is how this user likes to read on paper, which no
|
||||
/// collaborator and no agent has any business round-tripping.
|
||||
public static let printProfilesKey = "printProfiles"
|
||||
|
||||
/// **The reserved "Last Used" pseudo-profile's content** — the options the last print ran with, so
|
||||
/// ⌘P opens on what the user did last. Absent until the first print, which reads as the factory
|
||||
/// defaults (`PrintProfileStore.lastUsed`).
|
||||
public static let printLastUsedKey = "printLastUsed"
|
||||
|
||||
// MARK: The appearance override
|
||||
|
||||
/// **View ▸ Appearance** (11-command-nexus.md) — Auto / Light / Dark, app-wide and persisted
|
||||
@@ -289,6 +306,12 @@ public final class AppModel {
|
||||
/// one question.
|
||||
public let appearance: AppearanceStore
|
||||
|
||||
/// The app-wide named print profiles (11-command-nexus.md ▸ File ▸ Print…). Owned here for
|
||||
/// `appearance`'s reason exactly: app-scoped, persisted beside its neighbours, and reached by File ▸
|
||||
/// Print… — a menu row, which lives outside every scene's environment and therefore receives this
|
||||
/// object rather than looking a store up.
|
||||
public let printProfiles: PrintProfileStore
|
||||
|
||||
/// The app's one drag session (DRAG-REORDER.md; 04-interactions.md ▸ Drag and drop).
|
||||
///
|
||||
/// App-wide for the reason cross-board drags exist at all: **a drag crosses windows**, so the
|
||||
@@ -612,6 +635,7 @@ public final class AppModel {
|
||||
styleRecents = StyleRecents(defaults: preferences)
|
||||
zoom = BoardZoomStore(defaults: preferences)
|
||||
appearance = AppearanceStore(defaults: preferences)
|
||||
printProfiles = PrintProfileStore(defaults: preferences)
|
||||
clipboard = ClipboardStore(stagingRoot: clipboardStagingRoot)
|
||||
// Read once here rather than lazily, so File ▸ Open Recent is populated from the app's first
|
||||
// menu pass — a launch that restores boards never shows welcome, and a submenu that filled
|
||||
|
||||
Reference in New Issue
Block a user