Files
mobile-ios/Rosetta/DesignSystem/Components/ButtonStyles.swift

187 lines
5.7 KiB
Swift

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(.ultraThinMaterial)
}
}
}
// 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(fillColor.opacity(configuration.isPressed ? 0.7 : 1.0))
}
.glassEffect(.regular, in: Capsule())
} else {
configuration.label
.background { glassBackground(isPressed: configuration.isPressed) }
.clipShape(Capsule())
}
}
.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(fillColor.opacity(isPressed ? 0.7 : 1.0))
}
}
// 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 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 { geo in
let w = geo.size.width * highlightWidth
let travel = geo.size.width + w
let currentX = -w + phase * travel
LinearGradient(
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: w, height: geo.size.height)
.offset(x: currentX)
.frame(width: geo.size.width, height: geo.size.height, alignment: .leading)
}
.clipShape(Capsule())
.allowsHitTesting(false)
}
.onAppear {
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 {
shine()
}
}
// MARK: - Stagger Animation Helper
struct StaggeredAppearance: ViewModifier {
let key: String
let index: Int
let baseDelay: Double
let stagger: Double
@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(
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))
}
}