Add onboarding, auth flow, design system and project structure
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
111
Rosetta/DesignSystem/Colors.swift
Normal file
111
Rosetta/DesignSystem/Colors.swift
Normal file
@@ -0,0 +1,111 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Rosetta Color Tokens
|
||||
|
||||
enum RosettaColors {
|
||||
|
||||
// MARK: Brand
|
||||
|
||||
static let primaryBlue = Color(hex: 0x248AE6)
|
||||
static let primaryBlueDark = Color(hex: 0x238BE6)
|
||||
static let primaryBluePressed = Color(hex: 0x2B7CD3)
|
||||
static let primaryBlueDisabled = Color(hex: 0x9BCDFF)
|
||||
|
||||
// MARK: Semantic
|
||||
|
||||
static let error = Color(hex: 0xFF3B30)
|
||||
static let success = Color(hex: 0x34C759)
|
||||
static let warning = Color(hex: 0xFF9500)
|
||||
static let accent = Color(hex: 0xE91E63)
|
||||
static let online = Color(hex: 0x34C759)
|
||||
|
||||
// MARK: Auth Backgrounds
|
||||
|
||||
static let authBackground = Color(hex: 0x1B1B1B)
|
||||
static let authSurface = Color(hex: 0x2A2A2A)
|
||||
|
||||
// MARK: Shared Neutral
|
||||
|
||||
static let secondaryText = Color(hex: 0x8E8E93)
|
||||
static let tertiaryText = Color(hex: 0x666666)
|
||||
static let numberGray = Color(hex: 0x888888)
|
||||
static let hintText = Color(hex: 0x555555)
|
||||
static let subtleBorder = Color.white.opacity(0.15)
|
||||
static let cardFill = Color.white.opacity(0.06)
|
||||
|
||||
// MARK: Light Theme
|
||||
|
||||
enum Light {
|
||||
static let background = Color.white
|
||||
static let backgroundSecondary = Color(hex: 0xF2F3F5)
|
||||
static let surface = Color(hex: 0xF5F5F5)
|
||||
static let text = Color.black
|
||||
static let textSecondary = Color(hex: 0x666666)
|
||||
static let textTertiary = Color(hex: 0x999999)
|
||||
static let border = Color(hex: 0xE0E0E0)
|
||||
static let divider = Color(hex: 0xEEEEEE)
|
||||
static let messageBubble = Color(hex: 0xF5F5F5)
|
||||
static let messageBubbleOwn = Color(hex: 0xDCF8C6)
|
||||
static let inputBackground = Color(hex: 0xF2F3F5)
|
||||
}
|
||||
|
||||
// MARK: Dark Theme
|
||||
|
||||
enum Dark {
|
||||
static let background = Color(hex: 0x1E1E1E)
|
||||
static let backgroundSecondary = Color(hex: 0x2A2A2A)
|
||||
static let surface = Color(hex: 0x242424)
|
||||
static let text = Color.white
|
||||
static let textSecondary = Color(hex: 0x8E8E93)
|
||||
static let textTertiary = Color(hex: 0x666666)
|
||||
static let border = Color(hex: 0x2E2E2E)
|
||||
static let divider = Color(hex: 0x333333)
|
||||
static let messageBubble = Color(hex: 0x2A2A2A)
|
||||
static let messageBubbleOwn = Color(hex: 0x263341)
|
||||
static let inputBackground = Color(hex: 0x2A2A2A)
|
||||
}
|
||||
|
||||
// MARK: Seed Word Colors (12 unique, matching Android)
|
||||
|
||||
static let seedWordColors: [Color] = [
|
||||
Color(hex: 0x5E9FFF),
|
||||
Color(hex: 0xFF7EB3),
|
||||
Color(hex: 0x7B68EE),
|
||||
Color(hex: 0x50C878),
|
||||
Color(hex: 0xFF6B6B),
|
||||
Color(hex: 0x4ECDC4),
|
||||
Color(hex: 0xFFB347),
|
||||
Color(hex: 0xBA55D3),
|
||||
Color(hex: 0x87CEEB),
|
||||
Color(hex: 0xDDA0DD),
|
||||
Color(hex: 0x98D8C8),
|
||||
Color(hex: 0xF7DC6F),
|
||||
]
|
||||
|
||||
// MARK: Avatar Palette
|
||||
|
||||
static let avatarColors: [(background: Color, text: Color)] = [
|
||||
(Color(hex: 0xFF6B6B), .white),
|
||||
(Color(hex: 0x4ECDC4), .white),
|
||||
(Color(hex: 0x45B7D1), .white),
|
||||
(Color(hex: 0xF7B731), .white),
|
||||
(Color(hex: 0x5F27CD), .white),
|
||||
(Color(hex: 0x00D2D3), .white),
|
||||
(Color(hex: 0xFF9FF3), .white),
|
||||
(Color(hex: 0x54A0FF), .white),
|
||||
]
|
||||
}
|
||||
|
||||
// MARK: - Color Hex Initializer
|
||||
|
||||
extension Color {
|
||||
init(hex: UInt, alpha: Double = 1.0) {
|
||||
self.init(
|
||||
.sRGB,
|
||||
red: Double((hex >> 16) & 0xFF) / 255.0,
|
||||
green: Double((hex >> 8) & 0xFF) / 255.0,
|
||||
blue: Double(hex & 0xFF) / 255.0,
|
||||
opacity: alpha
|
||||
)
|
||||
}
|
||||
}
|
||||
39
Rosetta/DesignSystem/Components/AuthNavigationBar.swift
Normal file
39
Rosetta/DesignSystem/Components/AuthNavigationBar.swift
Normal file
@@ -0,0 +1,39 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AuthNavigationBar: View {
|
||||
let title: String?
|
||||
let onBack: () -> Void
|
||||
|
||||
init(title: String? = nil, onBack: @escaping () -> Void) {
|
||||
self.title = title
|
||||
self.onBack = onBack
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Button(action: onBack) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 44, height: 44)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.accessibilityLabel("Back")
|
||||
|
||||
if let title {
|
||||
Text(title)
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AuthNavigationBar(title: "Set Password", onBack: {})
|
||||
.background(RosettaColors.authBackground)
|
||||
}
|
||||
115
Rosetta/DesignSystem/Components/ButtonStyles.swift
Normal file
115
Rosetta/DesignSystem/Components/ButtonStyles.swift
Normal file
@@ -0,0 +1,115 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Primary Button (Liquid Glass)
|
||||
|
||||
struct RosettaPrimaryButtonStyle: ButtonStyle {
|
||||
var isEnabled: Bool = true
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
Group {
|
||||
if #available(iOS 26, *) {
|
||||
configuration.label
|
||||
.background {
|
||||
Capsule().fill(RosettaColors.primaryBlue.opacity(configuration.isPressed ? 0.7 : 1.0))
|
||||
}
|
||||
.glassEffect(.regular, in: Capsule())
|
||||
} else {
|
||||
configuration.label
|
||||
.background { glassBackground(isPressed: configuration.isPressed) }
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
.opacity(isEnabled ? 1.0 : 0.5)
|
||||
.scaleEffect(configuration.isPressed ? 0.97 : 1.0)
|
||||
.animation(.easeOut(duration: 0.15), value: configuration.isPressed)
|
||||
.allowsHitTesting(isEnabled)
|
||||
}
|
||||
|
||||
private func glassBackground(isPressed: Bool) -> some View {
|
||||
Capsule()
|
||||
.fill(RosettaColors.primaryBlue.opacity(isPressed ? 0.7 : 1.0))
|
||||
.overlay {
|
||||
Capsule()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color.white.opacity(0.18),
|
||||
Color.clear,
|
||||
Color.black.opacity(0.08),
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Shimmer Overlay
|
||||
|
||||
struct ShimmerModifier: ViewModifier {
|
||||
@State private var phase: CGFloat = -1.0
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.overlay {
|
||||
GeometryReader { geometry in
|
||||
let width = geometry.size.width * 0.6
|
||||
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color.white.opacity(0),
|
||||
Color.white.opacity(0.25),
|
||||
Color.white.opacity(0),
|
||||
],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
.frame(width: width)
|
||||
.offset(x: phase * (geometry.size.width + width))
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
withAnimation(
|
||||
.linear(duration: 2.0)
|
||||
.repeatForever(autoreverses: false)
|
||||
) {
|
||||
phase = 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func shimmer() -> some View {
|
||||
modifier(ShimmerModifier())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Stagger Animation Helper
|
||||
|
||||
struct StaggeredAppearance: ViewModifier {
|
||||
let index: Int
|
||||
let baseDelay: Double
|
||||
let stagger: Double
|
||||
@State private var isVisible = false
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.opacity(isVisible ? 1.0 : 0.0)
|
||||
.offset(y: isVisible ? 0 : 12)
|
||||
.onAppear {
|
||||
let delay = baseDelay + Double(index) * stagger
|
||||
withAnimation(.easeOut(duration: 0.4).delay(delay)) {
|
||||
isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func staggeredAppearance(index: Int, baseDelay: Double = 0.2, stagger: Double = 0.05) -> some View {
|
||||
modifier(StaggeredAppearance(index: index, baseDelay: baseDelay, stagger: stagger))
|
||||
}
|
||||
}
|
||||
34
Rosetta/DesignSystem/Components/GlassCard.swift
Normal file
34
Rosetta/DesignSystem/Components/GlassCard.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
import SwiftUI
|
||||
|
||||
struct GlassCard<Content: View>: View {
|
||||
let cornerRadius: CGFloat
|
||||
let fillOpacity: Double
|
||||
let content: () -> Content
|
||||
|
||||
init(
|
||||
cornerRadius: CGFloat = 12,
|
||||
fillOpacity: Double = 0.06,
|
||||
@ViewBuilder content: @escaping () -> Content
|
||||
) {
|
||||
self.cornerRadius = cornerRadius
|
||||
self.fillOpacity = fillOpacity
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 26, *) {
|
||||
content()
|
||||
.glassEffect(.regular, in: RoundedRectangle(cornerRadius: cornerRadius))
|
||||
} else {
|
||||
content()
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.fill(Color.white.opacity(fillOpacity))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.stroke(Color.white.opacity(0.08), lineWidth: 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Rosetta/DesignSystem/Components/LottieView.swift
Normal file
50
Rosetta/DesignSystem/Components/LottieView.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
import SwiftUI
|
||||
import Lottie
|
||||
|
||||
struct LottieView: UIViewRepresentable {
|
||||
let animationName: String
|
||||
var loopMode: LottieLoopMode = .playOnce
|
||||
var animationSpeed: CGFloat = 1.5
|
||||
var isPlaying: Bool = true
|
||||
|
||||
func makeUIView(context: Context) -> LottieAnimationView {
|
||||
let animationView = LottieAnimationView(name: animationName)
|
||||
animationView.contentMode = .scaleAspectFit
|
||||
animationView.loopMode = loopMode
|
||||
animationView.animationSpeed = animationSpeed
|
||||
animationView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
animationView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
||||
if isPlaying {
|
||||
animationView.play()
|
||||
}
|
||||
return animationView
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: LottieAnimationView, context: Context) {
|
||||
uiView.loopMode = loopMode
|
||||
uiView.animationSpeed = animationSpeed
|
||||
|
||||
if isPlaying && !uiView.isAnimationPlaying {
|
||||
uiView.play()
|
||||
} else if !isPlaying {
|
||||
uiView.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Crossfading Lottie Container
|
||||
|
||||
struct CrossfadingLottieView: View {
|
||||
let animationName: String
|
||||
let animationID: Int
|
||||
|
||||
var body: some View {
|
||||
LottieView(
|
||||
animationName: animationName,
|
||||
loopMode: .playOnce,
|
||||
animationSpeed: 1.5
|
||||
)
|
||||
.id(animationID)
|
||||
.transition(.opacity.animation(.easeInOut(duration: 0.4)))
|
||||
}
|
||||
}
|
||||
28
Rosetta/DesignSystem/Typography.swift
Normal file
28
Rosetta/DesignSystem/Typography.swift
Normal file
@@ -0,0 +1,28 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Rosetta Typography
|
||||
|
||||
enum RosettaFont {
|
||||
|
||||
static let headlineLarge = Font.system(size: 20, weight: .bold)
|
||||
static let titleLarge = Font.system(size: 17, weight: .medium)
|
||||
static let bodyLarge = Font.system(size: 16, weight: .regular)
|
||||
static let bodyMedium = Font.system(size: 16, weight: .regular)
|
||||
static let bodySmall = Font.system(size: 13, weight: .regular)
|
||||
static let labelLarge = Font.system(size: 14, weight: .medium)
|
||||
static let labelMedium = Font.system(size: 14, weight: .regular)
|
||||
static let labelSmall = Font.system(size: 13, weight: .regular)
|
||||
|
||||
// MARK: Onboarding Specific
|
||||
|
||||
static let onboardingTitle = Font.system(size: 30, weight: .bold)
|
||||
static let onboardingBody = Font.system(size: 17, weight: .regular)
|
||||
static let onboardingBodyBold = Font.system(size: 17, weight: .semibold)
|
||||
|
||||
// MARK: Auth Specific
|
||||
|
||||
static let authTitle = Font.system(size: 24, weight: .bold)
|
||||
static let authSubtitle = Font.system(size: 15, weight: .regular)
|
||||
static let seedWord = Font.system(size: 17, weight: .medium, design: .monospaced)
|
||||
static let seedWordNumber = Font.system(size: 14, weight: .regular)
|
||||
}
|
||||
Reference in New Issue
Block a user