Migrate iOS app to system colors and improve UI/UX

- Remove AppColors struct, migrate to iOS system colors throughout
- Redesign ContractorFormSheet to use native SwiftUI Form components
- Add color-coded icons to contractor form sections
- Improve dark mode contrast for task cards
- Add background colors to document detail fields
- Fix text alignment issues in ContractorDetailView
- Make task completion lists expandable/collapsible by default
- Clear app badge on launch and when app becomes active
- Update button styling with proper gradients and shadows
- Improve form field focus states and accessibility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-13 22:22:52 -06:00
parent 29c136d612
commit a2de0f3454
29 changed files with 475 additions and 593 deletions

View File

@@ -47,38 +47,235 @@ struct ContractorFormSheet: View {
var body: some View {
NavigationStack {
ZStack {
AppColors.background.ignoresSafeArea()
ScrollView {
VStack(spacing: AppSpacing.lg) {
basicInformationSection
contactInformationSection
businessDetailsSection
addressSection
notesSection
favoriteToggle
errorMessage
Form {
// Basic Information
Section {
HStack {
Image(systemName: "person")
.foregroundColor(.blue)
.frame(width: 24)
TextField("Name", text: $name)
.focused($focusedField, equals: .name)
}
HStack {
Image(systemName: "building.2")
.foregroundColor(.purple)
.frame(width: 24)
TextField("Company", text: $company)
.focused($focusedField, equals: .company)
}
} header: {
Text("Basic Information")
}
// Contact Information
Section {
HStack {
Image(systemName: "phone.fill")
.foregroundColor(.green)
.frame(width: 24)
TextField("Phone", text: $phone)
.keyboardType(.phonePad)
.focused($focusedField, equals: .phone)
}
HStack {
Image(systemName: "envelope.fill")
.foregroundColor(.orange)
.frame(width: 24)
TextField("Email", text: $email)
.keyboardType(.emailAddress)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.focused($focusedField, equals: .email)
}
HStack {
Image(systemName: "phone.badge.plus")
.foregroundColor(.green)
.frame(width: 24)
TextField("Secondary Phone", text: $secondaryPhone)
.keyboardType(.phonePad)
.focused($focusedField, equals: .secondaryPhone)
}
} header: {
Text("Contact Information")
} footer: {
Text("Required: Name and Phone")
.font(.caption)
}
// Business Details
Section {
Button(action: { showingSpecialtyPicker = true }) {
HStack {
Image(systemName: "wrench.and.screwdriver")
.foregroundColor(.blue)
.frame(width: 24)
Text(specialty.isEmpty ? "Specialty" : specialty)
.foregroundColor(specialty.isEmpty ? Color(.placeholderText) : Color(.label))
Spacer()
Image(systemName: "chevron.down")
.font(.caption)
.foregroundColor(Color(.tertiaryLabel))
}
}
HStack {
Image(systemName: "doc.badge")
.foregroundColor(.purple)
.frame(width: 24)
TextField("License Number", text: $licenseNumber)
.focused($focusedField, equals: .licenseNumber)
}
HStack {
Image(systemName: "globe")
.foregroundColor(.blue)
.frame(width: 24)
TextField("Website", text: $website)
.keyboardType(.URL)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.focused($focusedField, equals: .website)
}
} header: {
Text("Business Details")
}
// Address
Section {
HStack {
Image(systemName: "location.fill")
.foregroundColor(.red)
.frame(width: 24)
TextField("Street Address", text: $address)
.focused($focusedField, equals: .address)
}
HStack {
Image(systemName: "building.2.crop.circle")
.foregroundColor(.blue)
.frame(width: 24)
TextField("City", text: $city)
.focused($focusedField, equals: .city)
}
HStack(spacing: AppSpacing.sm) {
HStack {
Image(systemName: "map")
.foregroundColor(.green)
.frame(width: 24)
TextField("State", text: $state)
.focused($focusedField, equals: .state)
}
Divider()
.frame(height: 24)
TextField("ZIP", text: $zipCode)
.keyboardType(.numberPad)
.focused($focusedField, equals: .zipCode)
.frame(maxWidth: 100)
}
} header: {
Text("Address")
}
// Notes
Section {
HStack(alignment: .top) {
Image(systemName: "note.text")
.foregroundColor(.orange)
.frame(width: 24)
.padding(.top, 8)
TextEditor(text: $notes)
.frame(height: 100)
.focused($focusedField, equals: .notes)
}
} header: {
Text("Notes")
} footer: {
Text("Private notes about this contractor")
.font(.caption)
}
// Favorite
Section {
Toggle(isOn: $isFavorite) {
Label("Mark as Favorite", systemImage: "star.fill")
.foregroundColor(isFavorite ? .orange : Color(.label))
}
.tint(.orange)
}
// Error Message
if let error = viewModel.errorMessage {
Section {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.red)
Text(error)
.font(.callout)
.foregroundColor(.red)
}
}
.padding(AppSpacing.md)
}
}
.navigationTitle(contractor == nil ? "Add Contractor" : "Edit Contractor")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
cancelButton
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
saveButton
ToolbarItem(placement: .confirmationAction) {
Button(action: saveContractor) {
if viewModel.isCreating || viewModel.isUpdating {
ProgressView()
} else {
Text(contractor == nil ? "Add" : "Save")
.bold()
}
}
.disabled(!canSave || viewModel.isCreating || viewModel.isUpdating)
}
}
.sheet(isPresented: $showingSpecialtyPicker) {
SpecialtyPickerView(
selectedSpecialty: $specialty,
specialties: specialties
)
NavigationStack {
List {
ForEach(specialties, id: \.self) { spec in
Button(action: {
specialty = spec
showingSpecialtyPicker = false
}) {
HStack {
Text(spec)
.foregroundColor(Color(.label))
Spacer()
if specialty == spec {
Image(systemName: "checkmark")
.foregroundColor(.blue)
}
}
}
}
}
.navigationTitle("Select Specialty")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
showingSpecialtyPicker = false
}
}
}
}
.presentationDetents([.medium, .large])
}
.onAppear {
loadContractorData()
@@ -87,229 +284,6 @@ struct ContractorFormSheet: View {
}
}
// MARK: - Toolbar Items
private var cancelButton: some View {
Button("Cancel") {
dismiss()
}
.foregroundColor(AppColors.textSecondary)
}
private var saveButton: some View {
Button(action: saveContractor) {
if viewModel.isCreating || viewModel.isUpdating {
ProgressView()
} else {
Text(contractor == nil ? "Add" : "Save")
.foregroundColor(canSave ? AppColors.primary : AppColors.textTertiary)
}
}
.disabled(!canSave || viewModel.isCreating || viewModel.isUpdating)
}
// MARK: - Form Sections
private var basicInformationSection: some View {
VStack(spacing: AppSpacing.sm) {
SectionHeader(title: "Basic Information")
FormTextField(
title: "Name *",
text: $name,
icon: "person",
focused: $focusedField,
field: .name
)
FormTextField(
title: "Company",
text: $company,
icon: "building.2",
focused: $focusedField,
field: .company
)
}
}
private var contactInformationSection: some View {
VStack(spacing: AppSpacing.sm) {
SectionHeader(title: "Contact Information")
FormTextField(
title: "Phone *",
text: $phone,
icon: "phone",
keyboardType: .phonePad,
focused: $focusedField,
field: .phone
)
FormTextField(
title: "Email",
text: $email,
icon: "envelope",
keyboardType: .emailAddress,
focused: $focusedField,
field: .email
)
FormTextField(
title: "Secondary Phone",
text: $secondaryPhone,
icon: "phone",
keyboardType: .phonePad,
focused: $focusedField,
field: .secondaryPhone
)
}
}
private var businessDetailsSection: some View {
VStack(spacing: AppSpacing.sm) {
SectionHeader(title: "Business Details")
specialtyPickerButton
FormTextField(
title: "License Number",
text: $licenseNumber,
icon: "doc.badge",
focused: $focusedField,
field: .licenseNumber
)
FormTextField(
title: "Website",
text: $website,
icon: "globe",
keyboardType: .URL,
focused: $focusedField,
field: .website
)
}
}
private var specialtyPickerButton: some View {
Button(action: { showingSpecialtyPicker = true }) {
HStack {
Image(systemName: "wrench.and.screwdriver")
.foregroundColor(AppColors.textSecondary)
.frame(width: 20)
Text(specialty.isEmpty ? "Specialty" : specialty)
.foregroundColor(specialty.isEmpty ? AppColors.textTertiary : AppColors.textPrimary)
Spacer()
Image(systemName: "chevron.down")
.font(.caption)
.foregroundColor(AppColors.textTertiary)
}
.padding(AppSpacing.md)
.background(AppColors.surface)
.cornerRadius(AppRadius.md)
.overlay(
RoundedRectangle(cornerRadius: AppRadius.md)
.stroke(AppColors.border, lineWidth: 1)
)
}
}
private var addressSection: some View {
VStack(spacing: AppSpacing.sm) {
SectionHeader(title: "Address")
FormTextField(
title: "Street Address",
text: $address,
icon: "mappin",
focused: $focusedField,
field: .address
)
HStack(spacing: AppSpacing.sm) {
FormTextField(
title: "City",
text: $city,
focused: $focusedField,
field: .city
)
FormTextField(
title: "State",
text: $state,
focused: $focusedField,
field: .state
)
.frame(maxWidth: 100)
}
FormTextField(
title: "ZIP Code",
text: $zipCode,
keyboardType: .numberPad,
focused: $focusedField,
field: .zipCode
)
}
}
private var notesSection: some View {
VStack(alignment: .leading, spacing: AppSpacing.xxs) {
SectionHeader(title: "Notes")
HStack {
Image(systemName: "note.text")
.foregroundColor(AppColors.textSecondary)
.frame(width: 20)
Text("Private Notes")
.font(.footnote.weight(.medium))
.foregroundColor(AppColors.textSecondary)
}
TextEditor(text: $notes)
.frame(height: 100)
.padding(AppSpacing.sm)
.background(AppColors.surface)
.cornerRadius(AppRadius.md)
.overlay(
RoundedRectangle(cornerRadius: AppRadius.md)
.stroke(AppColors.border, lineWidth: 1)
)
.focused($focusedField, equals: .notes)
}
}
private var favoriteToggle: some View {
Toggle(isOn: $isFavorite) {
HStack {
Image(systemName: "star.fill")
.foregroundColor(isFavorite ? AppColors.warning : AppColors.textSecondary)
Text("Mark as Favorite")
.font(.body)
.foregroundColor(AppColors.textPrimary)
}
}
.padding(AppSpacing.md)
.background(AppColors.surface)
.cornerRadius(AppRadius.md)
}
@ViewBuilder
private var errorMessage: some View {
if let error = viewModel.errorMessage {
Text(error)
.font(.callout)
.foregroundColor(AppColors.error)
.padding(AppSpacing.sm)
.frame(maxWidth: .infinity)
.background(AppColors.error.opacity(0.1))
.cornerRadius(AppRadius.md)
}
}
// MARK: - Data Loading
private func loadContractorData() {
@@ -399,98 +373,3 @@ struct ContractorFormSheet: View {
}
}
}
// MARK: - Section Header
struct SectionHeader: View {
let title: String
var body: some View {
HStack {
Text(title)
.font(.headline)
.foregroundColor(AppColors.textPrimary)
Spacer()
}
}
}
// MARK: - Form Text Field
struct FormTextField: View {
let title: String
@Binding var text: String
var icon: String? = nil
var keyboardType: UIKeyboardType = .default
var focused: FocusState<ContractorFormField?>.Binding
var field: ContractorFormField
var body: some View {
VStack(alignment: .leading, spacing: AppSpacing.xxs) {
if let icon = icon {
HStack {
Image(systemName: icon)
.foregroundColor(AppColors.textSecondary)
.frame(width: 20)
Text(title)
.font(.footnote.weight(.medium))
.foregroundColor(AppColors.textSecondary)
}
} else {
Text(title)
.font(.footnote.weight(.medium))
.foregroundColor(AppColors.textSecondary)
}
TextField("", text: $text)
.keyboardType(keyboardType)
.autocapitalization(keyboardType == .emailAddress ? .none : .words)
.padding(AppSpacing.md)
.background(AppColors.surface)
.cornerRadius(AppRadius.md)
.overlay(
RoundedRectangle(cornerRadius: AppRadius.md)
.stroke(AppColors.border, lineWidth: 1)
)
.focused(focused, equals: field)
}
}
}
// MARK: - Specialty Picker
struct SpecialtyPickerView: View {
@Environment(\.dismiss) private var dismiss
@Binding var selectedSpecialty: String
let specialties: [String]
var body: some View {
NavigationView {
List {
ForEach(specialties, id: \.self) { specialty in
Button(action: {
selectedSpecialty = specialty
dismiss()
}) {
HStack {
Text(specialty)
.foregroundColor(AppColors.textPrimary)
Spacer()
if selectedSpecialty == specialty {
Image(systemName: "checkmark")
.foregroundColor(AppColors.primary)
}
}
}
}
}
.navigationTitle("Select Specialty")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
dismiss()
}
}
}
}
}
}