From 61ab95d1086eafe07921a3aa16370a9a563919ad Mon Sep 17 00:00:00 2001 From: Trey t Date: Thu, 5 Mar 2026 16:00:40 -0600 Subject: [PATCH] Polish UI consistency across all CRUD forms and fix data display issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rewrite ResidenceFormView to use standard Form/Section pattern matching TaskFormView - Remove unused organic form components (OrganicFormSection, OrganicFormTextField, etc.) - Fix DocumentFormView: NavigationView→NavigationStack, WarmGradientBackground→appBackgroundPrimary, listRowBackground→sectionBackground - Add Required footer to residence name field and task title/property fields - Remove redundant Required footers from pickers that always have values - Fix grey priority dots on kanban cards by guarding PriorityBadge in DynamicTaskCard and TaskCard - Fix empty frequency labels showing on task cards - Fix contractor Maps URL building to filter empty strings Co-Authored-By: Claude Opus 4.6 --- .../Contractor/ContractorDetailView.swift | 4 +- .../iosApp/Documents/DocumentFormView.swift | 20 +- iosApp/iosApp/Localizable.xcstrings | 9 +- iosApp/iosApp/ResidenceFormView.swift | 645 +++++------------- .../Subviews/Task/DynamicTaskCard.swift | 12 +- iosApp/iosApp/Subviews/Task/TaskCard.swift | 14 +- iosApp/iosApp/Task/TaskFormView.swift | 12 - 7 files changed, 195 insertions(+), 521 deletions(-) diff --git a/iosApp/iosApp/Contractor/ContractorDetailView.swift b/iosApp/iosApp/Contractor/ContractorDetailView.swift index 55a621a..a3fb454 100644 --- a/iosApp/iosApp/Contractor/ContractorDetailView.swift +++ b/iosApp/iosApp/Contractor/ContractorDetailView.swift @@ -320,7 +320,7 @@ struct ContractorDetailView: View { contractor.city, contractor.stateProvince, contractor.postalCode - ].compactMap { $0 }.joined(separator: ", ") + ].compactMap { $0 }.filter { !$0.isEmpty }.joined(separator: ", ") if let encoded = address.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: "maps://?address=\(encoded)") { @@ -430,7 +430,7 @@ struct ContractorDetailView: View { contractor.city, contractor.stateProvince, contractor.postalCode - ].compactMap { $0 }.joined(separator: ", ") + ].compactMap { $0 }.filter { !$0.isEmpty }.joined(separator: ", ") if let encoded = address.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: "maps://?address=\(encoded)") { diff --git a/iosApp/iosApp/Documents/DocumentFormView.swift b/iosApp/iosApp/Documents/DocumentFormView.swift index a63c039..b2b79f7 100644 --- a/iosApp/iosApp/Documents/DocumentFormView.swift +++ b/iosApp/iosApp/Documents/DocumentFormView.swift @@ -140,7 +140,7 @@ struct DocumentFormView: View { .font(.caption) .foregroundColor(Color.appError) } - .listRowBackground(Color.appBackgroundSecondary) + .sectionBackground() Section(L10n.Documents.warrantyClaims) { TextField(L10n.Documents.claimPhoneOptional, text: $claimPhone) @@ -151,14 +151,14 @@ struct DocumentFormView: View { TextField(L10n.Documents.claimWebsiteOptional, text: $claimWebsite) .keyboardType(.URL) } - .listRowBackground(Color.appBackgroundSecondary) + .sectionBackground() Section(L10n.Documents.warrantyDates) { TextField(L10n.Documents.purchaseDate, text: $purchaseDate) TextField(L10n.Documents.startDate, text: $startDate) TextField(L10n.Documents.endDate, text: $endDate) } - .listRowBackground(Color.appBackgroundSecondary) + .sectionBackground() } } @@ -171,7 +171,7 @@ struct DocumentFormView: View { .frame(height: 200) } } - .listRowBackground(Color.appBackgroundSecondary) + .sectionBackground() } Section(L10n.Documents.photos) { @@ -191,17 +191,17 @@ struct DocumentFormView: View { .foregroundColor(.secondary) } } - .listRowBackground(Color.appBackgroundSecondary) + .sectionBackground() } var body: some View { - NavigationView { + NavigationStack { Form { formContent } .listStyle(.plain) .scrollContentBackground(.hidden) - .background(WarmGradientBackground()) + .background(Color.appBackgroundPrimary) .navigationTitle(isEditMode ? (isWarranty ? L10n.Documents.editWarranty : L10n.Documents.editDocument) : (isWarranty ? L10n.Documents.addWarranty : L10n.Documents.addDocument)) .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -315,7 +315,7 @@ struct DocumentFormView: View { } } } - .listRowBackground(Color.appBackgroundSecondary) + .sectionBackground() // Basic Information Section { @@ -336,7 +336,7 @@ struct DocumentFormView: View { .font(.caption) .foregroundColor(Color.appError) } - .listRowBackground(Color.appBackgroundSecondary) + .sectionBackground() // Warranty-specific fields warrantySection @@ -362,7 +362,7 @@ struct DocumentFormView: View { .lineLimit(3...6) .keyboardDismissToolbar() } - .listRowBackground(Color.appBackgroundSecondary) + .sectionBackground() // Active Status (Edit mode only) if isEditMode { diff --git a/iosApp/iosApp/Localizable.xcstrings b/iosApp/iosApp/Localizable.xcstrings index a00c8da..fe49a53 100644 --- a/iosApp/iosApp/Localizable.xcstrings +++ b/iosApp/iosApp/Localizable.xcstrings @@ -1,9 +1,6 @@ { "sourceLanguage" : "en", "strings" : { - "" : { - - }, "*" : { }, @@ -24944,6 +24941,9 @@ "Share this 6-character code. They can enter it in the app to join." : { "comment" : "A description of how to share the invitation code with others.", "isCommentAutoGenerated" : true + }, + "Shared Users (%lld)" : { + }, "Sign in with Google" : { @@ -30148,8 +30148,7 @@ }, "Your Home Dashboard" : { - "comment" : "The title of the main view in the Home app.", - "isCommentAutoGenerated" : true + }, "Your home maintenance companion" : { "comment" : "The tagline for the app, describing its purpose.", diff --git a/iosApp/iosApp/ResidenceFormView.swift b/iosApp/iosApp/ResidenceFormView.swift index eec86a6..152740e 100644 --- a/iosApp/iosApp/ResidenceFormView.swift +++ b/iosApp/iosApp/ResidenceFormView.swift @@ -60,256 +60,201 @@ struct ResidenceFormView: View { var body: some View { NavigationStack { - ZStack { - WarmGradientBackground() + Form { + // Property Details + Section { + TextField(L10n.Residences.propertyName, text: $name) + .focused($focusedField, equals: .name) + .accessibilityIdentifier(AccessibilityIdentifiers.Residence.nameField) - ScrollView(showsIndicators: false) { - VStack(spacing: OrganicSpacing.comfortable) { - // Property Details Section - OrganicFormSection(title: L10n.Residences.propertyDetails, icon: "house_outline") { - VStack(spacing: 16) { - OrganicFormTextField( - label: L10n.Residences.propertyName, - placeholder: "My Home", - text: $name, - error: nameError.isEmpty ? nil : nameError, - accessibilityId: AccessibilityIdentifiers.Residence.nameField - ) - .focused($focusedField, equals: .name) + if !nameError.isEmpty { + FieldError(message: nameError) + } - OrganicFormPicker( - label: L10n.Residences.propertyType, - selection: $selectedPropertyType, - options: residenceTypes, - optionLabel: { $0.name }, - placeholder: L10n.Residences.selectType - ) - .accessibilityIdentifier(AccessibilityIdentifiers.Residence.propertyTypePicker) - } + Picker(L10n.Residences.propertyType, selection: $selectedPropertyType) { + Text(L10n.Residences.selectType).tag(nil as ResidenceType?) + ForEach(residenceTypes, id: \.self) { type in + Text(type.name).tag(type as ResidenceType?) } - .padding(.top, 8) + } + .accessibilityIdentifier(AccessibilityIdentifiers.Residence.propertyTypePicker) + } header: { + Text(L10n.Residences.propertyDetails) + } footer: { + Text(L10n.Residences.nameRequired) + .font(.caption) + .foregroundColor(Color.appError) + } + .sectionBackground() - // Address Section - OrganicFormSection(title: L10n.Residences.address, icon: "mappin.circle.fill") { - VStack(spacing: 16) { - OrganicFormTextField( - label: L10n.Residences.streetAddress, - placeholder: "123 Main St", - text: $streetAddress, - accessibilityId: AccessibilityIdentifiers.Residence.streetAddressField - ) - .focused($focusedField, equals: .streetAddress) + // Address + Section { + TextField(L10n.Residences.streetAddress, text: $streetAddress) + .focused($focusedField, equals: .streetAddress) + .accessibilityIdentifier(AccessibilityIdentifiers.Residence.streetAddressField) - OrganicFormTextField( - label: L10n.Residences.apartmentUnit, - placeholder: "Apt 4B", - text: $apartmentUnit, - accessibilityId: AccessibilityIdentifiers.Residence.apartmentUnitField - ) - .focused($focusedField, equals: .apartmentUnit) + TextField(L10n.Residences.apartmentUnit, text: $apartmentUnit) + .focused($focusedField, equals: .apartmentUnit) + .accessibilityIdentifier(AccessibilityIdentifiers.Residence.apartmentUnitField) - HStack(spacing: 12) { - OrganicFormTextField( - label: L10n.Residences.city, - placeholder: "City", - text: $city, - accessibilityId: AccessibilityIdentifiers.Residence.cityField - ) - .focused($focusedField, equals: .city) + TextField(L10n.Residences.city, text: $city) + .focused($focusedField, equals: .city) + .accessibilityIdentifier(AccessibilityIdentifiers.Residence.cityField) - OrganicFormTextField( - label: L10n.Residences.stateProvince, - placeholder: "State", - text: $stateProvince, - accessibilityId: AccessibilityIdentifiers.Residence.stateProvinceField - ) - .focused($focusedField, equals: .stateProvince) - .frame(maxWidth: 120) - } + TextField(L10n.Residences.stateProvince, text: $stateProvince) + .focused($focusedField, equals: .stateProvince) + .accessibilityIdentifier(AccessibilityIdentifiers.Residence.stateProvinceField) - HStack(spacing: 12) { - OrganicFormTextField( - label: L10n.Residences.postalCode, - placeholder: "12345", - text: $postalCode, - accessibilityId: AccessibilityIdentifiers.Residence.postalCodeField - ) - .focused($focusedField, equals: .postalCode) + TextField(L10n.Residences.postalCode, text: $postalCode) + .focused($focusedField, equals: .postalCode) + .keyboardType(.numberPad) + .accessibilityIdentifier(AccessibilityIdentifiers.Residence.postalCodeField) - OrganicFormTextField( - label: L10n.Residences.country, - placeholder: "USA", - text: $country, - accessibilityId: AccessibilityIdentifiers.Residence.countryField - ) - .focused($focusedField, equals: .country) - } - } - } + TextField(L10n.Residences.country, text: $country) + .focused($focusedField, equals: .country) + .accessibilityIdentifier(AccessibilityIdentifiers.Residence.countryField) + } header: { + Text(L10n.Residences.address) + } + .sectionBackground() - // Property Features Section - OrganicFormSection(title: L10n.Residences.propertyFeatures, icon: "square.grid.2x2.fill") { - VStack(spacing: 16) { - HStack(spacing: 12) { - OrganicFormTextField( - label: L10n.Residences.bedrooms, - placeholder: "0", - text: $bedrooms, - keyboardType: .numberPad, - accessibilityId: AccessibilityIdentifiers.Residence.bedroomsField - ) - .focused($focusedField, equals: .bedrooms) + // Property Features + Section { + TextField(L10n.Residences.bedrooms, text: $bedrooms) + .keyboardType(.numberPad) + .focused($focusedField, equals: .bedrooms) + .accessibilityIdentifier(AccessibilityIdentifiers.Residence.bedroomsField) - OrganicFormTextField( - label: L10n.Residences.bathrooms, - placeholder: "0.0", - text: $bathrooms, - keyboardType: .decimalPad, - accessibilityId: AccessibilityIdentifiers.Residence.bathroomsField - ) - .focused($focusedField, equals: .bathrooms) - } + TextField(L10n.Residences.bathrooms, text: $bathrooms) + .keyboardType(.decimalPad) + .focused($focusedField, equals: .bathrooms) + .accessibilityIdentifier(AccessibilityIdentifiers.Residence.bathroomsField) - HStack(spacing: 12) { - OrganicFormTextField( - label: L10n.Residences.squareFootage, - placeholder: "sq ft", - text: $squareFootage, - keyboardType: .numberPad, - accessibilityId: AccessibilityIdentifiers.Residence.squareFootageField - ) - .focused($focusedField, equals: .squareFootage) + TextField(L10n.Residences.squareFootage, text: $squareFootage) + .keyboardType(.numberPad) + .focused($focusedField, equals: .squareFootage) + .accessibilityIdentifier(AccessibilityIdentifiers.Residence.squareFootageField) - OrganicFormTextField( - label: L10n.Residences.lotSize, - placeholder: "acres", - text: $lotSize, - keyboardType: .decimalPad, - accessibilityId: AccessibilityIdentifiers.Residence.lotSizeField - ) - .focused($focusedField, equals: .lotSize) - } + TextField(L10n.Residences.lotSize, text: $lotSize) + .keyboardType(.decimalPad) + .focused($focusedField, equals: .lotSize) + .accessibilityIdentifier(AccessibilityIdentifiers.Residence.lotSizeField) - OrganicFormTextField( - label: L10n.Residences.yearBuilt, - placeholder: "2020", - text: $yearBuilt, - keyboardType: .numberPad, - accessibilityId: AccessibilityIdentifiers.Residence.yearBuiltField - ) - .focused($focusedField, equals: .yearBuilt) - } - } + TextField(L10n.Residences.yearBuilt, text: $yearBuilt) + .keyboardType(.numberPad) + .focused($focusedField, equals: .yearBuilt) + .accessibilityIdentifier(AccessibilityIdentifiers.Residence.yearBuiltField) + } header: { + Text(L10n.Residences.propertyFeatures) + } + .sectionBackground() - // Additional Details Section - OrganicFormSection(title: L10n.Residences.additionalDetails, icon: "text.alignleft") { - VStack(spacing: 16) { - OrganicFormTextArea( - label: L10n.Residences.description, - placeholder: "Add notes about your property...", - text: $description - ) - .accessibilityIdentifier(AccessibilityIdentifiers.Residence.descriptionField) + // Additional Details + Section { + TextField(L10n.Residences.description, text: $description, axis: .vertical) + .lineLimit(3...6) + .accessibilityIdentifier(AccessibilityIdentifiers.Residence.descriptionField) - OrganicFormToggle( - label: L10n.Residences.primaryResidence, - isOn: $isPrimary, - icon: "star.fill" - ) - .accessibilityIdentifier(AccessibilityIdentifiers.Residence.isPrimaryToggle) - } - } + Toggle(L10n.Residences.primaryResidence, isOn: $isPrimary) + .accessibilityIdentifier(AccessibilityIdentifiers.Residence.isPrimaryToggle) + } header: { + Text(L10n.Residences.additionalDetails) + } + .sectionBackground() - // Users Section (edit mode only, owner only) - if isEditMode && isCurrentUserOwner { - OrganicFormSection(title: "Shared Users (\(users.count))", icon: "person.2.fill") { - VStack(spacing: 12) { - if isLoadingUsers { - HStack { - Spacer() - ProgressView() - Spacer() - } - .padding(.vertical, 20) - } else if users.isEmpty { - Text("No shared users") - .font(.system(size: 14, weight: .medium)) - .foregroundColor(Color.appTextSecondary) - .padding(.vertical, 12) - } else { - ForEach(users, id: \.id) { user in - OrganicUserRow( - user: user, - isOwner: user.id == existingResidence?.ownerId, - onRemove: { - userToRemove = user - showRemoveUserConfirmation = true - } - ) - } - } - - Text("Use the share button to invite others") - .font(.system(size: 12, weight: .medium)) - .foregroundColor(Color.appTextSecondary) - } - } - } - - // Error Message - if let errorMessage = viewModel.errorMessage { - HStack(spacing: 10) { - Image(systemName: "exclamationmark.circle.fill") - .foregroundColor(Color.appError) - Text(errorMessage) - .font(.system(size: 14, weight: .medium)) - .foregroundColor(Color.appError) + // Users Section (edit mode only, owner only) + if isEditMode && isCurrentUserOwner { + Section { + if isLoadingUsers { + HStack { + Spacer() + ProgressView() Spacer() } - .padding(16) - .background(Color.appError.opacity(0.1)) - .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) - .padding(.horizontal, 16) + } else if users.isEmpty { + Text("No shared users") + .foregroundColor(Color.appTextSecondary) + } else { + ForEach(users, id: \.id) { user in + HStack { + VStack(alignment: .leading, spacing: 2) { + HStack(spacing: 6) { + Text(user.username) + .font(.body.weight(.medium)) + if user.id == existingResidence?.ownerId { + Text("Owner") + .font(.caption2.weight(.bold)) + .foregroundColor(Color.appTextOnPrimary) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.appPrimary) + .clipShape(Capsule()) + } + } + if !user.email.isEmpty { + Text(user.email) + .font(.caption) + .foregroundColor(Color.appTextSecondary) + } + } + Spacer() + if user.id != existingResidence?.ownerId { + Button { + userToRemove = user + showRemoveUserConfirmation = true + } label: { + Image(systemName: "trash") + .foregroundColor(Color.appError) + } + .buttonStyle(.plain) + } + } + } } - - Spacer() - .frame(height: 40) + } header: { + Text("Shared Users (\(users.count))") + } footer: { + Text("Use the share button to invite others") + .font(.caption) } - .padding(.horizontal, 16) + .sectionBackground() + } + + // Error Message + if let errorMessage = viewModel.errorMessage { + Section { + HStack(spacing: 8) { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(Color.appError) + Text(errorMessage) + .foregroundColor(Color.appError) + .font(.subheadline) + } + } + .sectionBackground() } - .keyboardDismissToolbar() } + .listStyle(.plain) + .scrollContentBackground(.hidden) + .background(Color.appBackgroundPrimary) .navigationTitle(isEditMode ? L10n.Residences.editTitle : L10n.Residences.addTitle) .navigationBarTitleDisplayMode(.inline) + .keyboardDismissToolbar() .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button(action: { isPresented = false }) { - Image(systemName: "xmark") - .font(.system(size: 14, weight: .semibold)) - .foregroundColor(Color.appTextSecondary) - .padding(8) - .background(Color.appBackgroundSecondary.opacity(0.8)) - .clipShape(Circle()) + ToolbarItem(placement: .cancellationAction) { + Button(L10n.Common.cancel) { + isPresented = false } .accessibilityIdentifier(AccessibilityIdentifiers.Residence.formCancelButton) } - ToolbarItem(placement: .navigationBarTrailing) { + ToolbarItem(placement: .confirmationAction) { Button(action: submitForm) { - HStack(spacing: 6) { - if viewModel.isLoading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: Color.appTextOnPrimary)) - .scaleEffect(0.8) - } + if viewModel.isLoading { + ProgressView() + } else { Text(L10n.Common.save) - .font(.system(size: 15, weight: .semibold)) } - .foregroundColor(canSave ? Color.appTextOnPrimary : Color.appTextSecondary) - .padding(.horizontal, 16) - .padding(.vertical, 8) - .background(canSave ? Color.appPrimary : Color.appTextSecondary.opacity(0.3)) - .clipShape(Capsule()) } .disabled(!canSave || viewModel.isLoading) .accessibilityIdentifier(AccessibilityIdentifiers.Residence.saveButton) @@ -499,272 +444,6 @@ struct ResidenceFormView: View { } } -// MARK: - Organic Form Components - -private struct OrganicFormSection: View { - let title: String - let icon: String - @ViewBuilder let content: Content - @Environment(\.colorScheme) var colorScheme - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - HStack(spacing: 10) { - ZStack { - Circle() - .fill(Color.appPrimary.opacity(0.1)) - .frame(width: 28, height: 28) - if icon == "house_outline" { - Image("house_outline") - .renderingMode(.template) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 14, height: 14) - .foregroundColor(Color.appPrimary) - } else { - Image(systemName: icon) - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(Color.appPrimary) - } - } - - Text(title.uppercased()) - .font(.system(size: 11, weight: .semibold, design: .rounded)) - .foregroundColor(Color.appTextSecondary) - .tracking(1.2) - } - - content - } - .padding(OrganicSpacing.cozy) - .background( - ZStack { - Color.appBackgroundSecondary - - GeometryReader { geo in - OrganicBlobShape(variation: Int.random(in: 0...2)) - .fill( - RadialGradient( - colors: [ - Color.appPrimary.opacity(colorScheme == .dark ? 0.06 : 0.03), - Color.appPrimary.opacity(0.01) - ], - center: .center, - startRadius: 0, - endRadius: geo.size.width * 0.4 - ) - ) - .frame(width: geo.size.width * 0.5, height: geo.size.height * 0.5) - .offset(x: geo.size.width * 0.5, y: -geo.size.height * 0.1) - .blur(radius: 15) - } - - GrainTexture(opacity: 0.012) - } - ) - .clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous)) - .naturalShadow(.medium) - } -} - -private struct OrganicFormTextField: View { - let label: String - let placeholder: String - @Binding var text: String - var error: String? = nil - var keyboardType: UIKeyboardType = .default - var accessibilityId: String? = nil - - var body: some View { - VStack(alignment: .leading, spacing: 6) { - Text(label) - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(Color.appTextSecondary) - - TextField(placeholder, text: $text) - .font(.system(size: 16, weight: .medium)) - .keyboardType(keyboardType) - .padding(14) - .background(Color.appBackgroundPrimary.opacity(0.5)) - .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) - .overlay( - RoundedRectangle(cornerRadius: 12, style: .continuous) - .stroke(error != nil ? Color.appError : Color.appTextSecondary.opacity(0.1), lineWidth: 1) - ) - .accessibilityIdentifier(accessibilityId ?? "") - - if let error = error { - Text(error) - .font(.system(size: 11, weight: .medium)) - .foregroundColor(Color.appError) - } - } - } -} - -private struct OrganicFormTextArea: View { - let label: String - let placeholder: String - @Binding var text: String - - var body: some View { - VStack(alignment: .leading, spacing: 6) { - Text(label) - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(Color.appTextSecondary) - - TextField(placeholder, text: $text, axis: .vertical) - .font(.system(size: 16, weight: .medium)) - .lineLimit(3...6) - .padding(14) - .background(Color.appBackgroundPrimary.opacity(0.5)) - .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) - .overlay( - RoundedRectangle(cornerRadius: 12, style: .continuous) - .stroke(Color.appTextSecondary.opacity(0.1), lineWidth: 1) - ) - } - } -} - -private struct OrganicFormPicker: View { - let label: String - @Binding var selection: T? - let options: [T] - let optionLabel: (T) -> String - let placeholder: String - - var body: some View { - VStack(alignment: .leading, spacing: 6) { - Text(label) - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(Color.appTextSecondary) - - Menu { - Button(action: { selection = nil }) { - Text(placeholder) - } - ForEach(options, id: \.self) { option in - Button(action: { selection = option }) { - Text(optionLabel(option)) - } - } - } label: { - HStack { - Text(selection.map { optionLabel($0) } ?? placeholder) - .font(.system(size: 16, weight: .medium)) - .foregroundColor(selection == nil ? Color.appTextSecondary : Color.appTextPrimary) - - Spacer() - - Image(systemName: "chevron.up.chevron.down") - .font(.system(size: 12, weight: .medium)) - .foregroundColor(Color.appTextSecondary) - } - .padding(14) - .background(Color.appBackgroundPrimary.opacity(0.5)) - .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) - .overlay( - RoundedRectangle(cornerRadius: 12, style: .continuous) - .stroke(Color.appTextSecondary.opacity(0.1), lineWidth: 1) - ) - } - } - } -} - -private struct OrganicFormToggle: View { - let label: String - @Binding var isOn: Bool - let icon: String - - var body: some View { - HStack(spacing: 12) { - ZStack { - Circle() - .fill(isOn ? Color.appAccent.opacity(0.15) : Color.appTextSecondary.opacity(0.1)) - .frame(width: 36, height: 36) - Image(systemName: icon) - .font(.system(size: 14, weight: .semibold)) - .foregroundColor(isOn ? Color.appAccent : Color.appTextSecondary) - } - - Text(label) - .font(.system(size: 16, weight: .medium)) - .foregroundColor(Color.appTextPrimary) - - Spacer() - - Toggle("", isOn: $isOn) - .labelsHidden() - .tint(Color.appPrimary) - } - .padding(14) - .background(Color.appBackgroundPrimary.opacity(0.5)) - .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) - } -} - -private struct OrganicUserRow: View { - let user: ResidenceUserResponse - let isOwner: Bool - let onRemove: () -> Void - - var body: some View { - HStack(spacing: 12) { - ZStack { - Circle() - .fill(Color.appPrimary.opacity(0.1)) - .frame(width: 40, height: 40) - Text(String(user.username.prefix(1)).uppercased()) - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(Color.appPrimary) - } - - VStack(alignment: .leading, spacing: 2) { - HStack(spacing: 6) { - Text(user.username) - .font(.system(size: 15, weight: .semibold)) - .foregroundColor(Color.appTextPrimary) - - if isOwner { - Text("Owner") - .font(.system(size: 10, weight: .bold)) - .foregroundColor(Color.appTextOnPrimary) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.appPrimary) - .clipShape(Capsule()) - } - } - - if !user.email.isEmpty { - Text(user.email) - .font(.system(size: 12, weight: .medium)) - .foregroundColor(Color.appTextSecondary) - } - } - - Spacer() - - if !isOwner { - Button(action: onRemove) { - Image(systemName: "trash") - .font(.system(size: 14, weight: .medium)) - .foregroundColor(Color.appError) - .padding(8) - .background(Color.appError.opacity(0.1)) - .clipShape(Circle()) - } - .buttonStyle(.plain) - } - } - .padding(12) - .background(Color.appBackgroundPrimary.opacity(0.5)) - .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) - } -} - #Preview("Add Mode") { ResidenceFormView(existingResidence: nil, isPresented: .constant(true)) } diff --git a/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift b/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift index 9f2854f..f6f9f07 100644 --- a/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift +++ b/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift @@ -32,7 +32,9 @@ struct DynamicTaskCard: View { Spacer() - PriorityBadge(priority: task.priorityName ?? "") + if let priorityName = task.priorityName, !priorityName.isEmpty { + PriorityBadge(priority: priorityName) + } } if !task.description_.isEmpty { @@ -43,9 +45,11 @@ struct DynamicTaskCard: View { } HStack { - Label(task.frequencyDisplayName ?? "", systemImage: "repeat") - .font(.caption) - .foregroundColor(Color.appTextSecondary) + if let frequency = task.frequencyDisplayName, !frequency.isEmpty { + Label(frequency, systemImage: "repeat") + .font(.caption) + .foregroundColor(Color.appTextSecondary) + } Spacer() diff --git a/iosApp/iosApp/Subviews/Task/TaskCard.swift b/iosApp/iosApp/Subviews/Task/TaskCard.swift index 3a5c9a7..5544c52 100644 --- a/iosApp/iosApp/Subviews/Task/TaskCard.swift +++ b/iosApp/iosApp/Subviews/Task/TaskCard.swift @@ -31,7 +31,9 @@ struct TaskCard: View { Spacer() - PriorityBadge(priority: task.priorityName ?? "") + if let priorityName = task.priorityName, !priorityName.isEmpty { + PriorityBadge(priority: priorityName) + } } // Description @@ -44,10 +46,12 @@ struct TaskCard: View { // Metadata Pills HStack(spacing: 10) { - TaskMetadataPill( - icon: "repeat", - text: task.frequencyDisplayName ?? "" - ) + if let frequency = task.frequencyDisplayName, !frequency.isEmpty { + TaskMetadataPill( + icon: "repeat", + text: frequency + ) + } Spacer() diff --git a/iosApp/iosApp/Task/TaskFormView.swift b/iosApp/iosApp/Task/TaskFormView.swift index 0730bef..f966cab 100644 --- a/iosApp/iosApp/Task/TaskFormView.swift +++ b/iosApp/iosApp/Task/TaskFormView.swift @@ -209,10 +209,6 @@ struct TaskFormView: View { } } header: { Text(L10n.Tasks.category) - } footer: { - Text(L10n.Tasks.required) - .font(.caption) - .foregroundColor(Color.appError) } .sectionBackground() @@ -246,10 +242,6 @@ struct TaskFormView: View { Text("Enter the number of days between each occurrence") .font(.caption) .foregroundColor(Color.appTextSecondary) - } else { - Text(L10n.Tasks.required) - .font(.caption) - .foregroundColor(Color.appError) } } .sectionBackground() @@ -265,10 +257,6 @@ struct TaskFormView: View { Toggle(L10n.Tasks.inProgressLabel, isOn: $inProgress) } header: { Text(L10n.Tasks.priorityAndStatus) - } footer: { - Text(L10n.Tasks.required) - .font(.caption) - .foregroundColor(Color.appError) } .sectionBackground()