diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilChatTranscriptView.swift b/ios/Packages/Sybil/Sources/Sybil/SybilChatTranscriptView.swift index fa23eb5..7194946 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilChatTranscriptView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilChatTranscriptView.swift @@ -15,7 +15,7 @@ struct SybilChatTranscriptView: View { var body: some View { ScrollViewReader { proxy in ScrollView { - LazyVStack(alignment: .leading, spacing: 24) { + LazyVStack(alignment: .leading, spacing: 26) { if isLoading && messages.isEmpty { Text("Loading messages…") .font(.sybil(.footnote)) @@ -113,13 +113,11 @@ private struct MessageBubble: View { Markdown(message.content) .tint(SybilTheme.primary) .foregroundStyle(isUser ? SybilTheme.text : SybilTheme.text.opacity(0.95)) - .markdownTextStyle { - FontSize(15) - } + .markdownTheme(.sybilReadable) } } .padding(.horizontal, isUser ? 14 : 2) - .padding(.vertical, isUser ? 11 : 2) + .padding(.vertical, isUser ? 13 : 2) .background( Group { if isUser { @@ -224,6 +222,7 @@ private struct ToolCallActivityChip: View { Text(summary) .font(.sybil(.subheadline)) .foregroundStyle(isFailed ? SybilTheme.danger.opacity(0.96) : SybilTheme.text.opacity(0.94)) + .lineSpacing(3) .fixedSize(horizontal: false, vertical: true) HStack(spacing: 6) { diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilMarkdownTheme.swift b/ios/Packages/Sybil/Sources/Sybil/SybilMarkdownTheme.swift new file mode 100644 index 0000000..582342c --- /dev/null +++ b/ios/Packages/Sybil/Sources/Sybil/SybilMarkdownTheme.swift @@ -0,0 +1,164 @@ +import MarkdownUI +import SwiftUI + +@MainActor +extension Theme { + static let sybilReadable: Theme = { + SybilFontRegistry.registerIfNeeded() + + return Theme() + .text { + FontFamily(.custom("Inter")) + FontSize(15) + } + .code { + FontFamilyVariant(.monospaced) + FontSize(.em(0.88)) + BackgroundColor(SybilTheme.surfaceStrong.opacity(0.78)) + } + .strong { + FontWeight(.semibold) + } + .link { + ForegroundColor(SybilTheme.accent) + } + .heading1 { configuration in + configuration.label + .fixedSize(horizontal: false, vertical: true) + .relativeLineSpacing(.em(0.18)) + .markdownMargin(top: .em(1.1), bottom: .em(0.5)) + .markdownTextStyle { + FontWeight(.semibold) + FontSize(.em(1.45)) + } + } + .heading2 { configuration in + configuration.label + .fixedSize(horizontal: false, vertical: true) + .relativeLineSpacing(.em(0.18)) + .markdownMargin(top: .em(1.0), bottom: .em(0.45)) + .markdownTextStyle { + FontWeight(.semibold) + FontSize(.em(1.25)) + } + } + .heading3 { configuration in + configuration.label + .fixedSize(horizontal: false, vertical: true) + .relativeLineSpacing(.em(0.18)) + .markdownMargin(top: .em(0.9), bottom: .em(0.4)) + .markdownTextStyle { + FontWeight(.semibold) + FontSize(.em(1.12)) + } + } + .heading4 { configuration in + configuration.label + .fixedSize(horizontal: false, vertical: true) + .relativeLineSpacing(.em(0.18)) + .markdownMargin(top: .em(0.85), bottom: .em(0.35)) + .markdownTextStyle { + FontWeight(.semibold) + } + } + .heading5 { configuration in + configuration.label + .fixedSize(horizontal: false, vertical: true) + .relativeLineSpacing(.em(0.18)) + .markdownMargin(top: .em(0.8), bottom: .em(0.35)) + .markdownTextStyle { + FontWeight(.semibold) + FontSize(.em(0.92)) + } + } + .heading6 { configuration in + configuration.label + .fixedSize(horizontal: false, vertical: true) + .relativeLineSpacing(.em(0.18)) + .markdownMargin(top: .em(0.8), bottom: .em(0.35)) + .markdownTextStyle { + FontWeight(.semibold) + FontSize(.em(0.86)) + } + } + .paragraph { configuration in + configuration.label + .fixedSize(horizontal: false, vertical: true) + .relativeLineSpacing(.em(0.36)) + .markdownMargin(top: .zero, bottom: .em(0.82)) + } + .blockquote { configuration in + HStack(alignment: .top, spacing: 10) { + RoundedRectangle(cornerRadius: 2) + .fill(SybilTheme.primary.opacity(0.55)) + .frame(width: 3) + configuration.label + .markdownTextStyle { + ForegroundColor(SybilTheme.textMuted) + } + } + .fixedSize(horizontal: false, vertical: true) + .markdownMargin(top: .em(0.25), bottom: .em(0.9)) + } + .codeBlock { configuration in + ScrollView(.horizontal) { + configuration.label + .fixedSize(horizontal: false, vertical: true) + .relativeLineSpacing(.em(0.28)) + .markdownTextStyle { + FontFamilyVariant(.monospaced) + FontSize(.em(0.88)) + } + .padding(12) + } + .background( + RoundedRectangle(cornerRadius: 10) + .fill(SybilTheme.surfaceStrong.opacity(0.82)) + ) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(SybilTheme.border.opacity(0.72), lineWidth: 1) + ) + .markdownMargin(top: .em(0.2), bottom: .em(1)) + } + .list { configuration in + configuration.label + .fixedSize(horizontal: false, vertical: true) + .markdownMargin(top: .em(0.08), bottom: .em(0.78)) + } + .listItem { configuration in + configuration.label + .markdownMargin(top: .em(0.28)) + } + .table { configuration in + configuration.label + .fixedSize(horizontal: false, vertical: true) + .markdownTableBorderStyle(.init(color: SybilTheme.border.opacity(0.85))) + .markdownTableBackgroundStyle( + .alternatingRows( + SybilTheme.surface.opacity(0.72), + SybilTheme.surfaceStrong.opacity(0.62) + ) + ) + .markdownMargin(top: .em(0.2), bottom: .em(1)) + } + .tableCell { configuration in + configuration.label + .markdownTextStyle { + if configuration.row == 0 { + FontWeight(.semibold) + } + BackgroundColor(nil) + } + .fixedSize(horizontal: false, vertical: true) + .relativeLineSpacing(.em(0.30)) + .padding(.vertical, 7) + .padding(.horizontal, 11) + } + .thematicBreak { + Divider() + .overlay(SybilTheme.border.opacity(0.82)) + .markdownMargin(top: .em(1.2), bottom: .em(1.2)) + } + }() +} diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilSearchResultsView.swift b/ios/Packages/Sybil/Sources/Sybil/SybilSearchResultsView.swift index b2a2a87..321d9ec 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilSearchResultsView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilSearchResultsView.swift @@ -97,9 +97,7 @@ struct SybilSearchResultsView: View { Markdown(answer) .tint(SybilTheme.primary) .foregroundStyle(SybilTheme.text) - .markdownTextStyle { - FontSize(15) - } + .markdownTheme(.sybilReadable) } if let answerError = search?.answerError, !answerError.isEmpty { @@ -180,6 +178,7 @@ private struct SearchResultCard: View { Text(result.title.orEmpty.orFallback(result.url)) .font(.sybil(.headline)) .foregroundStyle(SybilTheme.primary.opacity(0.96)) + .lineSpacing(2) .multilineTextAlignment(.leading) } .buttonStyle(.plain) @@ -187,6 +186,7 @@ private struct SearchResultCard: View { Text(result.title.orEmpty.orFallback(result.url)) .font(.sybil(.headline)) .foregroundStyle(SybilTheme.primary.opacity(0.96)) + .lineSpacing(2) } if let date = result.publishedDate, !date.isEmpty { @@ -202,6 +202,7 @@ private struct SearchResultCard: View { Text(result.url) .font(.sybil(.footnote)) .foregroundStyle(SybilTheme.text.opacity(0.92)) + .lineSpacing(2) .lineLimit(3) .textSelection(.enabled) @@ -210,6 +211,7 @@ private struct SearchResultCard: View { Text("• \(highlight)") .font(.sybil(.caption)) .foregroundStyle(SybilTheme.textMuted) + .lineSpacing(2) .fixedSize(horizontal: false, vertical: true) } }