import SwiftUI /// The root of the boards navigation stack: board list → lanes → cards → card detail /// (`BoardRoute`'s destinations). /// /// Renders all three of `BoardIndexStore.Phase` — the property the placeholder this replaces /// existed to prove, and the split this screen keeps. struct BoardsTabView: View { @Environment(BoardIndexStore.self) private var index @State private var isPresentingNewBoard = false var body: some View { NavigationStack { content .navigationTitle("Boards") .toolbar { if case .ready = index.phase { Button("New Board", systemImage: "plus") { isPresentingNewBoard = true } } } .navigationDestination(for: BoardRoute.self) { route in switch route { case let .lanes(boardRoot): BoardScreen(boardRoot: boardRoot) case let .cards(boardRoot, laneID): LaneScreen(boardRoot: boardRoot, laneID: laneID) case let .card(boardRoot, laneID, cardID): CardDetailScreen(boardRoot: boardRoot, laneID: laneID, cardID: cardID) } } } .task { index.start() } .titlePromptAlert("New Board", isPresented: $isPresentingNewBoard, placeholder: "Board Title") { title in Task { await index.createBoard(titled: title) } } } @ViewBuilder private var content: some View { switch index.phase { case .idle, .loading: ProgressView("Looking for boards") case let .unavailable(reason): // The hard-iCloud-requirement wall. `reason` distinguishes "sign in" from "the container // did not resolve", which are different asks of the user. ContentUnavailableView { Label("iCloud Required", systemImage: "icloud.slash") } description: { Text(reason == .noAccount ? "Sign into iCloud in Settings to use Lanework." : "Lanework can't reach its iCloud Drive folder right now.") } actions: { Button("Try Again") { index.start() } } case .ready where index.boards.isEmpty: ContentUnavailableView( "No Boards Yet", systemImage: "rectangle.stack", description: Text("Boards in your iCloud Drive will appear here.") ) case .ready: List(index.boards) { board in NavigationLink(value: BoardRoute.lanes(boardRoot: board.rootURL)) { BoardSummaryRow(board: board) } } .refreshable { index.refresh() } } } } /// One board row: title, lane/card counts, modified date, and — while the package is not fully /// current — a download-state subtitle in place of the counts a shallow scan cannot yet answer. private struct BoardSummaryRow: View { let board: BoardSummary var body: some View { VStack(alignment: .leading, spacing: 2) { Text(board.title) Text(subtitle) .font(.caption) .foregroundStyle(.secondary) } } private var subtitle: String { switch board.download { case .notDownloaded: "Waiting to download" case let .downloading(fraction): fraction.map { "Downloading \(Int($0 * 100))%" } ?? "Downloading" case .current, .unknown: countsAndModified } } private var countsAndModified: String { let counts: String if let lanes = board.laneCount, let cards = board.cardCount { counts = "\(lanes) lane\(lanes == 1 ? "" : "s") · \(cards) card\(cards == 1 ? "" : "s")" } else { counts = "—" } guard let modified = board.modified else { return counts } return "\(counts) · \(modified.formatted(.relative(presentation: .named)))" } }