This commit is contained in:
Daniel Arantes Loverde
2026-07-07 15:11:31 -03:00
parent 8b9ebdcb44
commit e51c99973f
293 changed files with 457 additions and 4919 deletions

View File

@@ -0,0 +1,203 @@
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<Content: View>: 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 onSetDefault: (() -> Void)? = nil
private var hasActions: Bool { onEdit != nil || onSetDefault != 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 hasActions {
Rectangle()
.fill(AppColors.backgroundLight)
.frame(width: 1, height: 96)
}
VStack(spacing: 20) {
if let onEdit {
Button(action: onEdit) {
Image(systemName: "pencil")
.font(.system(size: 20))
.foregroundStyle(AppColors.textMuted)
}
}
if let onSetDefault, item.isPrimary == false {
Button(action: onSetDefault) {
Image(systemName: "star")
.font(.system(size: 20))
.foregroundStyle(AppColors.textMuted)
}
}
}
.frame(width: hasActions ? 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
}