import AppKit import SwiftUI /// The colour vocabulary the `background` and `iconColor` fields are written in /// (03-board-ui.md § Styling ▸ Capabilities): **a kebab-case palette name, or a `#RRGGBB[AA]` /// hex**. This file is the single source for the name→hex mapping — the pathfinder's twelve icon /// tints and twelve backgrounds, carried over verbatim as the starting point ("The pathfinder's /// palettes (12 icon tints, 12 backgrounds) carry over"). /// /// ### Lenient, never an error /// /// A stored value is *cosmetic*, so an unrecognized one is not a load failure, not a warning, and /// not a placeholder colour: resolution simply yields `nil` and the call site falls back to its /// own default — no stripe for a card `background`, the standard secondary tint for an /// `iconColor` (03 § Card face). The bytes on disk are left exactly as written until the author /// changes them, which is what makes "custom hex is not pickable in-app but stays fully honored /// from disk" (03 § Controls) true in both directions: curated in-app, unlimited on disk. /// /// Names are matched **exactly** — kebab-case as the tables below spell them. `Background` is not /// `background`, and a near-miss degrades like any other unknown value rather than guessing at /// what the author meant. struct PaletteColor: Identifiable, Sendable { /// Kebab-case, exactly as written to frontmatter. let name: String let hex: String var id: String { name } } enum Palette { /// Icon-tint palette (`iconColor`). static let foregrounds: [PaletteColor] = [ PaletteColor(name: "obsidian", hex: "#000000"), PaletteColor(name: "aluminum", hex: "#9B9B9B"), PaletteColor(name: "soapstone", hex: "#D5D5D5"), PaletteColor(name: "chalk", hex: "#FFFFFF"), PaletteColor(name: "carnation", hex: "#FF576C"), PaletteColor(name: "rich-grapefruit", hex: "#FF864C"), PaletteColor(name: "smokey-tangerine", hex: "#E5A334"), PaletteColor(name: "fern", hex: "#50B23D"), PaletteColor(name: "light-teal", hex: "#00B7B7"), PaletteColor(name: "deep-sky-blue", hex: "#0084E5"), PaletteColor(name: "pale-violet", hex: "#8C59C5"), PaletteColor(name: "deep-cool-granite", hex: "#597199"), ] /// Background palette (`background`) — the twelve wells the style editor will offer, "every /// pair AA-verified at design time" (03-board-ui.md § Styling ▸ Controls). static let backgrounds: [PaletteColor] = [ PaletteColor(name: "obsidian", hex: "#000000"), PaletteColor(name: "shale", hex: "#5B5B5B"), PaletteColor(name: "aluminum", hex: "#9B9B9B"), PaletteColor(name: "chalk", hex: "#FFFFFF"), PaletteColor(name: "light-cayenne", hex: "#B6071E"), PaletteColor(name: "light-mocha", hex: "#B73C14"), PaletteColor(name: "smokey-mocha", hex: "#674611"), PaletteColor(name: "smokey-fern", hex: "#145312"), PaletteColor(name: "dark-teal", hex: "#005152"), PaletteColor(name: "smokey-ocean", hex: "#003168"), PaletteColor(name: "smokey-rich-eggplant", hex: "#290659"), PaletteColor(name: "intense-cool-shale", hex: "#1F2E45"), ] } // The pathfinder's panel round-trip helpers (`NSColor.paletteHexString`, `Palette.name(forHex:)`) // and its swatch drawing are deliberately not ported yet: nothing writes a colour until the style // editor lands, and an unused writer is a claim about a surface that doesn't exist. The styling // card brings them back when the editor needs them. extension Palette { /// Resolves a stored value — a palette name, or a hand-written `#RRGGBB`/`#RRGGBBAA` hex — /// searching the icon tints first, then the backgrounds. **Both tables answer either field**: /// the split is what each *picker* offers, not a namespace, so a hand-written /// `background: carnation` resolves rather than reading as garbage. /// /// `nil` means unrecognized, which is a rendering instruction ("use your default"), never an /// error — see this file's leading comment. static func nsColor(for value: String) -> NSColor? { if value.hasPrefix("#") { return NSColor(paletteHex: value) } guard let hex = (foregrounds + backgrounds).first(where: { $0.name == value })?.hex else { return nil } return NSColor(paletteHex: hex) } /// SwiftUI variant of `nsColor(for:)` — what the views actually call. static func color(named name: String) -> Color? { nsColor(for: name).map { Color(nsColor: $0) } } /// The lenient read of a whole frontmatter field: a missing or malformed `background` / /// `iconColor` resolves exactly like an unrecognized one — there is no colour, so use the /// default. /// /// Folding all three `FieldValue` shapes into one `nil` mirrors `ItemSymbol.name(_:fallback:)` /// and keeps every call site free of the distinction, which no renderer has a use for. static func color(for field: FieldValue) -> Color? { guard let value = field.value else { return nil } return color(named: value) } } // MARK: - Hex → colour extension NSColor { /// `#RRGGBB` or `#RRGGBBAA` → `NSColor` in **sRGB** — the colour space the hex digits name, /// so a value hand-written from a screenshot or a design tool renders as the same colour the /// author sampled. Returns `nil` for anything else: a missing `#`, a short or long digit run, /// or a non-hex character. convenience init?(paletteHex hex: String) { var string = hex if string.hasPrefix("#") { string.removeFirst() } var alpha: CGFloat = 1 if string.count == 8 { guard let alphaByte = UInt32(string.suffix(2), radix: 16) else { return nil } alpha = CGFloat(alphaByte) / 255 string.removeLast(2) } guard string.count == 6, let value = UInt32(string, radix: 16) else { return nil } self.init( srgbRed: CGFloat((value >> 16) & 0xFF) / 255, green: CGFloat((value >> 8) & 0xFF) / 255, blue: CGFloat(value & 0xFF) / 255, alpha: alpha ) } }