Closer/iphone/Closer/Theme/CloserTheme.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)
}
}