Files
web-terminal/ios/App/WebTerm/Wiring/AdaptiveRootView.swift
Yaojia Wang 823432b1c8 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/
2026-07-05 19:58:30 +02:00

111 lines
4.5 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 regularcompact
/// 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
}
}
// 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<Bool> {
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)
}
}
}
}
}