Add Warm Organic design system to iOS app
- Add OrganicDesign.swift with reusable components: - WarmGradientBackground, OrganicBlobShape, GrainTexture - OrganicDivider, OrganicCardBackground, NaturalShadow modifier - OrganicSpacing constants (cozy, comfortable, spacious, airy) - Update high-priority screens with organic styling: - LoginView: hero glow, organic card background, rounded fonts - ResidenceDetailView, ResidencesListView: warm backgrounds - ResidenceCard, SummaryCard, PropertyHeaderCard: organic cards - TaskCard: metadata pills, secondary buttons, card background - TaskFormView: organic loading overlay, templates button - CompletionHistorySheet: organic loading/error/empty states - ProfileView, NotificationPreferencesView, ThemeSelectionView - Update task badges with icons and capsule styling: - PriorityBadge: priority-specific icons - StatusBadge: status-specific icons - Fix TaskCard isOverdue error using DateUtils.isOverdue() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -47,8 +47,7 @@ struct ResidenceDetailView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.appBackgroundPrimary
|
||||
.ignoresSafeArea()
|
||||
WarmGradientBackground()
|
||||
|
||||
mainContent
|
||||
}
|
||||
@@ -208,19 +207,19 @@ private extension ResidenceDetailView {
|
||||
|
||||
@ViewBuilder
|
||||
func contentView(for residence: ResidenceResponse) -> some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: OrganicSpacing.comfortable) {
|
||||
PropertyHeaderCard(residence: residence)
|
||||
.padding(.horizontal)
|
||||
.padding(.top)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 8)
|
||||
|
||||
tasksSection
|
||||
.padding(.horizontal)
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
contractorsSection
|
||||
.padding(.horizontal)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
.padding(.bottom)
|
||||
.padding(.bottom, OrganicSpacing.airy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,49 +247,81 @@ private extension ResidenceDetailView {
|
||||
|
||||
@ViewBuilder
|
||||
var contractorsSection: some View {
|
||||
VStack(alignment: .leading, spacing: AppSpacing.md) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
// Section Header
|
||||
HStack(spacing: AppSpacing.sm) {
|
||||
Image(systemName: "person.2.fill")
|
||||
.font(.title2)
|
||||
.foregroundColor(Color.appPrimary)
|
||||
HStack(alignment: .center, spacing: 12) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.appPrimary.opacity(0.12))
|
||||
.frame(width: 40, height: 40)
|
||||
|
||||
Image(systemName: "person.2.fill")
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundColor(Color.appPrimary)
|
||||
}
|
||||
|
||||
Text(L10n.Residences.contractors)
|
||||
.font(.title2.weight(.bold))
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.font(.system(size: 20, weight: .bold, design: .rounded))
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, AppSpacing.sm)
|
||||
.padding(.top, 8)
|
||||
|
||||
if isLoadingContractors {
|
||||
HStack {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
.tint(Color.appPrimary)
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.padding(OrganicSpacing.cozy)
|
||||
} else if let error = contractorsError {
|
||||
Text("\(L10n.Common.error): \(error)")
|
||||
.foregroundColor(Color.appError)
|
||||
.padding()
|
||||
} else if contractors.isEmpty {
|
||||
// Empty state
|
||||
VStack(spacing: AppSpacing.md) {
|
||||
Image(systemName: "person.crop.circle.badge.plus")
|
||||
.font(.system(size: 48))
|
||||
.foregroundColor(Color.appTextSecondary.opacity(0.6))
|
||||
Text(L10n.Residences.noContractors)
|
||||
.font(.headline)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
Text(L10n.Residences.addContractorsPrompt)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
// Empty state with organic styling
|
||||
VStack(spacing: 16) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(
|
||||
RadialGradient(
|
||||
colors: [
|
||||
Color.appPrimary.opacity(0.12),
|
||||
Color.appPrimary.opacity(0.04)
|
||||
],
|
||||
center: .center,
|
||||
startRadius: 0,
|
||||
endRadius: 50
|
||||
)
|
||||
)
|
||||
.frame(width: 80, height: 80)
|
||||
|
||||
Image(systemName: "person.crop.circle.badge.plus")
|
||||
.font(.system(size: 32, weight: .medium))
|
||||
.foregroundColor(Color.appPrimary.opacity(0.6))
|
||||
}
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text(L10n.Residences.noContractors)
|
||||
.font(.system(size: 17, weight: .semibold, design: .rounded))
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
Text(L10n.Residences.addContractorsPrompt)
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(AppSpacing.xl)
|
||||
.background(Color.appBackgroundSecondary)
|
||||
.cornerRadius(AppRadius.lg)
|
||||
.padding(OrganicSpacing.spacious)
|
||||
.background(OrganicCardBackground(showBlob: true, blobVariation: 1))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 28, style: .continuous))
|
||||
.naturalShadow(.subtle)
|
||||
} else {
|
||||
// Contractors list
|
||||
VStack(spacing: AppSpacing.sm) {
|
||||
VStack(spacing: 12) {
|
||||
ForEach(contractors, id: \.id) { contractor in
|
||||
NavigationLink(destination: ContractorDetailView(contractorId: contractor.id)) {
|
||||
ContractorCard(
|
||||
@@ -300,7 +331,7 @@ private extension ResidenceDetailView {
|
||||
}
|
||||
)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.buttonStyle(OrganicCardButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,6 +339,17 @@ private extension ResidenceDetailView {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Organic Card Button Style
|
||||
|
||||
private struct OrganicCardButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.scaleEffect(configuration.isPressed ? 0.98 : 1.0)
|
||||
.opacity(configuration.isPressed ? 0.9 : 1.0)
|
||||
.animation(.easeOut(duration: 0.15), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Toolbars
|
||||
|
||||
private extension ResidenceDetailView {
|
||||
|
||||
@@ -14,8 +14,8 @@ struct ResidencesListView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.appBackgroundPrimary
|
||||
.ignoresSafeArea()
|
||||
// Warm organic background
|
||||
WarmGradientBackground()
|
||||
|
||||
if let response = viewModel.myResidences {
|
||||
ListAsyncContentView(
|
||||
@@ -29,7 +29,7 @@ struct ResidencesListView: View {
|
||||
)
|
||||
},
|
||||
emptyContent: {
|
||||
EmptyResidencesView()
|
||||
OrganicEmptyResidencesView()
|
||||
},
|
||||
onRefresh: {
|
||||
viewModel.loadMyResidences(forceRefresh: true)
|
||||
@@ -53,9 +53,7 @@ struct ResidencesListView: View {
|
||||
Button(action: {
|
||||
showingSettings = true
|
||||
}) {
|
||||
Image(systemName: "gearshape.fill")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundColor(Color.appPrimary)
|
||||
OrganicToolbarButton(systemName: "gearshape.fill")
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Navigation.settingsButton)
|
||||
}
|
||||
@@ -70,9 +68,7 @@ struct ResidencesListView: View {
|
||||
showingJoinResidence = true
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "person.badge.plus")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundColor(Color.appPrimary)
|
||||
OrganicToolbarButton(systemName: "person.badge.plus")
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
@@ -84,9 +80,7 @@ struct ResidencesListView: View {
|
||||
showingAddResidence = true
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundColor(Color.appPrimary)
|
||||
OrganicToolbarButton(systemName: "plus", isPrimary: true)
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.addButton)
|
||||
}
|
||||
@@ -148,6 +142,35 @@ struct ResidencesListView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Organic Toolbar Button
|
||||
|
||||
private struct OrganicToolbarButton: View {
|
||||
let systemName: String
|
||||
var isPrimary: Bool = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if isPrimary {
|
||||
Circle()
|
||||
.fill(Color.appPrimary)
|
||||
.frame(width: 32, height: 32)
|
||||
|
||||
Image(systemName: systemName)
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.foregroundColor(Color.appTextOnPrimary)
|
||||
} else {
|
||||
Circle()
|
||||
.fill(Color.appPrimary.opacity(0.1))
|
||||
.frame(width: 32, height: 32)
|
||||
|
||||
Image(systemName: systemName)
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundColor(Color.appPrimary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Residences Content View
|
||||
|
||||
private struct ResidencesContent: View {
|
||||
@@ -156,36 +179,51 @@ private struct ResidencesContent: View {
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: AppSpacing.lg) {
|
||||
// Summary Card
|
||||
VStack(spacing: OrganicSpacing.comfortable) {
|
||||
// Summary Card with enhanced styling
|
||||
SummaryCard(summary: summary)
|
||||
.padding(.horizontal, AppSpacing.md)
|
||||
.padding(.top, AppSpacing.sm)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 8)
|
||||
|
||||
// Properties Header
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: AppSpacing.xxs) {
|
||||
// Properties Section Header
|
||||
HStack(alignment: .center) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(L10n.Residences.yourProperties)
|
||||
.font(.title3.weight(.semibold))
|
||||
.font(.system(size: 20, weight: .bold, design: .rounded))
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
Text("\(residences.count) \(residences.count == 1 ? L10n.Residences.property : L10n.Residences.properties)")
|
||||
.font(.callout)
|
||||
.font(.system(size: 13, weight: .medium, design: .rounded))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, AppSpacing.md)
|
||||
|
||||
// Residences List
|
||||
ForEach(residences, id: \.id) { residence in
|
||||
NavigationLink(destination: ResidenceDetailView(residenceId: residence.id)) {
|
||||
ResidenceCard(residence: residence)
|
||||
.padding(.horizontal, AppSpacing.md)
|
||||
Spacer()
|
||||
|
||||
// Decorative leaf
|
||||
Image(systemName: "leaf.fill")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(Color.appPrimary.opacity(0.3))
|
||||
.rotationEffect(.degrees(-15))
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 8)
|
||||
|
||||
// Residences List with staggered animation
|
||||
LazyVStack(spacing: 16) {
|
||||
ForEach(Array(residences.enumerated()), id: \.element.id) { index, residence in
|
||||
NavigationLink(destination: ResidenceDetailView(residenceId: residence.id)) {
|
||||
ResidenceCard(residence: residence)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
.buttonStyle(OrganicCardButtonStyle())
|
||||
.transition(.asymmetric(
|
||||
insertion: .opacity.combined(with: .move(edge: .bottom)),
|
||||
removal: .opacity
|
||||
))
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
.padding(.bottom, AppSpacing.xxxl)
|
||||
.padding(.bottom, OrganicSpacing.airy)
|
||||
}
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
Color.clear.frame(height: 0)
|
||||
@@ -193,6 +231,97 @@ private struct ResidencesContent: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Organic Card Button Style
|
||||
|
||||
private struct OrganicCardButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.scaleEffect(configuration.isPressed ? 0.98 : 1.0)
|
||||
.opacity(configuration.isPressed ? 0.9 : 1.0)
|
||||
.animation(.easeOut(duration: 0.15), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Organic Empty Residences View
|
||||
|
||||
private struct OrganicEmptyResidencesView: View {
|
||||
@State private var isAnimating = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: OrganicSpacing.comfortable) {
|
||||
Spacer()
|
||||
|
||||
// Animated house illustration
|
||||
ZStack {
|
||||
// Background glow
|
||||
Circle()
|
||||
.fill(
|
||||
RadialGradient(
|
||||
colors: [
|
||||
Color.appPrimary.opacity(0.15),
|
||||
Color.appPrimary.opacity(0.05),
|
||||
Color.clear
|
||||
],
|
||||
center: .center,
|
||||
startRadius: 0,
|
||||
endRadius: 80
|
||||
)
|
||||
)
|
||||
.frame(width: 160, height: 160)
|
||||
.scaleEffect(isAnimating ? 1.1 : 1.0)
|
||||
.animation(
|
||||
Animation.easeInOut(duration: 3).repeatForever(autoreverses: true),
|
||||
value: isAnimating
|
||||
)
|
||||
|
||||
// House icon
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.appPrimary.opacity(0.1))
|
||||
.frame(width: 100, height: 100)
|
||||
|
||||
Image(systemName: "house.lodge.fill")
|
||||
.font(.system(size: 44, weight: .medium))
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.offset(y: isAnimating ? -2 : 2)
|
||||
.animation(
|
||||
Animation.easeInOut(duration: 2).repeatForever(autoreverses: true),
|
||||
value: isAnimating
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(spacing: 12) {
|
||||
Text("Welcome to Your Space")
|
||||
.font(.system(size: 24, weight: .bold, design: .rounded))
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
Text("Add your first property to start\nmanaging your home with ease")
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineSpacing(4)
|
||||
}
|
||||
.padding(.top, 8)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Decorative footer elements
|
||||
HStack(spacing: 40) {
|
||||
FloatingLeaf(delay: 0, size: 18, color: Color.appPrimary)
|
||||
FloatingLeaf(delay: 0.5, size: 14, color: Color.appAccent)
|
||||
FloatingLeaf(delay: 1.0, size: 20, color: Color.appPrimary)
|
||||
}
|
||||
.opacity(0.6)
|
||||
.padding(.bottom, 40)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.onAppear {
|
||||
isAnimating = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationView {
|
||||
ResidencesListView()
|
||||
|
||||
Reference in New Issue
Block a user