161 lines
6.0 KiB
Swift
161 lines
6.0 KiB
Swift
import SwiftUI
|
|
import CoreText
|
|
|
|
enum WatchTheme {
|
|
static func font(_ size: CGFloat, weight: Font.Weight = .regular) -> Font {
|
|
.custom("Inter-Regular", size: size).weight(weight)
|
|
}
|
|
|
|
static func registerFonts() {
|
|
guard let url = Bundle.main.url(forResource: "Inter", withExtension: "ttf") else { return }
|
|
CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil)
|
|
}
|
|
static let background = Color(hex: 0x0C141F)
|
|
static let night = Color(hex: 0x0A1019)
|
|
static let card = Color(hex: 0x151C27)
|
|
static let accent = Color("AccentColor")
|
|
static let mint = Color(hex: 0x79D3FF)
|
|
static let secondary = Color(hex: 0xBEC8CF)
|
|
static let text = Color(hex: 0xDCE3F3)
|
|
static let purple = Color(hex: 0xA78BFA)
|
|
static let pink = Color(hex: 0xE94697)
|
|
static let stop = Color(hex: 0xFFB4AB)
|
|
}
|
|
|
|
private extension Color {
|
|
init(hex: UInt32) {
|
|
self.init(red: Double((hex >> 16) & 255) / 255, green: Double((hex >> 8) & 255) / 255, blue: Double(hex & 255) / 255)
|
|
}
|
|
}
|
|
|
|
struct WatchCard<Content: View>: View {
|
|
@ViewBuilder var content: Content
|
|
var body: some View {
|
|
content.padding(12).frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(WatchTheme.card, in: RoundedRectangle(cornerRadius: 12))
|
|
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.white.opacity(0.08)))
|
|
}
|
|
}
|
|
|
|
struct WatchPillStyle: ButtonStyle {
|
|
var primary = false
|
|
var destructive = false
|
|
var height: CGFloat = 36
|
|
var fontSize: CGFloat = 14
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
configuration.label
|
|
.font(destructive ? .system(size: fontSize) : WatchTheme.font(fontSize, weight: .semibold))
|
|
.foregroundStyle(destructive ? WatchTheme.stop : primary ? Color(red: 0, green: 0.208, blue: 0.282) : WatchTheme.text)
|
|
.frame(maxWidth: .infinity).frame(height: height)
|
|
.background(primary ? WatchTheme.accent : destructive ? .clear : Color(red: 0.18, green: 0.208, blue: 0.255), in: Capsule())
|
|
.overlay(Capsule().stroke(destructive ? WatchTheme.stop.opacity(0.3) : .clear))
|
|
.opacity(configuration.isPressed ? 0.65 : 1)
|
|
.contentShape(Capsule())
|
|
}
|
|
}
|
|
|
|
struct WatchMenuRow: View {
|
|
let title: String
|
|
var color: Color = WatchTheme.accent
|
|
var body: some View {
|
|
HStack {
|
|
Text(title).font(WatchTheme.font(16, weight: .medium)).fixedSize(horizontal: false, vertical: true)
|
|
Spacer(minLength: 4)
|
|
Image("WatchChevron").resizable().scaledToFit().frame(width: 5, height: 8).accessibilityHidden(true)
|
|
}
|
|
.foregroundStyle(WatchTheme.text)
|
|
.padding(.horizontal, 15).frame(minHeight: 50)
|
|
.background(LinearGradient(colors: [color.opacity(0.1), WatchTheme.card], startPoint: .leading, endPoint: .trailing))
|
|
.overlay(alignment: .leading) { Rectangle().fill(color).frame(width: 4) }
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
}
|
|
|
|
struct WatchGlow: View {
|
|
var color: Color = WatchTheme.accent
|
|
var body: some View {
|
|
Circle()
|
|
.fill(RadialGradient(colors: [color.opacity(0.24), color.opacity(0)], center: .center, startRadius: 0, endRadius: 100))
|
|
.frame(width: 210, height: 210)
|
|
.allowsHitTesting(false).accessibilityHidden(true)
|
|
}
|
|
}
|
|
|
|
struct WatchEyebrow: View {
|
|
let title: String
|
|
var body: some View {
|
|
Text(title.uppercased()).font(WatchTheme.font(12, weight: .semibold))
|
|
.tracking(0.6).foregroundStyle(WatchTheme.secondary).multilineTextAlignment(.center)
|
|
.lineLimit(1).minimumScaleFactor(0.7).fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
}
|
|
|
|
func watchTime(_ seconds: TimeInterval) -> String {
|
|
let value = max(0, Int(seconds.rounded(.up)))
|
|
return String(format: "%02d:%02d", value / 60, value % 60)
|
|
}
|
|
|
|
extension View {
|
|
func haptikaScreen() -> some View {
|
|
scrollContentBackground(.hidden)
|
|
.background(WatchTheme.background.ignoresSafeArea())
|
|
.foregroundStyle(WatchTheme.text)
|
|
.tint(WatchTheme.accent)
|
|
}
|
|
}
|
|
|
|
// Explicit Debug-only presentation fixtures. Never saved or sent to HealthKit.
|
|
enum WatchDesignMode {
|
|
static var enabled: Bool {
|
|
#if DEBUG && targetEnvironment(simulator)
|
|
ProcessInfo.processInfo.arguments.contains("--figma-preview")
|
|
#else
|
|
false
|
|
#endif
|
|
}
|
|
static var initialScreen: WatchFigmaScreen? {
|
|
guard enabled,
|
|
let index = ProcessInfo.processInfo.arguments.firstIndex(of: "--figma-screen"),
|
|
ProcessInfo.processInfo.arguments.indices.contains(index + 1) else { return nil }
|
|
return WatchFigmaScreen(rawValue: ProcessInfo.processInfo.arguments[index + 1])
|
|
}
|
|
static var frozen: Bool { initialScreen != nil }
|
|
}
|
|
|
|
struct WatchFigmaCanvas<Content: View>: View {
|
|
var background: Color = WatchTheme.background
|
|
@Environment(\.dismiss) private var dismiss
|
|
@ViewBuilder var content: Content
|
|
var body: some View {
|
|
GeometryReader { geometry in
|
|
content.frame(width: geometry.size.width, height: geometry.size.height)
|
|
}
|
|
.scrollContentBackground(.hidden)
|
|
.foregroundStyle(WatchTheme.text)
|
|
.tint(WatchTheme.accent)
|
|
.background(background.ignoresSafeArea())
|
|
.toolbar(.hidden, for: .navigationBar)
|
|
.persistentSystemOverlays(.hidden)
|
|
.accessibilityAction(.escape) { dismiss() }
|
|
}
|
|
}
|
|
|
|
struct WatchBalanceAxis: View {
|
|
var compact = false
|
|
var position: Double?
|
|
var body: some View {
|
|
HStack(spacing: 4) {
|
|
if compact { Circle().fill(Color(red: 0.957, green: 0.247, blue: 0.369)).frame(width: 6, height: 6) }
|
|
GeometryReader { geometry in
|
|
Capsule().fill(LinearGradient(colors: [compact ? WatchTheme.accent.opacity(0) : Color(red: 0.957, green: 0.247, blue: 0.369), WatchTheme.accent], startPoint: .leading, endPoint: .trailing))
|
|
if !compact, let position {
|
|
Capsule().fill(WatchTheme.text).frame(width: 4, height: 14)
|
|
.position(x: geometry.size.width * min(1, max(0, position)), y: geometry.size.height / 2)
|
|
.shadow(color: .white.opacity(0.5), radius: 4)
|
|
}
|
|
}.frame(width: compact ? 32 : 120, height: compact ? 4 : 6)
|
|
if compact { Circle().fill(WatchTheme.accent).frame(width: 6, height: 6) }
|
|
}.accessibilityHidden(true)
|
|
}
|
|
}
|