Modernize task and residence card components

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 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-10 11:53:39 -06:00
parent 8e82f43aba
commit 9305371276
2 changed files with 208 additions and 87 deletions

View File

@@ -5,51 +5,100 @@ struct ResidenceCard: View {
let residence: ResidenceWithTasks let residence: ResidenceWithTasks
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: AppSpacing.md) {
VStack(alignment: .leading, spacing: 4) { // Header with property type icon
Text(residence.name) HStack(spacing: AppSpacing.sm) {
.font(.title3) ZStack {
.fontWeight(.bold) RoundedRectangle(cornerRadius: AppRadius.sm)
.foregroundColor(.primary) .fill(AppColors.primaryGradient)
.frame(width: 44, height: 44)
.shadow(color: AppColors.primary.opacity(0.3), radius: 6, y: 3)
Text(residence.streetAddress) Image(systemName: "house.fill")
.font(.subheadline) .font(.system(size: 20, weight: .semibold))
.foregroundColor(.secondary) .foregroundColor(.white)
}
Text("\(residence.city), \(residence.stateProvince)") VStack(alignment: .leading, spacing: AppSpacing.xxs) {
.font(.subheadline) Text(residence.name)
.foregroundColor(.secondary) .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() Divider()
HStack(spacing: 24) { // Task Stats
HStack(spacing: AppSpacing.sm) {
TaskStatChip( TaskStatChip(
icon: "list.bullet", icon: "list.bullet",
value: "\(residence.taskSummary.total)", value: "\(residence.taskSummary.total)",
label: "Tasks", label: "Tasks",
color: .blue color: AppColors.info
) )
TaskStatChip( TaskStatChip(
icon: "checkmark.circle.fill", icon: "checkmark.circle.fill",
value: "\(residence.taskSummary.completed)", value: "\(residence.taskSummary.completed)",
label: "Done", label: "Done",
color: .green color: AppColors.success
) )
TaskStatChip( TaskStatChip(
icon: "clock.fill", icon: "clock.fill",
value: "\(residence.taskSummary.pending)", value: "\(residence.taskSummary.pending)",
label: "Pending", label: "Pending",
color: .orange color: AppColors.warning
) )
} }
} }
.padding(20) .padding(AppSpacing.md)
.background(Color(.systemBackground)) .background(AppColors.surface)
.cornerRadius(12) .cornerRadius(AppRadius.lg)
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2) .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" updatedAt: "2024-01-01T00:00:00Z"
)) ))
.padding() .padding()
.background(AppColors.background)
} }

View File

@@ -12,12 +12,14 @@ struct TaskCard: View {
let onUnarchive: (() -> Void)? let onUnarchive: (() -> Void)?
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: AppSpacing.md) {
HStack { // Header
VStack(alignment: .leading, spacing: 4) { HStack(alignment: .top, spacing: AppSpacing.sm) {
VStack(alignment: .leading, spacing: AppSpacing.xs) {
Text(task.title) Text(task.title)
.font(.headline) .font(AppTypography.titleMedium)
.foregroundColor(.primary) .foregroundColor(AppColors.textPrimary)
.lineLimit(2)
if let status = task.status { if let status = task.status {
StatusBadge(status: status.name) StatusBadge(status: status.name)
@@ -29,37 +31,66 @@ struct TaskCard: View {
PriorityBadge(priority: task.priority.name) PriorityBadge(priority: task.priority.name)
} }
// Description
if let description = task.description_, !description.isEmpty { if let description = task.description_, !description.isEmpty {
Text(description) Text(description)
.font(.subheadline) .font(AppTypography.bodySmall)
.foregroundColor(.secondary) .foregroundColor(AppColors.textSecondary)
.lineLimit(2) .lineLimit(3)
} }
HStack { // Metadata
Label(task.frequency.displayName, systemImage: "repeat") HStack(spacing: AppSpacing.md) {
.font(.caption) HStack(spacing: AppSpacing.xxs) {
.foregroundColor(.secondary) 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() Spacer()
if let due_date = task.dueDate { if let dueDate = task.dueDate {
Label(formatDate(due_date), systemImage: "calendar") HStack(spacing: AppSpacing.xxs) {
.font(.caption) Image(systemName: "calendar")
.foregroundColor(.secondary) .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 { if task.completions.count > 0 {
Divider() Divider()
.padding(.vertical, AppSpacing.xxs)
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: AppSpacing.sm) {
HStack { HStack(spacing: AppSpacing.xs) {
Image(systemName: "checkmark.circle.fill") ZStack {
.foregroundColor(.green) 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))") Text("Completions (\(task.completions.count))")
.font(.caption) .font(AppTypography.labelMedium)
.fontWeight(.semibold) .fontWeight(.semibold)
.foregroundColor(AppColors.textPrimary)
} }
ForEach(task.completions, id: \.id) { completion in ForEach(task.completions, id: \.id) { completion in
@@ -68,92 +99,132 @@ struct TaskCard: View {
} }
} }
// Primary Actions
if task.showCompletedButton { if task.showCompletedButton {
VStack(spacing: 8) { VStack(spacing: AppSpacing.xs) {
if let onMarkInProgress = onMarkInProgress, task.status?.name != "in_progress" { if let onMarkInProgress = onMarkInProgress, task.status?.name != "in_progress" {
Button(action: onMarkInProgress) { Button(action: onMarkInProgress) {
HStack { HStack(spacing: AppSpacing.xs) {
Image(systemName: "play.circle.fill") Image(systemName: "play.circle.fill")
.resizable() .font(.system(size: 16, weight: .semibold))
.frame(width: 18, height: 18)
Text("In Progress") Text("In Progress")
.font(.subheadline.weight(.semibold)) .font(AppTypography.labelLarge)
.fontWeight(.semibold)
} }
.frame(maxWidth: .infinity) .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 { if task.showCompletedButton, let onComplete = onComplete {
Button(action: onComplete) { Button(action: onComplete) {
HStack { HStack(spacing: AppSpacing.xs) {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.resizable() .font(.system(size: 16, weight: .semibold))
.frame(width: 18, height: 18)
Text("Complete") Text("Complete")
.font(.subheadline.weight(.semibold)) .font(AppTypography.labelLarge)
.fontWeight(.semibold)
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.frame(height: 44)
.foregroundColor(.white)
.background(AppColors.success)
.cornerRadius(AppRadius.md)
} }
.buttonStyle(.borderedProminent)
} }
} }
} }
VStack(spacing: 8) { // Secondary Actions
Button(action: onEdit) { VStack(spacing: AppSpacing.xs) {
Label("Edit", systemImage: "pencil") HStack(spacing: AppSpacing.xs) {
.font(.subheadline) Button(action: onEdit) {
HStack(spacing: AppSpacing.xxs) {
Image(systemName: "pencil")
.font(.system(size: 14, weight: .medium))
Text("Edit")
.font(AppTypography.labelMedium)
}
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} .frame(height: 36)
.buttonStyle(.bordered) .foregroundColor(AppColors.primary)
.background(AppColors.surfaceSecondary)
.cornerRadius(AppRadius.sm)
}
if let onCancel = onCancel { if let onCancel = onCancel {
Button(action: onCancel) { Button(action: onCancel) {
Label("Cancel", systemImage: "xmark.circle") HStack(spacing: AppSpacing.xxs) {
.font(.subheadline) Image(systemName: "xmark.circle")
.font(.system(size: 14, weight: .medium))
Text("Cancel")
.font(AppTypography.labelMedium)
}
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} .frame(height: 36)
.buttonStyle(.bordered) .foregroundColor(AppColors.error)
.tint(.red) .background(AppColors.error.opacity(0.1))
} else if let onUncancel = onUncancel { .cornerRadius(AppRadius.sm)
Button(action: onUncancel) { }
Label("Restore", systemImage: "arrow.uturn.backward") } else if let onUncancel = onUncancel {
.font(.subheadline) 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(maxWidth: .infinity)
.frame(height: 36)
.foregroundColor(.white)
.background(AppColors.primary)
.cornerRadius(AppRadius.sm)
}
} }
.buttonStyle(.borderedProminent)
.tint(.blue)
} }
if task.archived { if task.archived {
if let onUnarchive = onUnarchive { if let onUnarchive = onUnarchive {
Button(action: onUnarchive) { Button(action: onUnarchive) {
Label("Unarchive", systemImage: "tray.and.arrow.up") HStack(spacing: AppSpacing.xxs) {
.font(.subheadline) Image(systemName: "tray.and.arrow.up")
.frame(maxWidth: .infinity) .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 { } else {
if let onArchive = onArchive { if let onArchive = onArchive {
Button(action: onArchive) { Button(action: onArchive) {
Label("Archive", systemImage: "archivebox") HStack(spacing: AppSpacing.xxs) {
.font(.subheadline) Image(systemName: "archivebox")
.frame(maxWidth: .infinity) .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) .padding(AppSpacing.md)
.background(Color(.systemBackground)) .background(AppColors.surface)
.cornerRadius(12) .cornerRadius(AppRadius.lg)
.shadow(color: Color.black.opacity(0.05), radius: 3, x: 0, y: 2) .shadow(color: AppShadow.md.color, radius: AppShadow.md.radius, x: AppShadow.md.x, y: AppShadow.md.y)
} }
private func formatDate(_ dateString: String) -> String { private func formatDate(_ dateString: String) -> String {
@@ -200,5 +271,5 @@ struct TaskCard: View {
) )
} }
.padding() .padding()
.background(Color(.systemGroupedBackground)) .background(AppColors.background)
} }