Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions Sources/UI/CompactComposerOverlayRows.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,18 @@ extension CompactComposerOverlayView {
@ViewBuilder
var quoteCardsRow: some View {
if !draftQuotes.isEmpty {
VStack(alignment: .leading, spacing: JinSpacing.xSmall + 2) {
ForEach(draftQuotes) { quote in
ComposerQuoteCardView(quote: quote) {
onRemoveQuote(quote)
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: JinSpacing.small) {
ForEach(draftQuotes) { quote in
ComposerQuoteCardView(quote: quote) {
onRemoveQuote(quote)
}
.equatable()
.transition(ComposerQuoteCardView.transition(reduceMotion: reduceMotion))
}
.equatable()
.transition(ComposerQuoteCardView.transition(reduceMotion: reduceMotion))
}
.padding(.horizontal, JinSpacing.xSmall)
.padding(.vertical, 2)
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions Sources/UI/ExpandedComposerOverlay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ struct ExpandedComposerOverlay<ControlsRow: View>: View {

if !draftQuotes.isEmpty {
ExpandedComposerAccessorySection(title: "Quotes", systemName: "quote.opening") {
ScrollView(.vertical, showsIndicators: true) {
VStack(alignment: .leading, spacing: JinSpacing.xSmall + 2) {
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: JinSpacing.small) {
ForEach(draftQuotes) { quote in
ComposerQuoteCardView(quote: quote) {
onRemoveQuote(quote)
Expand All @@ -95,8 +95,9 @@ struct ExpandedComposerOverlay<ControlsRow: View>: View {
.transition(ComposerQuoteCardView.transition(reduceMotion: reduceMotion))
}
}
.padding(.horizontal, JinSpacing.xSmall)
.padding(.vertical, 2)
}
.frame(maxHeight: 168)
}
}

Expand Down
152 changes: 85 additions & 67 deletions Sources/UI/QuoteCardViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,20 @@ private enum QuoteCardDensity {
private struct QuoteCardContainer<Accessory: View>: View {
let quote: QuoteContent
let density: QuoteCardDensity
let accessoryFocused: Bool
let accessory: Accessory

@State private var isHovering = false

init(
quote: QuoteContent,
density: QuoteCardDensity = .message,
accessoryFocused: Bool = false,
@ViewBuilder accessory: () -> Accessory
) {
self.quote = quote
self.density = density
self.accessoryFocused = accessoryFocused
self.accessory = accessory()
}

Expand Down Expand Up @@ -149,78 +154,69 @@ private struct QuoteCardContainer<Accessory: View>: View {
)
}

private var composerQuoteBody: some View {
VStack(alignment: .leading, spacing: 3) {
HStack(alignment: .center, spacing: JinSpacing.xSmall + 2) {
Image(systemName: "arrowshape.turn.up.left.fill")
.font(.system(size: 8.5, weight: .semibold))
.foregroundStyle(QuoteCardPalette.accent)

if let iconID = quote.sourceProviderIconID {
ProviderIconView(iconID: iconID, fallbackSystemName: "sparkles", size: 11)
.frame(width: 11, height: 11)
}

headerLabel

Spacer(minLength: JinSpacing.xSmall)
private var composerTooltip: String {
let header = composerHeader
let prefix: String
if let modelName = header.modelName {
prefix = "\(header.prefix) \(modelName)"
} else {
prefix = header.prefix
}
return "\(prefix)\n\n\(quote.quotedText)"
}

accessory
.layoutPriority(1)
}
private var composerQuoteBody: some View {
HStack(alignment: .top, spacing: 0) {
RoundedRectangle(cornerRadius: 1, style: .continuous)
.fill(QuoteCardPalette.accent)
.frame(width: 2)
.padding(.vertical, JinSpacing.small - 2)
.padding(.leading, JinSpacing.small - 2)

Text(quote.quotedText)
.font(.callout)
.foregroundStyle(.primary.opacity(0.86))
.lineLimit(2)
.font(.caption)
.foregroundStyle(.primary.opacity(0.85))
.lineLimit(QuoteCardLayout.composerLineLimit)
.truncationMode(.tail)
.frame(maxWidth: .infinity, alignment: .leading)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .topLeading)
.padding(.leading, JinSpacing.small)
.padding(.trailing, JinSpacing.small)
.padding(.vertical, JinSpacing.small)
}
.padding(.leading, JinSpacing.medium + 4)
.padding(.trailing, JinSpacing.small + 2)
.padding(.vertical, JinSpacing.small)
.frame(maxWidth: .infinity, alignment: .leading)
.fixedSize(horizontal: false, vertical: true)
.frame(
width: QuoteCardLayout.composerWidth,
height: QuoteCardLayout.composerHeight,
alignment: .topLeading
)
.background(
RoundedRectangle(cornerRadius: JinRadius.medium, style: .continuous)
.fill(JinSemanticColor.subtleSurface)
RoundedRectangle(cornerRadius: JinRadius.small, style: .continuous)
.fill(JinSemanticColor.subtleSurface.opacity(0.65))
)
.overlay(alignment: .leading) {
RoundedRectangle(cornerRadius: 1.25, style: .continuous)
.fill(QuoteCardPalette.accent)
.frame(width: 2.5)
.padding(.vertical, JinSpacing.small)
.padding(.leading, JinSpacing.small)
}
.overlay(
RoundedRectangle(cornerRadius: JinRadius.medium, style: .continuous)
.stroke(JinSemanticColor.separator.opacity(0.5), lineWidth: JinStrokeWidth.hairline)
RoundedRectangle(cornerRadius: JinRadius.small, style: .continuous)
.stroke(JinSemanticColor.separator.opacity(0.4), lineWidth: JinStrokeWidth.hairline)
)
}

@ViewBuilder
private var headerLabel: some View {
let header = composerHeader
if let modelName = header.modelName {
HStack(spacing: 4) {
Text(header.prefix)
.font(.caption2.weight(.medium))
.foregroundStyle(.secondary)
Text(modelName)
.font(.caption.weight(.semibold))
.foregroundStyle(.primary)
.lineLimit(1)
.truncationMode(.tail)
}
} else {
Text(header.prefix)
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
.lineLimit(1)
.overlay(alignment: .topTrailing) {
accessory
.opacity(isHovering || accessoryFocused ? 1 : 0)
.animation(.easeInOut(duration: 0.12), value: isHovering)
.animation(.easeInOut(duration: 0.12), value: accessoryFocused)
.padding(.top, 2)
.padding(.trailing, 2)
}
.contentShape(RoundedRectangle(cornerRadius: JinRadius.small, style: .continuous))
.onHover { isHovering = $0 }
.help(composerTooltip)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

enum QuoteCardLayout {
static let composerWidth: CGFloat = 160
static let composerHeight: CGFloat = 76
static let composerLineLimit: Int = 4
}

struct MessageQuoteCardView: View {
let quote: QuoteContent

Expand All @@ -235,9 +231,15 @@ struct ComposerQuoteCardView: View, Equatable {
let quote: DraftQuote
let onRemove: () -> Void

@State private var isDismissFocused = false

var body: some View {
QuoteCardContainer(quote: quote.content, density: .composer) {
QuoteDismissButton(action: onRemove)
QuoteCardContainer(
quote: quote.content,
density: .composer,
accessoryFocused: isDismissFocused
) {
QuoteDismissButton(action: onRemove, isFocused: $isDismissFocused)
}
}

Expand All @@ -258,26 +260,42 @@ struct ComposerQuoteCardView: View, Equatable {

private struct QuoteDismissButton: View {
let action: () -> Void
@Binding var isFocused: Bool

@State private var isHovering = false
@FocusState private var isButtonFocused: Bool

var body: some View {
Button(action: action) {
Image(systemName: "xmark")
.font(.system(size: 9, weight: .bold))
.font(.system(size: 8, weight: .bold))
.foregroundStyle(
isHovering
? AnyShapeStyle(Color.primary.opacity(0.78))
: AnyShapeStyle(HierarchicalShapeStyle.tertiary)
isHovering || isButtonFocused
? AnyShapeStyle(Color.primary.opacity(0.85))
: AnyShapeStyle(Color.primary.opacity(0.55))
)
.frame(width: 18, height: 18)
.frame(width: 16, height: 16)
.background(
Circle()
.fill(Color.primary.opacity(isHovering ? 0.08 : 0))
.fill(.regularMaterial)
.opacity(isHovering || isButtonFocused ? 1 : 0.85)
)
.overlay(
Circle()
.stroke(
isButtonFocused
? Color.accentColor.opacity(0.7)
: JinSemanticColor.separator.opacity(0.4),
lineWidth: isButtonFocused ? JinStrokeWidth.regular : JinStrokeWidth.hairline
)
)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.focused($isButtonFocused)
.onChange(of: isButtonFocused) { _, newValue in
isFocused = newValue
}
.onHover { isHovering = $0 }
.accessibilityLabel("Remove quote")
.help("Remove quote")
Expand Down
Loading