feat: v2
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user