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
50 lines
1.7 KiB
Swift
50 lines
1.7 KiB
Swift
import SwiftUI
|
|
|
|
/// A single-`TextField` alert — the "name a new thing" prompt shared by New Board, New Lane, and
|
|
/// New Card, so each toolbar button does not grow its own `@State` pair and `.alert` block.
|
|
private struct TitlePromptAlert: ViewModifier {
|
|
@Binding var isPresented: Bool
|
|
let titleText: String
|
|
let message: String?
|
|
let placeholder: String
|
|
let onSubmit: (String) -> Void
|
|
|
|
@State private var text = ""
|
|
|
|
func body(content: Content) -> some View {
|
|
content.alert(titleText, isPresented: $isPresented) {
|
|
TextField(placeholder, text: $text)
|
|
Button("Create") {
|
|
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
text = ""
|
|
onSubmit(trimmed)
|
|
}
|
|
Button("Cancel", role: .cancel) { text = "" }
|
|
} message: {
|
|
if let message { Text(message) }
|
|
}
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
/// Presents a title-only creation prompt. `onSubmit` receives the trimmed text, which may be
|
|
/// empty — every create call this feeds (`BoardIndexStore.createBoard`, `BoardWriter.createLane`,
|
|
/// `BoardWriter.createCard`) already treats an empty/whitespace title as "no title" and falls
|
|
/// back to the untitled placeholder, exactly as a hand-emptied field would.
|
|
func titlePromptAlert(
|
|
_ titleText: String,
|
|
isPresented: Binding<Bool>,
|
|
message: String? = nil,
|
|
placeholder: String = "Title",
|
|
onSubmit: @escaping (String) -> Void
|
|
) -> some View {
|
|
modifier(TitlePromptAlert(
|
|
isPresented: isPresented,
|
|
titleText: titleText,
|
|
message: message,
|
|
placeholder: placeholder,
|
|
onSubmit: onSubmit
|
|
))
|
|
}
|
|
}
|