import SwiftUI // MARK: - Карточка-герой /// Одна пара крупно: заголовок, сквозной график, цена и изменение за 24ч. struct HeroCard: View { let quote: Quote let updatedAt: Date /// Горизонтальный отступ контейнера — график компенсирует его /// и уходит под края виджета, как в макете. let horizontalPadding: CGFloat let iconSize: CGFloat let titleSize: CGFloat let priceSize: CGFloat let chartHeight: CGFloat var showFullDate = true var showAbsolute = true private var thumb: SymbolThumb { quote.thumb } var body: some View { VStack(alignment: .leading, spacing: 0) { PairHeader(thumb: thumb, icon: quote.icon, iconSize: iconSize, titleSize: titleSize) UpdatedAt(date: updatedAt, fontSize: titleSize * 0.62, withDate: showFullDate) .padding(.top, titleSize * 0.45) Spacer(minLength: 6) chart .frame(height: chartHeight) .padding(.horizontal, -horizontalPadding) Spacer(minLength: 6) PriceLabel(thumb: thumb, size: priceSize) StatsLine(thumb: thumb, fontSize: titleSize * 0.78, showAbsolute: showAbsolute) .padding(.top, titleSize * 0.4) } } @ViewBuilder private var chart: some View { if quote.series.count > 1 { Sparkline( values: quote.series, positive: thumb.isPositive, lineWidth: 2, pulseSize: iconSize * 0.52 ) } else { // Нет истории — держим ритм макета ровной линией. Rectangle() .fill( LinearGradient( colors: [ RapiraPalette.lineEdge(thumb.isPositive), RapiraPalette.line(thumb.isPositive).opacity(0.5), RapiraPalette.lineEdge(thumb.isPositive), ], startPoint: .leading, endPoint: .trailing ) ) .frame(height: 1.5) .frame(maxHeight: .infinity, alignment: .center) } } } // MARK: - Строка списка /// Компактная строка: иконка, пара, мини-график, цена и процент. struct CryptoRow: View { let quote: Quote let compact: Bool private var thumb: SymbolThumb { quote.thumb } private var iconSize: CGFloat { compact ? 24 : 26 } var body: some View { HStack(spacing: 8) { CoinIconView(currency: thumb.quoteCurrency, data: quote.icon, size: iconSize) VStack(alignment: .leading, spacing: 0) { HStack(spacing: 1) { Text(thumb.quoteCurrency) .foregroundStyle(.white) Text("/\(thumb.baseCurrency)") .foregroundStyle(.white.opacity(0.45)) } .font(.system(size: compact ? 12 : 13, weight: .bold)) .lineLimit(1) Text(thumb.quoteCurrencyName) .font(.system(size: compact ? 9 : 10, weight: .medium)) .foregroundStyle(.white.opacity(0.35)) .lineLimit(1) } Spacer(minLength: 4) if quote.series.count > 1 { Sparkline( values: quote.series, positive: thumb.isPositive, lineWidth: 1.5, pulseSize: 7, areaOpacity: 0.16, trailingRoom: 0.6 ) .frame(width: compact ? 46 : 56, height: 22) } VStack(alignment: .trailing, spacing: 2) { Text(thumb.priceFormatted) .font(.system(size: compact ? 12 : 13, weight: .bold)) .monospacedDigit() .foregroundStyle(.white) .lineLimit(1) .minimumScaleFactor(0.6) Text(thumb.changePercent) .font(.system(size: compact ? 10 : 10.5, weight: .semibold)) .monospacedDigit() .foregroundStyle(RapiraPalette.stat(thumb.isPositive)) } .frame(minWidth: compact ? 62 : 70, alignment: .trailing) } } }