feat(ipad): W1-W3 — adaptive split-view layout + finding fixes
T-iPad-2: AdaptiveRootView/LayoutPolicy (sole size-class decision), SplitRootView (NavigationSplitView sidebar+detail), StackRootView (iPhone path verbatim, zero regression); privacy shade hoisted to shared ZStack top for both branches T-iPad-3: KeyBarVisibility predicate (hide when hardware keyboard present), pointer context menu (copy/new-in-cwd/kill, all via existing channels) T-iPad-4: Projects multi-column grid on iPad (idiom-gated), adaptive sheet detents T-iPad-5 findings (4/4 fixed): kill onKillSession thread-through + AppCoordinator.killCurrentSession; split route-gated to .sessions (iPad first-run pairing); continue-last banner in split sidebar; iPad XCUITest deferred (covered by SidebarSelectionTests) Verified: iPhone 16 277 + iPad Pro 11 278 tests green; packages 261 + integration 10; zero changes under ios/Packages, src/, public/
This commit is contained in:
@@ -1,62 +1,39 @@
|
||||
import HostRegistry
|
||||
import SwiftUI
|
||||
|
||||
/// T-iOS-15 · Root of the app: route switch (Pairing / SessionList), terminal
|
||||
/// push, add-host sheet, scenePhase forwarding and the privacy shade.
|
||||
/// T-iOS-15 / T-iPad-2 · App 根入口。历史上这里是单一 `NavigationStack` +
|
||||
/// 隐私遮罩 + sheets/scenePhase/deepLink 的全部布线;iPad 自适应(T-iPad-2)
|
||||
/// 把它拆成两层:
|
||||
/// - `RootView`(本 struct)保持 `@main` 的唯一实例化点不变(WebTermApp 仍
|
||||
/// `RootView(coordinator:)`),内部只委托给 `AdaptiveRootView`。
|
||||
/// - `AdaptiveRootView` 按 `horizontalSizeClass` 选 `StackRootView`(compact,
|
||||
/// 现有路径,字节级不变)或 `SplitRootView`(regular,iPad 分栏),并把
|
||||
/// 隐私遮罩 / scenePhase / deepLink / sheets 这些**设备无关的横切布线**
|
||||
/// 上提到唯一一处,两分支共享(遮罩因此仍是两分支共同的 ZStack 顶层)。
|
||||
///
|
||||
/// The shade is the TOPMOST layer of this ZStack and appears whenever
|
||||
/// `scenePhase != .active` (exact rule — PrivacyShadePolicy + tests); it
|
||||
/// covers the whole navigation tree, terminal included. Sheets live above any
|
||||
/// overlay, but no sheet renders terminal bytes (pairing / plan gate only).
|
||||
/// `StackRootView` 是原 `RootView` 的 body **原样搬迁** —— compact 分支复用它
|
||||
/// 不改一行,故 iPhone 逐屏渲染与适配前一致(零回归硬性要求)。
|
||||
struct RootView: View {
|
||||
@Bindable var coordinator: AppCoordinator
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
NavigationStack {
|
||||
rootContent
|
||||
.navigationDestination(isPresented: terminalBinding) {
|
||||
terminalDestination
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
AdaptiveRootView(coordinator: coordinator)
|
||||
}
|
||||
}
|
||||
|
||||
/// Deep-link hint alert (unknown host / store failure, T-iOS-22).
|
||||
private var deepLinkHintBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { coordinator.deepLink.hintMessage != nil },
|
||||
set: { presented in
|
||||
guard !presented else { return }
|
||||
coordinator.deepLink.clearHint()
|
||||
}
|
||||
)
|
||||
/// T-iPad-2 · compact 宽度(iPhone / iPad Slide Over)的根视图 —— 原
|
||||
/// `RootView` 的 `NavigationStack` 主体原样搬迁,行为字节级不变。横切布线
|
||||
/// (遮罩/scenePhase/deepLink/sheets)不在这里,已上提到 `AdaptiveRootView`。
|
||||
struct StackRootView: View {
|
||||
@Bindable var coordinator: AppCoordinator
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
rootContent
|
||||
.navigationDestination(isPresented: terminalBinding) {
|
||||
terminalDestination
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Route switch
|
||||
@@ -140,7 +117,8 @@ struct RootView: View {
|
||||
if let controller = coordinator.terminalController {
|
||||
TerminalContainerView(
|
||||
controller: controller,
|
||||
onNewSessionInCwd: { coordinator.openNewSessionInCurrentCwd() }
|
||||
onNewSessionInCwd: { coordinator.openNewSessionInCurrentCwd() },
|
||||
onKillSession: { coordinator.killCurrentSession() } // T-iPad-3
|
||||
)
|
||||
// T-iOS-29 · identity PER CONTROLLER: an in-place session switch
|
||||
// (new-in-cwd, deep link) swaps the controller while the
|
||||
@@ -151,40 +129,15 @@ struct RootView: View {
|
||||
.id(controller.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum RootMetrics {
|
||||
enum RootMetrics {
|
||||
static let bannerHorizontalPadding: CGFloat = 16
|
||||
static let bannerVerticalPadding: CGFloat = 8
|
||||
}
|
||||
|
||||
private enum RootCopy {
|
||||
/// 根层用户可见文案(internal —— `SplitRootView` 复用同一「项目」标签,DRY)。
|
||||
enum RootCopy {
|
||||
static let continueLast = "继续上次会话"
|
||||
static let projects = "项目"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user