import SwiftUI import DSKit /// DEVONthink-style 3-column shell. RootView only ROUTES; each page owns its own interior treatment /// (no shell-level auto-inherit). macOS-only target. /// 인증 게이트: checking(부팅 시 refresh 쿠키 복귀 시도) → loggedOut(LoginView) → ready(3-pane 셸). public struct RootView: View { @Environment(AppModel.self) private var model @State private var columnVisibility: NavigationSplitViewVisibility = .all public init() {} public var body: some View { Group { switch model.authPhase { case .checking: ProgressView("서버 연결 확인 중") .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Sage.surface) case .loggedOut: LoginView() case .ready: shell } } .task { await model.bootstrap() } } private var shell: some View { NavigationSplitView(columnVisibility: $columnVisibility) { Sidebar() .navigationSplitViewColumnWidth(min: 220, ideal: 250) } content: { ContentColumn() .navigationSplitViewColumnWidth(min: 300, ideal: 380) } detail: { DetailColumn() } .navigationSplitViewStyle(.balanced) .tint(Sage.brand) .safeAreaInset(edge: .bottom) { // 라이브 데이터 호출 실패 가시화 (no-silent-fallback) — 닫기 전까지 유지. if let err = model.errorText { HStack(spacing: 10) { Text(err) .font(.callout) .foregroundStyle(.white) .lineLimit(2) Spacer() Button("닫기") { model.errorText = nil } .buttonStyle(.plain) .foregroundStyle(.white.opacity(0.85)) } .padding(.horizontal, 14) .padding(.vertical, 8) .background(Sage.danger) } } } } struct Sidebar: View { @Environment(AppModel.self) private var model var body: some View { let selection = Binding( get: { model.section }, set: { if let v = $0 { model.section = v } } ) List(selection: selection) { Section { ForEach(AppModel.Section.allCases) { s in Text(s.title).tag(s) } } if model.section == .documents, !model.tree.isEmpty { Section("도메인") { ForEach(model.tree) { node in DomainRow(node: node) } } } } .listStyle(.sidebar) .background(Sage.sidebar) } } struct DomainRow: View { @Environment(AppModel.self) private var model let node: DomainTreeNode var body: some View { HStack(spacing: 8) { Circle().fill(Sage.domainColor(node.name)).frame(width: 8, height: 8) Text(node.name).font(.callout).foregroundStyle(Sage.ink) Spacer() Text("\(node.count)").font(.caption).foregroundStyle(Sage.muted) } .contentShape(Rectangle()) .onTapGesture { model.section = .documents } } } struct ContentColumn: View { @Environment(AppModel.self) private var model var body: some View { Group { switch model.section { case .dashboard: DashboardView() case .documents: DocumentListView() case .search: SearchView() case .ask: AskView() case .memos: MemoListView() case .digest: DigestView() } } .navigationTitle(model.section.title) } } struct DetailColumn: View { @Environment(AppModel.self) private var model var body: some View { Group { switch model.section { case .documents: if let d = model.documentDetail { DocumentDetailView(detail: d) } else { EmptyState(text: "문서를 선택하세요") } case .memos: if let m = model.memoDetail { MemoDetailView(memo: m) } else { EmptyState(text: "메모를 선택하세요") } default: EmptyState(text: model.section.title) } } } } struct EmptyState: View { let text: String var body: some View { Text(text) .foregroundStyle(Sage.muted) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Sage.surface) } } #if DEBUG #Preview("DS App — full shell") { @Previewable @State var model = AppModel.preview RootView() .environment(model) .frame(minWidth: 1000, minHeight: 660) } #endif