This repository has been archived on 2026-10-04. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
hope-voice-ios/Sources/App/Nearlight.swift
T

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)
}
}