ios: some iPad fixes

This commit is contained in:
2026-05-03 18:38:16 -07:00
parent ae783020ef
commit 53a3b722ec
4 changed files with 97 additions and 44 deletions

View File

@@ -6,6 +6,7 @@ public struct SplitView: View {
@Environment(\.scenePhase) private var scenePhase @Environment(\.scenePhase) private var scenePhase
@State private var shouldRefreshOnForeground = false @State private var shouldRefreshOnForeground = false
@State private var composerFocusRequest = 0 @State private var composerFocusRequest = 0
@State private var columnVisibility: NavigationSplitViewVisibility = .automatic
private var keyboardActions: SybilKeyboardActions? { private var keyboardActions: SybilKeyboardActions? {
guard !viewModel.isCheckingSession, viewModel.isAuthenticated else { guard !viewModel.isCheckingSession, viewModel.isAuthenticated else {
@@ -50,10 +51,16 @@ public struct SplitView: View {
} else if horizontalSizeClass == .compact { } else if horizontalSizeClass == .compact {
SybilPhoneShellView(viewModel: viewModel) SybilPhoneShellView(viewModel: viewModel)
} else { } else {
NavigationSplitView { GeometryReader { proxy in
NavigationSplitView(columnVisibility: $columnVisibility) {
SybilSidebarView(viewModel: viewModel) SybilSidebarView(viewModel: viewModel)
} detail: { } detail: {
SybilWorkspaceView(viewModel: viewModel, composerFocusRequest: composerFocusRequest) { SybilWorkspaceView(
viewModel: viewModel,
composerFocusRequest: composerFocusRequest,
navigationLeadingControl: splitNavigationLeadingControl(for: proxy.size),
onShowSidebar: showSidebar
) {
viewModel.startNewChat() viewModel.startNewChat()
composerFocusRequest += 1 composerFocusRequest += 1
} }
@@ -62,6 +69,7 @@ public struct SplitView: View {
.tint(SybilTheme.primary) .tint(SybilTheme.primary)
} }
} }
}
.font(.sybil(.body)) .font(.sybil(.body))
.preferredColorScheme(.dark) .preferredColorScheme(.dark)
.focusedSceneValue(\.sybilKeyboardActions, keyboardActions) .focusedSceneValue(\.sybilKeyboardActions, keyboardActions)
@@ -93,6 +101,16 @@ public struct SplitView: View {
} }
} }
} }
private func splitNavigationLeadingControl(for size: CGSize) -> SybilWorkspaceNavigationLeadingControl {
return size.width < size.height ? .showSidebar : .hidden
}
private func showSidebar() {
withAnimation(.easeInOut(duration: 0.22)) {
columnVisibility = .all
}
}
} }
public struct SybilCommands: Commands { public struct SybilCommands: Commands {

View File

@@ -264,8 +264,7 @@ private struct SybilPhoneDestinationView: View {
var body: some View { var body: some View {
SybilWorkspaceView( SybilWorkspaceView(
viewModel: viewModel, viewModel: viewModel,
composerFocusRequest: composerFocusRequest, composerFocusRequest: composerFocusRequest
usesCustomChatNavigation: route.isChatTranscript
) { ) {
viewModel.startNewChat() viewModel.startNewChat()
composerFocusRequest += 1 composerFocusRequest += 1
@@ -297,14 +296,3 @@ private struct SybilPhoneDestinationView: View {
} }
} }
} }
private extension PhoneRoute {
var isChatTranscript: Bool {
switch self {
case .chat, .draftChat:
return true
case .search, .draftSearch, .settings:
return false
}
}
}

View File

@@ -6,6 +6,7 @@ struct SybilSearchResultsView: View {
var isLoading: Bool var isLoading: Bool
var isRunning: Bool var isRunning: Bool
var isStartingChat: Bool = false var isStartingChat: Bool = false
var topContentInset: CGFloat = 0
var onStartChat: (() -> Void)? = nil var onStartChat: (() -> Void)? = nil
var body: some View { var body: some View {
@@ -98,7 +99,8 @@ struct SybilSearchResultsView: View {
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 14) .padding(.horizontal, 14)
.padding(.vertical, 20) .padding(.top, 20 + topContentInset)
.padding(.bottom, 20)
} }
.scrollDismissesKeyboard(.interactively) .scrollDismissesKeyboard(.interactively)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)

View File

@@ -5,10 +5,18 @@ import SwiftUI
import UniformTypeIdentifiers import UniformTypeIdentifiers
import UIKit import UIKit
enum SybilWorkspaceNavigationLeadingControl {
case back
case hidden
case showSidebar
}
struct SybilWorkspaceView: View { struct SybilWorkspaceView: View {
@Bindable var viewModel: SybilViewModel @Bindable var viewModel: SybilViewModel
var composerFocusRequest: Int = 0 var composerFocusRequest: Int = 0
var usesCustomChatNavigation: Bool = false var usesCustomWorkspaceNavigation: Bool = true
var navigationLeadingControl: SybilWorkspaceNavigationLeadingControl = .back
var onShowSidebar: (() -> Void)? = nil
var onRequestNewChat: (() -> Void)? = nil var onRequestNewChat: (() -> Void)? = nil
@FocusState private var composerFocused: Bool @FocusState private var composerFocused: Bool
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@@ -26,7 +34,7 @@ struct SybilWorkspaceView: View {
@State private var newChatSwipeDidTriggerHaptic = false @State private var newChatSwipeDidTriggerHaptic = false
@State private var newChatSwipeFeedbackGenerator: UIImpactFeedbackGenerator? @State private var newChatSwipeFeedbackGenerator: UIImpactFeedbackGenerator?
private let customChatNavigationContentInset: CGFloat = 96 private let customWorkspaceNavigationContentInset: CGFloat = 96
private var isSettingsSelected: Bool { private var isSettingsSelected: Bool {
if case .settings = viewModel.selectedItem { if case .settings = viewModel.selectedItem {
@@ -39,8 +47,8 @@ struct SybilWorkspaceView: View {
viewModel.errorMessage != nil viewModel.errorMessage != nil
} }
private var showsCustomChatNavigation: Bool { private var showsCustomWorkspaceNavigation: Bool {
usesCustomChatNavigation && !isSettingsSelected && !viewModel.isSearchMode usesCustomWorkspaceNavigation && !isSettingsSelected
} }
private var transcriptScrollContextID: String { private var transcriptScrollContextID: String {
@@ -93,12 +101,12 @@ struct SybilWorkspaceView: View {
} }
.offset(x: newChatSwipeCompletionOffset) .offset(x: newChatSwipeCompletionOffset)
.background(SybilTheme.background) .background(SybilTheme.background)
.navigationTitle(showsCustomChatNavigation ? "" : viewModel.selectedTitle) .navigationTitle(showsCustomWorkspaceNavigation ? "" : viewModel.selectedTitle)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarRole(.editor) .toolbarRole(.editor)
.toolbar(showsCustomChatNavigation ? .hidden : .visible, for: .navigationBar) .toolbar(showsCustomWorkspaceNavigation ? .hidden : .visible, for: .navigationBar)
.toolbar { .toolbar {
if !isSettingsSelected && !showsCustomChatNavigation { if !isSettingsSelected && !showsCustomWorkspaceNavigation {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
if viewModel.isSearchMode { if viewModel.isSearchMode {
searchModeChip searchModeChip
@@ -124,10 +132,10 @@ struct SybilWorkspaceView: View {
ZStack(alignment: .top) { ZStack(alignment: .top) {
workspaceContentStack workspaceContentStack
if showsCustomChatNavigation { if showsCustomWorkspaceNavigation {
SybilChatCharacterBackdrop(isBusy: viewModel.isSending) SybilWorkspaceCharacterBackdrop(isBusy: viewModel.isSending)
.allowsHitTesting(false) .allowsHitTesting(false)
customChatNavigationBar customWorkspaceNavigationBar
} }
} }
} }
@@ -149,7 +157,8 @@ struct SybilWorkspaceView: View {
search: viewModel.selectedSearch, search: viewModel.selectedSearch,
isLoading: viewModel.isLoadingSelection, isLoading: viewModel.isLoadingSelection,
isRunning: viewModel.isSending, isRunning: viewModel.isSending,
isStartingChat: viewModel.isCreatingSearchChat isStartingChat: viewModel.isCreatingSearchChat,
topContentInset: showsCustomWorkspaceNavigation ? customWorkspaceNavigationContentInset : 0
) { ) {
Task { Task {
await viewModel.startChatFromSelectedSearch() await viewModel.startChatFromSelectedSearch()
@@ -160,7 +169,7 @@ struct SybilWorkspaceView: View {
messages: viewModel.displayedMessages, messages: viewModel.displayedMessages,
isLoading: viewModel.isLoadingSelection, isLoading: viewModel.isLoadingSelection,
isSending: viewModel.isSending, isSending: viewModel.isSending,
topContentInset: showsCustomChatNavigation ? customChatNavigationContentInset : 0 topContentInset: showsCustomWorkspaceNavigation ? customWorkspaceNavigationContentInset : 0
) )
.id(transcriptScrollContextID) .id(transcriptScrollContextID)
} }
@@ -193,15 +202,9 @@ struct SybilWorkspaceView: View {
} }
} }
private var customChatNavigationBar: some View { private var customWorkspaceNavigationBar: some View {
HStack(spacing: 14) { HStack(spacing: 14) {
Button { workspaceNavigationLeadingControl
dismiss()
} label: {
SybilNavigationIcon(systemImage: "chevron.left")
}
.buttonStyle(.plain)
.accessibilityLabel("Back")
Text(viewModel.selectedTitle) Text(viewModel.selectedTitle)
.font(.sybil(size: 16, weight: .semibold)) .font(.sybil(size: 16, weight: .semibold))
@@ -211,7 +214,7 @@ struct SybilWorkspaceView: View {
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
providerModelNavigationMenu workspaceNavigationTrailingControl
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.top, 10) .padding(.top, 10)
@@ -222,6 +225,32 @@ struct SybilWorkspaceView: View {
} }
} }
@ViewBuilder
private var workspaceNavigationLeadingControl: some View {
switch navigationLeadingControl {
case .back:
Button {
dismiss()
} label: {
SybilNavigationIcon(systemImage: "chevron.left")
}
.buttonStyle(.plain)
.accessibilityLabel("Back")
case .showSidebar:
Button {
onShowSidebar?()
} label: {
SybilNavigationIcon(systemImage: "sidebar.left")
}
.buttonStyle(.plain)
.accessibilityLabel("Show sidebar")
case .hidden:
EmptyView()
}
}
private func beginNewChatSwipe(containerWidth: CGFloat) { private func beginNewChatSwipe(containerWidth: CGFloat) {
let update = { let update = {
newChatSwipeContainerWidth = max(containerWidth, 1) newChatSwipeContainerWidth = max(containerWidth, 1)
@@ -367,6 +396,22 @@ struct SybilWorkspaceView: View {
} }
} }
@ViewBuilder
private var workspaceNavigationTrailingControl: some View {
if viewModel.isSearchMode {
searchModeNavigationLabel
} else {
providerModelNavigationMenu
}
}
private var searchModeNavigationLabel: some View {
Label("Search", systemImage: "globe")
.font(.sybil(.caption, weight: .medium))
.foregroundStyle(SybilTheme.accent)
.lineLimit(1)
}
private func providerModelMenu<Label: View>(@ViewBuilder label: @escaping () -> Label) -> some View { private func providerModelMenu<Label: View>(@ViewBuilder label: @escaping () -> Label) -> some View {
Menu { Menu {
providerModelMenuItems providerModelMenuItems
@@ -983,7 +1028,7 @@ private struct SybilNavigationFadeBackground: View {
} }
} }
private struct SybilChatCharacterBackdrop: View { private struct SybilWorkspaceCharacterBackdrop: View {
var isBusy: Bool var isBusy: Bool
var body: some View { var body: some View {