feat: Update text color to improve readability across multiple authentication views

This commit is contained in:
2026-02-28 13:21:24 +05:00
parent 7272f24595
commit d1fcc04125
9 changed files with 379 additions and 107 deletions

View File

@@ -0,0 +1,81 @@
import SwiftUI
// MARK: - Glass Modifier (5-layer glass that works on black)
//
// Layer stack:
// 1. .ultraThinMaterial system blur
// 2. black.opacity(0.22) dark tint (depth on dark mode)
// 3. whiteclear gradient highlight / light refraction, blendMode(.screen)
// 4. double stroke outer weak + inner stronger = glass edge
// 5. shadow depth
struct GlassModifier: ViewModifier {
let cornerRadius: CGFloat
func body(content: Content) -> some View {
let shape = RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
if #available(iOS 26, *) {
content
.background {
shape.fill(.clear)
.glassEffect(.regular, in: .rect(cornerRadius: cornerRadius))
}
} else {
content
.background {
ZStack {
shape.fill(.ultraThinMaterial)
shape.fill(Color.black.opacity(0.22))
shape.fill(
LinearGradient(
colors: [Color.white.opacity(0.14), .clear],
startPoint: .top,
endPoint: .bottom
)
).blendMode(.screen)
shape.stroke(Color.white.opacity(0.10), lineWidth: 1)
shape.stroke(Color.white.opacity(0.18), lineWidth: 1).padding(1.5)
}
.shadow(color: Color.black.opacity(0.45), radius: 22, y: 14)
}
}
}
}
// MARK: - View Extension
extension View {
/// 5-layer frosted glass background.
func glass(cornerRadius: CGFloat = 24) -> some View {
modifier(GlassModifier(cornerRadius: cornerRadius))
}
/// Glass capsule convenience for pill-shaped elements.
@ViewBuilder
func glassCapsule() -> some View {
if #available(iOS 26, *) {
background {
Capsule().fill(.clear)
.glassEffect(.regular, in: .capsule)
}
} else {
background {
ZStack {
Capsule().fill(.ultraThinMaterial)
Capsule().fill(Color.black.opacity(0.22))
Capsule().fill(
LinearGradient(
colors: [Color.white.opacity(0.14), .clear],
startPoint: .top,
endPoint: .bottom
)
).blendMode(.screen)
Capsule().stroke(Color.white.opacity(0.10), lineWidth: 1)
Capsule().stroke(Color.white.opacity(0.18), lineWidth: 1).padding(1.5)
}
.shadow(color: Color.black.opacity(0.45), radius: 22, y: 14)
}
}
}
}

View File

@@ -1,9 +1,24 @@
import SwiftUI
import UIKit
// MARK: - Glass Effect ID Modifier (iOS 26+)
private struct GlassEffectIDModifier: ViewModifier {
let id: String
let namespace: Namespace.ID?
nonisolated func body(content: Content) -> some View {
if #available(iOS 26, *), let namespace {
content.glassEffectID(id, in: namespace)
} else {
content
}
}
}
// MARK: - Tab
enum RosettaTab: CaseIterable {
enum RosettaTab: CaseIterable, Sendable {
case chats
case settings
case search
@@ -41,26 +56,33 @@ struct TabBadge {
}
// MARK: - RosettaTabBar
/// Figma spec:
/// Container: padding(25h, 16t, 25b), gap=8
/// Main pill: 282x62, r=296, padding(4h, 3v), glass+shadow
/// Each tab: 99x56, icon 30pt, label 10pt
/// Selected: #EDEDED rect r=100, icon+label #008BFF, label bold
/// Unselected: icon+label #404040
/// Search pill: 62x62, glass+shadow, icon 17pt #404040
struct RosettaTabBar: View {
let selectedTab: RosettaTab
var onTabSelected: ((RosettaTab) -> Void)?
var badges: [TabBadge] = []
@Namespace private var glassNS
var body: some View {
if #available(iOS 26, *) {
GlassEffectContainer(spacing: 8) {
tabBarContent
}
.padding(.horizontal, 25)
.padding(.top, 4)
} else {
tabBarContent
.padding(.horizontal, 25)
.padding(.top, 4)
}
}
private var tabBarContent: some View {
HStack(spacing: 8) {
mainTabsPill
searchPill
}
.padding(.horizontal, 25)
.padding(.top, 4)
}
}
@@ -68,145 +90,314 @@ struct RosettaTabBar: View {
private extension RosettaTabBar {
var mainTabsPill: some View {
// Content on top NOT clipped (lens can pop out)
HStack(spacing: 0) {
ForEach(RosettaTab.allCases.filter { $0 != .search }, id: \.self) { tab in
tabItem(tab)
TabItemButton(
tab: tab,
isSelected: tab == selectedTab,
badgeText: badges.first(where: { $0.tab == tab })?.text,
onTap: { onTabSelected?(tab) },
glassNamespace: glassNS
)
}
}
.padding(.horizontal, 4)
.padding(.top, 3)
.padding(.bottom, 3)
.frame(height: 62)
.applyGlassPill()
// Background clipped separately content stays unclipped
.background {
mainPillGlass
}
}
func tabItem(_ tab: RosettaTab) -> some View {
let isSelected = tab == selectedTab
let badgeText = badges.first(where: { $0.tab == tab })?.text
return Button {
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
onTabSelected?(tab)
@ViewBuilder
var mainPillGlass: some View {
if #available(iOS 26, *) {
Capsule().fill(.clear).glassEffect(.regular, in: .capsule)
} else {
ZStack {
// 1. Material
Capsule().fill(.ultraThinMaterial)
// 2. Dark tint
Capsule().fill(Color.black.opacity(0.22))
// 3. Highlight
Capsule().fill(
LinearGradient(
colors: [Color.white.opacity(0.14), .clear],
startPoint: .top,
endPoint: .bottom
)
).blendMode(.screen)
// 4a. Outer stroke
Capsule().stroke(Color.white.opacity(0.10), lineWidth: 1)
// 4b. Inner stroke
Capsule().stroke(Color.white.opacity(0.18), lineWidth: 1).padding(1.5)
}
} label: {
// 5. Shadows
.shadow(color: Color.black.opacity(0.45), radius: 22, y: 14)
}
}
}
// MARK: - Tab Item Button
private struct TabItemButton: View {
let tab: RosettaTab
let isSelected: Bool
let badgeText: String?
let onTap: () -> Void
var glassNamespace: Namespace.ID?
@State private var pressed = false
var body: some View {
Button(action: onTap) {
VStack(spacing: 1) {
ZStack(alignment: .topTrailing) {
Image(systemName: isSelected ? tab.selectedIcon : tab.icon)
.font(.system(size: 22))
.foregroundStyle(isSelected ? RosettaColors.primaryBlue : RosettaColors.adaptive(light: Color(hex: 0x404040), dark: Color(hex: 0x8E8E93)))
.foregroundStyle(tabColor)
.frame(height: 30)
if let badgeText {
badgeView(badgeText)
Text(badgeText)
.font(.system(size: 10, weight: .medium))
.foregroundStyle(.white)
.padding(.horizontal, badgeText.count > 2 ? 4 : 0)
.frame(minWidth: 18, minHeight: 18)
.background(Capsule().fill(RosettaColors.error))
.offset(x: 10, y: -4)
}
}
Text(tab.label)
.font(.system(size: 10, weight: isSelected ? .bold : .medium))
.foregroundStyle(isSelected ? RosettaColors.primaryBlue : RosettaColors.adaptive(light: Color(hex: 0x404040), dark: Color(hex: 0x8E8E93)))
.foregroundStyle(tabColor)
}
.padding(.horizontal, 8)
.padding(.top, 6)
.padding(.bottom, 7)
.frame(maxWidth: .infinity)
.background {
if isSelected {
if isSelected && !pressed {
RoundedRectangle(cornerRadius: 100)
.fill(RosettaColors.adaptive(
light: Color(hex: 0xEDEDED),
dark: Color.white.opacity(0.12)
))
.padding(.horizontal, -8)
.padding(.vertical, -6)
}
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
// Lens: padding glass bubble scale lift
.padding(14)
.background {
if pressed {
lensBubble
.transition(.scale(scale: 0.8).combined(with: .opacity))
}
}
.scaleEffect(pressed ? 1.12 : 1)
.offset(y: pressed ? -28 : 0)
.shadow(color: .black.opacity(pressed ? 0.45 : 0), radius: 22, y: 14)
.shadow(color: Color.cyan.opacity(pressed ? 0.12 : 0), radius: 20, y: 0)
.animation(.spring(response: 0.34, dampingFraction: 0.65), value: pressed)
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
if !pressed {
pressed = true
UIImpactFeedbackGenerator(style: .light).impactOccurred()
}
}
.onEnded { _ in pressed = false }
)
.modifier(GlassEffectIDModifier(id: "\(tab)", namespace: glassNamespace))
.accessibilityLabel(tab.label)
.accessibilityAddTraits(isSelected ? .isSelected : [])
}
private var tabColor: Color {
isSelected
? RosettaColors.primaryBlue
: RosettaColors.adaptive(
light: Color(hex: 0x404040),
dark: Color(hex: 0x8E8E93)
)
}
// MARK: Lens Bubble
@ViewBuilder
private var lensBubble: some View {
if #available(iOS 26, *) {
Circle()
.fill(.clear)
.glassEffect(.regular.interactive(), in: .circle)
} else {
ZStack {
// 1. Material
Circle().fill(.ultraThinMaterial)
// 2. Dark tint
Circle().fill(Color.black.opacity(0.22))
// 3. Highlight (topbottom, screen blend)
Circle().fill(
LinearGradient(
colors: [Color.white.opacity(0.14), .clear],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
).blendMode(.screen)
// 4a. Outer stroke
Circle().stroke(Color.white.opacity(0.10), lineWidth: 1)
// 4b. Inner stroke
Circle().stroke(Color.white.opacity(0.18), lineWidth: 1).padding(1.5)
// 6. Rainbow (thin, subtle, screen blend)
Circle().stroke(
AngularGradient(
colors: [
Color.cyan.opacity(0.55),
Color.blue.opacity(0.55),
Color.purple.opacity(0.55),
Color.pink.opacity(0.55),
Color.orange.opacity(0.55),
Color.yellow.opacity(0.45),
Color.cyan.opacity(0.55),
],
center: .center
),
lineWidth: 1.4
).blendMode(.screen)
}
}
}
}
// MARK: - Search Pill
private extension RosettaTabBar {
var searchPill: some View {
let isSelected = selectedTab == .search
SearchPillButton(
isSelected: selectedTab == .search,
onTap: { onTabSelected?(.search) },
glassNamespace: glassNS
)
}
}
return Button {
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
onTabSelected?(.search)
}
} label: {
Image(systemName: isSelected ? "magnifyingglass" : "magnifyingglass")
private struct SearchPillButton: View {
let isSelected: Bool
let onTap: () -> Void
var glassNamespace: Namespace.ID?
@State private var pressed = false
var body: some View {
Button(action: onTap) {
Image(systemName: "magnifyingglass")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(isSelected ? RosettaColors.primaryBlue : RosettaColors.adaptive(light: Color(hex: 0x404040), dark: Color(hex: 0x8E8E93)))
.frame(width: 54, height: 54)
.background {
if isSelected {
Circle()
.fill(RosettaColors.adaptive(
light: Color(hex: 0xEDEDED),
dark: Color.white.opacity(0.12)
))
}
}
.foregroundStyle(
isSelected
? RosettaColors.primaryBlue
: RosettaColors.adaptive(
light: Color(hex: 0x404040),
dark: Color(hex: 0x8E8E93)
)
)
}
.buttonStyle(.plain)
.padding(4)
// Lens
.padding(14)
.background {
if pressed {
searchLensBubble
.transition(.scale(scale: 0.8).combined(with: .opacity))
}
}
.scaleEffect(pressed ? 1.15 : 1)
.offset(y: pressed ? -28 : 0)
.shadow(color: .black.opacity(pressed ? 0.45 : 0), radius: 22, y: 14)
.shadow(color: Color.cyan.opacity(pressed ? 0.12 : 0), radius: 20, y: 0)
.animation(.spring(response: 0.34, dampingFraction: 0.65), value: pressed)
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
if !pressed {
pressed = true
UIImpactFeedbackGenerator(style: .light).impactOccurred()
}
}
.onEnded { _ in pressed = false }
)
.frame(width: 62, height: 62)
.applyGlassPill()
// Background clipped separately
.background { searchPillGlass }
.modifier(GlassEffectIDModifier(id: "search", namespace: glassNamespace))
.accessibilityLabel("Search")
.accessibilityAddTraits(isSelected ? .isSelected : [])
}
}
// MARK: - Glass Pill
// MARK: Lens for search
private struct GlassPillModifier: ViewModifier {
func body(content: Content) -> some View {
@ViewBuilder
private var searchLensBubble: some View {
if #available(iOS 26, *) {
content
.glassEffect(.regular, in: .capsule)
Circle()
.fill(.clear)
.glassEffect(.regular.interactive(), in: .circle)
} else {
content
.background(
Capsule()
.fill(RosettaColors.adaptive(
light: Color.white.opacity(0.65),
dark: Color(hex: 0x2A2A2A).opacity(0.8)
))
.shadow(color: RosettaColors.adaptive(
light: Color(hex: 0xDDDDDD).opacity(0.5),
dark: Color.black.opacity(0.3)
), radius: 16, y: 4)
)
ZStack {
Circle().fill(.ultraThinMaterial)
Circle().fill(Color.black.opacity(0.22))
Circle().fill(
LinearGradient(
colors: [Color.white.opacity(0.14), .clear],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
).blendMode(.screen)
Circle().stroke(Color.white.opacity(0.10), lineWidth: 1)
Circle().stroke(Color.white.opacity(0.18), lineWidth: 1).padding(1.5)
Circle().stroke(
AngularGradient(
colors: [
Color.cyan.opacity(0.55),
Color.blue.opacity(0.55),
Color.purple.opacity(0.55),
Color.pink.opacity(0.55),
Color.orange.opacity(0.55),
Color.yellow.opacity(0.45),
Color.cyan.opacity(0.55),
],
center: .center
),
lineWidth: 1.4
).blendMode(.screen)
}
}
}
}
private extension View {
func applyGlassPill() -> some View {
modifier(GlassPillModifier())
}
}
// MARK: - Helpers
private extension RosettaTabBar {
func badgeView(_ text: String) -> some View {
Text(text)
.font(.system(size: 10, weight: .medium))
.foregroundStyle(.white)
.padding(.horizontal, text.count > 2 ? 4 : 0)
.frame(minWidth: 18, minHeight: 18)
.background(Capsule().fill(RosettaColors.error))
.offset(x: 10, y: -4)
}
var safeAreaBottom: CGFloat {
guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = scene.windows.first(where: \.isKeyWindow) else { return 0 }
return window.safeAreaInsets.bottom
@ViewBuilder
private var searchPillGlass: some View {
if #available(iOS 26, *) {
Circle().fill(.clear).glassEffect(.regular, in: .circle)
} else {
ZStack {
Circle().fill(.ultraThinMaterial)
Circle().fill(Color.black.opacity(0.22))
Circle().fill(
LinearGradient(
colors: [Color.white.opacity(0.14), .clear],
startPoint: .top,
endPoint: .bottom
)
).blendMode(.screen)
Circle().stroke(Color.white.opacity(0.10), lineWidth: 1)
Circle().stroke(Color.white.opacity(0.18), lineWidth: 1).padding(1.5)
}
.shadow(color: Color.black.opacity(0.45), radius: 22, y: 14)
}
}
}
@@ -214,12 +405,12 @@ private extension RosettaTabBar {
#Preview {
ZStack(alignment: .bottom) {
RosettaColors.Adaptive.background.ignoresSafeArea()
Color.black.ignoresSafeArea()
VStack {
Spacer()
Text("Content here")
.foregroundStyle(RosettaColors.Adaptive.text)
Text("Hold a tab to see the lens")
.foregroundStyle(.white.opacity(0.5))
Spacer()
}

View File

@@ -58,7 +58,7 @@ private extension ConfirmSeedPhraseView {
Text("Enter words #2, #5, #9, #12 to confirm\nyou've backed up your phrase.")
.font(.system(size: 15))
.foregroundStyle(RosettaColors.secondaryText)
.foregroundStyle(Color.white.opacity(0.7))
.multilineTextAlignment(.center)
.lineSpacing(3)
}

View File

@@ -51,7 +51,7 @@ private extension ImportSeedPhraseView {
Text("Enter your 12-word recovery phrase\nto restore your account.")
.font(.system(size: 15))
.foregroundStyle(RosettaColors.secondaryText)
.foregroundStyle(Color.white.opacity(0.7))
.multilineTextAlignment(.center)
.lineSpacing(3)
}

View File

@@ -107,7 +107,7 @@ struct WeakPasswordWarning: View {
Text("Your password is too weak. Consider using at least 6 characters for better security.")
.font(.system(size: 13))
.foregroundStyle(RosettaColors.secondaryText)
.foregroundStyle(Color.white.opacity(0.7))
.lineSpacing(2)
}
.padding(14)

View File

@@ -54,7 +54,7 @@ private extension SeedPhraseView {
Text("Write down these 12 words in order.\nYou'll need them to restore your account.")
.font(.system(size: 15))
.foregroundStyle(RosettaColors.secondaryText)
.foregroundStyle(Color.white.opacity(0.7))
.multilineTextAlignment(.center)
.lineSpacing(3)
.opacity(isContentVisible ? 1.0 : 0.0)

View File

@@ -103,7 +103,7 @@ private extension SetPasswordView {
? "Set a password to protect your recovered account.\nYou'll need it to unlock Rosetta."
: "This password encrypts your keys locally.\nYou'll need it to unlock Rosetta.")
.font(.system(size: 14))
.foregroundStyle(RosettaColors.secondaryText)
.foregroundStyle(Color.white.opacity(0.7))
.multilineTextAlignment(.center)
.lineSpacing(2)
}
@@ -175,7 +175,7 @@ private extension SetPasswordView {
Image(systemName: isRevealed ? "eye.slash" : "eye")
.font(.system(size: 16))
.foregroundStyle(RosettaColors.secondaryText)
.foregroundStyle(Color.white.opacity(0.5))
.frame(width: 30, height: 30)
.contentShape(Rectangle())
.onTapGesture {
@@ -218,7 +218,7 @@ private extension SetPasswordView {
Text("Your password is never stored or sent anywhere. It's only used to encrypt your keys locally.")
.font(.system(size: 13))
.foregroundStyle(RosettaColors.secondaryText)
.foregroundStyle(Color.white.opacity(0.7))
.lineSpacing(2)
}
.padding(16)

View File

@@ -79,7 +79,7 @@ struct UnlockView: View {
// Subtitle matching Android
Text("For unlock account enter password")
.font(.system(size: 15))
.foregroundStyle(RosettaColors.secondaryText)
.foregroundStyle(Color.white.opacity(0.7))
.opacity(showSubtitle ? 1 : 0)
.offset(y: showSubtitle ? 0 : 8)
@@ -136,7 +136,7 @@ private extension UnlockView {
Image(systemName: showPassword ? "eye.slash" : "eye")
.font(.system(size: 18))
.foregroundStyle(Color(white: 0.45))
.foregroundStyle(Color.white.opacity(0.5))
.contentShape(Rectangle())
.onTapGesture { showPassword.toggle() }
}
@@ -195,7 +195,7 @@ private extension UnlockView {
VStack(spacing: 4) {
HStack(spacing: 0) {
Text("You can also ")
.foregroundStyle(RosettaColors.secondaryText)
.foregroundStyle(Color.white.opacity(0.7))
Button {
onCreateNewAccount?()
@@ -207,13 +207,13 @@ private extension UnlockView {
.buttonStyle(.plain)
Text(" or")
.foregroundStyle(RosettaColors.secondaryText)
.foregroundStyle(Color.white.opacity(0.7))
}
.font(.system(size: 15))
HStack(spacing: 0) {
Text("create a ")
.foregroundStyle(RosettaColors.secondaryText)
.foregroundStyle(Color.white.opacity(0.7))
Button {
onCreateNewAccount?()

View File

@@ -90,7 +90,7 @@ private extension WelcomeView {
var subtitleSection: some View {
Text("Secure messaging with\ncryptographic keys")
.font(.system(size: 16))
.foregroundStyle(RosettaColors.secondaryText)
.foregroundStyle(Color.white.opacity(0.7))
.multilineTextAlignment(.center)
.opacity(isVisible ? 1.0 : 0.0)
.offset(y: isVisible ? 0 : 12)
@@ -122,7 +122,7 @@ private extension WelcomeView {
Text(label)
.font(.system(size: 13, weight: .medium))
.foregroundStyle(RosettaColors.secondaryText)
.foregroundStyle(Color.white.opacity(0.7))
}
.accessibilityElement(children: .combine)
.accessibilityLabel(label)