feat: Refactor UI components and improve animations for onboarding and authentication flows
This commit is contained in:
@@ -22,7 +22,7 @@ enum RosettaColors {
|
||||
|
||||
// MARK: Auth Backgrounds
|
||||
|
||||
static let authBackground = Color(hex: 0x1B1B1B)
|
||||
static let authBackground = Color.black
|
||||
static let authSurface = Color(hex: 0x2A2A2A)
|
||||
|
||||
// MARK: Shared Neutral
|
||||
@@ -60,7 +60,7 @@ enum RosettaColors {
|
||||
// MARK: Dark Theme
|
||||
|
||||
enum Dark {
|
||||
static let background = Color(hex: 0x1E1E1E)
|
||||
static let background = Color.black
|
||||
static let backgroundSecondary = Color(hex: 0x2A2A2A)
|
||||
static let surface = Color(hex: 0x242424)
|
||||
static let text = Color.white
|
||||
|
||||
@@ -11,14 +11,8 @@ struct AuthNavigationBar: View {
|
||||
|
||||
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")
|
||||
GlassBackButton(action: onBack)
|
||||
.accessibilityLabel("Back")
|
||||
|
||||
if let title {
|
||||
Text(title)
|
||||
|
||||
@@ -1,16 +1,56 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Glass Back Button (circular, like screenshot reference)
|
||||
|
||||
struct GlassBackButton: View {
|
||||
let action: () -> Void
|
||||
var size: CGFloat = 44
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: size, height: size)
|
||||
}
|
||||
.background {
|
||||
glassCircle
|
||||
}
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var glassCircle: some View {
|
||||
if #available(iOS 26, *) {
|
||||
Circle()
|
||||
.fill(Color.white.opacity(0.08))
|
||||
.glassEffect(.regular, in: .circle)
|
||||
} else {
|
||||
Circle()
|
||||
.fill(Color.white.opacity(0.08))
|
||||
.overlay {
|
||||
Circle()
|
||||
.stroke(Color.white.opacity(0.12), lineWidth: 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Primary Button (Liquid Glass)
|
||||
|
||||
struct RosettaPrimaryButtonStyle: ButtonStyle {
|
||||
var isEnabled: Bool = true
|
||||
|
||||
private var fillColor: Color {
|
||||
isEnabled ? RosettaColors.primaryBlue : Color(white: 0.22)
|
||||
}
|
||||
|
||||
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))
|
||||
Capsule().fill(fillColor.opacity(configuration.isPressed ? 0.7 : 1.0))
|
||||
}
|
||||
.glassEffect(.regular, in: Capsule())
|
||||
} else {
|
||||
@@ -19,21 +59,20 @@ struct RosettaPrimaryButtonStyle: ButtonStyle {
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
.opacity(isEnabled ? 1.0 : 0.5)
|
||||
.scaleEffect(configuration.isPressed ? 0.97 : 1.0)
|
||||
.scaleEffect(configuration.isPressed && isEnabled ? 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))
|
||||
.fill(fillColor.opacity(isPressed ? 0.7 : 1.0))
|
||||
.overlay {
|
||||
Capsule()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color.white.opacity(0.18),
|
||||
Color.white.opacity(isEnabled ? 0.18 : 0.05),
|
||||
Color.clear,
|
||||
Color.black.opacity(0.08),
|
||||
],
|
||||
@@ -45,71 +84,121 @@ struct RosettaPrimaryButtonStyle: ButtonStyle {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Shimmer Overlay
|
||||
// MARK: - Shine Overlay (Telegram-style color sweep)
|
||||
//
|
||||
// Instead of a white glare, a lighter/cyan tint of the button's own colour
|
||||
// glides across — exactly the look Telegram uses on its primary CTA.
|
||||
|
||||
struct ShimmerModifier: ViewModifier {
|
||||
@State private var phase: CGFloat = -1.0
|
||||
struct ShineModifier: ViewModifier {
|
||||
let speed: Double
|
||||
let highlightWidth: CGFloat
|
||||
let tintColor: Color
|
||||
let delay: Double
|
||||
|
||||
@State private var phase: CGFloat = 0
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.overlay {
|
||||
GeometryReader { geometry in
|
||||
let width = geometry.size.width * 0.6
|
||||
GeometryReader { geo in
|
||||
let w = geo.size.width * highlightWidth
|
||||
let travel = geo.size.width + w
|
||||
let currentX = -w + phase * travel
|
||||
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color.white.opacity(0),
|
||||
Color.white.opacity(0.25),
|
||||
Color.white.opacity(0),
|
||||
stops: [
|
||||
.init(color: .clear, location: 0),
|
||||
.init(color: tintColor, location: 0.45),
|
||||
.init(color: tintColor, location: 0.55),
|
||||
.init(color: .clear, location: 1),
|
||||
],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
.frame(width: width)
|
||||
.offset(x: phase * (geometry.size.width + width))
|
||||
.clipShape(Capsule())
|
||||
.frame(width: w, height: geo.size.height)
|
||||
.offset(x: currentX)
|
||||
.frame(width: geo.size.width, height: geo.size.height, alignment: .leading)
|
||||
}
|
||||
.clipShape(Capsule())
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
.onAppear {
|
||||
withAnimation(
|
||||
.linear(duration: 2.0)
|
||||
.repeatForever(autoreverses: false)
|
||||
) {
|
||||
phase = 1.0
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
||||
withAnimation(
|
||||
.easeInOut(duration: speed)
|
||||
.repeatForever(autoreverses: false)
|
||||
) {
|
||||
phase = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func shine(
|
||||
speed: Double = 2.0,
|
||||
highlightWidth: CGFloat = 0.55,
|
||||
tintColor: Color = Color(red: 0.35, green: 0.82, blue: 0.98).opacity(0.50),
|
||||
delay: Double = 0.3
|
||||
) -> some View {
|
||||
modifier(ShineModifier(
|
||||
speed: speed,
|
||||
highlightWidth: highlightWidth,
|
||||
tintColor: tintColor,
|
||||
delay: delay
|
||||
))
|
||||
}
|
||||
|
||||
/// Legacy alias
|
||||
func shimmer() -> some View {
|
||||
modifier(ShimmerModifier())
|
||||
shine()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Stagger Animation Helper
|
||||
|
||||
struct StaggeredAppearance: ViewModifier {
|
||||
let key: String
|
||||
let index: Int
|
||||
let baseDelay: Double
|
||||
let stagger: Double
|
||||
@State private var isVisible = false
|
||||
@State private var isVisible: Bool
|
||||
|
||||
/// Tracks which screen keys have already animated so we don't replay on re-navigation.
|
||||
private static var animatedKeys: Set<String> = []
|
||||
|
||||
init(key: String, index: Int, baseDelay: Double, stagger: Double) {
|
||||
self.key = key
|
||||
self.index = index
|
||||
self.baseDelay = baseDelay
|
||||
self.stagger = stagger
|
||||
// Start visible immediately if this screen already animated — no flash frame.
|
||||
_isVisible = State(initialValue: Self.animatedKeys.contains(key))
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.opacity(isVisible ? 1.0 : 0.0)
|
||||
.offset(y: isVisible ? 0 : 12)
|
||||
.onAppear {
|
||||
guard !isVisible else { return }
|
||||
let delay = baseDelay + Double(index) * stagger
|
||||
withAnimation(.easeOut(duration: 0.4).delay(delay)) {
|
||||
isVisible = true
|
||||
}
|
||||
Self.animatedKeys.insert(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func staggeredAppearance(index: Int, baseDelay: Double = 0.2, stagger: Double = 0.05) -> some View {
|
||||
modifier(StaggeredAppearance(index: index, baseDelay: baseDelay, stagger: stagger))
|
||||
func staggeredAppearance(
|
||||
key: String = "default",
|
||||
index: Int,
|
||||
baseDelay: Double = 0.2,
|
||||
stagger: Double = 0.05
|
||||
) -> some View {
|
||||
modifier(StaggeredAppearance(key: key, index: index, baseDelay: baseDelay, stagger: stagger))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user