Files
Kisani/Shared/Theme/AppTheme.swift
Robin Kutesa 04e7a77c1a Apply Emil Kowalski design across all screens
Near-monochrome palette (ink tokens), black primary buttons, spring
animations, 0.5pt hairline dividers, shadow cards, squircle corners.
Replaces all blue/coloured accents. Adds ButtonStyles component.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 00:43:31 +03:00

106 lines
3.8 KiB
Swift

import SwiftUI
enum AppTheme {
// MARK: Surfaces
static let background = Color.white
static let surfaceRaised = Color.white
static let surfaceSunken = Color(hex: "#F5F5F5")
// MARK: Ink
static let ink = Color(hex: "#0F0F0F")
static let inkSecondary = Color(hex: "#6B6B6B")
static let inkTertiary = Color(hex: "#ABABAB")
static let inkQuaternary = Color(hex: "#D4D4D4")
// MARK: Semantic
static let positive = Color(hex: "#16A34A")
static let destructive = Color(hex: "#DC2626")
static let interactive = Color(hex: "#2563EB")
// MARK: Chrome
static let separator = Color.black.opacity(0.07)
static let cardShadow = Color.black.opacity(0.05)
static let cardBorderColor = Color.black.opacity(0.07)
// MARK: Metrics
static let radius: CGFloat = 14
static let radiusLarge: CGFloat = 18
static let hPad: CGFloat = 20
static let cardPad: CGFloat = 16
static let sectionGap: CGFloat = 28
// MARK: Typography
static func display(_ size: CGFloat = 32) -> Font {
.system(size: size, weight: .semibold, design: .default)
}
static func title(_ size: CGFloat = 20) -> Font {
.system(size: size, weight: .semibold)
}
static func headline(_ size: CGFloat = 15) -> Font {
.system(size: size, weight: .medium)
}
static func body(_ size: CGFloat = 15) -> Font {
.system(size: size, weight: .regular)
}
static func caption(_ size: CGFloat = 13) -> Font {
.system(size: size, weight: .regular)
}
static func micro(_ size: CGFloat = 11) -> Font {
.system(size: size, weight: .medium)
}
// MARK: Back-compat aliases used by existing call sites
static let blue = interactive
static let green = positive
static let red = destructive
static let textPrimary = ink
static let textSecondary = inkSecondary
static let textTertiary = inkTertiary
static let cardBorder = cardBorderColor
static let connectedBorder = positive
static let lanBorder = interactive
static let cardCornerRadius = radius
static let cardBackground = surfaceRaised
static let titleFont = Font.system(size: 28, weight: .bold)
static let headlineFont = Font.system(size: 17, weight: .semibold)
static let bodyFont = Font.system(size: 15, weight: .regular)
static let captionFont = Font.system(size: 13, weight: .regular)
static let smallFont = Font.system(size: 11, weight: .medium)
static let sectionSpacing: CGFloat = 28
}
// MARK: Card modifier
struct CardStyle: ViewModifier {
var padded: Bool = true
func body(content: Content) -> some View {
content
.if(padded) { $0.padding(AppTheme.cardPad) }
.background(AppTheme.surfaceRaised)
.clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous))
.shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2)
}
}
extension View {
func card(padded: Bool = true) -> some View {
modifier(CardStyle(padded: padded))
}
@ViewBuilder func `if`<T: View>(_ condition: Bool, transform: (Self) -> T) -> some View {
if condition { transform(self) } else { self }
}
}
extension Color {
init(hex: String) {
var h = hex.trimmingCharacters(in: .init(charactersIn: "#"))
if h.count == 3 { h = h.map { "\($0)\($0)" }.joined() }
let n = UInt64(h, radix: 16) ?? 0
self.init(
red: Double((n >> 16) & 0xFF) / 255,
green: Double((n >> 8) & 0xFF) / 255,
blue: Double( n & 0xFF) / 255
)
}
}