diff --git a/Kasztelan.xcodeproj/project.pbxproj b/Kasztelan.xcodeproj/project.pbxproj index a21ea20..fbbd68a 100644 --- a/Kasztelan.xcodeproj/project.pbxproj +++ b/Kasztelan.xcodeproj/project.pbxproj @@ -292,12 +292,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 11; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Kasztelan/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink"; IPHONEOS_DEPLOYMENT_TARGET = 18.0; - MARKETING_VERSION = 1.0.8; + MARKETING_VERSION = 1.0.9; PRODUCT_BUNDLE_IDENTIFIER = uk.co.kasztelan.app.ios; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -316,12 +316,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 11; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Kasztelan/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink"; IPHONEOS_DEPLOYMENT_TARGET = 18.0; - MARKETING_VERSION = 1.0.8; + MARKETING_VERSION = 1.0.9; PRODUCT_BUNDLE_IDENTIFIER = uk.co.kasztelan.app.ios; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/Kasztelan/AuthView.swift b/Kasztelan/AuthView.swift index 0850925..bf56c22 100644 --- a/Kasztelan/AuthView.swift +++ b/Kasztelan/AuthView.swift @@ -20,13 +20,19 @@ struct AuthView: View { } .padding(.bottom, 18) - if registering { registrationForm } - else { loginForm } + if registering { + registrationForm + .transition(.move(edge: .trailing).combined(with: .opacity)) + } else { + loginForm + .transition(.move(edge: .leading).combined(with: .opacity)) + } } .padding(22) + .animation(.spring(response: 0.42, dampingFraction: 0.86), value: registering) } .scrollDismissesKeyboard(.interactively) - .background(KTheme.background) + .background(AmbientBackground()) } private var loginForm: some View { @@ -55,9 +61,7 @@ struct AuthView: View { } .accessibilityLabel(model.text("Otwórz resetowanie hasła", "Open password reset")) Button(model.text("Załóż konto", "Create account")) { registering = true } - .frame(maxWidth: .infinity, minHeight: 50) - .background(KTheme.panel) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .buttonStyle(GlassButtonStyle()) } } @@ -85,9 +89,7 @@ struct AuthView: View { } .buttonStyle(PrimaryButtonStyle()) Button(model.text("Mam już konto", "I already have an account")) { registering = false } - .frame(maxWidth: .infinity, minHeight: 50) - .background(KTheme.panel) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .buttonStyle(GlassButtonStyle()) } } @@ -120,7 +122,9 @@ struct LanguageButton: View { .font(.subheadline.bold()) .foregroundStyle(KTheme.orange) .frame(width: 44, height: 44, alignment: .center) - .background(KTheme.panel, in: Circle()) + .background(.ultraThinMaterial, in: Circle()) + .overlay { Circle().stroke(.white.opacity(0.20), lineWidth: 1) } + .shadow(color: KTheme.orange.opacity(0.18), radius: 12) .contentShape(Circle()) } .buttonStyle(.plain) diff --git a/Kasztelan/CardView.swift b/Kasztelan/CardView.swift index 47b9b24..6561de8 100644 --- a/Kasztelan/CardView.swift +++ b/Kasztelan/CardView.swift @@ -39,7 +39,7 @@ struct CardView: View { .padding(.vertical, compact ? 8 : 14) .frame(width: geometry.size.width, height: geometry.size.height, alignment: .top) } - .background(KTheme.background) + .background(.clear) .fullScreenCover(isPresented: $cardScannerPresented) { CardScannerScreen( onScan: acceptScannedCard, @@ -120,8 +120,7 @@ struct CardView: View { .tint(.white) .padding(.horizontal, 14) .frame(minHeight: 50) - .background(KTheme.panel.opacity(0.92)) - .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) .overlay { RoundedRectangle(cornerRadius: 12, style: .continuous) .stroke(Color.white.opacity(0.35), lineWidth: 1) @@ -144,8 +143,7 @@ struct CardView: View { Label(model.text("Skanuj kartę", "Scan card"), systemImage: "camera.viewfinder") .frame(maxWidth: .infinity, minHeight: 22) } - .buttonStyle(.bordered) - .tint(.white) + .buttonStyle(GlassButtonStyle()) .font(.subheadline.weight(.semibold)) Button { cardNumberFocused = false @@ -154,17 +152,43 @@ struct CardView: View { Text(model.text("Nie mam karty — wygeneruj cyfrową", "I don't have a card — create a digital one")) .frame(maxWidth: .infinity, minHeight: 22) } - .buttonStyle(.bordered) - .tint(.white) + .buttonStyle(GlassButtonStyle()) .font(.subheadline.weight(.semibold)) } } } .frame(maxWidth: .infinity) .padding(compact ? 14 : 22) - .background(LinearGradient(colors: [KTheme.orange, KTheme.pink, KTheme.purple], startPoint: .topLeading, endPoint: .bottomTrailing)) + .background { + ZStack { + LinearGradient( + colors: [KTheme.orange.opacity(0.94), KTheme.pink.opacity(0.90), KTheme.purple.opacity(0.94)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + Circle() + .fill(.white.opacity(0.16)) + .frame(width: 210, height: 210) + .blur(radius: 10) + .offset(x: 150, y: -130) + Circle() + .fill(KTheme.cyan.opacity(0.14)) + .frame(width: 140, height: 140) + .blur(radius: 16) + .offset(x: -145, y: 150) + LinearGradient( + colors: [.white.opacity(0.22), .clear, .black.opacity(0.12)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + } + } .clipShape(RoundedRectangle(cornerRadius: 26, style: .continuous)) - .shadow(color: KTheme.pink.opacity(0.2), radius: 18, y: 8) + .overlay { + RoundedRectangle(cornerRadius: 26, style: .continuous) + .stroke(.white.opacity(0.32), lineWidth: 1) + } + .shadow(color: KTheme.pink.opacity(0.28), radius: 28, y: 16) } private func acceptScannedCard(_ scan: ScannedCard) { diff --git a/Kasztelan/KasztelanApp.swift b/Kasztelan/KasztelanApp.swift index 49b4bac..dd40d53 100644 --- a/Kasztelan/KasztelanApp.swift +++ b/Kasztelan/KasztelanApp.swift @@ -144,7 +144,7 @@ struct RootView: View { var body: some View { ZStack { - KTheme.background.ignoresSafeArea() + AmbientBackground() if model.isLoading { ProgressView(model.text("Sprawdzam konto…", "Checking your account…")) .tint(KTheme.orange) diff --git a/Kasztelan/MainTabView.swift b/Kasztelan/MainTabView.swift index 1943043..dfe4268 100644 --- a/Kasztelan/MainTabView.swift +++ b/Kasztelan/MainTabView.swift @@ -17,6 +17,8 @@ struct MainTabView: View { .tag(2) } .tint(KTheme.orange) + .toolbarBackground(.ultraThinMaterial, for: .tabBar) + .toolbarBackground(.visible, for: .tabBar) .task { await NotificationService.requestPermission() } } } @@ -30,6 +32,7 @@ struct AppToolbar: ToolbarContent { .resizable() .scaledToFit() .frame(width: 170, height: 44) + .shadow(color: KTheme.pink.opacity(0.28), radius: 12) } ToolbarItem(placement: .topBarTrailing) { LanguageButton() } } diff --git a/Kasztelan/MessagesView.swift b/Kasztelan/MessagesView.swift index 9efe3d4..9e4d0af 100644 --- a/Kasztelan/MessagesView.swift +++ b/Kasztelan/MessagesView.swift @@ -27,7 +27,7 @@ struct MessagesView: View { } .padding(20) } - .background(KTheme.background) + .background(AmbientBackground()) .toolbar { AppToolbar() } .refreshable { await model.refresh() } .task { await model.refresh() } diff --git a/Kasztelan/ProfileView.swift b/Kasztelan/ProfileView.swift index cdd9bb2..1371ee7 100644 --- a/Kasztelan/ProfileView.swift +++ b/Kasztelan/ProfileView.swift @@ -34,9 +34,7 @@ struct ProfileView: View { if model.cardNumber.isEmpty { cardAssignment } Button(model.text("Zmień hasło", "Change password")) { changingPassword = true } - .frame(maxWidth: .infinity, minHeight: 52) - .background(KTheme.panel) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .buttonStyle(GlassButtonStyle()) Button(model.text("Wyloguj", "Sign out")) { Task { await model.logout() } } .buttonStyle(PrimaryButtonStyle()) Button(model.text("Usuń konto", "Delete account"), role: .destructive) { deletingAccount = true } @@ -53,7 +51,7 @@ struct ProfileView: View { } .padding(20) } - .background(KTheme.background) + .background(AmbientBackground()) .toolbar { AppToolbar() } .fullScreenCover(isPresented: $editingProfile) { EditProfileView() } .sheet(isPresented: $changingPassword) { ChangePasswordView() } @@ -69,7 +67,7 @@ struct ProfileView: View { Spacer() } .padding(20) - .background(KTheme.background) + .background(AmbientBackground()) .navigationTitle(model.text("Usuń konto", "Delete account")) .toolbar { ToolbarItem(placement: .cancellationAction) { Button(model.text("Anuluj", "Cancel")) { deletingAccount = false } } } } @@ -95,9 +93,7 @@ struct ProfileView: View { Button(model.text("Nie mam karty — wygeneruj cyfrową", "I don't have a card — create a digital one")) { Task { _ = await model.generateCard() } } - .frame(maxWidth: .infinity, minHeight: 50) - .background(KTheme.panel) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .buttonStyle(GlassButtonStyle()) } .kasztelanPanel() } @@ -138,7 +134,7 @@ struct EditProfileView: View { .frame(maxWidth: .infinity) .padding(20) } - .background(KTheme.background) + .background(AmbientBackground()) .navigationTitle(model.text("Edytuj profil", "Edit profile")) .toolbar { ToolbarItem(placement: .cancellationAction) { Button(model.text("Anuluj", "Cancel")) { dismiss() } } } .onAppear { form = model.profileForm() } @@ -174,7 +170,7 @@ struct ChangePasswordView: View { Spacer() } .padding(20) - .background(KTheme.background) + .background(AmbientBackground()) .navigationTitle(model.text("Zmień hasło", "Change password")) .toolbar { ToolbarItem(placement: .cancellationAction) { Button(model.text("Anuluj", "Cancel")) { dismiss() } } } } diff --git a/Kasztelan/Theme.swift b/Kasztelan/Theme.swift index 7f6dfa6..9108dee 100644 --- a/Kasztelan/Theme.swift +++ b/Kasztelan/Theme.swift @@ -1,24 +1,94 @@ import SwiftUI enum KTheme { - static let background = Color(red: 11/255, green: 11/255, blue: 14/255) - static let panel = Color(red: 28/255, green: 28/255, blue: 34/255) + static let background = Color(red: 5/255, green: 6/255, blue: 12/255) + static let panel = Color(red: 28/255, green: 28/255, blue: 38/255) static let orange = Color(red: 1, green: 106/255, blue: 0) static let pink = Color(red: 242/255, green: 29/255, blue: 167/255) static let purple = Color(red: 111/255, green: 60/255, blue: 1) + static let cyan = Color(red: 41/255, green: 209/255, blue: 1) +} + +struct AmbientBackground: View { + @Environment(\.accessibilityReduceMotion) private var reduceMotion + @State private var isMoving = false + + var body: some View { + ZStack { + KTheme.background + RadialGradient( + colors: [KTheme.purple.opacity(0.32), .clear], + center: .topLeading, + startRadius: 10, + endRadius: 420 + ) + .scaleEffect(isMoving ? 1.18 : 0.92) + .offset(x: isMoving ? 36 : -28, y: isMoving ? 70 : -50) + + Circle() + .fill(KTheme.pink.opacity(0.22)) + .frame(width: 260, height: 260) + .blur(radius: 42) + .offset(x: isMoving ? 150 : 95, y: isMoving ? -250 : -190) + + Circle() + .fill(KTheme.orange.opacity(0.20)) + .frame(width: 220, height: 220) + .blur(radius: 48) + .offset(x: isMoving ? -150 : -90, y: isMoving ? 320 : 250) + + Circle() + .fill(KTheme.cyan.opacity(0.12)) + .frame(width: 180, height: 180) + .blur(radius: 40) + .offset(x: isMoving ? -120 : -165, y: isMoving ? -40 : 80) + + LinearGradient( + colors: [.white.opacity(0.045), .clear, .black.opacity(0.25)], + startPoint: .top, + endPoint: .bottom + ) + } + .ignoresSafeArea() + .onAppear { + guard !reduceMotion else { return } + withAnimation(.easeInOut(duration: 9).repeatForever(autoreverses: true)) { + isMoving = true + } + } + } } struct PanelModifier: ViewModifier { func body(content: Content) -> some View { content .padding(18) - .background(KTheme.panel) - .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous)) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 22, style: .continuous)) + .overlay { + RoundedRectangle(cornerRadius: 22, style: .continuous) + .stroke( + LinearGradient( + colors: [.white.opacity(0.30), .white.opacity(0.06), KTheme.pink.opacity(0.16)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: 1 + ) + } + .shadow(color: .black.opacity(0.22), radius: 24, y: 14) } } extension View { func kasztelanPanel() -> some View { modifier(PanelModifier()) } + + func glassSurface(cornerRadius: CGFloat = 18) -> some View { + background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) + .overlay { + RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) + .stroke(.white.opacity(0.20), lineWidth: 1) + } + } } struct PrimaryButtonStyle: ButtonStyle { @@ -27,8 +97,37 @@ struct PrimaryButtonStyle: ButtonStyle { .font(.headline) .foregroundStyle(.white) .frame(maxWidth: .infinity, minHeight: 54) - .background(KTheme.pink.opacity(configuration.isPressed ? 0.72 : 1)) + .background( + LinearGradient( + colors: [KTheme.pink, KTheme.purple, KTheme.orange], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .opacity(configuration.isPressed ? 0.72 : 1) + ) .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) + .overlay { + RoundedRectangle(cornerRadius: 16, style: .continuous) + .stroke(.white.opacity(configuration.isPressed ? 0.12 : 0.30), lineWidth: 1) + } + .shadow(color: KTheme.pink.opacity(configuration.isPressed ? 0.10 : 0.30), radius: 18, y: 9) + .scaleEffect(configuration.isPressed ? 0.975 : 1) + .animation(.spring(response: 0.24, dampingFraction: 0.72), value: configuration.isPressed) + } +} + +struct GlassButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .foregroundStyle(.white) + .frame(maxWidth: .infinity, minHeight: 52) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16, style: .continuous)) + .overlay { + RoundedRectangle(cornerRadius: 16, style: .continuous) + .stroke(.white.opacity(configuration.isPressed ? 0.10 : 0.24), lineWidth: 1) + } + .scaleEffect(configuration.isPressed ? 0.975 : 1) + .animation(.spring(response: 0.24, dampingFraction: 0.72), value: configuration.isPressed) } } @@ -56,8 +155,12 @@ struct KasztelanField: View { .autocorrectionDisabled(keyboard == .emailAddress) .padding(.horizontal, 16) .frame(maxWidth: .infinity, minHeight: 54) - .background(KTheme.panel) - .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 14, style: .continuous)) + .overlay { + RoundedRectangle(cornerRadius: 14, style: .continuous) + .stroke(isFocused ? KTheme.orange.opacity(0.75) : .white.opacity(0.18), lineWidth: 1) + } + .shadow(color: isFocused ? KTheme.orange.opacity(0.14) : .clear, radius: 12) .foregroundStyle(.white) .toolbar { ToolbarItemGroup(placement: .keyboard) { diff --git a/Kasztelan/WalletView.swift b/Kasztelan/WalletView.swift index d3bd74a..a6279bb 100644 --- a/Kasztelan/WalletView.swift +++ b/Kasztelan/WalletView.swift @@ -31,18 +31,28 @@ struct WalletView: View { private var walletCarousel: some View { GeometryReader { geometry in - AnyView(currentPage) - .id(pageIdentity) - .offset(x: dragOffset) - .offset(y: carouselArc(width: geometry.size.width)) - .scaleEffect(carouselScale(width: geometry.size.width)) - .rotation3DEffect(carouselAngle(width: geometry.size.width), axis: (x: 0, y: 1, z: 0), perspective: 0.9) - .opacity(carouselOpacity(width: geometry.size.width)) - .shadow(color: .black.opacity(carouselShadow), radius: 24, x: dragOffset < 0 ? 12 : -12) - .contentShape(Rectangle()) - .simultaneousGesture(carouselGesture(width: geometry.size.width)) - .overlay(alignment: .bottom) { pageIndicator } - .clipped() + ZStack { + AmbientBackground() + WalletMotionBubbles(progress: carouselProgress(width: geometry.size.width)) + + AnyView(currentPage) + .id(pageIdentity) + .offset(x: dragOffset) + .offset(y: carouselArc(width: geometry.size.width)) + .scaleEffect(carouselScale(width: geometry.size.width)) + .rotation3DEffect( + carouselAngle(width: geometry.size.width), + axis: (x: 0.08, y: 1, z: 0), + perspective: 0.62 + ) + .rotationEffect(.degrees(Double(carouselProgress(width: geometry.size.width)) * -2.4)) + .opacity(carouselOpacity(width: geometry.size.width)) + .shadow(color: KTheme.purple.opacity(carouselShadow), radius: 32, x: dragOffset < 0 ? 16 : -16, y: 12) + .contentShape(Rectangle()) + .simultaneousGesture(carouselGesture(width: geometry.size.width)) + } + .overlay(alignment: .bottom) { pageIndicator } + .clipped() } } @@ -93,23 +103,27 @@ struct WalletView: View { } private func carouselScale(width: CGFloat) -> CGFloat { - 1 - min(abs(dragOffset) / max(width, 1), 1) * 0.14 + 1 - min(abs(carouselProgress(width: width)), 1) * 0.10 } private func carouselAngle(width: CGFloat) -> Angle { - .degrees(Double(dragOffset / max(width, 1)) * 68) + .degrees(Double(carouselProgress(width: width)) * 54) } private func carouselArc(width: CGFloat) -> CGFloat { - let progress = min(abs(dragOffset) / max(width, 1), 1) - return progress * progress * 34 + let progress = min(abs(carouselProgress(width: width)), 1) + return progress * progress * 24 } private func carouselOpacity(width: CGFloat) -> Double { - 1 - min(Double(abs(dragOffset) / max(width, 1)), 1) * 0.28 + 1 - min(Double(abs(carouselProgress(width: width))), 1) * 0.22 } - private var carouselShadow: Double { min(Double(abs(dragOffset) / 520), 0.42) } + private func carouselProgress(width: CGFloat) -> CGFloat { + dragOffset / max(width, 1) + } + + private var carouselShadow: Double { 0.12 + min(Double(abs(dragOffset) / 520), 0.34) } private func carouselGesture(width: CGFloat) -> some Gesture { DragGesture(minimumDistance: 18) @@ -163,8 +177,8 @@ struct WalletView: View { let predicted = value.predictedEndTranslation.width guard horizontal, max(abs(value.translation.width), abs(predicted)) > max(52, width * 0.15) else { changingPage = true - withAnimation(.easeOut(duration: 0.14)) { dragOffset = 0 } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { changingPage = false } + withAnimation(.spring(response: 0.34, dampingFraction: 0.82)) { dragOffset = 0 } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { changingPage = false } return } changingPage = true @@ -174,15 +188,53 @@ struct WalletView: View { instant.disablesAnimations = true withTransaction(instant) { selectedPage = (selectedPage + direction + pageCount) % pageCount - dragOffset = direction > 0 ? width * 0.72 : -width * 0.72 + dragOffset = direction > 0 ? width * 0.82 : -width * 0.82 } DispatchQueue.main.async { - withAnimation(.easeOut(duration: 0.26)) { dragOffset = 0 } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.27) { changingPage = false } + withAnimation(.spring(response: 0.48, dampingFraction: 0.84, blendDuration: 0.08)) { dragOffset = 0 } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.50) { changingPage = false } } } } +private struct WalletMotionBubbles: View { + let progress: CGFloat + + var body: some View { + ZStack { + bubble(size: 112, tint: KTheme.pink.opacity(0.20)) + .offset(x: -145 + progress * 90, y: -230 - abs(progress) * 24) + bubble(size: 74, tint: KTheme.orange.opacity(0.22)) + .offset(x: 145 + progress * 55, y: -80 + progress * 42) + bubble(size: 148, tint: KTheme.purple.opacity(0.17)) + .offset(x: 125 - progress * 100, y: 255) + bubble(size: 48, tint: KTheme.cyan.opacity(0.18)) + .offset(x: -120 - progress * 50, y: 150 + progress * 30) + } + .blur(radius: abs(progress) * 1.5) + .allowsHitTesting(false) + } + + private func bubble(size: CGFloat, tint: Color) -> some View { + Circle() + .fill(.ultraThinMaterial) + .frame(width: size, height: size) + .overlay { + Circle() + .fill( + RadialGradient( + colors: [.white.opacity(0.28), tint, .clear], + center: .topLeading, + startRadius: 1, + endRadius: size + ) + ) + } + .overlay { Circle().stroke(.white.opacity(0.14), lineWidth: 1) } + .shadow(color: tint, radius: 24) + } +} + private struct AddDiscountCardView: View { @EnvironmentObject private var model: AppModel let scan: () -> Void @@ -209,7 +261,7 @@ private struct AddDiscountCardView: View { } .padding(.bottom, 34) .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(KTheme.background) + .background(.clear) } } @@ -249,12 +301,26 @@ private struct DiscountCardView: View { } .frame(maxWidth: .infinity) .padding(22) - .background(LinearGradient( - colors: [KTheme.purple, KTheme.pink, KTheme.orange], - startPoint: .topLeading, - endPoint: .bottomTrailing - )) + .background( + ZStack { + LinearGradient( + colors: [KTheme.purple.opacity(0.92), KTheme.pink.opacity(0.88), KTheme.orange.opacity(0.86)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + Circle() + .fill(.white.opacity(0.15)) + .frame(width: 170, height: 170) + .blur(radius: 8) + .offset(x: 145, y: -100) + } + ) .clipShape(RoundedRectangle(cornerRadius: 26, style: .continuous)) + .overlay { + RoundedRectangle(cornerRadius: 26, style: .continuous) + .stroke(.white.opacity(0.28), lineWidth: 1) + } + .shadow(color: KTheme.pink.opacity(0.24), radius: 28, y: 16) Text(model.text("Dostępna offline • zaszyfrowana synchronizacja", "Available offline • encrypted sync")) .font(.footnote) @@ -264,7 +330,7 @@ private struct DiscountCardView: View { } .padding(.horizontal, 20) .padding(.vertical, 14) - .background(KTheme.background) + .background(.clear) .alert(model.text("Usunąć kartę?", "Delete this card?"), isPresented: $confirmingDelete) { Button(model.text("Anuluj", "Cancel"), role: .cancel) {} Button(model.text("Usuń", "Delete"), role: .destructive, action: delete) @@ -294,7 +360,7 @@ private struct CardNameSheet: View { Spacer() } .padding(20) - .background(KTheme.background) + .background(AmbientBackground()) .toolbar { ToolbarItem(placement: .cancellationAction) { Button(model.text("Anuluj", "Cancel")) { dismiss() }