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

@@ -11,14 +11,26 @@ struct ProjectsScreen: View {
@Bindable var viewModel: ProjectsViewModel
/// "" AppCoordinator.openProject
var onOpen: (ProjectOpenRequest) -> Void = { _ in }
/// T-iPad-4 · idiom `ProjectsGridLayout`/
/// `ProjectsSheetSizing` iPhone List
/// iPad + sheet detentsidiom
/// @Environment `ProjectsLayout`
/// idiom size classiPad sheet compact
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
private enum Metrics {
static let rowSpacing: CGFloat = 2
static let chipSpacing: CGFloat = 6
// T-iPad-4 · iPad.padiPhone
static let gridSectionSpacing: CGFloat = 12
static let gridPadding: CGFloat = 16
static let gridCardVerticalPadding: CGFloat = 8
static let gridCardHorizontalPadding: CGFloat = 10
static let gridCardCornerRadius: CGFloat = 10
}
var body: some View {
list
adaptiveContent
.navigationTitle(ProjectsCopy.title)
.navigationBarTitleDisplayMode(.inline)
.searchable(text: $viewModel.searchText, prompt: ProjectsCopy.searchPrompt)
@@ -36,6 +48,22 @@ struct ProjectsScreen: View {
onOpenClaude: { viewModel.requestOpenClaude(cwd: $0) }
)
}
// T-iPad-4 · iPhone sheet iPad
// detents
.adaptiveProjectsSheetDetents(idiom: idiom)
}
// MARK: - idiom ProjectsGridLayout
/// iPhone `list`iPad
/// `gridList`///prefs `ProjectsViewModel`
///
@ViewBuilder private var adaptiveContent: some View {
if ProjectsGridLayout.usesGrid(idiom: idiom) {
gridList
} else {
list
}
}
// MARK: - List
@@ -61,6 +89,75 @@ struct ProjectsScreen: View {
}
}
// MARK: - Gridregular
/// iPad / `ProjectsGridLayout.columnCount`
/// ****GeometryReader/// List
/// builder`groupHeader`/`projectRow`/`errorRows`
/// //prefs
private var gridList: some View {
GeometryReader { proxy in
let columns = ProjectsGridLayout.columnCount(
availableWidth: proxy.size.width,
idiom: idiom
)
ScrollView {
LazyVStack(alignment: .leading, spacing: Metrics.gridSectionSpacing) {
errorRows
if let message = viewModel.emptyStateMessage {
Text(message)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .center)
}
ForEach(viewModel.groups) { group in
gridSection(group, columns: columns)
}
}
.padding(Metrics.gridPadding)
}
.overlay {
if !viewModel.hasLoadedOnce && viewModel.fetchErrorMessage == nil {
ProgressView()
}
}
}
}
@ViewBuilder private func gridSection(_ group: ProjectGroup, columns: Int) -> some View {
let isCollapsed = viewModel.isCollapsed(group)
VStack(alignment: .leading, spacing: Metrics.rowSpacing) {
if group.kind != .flat {
groupHeader(group, isCollapsed: isCollapsed)
.padding(.top, Metrics.gridSectionSpacing)
}
if !isCollapsed {
LazyVGrid(
columns: gridColumns(columns),
alignment: .leading,
spacing: Metrics.chipSpacing
) {
ForEach(group.projects, id: \.path) { project in
projectRow(project, group: group)
.padding(.vertical, Metrics.gridCardVerticalPadding)
.padding(.horizontal, Metrics.gridCardHorizontalPadding)
.background(
.quaternary,
in: RoundedRectangle(cornerRadius: Metrics.gridCardCornerRadius)
)
}
}
}
}
}
/// `columns >= 1` `ProjectsGridLayout`
private func gridColumns(_ count: Int) -> [GridItem] {
Array(
repeating: GridItem(.flexible(), spacing: Metrics.chipSpacing),
count: max(count, ProjectsGridLayout.singleColumn)
)
}
/// prefs
@ViewBuilder private var errorRows: some View {
ForEach(