14 KiB
14 KiB
App Colors Architecture Rules
Color-system rules for the planned PureYAML native editor. Build the colors package with HSV-based color management, dynamic light/dark support, and semantic naming. The colors package is independent from the theme package, which combines colors + fonts.
Core rules
Rule 1: Package structure
Structure the design system as separate packages:
- AppColors: standalone color system (zero dependencies)
- AppFont: standalone font system (zero dependencies)
- AppTheme: combines AppColors + AppFont
Rule 2: HSV internal representation
Use HSV (Hue, Saturation, Value) internally:
- Store all colors as HSV components.
- Calculate dark/light variants by HSV manipulation.
- Convert to/from RGB only when handing a SwiftUI
Colorto the UI.
Rule 3: Semantic color names (Apple HIG)
Use these exact semantic names:
primary: primary brand/action color (like systemBlue)success: success states (like systemGreen)secondary: secondary brand color (like systemPurple)destructive: destructive/error actions (like systemRed). Apple uses "destructive", NOT "danger".label: primary text color (likeUIColor.label)secondaryLabel: secondary text coloronPrimary: text on primary-colored backgroundsbackground: primary background (like systemBackground)secondaryBackground: secondary/elevated background
Rule 4: Dynamic color support
Create dynamic colors that adapt to appearance:
- Use
UIColordynamic colors on iOS. - Use
NSColordynamic colors on macOS. - Fall back to the light variant on other platforms.
Rule 5: Initialization modes
Support two initialization modes:
- Explicit colors: caller provides light and dark variants.
- System fallback: use system colors as defaults.
Package hierarchy
AppColors (standalone - zero dependencies)
↓
AppFont (standalone - zero dependencies)
↓
AppTheme (combines AppColors + AppFont)
AppColors package structure
Sources/AppColors/
├── HSVColor.swift # HSV color representation
├── Color+Dynamic.swift # Dynamic color extension
├── Color+HSV.swift # HSV conversion utilities
├── AppColors.swift # Main semantic colors
└── SystemColorDefaults.swift # System color fallbacks
Package.swift configuration
let appColorsTarget = Target.target(
name: "AppColors",
dependencies: [] // Foundation layer; zero dependencies
)
let appFontTarget = Target.target(
name: "AppFont",
dependencies: [],
resources: [.process("Fonts")]
)
let appThemeTarget = Target.target(
name: "AppTheme",
dependencies: [
"AppColors",
"AppFont",
]
)
Implementation patterns
1. HSV color representation
import SwiftUI
/// Internal HSV color representation for manipulation.
public struct HSVColor: Equatable, Sendable {
public let hue: Double // 0.0 - 1.0
public let saturation: Double // 0.0 - 1.0
public let value: Double // 0.0 - 1.0
public let alpha: Double // 0.0 - 1.0
public init(hue: Double, saturation: Double, value: Double, alpha: Double = 1.0) {
self.hue = hue
self.saturation = saturation
self.value = value
self.alpha = alpha
}
public func toColor() -> Color {
Color(hue: hue, saturation: saturation, brightness: value, opacity: alpha)
}
/// Dark variant: reduce brightness, slightly increase saturation.
public func darkVariant() -> HSVColor {
HSVColor(
hue: hue,
saturation: min(1.0, saturation * 1.1),
value: max(0.15, value * 0.6),
alpha: alpha
)
}
/// Light variant: increase brightness, slightly reduce saturation.
public func lightVariant() -> HSVColor {
HSVColor(
hue: hue,
saturation: max(0.0, saturation * 0.85),
value: min(1.0, value * 1.3),
alpha: alpha
)
}
public func adjustingValue(by factor: Double) -> HSVColor {
HSVColor(hue: hue, saturation: saturation, value: min(1.0, max(0.0, value * factor)), alpha: alpha)
}
public func adjustingSaturation(by factor: Double) -> HSVColor {
HSVColor(hue: hue, saturation: min(1.0, max(0.0, saturation * factor)), value: value, alpha: alpha)
}
}
2. Color + HSV extension
import SwiftUI
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
extension Color {
public init(hsv: HSVColor) {
self = hsv.toColor()
}
public func toHSV() -> HSVColor {
#if canImport(UIKit)
let uiColor = UIColor(self)
var hue: CGFloat = 0, saturation: CGFloat = 0, brightness: CGFloat = 0, alpha: CGFloat = 0
uiColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
return HSVColor(hue: Double(hue), saturation: Double(saturation), value: Double(brightness), alpha: Double(alpha))
#elseif canImport(AppKit)
let nsColor = NSColor(self)
let converted = nsColor.usingColorSpace(.deviceRGB) ?? nsColor
var hue: CGFloat = 0, saturation: CGFloat = 0, brightness: CGFloat = 0, alpha: CGFloat = 0
converted.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
return HSVColor(hue: Double(hue), saturation: Double(saturation), value: Double(brightness), alpha: Double(alpha))
#else
return HSVColor(hue: 0, saturation: 0, value: 0.5, alpha: 1.0)
#endif
}
}
3. Dynamic color extension
import SwiftUI
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
extension Color {
/// Creates a dynamic color that adapts to appearance.
public init(light: Color, dark: Color) {
#if os(iOS)
self = Color(UIColor { traitCollection in
traitCollection.userInterfaceStyle == .dark ? UIColor(dark) : UIColor(light)
})
#elseif os(macOS)
self = Color(NSColor(name: nil) { appearance in
appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua ? NSColor(dark) : NSColor(light)
})
#else
self = light
#endif
}
/// Dynamic color from HSV, auto-calculating the dark variant.
public init(lightHSV: HSVColor) {
self.init(light: lightHSV.toColor(), dark: lightHSV.darkVariant().toColor())
}
/// Dynamic color from HSV, auto-calculating the light variant.
public init(darkHSV: HSVColor) {
self.init(light: darkHSV.lightVariant().toColor(), dark: darkHSV.toColor())
}
}
4. System color defaults
import SwiftUI
/// System color defaults for fallback (Apple HIG inspired).
public enum SystemColorDefaults {
public static let primary = HSVColor(hue: 0.58, saturation: 0.8, value: 0.9) // Blue
public static let success = HSVColor(hue: 0.33, saturation: 0.7, value: 0.8) // Green
public static let secondary = HSVColor(hue: 0.75, saturation: 0.6, value: 0.85) // Purple
public static let destructive = HSVColor(hue: 0.0, saturation: 0.8, value: 0.9) // Red
public static let label = HSVColor(hue: 0.0, saturation: 0.0, value: 0.1) // Near-black
public static let secondaryLabel = HSVColor(hue: 0.0, saturation: 0.0, value: 0.5) // Medium gray
public static let onPrimary = HSVColor(hue: 0.0, saturation: 0.0, value: 1.0) // White
public static let background = HSVColor(hue: 0.0, saturation: 0.0, value: 1.0) // White
public static let secondaryBackground = HSVColor(hue: 0.0, saturation: 0.0, value: 0.95) // Light gray
}
5. AppColors main structure
import SwiftUI
/// Semantic color palette (Apple HIG naming).
public struct AppColors: Sendable {
// Semantic
public let primary: Color
public let success: Color
public let secondary: Color
public let destructive: Color
// Text
public let label: Color
public let secondaryLabel: Color
public let onPrimary: Color
// Background
public let background: Color
public let secondaryBackground: Color
/// Initialize with explicit HSV light-mode colors. Dark variants are calculated automatically.
public init(
primaryHSV: HSVColor,
successHSV: HSVColor,
secondaryHSV: HSVColor,
destructiveHSV: HSVColor,
labelHSV: HSVColor,
secondaryLabelHSV: HSVColor,
onPrimaryHSV: HSVColor,
backgroundHSV: HSVColor,
secondaryBackgroundHSV: HSVColor
) {
primary = Color(lightHSV: primaryHSV)
success = Color(lightHSV: successHSV)
secondary = Color(lightHSV: secondaryHSV)
destructive = Color(lightHSV: destructiveHSV)
label = Color(lightHSV: labelHSV)
secondaryLabel = Color(lightHSV: secondaryLabelHSV)
onPrimary = Color(lightHSV: onPrimaryHSV)
background = Color(lightHSV: backgroundHSV)
secondaryBackground = Color(lightHSV: secondaryBackgroundHSV)
}
/// Initialize with explicit light/dark Color pairs.
public init(
primary: (light: Color, dark: Color),
success: (light: Color, dark: Color),
secondary: (light: Color, dark: Color),
destructive: (light: Color, dark: Color),
label: (light: Color, dark: Color),
secondaryLabel: (light: Color, dark: Color),
onPrimary: (light: Color, dark: Color),
background: (light: Color, dark: Color),
secondaryBackground: (light: Color, dark: Color)
) {
self.primary = Color(light: primary.light, dark: primary.dark)
self.success = Color(light: success.light, dark: success.dark)
self.secondary = Color(light: secondary.light, dark: secondary.dark)
self.destructive = Color(light: destructive.light, dark: destructive.dark)
self.label = Color(light: label.light, dark: label.dark)
self.secondaryLabel = Color(light: secondaryLabel.light, dark: secondaryLabel.dark)
self.onPrimary = Color(light: onPrimary.light, dark: onPrimary.dark)
self.background = Color(light: background.light, dark: background.dark)
self.secondaryBackground = Color(light: secondaryBackground.light, dark: secondaryBackground.dark)
}
/// Default palette using system colors.
public static let system = AppColors(
primaryHSV: SystemColorDefaults.primary,
successHSV: SystemColorDefaults.success,
secondaryHSV: SystemColorDefaults.secondary,
destructiveHSV: SystemColorDefaults.destructive,
labelHSV: SystemColorDefaults.label,
secondaryLabelHSV: SystemColorDefaults.secondaryLabel,
onPrimaryHSV: SystemColorDefaults.onPrimary,
backgroundHSV: SystemColorDefaults.background,
secondaryBackgroundHSV: SystemColorDefaults.secondaryBackground
)
}
// MARK: - Environment
private struct AppColorsKey: EnvironmentKey {
static let defaultValue = AppColors.system
}
extension EnvironmentValues {
public var appColors: AppColors {
get { self[AppColorsKey.self] }
set { self[AppColorsKey.self] = newValue }
}
}
extension View {
public func appColors(_ colors: AppColors) -> some View {
environment(\.appColors, colors)
}
}
6. AppTheme package (combines colors + fonts)
import SwiftUI
import AppColors
import AppFont
/// Complete theme combining colors and typography.
public struct AppTheme: Sendable {
public let colors: AppColors
public init(colors: AppColors = .system) {
self.colors = colors
}
public static let system = AppTheme(colors: .system)
}
private struct AppThemeKey: EnvironmentKey {
static let defaultValue = AppTheme.system
}
extension EnvironmentValues {
public var appTheme: AppTheme {
get { self[AppThemeKey.self] }
set { self[AppThemeKey.self] = newValue }
}
}
extension View {
public func appTheme(_ theme: AppTheme) -> some View {
environment(\.appTheme, theme)
}
}
Usage patterns
Basic usage with system colors
import SwiftUI
import AppColors
struct MyView: View {
@Environment(\.appColors) var colors
var body: some View {
VStack {
Text("Primary Text")
.foregroundColor(colors.label)
Button("Action") {}
.foregroundColor(colors.onPrimary)
.background(colors.primary)
}
.background(colors.background)
}
}
Custom brand colors
let customColors = AppColors(
primaryHSV: HSVColor(hue: 0.6, saturation: 0.85, value: 0.95),
successHSV: HSVColor(hue: 0.33, saturation: 0.7, value: 0.8),
secondaryHSV: HSVColor(hue: 0.75, saturation: 0.6, value: 0.85),
destructiveHSV: HSVColor(hue: 0.0, saturation: 0.8, value: 0.9),
labelHSV: HSVColor(hue: 0, saturation: 0, value: 0.1),
secondaryLabelHSV: HSVColor(hue: 0, saturation: 0, value: 0.5),
onPrimaryHSV: HSVColor(hue: 0, saturation: 0, value: 1.0),
backgroundHSV: HSVColor(hue: 0, saturation: 0, value: 1.0),
secondaryBackgroundHSV: HSVColor(hue: 0, saturation: 0, value: 0.95)
)
@main
struct PureYAMLApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.appColors(customColors)
}
}
}
Common mistakes
- Making
AppColorsdepend onAppFont(it MUST be standalone). - Storing colors as RGB internally instead of HSV.
- Hardcoding dark-mode colors instead of calculating them from HSV.
- Static colors that do not adapt to dark mode.
- Non-semantic names like
blue,red,lightGray. - Material Design naming (
error,surface,onSurface). Use Apple HIG:destructive,background,label.
Checklist
AppColorsis a standalone package (zero dependencies)AppFontis a standalone package (zero dependencies)AppThemecombinesAppColors+AppFontHSVColordefined with hue, saturation, value, alphaHSVColorhasdarkVariant()andlightVariant()Color+Dynamicimplementsinit(light:dark:)Color+HSVconverts betweenColorandHSVColor- All 9 semantic colors present, Apple HIG naming
- Used
destructive(not "danger"/"error") - Used
label/secondaryLabel(not "textPrimary") - Used
background/secondaryBackground(not "bgPrimary") SystemColorDefaultsprovides fallback valuesAppColors.systemstatic property exists- Environment keys for
appColorsandappTheme - Platform-specific dynamic colors (
#if os(iOS)/os(macOS))