|
| 1 | +import SwiftUI |
| 2 | + |
| 3 | +// MARK: - Color from hex |
| 4 | +extension Color { |
| 5 | + init(hex: String) { |
| 6 | + let s = hex.trimmingCharacters(in: CharacterSet(charactersIn: "#")) |
| 7 | + var v: UInt64 = 0 |
| 8 | + Scanner(string: s).scanHexInt64(&v) |
| 9 | + let r, g, b, a: Double |
| 10 | + switch s.count { |
| 11 | + case 8: r = Double((v >> 24) & 0xFF)/255; g = Double((v >> 16) & 0xFF)/255; b = Double((v >> 8) & 0xFF)/255; a = Double(v & 0xFF)/255 |
| 12 | + default: r = Double((v >> 16) & 0xFF)/255; g = Double((v >> 8) & 0xFF)/255; b = Double(v & 0xFF)/255; a = 1 |
| 13 | + } |
| 14 | + self.init(.sRGB, red: r, green: g, blue: b, opacity: a) |
| 15 | + } |
| 16 | +} |
| 17 | + |
| 18 | +// MARK: - Brand theme |
| 19 | +enum Theme { |
| 20 | + static let bg = Color(hex: "#0A0A0D") |
| 21 | + static let bgElevated = Color(hex: "#101015") |
| 22 | + static let surface = Color(hex: "#16161D") |
| 23 | + static let surface2 = Color(hex: "#1E1E27") |
| 24 | + static let stroke = Color.white.opacity(0.07) |
| 25 | + static let primary = Color(hex: "#14B8A6") // OpenProgression teal |
| 26 | + static let primaryDim = Color(hex: "#0E7C70") |
| 27 | + static let text = Color(hex: "#F5F5F7") |
| 28 | + static let textDim = Color(hex: "#9A9AA6") |
| 29 | + static let textFaint = Color(hex: "#6A6A76") |
| 30 | + |
| 31 | + // 7-level gradient (Beginner -> Rx). Falls back if data omits a color. |
| 32 | + static let levelColors: [Color] = ["#4ADE80","#22C55E","#EAB308","#F97316","#EF4444","#DC2626","#991B1B"].map { Color(hex: $0) } |
| 33 | + static func levelColor(_ number: Int) -> Color { |
| 34 | + let i = max(1, min(7, number)) - 1 |
| 35 | + return levelColors[i] |
| 36 | + } |
| 37 | + |
| 38 | + static var levelGradient: LinearGradient { |
| 39 | + LinearGradient(colors: levelColors, startPoint: .leading, endPoint: .trailing) |
| 40 | + } |
| 41 | +} |
| 42 | + |
| 43 | +// MARK: - Typography (SF Pro, premium weights). Rounded for display/brand. |
| 44 | +extension Font { |
| 45 | + static func display(_ size: CGFloat, _ weight: Weight = .heavy) -> Font { |
| 46 | + .system(size: size, weight: weight, design: .rounded) |
| 47 | + } |
| 48 | + static func brand(_ size: CGFloat) -> Font { .system(size: size, weight: .black, design: .rounded) } |
| 49 | +} |
| 50 | + |
| 51 | +// MARK: - Reusable view modifiers |
| 52 | +struct CardStyle: ViewModifier { |
| 53 | + var padding: CGFloat = 18 |
| 54 | + func body(content: Content) -> some View { |
| 55 | + content |
| 56 | + .padding(padding) |
| 57 | + .background(Theme.surface, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) |
| 58 | + .overlay(RoundedRectangle(cornerRadius: 20, style: .continuous).strokeBorder(Theme.stroke, lineWidth: 1)) |
| 59 | + } |
| 60 | +} |
| 61 | +extension View { |
| 62 | + func card(padding: CGFloat = 18) -> some View { modifier(CardStyle(padding: padding)) } |
| 63 | +} |
0 commit comments