diff --git a/ios/Packages/Sybil/Sources/Sybil/SplitView.swift b/ios/Packages/Sybil/Sources/Sybil/SplitView.swift index 460ef3a..ab909f7 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SplitView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SplitView.swift @@ -4,8 +4,9 @@ public struct SplitView: View { @State private var viewModel = SybilViewModel() @Environment(\.horizontalSizeClass) private var horizontalSizeClass - public init() { + @MainActor public init() { SybilFontRegistry.registerIfNeeded() + SybilTheme.applySystemAppearance() } public var body: some View { @@ -25,7 +26,6 @@ public struct SplitView: View { } else { NavigationSplitView { SybilSidebarView(viewModel: viewModel) - .navigationTitle("Sybil") } detail: { SybilWorkspaceView(viewModel: viewModel) } @@ -34,6 +34,7 @@ public struct SplitView: View { } } .font(.sybil(.body)) + .preferredColorScheme(.dark) .task { await viewModel.bootstrap() } diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilChatTranscriptView.swift b/ios/Packages/Sybil/Sources/Sybil/SybilChatTranscriptView.swift index 7194946..d8f342b 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilChatTranscriptView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilChatTranscriptView.swift @@ -25,6 +25,7 @@ struct SybilChatTranscriptView: View { ForEach(messages) { message in MessageBubble(message: message, isSending: isSending) + .frame(maxWidth: .infinity) .id(message.id) } @@ -86,10 +87,8 @@ private struct MessageBubble: View { } var body: some View { - HStack(alignment: .top) { - if isUser { - Spacer(minLength: 44) - } + HStack(alignment: .top, spacing: 0) { + leadingSpacer if let toolCallMetadata { ToolCallActivityChip( @@ -136,12 +135,24 @@ private struct MessageBubble: View { .frame(maxWidth: isUser ? 420 : nil, alignment: isUser ? .trailing : .leading) } - if !isUser { - Spacer(minLength: 0) - } + trailingSpacer } .frame(maxWidth: .infinity, alignment: isUser ? .trailing : .leading) } + + @ViewBuilder + private var leadingSpacer: some View { + if isUser { + Spacer(minLength: 44) + } + } + + @ViewBuilder + private var trailingSpacer: some View { + if !isUser { + Spacer(minLength: 0) + } + } } private struct ToolCallActivityChip: View { diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilMarkdownTheme.swift b/ios/Packages/Sybil/Sources/Sybil/SybilMarkdownTheme.swift index 582342c..db10a5b 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilMarkdownTheme.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilMarkdownTheme.swift @@ -10,6 +10,7 @@ extension Theme { .text { FontFamily(.custom("Inter")) FontSize(15) + ForegroundColor(SybilTheme.text) } .code { FontFamilyVariant(.monospaced) diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilPhoneShellView.swift b/ios/Packages/Sybil/Sources/Sybil/SybilPhoneShellView.swift index 8dde7e7..05bc963 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilPhoneShellView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilPhoneShellView.swift @@ -30,8 +30,8 @@ struct SybilPhoneShellView: View { .navigationTitle("") .navigationBarTitleDisplayMode(.inline) .toolbar { - ToolbarItem(placement: .principal) { - SybilWordmark(size: 19) + ToolbarItem(placement: .topBarLeading) { + SybilWordmark(size: 18) } } .navigationDestination(for: PhoneRoute.self) { route in diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilSidebarView.swift b/ios/Packages/Sybil/Sources/Sybil/SybilSidebarView.swift index 567e435..12d7611 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilSidebarView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilSidebarView.swift @@ -18,8 +18,6 @@ struct SybilSidebarView: View { var body: some View { VStack(spacing: 0) { VStack(alignment: .leading, spacing: 14) { - SybilWordmark(size: 31) - VStack(spacing: 10) { sidebarActionButton( title: "New chat", @@ -174,6 +172,13 @@ struct SybilSidebarView: View { .padding(10) } .background(SybilTheme.panelGradient) + .navigationTitle("") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + SybilWordmark(size: 18) + } + } } private func sidebarActionButton( diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilTheme.swift b/ios/Packages/Sybil/Sources/Sybil/SybilTheme.swift index 8d8fbc1..a8162d4 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilTheme.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilTheme.swift @@ -1,6 +1,7 @@ import CoreText import Foundation import SwiftUI +import UIKit enum SybilFontRegistry { static func registerIfNeeded() { @@ -78,6 +79,23 @@ enum SybilTheme { static let userBubble = Color(red: 0.29, green: 0.13, blue: 0.65) static let danger = Color(red: 0.96, green: 0.32, blue: 0.40) + @MainActor static func applySystemAppearance() { + let navAppearance = UINavigationBarAppearance() + navAppearance.configureWithOpaqueBackground() + navAppearance.backgroundColor = UIColor(red: 0.02, green: 0.02, blue: 0.05, alpha: 1) + navAppearance.shadowColor = UIColor(red: 0.24, green: 0.20, blue: 0.38, alpha: 0.9) + navAppearance.titleTextAttributes = [ + .foregroundColor: UIColor(red: 0.96, green: 0.94, blue: 1.0, alpha: 1) + ] + navAppearance.largeTitleTextAttributes = navAppearance.titleTextAttributes + + UINavigationBar.appearance().prefersLargeTitles = false + UINavigationBar.appearance().standardAppearance = navAppearance + UINavigationBar.appearance().compactAppearance = navAppearance + UINavigationBar.appearance().scrollEdgeAppearance = navAppearance + UINavigationBar.appearance().compactScrollEdgeAppearance = navAppearance + } + static var backgroundGradient: LinearGradient { LinearGradient( colors: [ diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift b/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift index b538448..028535f 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift @@ -3,13 +3,8 @@ import SwiftUI struct SybilWorkspaceView: View { @Bindable var viewModel: SybilViewModel - @Environment(\.horizontalSizeClass) private var horizontalSizeClass @FocusState private var composerFocused: Bool - private var isCompact: Bool { - horizontalSizeClass == .compact - } - private var isSettingsSelected: Bool { if case .settings = viewModel.selectedItem { return true @@ -18,7 +13,7 @@ struct SybilWorkspaceView: View { } private var showsHeader: Bool { - !isCompact || viewModel.errorMessage != nil + viewModel.errorMessage != nil } var body: some View { @@ -55,20 +50,17 @@ struct SybilWorkspaceView: View { composerBar } } - .navigationTitle(isCompact ? "" : viewModel.selectedTitle) + .navigationTitle(viewModel.selectedTitle) + .navigationBarTitleDisplayMode(.inline) + .toolbarRole(.editor) .toolbar { - if isCompact { - ToolbarItem(placement: .principal) { - Text(viewModel.selectedTitle) - .font(.sybil(.headline, weight: .semibold)) - .foregroundStyle(SybilTheme.text) - .lineLimit(1) - } - } - - if isCompact && !viewModel.isSearchMode && !isSettingsSelected { + if !isSettingsSelected { ToolbarItem(placement: .topBarTrailing) { - compactProviderModelMenu + if viewModel.isSearchMode { + searchModeChip + } else { + providerModelMenu + } } } } @@ -78,30 +70,6 @@ struct SybilWorkspaceView: View { private var header: some View { VStack(alignment: .leading, spacing: 12) { - if !isCompact { - HStack(alignment: .top, spacing: 12) { - Spacer() - - if !viewModel.isSearchMode && !isSettingsSelected { - providerControls - } else if viewModel.isSearchMode { - Label("Search mode", systemImage: "globe") - .font(.sybil(.caption, weight: .medium)) - .foregroundStyle(SybilTheme.accent) - .padding(.horizontal, 10) - .padding(.vertical, 7) - .background( - RoundedRectangle(cornerRadius: 10) - .fill(SybilTheme.accent.opacity(0.10)) - .overlay( - RoundedRectangle(cornerRadius: 10) - .stroke(SybilTheme.accent.opacity(0.24), lineWidth: 1) - ) - ) - } - } - } - if let error = viewModel.errorMessage { Text(error) .font(.sybil(.footnote)) @@ -114,7 +82,7 @@ struct SybilWorkspaceView: View { .background(SybilTheme.panelGradient.opacity(0.58)) } - private var compactProviderModelMenu: some View { + private var providerModelMenu: some View { Menu { Text("\(viewModel.provider.displayName) • \(viewModel.model)") .font(.sybil(.caption)) @@ -158,55 +126,20 @@ struct SybilWorkspaceView: View { .accessibilityLabel("Provider and model") } - private var providerControls: some View { - HStack(spacing: 8) { - Menu { - ForEach(Provider.allCases, id: \.self) { candidate in - Button(candidate.displayName) { - viewModel.setProvider(candidate) - } - } - } label: { - Label(viewModel.provider.displayName, systemImage: "chevron.down") - .labelStyle(.titleAndIcon) - .font(.sybil(.caption, weight: .medium)) - .foregroundStyle(SybilTheme.text) - .padding(.horizontal, 10) - .padding(.vertical, 7) - .background( - RoundedRectangle(cornerRadius: 10) - .fill(SybilTheme.surface.opacity(0.78)) - .overlay( - RoundedRectangle(cornerRadius: 10) - .stroke(SybilTheme.border.opacity(0.88), lineWidth: 1) - ) + private var searchModeChip: some View { + Label("Search", systemImage: "globe") + .font(.sybil(.caption, weight: .medium)) + .foregroundStyle(SybilTheme.accent) + .padding(.horizontal, 10) + .padding(.vertical, 7) + .background( + Capsule() + .fill(SybilTheme.accent.opacity(0.10)) + .overlay( + Capsule() + .stroke(SybilTheme.accent.opacity(0.24), lineWidth: 1) ) - } - - Menu { - ForEach(viewModel.providerModelOptions, id: \.self) { model in - Button(model) { - viewModel.setModel(model) - } - } - } label: { - Label(viewModel.model, systemImage: "chevron.down") - .labelStyle(.titleAndIcon) - .font(.sybil(.caption, weight: .medium)) - .foregroundStyle(SybilTheme.text) - .lineLimit(1) - .padding(.horizontal, 10) - .padding(.vertical, 7) - .background( - RoundedRectangle(cornerRadius: 10) - .fill(SybilTheme.surface.opacity(0.78)) - .overlay( - RoundedRectangle(cornerRadius: 10) - .stroke(SybilTheme.border.opacity(0.88), lineWidth: 1) - ) - ) - } - } + ) } private var composerBar: some View {