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:
@@ -77,7 +77,7 @@ fun DocumentsScreen(
|
||||
Icon(
|
||||
if (showActiveOnly) Icons.Default.CheckCircle else Icons.Default.CheckCircleOutline,
|
||||
"Filter active",
|
||||
tint = if (showActiveOnly) Color(0xFF10B981) else LocalContentColor.current
|
||||
tint = if (showActiveOnly) MaterialTheme.colorScheme.secondary else LocalContentColor.current
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ fun DocumentsScreen(
|
||||
Icons.Default.FilterList,
|
||||
"Filters",
|
||||
tint = if (selectedCategory != null || selectedDocType != null)
|
||||
Color(0xFF3B82F6) else LocalContentColor.current
|
||||
MaterialTheme.colorScheme.primary else LocalContentColor.current
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.tt.mycrib.MyCrib</string>
|
||||
<string>group.com.tt.mycrib.MyCribDev</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -394,7 +394,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.tt.mycrib.MyCrib;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.tt.mycrib.MyCribDev;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@@ -423,7 +423,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.tt.mycrib.MyCrib.MyCrib;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.tt.mycrib.MyCribDev.MyCribDev;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
@@ -457,7 +457,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.tt.mycrib.MyCrib.MyCrib;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.tt.mycrib.MyCribDev.MyCribDev;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
@@ -663,7 +663,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.tt.mycrib.MyCrib;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.tt.mycrib.MyCribDev;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
||||
@@ -10,12 +10,12 @@ struct ContractorCard: View {
|
||||
// Avatar
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(AppColors.primary.opacity(0.1))
|
||||
.fill(.blue.opacity(0.1))
|
||||
.frame(width: 56, height: 56)
|
||||
|
||||
Image(systemName: "person.fill")
|
||||
.font(.title2)
|
||||
.foregroundColor(AppColors.primary)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
|
||||
// Content
|
||||
@@ -24,13 +24,13 @@ struct ContractorCard: View {
|
||||
HStack(spacing: AppSpacing.xxs) {
|
||||
Text(contractor.name)
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundColor(AppColors.textPrimary)
|
||||
.foregroundColor(Color(.label))
|
||||
.lineLimit(1)
|
||||
|
||||
if contractor.isFavorite {
|
||||
Image(systemName: "star.fill")
|
||||
.font(.caption)
|
||||
.foregroundColor(AppColors.warning)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ struct ContractorCard: View {
|
||||
if let company = contractor.company {
|
||||
Text(company)
|
||||
.font(.body)
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
@@ -48,21 +48,21 @@ struct ContractorCard: View {
|
||||
if let specialty = contractor.specialty {
|
||||
Label(specialty, systemImage: "wrench.and.screwdriver")
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
}
|
||||
|
||||
// Rating
|
||||
if let rating = contractor.averageRating, rating.doubleValue > 0 {
|
||||
Label(String(format: "%.1f", rating.doubleValue), systemImage: "star.fill")
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(AppColors.warning)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
|
||||
// Task count
|
||||
if contractor.taskCount > 0 {
|
||||
Label("\(contractor.taskCount) tasks", systemImage: "checkmark.circle")
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(AppColors.success)
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,17 +73,17 @@ struct ContractorCard: View {
|
||||
Button(action: onToggleFavorite) {
|
||||
Image(systemName: contractor.isFavorite ? "star.fill" : "star")
|
||||
.font(.title3)
|
||||
.foregroundColor(contractor.isFavorite ? AppColors.warning : AppColors.textTertiary)
|
||||
.foregroundColor(contractor.isFavorite ? .orange : Color(.tertiaryLabel))
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
|
||||
// Chevron
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundColor(AppColors.textTertiary)
|
||||
.foregroundColor(Color(.tertiaryLabel))
|
||||
}
|
||||
.padding(AppSpacing.md)
|
||||
.background(AppColors.surface)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.lg)
|
||||
.shadow(color: AppShadow.sm.color, radius: AppShadow.sm.radius, x: AppShadow.sm.x, y: AppShadow.sm.y)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ struct ContractorDetailView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
AppColors.background.ignoresSafeArea()
|
||||
Color(.systemGroupedBackground).ignoresSafeArea()
|
||||
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
@@ -29,24 +29,24 @@ struct ContractorDetailView: View {
|
||||
// Avatar
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(AppColors.primary.opacity(0.1))
|
||||
.fill(.blue.opacity(0.1))
|
||||
.frame(width: 80, height: 80)
|
||||
|
||||
Image(systemName: "person.fill")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(AppColors.primary)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
|
||||
// Name
|
||||
Text(contractor.name)
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundColor(AppColors.textPrimary)
|
||||
.foregroundColor(Color(.label))
|
||||
|
||||
// Company
|
||||
if let company = contractor.company {
|
||||
Text(company)
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
}
|
||||
|
||||
// Specialty Badge
|
||||
@@ -59,8 +59,8 @@ struct ContractorDetailView: View {
|
||||
}
|
||||
.padding(.horizontal, AppSpacing.sm)
|
||||
.padding(.vertical, AppSpacing.xxs)
|
||||
.background(AppColors.primary.opacity(0.1))
|
||||
.foregroundColor(AppColors.primary)
|
||||
.background(.blue.opacity(0.1))
|
||||
.foregroundColor(.blue)
|
||||
.cornerRadius(AppRadius.full)
|
||||
}
|
||||
|
||||
@@ -69,41 +69,41 @@ struct ContractorDetailView: View {
|
||||
HStack(spacing: AppSpacing.xxs) {
|
||||
ForEach(0..<5) { index in
|
||||
Image(systemName: index < Int(rating.doubleValue) ? "star.fill" : "star")
|
||||
.foregroundColor(AppColors.warning)
|
||||
.foregroundColor(.orange)
|
||||
.font(.caption)
|
||||
}
|
||||
Text(String(format: "%.1f", rating.doubleValue))
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundColor(AppColors.textPrimary)
|
||||
.foregroundColor(Color(.label))
|
||||
}
|
||||
|
||||
if contractor.taskCount > 0 {
|
||||
Text("\(contractor.taskCount) completed tasks")
|
||||
.font(.callout)
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(AppSpacing.lg)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(AppColors.surface)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.lg)
|
||||
.shadow(color: AppShadow.sm.color, radius: AppShadow.sm.radius, x: AppShadow.sm.x, y: AppShadow.sm.y)
|
||||
|
||||
// Contact Information
|
||||
DetailSection(title: "Contact Information") {
|
||||
DetailRow(icon: "phone", label: "Phone", value: contractor.phone, iconColor: AppColors.primary)
|
||||
DetailRow(icon: "phone", label: "Phone", value: contractor.phone, iconColor: .blue)
|
||||
|
||||
if let email = contractor.email {
|
||||
DetailRow(icon: "envelope", label: "Email", value: email, iconColor: AppColors.accent)
|
||||
DetailRow(icon: "envelope", label: "Email", value: email, iconColor: .purple)
|
||||
}
|
||||
|
||||
if let secondaryPhone = contractor.secondaryPhone {
|
||||
DetailRow(icon: "phone", label: "Secondary Phone", value: secondaryPhone, iconColor: AppColors.success)
|
||||
DetailRow(icon: "phone", label: "Secondary Phone", value: secondaryPhone, iconColor: .green)
|
||||
}
|
||||
|
||||
if let website = contractor.website {
|
||||
DetailRow(icon: "globe", label: "Website", value: website, iconColor: AppColors.warning)
|
||||
DetailRow(icon: "globe", label: "Website", value: website, iconColor: .orange)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ struct ContractorDetailView: View {
|
||||
if contractor.licenseNumber != nil {
|
||||
DetailSection(title: "Business Details") {
|
||||
if let licenseNumber = contractor.licenseNumber {
|
||||
DetailRow(icon: "doc.badge", label: "License Number", value: licenseNumber, iconColor: AppColors.primary)
|
||||
DetailRow(icon: "doc.badge", label: "License Number", value: licenseNumber, iconColor: .blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,7 +130,7 @@ struct ContractorDetailView: View {
|
||||
icon: "mappin.circle",
|
||||
label: "Location",
|
||||
value: addressComponents.joined(separator: "\n"),
|
||||
iconColor: AppColors.error
|
||||
iconColor: .red
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -141,7 +141,8 @@ struct ContractorDetailView: View {
|
||||
DetailSection(title: "Notes") {
|
||||
Text(notes)
|
||||
.font(.body)
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(AppSpacing.md)
|
||||
}
|
||||
}
|
||||
@@ -150,11 +151,11 @@ struct ContractorDetailView: View {
|
||||
DetailSection(title: "Task History") {
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle")
|
||||
.foregroundColor(AppColors.success)
|
||||
.foregroundColor(.green)
|
||||
Spacer()
|
||||
Text("\(contractor.taskCount) completed tasks")
|
||||
.font(.body)
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
}
|
||||
.padding(AppSpacing.md)
|
||||
}
|
||||
@@ -188,7 +189,7 @@ struct ContractorDetailView: View {
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
.foregroundColor(AppColors.primary)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,13 +237,13 @@ struct DetailSection<Content: View>: View {
|
||||
VStack(alignment: .leading, spacing: AppSpacing.sm) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.foregroundColor(AppColors.textPrimary)
|
||||
.foregroundColor(Color(.label))
|
||||
.padding(.horizontal, AppSpacing.md)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
content()
|
||||
}
|
||||
.background(AppColors.surface)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.lg)
|
||||
.shadow(color: AppShadow.sm.color, radius: AppShadow.sm.radius, x: AppShadow.sm.x, y: AppShadow.sm.y)
|
||||
}
|
||||
@@ -254,7 +255,7 @@ struct DetailRow: View {
|
||||
let icon: String
|
||||
let label: String
|
||||
let value: String
|
||||
var iconColor: Color = AppColors.textSecondary
|
||||
var iconColor: Color = Color(.secondaryLabel)
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: AppSpacing.sm) {
|
||||
@@ -265,11 +266,11 @@ struct DetailRow: View {
|
||||
VStack(alignment: .leading, spacing: AppSpacing.xxs) {
|
||||
Text(label)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
|
||||
Text(value)
|
||||
.font(.body)
|
||||
.foregroundColor(AppColors.textPrimary)
|
||||
.foregroundColor(Color(.label))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ struct ContractorsListView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
AppColors.background.ignoresSafeArea()
|
||||
Color(.systemGroupedBackground).ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// Search Bar
|
||||
@@ -115,7 +115,7 @@ struct ContractorsListView: View {
|
||||
loadContractors()
|
||||
}) {
|
||||
Image(systemName: showFavoritesOnly ? "star.fill" : "star")
|
||||
.foregroundColor(showFavoritesOnly ? AppColors.warning : AppColors.textSecondary)
|
||||
.foregroundColor(showFavoritesOnly ? .orange : Color(.secondaryLabel))
|
||||
}
|
||||
|
||||
// Specialty Filter
|
||||
@@ -139,14 +139,14 @@ struct ContractorsListView: View {
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "line.3.horizontal.decrease.circle")
|
||||
.foregroundColor(selectedSpecialty != nil ? AppColors.primary : AppColors.textSecondary)
|
||||
.foregroundColor(selectedSpecialty != nil ? .blue : Color(.secondaryLabel))
|
||||
}
|
||||
|
||||
// Add Button
|
||||
Button(action: { showingAddSheet = true }) {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.title2)
|
||||
.foregroundColor(AppColors.primary)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,7 +201,7 @@ struct SearchBar: View {
|
||||
var body: some View {
|
||||
HStack(spacing: AppSpacing.sm) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
|
||||
TextField(placeholder, text: $text)
|
||||
.font(.body)
|
||||
@@ -209,12 +209,12 @@ struct SearchBar: View {
|
||||
if !text.isEmpty {
|
||||
Button(action: { text = "" }) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(AppSpacing.sm)
|
||||
.background(AppColors.surface)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.md)
|
||||
.shadow(color: AppShadow.sm.color, radius: AppShadow.sm.radius, x: AppShadow.sm.x, y: AppShadow.sm.y)
|
||||
}
|
||||
@@ -242,8 +242,8 @@ struct FilterChip: View {
|
||||
}
|
||||
.padding(.horizontal, AppSpacing.sm)
|
||||
.padding(.vertical, AppSpacing.xxs)
|
||||
.background(AppColors.primary.opacity(0.1))
|
||||
.foregroundColor(AppColors.primary)
|
||||
.background(.blue.opacity(0.1))
|
||||
.foregroundColor(.blue)
|
||||
.cornerRadius(AppRadius.full)
|
||||
}
|
||||
}
|
||||
@@ -256,16 +256,16 @@ struct EmptyContractorsView: View {
|
||||
VStack(spacing: AppSpacing.md) {
|
||||
Image(systemName: "person.badge.plus")
|
||||
.font(.system(size: 64))
|
||||
.foregroundColor(AppColors.textTertiary)
|
||||
.foregroundColor(Color(.tertiaryLabel))
|
||||
|
||||
Text(hasFilters ? "No contractors found" : "No contractors yet")
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
|
||||
if !hasFilters {
|
||||
Text("Add your first contractor to get started")
|
||||
.font(.callout)
|
||||
.foregroundColor(AppColors.textTertiary)
|
||||
.foregroundColor(Color(.tertiaryLabel))
|
||||
}
|
||||
}
|
||||
.padding(AppSpacing.xl)
|
||||
|
||||
@@ -3,55 +3,6 @@ import SwiftUI
|
||||
// MARK: - Design System
|
||||
// Modern, sleek design system for MyCrib with Light and Dark mode support
|
||||
|
||||
struct AppColors {
|
||||
// Primary Colors - Modern blue gradient
|
||||
static let primary = Color(hex: "2563EB") ?? .blue
|
||||
static let primaryLight = Color(hex: "3B82F6") ?? .blue
|
||||
static let primaryDark = Color(hex: "1E40AF") ?? .blue
|
||||
|
||||
// Accent Colors
|
||||
static let accent = Color(hex: "8B5CF6") ?? .purple
|
||||
static let accentLight = Color(hex: "A78BFA") ?? .purple
|
||||
|
||||
// Semantic Colors
|
||||
static let success = Color(hex: "10B981") ?? .green
|
||||
static let warning = Color(hex: "F59E0B") ?? .orange
|
||||
static let error = Color(hex: "EF4444") ?? .red
|
||||
static let info = Color(hex: "3B82F6") ?? .blue
|
||||
|
||||
// Adaptive Neutral Colors - Automatically adapt to light/dark mode
|
||||
static let background = Color(uiColor: .systemGroupedBackground)
|
||||
static let surface = Color(uiColor: .secondarySystemGroupedBackground)
|
||||
static let surfaceSecondary = Color(uiColor: .tertiarySystemGroupedBackground)
|
||||
|
||||
static let textPrimary = Color(uiColor: .label)
|
||||
static let textSecondary = Color(uiColor: .secondaryLabel)
|
||||
static let textTertiary = Color(uiColor: .tertiaryLabel)
|
||||
|
||||
static let border = Color(uiColor: .separator)
|
||||
static let borderLight = Color(uiColor: .opaqueSeparator)
|
||||
|
||||
// Task Status Colors
|
||||
static let taskUpcoming = Color(hex: "3B82F6") ?? .blue
|
||||
static let taskInProgress = Color(hex: "F59E0B") ?? .orange
|
||||
static let taskCompleted = Color(hex: "10B981") ?? .green
|
||||
static let taskCanceled = Color(hex: "6B7280") ?? .gray
|
||||
static let taskArchived = Color(hex: "9CA3AF") ?? .gray
|
||||
|
||||
// Gradient
|
||||
static let primaryGradient = LinearGradient(
|
||||
colors: [primary, primaryLight],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
|
||||
static let accentGradient = LinearGradient(
|
||||
colors: [accent, accentLight],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
}
|
||||
|
||||
struct AppSpacing {
|
||||
static let xxs: CGFloat = 4
|
||||
static let xs: CGFloat = 8
|
||||
@@ -102,7 +53,7 @@ struct CardStyle: ViewModifier {
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.background(AppColors.surface)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.lg)
|
||||
.shadow(color: shadow.color, radius: shadow.radius, x: shadow.x, y: shadow.y)
|
||||
}
|
||||
@@ -118,7 +69,7 @@ struct PrimaryButtonStyle: ButtonStyle {
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 56)
|
||||
.background(
|
||||
configuration.isPressed ? AppColors.primaryDark : AppColors.primary
|
||||
configuration.isPressed ? .blue : .blue
|
||||
)
|
||||
.cornerRadius(AppRadius.md)
|
||||
.scaleEffect(configuration.isPressed ? 0.98 : 1.0)
|
||||
@@ -130,10 +81,10 @@ struct SecondaryButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(.headline)
|
||||
.foregroundColor(AppColors.primary)
|
||||
.foregroundColor(.blue)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 56)
|
||||
.background(AppColors.surfaceSecondary)
|
||||
.background(Color(.tertiarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.md)
|
||||
.scaleEffect(configuration.isPressed ? 0.98 : 1.0)
|
||||
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
|
||||
@@ -144,11 +95,11 @@ struct TextFieldStyle: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.padding(AppSpacing.md)
|
||||
.background(AppColors.surfaceSecondary)
|
||||
.background(Color(.tertiarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.md)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: AppRadius.md)
|
||||
.stroke(AppColors.borderLight, lineWidth: 1)
|
||||
.stroke(Color(.opaqueSeparator), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ struct DocumentCard: View {
|
||||
switch document.documentType {
|
||||
case "warranty": return .blue
|
||||
case "manual": return .purple
|
||||
case "receipt": return AppColors.success
|
||||
case "inspection": return AppColors.warning
|
||||
case "receipt": return .green
|
||||
case "inspection": return .orange
|
||||
default: return .gray
|
||||
}
|
||||
}
|
||||
@@ -41,13 +41,13 @@ struct DocumentCard: View {
|
||||
Text(document.title)
|
||||
.font(.title3.weight(.semibold))
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(AppColors.textPrimary)
|
||||
.foregroundColor(Color(.label))
|
||||
.lineLimit(1)
|
||||
|
||||
if let description = document.description_, !description.isEmpty {
|
||||
Text(description)
|
||||
.font(.callout)
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ struct DocumentCard: View {
|
||||
if let fileSize = document.fileSize {
|
||||
Text(formatFileSize(Int(fileSize)))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,11 +71,11 @@ struct DocumentCard: View {
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.padding(AppSpacing.md)
|
||||
.background(AppColors.surface)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.md)
|
||||
.shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1)
|
||||
}
|
||||
|
||||
@@ -9,15 +9,15 @@ struct EmptyStateView: View {
|
||||
VStack(spacing: AppSpacing.md) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 64))
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
|
||||
Text(title)
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
|
||||
Text(message)
|
||||
.font(.body)
|
||||
.foregroundColor(AppColors.textTertiary)
|
||||
.foregroundColor(Color(.tertiaryLabel))
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding(AppSpacing.lg)
|
||||
|
||||
@@ -10,10 +10,10 @@ struct WarrantyCard: View {
|
||||
|
||||
var statusColor: Color {
|
||||
if !document.isActive { return .gray }
|
||||
if daysUntilExpiration < 0 { return AppColors.error }
|
||||
if daysUntilExpiration < 30 { return AppColors.warning }
|
||||
if daysUntilExpiration < 0 { return .red }
|
||||
if daysUntilExpiration < 30 { return .orange }
|
||||
if daysUntilExpiration < 90 { return .yellow }
|
||||
return AppColors.success
|
||||
return .green
|
||||
}
|
||||
|
||||
var statusText: String {
|
||||
@@ -31,11 +31,11 @@ struct WarrantyCard: View {
|
||||
Text(document.title)
|
||||
.font(.title3.weight(.semibold))
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(AppColors.textPrimary)
|
||||
.foregroundColor(Color(.label))
|
||||
|
||||
Text(document.itemName ?? "")
|
||||
.font(.body)
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -58,11 +58,11 @@ struct WarrantyCard: View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Provider")
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
Text(document.provider ?? "N/A")
|
||||
.font(.body)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(AppColors.textPrimary)
|
||||
.foregroundColor(Color(.label))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -70,11 +70,11 @@ struct WarrantyCard: View {
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
Text("Expires")
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
Text(document.endDate ?? "N/A")
|
||||
.font(.body)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(AppColors.textPrimary)
|
||||
.foregroundColor(Color(.label))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ struct WarrantyCard: View {
|
||||
}
|
||||
}
|
||||
.padding(AppSpacing.md)
|
||||
.background(AppColors.surface)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.md)
|
||||
.shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1)
|
||||
}
|
||||
|
||||
@@ -400,6 +400,9 @@ struct DocumentDetailView: View {
|
||||
.font(.body)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(12)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
private func getStatusColor(isActive: Bool, daysUntilExpiration: Int32) -> Color {
|
||||
|
||||
@@ -28,7 +28,7 @@ struct DocumentsWarrantiesView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
AppColors.background.ignoresSafeArea()
|
||||
Color(.systemGroupedBackground).ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// Segmented Control for Tabs
|
||||
@@ -104,7 +104,7 @@ struct DocumentsWarrantiesView: View {
|
||||
loadWarranties()
|
||||
}) {
|
||||
Image(systemName: showActiveOnly ? "checkmark.circle.fill" : "checkmark.circle")
|
||||
.foregroundColor(showActiveOnly ? AppColors.success : AppColors.textSecondary)
|
||||
.foregroundColor(showActiveOnly ? .green : Color(.secondaryLabel))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ struct DocumentsWarrantiesView: View {
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "line.3.horizontal.decrease.circle")
|
||||
.foregroundColor((selectedCategory != nil || selectedDocType != nil) ? AppColors.primary : AppColors.textSecondary)
|
||||
.foregroundColor((selectedCategory != nil || selectedDocType != nil) ? .blue : Color(.secondaryLabel))
|
||||
}
|
||||
|
||||
// Add Button
|
||||
@@ -158,7 +158,7 @@ struct DocumentsWarrantiesView: View {
|
||||
}) {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.title2)
|
||||
.foregroundColor(AppColors.primary)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ struct HomeScreenView: View {
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
AppColors.background
|
||||
Color(.systemGroupedBackground)
|
||||
.ignoresSafeArea()
|
||||
|
||||
if viewModel.isLoading {
|
||||
@@ -17,7 +17,7 @@ struct HomeScreenView: View {
|
||||
.scaleEffect(1.2)
|
||||
Text("Loading...")
|
||||
.font(.body)
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
}
|
||||
} else {
|
||||
ScrollView(showsIndicators: false) {
|
||||
@@ -27,11 +27,11 @@ struct HomeScreenView: View {
|
||||
Text("Hello!")
|
||||
.font(.title.weight(.bold))
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(AppColors.textPrimary)
|
||||
.foregroundColor(Color(.label))
|
||||
|
||||
Text("Welcome to MyCrib")
|
||||
.font(.body)
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, AppSpacing.md)
|
||||
@@ -80,7 +80,7 @@ struct HomeScreenView: View {
|
||||
Image(systemName: "rectangle.portrait.and.arrow.right")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
}
|
||||
.foregroundColor(AppColors.error)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
|
||||
@@ -25,7 +25,7 @@ struct LoginView: View {
|
||||
|
||||
private var buttonBackgroundColor: Color {
|
||||
if viewModel.isLoading || !isFormValid {
|
||||
return AppColors.textTertiary
|
||||
return Color(.tertiaryLabel)
|
||||
}
|
||||
return .clear
|
||||
}
|
||||
@@ -38,7 +38,7 @@ struct LoginView: View {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
// Background gradient
|
||||
AppColors.background
|
||||
Color(.systemGroupedBackground)
|
||||
.ignoresSafeArea()
|
||||
|
||||
ScrollView {
|
||||
@@ -51,9 +51,9 @@ struct LoginView: View {
|
||||
// App Icon with gradient
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(AppColors.primaryGradient)
|
||||
.fill(LinearGradient(colors: [.blue, .blue.opacity(0.8)], startPoint: .topLeading, endPoint: .bottomTrailing))
|
||||
.frame(width: 100, height: 100)
|
||||
.shadow(color: AppColors.primary.opacity(0.3), radius: 20, y: 10)
|
||||
.shadow(color: .blue.opacity(0.3), radius: 20, y: 10)
|
||||
|
||||
Image(systemName: "house.fill")
|
||||
.font(.system(size: 50, weight: .semibold))
|
||||
@@ -63,11 +63,11 @@ struct LoginView: View {
|
||||
VStack(spacing: AppSpacing.xs) {
|
||||
Text("Welcome Back")
|
||||
.font(.title2.weight(.bold))
|
||||
.foregroundColor(AppColors.textPrimary)
|
||||
.foregroundColor(Color(.label))
|
||||
|
||||
Text("Sign in to manage your properties")
|
||||
.font(.body)
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,11 +77,11 @@ struct LoginView: View {
|
||||
VStack(alignment: .leading, spacing: AppSpacing.xs) {
|
||||
Text("Email or Username")
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
|
||||
HStack(spacing: AppSpacing.sm) {
|
||||
Image(systemName: "envelope.fill")
|
||||
.foregroundColor(AppColors.textTertiary)
|
||||
.foregroundColor(Color(.tertiaryLabel))
|
||||
.frame(width: 20)
|
||||
|
||||
TextField("Enter your email", text: $viewModel.username)
|
||||
@@ -98,13 +98,13 @@ struct LoginView: View {
|
||||
}
|
||||
}
|
||||
.padding(AppSpacing.md)
|
||||
.background(AppColors.surface)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.md)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: AppRadius.md)
|
||||
.stroke(focusedField == .username ? AppColors.primary : AppColors.border, lineWidth: 1.5)
|
||||
.stroke(focusedField == .username ? .blue : Color(.separator), lineWidth: 1.5)
|
||||
)
|
||||
.shadow(color: focusedField == .username ? AppColors.primary.opacity(0.1) : .clear, radius: 8)
|
||||
.shadow(color: focusedField == .username ? .blue.opacity(0.1) : .clear, radius: 8)
|
||||
.animation(.easeInOut(duration: 0.2), value: focusedField)
|
||||
}
|
||||
|
||||
@@ -112,11 +112,11 @@ struct LoginView: View {
|
||||
VStack(alignment: .leading, spacing: AppSpacing.xs) {
|
||||
Text("Password")
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
|
||||
HStack(spacing: AppSpacing.sm) {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(AppColors.textTertiary)
|
||||
.foregroundColor(Color(.tertiaryLabel))
|
||||
.frame(width: 20)
|
||||
|
||||
Group {
|
||||
@@ -143,18 +143,18 @@ struct LoginView: View {
|
||||
isPasswordVisible.toggle()
|
||||
}) {
|
||||
Image(systemName: isPasswordVisible ? "eye.slash.fill" : "eye.fill")
|
||||
.foregroundColor(AppColors.textTertiary)
|
||||
.foregroundColor(Color(.tertiaryLabel))
|
||||
.frame(width: 20)
|
||||
}
|
||||
}
|
||||
.padding(AppSpacing.md)
|
||||
.background(AppColors.surface)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.md)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: AppRadius.md)
|
||||
.stroke(focusedField == .password ? AppColors.primary : AppColors.border, lineWidth: 1.5)
|
||||
.stroke(focusedField == .password ? .blue : Color(.separator), lineWidth: 1.5)
|
||||
)
|
||||
.shadow(color: focusedField == .password ? AppColors.primary.opacity(0.1) : .clear, radius: 8)
|
||||
.shadow(color: focusedField == .password ? .blue.opacity(0.1) : .clear, radius: 8)
|
||||
.animation(.easeInOut(duration: 0.2), value: focusedField)
|
||||
.onChange(of: viewModel.password) { _, _ in
|
||||
viewModel.clearError()
|
||||
@@ -168,21 +168,21 @@ struct LoginView: View {
|
||||
showPasswordReset = true
|
||||
}
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(AppColors.primary)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
|
||||
// Error Message
|
||||
if let errorMessage = viewModel.errorMessage {
|
||||
HStack(spacing: AppSpacing.sm) {
|
||||
Image(systemName: "exclamationmark.circle.fill")
|
||||
.foregroundColor(AppColors.error)
|
||||
.foregroundColor(.red)
|
||||
Text(errorMessage)
|
||||
.font(.callout)
|
||||
.foregroundColor(AppColors.error)
|
||||
.foregroundColor(.red)
|
||||
Spacer()
|
||||
}
|
||||
.padding(AppSpacing.md)
|
||||
.background(AppColors.error.opacity(0.1))
|
||||
.background(.red.opacity(0.1))
|
||||
.cornerRadius(AppRadius.md)
|
||||
}
|
||||
|
||||
@@ -196,18 +196,18 @@ struct LoginView: View {
|
||||
HStack(spacing: AppSpacing.xs) {
|
||||
Text("Don't have an account?")
|
||||
.font(.body)
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
|
||||
Button("Sign Up") {
|
||||
showingRegister = true
|
||||
}
|
||||
.font(.body)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(AppColors.primary)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
.padding(AppSpacing.xl)
|
||||
.background(AppColors.surface)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.xxl)
|
||||
.shadow(color: .black.opacity(0.08), radius: 20, y: 10)
|
||||
.padding(.horizontal, AppSpacing.lg)
|
||||
@@ -285,18 +285,17 @@ struct LoginView: View {
|
||||
.background(loginButtonBackground)
|
||||
.cornerRadius(AppRadius.md)
|
||||
.shadow(
|
||||
color: shouldShowShadow ? AppColors.primary.opacity(0.3) : .clear,
|
||||
color: shouldShowShadow ? .blue.opacity(0.3) : .clear,
|
||||
radius: 10,
|
||||
y: 5
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var loginButtonBackground: some View {
|
||||
private var loginButtonBackground: AnyShapeStyle {
|
||||
if viewModel.isLoading || !isFormValid {
|
||||
AppColors.textTertiary
|
||||
AnyShapeStyle(Color(.tertiaryLabel))
|
||||
} else {
|
||||
AppColors.primaryGradient
|
||||
AnyShapeStyle(LinearGradient(colors: [.blue, .blue.opacity(0.8)], startPoint: .topLeading, endPoint: .bottomTrailing))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,23 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
|
||||
await PushNotificationManager.shared.requestNotificationPermission()
|
||||
}
|
||||
|
||||
// Clear badge when app launches
|
||||
Task { @MainActor in
|
||||
PushNotificationManager.shared.clearBadge()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - App Lifecycle
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Clear badge when app becomes active
|
||||
Task { @MainActor in
|
||||
PushNotificationManager.shared.clearBadge()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Remote Notifications
|
||||
|
||||
func application(
|
||||
|
||||
@@ -8,7 +8,7 @@ struct ResidencesListView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
AppColors.background
|
||||
Color(.systemGroupedBackground)
|
||||
.ignoresSafeArea()
|
||||
|
||||
if viewModel.myResidences == nil && viewModel.isLoading {
|
||||
@@ -17,7 +17,7 @@ struct ResidencesListView: View {
|
||||
.scaleEffect(1.2)
|
||||
Text("Loading properties...")
|
||||
.font(.body)
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
}
|
||||
} else if let error = viewModel.errorMessage {
|
||||
ErrorView(message: error) {
|
||||
@@ -39,10 +39,10 @@ struct ResidencesListView: View {
|
||||
VStack(alignment: .leading, spacing: AppSpacing.xxs) {
|
||||
Text("Your Properties")
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundColor(AppColors.textPrimary)
|
||||
.foregroundColor(Color(.label))
|
||||
Text("\(response.residences.count) \(response.residences.count == 1 ? "property" : "properties")")
|
||||
.font(.callout)
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
@@ -77,7 +77,7 @@ struct ResidencesListView: View {
|
||||
}) {
|
||||
Image(systemName: "person.badge.plus")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundColor(AppColors.primary)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
@@ -85,7 +85,7 @@ struct ResidencesListView: View {
|
||||
}) {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundColor(AppColors.primary)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ struct HomeNavigationCard: View {
|
||||
// Icon with gradient background
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: AppRadius.md)
|
||||
.fill(AppColors.primaryGradient)
|
||||
.fill(LinearGradient(colors: [.blue, .blue.opacity(0.8)], startPoint: .topLeading, endPoint: .bottomTrailing))
|
||||
.frame(width: 60, height: 60)
|
||||
.shadow(color: AppColors.primary.opacity(0.3), radius: 8, y: 4)
|
||||
.shadow(color: .blue.opacity(0.3), radius: 8, y: 4)
|
||||
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 28, weight: .semibold))
|
||||
@@ -24,11 +24,11 @@ struct HomeNavigationCard: View {
|
||||
Text(title)
|
||||
.font(.title3.weight(.semibold))
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(AppColors.textPrimary)
|
||||
.foregroundColor(Color(.label))
|
||||
|
||||
Text(subtitle)
|
||||
.font(.callout)
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -36,10 +36,10 @@ struct HomeNavigationCard: View {
|
||||
// Chevron
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundColor(AppColors.textTertiary)
|
||||
.foregroundColor(Color(.tertiaryLabel))
|
||||
}
|
||||
.padding(AppSpacing.lg)
|
||||
.background(AppColors.surface)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.lg)
|
||||
.shadow(color: AppShadow.md.color, radius: AppShadow.md.radius, x: AppShadow.md.x, y: AppShadow.md.y)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ struct OverviewCard: View {
|
||||
HStack(spacing: AppSpacing.sm) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(AppColors.primaryGradient)
|
||||
.fill(LinearGradient(colors: [.blue, .blue.opacity(0.8)], startPoint: .topLeading, endPoint: .bottomTrailing))
|
||||
.frame(width: 44, height: 44)
|
||||
|
||||
Image(systemName: "chart.bar.fill")
|
||||
@@ -21,7 +21,7 @@ struct OverviewCard: View {
|
||||
|
||||
Text("Overview")
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundColor(AppColors.textPrimary)
|
||||
.foregroundColor(Color(.label))
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
@@ -32,7 +32,7 @@ struct OverviewCard: View {
|
||||
icon: "house.fill",
|
||||
value: "\(summary.totalResidences)",
|
||||
label: "Properties",
|
||||
color: AppColors.primary
|
||||
color: .blue
|
||||
)
|
||||
|
||||
Divider()
|
||||
@@ -42,7 +42,7 @@ struct OverviewCard: View {
|
||||
icon: "list.bullet",
|
||||
value: "\(summary.totalTasks)",
|
||||
label: "Total Tasks",
|
||||
color: AppColors.info
|
||||
color: .blue
|
||||
)
|
||||
|
||||
Divider()
|
||||
@@ -52,12 +52,12 @@ struct OverviewCard: View {
|
||||
icon: "clock.fill",
|
||||
value: "\(summary.totalPending)",
|
||||
label: "Pending",
|
||||
color: AppColors.warning
|
||||
color: .orange
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(AppSpacing.xl)
|
||||
.background(AppColors.surface)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.xl)
|
||||
.shadow(color: AppShadow.lg.color, radius: AppShadow.lg.radius, x: AppShadow.lg.x, y: AppShadow.lg.y)
|
||||
.padding(.horizontal, AppSpacing.md)
|
||||
|
||||
@@ -4,7 +4,7 @@ struct StatView: View {
|
||||
let icon: String
|
||||
let value: String
|
||||
let label: String
|
||||
var color: Color = AppColors.primary
|
||||
var color: Color = .blue
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: AppSpacing.sm) {
|
||||
@@ -21,11 +21,11 @@ struct StatView: View {
|
||||
Text(value)
|
||||
.font(.title2.weight(.semibold))
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(AppColors.textPrimary)
|
||||
.foregroundColor(Color(.label))
|
||||
|
||||
Text(label)
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
@@ -10,9 +10,9 @@ struct ResidenceCard: View {
|
||||
HStack(spacing: AppSpacing.sm) {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: AppRadius.sm)
|
||||
.fill(AppColors.primaryGradient)
|
||||
.fill(LinearGradient(colors: [.blue, .blue.opacity(0.8)], startPoint: .topLeading, endPoint: .bottomTrailing))
|
||||
.frame(width: 44, height: 44)
|
||||
.shadow(color: AppColors.primary.opacity(0.3), radius: 6, y: 3)
|
||||
.shadow(color: .blue.opacity(0.3), radius: 6, y: 3)
|
||||
|
||||
Image(systemName: "house.fill")
|
||||
.font(.system(size: 20, weight: .semibold))
|
||||
@@ -23,12 +23,12 @@ struct ResidenceCard: View {
|
||||
Text(residence.name)
|
||||
.font(.title3.weight(.semibold))
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(AppColors.textPrimary)
|
||||
.foregroundColor(Color(.label))
|
||||
.lineLimit(1)
|
||||
|
||||
Text(residence.propertyType)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(AppColors.textTertiary)
|
||||
.foregroundColor(Color(.tertiaryLabel))
|
||||
.textCase(.uppercase)
|
||||
.tracking(0.5)
|
||||
}
|
||||
@@ -38,11 +38,11 @@ struct ResidenceCard: View {
|
||||
if residence.isPrimary {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(AppColors.accent.opacity(0.1))
|
||||
.fill(.purple.opacity(0.1))
|
||||
.frame(width: 32, height: 32)
|
||||
Image(systemName: "star.fill")
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.foregroundColor(AppColors.accent)
|
||||
.foregroundColor(.purple)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,19 +52,19 @@ struct ResidenceCard: View {
|
||||
HStack(spacing: AppSpacing.xxs) {
|
||||
Image(systemName: "mappin.circle.fill")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(AppColors.textTertiary)
|
||||
.foregroundColor(Color(.tertiaryLabel))
|
||||
Text(residence.streetAddress)
|
||||
.font(.callout)
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
}
|
||||
|
||||
HStack(spacing: AppSpacing.xxs) {
|
||||
Image(systemName: "location.fill")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(AppColors.textTertiary)
|
||||
.foregroundColor(Color(.tertiaryLabel))
|
||||
Text("\(residence.city), \(residence.stateProvince)")
|
||||
.font(.callout)
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
}
|
||||
}
|
||||
.padding(.vertical, AppSpacing.xs)
|
||||
@@ -77,26 +77,26 @@ struct ResidenceCard: View {
|
||||
icon: "list.bullet",
|
||||
value: "\(residence.taskSummary.total)",
|
||||
label: "Tasks",
|
||||
color: AppColors.info
|
||||
color: .blue
|
||||
)
|
||||
|
||||
TaskStatChip(
|
||||
icon: "checkmark.circle.fill",
|
||||
value: "\(residence.taskSummary.completed)",
|
||||
label: "Done",
|
||||
color: AppColors.success
|
||||
color: .green
|
||||
)
|
||||
|
||||
TaskStatChip(
|
||||
icon: "clock.fill",
|
||||
value: "\(residence.taskSummary.pending)",
|
||||
label: "Pending",
|
||||
color: AppColors.warning
|
||||
color: .orange
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(AppSpacing.md)
|
||||
.background(AppColors.surface)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.lg)
|
||||
.shadow(color: AppShadow.md.color, radius: AppShadow.md.radius, x: AppShadow.md.x, y: AppShadow.md.y)
|
||||
}
|
||||
@@ -138,5 +138,5 @@ struct ResidenceCard: View {
|
||||
updatedAt: "2024-01-01T00:00:00Z"
|
||||
))
|
||||
.padding()
|
||||
.background(AppColors.background)
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ struct CompletionCardView: View {
|
||||
HStack(alignment: .top, spacing: 6) {
|
||||
Image(systemName: "wrench.and.screwdriver")
|
||||
.font(.caption2)
|
||||
.foregroundColor(AppColors.primary)
|
||||
.foregroundColor(.blue)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("By: \(contractorDetails.name)")
|
||||
|
||||
@@ -13,6 +13,8 @@ struct DynamicTaskCard: View {
|
||||
let onArchive: () -> Void
|
||||
let onUnarchive: () -> Void
|
||||
|
||||
@State private var isCompletionsExpanded = false
|
||||
|
||||
var body: some View {
|
||||
let _ = print("📋 DynamicTaskCard - Task: \(task.title), ButtonTypes: \(buttonTypes)")
|
||||
|
||||
@@ -20,7 +22,7 @@ struct DynamicTaskCard: View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(task.title)
|
||||
.font(.headline)
|
||||
.font(.title3)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
if let status = task.status {
|
||||
@@ -64,10 +66,23 @@ struct DynamicTaskCard: View {
|
||||
Text("Completions (\(task.completions.count))")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Image(systemName: isCompletionsExpanded ? "chevron.up" : "chevron.down")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
|
||||
isCompletionsExpanded.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(task.completions, id: \.id) { completion in
|
||||
CompletionCardView(completion: completion)
|
||||
if isCompletionsExpanded {
|
||||
ForEach(task.completions, id: \.id) { completion in
|
||||
CompletionCardView(completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,18 +102,22 @@ struct DynamicTaskCard: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.background(Color.blue.opacity(0.1))
|
||||
.foregroundColor(.blue)
|
||||
.cornerRadius(8)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color.blue, lineWidth: 2)
|
||||
)
|
||||
}
|
||||
.zIndex(10)
|
||||
.menuOrder(.fixed)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.background(Color(.systemBackground))
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(12)
|
||||
.shadow(color: Color.black.opacity(0.05), radius: 3, x: 0, y: 2)
|
||||
.shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 2)
|
||||
.simultaneousGesture(TapGesture(), including: .subviews)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ struct PriorityBadge: View {
|
||||
|
||||
private var priorityColor: Color {
|
||||
switch priority.lowercased() {
|
||||
case "high": return AppColors.error
|
||||
case "medium": return AppColors.warning
|
||||
case "low": return AppColors.success
|
||||
default: return AppColors.textTertiary
|
||||
case "high": return .red
|
||||
case "medium": return .orange
|
||||
case "low": return .green
|
||||
default: return Color(.tertiaryLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,5 +36,5 @@ struct PriorityBadge: View {
|
||||
PriorityBadge(priority: "low")
|
||||
}
|
||||
.padding()
|
||||
.background(AppColors.background)
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
|
||||
@@ -24,11 +24,11 @@ struct StatusBadge: View {
|
||||
|
||||
private var statusColor: Color {
|
||||
switch status {
|
||||
case "completed": return AppColors.success
|
||||
case "in_progress": return AppColors.taskInProgress
|
||||
case "pending": return AppColors.warning
|
||||
case "cancelled": return AppColors.error
|
||||
default: return AppColors.textTertiary
|
||||
case "completed": return .green
|
||||
case "in_progress": return .orange
|
||||
case "pending": return .orange
|
||||
case "cancelled": return .red
|
||||
default: return Color(.tertiaryLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,5 +41,5 @@ struct StatusBadge: View {
|
||||
StatusBadge(status: "cancelled")
|
||||
}
|
||||
.padding()
|
||||
.background(AppColors.background)
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ struct TaskCard: View {
|
||||
let onArchive: (() -> Void)?
|
||||
let onUnarchive: (() -> Void)?
|
||||
|
||||
@State private var isCompletionsExpanded = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: AppSpacing.md) {
|
||||
// Header
|
||||
@@ -18,7 +20,7 @@ struct TaskCard: View {
|
||||
VStack(alignment: .leading, spacing: AppSpacing.xs) {
|
||||
Text(task.title)
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundColor(AppColors.textPrimary)
|
||||
.foregroundColor(Color(.label))
|
||||
.lineLimit(2)
|
||||
|
||||
if let status = task.status {
|
||||
@@ -35,7 +37,7 @@ struct TaskCard: View {
|
||||
if let description = task.description_, !description.isEmpty {
|
||||
Text(description)
|
||||
.font(.callout)
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
.lineLimit(3)
|
||||
}
|
||||
|
||||
@@ -44,14 +46,14 @@ struct TaskCard: View {
|
||||
HStack(spacing: AppSpacing.xxs) {
|
||||
Image(systemName: "repeat")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(AppColors.textTertiary)
|
||||
.foregroundColor(Color(.tertiaryLabel))
|
||||
Text(task.frequency.displayName)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
}
|
||||
.padding(.horizontal, AppSpacing.sm)
|
||||
.padding(.vertical, AppSpacing.xxs)
|
||||
.background(AppColors.surfaceSecondary)
|
||||
.background(Color(.tertiarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.xs)
|
||||
|
||||
Spacer()
|
||||
@@ -60,14 +62,14 @@ struct TaskCard: View {
|
||||
HStack(spacing: AppSpacing.xxs) {
|
||||
Image(systemName: "calendar")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(AppColors.textTertiary)
|
||||
.foregroundColor(Color(.tertiaryLabel))
|
||||
Text(formatDate(dueDate))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
}
|
||||
.padding(.horizontal, AppSpacing.sm)
|
||||
.padding(.vertical, AppSpacing.xxs)
|
||||
.background(AppColors.surfaceSecondary)
|
||||
.background(Color(.tertiarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.xs)
|
||||
}
|
||||
}
|
||||
@@ -81,20 +83,32 @@ struct TaskCard: View {
|
||||
HStack(spacing: AppSpacing.xs) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(AppColors.success.opacity(0.1))
|
||||
.fill(Color.green.opacity(0.1))
|
||||
.frame(width: 24, height: 24)
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundColor(AppColors.success)
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
Text("Completions (\(task.completions.count))")
|
||||
.font(.footnote.weight(.medium))
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(AppColors.textPrimary)
|
||||
.foregroundColor(Color(.label))
|
||||
Spacer()
|
||||
Image(systemName: isCompletionsExpanded ? "chevron.up" : "chevron.down")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
|
||||
isCompletionsExpanded.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(task.completions, id: \.id) { completion in
|
||||
CompletionCardView(completion: completion)
|
||||
if isCompletionsExpanded {
|
||||
ForEach(task.completions, id: \.id) { completion in
|
||||
CompletionCardView(completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,8 +127,8 @@ struct TaskCard: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 44)
|
||||
.foregroundColor(AppColors.warning)
|
||||
.background(AppColors.warning.opacity(0.1))
|
||||
.foregroundColor(.orange)
|
||||
.background(Color.orange.opacity(0.1))
|
||||
.cornerRadius(AppRadius.md)
|
||||
}
|
||||
}
|
||||
@@ -131,7 +145,7 @@ struct TaskCard: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 44)
|
||||
.foregroundColor(.white)
|
||||
.background(AppColors.success)
|
||||
.background(.green)
|
||||
.cornerRadius(AppRadius.md)
|
||||
}
|
||||
}
|
||||
@@ -150,8 +164,8 @@ struct TaskCard: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 36)
|
||||
.foregroundColor(AppColors.primary)
|
||||
.background(AppColors.surfaceSecondary)
|
||||
.foregroundColor(.blue)
|
||||
.background(Color(.tertiarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.sm)
|
||||
}
|
||||
|
||||
@@ -165,8 +179,8 @@ struct TaskCard: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 36)
|
||||
.foregroundColor(AppColors.error)
|
||||
.background(AppColors.error.opacity(0.1))
|
||||
.foregroundColor(.red)
|
||||
.background(Color.red.opacity(0.1))
|
||||
.cornerRadius(AppRadius.sm)
|
||||
}
|
||||
} else if let onUncancel = onUncancel {
|
||||
@@ -180,7 +194,7 @@ struct TaskCard: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 36)
|
||||
.foregroundColor(.white)
|
||||
.background(AppColors.primary)
|
||||
.background(.blue)
|
||||
.cornerRadius(AppRadius.sm)
|
||||
}
|
||||
}
|
||||
@@ -197,8 +211,8 @@ struct TaskCard: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 36)
|
||||
.foregroundColor(AppColors.primary)
|
||||
.background(AppColors.surfaceSecondary)
|
||||
.foregroundColor(.blue)
|
||||
.background(Color(.tertiarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.sm)
|
||||
}
|
||||
}
|
||||
@@ -213,8 +227,8 @@ struct TaskCard: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 36)
|
||||
.foregroundColor(AppColors.textSecondary)
|
||||
.background(AppColors.surfaceSecondary)
|
||||
.foregroundColor(Color(.secondaryLabel))
|
||||
.background(Color(.tertiarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.sm)
|
||||
}
|
||||
}
|
||||
@@ -222,7 +236,7 @@ struct TaskCard: View {
|
||||
}
|
||||
}
|
||||
.padding(AppSpacing.md)
|
||||
.background(AppColors.surface)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(AppRadius.lg)
|
||||
.shadow(color: AppShadow.md.color, radius: AppShadow.md.radius, x: AppShadow.md.x, y: AppShadow.md.y)
|
||||
}
|
||||
@@ -271,5 +285,5 @@ struct TaskCard: View {
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
.background(AppColors.background)
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
|
||||
@@ -393,7 +393,7 @@ struct ContractorPickerView: View {
|
||||
Spacer()
|
||||
if selectedContractor == nil {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundStyle(AppColors.primary)
|
||||
.foregroundStyle(.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -441,7 +441,7 @@ struct ContractorPickerView: View {
|
||||
|
||||
if selectedContractor?.id == contractor.id {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundStyle(AppColors.primary)
|
||||
.foregroundStyle(.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<string>development</string>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.tt.mycrib.MyCrib</string>
|
||||
<string>group.com.tt.mycrib.MyCribDev</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Reference in New Issue
Block a user