import SwiftUI /// T-iPad-2 · regular 宽度(iPad 全屏 / 大分屏 / Stage Manager 大窗)的分栏根 /// 视图。左 sidebar = 会话列表(复用 `SessionListScreen` **不改一行** + 与 /// stack 同款的「项目」leading 工具栏入口);右 detail = `TerminalContainerView` /// (终端 + gate/digest 叠层,复用不动),无打开会话时给「选择或新建会话」占位。 /// /// 设计要点: /// - **detail 复用 `TerminalContainerView`**:终端组装与它挂 push destination 还是 /// split detail 无关,只换外层容器、内容零改(PLAN_IOS_IPAD §1)。 /// - **选中态经 `AppCoordinator` 单一真值**:sidebar 触发面全部走 /// `selectSidebarItem` / `open`(同 stack 的既有路由),不新开会话生命周期。 /// `coordinator.sidebarSelection` 提供二向绑定;行 highlight 需 sidebar List /// 采纳 `selection:`(`SessionListScreen` 的 List,属列表组 Owns),见文末注。 /// - **detail 终端仍带 `.id(controller.id)`**:换会话时新 controller → 新 /// SwiftTerm 视图(回放 ring buffer),identity 稳定复用 T-iOS-29 语义。 /// - **隐私遮罩不在这里**:它在 `AdaptiveRootView` 的 ZStack 顶层,两分支共享, /// 故 detail 终端字节同样在 `scenePhase != .active` 时被遮(安全不变式)。 /// - **视觉层**:横幅/占位/工具栏全部走冻结 `DS.*` token 与共享 Primitive, /// 与 stack 分支像素级一致(继续横幅、项目入口都是同一组件)。 struct SplitRootView: View { @Bindable var coordinator: AppCoordinator @Environment(\.accessibilityReduceMotion) private var reduceMotion var body: some View { NavigationSplitView { sidebar } detail: { detail } } // MARK: - Sidebar(会话列表 + 项目入口,复用 SessionListScreen) private var sidebar: some View { SessionListScreen( viewModel: coordinator.sessionList, onOpen: { request in // 分栏只是又一个触发面:映射到同一 selectSidebarItem 路由。 coordinator.selectSidebarItem(sidebarItem(for: request)) }, onAddHost: { coordinator.presentAddHost() }, onDeviceCert: { coordinator.presentDeviceCert() }, onEnroll: { coordinator.presentEnrollment() } ) .safeAreaInset(edge: .bottom) { continueLastBanner } .animation( DS.Motion.gated(DS.Motion.base, reduceMotion: reduceMotion), value: coordinator.continueLastSessionId ) .toolbar { ProjectsToolbarItem(coordinator: coordinator) } } /// 与 stack 底栏同款「继续上次会话」(冷启动第 5 步)—— 复用共享 /// `ContinueLastBanner`,iPad 用户不丢该入口(T-iPad-5 finding)。 @ViewBuilder private var continueLastBanner: some View { if coordinator.continueLastSessionId != nil { ContinueLastBanner { coordinator.openContinueLast() } .transition(.move(edge: .bottom).combined(with: .opacity)) } } /// 列表的 `OpenRequest` → sidebar 选中项(sessionId 有无 = 采纳既有 / 新建)。 private func sidebarItem(for request: SessionListViewModel.OpenRequest) -> SidebarItem { request.sessionId.map(SidebarItem.session) ?? .newSession } // MARK: - Detail(终端或占位) @ViewBuilder private var detail: some View { if let controller = coordinator.terminalController { NavigationStack { TerminalContainerView( controller: controller, onNewSessionInCwd: { coordinator.openNewSessionInCurrentCwd() }, onKillSession: { coordinator.killCurrentSession() } // T-iPad-3 ) .id(controller.id) // T-iOS-29 · identity per controller } } else { placeholder .transition(.opacity) } } /// 空 detail 占位 —— accent 图标 + 邀约式文案(DS 方向:ContentUnavailableView /// + 好符号 + 友好中文)。图标着 accent 靛紫,与全 App 强调色呼应。 private var placeholder: some View { ContentUnavailableView { Label { Text(SplitCopy.placeholderTitle) } icon: { Image(systemName: "terminal") .foregroundStyle(DS.Palette.accent) } } description: { Text(SplitCopy.placeholderHint) } } } /// split 专属用户可见文案(「项目」/「继续」标签复用 `RootCopy`,DRY)。 private enum SplitCopy { static let placeholderTitle = "选择或新建会话" static let placeholderHint = "从左侧选择一个运行中的会话,或新建一个开始工作。" }