import Foundation /// A board template — what File ▸ New Board… (⌥⌘N) instantiates (09-templates.md). /// /// ### One template today, and that is the shape of this card, not a shortcut /// /// 09 settles both the inventory (all ten pathfinder templates carry over) and the definition /// format, and the format is the interesting part: **a template is itself a board** — a schema-valid /// board folder in the app's resources, read by the same `BoardLoader`, its `index.md` supplying the /// display name (`title`), the picker blurb (the body), the icon, and the chooser position /// (`template.order`). None of that exists yet. What this card ships is the *entry point*: the /// chooser window, the save panel, and a real path from ⌥⌘N to an open board, with exactly one /// template behind it so that path is exercised rather than described. /// // m9-templates: the inventory becomes a walk of `/Templates/*.kanban` plus the user store in // Application Support, each folder loaded through `BoardLoader` — `name`/`blurb`/`icon` off the // template board's own `index.md`, order off its `template.order`, an unloadable user template still // listed (by folder name, marked unloadable, carrying the loader's specifics) but not instantiable. // `laneTitles` stops existing at that point: instantiation becomes a tree copy that skips tombstones, // mints fresh GUIDs, and stamps `created`/`modified` fresh (`BoardWriter.CopyStamps.born`), never // copying `.git`. The chooser's mini preview renders from the loaded `BoardModel` rather than from // these strings. struct BoardTemplate: Identifiable, Sendable, Equatable { /// The bundle folder name a real template would have (`basic.kanban` → `basic`) — 09 calls it /// "the template's stable slug (tests, a11y ids)", so it is the identity here too. let slug: String /// The chooser's display name — a real template's `title`. let name: String /// The chooser's blurb — a real template's `index.md` body, which also becomes the new board's /// description. Nothing is written from it yet: this card creates lanes, not board bodies. let blurb: String /// The board icon shown in the picker and inherited by the new board. let icon: String /// The lanes to create, in order. let laneTitles: [String] var id: String { slug } /// The plain scaffold, and the one template that exists. /// // m9-templates: the bundled `basic.kanban` is 09's "plain To Do / Done scaffold" — two lanes, // not these three. The third is here because a chooser preview with two lanes reads as a mistake // and because this stub's whole job is to prove the create path; when the bundled template // arrives it replaces this value wholesale and 09's inventory is the only source. static let basic = BoardTemplate( slug: "basic", name: "Basic", blurb: "Three lanes to move work through.", icon: ItemSymbol.board, laneTitles: ["To Do", "Doing", "Done"] ) /// Every template the chooser offers, in chooser order. static let all: [BoardTemplate] = [.basic] // MARK: - Instantiation /// Writes this template to `rootURL`: the board's `index.md`, then one lane per title, in order. /// /// **The board's title is the document name the user chose**, not the template's — 09 /// ▸ Instantiation says so, and 01-storage-format.md § Board naming is the reason: display name /// and folder name start out matching, so a board called "Roadmap" on disk is called "Roadmap" in /// its window title. An extension-less name is as legal a board as a `.kanban` one, so the /// extension is stripped rather than required. /// /// Lanes land at `1024`, `2048`, `3072` without this function saying so: each `createLane` call /// appends after the visible siblings the previous one left behind (`Ranks.append(toVisible:)`), /// which is what makes the array's order the board's order. /// /// Separated from the panel and from the window flow deliberately — this is the whole of what /// "instantiate a template" means on disk, and a test drives it against a temp folder without /// going anywhere near `NSSavePanel`. func instantiate(at rootURL: URL) throws(BoardWriteError) { try BoardWriter.createBoard(at: rootURL, title: Self.documentName(of: rootURL)) for title in laneTitles { _ = try BoardWriter.createLane(inBoard: rootURL, title: title) } } /// The document name behind a chosen URL — `~/Boards/Roadmap.kanban` → `Roadmap`. static func documentName(of rootURL: URL) -> String { rootURL.deletingPathExtension().lastPathComponent } }