commit 2d158a50fc69bb391a6bcc75d11eba4403e27158
parent 31a6c4911c5d2ec1011a29d0a050ea174b76255c
Author: Michael Camilleri <[email protected]>
Date: Sun, 15 Feb 2026 05:32:54 +0900
Use accent colours in iOS
Co-Authored-By: Claude 4.5 Sonnet <[email protected]>
Diffstat:
4 files changed, 73 insertions(+), 11 deletions(-)
diff --git a/Listless/Views/TaskListView.swift b/Listless/Views/TaskListView.swift
@@ -126,7 +126,7 @@ struct TaskListView: View {
}
.frame(maxWidth: .infinity, alignment: .topLeading)
#if os(iOS)
- .padding(.horizontal, 16)
+ .padding(.trailing, 16)
.padding(.vertical, 12)
#endif
.dropDestination(for: String.self) { items, location in
diff --git a/ListlessiOS/Views/AppColors.swift b/ListlessiOS/Views/AppColors.swift
@@ -2,15 +2,15 @@ import SwiftUI
import UIKit
extension Color {
- /// Warm gray canvas shown behind task cards and beneath completed-task text.
+ /// Canvas behind task cards: warm gray in light mode, black in dark mode.
static let outerBackground = Color(uiColor: UIColor { traits in
traits.userInterfaceStyle == .dark
- ? UIColor(red: 0.173, green: 0.165, blue: 0.153, alpha: 1) // #2C2A27
+ ? .black
: UIColor(red: 0.922, green: 0.906, blue: 0.886, alpha: 1) // #EBE7E2
})
- /// Stark card background: white in light mode, black in dark mode.
+ /// Card surface: white in light mode, elevated dark gray in dark mode.
static let taskCard = Color(uiColor: UIColor { traits in
- traits.userInterfaceStyle == .dark ? .black : .white
+ traits.userInterfaceStyle == .dark ? UIColor.secondarySystemBackground : .white
})
}
diff --git a/ListlessiOS/Views/TaskRowSwipeGesture.swift b/ListlessiOS/Views/TaskRowSwipeGesture.swift
@@ -9,6 +9,7 @@ extension View {
swipeOffset: Binding<CGFloat>,
swipeDirection: Binding<TaskRowSwipeGesture.SwipeDirection>,
isTriggered: Binding<Bool>,
+ completeColor: Color = .green,
onComplete: @escaping () -> Void,
onDelete: @escaping () -> Void,
onSwipeActiveChanged: @escaping (Bool) -> Void = { _ in }
@@ -21,6 +22,7 @@ extension View {
swipeOffset: swipeOffset,
swipeDirection: swipeDirection,
isTriggered: isTriggered,
+ completeColor: completeColor,
onComplete: onComplete,
onDelete: onDelete,
onSwipeActiveChanged: onSwipeActiveChanged
@@ -35,6 +37,7 @@ struct TaskRowSwipeGesture: ViewModifier {
@Binding var swipeOffset: CGFloat
@Binding var swipeDirection: SwipeDirection
@Binding var isTriggered: Bool
+ let completeColor: Color
let onComplete: () -> Void
let onDelete: () -> Void
let onSwipeActiveChanged: (Bool) -> Void
@@ -85,8 +88,8 @@ struct TaskRowSwipeGesture: ViewModifier {
@ViewBuilder
private var swipeBackground: some View {
if swipeDirection == .right {
- // Complete action — plain green background
- Color.green.opacity(backgroundOpacity(offset: swipeOffset))
+ // Complete action — accent color background
+ completeColor.opacity(backgroundOpacity(offset: swipeOffset))
} else if swipeDirection == .left {
// Delete action (red background, trash icon)
Color.red.opacity(backgroundOpacity(offset: swipeOffset))
diff --git a/ListlessiOS/Views/TaskRowView.swift b/ListlessiOS/Views/TaskRowView.swift
@@ -3,6 +3,8 @@ import SwiftUI
struct TaskRowView: View {
let task: TaskItem
let taskID: UUID
+ let index: Int
+ let totalTasks: Int
let isSelected: Bool
let onToggle: (TaskItem) -> Void
let onTitleChange: (TaskItem, String) -> Void
@@ -17,6 +19,7 @@ struct TaskRowView: View {
@State private var swipeOffset: CGFloat = 0
@State private var swipeDirection: TaskRowSwipeGesture.SwipeDirection = .none
@State private var isSwipeTriggered: Bool = false
+ @State private var cachedAccentColor: Color = .clear
init(
task: TaskItem,
@@ -35,6 +38,8 @@ struct TaskRowView: View {
) {
self.task = task
self.taskID = taskID
+ self.index = index
+ self.totalTasks = totalTasks
self.isSelected = isSelected
self.onToggle = onToggle
self.onTitleChange = onTitleChange
@@ -95,15 +100,32 @@ struct TaskRowView: View {
}
}
.background(cardBackground)
- .clipShape(RoundedRectangle(cornerRadius: task.isCompleted ? 0 : 14))
+ .clipShape(
+ UnevenRoundedRectangle(
+ topLeadingRadius: 0, bottomLeadingRadius: 0,
+ bottomTrailingRadius: task.isCompleted ? 0 : 14,
+ topTrailingRadius: task.isCompleted ? 0 : 14
+ )
+ )
.overlay {
if isSelected && !task.isCompleted {
- RoundedRectangle(cornerRadius: 14)
- .strokeBorder(Color.accentColor, lineWidth: 2)
+ UnevenRoundedRectangle(
+ topLeadingRadius: 0, bottomLeadingRadius: 0,
+ bottomTrailingRadius: 14, topTrailingRadius: 14
+ )
+ .strokeBorder(Color.accentColor, lineWidth: 2)
+ }
+ }
+ .overlay(alignment: .leading) {
+ if !task.isCompleted {
+ Rectangle()
+ .fill(cachedAccentColor)
+ .frame(width: 8)
}
}
.onAppear {
editingTitle = task.title
+ cachedAccentColor = computeAccentColor()
}
.onChange(of: editingTitle) {
guard !task.isCompleted else { return }
@@ -114,6 +136,9 @@ struct TaskRowView: View {
editingTitle = newValue
}
}
+ .onChange(of: "\(index)-\(totalTasks)") { _, _ in
+ cachedAccentColor = computeAccentColor()
+ }
.taskSwipeGesture(
isActive: true,
isEditing: isCurrentlyEditing,
@@ -121,10 +146,44 @@ struct TaskRowView: View {
swipeOffset: $swipeOffset,
swipeDirection: $swipeDirection,
isTriggered: $isSwipeTriggered,
+ completeColor: cachedAccentColor,
onComplete: { onToggle(task) },
onDelete: { onDelete(task) }
)
- .clipShape(RoundedRectangle(cornerRadius: task.isCompleted ? 0 : 14))
+ .clipShape(
+ UnevenRoundedRectangle(
+ topLeadingRadius: 0, bottomLeadingRadius: 0,
+ bottomTrailingRadius: task.isCompleted ? 0 : 14,
+ topTrailingRadius: task.isCompleted ? 0 : 14
+ )
+ )
+ }
+
+ private func computeAccentColor() -> Color {
+ guard !task.isCompleted else { return .clear }
+ guard totalTasks > 1 else { return Color(hue: 0.98, saturation: 0.85, brightness: 1.0) }
+
+ // Gradient: coral/red → pink/magenta → purple/blue (matches macOS)
+ let progress = Double(index) / Double(totalTasks - 1)
+ let topColor = Color(hue: 0.98, saturation: 0.85, brightness: 1.0)
+ let midColor = Color(hue: 0.88, saturation: 0.75, brightness: 0.95)
+ let bottomColor = Color(hue: 0.72, saturation: 0.65, brightness: 0.85)
+
+ if progress < 0.5 {
+ return interpolateColor(from: topColor, to: midColor, progress: progress * 2.0)
+ } else {
+ return interpolateColor(from: midColor, to: bottomColor, progress: (progress - 0.5) * 2.0)
+ }
+ }
+
+ private func interpolateColor(from: Color, to: Color, progress: Double) -> Color {
+ let fromHSB = PlatformColor(from).hsba
+ let toHSB = PlatformColor(to).hsba
+ return Color(
+ hue: fromHSB.hue + (toHSB.hue - fromHSB.hue) * progress,
+ saturation: fromHSB.saturation + (toHSB.saturation - fromHSB.saturation) * progress,
+ brightness: fromHSB.brightness + (toHSB.brightness - fromHSB.brightness) * progress
+ )
}
@ViewBuilder