Archived
122 lines
6.1 KiB
Swift
122 lines
6.1 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)
|
|
|
|
/// Opal screen background: luminous pink-white #FFFDFD / deep plum #201820.
|
|
static let ground = Color(light: UIColor(red: 255 / 255, green: 253 / 255, blue: 253 / 255, alpha: 1),
|
|
dark: UIColor(red: 32 / 255, green: 24 / 255, blue: 32 / 255, alpha: 1))
|
|
/// Controls, grouped rows, dock: #FFF7FA / #302330.
|
|
static let surface = Color(light: UIColor(red: 255 / 255, green: 247 / 255, blue: 250 / 255, alpha: 1),
|
|
dark: UIColor(red: 48 / 255, green: 35 / 255, blue: 48 / 255, alpha: 1))
|
|
/// The square persona's pale pink stage: #FFF2F7 / #3B2836.
|
|
static let personaSurface = Color(light: UIColor(red: 255 / 255, green: 242 / 255, blue: 247 / 255, alpha: 1),
|
|
dark: UIColor(red: 59 / 255, green: 40 / 255, blue: 54 / 255, alpha: 1))
|
|
/// Primary text: readable dark plum #302B40 / #FFF2F7.
|
|
static let ink = Color(light: UIColor(red: 48 / 255, green: 43 / 255, blue: 64 / 255, alpha: 1),
|
|
dark: UIColor(red: 255 / 255, green: 242 / 255, blue: 247 / 255, alpha: 1))
|
|
/// Supporting text, inactive icons: plum #706074 / #CBB6C7.
|
|
static let quiet = Color(light: UIColor(red: 112 / 255, green: 96 / 255, blue: 116 / 255, alpha: 1),
|
|
dark: UIColor(red: 203 / 255, green: 182 / 255, blue: 199 / 255, alpha: 1))
|
|
/// Opal action rose #A64270 / #F0A8C8; retains the existing semantic API.
|
|
static let rosewood = Color(light: UIColor(red: 166 / 255, green: 66 / 255, blue: 112 / 255, alpha: 1),
|
|
dark: UIColor(red: 240 / 255, green: 168 / 255, blue: 200 / 255, 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: white on strong rose, deep plum on the dark-mode pink.
|
|
static let onRosewood = Color(light: .white,
|
|
dark: UIColor(red: 32 / 255, green: 24 / 255, blue: 32 / 255, alpha: 1))
|
|
|
|
// 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)
|
|
}
|
|
}
|