Build the welcome screen
The welcome window becomes the real thing: Xcode-style, hidden title bar with background drag, branding and actions left, recents right — rows carrying the board symbol, name, location, and the registry's cached lane/card counts (stamped at close, never a scan at welcome time), sorted by last opened. Launch failures surface row-level per 02: a failure joins its recents row as a warning caption, an unresolvable bookmark renders unavailable with Forget its one affordance, and only a failure with no row to carry it falls back to a compact list; a board opening again heals its row. New Board (Opt-Cmd-N) opens the Pages-style template chooser — shipped with the single Basic template and the m9 seams marked — flowing through the save panel into createBoard/createLane and straight into a board window. Open Recent gains its submenu with Clear Menu (byte-identical to forgetting every row, pinned by test), and File > Duplicate forks the frontmost board to a Finder-style copy sibling: pending work flushes first through the close flush's step two alone (sessions stay open — 09's stated exception), every GUID and tombstone carries (the whole-board carve-out from copies-remint), and the copy opens in its own window while the original stays put. 36 new tests. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
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 `<bundle>/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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user