Cards open to be read — the mobile detail screen turns read-only, editing moves behind a transactional Save
CardDetailScreen renders title, inline-Markdown body, and a quiet dates footer; all writing moves to the new CardEditScreen, a full-screen cover with segmented Details/Body panes. Drafts commit in one perform bracket on Save only — Cancel guards dirty drafts with a discard confirmation, and a failed write keeps the sheet and its drafts and raises an alert instead of dismissing. CardAttributesSection becomes a pure binding-driven editor with no write path of its own. The UI test walk crosses the new split, with body-pane and discard coverage, and a deterministic replaceAllText helper retires the flaky ⌘A select-all. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The card detail screen's Attributes section: icon, icon colour, and background colour — the
|
||||
/// three *typed* style fields a card carries (`FrontmatterFields.icon`/`.iconColor`/`.background`).
|
||||
/// Every pick writes immediately through `BoardWriter.updateIndex` — these are one-tap choices
|
||||
/// from a fixed set, not free text, so there is no draft to debounce the way title/body have.
|
||||
/// The card editor's Attributes section: icon, icon colour, and background colour — the three
|
||||
/// *typed* style fields a card carries (`FrontmatterFields.icon`/`.iconColor`/`.background`).
|
||||
///
|
||||
/// **Drafted, not written.** Every pick here mutates a `Binding<String?>` the caller owns —
|
||||
/// `CardEditScreen`'s own `@State` drafts — rather than reaching for `BoardWriter` itself: the
|
||||
/// transactional edit model commits title, body, and these three keys together in one `Save`, so
|
||||
/// this section has no session, no card, and no write code of its own. It is a pure "edit these
|
||||
/// three bindings" view; the icon picker sheet is unchanged mechanically, it just feeds a binding
|
||||
/// instead of firing a write.
|
||||
///
|
||||
/// **There is no labels row.** `labels` is not a field either `BoardModel` or `FrontmatterDocument`
|
||||
/// exposes as typed: `BoardModel.document`'s own doc comment names it as one of the reserved keys
|
||||
@@ -13,10 +18,9 @@ import SwiftUI
|
||||
/// surgical edits exist to keep (agent-written or hand-written overlays round-trip untouched). If
|
||||
/// a typed `labels` field is ever added to the storage schema, its editor belongs in this section.
|
||||
struct CardAttributesSection: View {
|
||||
let card: Card
|
||||
let laneID: ItemID
|
||||
let cardID: ItemID
|
||||
let session: BoardSession
|
||||
@Binding var icon: String?
|
||||
@Binding var iconColor: String?
|
||||
@Binding var background: String?
|
||||
|
||||
@State private var isPresentingIconPicker = false
|
||||
|
||||
@@ -26,7 +30,7 @@ struct CardAttributesSection: View {
|
||||
isPresentingIconPicker = true
|
||||
} label: {
|
||||
LabeledContent("Icon") {
|
||||
if let icon = card.icon.value, !icon.isEmpty {
|
||||
if let icon, !icon.isEmpty {
|
||||
Image(systemName: icon)
|
||||
} else {
|
||||
Text("None").foregroundStyle(.secondary)
|
||||
@@ -35,17 +39,17 @@ struct CardAttributesSection: View {
|
||||
}
|
||||
.tint(.primary)
|
||||
|
||||
swatchRow(title: "Icon Color", swatches: CardPalette.foregrounds, current: card.iconColor.value) { name in
|
||||
setStyle(FrontmatterKeys.iconColor, to: name)
|
||||
swatchRow(title: "Icon Color", swatches: CardPalette.foregrounds, current: iconColor) { name in
|
||||
iconColor = name
|
||||
}
|
||||
|
||||
swatchRow(title: "Background", swatches: CardPalette.backgrounds, current: card.background.value) { name in
|
||||
setStyle(FrontmatterKeys.background, to: name)
|
||||
swatchRow(title: "Background", swatches: CardPalette.backgrounds, current: background) { name in
|
||||
background = name
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isPresentingIconPicker) {
|
||||
IconPickerSheet(current: card.icon.value) { name in
|
||||
setStyle(FrontmatterKeys.icon, to: name)
|
||||
IconPickerSheet(current: icon) { name in
|
||||
icon = name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,28 +81,6 @@ struct CardAttributesSection: View {
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
/// Writes one style key immediately. `operation: .style` is the vocabulary's own case for
|
||||
/// "`updateIndex` on behalf of styling flows" (`WriteOperation.style`); `card.title.value` is
|
||||
/// read before the closure runs so a failure banner can still name the card by the title on
|
||||
/// screen.
|
||||
private func setStyle(_ key: String, to value: String?) {
|
||||
let laneID = self.laneID
|
||||
let cardID = self.cardID
|
||||
let cardTitle = card.title.value
|
||||
Task {
|
||||
// The closure's throws type must be spelled out — a trailing closure literal does not
|
||||
// pick up `perform`'s `throws(BoardWriteError)` from context alone.
|
||||
await session.perform { (root: URL) throws(BoardWriteError) -> Void in
|
||||
let folder = root
|
||||
.appendingPathComponent(laneID.rawValue, isDirectory: true)
|
||||
.appendingPathComponent(cardID.rawValue, isDirectory: true)
|
||||
try BoardWriter.updateIndex(inItemFolder: folder, operation: .style(title: cardTitle)) { document in
|
||||
document.setStyleValue(value, for: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One colour well: a filled circle, a slashed placeholder for "None", and a selection ring.
|
||||
|
||||
Reference in New Issue
Block a user