From 53a3b722ec35c5e9275f616629070f3de94fc037 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sun, 3 May 2026 18:38:16 -0700 Subject: [PATCH] ios: some iPad fixes --- .../Sybil/Sources/Sybil/SplitView.swift | 34 +++++-- .../Sources/Sybil/SybilPhoneShellView.swift | 14 +-- .../Sybil/SybilSearchResultsView.swift | 4 +- .../Sources/Sybil/SybilWorkspaceView.swift | 89 ++++++++++++++----- 4 files changed, 97 insertions(+), 44 deletions(-) diff --git a/ios/Packages/Sybil/Sources/Sybil/SplitView.swift b/ios/Packages/Sybil/Sources/Sybil/SplitView.swift index ad8969f..983001c 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SplitView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SplitView.swift @@ -6,6 +6,7 @@ public struct SplitView: View { @Environment(\.scenePhase) private var scenePhase @State private var shouldRefreshOnForeground = false @State private var composerFocusRequest = 0 + @State private var columnVisibility: NavigationSplitViewVisibility = .automatic private var keyboardActions: SybilKeyboardActions? { guard !viewModel.isCheckingSession, viewModel.isAuthenticated else { @@ -50,16 +51,23 @@ public struct SplitView: View { } else if horizontalSizeClass == .compact { SybilPhoneShellView(viewModel: viewModel) } else { - NavigationSplitView { - SybilSidebarView(viewModel: viewModel) - } detail: { - SybilWorkspaceView(viewModel: viewModel, composerFocusRequest: composerFocusRequest) { - viewModel.startNewChat() - composerFocusRequest += 1 + GeometryReader { proxy in + NavigationSplitView(columnVisibility: $columnVisibility) { + SybilSidebarView(viewModel: viewModel) + } detail: { + SybilWorkspaceView( + viewModel: viewModel, + composerFocusRequest: composerFocusRequest, + navigationLeadingControl: splitNavigationLeadingControl(for: proxy.size), + onShowSidebar: showSidebar + ) { + viewModel.startNewChat() + composerFocusRequest += 1 + } } + .navigationSplitViewStyle(.balanced) + .tint(SybilTheme.primary) } - .navigationSplitViewStyle(.balanced) - .tint(SybilTheme.primary) } } .font(.sybil(.body)) @@ -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 { diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilPhoneShellView.swift b/ios/Packages/Sybil/Sources/Sybil/SybilPhoneShellView.swift index 425991e..6c6a30a 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilPhoneShellView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilPhoneShellView.swift @@ -264,8 +264,7 @@ private struct SybilPhoneDestinationView: View { var body: some View { SybilWorkspaceView( viewModel: viewModel, - composerFocusRequest: composerFocusRequest, - usesCustomChatNavigation: route.isChatTranscript + composerFocusRequest: composerFocusRequest ) { viewModel.startNewChat() 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 - } - } -} diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilSearchResultsView.swift b/ios/Packages/Sybil/Sources/Sybil/SybilSearchResultsView.swift index 04ca645..b40d222 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilSearchResultsView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilSearchResultsView.swift @@ -6,6 +6,7 @@ struct SybilSearchResultsView: View { var isLoading: Bool var isRunning: Bool var isStartingChat: Bool = false + var topContentInset: CGFloat = 0 var onStartChat: (() -> Void)? = nil var body: some View { @@ -98,7 +99,8 @@ struct SybilSearchResultsView: View { } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 14) - .padding(.vertical, 20) + .padding(.top, 20 + topContentInset) + .padding(.bottom, 20) } .scrollDismissesKeyboard(.interactively) .frame(maxWidth: .infinity, alignment: .leading) diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift b/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift index 320b050..d828a02 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift @@ -5,10 +5,18 @@ import SwiftUI import UniformTypeIdentifiers import UIKit +enum SybilWorkspaceNavigationLeadingControl { + case back + case hidden + case showSidebar +} + struct SybilWorkspaceView: View { @Bindable var viewModel: SybilViewModel var composerFocusRequest: Int = 0 - var usesCustomChatNavigation: Bool = false + var usesCustomWorkspaceNavigation: Bool = true + var navigationLeadingControl: SybilWorkspaceNavigationLeadingControl = .back + var onShowSidebar: (() -> Void)? = nil var onRequestNewChat: (() -> Void)? = nil @FocusState private var composerFocused: Bool @Environment(\.dismiss) private var dismiss @@ -26,7 +34,7 @@ struct SybilWorkspaceView: View { @State private var newChatSwipeDidTriggerHaptic = false @State private var newChatSwipeFeedbackGenerator: UIImpactFeedbackGenerator? - private let customChatNavigationContentInset: CGFloat = 96 + private let customWorkspaceNavigationContentInset: CGFloat = 96 private var isSettingsSelected: Bool { if case .settings = viewModel.selectedItem { @@ -39,8 +47,8 @@ struct SybilWorkspaceView: View { viewModel.errorMessage != nil } - private var showsCustomChatNavigation: Bool { - usesCustomChatNavigation && !isSettingsSelected && !viewModel.isSearchMode + private var showsCustomWorkspaceNavigation: Bool { + usesCustomWorkspaceNavigation && !isSettingsSelected } private var transcriptScrollContextID: String { @@ -93,12 +101,12 @@ struct SybilWorkspaceView: View { } .offset(x: newChatSwipeCompletionOffset) .background(SybilTheme.background) - .navigationTitle(showsCustomChatNavigation ? "" : viewModel.selectedTitle) + .navigationTitle(showsCustomWorkspaceNavigation ? "" : viewModel.selectedTitle) .navigationBarTitleDisplayMode(.inline) .toolbarRole(.editor) - .toolbar(showsCustomChatNavigation ? .hidden : .visible, for: .navigationBar) + .toolbar(showsCustomWorkspaceNavigation ? .hidden : .visible, for: .navigationBar) .toolbar { - if !isSettingsSelected && !showsCustomChatNavigation { + if !isSettingsSelected && !showsCustomWorkspaceNavigation { ToolbarItem(placement: .topBarTrailing) { if viewModel.isSearchMode { searchModeChip @@ -124,10 +132,10 @@ struct SybilWorkspaceView: View { ZStack(alignment: .top) { workspaceContentStack - if showsCustomChatNavigation { - SybilChatCharacterBackdrop(isBusy: viewModel.isSending) + if showsCustomWorkspaceNavigation { + SybilWorkspaceCharacterBackdrop(isBusy: viewModel.isSending) .allowsHitTesting(false) - customChatNavigationBar + customWorkspaceNavigationBar } } } @@ -149,7 +157,8 @@ struct SybilWorkspaceView: View { search: viewModel.selectedSearch, isLoading: viewModel.isLoadingSelection, isRunning: viewModel.isSending, - isStartingChat: viewModel.isCreatingSearchChat + isStartingChat: viewModel.isCreatingSearchChat, + topContentInset: showsCustomWorkspaceNavigation ? customWorkspaceNavigationContentInset : 0 ) { Task { await viewModel.startChatFromSelectedSearch() @@ -160,7 +169,7 @@ struct SybilWorkspaceView: View { messages: viewModel.displayedMessages, isLoading: viewModel.isLoadingSelection, isSending: viewModel.isSending, - topContentInset: showsCustomChatNavigation ? customChatNavigationContentInset : 0 + topContentInset: showsCustomWorkspaceNavigation ? customWorkspaceNavigationContentInset : 0 ) .id(transcriptScrollContextID) } @@ -193,15 +202,9 @@ struct SybilWorkspaceView: View { } } - private var customChatNavigationBar: some View { + private var customWorkspaceNavigationBar: some View { HStack(spacing: 14) { - Button { - dismiss() - } label: { - SybilNavigationIcon(systemImage: "chevron.left") - } - .buttonStyle(.plain) - .accessibilityLabel("Back") + workspaceNavigationLeadingControl Text(viewModel.selectedTitle) .font(.sybil(size: 16, weight: .semibold)) @@ -211,7 +214,7 @@ struct SybilWorkspaceView: View { .frame(maxWidth: .infinity, alignment: .leading) .multilineTextAlignment(.leading) - providerModelNavigationMenu + workspaceNavigationTrailingControl } .padding(.horizontal, 16) .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) { let update = { 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(@ViewBuilder label: @escaping () -> Label) -> some View { Menu { providerModelMenuItems @@ -983,7 +1028,7 @@ private struct SybilNavigationFadeBackground: View { } } -private struct SybilChatCharacterBackdrop: View { +private struct SybilWorkspaceCharacterBackdrop: View { var isBusy: Bool var body: some View {