Polish UI consistency across all CRUD forms and fix data display issues

- 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 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-05 16:00:40 -06:00
parent 48081c0cc8
commit 61ab95d108
7 changed files with 195 additions and 521 deletions

View File

@@ -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)") {

View File

@@ -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 {

View File

@@ -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.",

View File

@@ -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<Content: View>: 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<T: Hashable>: 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))
}

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()