120 lines
5.4 KiB
Swift
120 lines
5.4 KiB
Swift
import SwiftUI
|
|
import UIKit
|
|
|
|
/// Nearlight design tokens (design spec: local://ui-design-spec.md).
|
|
///
|
|
/// Views reference semantic roles; the primitive hex values live only here.
|
|
/// All custom surfaces are opaque; native system materials are untouched.
|
|
enum Nearlight {
|
|
// MARK: Palette (light / dark variants)
|
|
|
|
/// Screen background.
|
|
static let ground = Color(light: UIColor(red: 0.957, green: 0.949, blue: 0.965, alpha: 1),
|
|
dark: UIColor(red: 0.110, green: 0.098, blue: 0.125, alpha: 1))
|
|
/// Controls, grouped rows, dock.
|
|
static let surface = Color(light: .white,
|
|
dark: UIColor(red: 0.157, green: 0.137, blue: 0.180, alpha: 1))
|
|
/// Primary text.
|
|
static let ink = Color(light: UIColor(red: 0.161, green: 0.137, blue: 0.184, alpha: 1),
|
|
dark: UIColor(red: 0.965, green: 0.937, blue: 0.969, alpha: 1))
|
|
/// Supporting text, inactive icons.
|
|
static let quiet = Color(light: UIColor(red: 0.396, green: 0.365, blue: 0.424, alpha: 1),
|
|
dark: UIColor(red: 0.749, green: 0.698, blue: 0.776, alpha: 1))
|
|
/// Action tint, selection, Lightwell.
|
|
static let rosewood = Color(light: UIColor(red: 0.529, green: 0.278, blue: 0.380, alpha: 1),
|
|
dark: UIColor(red: 0.890, green: 0.635, blue: 0.737, alpha: 1))
|
|
/// Errors and destructive actions.
|
|
static let alert = Color(light: UIColor(red: 0.675, green: 0.208, blue: 0.282, alpha: 1),
|
|
dark: UIColor(red: 1.0, green: 0.604, blue: 0.675, alpha: 1))
|
|
|
|
// MARK: Derived roles
|
|
|
|
/// Decorative separator: Ink at 14% over its parent.
|
|
static var separator: Color { ink.opacity(0.14) }
|
|
/// Selected wash: Rosewood at 10% over Surface.
|
|
static var selectedWash: Color { rosewood.opacity(0.10) }
|
|
/// Error wash: Alert at 8% over Surface.
|
|
static var errorWash: Color { alert.opacity(0.08) }
|
|
/// Primary-action label: Surface in light, Ground in dark.
|
|
static var onRosewood: Color {
|
|
UITraitCollection.current.userInterfaceStyle == .dark ? ground : surface
|
|
}
|
|
|
|
// MARK: Spacing scale
|
|
|
|
static let s4: CGFloat = 4
|
|
static let s8: CGFloat = 8
|
|
static let s12: CGFloat = 12
|
|
static let s16: CGFloat = 16
|
|
static let s20: CGFloat = 20
|
|
static let s24: CGFloat = 24
|
|
static let s32: CGFloat = 32
|
|
static let s48: CGFloat = 48
|
|
|
|
// MARK: Radius scale
|
|
|
|
static let radiusSegment: CGFloat = 8
|
|
static let radiusControl: CGFloat = 12
|
|
static let radiusDock: CGFloat = 16
|
|
|
|
// MARK: Motion tokens
|
|
|
|
/// quick = 0.12s easeOut.
|
|
static let quick: Animation = .easeOut(duration: 0.12)
|
|
/// standard = 0.22s timingCurve(0.22, 1, 0.36, 1).
|
|
static let standard: Animation = .timingCurve(0.22, 1, 0.36, 1, duration: 0.22)
|
|
/// settle = 0.32s timingCurve(0.22, 1, 0.36, 1).
|
|
static let settle: Animation = .timingCurve(0.22, 1, 0.36, 1, duration: 0.32)
|
|
/// cycle = 2.4s easeInOut (Lightwell pulses).
|
|
static let cycle: Animation = .easeInOut(duration: 2.4)
|
|
}
|
|
|
|
extension Color {
|
|
/// Builds an adaptive color from explicit light/dark UIColors without
|
|
/// requiring an asset catalog.
|
|
init(light: UIColor, dark: UIColor) {
|
|
self.init(uiColor: UIColor { traits in
|
|
traits.userInterfaceStyle == .dark ? dark : light
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Shared typography roles from the spec. SF Pro default design except the
|
|
/// rounded welcome heading and wordmark.
|
|
enum NearlightFont {
|
|
/// Welcome heading: 32 medium, rounded, largeTitle metrics.
|
|
static func welcome(_ sizeCategory: DynamicTypeSize) -> Font {
|
|
scaled(.largeTitle, base: 32, weight: .medium, design: .rounded, sizeCategory: sizeCategory)
|
|
}
|
|
/// "Hope" wordmark: 20 semibold, rounded, title3 metrics.
|
|
static func wordmark(_ sizeCategory: DynamicTypeSize) -> Font {
|
|
scaled(.title3, base: 20, weight: .semibold, design: .rounded, sizeCategory: sizeCategory)
|
|
}
|
|
/// Section/detail heading: 22 semibold, title2 metrics.
|
|
static func heading(_ sizeCategory: DynamicTypeSize) -> Font {
|
|
scaled(.title2, base: 22, weight: .semibold, sizeCategory: sizeCategory)
|
|
}
|
|
/// Control/row label: 17 semibold, body metrics.
|
|
static func control(_ sizeCategory: DynamicTypeSize) -> Font {
|
|
scaled(.body, base: 17, weight: .semibold, sizeCategory: sizeCategory)
|
|
}
|
|
/// Status/support: 15 regular, subheadline metrics.
|
|
static func support(_ sizeCategory: DynamicTypeSize) -> Font {
|
|
scaled(.subheadline, base: 15, weight: .regular, sizeCategory: sizeCategory)
|
|
}
|
|
/// Speaker/timestamp: 13 medium, footnote metrics.
|
|
static func metadata(_ sizeCategory: DynamicTypeSize) -> Font {
|
|
scaled(.footnote, base: 13, weight: .medium, sizeCategory: sizeCategory)
|
|
}
|
|
|
|
/// Scales a base size with the given text style via UIFontMetrics and
|
|
/// returns a SwiftUI font with the requested weight/design.
|
|
private static func scaled(_ textStyle: UIFont.TextStyle, base: CGFloat, weight: Font.Weight,
|
|
design: Font.Design = .default, sizeCategory: DynamicTypeSize) -> Font {
|
|
// UIFontMetrics scales the base size against the CURRENT Dynamic Type
|
|
// setting at render time (no explicit traits: inherits environment).
|
|
let scaledSize = UIFontMetrics(forTextStyle: textStyle).scaledValue(for: base)
|
|
return .system(size: scaledSize, weight: weight, design: design)
|
|
}
|
|
}
|