import SwiftUI /// 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` 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 /// that "ride along uninterpreted via `document.unknownFields`", alongside `assignees`, `due`, /// `remote`. Rendering or editing it here would mean this screen parsing and rewriting a key the /// model layer deliberately treats as opaque — exactly the unknown-field promise `updateIndex`'s /// 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 { @Binding var icon: String? @Binding var iconColor: String? @Binding var background: String? @State private var isPresentingIconPicker = false var body: some View { Section("Attributes") { Button { isPresentingIconPicker = true } label: { LabeledContent("Icon") { if let icon, !icon.isEmpty { Image(systemName: icon) } else { Text("None").foregroundStyle(.secondary) } } } .tint(.primary) swatchRow(title: "Icon Color", swatches: CardPalette.foregrounds, current: iconColor) { name in iconColor = name } swatchRow(title: "Background", swatches: CardPalette.backgrounds, current: background) { name in background = name } } .sheet(isPresented: $isPresentingIconPicker) { IconPickerSheet(current: icon) { name in icon = name } } } @ViewBuilder private func swatchRow( title: String, swatches: [CardPalette.Swatch], current: String?, onSelect: @escaping (String?) -> Void ) -> some View { VStack(alignment: .leading, spacing: 6) { Text(title) .font(.subheadline) .foregroundStyle(.secondary) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 10) { SwatchButton(isSelected: current == nil, color: nil) { onSelect(nil) } ForEach(swatches) { swatch in SwatchButton( isSelected: current == swatch.name, color: CardPalette.color(named: swatch.name, in: swatches) ) { onSelect(swatch.name) } } } } } .padding(.vertical, 4) } } /// One colour well: a filled circle, a slashed placeholder for "None", and a selection ring. private struct SwatchButton: View { let isSelected: Bool let color: Color? let action: () -> Void var body: some View { Button(action: action) { Circle() .fill(color ?? Color(.systemGray5)) .frame(width: 28, height: 28) .overlay { if color == nil { Image(systemName: "slash.circle") .font(.caption) .foregroundStyle(.secondary) } } .overlay { Circle() .strokeBorder(isSelected ? Color.accentColor : .clear, lineWidth: 2) .padding(-3) } } .buttonStyle(.plain) } } /// The icon picker's grid — `CardPalette.icons` plus a "None" well that removes the key. private struct IconPickerSheet: View { let current: String? let onSelect: (String?) -> Void @Environment(\.dismiss) private var dismiss private let columns = Array(repeating: GridItem(.flexible()), count: 6) var body: some View { NavigationStack { ScrollView { LazyVGrid(columns: columns, spacing: 20) { Button { onSelect(nil) dismiss() } label: { Image(systemName: "slash.circle") .font(.title2) .foregroundStyle(current == nil ? Color.accentColor : .secondary) } ForEach(CardPalette.icons, id: \.self) { name in Button { onSelect(name) dismiss() } label: { Image(systemName: name) .font(.title2) .foregroundStyle(current == name ? Color.accentColor : .primary) } } } .padding() } .navigationTitle("Icon") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } } } }