import ByreAPI import SwiftUI typealias LeadNote = Components.Schemas.LeadNote @MainActor private var notesProject = "" struct NotesView: View { var body: some View { ScrollView { VStack(alignment: .leading, spacing: 0) { Text("Your lead notes your priorities and preferences as it works with you, and starts every session with them.") .foregroundStyle(Palette.muted) .fixedSize(horizontal: false, vertical: true) .padding(.top, 16) LeadNotes() } .padding(.horizontal, 16) .padding(.top, 8) .padding(.bottom, 16) .frame(maxWidth: .infinity, alignment: .leading) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Palette.bg) } } private struct LeadNotes: View { @Environment(Store.self) private var store @Environment(Overlays.self) private var overlays @ViewState private var list: [LeadNote]? @ViewState private var failure: String? @ViewState private var loads = 0 @ViewState private var project = notesProject @ViewState private var off: (() -> Void)? var body: some View { VStack(alignment: .leading, spacing: 0) { if let list { if list.isEmpty { hint("Your lead hasn't noted anything about how you work yet.") } else { let groups = [("", "Every project")] + store.projects.map { ($0.id, $0.name) } let current = groups.contains { $0.0 == project } ? project : "" let shown = list.filter { ($0.projectId ?? "") == current } let label = { (g: (String, String)) in "\(g.1) (\(list.filter { ($0.projectId ?? "") == g.0 }.count))" } Menu { ForEach(groups, id: \.0) { g in Button(label(g)) { project = g.0 notesProject = g.0 } } } label: { HStack(spacing: 8) { Text(label(groups.first { $0.0 == current } ?? groups[0])) Image(systemName: "chevron.up.chevron.down") .font(.system(size: 9, weight: .semibold)) } .foregroundStyle(Palette.text) .modifier(FieldStyle()) .contentShape(Rectangle()) } .menuStyle(.button) .buttonStyle(.plain) .menuIndicator(.hidden) .fixedSize() .help("Which notes to show") .padding(.bottom, 8) if shown.isEmpty { hint("No notes here yet.") } else { ForEach(shown, id: \.id) { note in NoteRow(note: note) } } } } else if let failure { hint(failure) } } .padding(.top, 8) .onAppear { off = store.events.add { ev in if ev.kind == "lead.note_saved" || ev.kind == "lead.note_deleted" { load() } } load() } .onDisappear { off?() off = nil loads += 1 } } private func hint(_ text: String) -> some View { Text(text).foregroundStyle(Palette.muted) } private func load() { loads += 1 let n = loads Task { do { let loaded = try await store.api.client.listLeadNotes().ok.body.json if n == loads { list = loaded } } catch { failure = errorText(error) list = nil } } } } private struct NoteRow: View { let note: LeadNote @Environment(Store.self) private var store @Environment(Overlays.self) private var overlays @ViewState private var frame: CGRect = .zero var body: some View { HStack(alignment: .firstTextBaseline, spacing: 12) { Text(note.text) .textSelection(.enabled) .frame(maxWidth: .infinity, alignment: .leading) .fixedSize(horizontal: false, vertical: true) Button("delete") { Task { if !(await overlays.showConfirm(["Delete this note? Your lead stops applying it.", note.text], action: "Delete", danger: true, anchor: frame)) { return } do { _ = try await store.api.client.deleteLeadNote(path: .init(noteId: note.id)).noContent } catch { overlays.showAlert(errorText(error)) } } } .buttonStyle(SmallLinkButtonStyle()) .onGeometryChange(for: CGRect.self) { g in g.frame(in: .named("window")) } action: { f in frame = f } } .padding(.vertical, 6) .overlay(alignment: .top) { Rectangle().fill(Palette.border).frame(height: 1) } } }