Files
honeyDueKMP/iosApp/iosApp/Design/DesignSystem.swift
Trey t 9c574c4343 Harden iOS app with audit fixes, UI consistency, and sheet race condition fixes
Applies verified fixes from deep audit (concurrency, performance, security,
accessibility), standardizes CRUD form buttons to Add/Save pattern, removes
.drawingGroup() that broke search bar TextFields, and converts vulnerable
.sheet(isPresented:) + if-let patterns to safe presentation to prevent
blank white modals.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:59:56 -06:00

180 lines
5.9 KiB
Swift

import SwiftUI
// MARK: - Design System
// Modern, sleek design system for Casera with Light and Dark mode support
// MARK: - Colors
extension Color {
// MARK: - Dynamic Theme Resolution
/// Shared App Group defaults for reading the active theme.
/// Thread-safe: UserDefaults is safe to read from any thread/actor.
private static let _themeDefaults: UserDefaults = {
UserDefaults(suiteName: "group.com.tt.casera.CaseraDev") ?? .standard
}()
private static func themed(_ name: String) -> Color {
// Read theme directly from shared UserDefaults instead of going through
// @MainActor-isolated ThemeManager.shared. This is safe to call from any
// actor context (including widget timeline providers and background threads).
let theme = _themeDefaults.string(forKey: "selectedTheme") ?? ThemeID.bright.rawValue
return Color("\(theme)/\(name)", bundle: nil)
}
// MARK: - Semantic Colors (Use These in UI)
// These dynamically resolve based on the active theme stored in App Group UserDefaults.
// Safe to call from any actor context (main app, widget extensions, background threads).
static var appPrimary: Color { themed("Primary") }
static var appSecondary: Color { themed("Secondary") }
static var appAccent: Color { themed("Accent") }
static var appBackgroundPrimary: Color { themed("BackgroundPrimary") }
static var appBackgroundSecondary: Color { themed("BackgroundSecondary") }
static var appError: Color { themed("Error") }
static var appTextPrimary: Color { themed("TextPrimary") }
static var appTextSecondary: Color { themed("TextSecondary") }
static var appTextOnPrimary: Color { themed("TextOnPrimary") }
// MARK: - Hex Support
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 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
return nil
}
self.init(
.sRGB,
red: Double(r) / 255,
green: Double(g) / 255,
blue: Double(b) / 255,
opacity: Double(a) / 255
)
}
}
// MARK: - Spacing
struct AppSpacing {
static let xxs: CGFloat = 4
static let xs: CGFloat = 8
static let sm: CGFloat = 12
static let md: CGFloat = 16
static let lg: CGFloat = 24
static let xl: CGFloat = 32
static let xxl: CGFloat = 48
static let xxxl: CGFloat = 64
}
struct AppRadius {
static let xs: CGFloat = 4
static let sm: CGFloat = 8
static let md: CGFloat = 12
static let lg: CGFloat = 16
static let xl: CGFloat = 20
static let xxl: CGFloat = 24
static let full: CGFloat = 9999
}
struct AppShadow {
static let sm = Shadow(color: .black.opacity(0.05), radius: 2, y: 1)
static let md = Shadow(color: .black.opacity(0.1), radius: 4, y: 2)
static let lg = Shadow(color: .black.opacity(0.1), radius: 8, y: 4)
static let xl = Shadow(color: .black.opacity(0.15), radius: 16, y: 8)
struct Shadow {
let color: Color
let radius: CGFloat
let x: CGFloat
let y: CGFloat
init(color: Color, radius: CGFloat, x: CGFloat = 0, y: CGFloat) {
self.color = color
self.radius = radius
self.x = x
self.y = y
}
}
}
// MARK: - View Modifiers
struct CardStyle: ViewModifier {
var shadow: AppShadow.Shadow = AppShadow.md
var padding: CGFloat = AppSpacing.md
func body(content: Content) -> some View {
content
.background(Color.appBackgroundSecondary)
.cornerRadius(AppRadius.lg)
.shadow(color: shadow.color, radius: shadow.radius, x: shadow.x, y: shadow.y)
}
}
struct PrimaryButtonStyle: ButtonStyle {
var isLoading: Bool = false
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.headline)
.foregroundColor(.appTextOnPrimary)
.frame(maxWidth: .infinity)
.frame(height: 56)
.background(
configuration.isPressed ? Color.appPrimary.opacity(0.8) : Color.appPrimary
)
.cornerRadius(AppRadius.md)
.scaleEffect(configuration.isPressed ? 0.98 : 1.0)
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
}
}
struct SecondaryButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.headline)
.foregroundColor(.appPrimary)
.frame(maxWidth: .infinity)
.frame(height: 56)
.background(Color.appPrimary.opacity(0.1))
.cornerRadius(AppRadius.md)
.scaleEffect(configuration.isPressed ? 0.98 : 1.0)
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
}
}
struct TextFieldStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding(AppSpacing.md)
.background(Color.appBackgroundSecondary)
.cornerRadius(AppRadius.md)
.overlay(
RoundedRectangle(cornerRadius: AppRadius.md)
.stroke(Color.appTextSecondary.opacity(0.3), lineWidth: 1)
)
}
}
// MARK: - View Extensions
extension View {
func cardStyle(shadow: AppShadow.Shadow = AppShadow.md) -> some View {
modifier(CardStyle(shadow: shadow))
}
func textFieldStyle() -> some View {
modifier(TextFieldStyle())
}
}