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:
@@ -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 detents。idiom 是设备常量、
|
||||
/// 运行期不变,故非 @Environment 即可。见 `ProjectsLayout` 注释解释为何用
|
||||
/// idiom 而非 size class(iPad 表单 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(.pad)分支,iPhone 不受影响。
|
||||
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: - Grid(regular 宽度:多列网格)
|
||||
|
||||
/// 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(
|
||||
|
||||
Reference in New Issue
Block a user