import Foundation /// One board template — **a schema-valid board folder that loaded** (09-templates.md ▸ Definition /// format: "A template is itself a board"). /// /// ### There is no template model, only a board model /// /// Everything the chooser shows and everything instantiation reproduces is read off the template /// board's own `index.md`, through the ordinary `BoardLoader`: `title` is the display name, /// `icon`/`iconColor` are the picker icon and what the new board inherits, the **body** is the /// blurb (which becomes the new board's description by simply being copied), and `template.order` /// is the chooser position. This type holds the loaded `BoardModel` and derives those from it — it /// stores no copies, so a template's identity can never drift from its file. /// /// That is 09's "dogfood" clause taken literally: "the template format *is* the board format — no /// second schema, no parallel Swift model to keep in sync". The pathfinder's Swift-struct catalog /// (and the `laneTitles` stub that stood in for it here) is exactly what this replaces. /// /// ### A value you can hold is a template that loaded /// /// The initializer is the load (`TemplateEngine.load(templateAt:origin:)`), so there is no /// "unloadable template" case in this type: an unloadable *user* template is a chooser row, not a /// template — 09 says it is "still listed — by folder name, marked unloadable, carrying the loader's /// fail-fast specifics — but can't be instantiated or previewed", and that listing is the chooser /// card's, built from the loader's error rather than from a half-built value of this type. Anything /// holding a `BoardTemplate` is therefore holding something instantiable. struct BoardTemplate: Identifiable, Sendable, Equatable { /// Which store the template came from — 09's two tiers ("bundled templates by `template.order`, /// then keyed user templates by `template.order`, then keyless user boards last"). Carried /// rather than derived from the URL: the chooser's ordering and its Reveal in Finder affordance /// both turn on the tier, and re-deriving it from a path prefix would be a second answer to a /// question the discovery walk already answered. enum Origin: Sendable, Equatable { case bundled case user } /// The template folder itself — `/Templates/basic.kanban`, or a folder in the user /// store. The instantiation source, and the identity here. let url: URL let origin: Origin /// The template board as the ordinary loader read it. Instantiation does **not** use this — it /// copies the tree on disk — but the chooser's mini per-lane preview renders from it, and every /// derived property below reads it. let model: BoardModel /// A template is identified by where it lives: slugs are stable but not unique across the two /// stores (a user template may legitimately be called `basic.kanban` too), and the chooser's /// selection must never be ambiguous between tiers. var id: String { url.path } /// The stable slug — 09: "The bundle folder name (`basic.kanban`) is the template's stable slug /// (tests, a11y ids)". The extension is dropped because an extension-less board folder is /// equally legal (01-storage-format.md § Document packaging) and `basic` is the name the design /// uses. var slug: String { url.deletingPathExtension().lastPathComponent } /// The display name: the board's `title`, falling back to the folder name — 01-storage-format.md /// § Board naming's rule, unchanged, because a template is a board. var name: String { model.title.value ?? slug } /// The picker blurb — the template board's **body**, which is also what the instantiated board /// carries as its description (09: the body is "shown in the picker *and* becoming the new /// board's description"). Whitespace-trimmed for display only; the bytes on disk are copied /// verbatim by instantiation and never touched here. var blurb: String { model.document.body.trimmingCharacters(in: .whitespacesAndNewlines) } /// The board icon shown in the picker and inherited by the new board — the lenient `icon` rule /// (`ItemSymbol`), so a template naming a symbol this OS cannot draw shows the board default /// rather than an empty box. var icon: String { ItemSymbol.name(model.icon, fallback: ItemSymbol.board) } /// The icon's palette tint, or `nil` for the chrome default — read raw, since `Palette` is the /// one place a colour name is resolved. var iconColor: String? { model.iconColor.value } /// The chooser position — `template.order`, 09's **one** picker key and its only subkey. /// /// Read out of the model's opaque `YAMLValue` rather than through a typed accessor, deliberately: /// 09 keeps the key's shape open ("future subkeys possible"), and `BoardModel.template` is /// carried raw for exactly that reason. `nil` covers every shape that is not a number under /// `order` — a missing key, a hand-dropped board that never had one, a malformed value — which /// is one case to the chooser: 09's keyless tier, sorted by display name. var order: Double? { guard case let .mapping(pairs) = model.template else { return nil } guard let value = pairs.last(where: { $0.key == .string("order") })?.value else { return nil } switch value { case let .int(number): return Double(number) case let .double(number): return number default: return nil } } /// The template's lanes in display order, for the chooser's mini per-lane preview — a real /// `BoardModel`'s lanes, not a list of strings the app maintains by hand. var lanes: [Lane] { model.lanes } }