import SwiftUI #if canImport(UIKit) import UIKit #endif func formatZipCodeBR(_ input: String) -> String { let digits = input.filter(\.isNumber) let limited = String(digits.prefix(8)) if limited.count <= 5 { return limited } let prefix = String(limited.prefix(5)) let suffix = String(limited.dropFirst(5)) return "\(prefix)-\(suffix)" } func normalizeZipCodeForAPI(_ input: String) -> String { String(input.filter(\.isNumber).prefix(8)) } func triggerLightHaptic() { #if canImport(UIKit) UIImpactFeedbackGenerator(style: .light).impactOccurred() #endif } func triggerSelectionHaptic() { #if canImport(UIKit) UISelectionFeedbackGenerator().selectionChanged() #endif } struct SwipeToDeleteAddressRow: View { let rowId: String @Binding var openRowId: String? let isDeleting: Bool let onDelete: () -> Void @ViewBuilder var content: () -> Content @State var contentOffset: CGFloat = 0 private let deleteWidth: CGFloat = 92 private let openThreshold: CGFloat = 32 private var showsDeleteAction: Bool { contentOffset < -2 || openRowId == rowId } var body: some View { ZStack(alignment: .trailing) { HStack(spacing: 0) { Spacer(minLength: 0) Button(action: { triggerLightHaptic() onDelete() }) { VStack(spacing: 8) { Image(systemName: "trash.fill") .font(.system(size: 20, weight: .semibold)) Text(isDeleting ? "..." : "Excluir") .font(AppTypography.overline) } .foregroundStyle(AppColors.textInverse) .frame(width: deleteWidth) .frame(maxHeight: .infinity) .background(Color.red) } .buttonStyle(.plain) .disabled(isDeleting) .opacity(showsDeleteAction ? 1 : 0) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusXL, style: .continuous)) } content() .offset(x: contentOffset) .gesture( DragGesture(minimumDistance: 8) .onChanged { value in guard isDeleting == false else { return } if value.translation.width < 0 { contentOffset = max(-deleteWidth, value.translation.width) } else if openRowId == rowId { contentOffset = min(0, -deleteWidth + value.translation.width) } } .onEnded { _ in guard isDeleting == false else { return } if contentOffset <= -openThreshold { let wasClosed = openRowId != rowId contentOffset = -deleteWidth openRowId = rowId if wasClosed { triggerSelectionHaptic() } } else { contentOffset = 0 if openRowId == rowId { openRowId = nil } } } ) .animation(.easeOut(duration: 0.18), value: contentOffset) } .clipped() .animation(.easeOut(duration: 0.18), value: showsDeleteAction) .onChange(of: openRowId) { _, newValue in if newValue != rowId { contentOffset = 0 } } .onChange(of: isDeleting) { _, newValue in if newValue { contentOffset = 0 } } } } struct AddressCard: View { let item: AddressListItem var onEdit: (() -> Void)? = nil var onDelete: (() -> Void)? = nil var body: some View { HStack(spacing: 14) { icon VStack(alignment: .leading, spacing: 8) { HStack(spacing: 10) { Text(item.title) .font(AppTypography.heading2) .foregroundStyle(AppColors.textPrimary) .lineLimit(1) .minimumScaleFactor(0.9) if item.isPrimary { Text("PRINCIPAL") .font(AppTypography.overline) .foregroundStyle(AppColors.textPrimary) .lineLimit(1) .padding(EdgeInsets(top: 5, leading: 9, bottom: 5, trailing: 9)) .background(AppColors.tertiary) .clipShape(Capsule()) } } Text(item.detail) .font(AppTypography.body) .foregroundStyle(AppColors.textMuted) .lineLimit(2) } Spacer(minLength: 8) if onEdit != nil || onDelete != nil { Rectangle() .fill(AppColors.backgroundLight) .frame(width: 1, height: 96) } VStack(spacing: 24) { if let onEdit { Button(action: onEdit) { Image(systemName: "pencil") .font(.system(size: 22)) .foregroundStyle(AppColors.textMuted) } } if let onDelete { Button(action: onDelete) { Image(systemName: "trash") .font(.system(size: 22)) .foregroundStyle(AppColors.textMuted) } } } .frame(width: onEdit != nil || onDelete != nil ? 40 : 0) } .padding(.horizontal, 16) .padding(.vertical, 20) .background(AppColors.surface) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusXL, style: .continuous)) } var icon: some View { Image(systemName: item.icon) .font(.system(size: 28)) .foregroundStyle(item.isPrimary ? AppColors.primary : AppColors.textPrimary) .frame(width: 84, height: 84) .background(item.isPrimary ? AppColors.brandSoft : AppColors.backgroundLight) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) } } struct AddressListItem: Identifiable { let id = UUID() let title: String let detail: String let icon: String let isPrimary: Bool }