import SwiftUI /// T-iPad-2 · size-class 驱动的单一根视图。按 `LayoutPolicy.mode` 在 compact /// 宽度(iPhone / iPad Slide Over)选 `StackRootView`(现有路径,字节级不变)、 /// 在 regular 宽度(iPad 全屏 / 大分屏 / Stage Manager 大窗)选 `SplitRootView` /// (分栏)。 /// /// **横切布线只声明一次、两分支共享**(PLAN_IOS_IPAD §5 T-iPad-2): /// - 隐私遮罩:ZStack 的**最顶层**,覆盖整棵导航树(含 split detail 的终端 /// 字节)——`scenePhase != .active` 即遮(安全不变式,见 `PrivacyShadePolicy`)。 /// 上提到这里,split detail 与 stack push 同样被遮,无遗漏。 /// - `.task` bootstrap / `.onChange(scenePhase)` / `.onOpenURL` / deep-link 提示 /// alert / add-host sheet / Projects sheet:全部设备无关,原样从旧 `RootView` /// 搬到这一层,故 compact 分支拿到与适配前完全一致的横切行为。 /// /// size class 是**运行时可变量**(iPad 拉出 Slide Over 立刻 regular→compact), /// 所以这不是两套 UI 分叉,而是同一次运行内的自适应切换:`terminalController` /// 归 `AppCoordinator` 所有,与布局分支正交,切换时不重建(T-iOS-29 `.id` /// 稳定性的前提)。 struct AdaptiveRootView: View { @Bindable var coordinator: AppCoordinator @Environment(\.scenePhase) private var scenePhase @Environment(\.horizontalSizeClass) private var horizontalSizeClass var body: some View { ZStack { layoutBranch if PrivacyShadePolicy.isShadeVisible(for: scenePhase) { PrivacyShadeView() } } .task { await coordinator.bootstrap() } .onChange(of: scenePhase) { _, phase in coordinator.handleScenePhase(phase) } .onOpenURL { coordinator.handleDeepLink(url: $0) } // T-iOS-22 .alert(DeepLinkCopy.hintTitle, isPresented: deepLinkHintBinding) { Button(DeepLinkCopy.hintConfirm) { coordinator.deepLink.clearHint() } } message: { Text(coordinator.deepLink.hintMessage ?? "") } .sheet( isPresented: $coordinator.isAddHostPresented, onDismiss: { coordinator.addHostDismissed() } ) { addHostSheet } .sheet( isPresented: $coordinator.isProjectsPresented, onDismiss: { coordinator.projectsDismissed() } ) { projectsSheet } .sheet(isPresented: $coordinator.isDeviceCertPresented) { deviceCertSheet } .sheet( isPresented: $coordinator.isEnrollmentPresented, onDismiss: { coordinator.enrollmentDismissed() } ) { enrollmentSheet } } // MARK: - Layout branch (the SOLE size-class consumer) @ViewBuilder private var layoutBranch: some View { // Split only makes sense once we're in the session list. `.loading` and // `.pairing` (genuine iPad first-run, no paired host) get the full-screen // stack flow regardless of size class — a split sidebar has nothing to // list yet and would strand the user on the not-paired empty state // (T-iPad-5 finding). Route-gate the split branch. switch LayoutPolicy.mode(horizontalSizeClass: horizontalSizeClass) { case .split where coordinator.route == .sessions: SplitRootView(coordinator: coordinator) default: StackRootView(coordinator: coordinator) } } // MARK: - Deep-link hint alert (T-iOS-22) /// unknown host / store failure 提示(同旧 `RootView`)。 private var deepLinkHintBinding: Binding { Binding( get: { coordinator.deepLink.hintMessage != nil }, set: { presented in guard !presented else { return } coordinator.deepLink.clearHint() } ) } // MARK: - Add-host sheet (multi-host entry, list header) @ViewBuilder private var addHostSheet: some View { if let viewModel = coordinator.addHostPairingViewModel { NavigationStack { PairingScreen(viewModel: viewModel) { host in coordinator.completeAddHost(host) } } } } // MARK: - Projects sheet (T-iOS-26) /// 自带 NavigationStack:列表 → 详情 → 差异都在 sheet 内 push; /// "在此仓库开新会话" 关掉 sheet 并在根导航开终端。 @ViewBuilder private var projectsSheet: some View { if let viewModel = coordinator.projectsViewModel { NavigationStack { ProjectsScreen(viewModel: viewModel) { request in coordinator.openProject(request) } } } } // MARK: - Device certificate sheet (C-iOS-3, HIGH reachability fix) /// 自带 NavigationStack(`ClientCertScreen` 用 `.navigationTitle`)+ 右上「完成」 /// 关闭按钮,保证导入 .p12 后能明确返回。`ClientCertScreen` 自持 /// `ClientCertViewModel`(keychain store),此处零依赖注入。 @ViewBuilder private var deviceCertSheet: some View { NavigationStack { ClientCertScreen() .toolbar { ToolbarItem(placement: .topBarTrailing) { Button(RootCopy.done) { coordinator.isDeviceCertPresented = false } } } } } // MARK: - Device enrollment sheet (B3, zero-.p12 auto-cert) /// 自带 NavigationStack + 右上「完成」。VM 由 coordinator 每次进入构建 /// (`presentEnrollment`);关闭后 `enrollmentDismissed` 丢弃 VM 并跑一次轮换。 @ViewBuilder private var enrollmentSheet: some View { if let viewModel = coordinator.enrollmentViewModel { NavigationStack { EnrollmentScreen(model: viewModel) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button(RootCopy.done) { coordinator.isEnrollmentPresented = false } } } } } } }