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:
Trey t
2025-12-16 20:15:32 -06:00
parent 67f8dcc80f
commit 3598a8d57f
15 changed files with 2318 additions and 703 deletions

View File

@@ -10,15 +10,20 @@ struct CompletionHistorySheet: View {
var body: some View {
NavigationStack {
Group {
if viewModel.isLoadingCompletions {
loadingView
} else if let error = viewModel.completionsError {
errorView(error)
} else if viewModel.completions.isEmpty {
emptyView
} else {
completionsList
ZStack {
WarmGradientBackground()
.ignoresSafeArea()
Group {
if viewModel.isLoadingCompletions {
loadingView
} else if let error = viewModel.completionsError {
errorView(error)
} else if viewModel.completions.isEmpty {
emptyView
} else {
completionsList
}
}
}
.navigationTitle(L10n.Tasks.completionHistory)
@@ -28,9 +33,9 @@ struct CompletionHistorySheet: View {
Button(L10n.Common.done) {
isPresented = false
}
.font(.system(size: 16, weight: .semibold, design: .rounded))
}
}
.background(Color.appBackgroundPrimary)
}
.onAppear {
viewModel.loadCompletions(taskId: taskId)
@@ -43,56 +48,80 @@ struct CompletionHistorySheet: View {
// MARK: - Subviews
private var loadingView: some View {
VStack(spacing: AppSpacing.md) {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: Color.appPrimary))
.scaleEffect(1.5)
VStack(spacing: OrganicSpacing.comfortable) {
ZStack {
Circle()
.fill(Color.appPrimary.opacity(0.1))
.frame(width: 64, height: 64)
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: Color.appPrimary))
.scaleEffect(1.2)
}
Text(L10n.Tasks.loadingCompletions)
.font(.subheadline)
.font(.system(size: 15, weight: .medium, design: .rounded))
.foregroundColor(Color.appTextSecondary)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
private func errorView(_ error: String) -> some View {
VStack(spacing: AppSpacing.md) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 48))
.foregroundColor(Color.appError)
VStack(spacing: OrganicSpacing.comfortable) {
ZStack {
Circle()
.fill(Color.appError.opacity(0.1))
.frame(width: 80, height: 80)
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 32, weight: .semibold))
.foregroundColor(Color.appError)
}
Text(L10n.Tasks.failedToLoad)
.font(.headline)
.font(.system(size: 18, weight: .bold, design: .rounded))
.foregroundColor(Color.appTextPrimary)
Text(error)
.font(.subheadline)
.font(.system(size: 14, weight: .medium))
.foregroundColor(Color.appTextSecondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
.padding(.horizontal, OrganicSpacing.spacious)
Button(action: {
viewModel.loadCompletions(taskId: taskId)
}) {
Label(L10n.Common.retry, systemImage: "arrow.clockwise")
.foregroundColor(Color.appPrimary)
HStack(spacing: 8) {
Image(systemName: "arrow.clockwise")
.font(.system(size: 14, weight: .semibold))
Text(L10n.Common.retry)
.font(.system(size: 15, weight: .semibold, design: .rounded))
}
.foregroundColor(Color.appTextOnPrimary)
.padding(.horizontal, OrganicSpacing.comfortable)
.padding(.vertical, 12)
.background(Color.appPrimary)
.clipShape(Capsule())
}
.padding(.top, AppSpacing.sm)
.padding(.top, 8)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
private var emptyView: some View {
VStack(spacing: AppSpacing.md) {
Image(systemName: "checkmark.circle")
.font(.system(size: 48))
.foregroundColor(Color.appTextSecondary.opacity(0.5))
VStack(spacing: OrganicSpacing.comfortable) {
ZStack {
Circle()
.fill(Color.appTextSecondary.opacity(0.08))
.frame(width: 80, height: 80)
Image(systemName: "checkmark.circle")
.font(.system(size: 36, weight: .medium))
.foregroundColor(Color.appTextSecondary.opacity(0.5))
}
Text(L10n.Tasks.noCompletionsYet)
.font(.headline)
.font(.system(size: 18, weight: .bold, design: .rounded))
.foregroundColor(Color.appTextPrimary)
Text(L10n.Tasks.notCompleted)
.font(.subheadline)
.font(.system(size: 14, weight: .medium))
.foregroundColor(Color.appTextSecondary)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -100,30 +129,46 @@ struct CompletionHistorySheet: View {
private var completionsList: some View {
ScrollView {
VStack(spacing: AppSpacing.sm) {
VStack(spacing: OrganicSpacing.cozy) {
// Task title header
HStack {
Image(systemName: "doc.text")
.foregroundColor(Color.appPrimary)
HStack(spacing: 12) {
ZStack {
Circle()
.fill(Color.appPrimary.opacity(0.12))
.frame(width: 36, height: 36)
Image(systemName: "doc.text")
.font(.system(size: 14, weight: .semibold))
.foregroundColor(Color.appPrimary)
}
Text(taskTitle)
.font(.subheadline)
.fontWeight(.semibold)
.font(.system(size: 15, weight: .semibold, design: .rounded))
.foregroundColor(Color.appTextPrimary)
.lineLimit(2)
Spacer()
Text("\(viewModel.completions.count) \(viewModel.completions.count == 1 ? L10n.Tasks.completion : L10n.Tasks.completions)")
.font(.caption)
.font(.system(size: 12, weight: .medium))
.foregroundColor(Color.appTextSecondary)
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(
Capsule()
.fill(Color.appTextSecondary.opacity(0.1))
)
}
.padding()
.padding(OrganicSpacing.cozy)
.background(Color.appBackgroundSecondary)
.cornerRadius(AppRadius.md)
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
.naturalShadow(.subtle)
// Completions list
ForEach(viewModel.completions, id: \.id) { completion in
CompletionHistoryCard(completion: completion)
}
}
.padding()
.padding(OrganicSpacing.cozy)
}
}
}
@@ -134,20 +179,20 @@ struct CompletionHistoryCard: View {
@State private var showPhotoSheet = false
var body: some View {
VStack(alignment: .leading, spacing: AppSpacing.sm) {
VStack(alignment: .leading, spacing: 14) {
// Header with date and completed by
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(DateUtils.formatDateTimeWithTime(completion.completionDate))
.font(.headline)
.font(.system(size: 16, weight: .bold, design: .rounded))
.foregroundColor(Color.appTextPrimary)
if let completedBy = completion.completedByName, !completedBy.isEmpty {
HStack(spacing: 4) {
HStack(spacing: 5) {
Image(systemName: "person.fill")
.font(.caption2)
.font(.system(size: 10, weight: .medium))
Text("\(L10n.Tasks.completedByName) \(completedBy)")
.font(.caption)
.font(.system(size: 12, weight: .medium))
}
.foregroundColor(Color.appTextSecondary)
}
@@ -157,39 +202,48 @@ struct CompletionHistoryCard: View {
// Rating badge
if let rating = completion.rating {
HStack(spacing: 2) {
HStack(spacing: 4) {
Image(systemName: "star.fill")
.font(.caption)
.font(.system(size: 11, weight: .bold))
Text("\(rating)")
.font(.subheadline)
.fontWeight(.bold)
.font(.system(size: 13, weight: .bold, design: .rounded))
}
.foregroundColor(Color.appAccent)
.padding(.horizontal, 10)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Color.appAccent.opacity(0.1))
.cornerRadius(AppRadius.sm)
.background(
Capsule()
.fill(Color.appAccent.opacity(0.12))
.overlay(
Capsule()
.stroke(Color.appAccent.opacity(0.2), lineWidth: 1)
)
)
}
}
Divider()
OrganicDivider()
// Contractor info
if let contractor = completion.contractorDetails {
HStack(spacing: AppSpacing.sm) {
Image(systemName: "wrench.and.screwdriver.fill")
.foregroundColor(Color.appPrimary)
.frame(width: 24)
HStack(spacing: 12) {
ZStack {
Circle()
.fill(Color.appPrimary.opacity(0.1))
.frame(width: 32, height: 32)
Image(systemName: "wrench.and.screwdriver.fill")
.font(.system(size: 13, weight: .semibold))
.foregroundColor(Color.appPrimary)
}
VStack(alignment: .leading, spacing: 2) {
Text(contractor.name)
.font(.subheadline)
.fontWeight(.medium)
.font(.system(size: 14, weight: .semibold, design: .rounded))
.foregroundColor(Color.appTextPrimary)
if let company = contractor.company {
Text(company)
.font(.caption)
.font(.system(size: 12, weight: .medium))
.foregroundColor(Color.appTextSecondary)
}
}
@@ -198,28 +252,33 @@ struct CompletionHistoryCard: View {
// Cost
if let cost = completion.actualCost {
HStack(spacing: AppSpacing.sm) {
Image(systemName: "dollarsign.circle.fill")
.foregroundColor(Color.appPrimary)
.frame(width: 24)
HStack(spacing: 12) {
ZStack {
Circle()
.fill(Color.appPrimary.opacity(0.1))
.frame(width: 32, height: 32)
Image(systemName: "dollarsign.circle.fill")
.font(.system(size: 14, weight: .semibold))
.foregroundColor(Color.appPrimary)
}
Text("$\(cost)")
.font(.subheadline)
.fontWeight(.semibold)
.font(.system(size: 15, weight: .bold, design: .rounded))
.foregroundColor(Color.appPrimary)
}
}
// Notes
if !completion.notes.isEmpty {
VStack(alignment: .leading, spacing: 4) {
VStack(alignment: .leading, spacing: 6) {
Text(L10n.Tasks.notes)
.font(.caption)
.fontWeight(.semibold)
.font(.system(size: 11, weight: .semibold, design: .rounded))
.foregroundColor(Color.appTextSecondary)
.textCase(.uppercase)
.tracking(0.5)
Text(completion.notes)
.font(.subheadline)
.font(.system(size: 14, weight: .medium))
.foregroundColor(Color.appTextPrimary)
}
.padding(.top, 4)
@@ -230,26 +289,27 @@ struct CompletionHistoryCard: View {
Button(action: {
showPhotoSheet = true
}) {
HStack {
HStack(spacing: 8) {
Image(systemName: "photo.on.rectangle.angled")
.font(.subheadline)
.font(.system(size: 14, weight: .semibold))
Text("\(L10n.Tasks.viewPhotos) (\(completion.images.count))")
.font(.subheadline)
.fontWeight(.semibold)
.font(.system(size: 14, weight: .semibold, design: .rounded))
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(Color.appPrimary.opacity(0.1))
.padding(.vertical, 14)
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(Color.appPrimary.opacity(0.12))
)
.foregroundColor(Color.appPrimary)
.cornerRadius(AppRadius.sm)
}
.padding(.top, 4)
.padding(.top, 6)
}
}
.padding()
.padding(OrganicSpacing.cozy)
.background(Color.appBackgroundSecondary)
.cornerRadius(AppRadius.lg)
.shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
.naturalShadow(.medium)
.sheet(isPresented: $showPhotoSheet) {
PhotoViewerSheet(images: completion.images)
}