The phone joins the format — KanbanMobile MVP: shared storage verbatim over an iCloud container
A second product, not a second edition: dev.rzen.indie.KanbanMobile (iOS 26,
iPhone-only) compiles Kanban/Storage as source files, so a format change that
breaks the phone breaks this build the day it's made. Boards live in
iCloud.dev.rzen.indie.Kanban — named after the Mac bundle id so the Mac app can
adopt the container later without a migration; until then the folder is
"Lanework" in iCloud Drive and the Mac opens boards there through the open
panel.
EchoLedger grows #if os(macOS) gates around its three consumer surfaces
(verdicts/BoardDiff, harvest/HarvestedReceipt, comment retirement/CommentPath)
— the recording side BoardWriter stamps compiles on every platform, and the
gates are the seam a future phone verdict surface lands behind. AgentGuide
stays Mac-only.
The phone's watcher is NSMetadataQuery: BoardIndexStore (one query, package
UTI export makes a .kanban directory one item, equality-gated rescans,
download kicks per pass), BoardSession (materialization sweep before every
fail-fast walk, NSFileCoordinator brackets, perform{} = coordinated write then
awaited reload, ParseMemo threaded), CloudHome (off-main container resolution,
LANEWORK_LOCAL_ROOT DEBUG override for simulator work without an account).
Screens: Boards -> lanes -> cards -> card editor, value-routed by ItemID with
every screen re-reading the live snapshot; leading swipe moves a card via
confirmationDialog, trailing swipe sends it to .trash/; the editor commits
title through the Mac's canonical rename path and body through writeBody, with
drafts that survive reloads; attributes are the three typed style fields
(icon, iconColor, background) — labels is a reserved unknown-field key and
deliberately has no editor. Settings carries IndieBackup (backup root = the
container's Documents, restore rebuild = an index rescan, controller
constructed only once the home resolves).
Arbiter: KanbanMobile green for iOS Simulator, Kanban green for macOS, 3006
unit tests / 517 suites passed (PointerLatencyTests excluded — mid-rework
uncommitted in a parallel session).
Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The colour and icon vocabulary `icon`, `iconColor`, and `background` are written in.
|
||||
///
|
||||
/// **Mirrors `Kanban/UI/Palette.swift` and `SymbolPickerCatalog`, not shared with them.**
|
||||
/// `project.yml` excludes `Kanban/UI` from the phone target outright — it is AppKit-only — so this
|
||||
/// is an independent copy of the same name→hex tables and the same curated symbol grid, kept as
|
||||
/// pure data (no `NSColor`, no `AppKit`). The two pickers are free to diverge; nothing here reads
|
||||
/// the Mac's file or vice versa.
|
||||
enum CardPalette {
|
||||
|
||||
struct Swatch: Identifiable, Sendable {
|
||||
/// Kebab-case, exactly as written to frontmatter (`iconColor`/`background`'s `color`
|
||||
/// subkey read this way — see `FrontmatterFields.swift`).
|
||||
let name: String
|
||||
let hex: String
|
||||
var id: String { name }
|
||||
}
|
||||
|
||||
/// `iconColor`'s twelve wells.
|
||||
static let foregrounds: [Swatch] = [
|
||||
Swatch(name: "obsidian", hex: "#000000"),
|
||||
Swatch(name: "aluminum", hex: "#9B9B9B"),
|
||||
Swatch(name: "soapstone", hex: "#D5D5D5"),
|
||||
Swatch(name: "chalk", hex: "#FFFFFF"),
|
||||
Swatch(name: "carnation", hex: "#FF576C"),
|
||||
Swatch(name: "rich-grapefruit", hex: "#FF864C"),
|
||||
Swatch(name: "smokey-tangerine", hex: "#E5A334"),
|
||||
Swatch(name: "fern", hex: "#50B23D"),
|
||||
Swatch(name: "light-teal", hex: "#00B7B7"),
|
||||
Swatch(name: "deep-sky-blue", hex: "#0084E5"),
|
||||
Swatch(name: "pale-violet", hex: "#8C59C5"),
|
||||
Swatch(name: "deep-cool-granite", hex: "#597199"),
|
||||
]
|
||||
|
||||
/// `background`'s twelve wells — written into the mapping's `color` subkey
|
||||
/// (`FrontmatterDocument.setStyleValue`).
|
||||
static let backgrounds: [Swatch] = [
|
||||
Swatch(name: "obsidian", hex: "#000000"),
|
||||
Swatch(name: "shale", hex: "#5B5B5B"),
|
||||
Swatch(name: "aluminum", hex: "#9B9B9B"),
|
||||
Swatch(name: "chalk", hex: "#FFFFFF"),
|
||||
Swatch(name: "light-cayenne", hex: "#B6071E"),
|
||||
Swatch(name: "light-mocha", hex: "#B73C14"),
|
||||
Swatch(name: "smokey-mocha", hex: "#674611"),
|
||||
Swatch(name: "smokey-fern", hex: "#145312"),
|
||||
Swatch(name: "dark-teal", hex: "#005152"),
|
||||
Swatch(name: "smokey-ocean", hex: "#003168"),
|
||||
Swatch(name: "smokey-rich-eggplant", hex: "#290659"),
|
||||
Swatch(name: "intense-cool-shale", hex: "#1F2E45"),
|
||||
]
|
||||
|
||||
/// A general "boards and projects" grid — `SymbolPickerCatalog.defaultSet`, mirrored for the
|
||||
/// same reason as the colour tables above.
|
||||
static let icons: [String] = [
|
||||
"star", "flag", "heart", "bolt", "flame", "leaf", "drop", "sun.max", "moon", "sparkles",
|
||||
"tag", "bookmark", "pin", "bell", "paperplane", "tray", "folder", "archivebox", "doc.text",
|
||||
"list.bullet", "checklist", "calendar", "clock", "hammer", "wrench.and.screwdriver",
|
||||
"paintbrush", "lightbulb", "brain", "book", "graduationcap", "briefcase", "cart", "house",
|
||||
"airplane", "gamecontroller", "globe",
|
||||
]
|
||||
|
||||
/// Resolves a stored palette name to a colour, for rendering a well or a row accent.
|
||||
///
|
||||
/// **Lenient, like the field itself** (`Kanban/UI/Palette.swift`'s own framing): an
|
||||
/// unrecognized name — a custom hex the field allows but this picker does not offer, a typo, a
|
||||
/// name from some future palette — answers `nil`, and every caller here falls back to its own
|
||||
/// default rather than guessing at a colour. Nothing on disk is affected either way.
|
||||
static func color(named name: String, in swatches: [Swatch]) -> Color? {
|
||||
guard let swatch = swatches.first(where: { $0.name == name }) else { return nil }
|
||||
return Color(paletteHex: swatch.hex)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Color {
|
||||
/// `#RRGGBB` or `#RRGGBBAA` — the two forms `CardPalette.Swatch.hex` uses. Malformed input
|
||||
/// answers `nil` rather than a guessed colour, matching `CardPalette.color(named:in:)`'s own
|
||||
/// contract.
|
||||
init?(paletteHex hex: String) {
|
||||
var value = hex
|
||||
if value.hasPrefix("#") { value.removeFirst() }
|
||||
guard value.count == 6 || value.count == 8, let intValue = UInt64(value, radix: 16) else { return nil }
|
||||
|
||||
let r, g, b, a: Double
|
||||
if value.count == 8 {
|
||||
r = Double((intValue >> 24) & 0xFF) / 255
|
||||
g = Double((intValue >> 16) & 0xFF) / 255
|
||||
b = Double((intValue >> 8) & 0xFF) / 255
|
||||
a = Double(intValue & 0xFF) / 255
|
||||
} else {
|
||||
r = Double((intValue >> 16) & 0xFF) / 255
|
||||
g = Double((intValue >> 8) & 0xFF) / 255
|
||||
b = Double(intValue & 0xFF) / 255
|
||||
a = 1
|
||||
}
|
||||
self.init(red: r, green: g, blue: b, opacity: a)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user