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