215 lines
7.0 KiB
Swift
215 lines
7.0 KiB
Swift
import SwiftUI
|
|
#if canImport(UIKit)
|
|
import UIKit
|
|
#endif
|
|
|
|
// MARK: - Theme
|
|
|
|
extension Color {
|
|
private static func adaptive(light: String, dark: String) -> Color {
|
|
#if canImport(UIKit)
|
|
Color(UIColor { traitCollection in
|
|
UIColor(hex: traitCollection.userInterfaceStyle == .dark ? dark : light)
|
|
})
|
|
#else
|
|
Color(hex: light)
|
|
#endif
|
|
}
|
|
|
|
// Primary palette
|
|
static let closerPrimary = Color.adaptive(light: "B98AF4", dark: "CFA7FF")
|
|
static let closerSecondary = Color.adaptive(light: "E7A2D1", dark: "FFAFD9")
|
|
static let closerBackground = Color.adaptive(light: "FFFBFE", dark: "18111E")
|
|
static let closerSurface = Color.adaptive(light: "F5F0FF", dark: "211729")
|
|
static let closerOnPrimary = Color.white
|
|
static let closerText = Color.adaptive(light: "1C1B1F", dark: "F2E8F6")
|
|
static let closerTextSecondary = Color.adaptive(light: "49454F", dark: "D9C8E2")
|
|
static let closerDivider = Color.adaptive(light: "E6E0E9", dark: "5A4666")
|
|
|
|
// Semantic
|
|
static let closerSuccess = Color.adaptive(light: "4CAF50", dark: "8DD99B")
|
|
static let closerWarning = Color.adaptive(light: "FF9800", dark: "FFC46B")
|
|
static let closerDanger = Color.adaptive(light: "F44336", dark: "FFB3BA")
|
|
static let closerGold = Color.adaptive(light: "FFD700", dark: "FFE680")
|
|
|
|
// Category colors
|
|
static let categoryCommunication = Color.adaptive(light: "B98AF4", dark: "CFA7FF")
|
|
static let categoryIntimacy = Color.adaptive(light: "E7A2D1", dark: "FFAFD9")
|
|
static let categoryFun = Color.adaptive(light: "FFB74D", dark: "FFD38A")
|
|
static let categoryGoals = Color.adaptive(light: "81C784", dark: "A6DFA8")
|
|
static let categoryAdventure = Color.adaptive(light: "64B5F6", dark: "9AD1FF")
|
|
|
|
// Streak
|
|
static let streakActive = Color.adaptive(light: "FF6B6B", dark: "FF9A9A")
|
|
static let streakInactive = Color.adaptive(light: "E0E0E0", dark: "5A4666")
|
|
|
|
init(hex: String) {
|
|
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
|
var int: UInt64 = 0
|
|
Scanner(string: hex).scanHexInt64(&int)
|
|
let a, r, g, b: UInt64
|
|
switch hex.count {
|
|
case 6:
|
|
(a, r, g, b) = (255, (int >> 16) & 0xFF, (int >> 8) & 0xFF, int & 0xFF)
|
|
case 8:
|
|
(a, r, g, b) = ((int >> 24) & 0xFF, (int >> 16) & 0xFF, (int >> 8) & 0xFF, int & 0xFF)
|
|
default:
|
|
(a, r, g, b) = (255, 0, 0, 0)
|
|
}
|
|
self.init(
|
|
.sRGB,
|
|
red: Double(r) / 255,
|
|
green: Double(g) / 255,
|
|
blue: Double(b) / 255,
|
|
opacity: Double(a) / 255
|
|
)
|
|
}
|
|
}
|
|
|
|
#if canImport(UIKit)
|
|
private extension UIColor {
|
|
convenience init(hex: String) {
|
|
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
|
var int: UInt64 = 0
|
|
Scanner(string: hex).scanHexInt64(&int)
|
|
let a, r, g, b: UInt64
|
|
switch hex.count {
|
|
case 6:
|
|
(a, r, g, b) = (255, (int >> 16) & 0xFF, (int >> 8) & 0xFF, int & 0xFF)
|
|
case 8:
|
|
(a, r, g, b) = ((int >> 24) & 0xFF, (int >> 16) & 0xFF, (int >> 8) & 0xFF, int & 0xFF)
|
|
default:
|
|
(a, r, g, b) = (255, 0, 0, 0)
|
|
}
|
|
self.init(
|
|
red: Double(r) / 255,
|
|
green: Double(g) / 255,
|
|
blue: Double(b) / 255,
|
|
alpha: Double(a) / 255
|
|
)
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// MARK: - Typography
|
|
|
|
enum CloserFont {
|
|
static let largeTitle = Font.system(size: 34, weight: .bold, design: .default)
|
|
static let title1 = Font.system(size: 28, weight: .bold)
|
|
static let title2 = Font.system(size: 22, weight: .semibold)
|
|
static let title3 = Font.system(size: 20, weight: .semibold)
|
|
static let headline = Font.system(size: 17, weight: .semibold)
|
|
static let body = Font.system(size: 17, weight: .regular)
|
|
static let callout = Font.system(size: 16, weight: .regular)
|
|
static let subheadline = Font.system(size: 15, weight: .regular)
|
|
static let footnote = Font.system(size: 13, weight: .regular)
|
|
static let caption = Font.system(size: 12, weight: .regular)
|
|
static let caption2 = Font.system(size: 11, weight: .regular)
|
|
}
|
|
|
|
// MARK: - Spacing
|
|
|
|
enum CloserSpacing {
|
|
static let xs: CGFloat = 4
|
|
static let sm: CGFloat = 8
|
|
static let md: CGFloat = 12
|
|
static let lg: CGFloat = 16
|
|
static let xl: CGFloat = 24
|
|
static let xxl: CGFloat = 32
|
|
static let xxxl: CGFloat = 48
|
|
}
|
|
|
|
// MARK: - Corner Radius
|
|
|
|
enum CloserRadius {
|
|
static let small: CGFloat = 8
|
|
static let medium: CGFloat = 12
|
|
static let large: CGFloat = 16
|
|
static let xlarge: CGFloat = 24
|
|
static let full: CGFloat = 999
|
|
}
|
|
|
|
// MARK: - Shadow
|
|
|
|
extension View {
|
|
func closerShadow(level: ShadowLevel = .medium) -> some View {
|
|
switch level {
|
|
case .small:
|
|
return self.shadow(color: .black.opacity(0.1), radius: 4, x: 0, y: 2)
|
|
case .medium:
|
|
return self.shadow(color: .black.opacity(0.12), radius: 8, x: 0, y: 4)
|
|
case .large:
|
|
return self.shadow(color: .black.opacity(0.15), radius: 16, x: 0, y: 8)
|
|
}
|
|
}
|
|
}
|
|
|
|
enum ShadowLevel {
|
|
case small, medium, large
|
|
}
|
|
|
|
// MARK: - Button Styles
|
|
|
|
struct PrimaryButtonStyle: ButtonStyle {
|
|
let isDisabled: Bool
|
|
|
|
init(isDisabled: Bool = false) {
|
|
self.isDisabled = isDisabled
|
|
}
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
configuration.label
|
|
.font(CloserFont.headline)
|
|
.foregroundColor(.closerOnPrimary)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, CloserSpacing.lg)
|
|
.background(isDisabled ? Color.closerPrimary.opacity(0.4) : Color.closerPrimary)
|
|
.cornerRadius(CloserRadius.large)
|
|
.opacity(configuration.isPressed ? 0.9 : 1.0)
|
|
}
|
|
}
|
|
|
|
struct SecondaryButtonStyle: ButtonStyle {
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
configuration.label
|
|
.font(CloserFont.headline)
|
|
.foregroundColor(.closerPrimary)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, CloserSpacing.lg)
|
|
.background(Color.closerSurface)
|
|
.cornerRadius(CloserRadius.large)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: CloserRadius.large)
|
|
.stroke(Color.closerPrimary, lineWidth: 1.5)
|
|
)
|
|
.opacity(configuration.isPressed ? 0.8 : 1.0)
|
|
}
|
|
}
|
|
|
|
// MARK: - View Modifiers
|
|
|
|
struct CloserCardModifier: ViewModifier {
|
|
func body(content: Content) -> some View {
|
|
content
|
|
.background(Color.closerSurface)
|
|
.cornerRadius(CloserRadius.large)
|
|
.closerShadow(level: .small)
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func closerCard() -> some View {
|
|
modifier(CloserCardModifier())
|
|
}
|
|
|
|
func closerPadding() -> some View {
|
|
padding(.horizontal, CloserSpacing.xl)
|
|
}
|
|
|
|
func closerSectionTitle() -> some View {
|
|
font(CloserFont.title3)
|
|
.foregroundColor(.closerText)
|
|
.padding(.bottom, CloserSpacing.sm)
|
|
}
|
|
}
|