feat: Update text color to improve readability across multiple authentication views
This commit is contained in:
81
Rosetta/DesignSystem/Components/GlassModifier.swift
Normal file
81
Rosetta/DesignSystem/Components/GlassModifier.swift
Normal 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. white→clear 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,24 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
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
|
// MARK: - Tab
|
||||||
|
|
||||||
enum RosettaTab: CaseIterable {
|
enum RosettaTab: CaseIterable, Sendable {
|
||||||
case chats
|
case chats
|
||||||
case settings
|
case settings
|
||||||
case search
|
case search
|
||||||
@@ -41,26 +56,33 @@ struct TabBadge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - RosettaTabBar
|
// 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 {
|
struct RosettaTabBar: View {
|
||||||
let selectedTab: RosettaTab
|
let selectedTab: RosettaTab
|
||||||
var onTabSelected: ((RosettaTab) -> Void)?
|
var onTabSelected: ((RosettaTab) -> Void)?
|
||||||
var badges: [TabBadge] = []
|
var badges: [TabBadge] = []
|
||||||
|
|
||||||
|
@Namespace private var glassNS
|
||||||
|
|
||||||
var body: some View {
|
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) {
|
HStack(spacing: 8) {
|
||||||
mainTabsPill
|
mainTabsPill
|
||||||
searchPill
|
searchPill
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 25)
|
|
||||||
.padding(.top, 4)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,145 +90,314 @@ struct RosettaTabBar: View {
|
|||||||
|
|
||||||
private extension RosettaTabBar {
|
private extension RosettaTabBar {
|
||||||
var mainTabsPill: some View {
|
var mainTabsPill: some View {
|
||||||
|
// Content on top — NOT clipped (lens can pop out)
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
ForEach(RosettaTab.allCases.filter { $0 != .search }, id: \.self) { tab in
|
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(.horizontal, 4)
|
||||||
.padding(.top, 3)
|
.padding(.top, 3)
|
||||||
.padding(.bottom, 3)
|
.padding(.bottom, 3)
|
||||||
.frame(height: 62)
|
.frame(height: 62)
|
||||||
.applyGlassPill()
|
// Background clipped separately — content stays unclipped
|
||||||
|
.background {
|
||||||
|
mainPillGlass
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func tabItem(_ tab: RosettaTab) -> some View {
|
@ViewBuilder
|
||||||
let isSelected = tab == selectedTab
|
var mainPillGlass: some View {
|
||||||
let badgeText = badges.first(where: { $0.tab == tab })?.text
|
if #available(iOS 26, *) {
|
||||||
|
Capsule().fill(.clear).glassEffect(.regular, in: .capsule)
|
||||||
return Button {
|
} else {
|
||||||
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
|
ZStack {
|
||||||
onTabSelected?(tab)
|
// 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) {
|
VStack(spacing: 1) {
|
||||||
ZStack(alignment: .topTrailing) {
|
ZStack(alignment: .topTrailing) {
|
||||||
Image(systemName: isSelected ? tab.selectedIcon : tab.icon)
|
Image(systemName: isSelected ? tab.selectedIcon : tab.icon)
|
||||||
.font(.system(size: 22))
|
.font(.system(size: 22))
|
||||||
.foregroundStyle(isSelected ? RosettaColors.primaryBlue : RosettaColors.adaptive(light: Color(hex: 0x404040), dark: Color(hex: 0x8E8E93)))
|
.foregroundStyle(tabColor)
|
||||||
.frame(height: 30)
|
.frame(height: 30)
|
||||||
|
|
||||||
if let badgeText {
|
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)
|
Text(tab.label)
|
||||||
.font(.system(size: 10, weight: isSelected ? .bold : .medium))
|
.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)
|
.frame(maxWidth: .infinity)
|
||||||
.background {
|
.background {
|
||||||
if isSelected {
|
if isSelected && !pressed {
|
||||||
RoundedRectangle(cornerRadius: 100)
|
RoundedRectangle(cornerRadius: 100)
|
||||||
.fill(RosettaColors.adaptive(
|
.fill(RosettaColors.adaptive(
|
||||||
light: Color(hex: 0xEDEDED),
|
light: Color(hex: 0xEDEDED),
|
||||||
dark: Color.white.opacity(0.12)
|
dark: Color.white.opacity(0.12)
|
||||||
))
|
))
|
||||||
|
.padding(.horizontal, -8)
|
||||||
|
.padding(.vertical, -6)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.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)
|
.accessibilityLabel(tab.label)
|
||||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
.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 (top→bottom, 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
|
// MARK: - Search Pill
|
||||||
|
|
||||||
private extension RosettaTabBar {
|
private extension RosettaTabBar {
|
||||||
var searchPill: some View {
|
var searchPill: some View {
|
||||||
let isSelected = selectedTab == .search
|
SearchPillButton(
|
||||||
|
isSelected: selectedTab == .search,
|
||||||
|
onTap: { onTabSelected?(.search) },
|
||||||
|
glassNamespace: glassNS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Button {
|
private struct SearchPillButton: View {
|
||||||
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
|
let isSelected: Bool
|
||||||
onTabSelected?(.search)
|
let onTap: () -> Void
|
||||||
}
|
var glassNamespace: Namespace.ID?
|
||||||
} label: {
|
|
||||||
Image(systemName: isSelected ? "magnifyingglass" : "magnifyingglass")
|
@State private var pressed = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: onTap) {
|
||||||
|
Image(systemName: "magnifyingglass")
|
||||||
.font(.system(size: 17, weight: .semibold))
|
.font(.system(size: 17, weight: .semibold))
|
||||||
.foregroundStyle(isSelected ? RosettaColors.primaryBlue : RosettaColors.adaptive(light: Color(hex: 0x404040), dark: Color(hex: 0x8E8E93)))
|
.foregroundStyle(
|
||||||
.frame(width: 54, height: 54)
|
isSelected
|
||||||
.background {
|
? RosettaColors.primaryBlue
|
||||||
if isSelected {
|
: RosettaColors.adaptive(
|
||||||
Circle()
|
light: Color(hex: 0x404040),
|
||||||
.fill(RosettaColors.adaptive(
|
dark: Color(hex: 0x8E8E93)
|
||||||
light: Color(hex: 0xEDEDED),
|
)
|
||||||
dark: Color.white.opacity(0.12)
|
)
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.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)
|
.frame(width: 62, height: 62)
|
||||||
.applyGlassPill()
|
// Background clipped separately
|
||||||
|
.background { searchPillGlass }
|
||||||
|
.modifier(GlassEffectIDModifier(id: "search", namespace: glassNamespace))
|
||||||
.accessibilityLabel("Search")
|
.accessibilityLabel("Search")
|
||||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Glass Pill
|
// MARK: Lens for search
|
||||||
|
|
||||||
private struct GlassPillModifier: ViewModifier {
|
@ViewBuilder
|
||||||
func body(content: Content) -> some View {
|
private var searchLensBubble: some View {
|
||||||
if #available(iOS 26, *) {
|
if #available(iOS 26, *) {
|
||||||
content
|
Circle()
|
||||||
.glassEffect(.regular, in: .capsule)
|
.fill(.clear)
|
||||||
|
.glassEffect(.regular.interactive(), in: .circle)
|
||||||
} else {
|
} else {
|
||||||
content
|
ZStack {
|
||||||
.background(
|
Circle().fill(.ultraThinMaterial)
|
||||||
Capsule()
|
Circle().fill(Color.black.opacity(0.22))
|
||||||
.fill(RosettaColors.adaptive(
|
Circle().fill(
|
||||||
light: Color.white.opacity(0.65),
|
LinearGradient(
|
||||||
dark: Color(hex: 0x2A2A2A).opacity(0.8)
|
colors: [Color.white.opacity(0.14), .clear],
|
||||||
))
|
startPoint: .topLeading,
|
||||||
.shadow(color: RosettaColors.adaptive(
|
endPoint: .bottomTrailing
|
||||||
light: Color(hex: 0xDDDDDD).opacity(0.5),
|
|
||||||
dark: Color.black.opacity(0.3)
|
|
||||||
), radius: 16, y: 4)
|
|
||||||
)
|
)
|
||||||
|
).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 {
|
@ViewBuilder
|
||||||
guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
private var searchPillGlass: some View {
|
||||||
let window = scene.windows.first(where: \.isKeyWindow) else { return 0 }
|
if #available(iOS 26, *) {
|
||||||
return window.safeAreaInsets.bottom
|
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 {
|
#Preview {
|
||||||
ZStack(alignment: .bottom) {
|
ZStack(alignment: .bottom) {
|
||||||
RosettaColors.Adaptive.background.ignoresSafeArea()
|
Color.black.ignoresSafeArea()
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("Content here")
|
Text("Hold a tab to see the lens")
|
||||||
.foregroundStyle(RosettaColors.Adaptive.text)
|
.foregroundStyle(.white.opacity(0.5))
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ private extension ConfirmSeedPhraseView {
|
|||||||
|
|
||||||
Text("Enter words #2, #5, #9, #12 to confirm\nyou've backed up your phrase.")
|
Text("Enter words #2, #5, #9, #12 to confirm\nyou've backed up your phrase.")
|
||||||
.font(.system(size: 15))
|
.font(.system(size: 15))
|
||||||
.foregroundStyle(RosettaColors.secondaryText)
|
.foregroundStyle(Color.white.opacity(0.7))
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.lineSpacing(3)
|
.lineSpacing(3)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ private extension ImportSeedPhraseView {
|
|||||||
|
|
||||||
Text("Enter your 12-word recovery phrase\nto restore your account.")
|
Text("Enter your 12-word recovery phrase\nto restore your account.")
|
||||||
.font(.system(size: 15))
|
.font(.system(size: 15))
|
||||||
.foregroundStyle(RosettaColors.secondaryText)
|
.foregroundStyle(Color.white.opacity(0.7))
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.lineSpacing(3)
|
.lineSpacing(3)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ struct WeakPasswordWarning: View {
|
|||||||
|
|
||||||
Text("Your password is too weak. Consider using at least 6 characters for better security.")
|
Text("Your password is too weak. Consider using at least 6 characters for better security.")
|
||||||
.font(.system(size: 13))
|
.font(.system(size: 13))
|
||||||
.foregroundStyle(RosettaColors.secondaryText)
|
.foregroundStyle(Color.white.opacity(0.7))
|
||||||
.lineSpacing(2)
|
.lineSpacing(2)
|
||||||
}
|
}
|
||||||
.padding(14)
|
.padding(14)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ private extension SeedPhraseView {
|
|||||||
|
|
||||||
Text("Write down these 12 words in order.\nYou'll need them to restore your account.")
|
Text("Write down these 12 words in order.\nYou'll need them to restore your account.")
|
||||||
.font(.system(size: 15))
|
.font(.system(size: 15))
|
||||||
.foregroundStyle(RosettaColors.secondaryText)
|
.foregroundStyle(Color.white.opacity(0.7))
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.lineSpacing(3)
|
.lineSpacing(3)
|
||||||
.opacity(isContentVisible ? 1.0 : 0.0)
|
.opacity(isContentVisible ? 1.0 : 0.0)
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ private extension SetPasswordView {
|
|||||||
? "Set a password to protect your recovered account.\nYou'll need it to unlock Rosetta."
|
? "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.")
|
: "This password encrypts your keys locally.\nYou'll need it to unlock Rosetta.")
|
||||||
.font(.system(size: 14))
|
.font(.system(size: 14))
|
||||||
.foregroundStyle(RosettaColors.secondaryText)
|
.foregroundStyle(Color.white.opacity(0.7))
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.lineSpacing(2)
|
.lineSpacing(2)
|
||||||
}
|
}
|
||||||
@@ -175,7 +175,7 @@ private extension SetPasswordView {
|
|||||||
|
|
||||||
Image(systemName: isRevealed ? "eye.slash" : "eye")
|
Image(systemName: isRevealed ? "eye.slash" : "eye")
|
||||||
.font(.system(size: 16))
|
.font(.system(size: 16))
|
||||||
.foregroundStyle(RosettaColors.secondaryText)
|
.foregroundStyle(Color.white.opacity(0.5))
|
||||||
.frame(width: 30, height: 30)
|
.frame(width: 30, height: 30)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.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.")
|
Text("Your password is never stored or sent anywhere. It's only used to encrypt your keys locally.")
|
||||||
.font(.system(size: 13))
|
.font(.system(size: 13))
|
||||||
.foregroundStyle(RosettaColors.secondaryText)
|
.foregroundStyle(Color.white.opacity(0.7))
|
||||||
.lineSpacing(2)
|
.lineSpacing(2)
|
||||||
}
|
}
|
||||||
.padding(16)
|
.padding(16)
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ struct UnlockView: View {
|
|||||||
// Subtitle — matching Android
|
// Subtitle — matching Android
|
||||||
Text("For unlock account enter password")
|
Text("For unlock account enter password")
|
||||||
.font(.system(size: 15))
|
.font(.system(size: 15))
|
||||||
.foregroundStyle(RosettaColors.secondaryText)
|
.foregroundStyle(Color.white.opacity(0.7))
|
||||||
.opacity(showSubtitle ? 1 : 0)
|
.opacity(showSubtitle ? 1 : 0)
|
||||||
.offset(y: showSubtitle ? 0 : 8)
|
.offset(y: showSubtitle ? 0 : 8)
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ private extension UnlockView {
|
|||||||
|
|
||||||
Image(systemName: showPassword ? "eye.slash" : "eye")
|
Image(systemName: showPassword ? "eye.slash" : "eye")
|
||||||
.font(.system(size: 18))
|
.font(.system(size: 18))
|
||||||
.foregroundStyle(Color(white: 0.45))
|
.foregroundStyle(Color.white.opacity(0.5))
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture { showPassword.toggle() }
|
.onTapGesture { showPassword.toggle() }
|
||||||
}
|
}
|
||||||
@@ -195,7 +195,7 @@ private extension UnlockView {
|
|||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
Text("You can also ")
|
Text("You can also ")
|
||||||
.foregroundStyle(RosettaColors.secondaryText)
|
.foregroundStyle(Color.white.opacity(0.7))
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
onCreateNewAccount?()
|
onCreateNewAccount?()
|
||||||
@@ -207,13 +207,13 @@ private extension UnlockView {
|
|||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
Text(" or")
|
Text(" or")
|
||||||
.foregroundStyle(RosettaColors.secondaryText)
|
.foregroundStyle(Color.white.opacity(0.7))
|
||||||
}
|
}
|
||||||
.font(.system(size: 15))
|
.font(.system(size: 15))
|
||||||
|
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
Text("create a ")
|
Text("create a ")
|
||||||
.foregroundStyle(RosettaColors.secondaryText)
|
.foregroundStyle(Color.white.opacity(0.7))
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
onCreateNewAccount?()
|
onCreateNewAccount?()
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ private extension WelcomeView {
|
|||||||
var subtitleSection: some View {
|
var subtitleSection: some View {
|
||||||
Text("Secure messaging with\ncryptographic keys")
|
Text("Secure messaging with\ncryptographic keys")
|
||||||
.font(.system(size: 16))
|
.font(.system(size: 16))
|
||||||
.foregroundStyle(RosettaColors.secondaryText)
|
.foregroundStyle(Color.white.opacity(0.7))
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.opacity(isVisible ? 1.0 : 0.0)
|
.opacity(isVisible ? 1.0 : 0.0)
|
||||||
.offset(y: isVisible ? 0 : 12)
|
.offset(y: isVisible ? 0 : 12)
|
||||||
@@ -122,7 +122,7 @@ private extension WelcomeView {
|
|||||||
|
|
||||||
Text(label)
|
Text(label)
|
||||||
.font(.system(size: 13, weight: .medium))
|
.font(.system(size: 13, weight: .medium))
|
||||||
.foregroundStyle(RosettaColors.secondaryText)
|
.foregroundStyle(Color.white.opacity(0.7))
|
||||||
}
|
}
|
||||||
.accessibilityElement(children: .combine)
|
.accessibilityElement(children: .combine)
|
||||||
.accessibilityLabel(label)
|
.accessibilityLabel(label)
|
||||||
|
|||||||
Reference in New Issue
Block a user