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:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user