feat: watch design from figma
This commit is contained in:
@@ -1,29 +1,160 @@
|
||||
import SwiftUI
|
||||
import CoreText
|
||||
|
||||
enum WatchTheme {
|
||||
static let background = Color(red: 0.015, green: 0.04, blue: 0.10)
|
||||
static let card = Color(red: 0.035, green: 0.12, blue: 0.22)
|
||||
static let accent = Color(red: 0.37, green: 0.79, blue: 0.96)
|
||||
static let mint = Color(red: 0.38, green: 0.88, blue: 0.78)
|
||||
static let secondary = Color(red: 0.66, green: 0.73, blue: 0.84)
|
||||
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(10)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(WatchTheme.card.opacity(0.82), in: RoundedRectangle(cornerRadius: 18))
|
||||
.overlay(RoundedRectangle(cornerRadius: 18).stroke(WatchTheme.accent.opacity(0.22)))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user