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:
Yaojia Wang
2026-07-05 19:58:30 +02:00
parent 77502ec4fe
commit 823432b1c8
18 changed files with 1594 additions and 84 deletions

View File

@@ -0,0 +1,73 @@
import SwiftUI
import UIKit
/// T-iPad-4 · Projects **** / / sheet
/// detents 仿 `LayoutPolicy`
/// 100%
///
/// ** `UIUserInterfaceIdiom` `horizontalSizeClass`**
/// iPad Projects ** sheet**form sheet
/// `horizontalSizeClass` **compact** iPhone
/// size class iPhone iPhoneiPad
/// Projects sheet idiom Projects /
/// idiom+ stack/split `LayoutPolicy`
/// size-class
enum ProjectsGridLayout {
/// iPhone / iPad 退
static let singleColumn = 1
/// iPad 2 sheet ~440540pt
static let twoColumnMinWidth: CGFloat = 400
/// iPad 3 /
static let threeColumnMinWidth: CGFloat = 760
/// iPhone`.phone`/ `singleColumn` List
/// iPad`.pad` **** 13 `>= 1`
/// 0
static func columnCount(
availableWidth: CGFloat,
idiom: UIUserInterfaceIdiom
) -> Int {
guard idiom == .pad else { return singleColumn }
if availableWidth >= threeColumnMinWidth { return 3 }
if availableWidth >= twoColumnMinWidth { return 2 }
return singleColumn
}
/// iPad iPhone List
static func usesGrid(idiom: UIUserInterfaceIdiom) -> Bool {
idiom == .pad
}
}
/// T-iPad-4 · Projects sheet detents idiom
enum ProjectsSheetSizing {
/// iPhone `nil`** `.presentationDetents`** sheet
/// iPhone iPad `[.medium, .large]`
///
static func detents(idiom: UIUserInterfaceIdiom) -> Set<PresentationDetent>? {
idiom == .pad ? [.medium, .large] : nil
}
}
// MARK: - iPhone iPad detents modifier
/// `.presentationDetents` modifier nil `content`
/// iPhone sheet
private struct AdaptiveSheetDetents: ViewModifier {
let detents: Set<PresentationDetent>?
func body(content: Content) -> some View {
if let detents {
content.presentationDetents(detents)
} else {
content
}
}
}
extension View {
/// iPhone sheetiPad detents
func adaptiveProjectsSheetDetents(idiom: UIUserInterfaceIdiom) -> some View {
modifier(AdaptiveSheetDetents(detents: ProjectsSheetSizing.detents(idiom: idiom)))
}
}