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

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