ios: update style

This commit is contained in:
2026-05-02 16:23:00 -07:00
parent adb9e15b6c
commit cb368a4005
15 changed files with 667 additions and 217 deletions

View File

@@ -3,8 +3,13 @@ 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
@@ -12,12 +17,18 @@ struct SybilWorkspaceView: View {
return false
}
private var showsHeader: Bool {
!isCompact || viewModel.errorMessage != nil
}
var body: some View {
VStack(spacing: 0) {
header
if showsHeader {
header
Divider()
.overlay(SybilTheme.border)
Divider()
.overlay(SybilTheme.border)
}
Group {
if isSettingsSelected {
@@ -44,7 +55,23 @@ struct SybilWorkspaceView: View {
composerBar
}
}
.navigationTitle(viewModel.selectedTitle)
.navigationTitle(isCompact ? "" : viewModel.selectedTitle)
.toolbar {
if isCompact {
ToolbarItem(placement: .principal) {
Text(viewModel.selectedTitle)
.font(.sybil(.headline, weight: .semibold))
.foregroundStyle(SybilTheme.text)
.lineLimit(1)
}
}
if isCompact && !viewModel.isSearchMode && !isSettingsSelected {
ToolbarItem(placement: .topBarTrailing) {
compactProviderModelMenu
}
}
}
.background(SybilTheme.background)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.onChange(of: viewModel.isSending) { _, isSending in
@@ -56,37 +83,84 @@ struct SybilWorkspaceView: View {
private var header: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(alignment: .top, spacing: 12) {
Spacer()
if !isCompact {
HStack(alignment: .top, spacing: 12) {
Spacer()
if !viewModel.isSearchMode && !isSettingsSelected {
providerControls
} else if viewModel.isSearchMode {
Label("Search mode", systemImage: "globe")
.font(.caption.weight(.medium))
.foregroundStyle(SybilTheme.textMuted)
.padding(.horizontal, 10)
.padding(.vertical, 7)
.background(
Capsule()
.fill(SybilTheme.surface)
.overlay(
Capsule()
.stroke(SybilTheme.border, lineWidth: 1)
)
)
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(.footnote)
.font(.sybil(.footnote))
.foregroundStyle(SybilTheme.danger)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(SybilTheme.panelGradient.opacity(0.58))
}
private var compactProviderModelMenu: some View {
Menu {
Text("\(viewModel.provider.displayName)\(viewModel.model)")
.font(.sybil(.caption))
Divider()
ForEach(Provider.allCases, id: \.self) { candidate in
Menu(candidate.displayName) {
let models = viewModel.modelOptions(for: candidate)
if models.isEmpty {
Text("No models")
} else {
ForEach(models, id: \.self) { candidateModel in
Button {
viewModel.setProvider(candidate, model: candidateModel)
} label: {
if viewModel.provider == candidate && viewModel.model == candidateModel {
Label(candidateModel, systemImage: "checkmark")
} else {
Text(candidateModel)
}
}
}
}
}
}
} label: {
Image(systemName: "ellipsis")
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(SybilTheme.text)
.frame(width: 34, height: 34)
.background(
Circle()
.fill(SybilTheme.surface.opacity(0.78))
)
.overlay(
Circle()
.stroke(SybilTheme.border.opacity(0.82), lineWidth: 1)
)
}
.accessibilityLabel("Provider and model")
}
private var providerControls: some View {
@@ -100,16 +174,16 @@ struct SybilWorkspaceView: View {
} label: {
Label(viewModel.provider.displayName, systemImage: "chevron.down")
.labelStyle(.titleAndIcon)
.font(.caption.weight(.medium))
.font(.sybil(.caption, weight: .medium))
.foregroundStyle(SybilTheme.text)
.padding(.horizontal, 10)
.padding(.vertical, 7)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(SybilTheme.surface)
.fill(SybilTheme.surface.opacity(0.78))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(SybilTheme.border, lineWidth: 1)
.stroke(SybilTheme.border.opacity(0.88), lineWidth: 1)
)
)
}
@@ -123,17 +197,17 @@ struct SybilWorkspaceView: View {
} label: {
Label(viewModel.model, systemImage: "chevron.down")
.labelStyle(.titleAndIcon)
.font(.caption.weight(.medium))
.font(.sybil(.caption, weight: .medium))
.foregroundStyle(SybilTheme.text)
.lineLimit(1)
.padding(.horizontal, 10)
.padding(.vertical, 7)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(SybilTheme.surface)
.fill(SybilTheme.surface.opacity(0.78))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(SybilTheme.border, lineWidth: 1)
.stroke(SybilTheme.border.opacity(0.88), lineWidth: 1)
)
)
}
@@ -161,10 +235,10 @@ struct SybilWorkspaceView: View {
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(SybilTheme.surface)
.fill(SybilTheme.composerGradient)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(SybilTheme.border, lineWidth: 1)
.stroke(SybilTheme.primary.opacity(0.34), lineWidth: 1)
)
)
.foregroundStyle(SybilTheme.text)
@@ -175,11 +249,15 @@ struct SybilWorkspaceView: View {
}
} label: {
Image(systemName: viewModel.isSearchMode ? "magnifyingglass" : "arrow.up")
.font(.headline.weight(.semibold))
.font(.system(size: 17, weight: .semibold))
.frame(width: 40, height: 40)
.background(
Circle()
.fill(viewModel.composer.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isSending ? SybilTheme.surface : SybilTheme.primary)
.fill(
viewModel.composer.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isSending
? AnyShapeStyle(SybilTheme.surface)
: AnyShapeStyle(SybilTheme.primaryGradient)
)
)
.foregroundStyle(viewModel.composer.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isSending ? SybilTheme.textMuted : SybilTheme.text)
}
@@ -188,6 +266,15 @@ struct SybilWorkspaceView: View {
}
.padding(.horizontal, 14)
.padding(.vertical, 12)
.background(SybilTheme.background)
.background(
LinearGradient(
colors: [
SybilTheme.background.opacity(0.18),
SybilTheme.background.opacity(0.96)
],
startPoint: .top,
endPoint: .bottom
)
)
}
}