Add shared utilities and refactor iOS codebase for DRY compliance

Create centralized shared utilities in iosApp/Shared/:
- Extensions: ViewExtensions, DateExtensions, StringExtensions, DoubleExtensions
- Components: FormComponents, SharedEmptyStateView, ButtonStyles
- Modifiers: CardModifiers
- Utilities: ValidationHelpers, ErrorMessages

Migrate existing views to use shared utilities:
- LoginView: Use IconTextField, FieldLabel, FieldError, OrganicPrimaryButton
- TaskFormView: Use .loadingOverlay() modifier
- TaskCard/DynamicTaskCard: Use .toFormattedDate() extension
- CompletionCardView: Use .toCurrency() (with KotlinDouble support)
- ResidenceDetailView: Use OrganicEmptyState, StandardLoadingView
- Profile views: Use .standardFormStyle(), .sectionBackground()
- Form views: Use consistent form styling modifiers

Benefits:
- Eliminates ~180 lines of duplicate code
- Consistent styling across all forms and components
- KotlinDouble extensions for seamless KMM interop
- Single source of truth for UI patterns

🤖 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-17 13:19:59 -06:00
parent 44c7b23cc2
commit 42eda6a8c8
28 changed files with 3607 additions and 287 deletions

View File

@@ -67,9 +67,7 @@ struct TaskFormView: View {
_inProgress = State(initialValue: task.inProgress)
// Parse date from string - use effective due date (nextDueDate if set, otherwise dueDate)
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
_dueDate = State(initialValue: formatter.date(from: task.effectiveDueDate ?? "") ?? Date())
_dueDate = State(initialValue: (task.effectiveDueDate ?? "").toDate() ?? Date())
_intervalDays = State(initialValue: task.customIntervalDays != nil ? String(task.customIntervalDays!.int32Value) : "")
_estimatedCost = State(initialValue: task.estimatedCost != nil ? String(task.estimatedCost!.doubleValue) : "")
@@ -97,11 +95,7 @@ struct TaskFormView: View {
var body: some View {
NavigationStack {
ZStack {
WarmGradientBackground()
.ignoresSafeArea()
Form {
Form {
// Residence Picker (only if needed)
if needsResidenceSelection, let residences = residences {
Section {
@@ -113,9 +107,7 @@ struct TaskFormView: View {
}
if !residenceError.isEmpty {
Text(residenceError)
.font(.caption)
.foregroundColor(Color.appError)
FieldError(message: residenceError)
}
} header: {
Text(L10n.Tasks.property)
@@ -124,7 +116,7 @@ struct TaskFormView: View {
.font(.caption)
.foregroundColor(Color.appError)
}
.listRowBackground(Color.appBackgroundSecondary)
.sectionBackground()
}
// Browse Templates Button (only for new tasks)
@@ -169,7 +161,7 @@ struct TaskFormView: View {
.font(.system(size: 12, weight: .medium))
.foregroundColor(Color.appTextSecondary)
}
.listRowBackground(Color.appBackgroundSecondary)
.sectionBackground()
}
Section {
@@ -192,9 +184,7 @@ struct TaskFormView: View {
}
if !titleError.isEmpty {
Text(titleError)
.font(.caption)
.foregroundColor(Color.appError)
FieldError(message: titleError)
}
TextField(L10n.Tasks.descriptionOptional, text: $description, axis: .vertical)
@@ -208,7 +198,7 @@ struct TaskFormView: View {
.font(.caption)
.foregroundColor(Color.appError)
}
.listRowBackground(Color.appBackgroundSecondary)
.sectionBackground()
Section {
Picker(L10n.Tasks.category, selection: $selectedCategory) {
@@ -224,7 +214,7 @@ struct TaskFormView: View {
.font(.caption)
.foregroundColor(Color.appError)
}
.listRowBackground(Color.appBackgroundSecondary)
.sectionBackground()
Section {
Picker(L10n.Tasks.frequency, selection: $selectedFrequency) {
@@ -262,7 +252,7 @@ struct TaskFormView: View {
.foregroundColor(Color.appError)
}
}
.listRowBackground(Color.appBackgroundSecondary)
.sectionBackground()
Section {
Picker(L10n.Tasks.priority, selection: $selectedPriority) {
@@ -280,14 +270,14 @@ struct TaskFormView: View {
.font(.caption)
.foregroundColor(Color.appError)
}
.listRowBackground(Color.appBackgroundSecondary)
.sectionBackground()
Section(header: Text(L10n.Tasks.cost)) {
TextField(L10n.Tasks.estimatedCost, text: $estimatedCost)
.keyboardType(.decimalPad)
.focused($focusedField, equals: .estimatedCost)
}
.listRowBackground(Color.appBackgroundSecondary)
.sectionBackground()
.keyboardDismissToolbar()
if let errorMessage = viewModel.errorMessage {
@@ -296,43 +286,11 @@ struct TaskFormView: View {
.foregroundColor(Color.appError)
.font(.caption)
}
.listRowBackground(Color.appBackgroundSecondary)
.sectionBackground()
}
}
.disabled(isLoadingLookups)
.blur(radius: isLoadingLookups ? 3 : 0)
if isLoadingLookups {
VStack(spacing: OrganicSpacing.comfortable) {
ZStack {
Circle()
.fill(Color.appPrimary.opacity(0.1))
.frame(width: 64, height: 64)
ProgressView()
.scaleEffect(1.2)
.tint(Color.appPrimary)
}
Text(L10n.Tasks.loading)
.font(.system(size: 15, weight: .medium, design: .rounded))
.foregroundColor(Color.appTextSecondary)
}
.padding(OrganicSpacing.spacious)
.background(
RoundedRectangle(cornerRadius: 24, style: .continuous)
.fill(Color.appBackgroundSecondary)
.overlay(
GrainTexture(opacity: 0.015)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
)
)
.naturalShadow(.medium)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.appBackgroundPrimary.opacity(0.9))
}
}
.listStyle(.plain)
.scrollContentBackground(.hidden)
.background(Color.clear)
.loadingOverlay(isLoading: isLoadingLookups, message: L10n.Tasks.loading)
.standardFormStyle()
.navigationTitle(isEditMode ? L10n.Tasks.editTitle : L10n.Tasks.addTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
@@ -498,10 +456,8 @@ struct TaskFormView: View {
return
}
// Format date as yyyy-MM-dd
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let dueDateString = dateFormatter.string(from: dueDate)
// Format date as yyyy-MM-dd using extension
let dueDateString = dueDate.formattedAPI()
if isEditMode, let task = existingTask {
// UPDATE existing task