From 9305371276a541f8c77e1c116609b3655e11eebe Mon Sep 17 00:00:00 2001 From: Trey t Date: Mon, 10 Nov 2025 11:53:39 -0600 Subject: [PATCH] Modernize task and residence card components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied modern design system to card components for consistent, sleek appearance. TaskCard Improvements: - Modern typography with design system fonts - Pill-style metadata badges with icons - Color-coded action buttons (success green, warning orange) - Improved spacing and visual hierarchy - Enhanced completion section with icon badge - Redesigned secondary actions with better contrast ResidenceCard Improvements: - Gradient icon background for property type - Star badge for primary residence with accent color - Location icons for address fields - Modernized task stats with color coding - Consistent shadow and corner radius Both cards now use: - AppColors for consistent color palette - AppTypography for font hierarchy - AppSpacing for consistent spacing - AppRadius for uniform corner radii - AppShadow for depth and elevation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../Subviews/Residence/ResidenceCard.swift | 90 ++++++-- iosApp/iosApp/Subviews/Task/TaskCard.swift | 205 ++++++++++++------ 2 files changed, 208 insertions(+), 87 deletions(-) diff --git a/iosApp/iosApp/Subviews/Residence/ResidenceCard.swift b/iosApp/iosApp/Subviews/Residence/ResidenceCard.swift index d43b488..ec48804 100644 --- a/iosApp/iosApp/Subviews/Residence/ResidenceCard.swift +++ b/iosApp/iosApp/Subviews/Residence/ResidenceCard.swift @@ -5,51 +5,100 @@ struct ResidenceCard: View { let residence: ResidenceWithTasks var body: some View { - VStack(alignment: .leading, spacing: 12) { - VStack(alignment: .leading, spacing: 4) { - Text(residence.name) - .font(.title3) - .fontWeight(.bold) - .foregroundColor(.primary) + VStack(alignment: .leading, spacing: AppSpacing.md) { + // Header with property type icon + HStack(spacing: AppSpacing.sm) { + ZStack { + RoundedRectangle(cornerRadius: AppRadius.sm) + .fill(AppColors.primaryGradient) + .frame(width: 44, height: 44) + .shadow(color: AppColors.primary.opacity(0.3), radius: 6, y: 3) - Text(residence.streetAddress) - .font(.subheadline) - .foregroundColor(.secondary) + Image(systemName: "house.fill") + .font(.system(size: 20, weight: .semibold)) + .foregroundColor(.white) + } - Text("\(residence.city), \(residence.stateProvince)") - .font(.subheadline) - .foregroundColor(.secondary) + VStack(alignment: .leading, spacing: AppSpacing.xxs) { + Text(residence.name) + .font(AppTypography.titleMedium) + .fontWeight(.bold) + .foregroundColor(AppColors.textPrimary) + .lineLimit(1) + + Text(residence.propertyType) + .font(AppTypography.labelSmall) + .foregroundColor(AppColors.textTertiary) + .textCase(.uppercase) + .tracking(0.5) + } + + Spacer() + + if residence.isPrimary { + ZStack { + Circle() + .fill(AppColors.accent.opacity(0.1)) + .frame(width: 32, height: 32) + Image(systemName: "star.fill") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(AppColors.accent) + } + } } + // Address + VStack(alignment: .leading, spacing: AppSpacing.xxs) { + HStack(spacing: AppSpacing.xxs) { + Image(systemName: "mappin.circle.fill") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(AppColors.textTertiary) + Text(residence.streetAddress) + .font(AppTypography.bodySmall) + .foregroundColor(AppColors.textSecondary) + } + + HStack(spacing: AppSpacing.xxs) { + Image(systemName: "location.fill") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(AppColors.textTertiary) + Text("\(residence.city), \(residence.stateProvince)") + .font(AppTypography.bodySmall) + .foregroundColor(AppColors.textSecondary) + } + } + .padding(.vertical, AppSpacing.xs) + Divider() - HStack(spacing: 24) { + // Task Stats + HStack(spacing: AppSpacing.sm) { TaskStatChip( icon: "list.bullet", value: "\(residence.taskSummary.total)", label: "Tasks", - color: .blue + color: AppColors.info ) TaskStatChip( icon: "checkmark.circle.fill", value: "\(residence.taskSummary.completed)", label: "Done", - color: .green + color: AppColors.success ) TaskStatChip( icon: "clock.fill", value: "\(residence.taskSummary.pending)", label: "Pending", - color: .orange + color: AppColors.warning ) } } - .padding(20) - .background(Color(.systemBackground)) - .cornerRadius(12) - .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2) + .padding(AppSpacing.md) + .background(AppColors.surface) + .cornerRadius(AppRadius.lg) + .shadow(color: AppShadow.md.color, radius: AppShadow.md.radius, x: AppShadow.md.x, y: AppShadow.md.y) } } @@ -89,4 +138,5 @@ struct ResidenceCard: View { updatedAt: "2024-01-01T00:00:00Z" )) .padding() + .background(AppColors.background) } diff --git a/iosApp/iosApp/Subviews/Task/TaskCard.swift b/iosApp/iosApp/Subviews/Task/TaskCard.swift index 9e4948b..709f2eb 100644 --- a/iosApp/iosApp/Subviews/Task/TaskCard.swift +++ b/iosApp/iosApp/Subviews/Task/TaskCard.swift @@ -12,12 +12,14 @@ struct TaskCard: View { let onUnarchive: (() -> Void)? var body: some View { - VStack(alignment: .leading, spacing: 12) { - HStack { - VStack(alignment: .leading, spacing: 4) { + VStack(alignment: .leading, spacing: AppSpacing.md) { + // Header + HStack(alignment: .top, spacing: AppSpacing.sm) { + VStack(alignment: .leading, spacing: AppSpacing.xs) { Text(task.title) - .font(.headline) - .foregroundColor(.primary) + .font(AppTypography.titleMedium) + .foregroundColor(AppColors.textPrimary) + .lineLimit(2) if let status = task.status { StatusBadge(status: status.name) @@ -29,37 +31,66 @@ struct TaskCard: View { PriorityBadge(priority: task.priority.name) } + // Description if let description = task.description_, !description.isEmpty { Text(description) - .font(.subheadline) - .foregroundColor(.secondary) - .lineLimit(2) + .font(AppTypography.bodySmall) + .foregroundColor(AppColors.textSecondary) + .lineLimit(3) } - HStack { - Label(task.frequency.displayName, systemImage: "repeat") - .font(.caption) - .foregroundColor(.secondary) + // Metadata + HStack(spacing: AppSpacing.md) { + HStack(spacing: AppSpacing.xxs) { + Image(systemName: "repeat") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(AppColors.textTertiary) + Text(task.frequency.displayName) + .font(AppTypography.labelSmall) + .foregroundColor(AppColors.textSecondary) + } + .padding(.horizontal, AppSpacing.sm) + .padding(.vertical, AppSpacing.xxs) + .background(AppColors.surfaceSecondary) + .cornerRadius(AppRadius.xs) Spacer() - if let due_date = task.dueDate { - Label(formatDate(due_date), systemImage: "calendar") - .font(.caption) - .foregroundColor(.secondary) + if let dueDate = task.dueDate { + HStack(spacing: AppSpacing.xxs) { + Image(systemName: "calendar") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(AppColors.textTertiary) + Text(formatDate(dueDate)) + .font(AppTypography.labelSmall) + .foregroundColor(AppColors.textSecondary) + } + .padding(.horizontal, AppSpacing.sm) + .padding(.vertical, AppSpacing.xxs) + .background(AppColors.surfaceSecondary) + .cornerRadius(AppRadius.xs) } } + // Completions if task.completions.count > 0 { Divider() + .padding(.vertical, AppSpacing.xxs) - VStack(alignment: .leading, spacing: 8) { - HStack { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.green) + VStack(alignment: .leading, spacing: AppSpacing.sm) { + HStack(spacing: AppSpacing.xs) { + ZStack { + Circle() + .fill(AppColors.success.opacity(0.1)) + .frame(width: 24, height: 24) + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(AppColors.success) + } Text("Completions (\(task.completions.count))") - .font(.caption) + .font(AppTypography.labelMedium) .fontWeight(.semibold) + .foregroundColor(AppColors.textPrimary) } ForEach(task.completions, id: \.id) { completion in @@ -68,92 +99,132 @@ struct TaskCard: View { } } + // Primary Actions if task.showCompletedButton { - VStack(spacing: 8) { + VStack(spacing: AppSpacing.xs) { if let onMarkInProgress = onMarkInProgress, task.status?.name != "in_progress" { Button(action: onMarkInProgress) { - HStack { + HStack(spacing: AppSpacing.xs) { Image(systemName: "play.circle.fill") - .resizable() - .frame(width: 18, height: 18) + .font(.system(size: 16, weight: .semibold)) Text("In Progress") - .font(.subheadline.weight(.semibold)) + .font(AppTypography.labelLarge) + .fontWeight(.semibold) } .frame(maxWidth: .infinity) + .frame(height: 44) + .foregroundColor(AppColors.warning) + .background(AppColors.warning.opacity(0.1)) + .cornerRadius(AppRadius.md) } - .buttonStyle(.bordered) - .tint(.orange) } if task.showCompletedButton, let onComplete = onComplete { Button(action: onComplete) { - HStack { + HStack(spacing: AppSpacing.xs) { Image(systemName: "checkmark.circle.fill") - .resizable() - .frame(width: 18, height: 18) + .font(.system(size: 16, weight: .semibold)) Text("Complete") - .font(.subheadline.weight(.semibold)) + .font(AppTypography.labelLarge) + .fontWeight(.semibold) } .frame(maxWidth: .infinity) + .frame(height: 44) + .foregroundColor(.white) + .background(AppColors.success) + .cornerRadius(AppRadius.md) } - .buttonStyle(.borderedProminent) } } } - VStack(spacing: 8) { - Button(action: onEdit) { - Label("Edit", systemImage: "pencil") - .font(.subheadline) + // Secondary Actions + VStack(spacing: AppSpacing.xs) { + HStack(spacing: AppSpacing.xs) { + Button(action: onEdit) { + HStack(spacing: AppSpacing.xxs) { + Image(systemName: "pencil") + .font(.system(size: 14, weight: .medium)) + Text("Edit") + .font(AppTypography.labelMedium) + } .frame(maxWidth: .infinity) - } - .buttonStyle(.bordered) + .frame(height: 36) + .foregroundColor(AppColors.primary) + .background(AppColors.surfaceSecondary) + .cornerRadius(AppRadius.sm) + } - if let onCancel = onCancel { - Button(action: onCancel) { - Label("Cancel", systemImage: "xmark.circle") - .font(.subheadline) + if let onCancel = onCancel { + Button(action: onCancel) { + HStack(spacing: AppSpacing.xxs) { + Image(systemName: "xmark.circle") + .font(.system(size: 14, weight: .medium)) + Text("Cancel") + .font(AppTypography.labelMedium) + } .frame(maxWidth: .infinity) - } - .buttonStyle(.bordered) - .tint(.red) - } else if let onUncancel = onUncancel { - Button(action: onUncancel) { - Label("Restore", systemImage: "arrow.uturn.backward") - .font(.subheadline) + .frame(height: 36) + .foregroundColor(AppColors.error) + .background(AppColors.error.opacity(0.1)) + .cornerRadius(AppRadius.sm) + } + } else if let onUncancel = onUncancel { + Button(action: onUncancel) { + HStack(spacing: AppSpacing.xxs) { + Image(systemName: "arrow.uturn.backward") + .font(.system(size: 14, weight: .medium)) + Text("Restore") + .font(AppTypography.labelMedium) + } .frame(maxWidth: .infinity) + .frame(height: 36) + .foregroundColor(.white) + .background(AppColors.primary) + .cornerRadius(AppRadius.sm) + } } - .buttonStyle(.borderedProminent) - .tint(.blue) } if task.archived { if let onUnarchive = onUnarchive { Button(action: onUnarchive) { - Label("Unarchive", systemImage: "tray.and.arrow.up") - .font(.subheadline) - .frame(maxWidth: .infinity) + HStack(spacing: AppSpacing.xxs) { + Image(systemName: "tray.and.arrow.up") + .font(.system(size: 14, weight: .medium)) + Text("Unarchive") + .font(AppTypography.labelMedium) + } + .frame(maxWidth: .infinity) + .frame(height: 36) + .foregroundColor(AppColors.primary) + .background(AppColors.surfaceSecondary) + .cornerRadius(AppRadius.sm) } - .buttonStyle(.bordered) - .tint(.blue) } } else { if let onArchive = onArchive { Button(action: onArchive) { - Label("Archive", systemImage: "archivebox") - .font(.subheadline) - .frame(maxWidth: .infinity) + HStack(spacing: AppSpacing.xxs) { + Image(systemName: "archivebox") + .font(.system(size: 14, weight: .medium)) + Text("Archive") + .font(AppTypography.labelMedium) + } + .frame(maxWidth: .infinity) + .frame(height: 36) + .foregroundColor(AppColors.textSecondary) + .background(AppColors.surfaceSecondary) + .cornerRadius(AppRadius.sm) } - .buttonStyle(.bordered) - .tint(.gray) } } } } - .padding(16) - .background(Color(.systemBackground)) - .cornerRadius(12) - .shadow(color: Color.black.opacity(0.05), radius: 3, x: 0, y: 2) + .padding(AppSpacing.md) + .background(AppColors.surface) + .cornerRadius(AppRadius.lg) + .shadow(color: AppShadow.md.color, radius: AppShadow.md.radius, x: AppShadow.md.x, y: AppShadow.md.y) } private func formatDate(_ dateString: String) -> String { @@ -200,5 +271,5 @@ struct TaskCard: View { ) } .padding() - .background(Color(.systemGroupedBackground)) + .background(AppColors.background) }