listless

A simple list app for Apple platforms
Log | Files | Refs | README | LICENSE

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:
MListless/Views/TaskListView.swift | 2+-
MListlessiOS/Views/AppColors.swift | 8++++----
MListlessiOS/Views/TaskRowSwipeGesture.swift | 7+++++--
MListlessiOS/Views/TaskRowView.swift | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
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