Matrix: TickTick-faithful placement — axis-split urgency, category windows, Manual override (KC-21)

Refines the Eisenhower Matrix toward TickTick's "view over attributes" model:

- Split the single quadrant override into two axes: importance is always
  Priority (High = top row); urgency is date-derived unless manually pinned
  via the new `urgencyOverride` flag. Horizontal drags pin urgency instead of
  fabricating/destroying a real due date; vertical drags set Priority directly.
- A drag only flags "Manual" when the target column disagrees with the
  date-derived urgency, so dropping into a task's natural column stays on Auto.
- Tiles show a "Manual" badge; the task menu gains "Reset Matrix Urgency".
- Urgency window is now 3 days by default, widened to 14 for prep-heavy event
  types (birthday/domain/annual + subscription/renewal/exam/deadline titles).
- New tasks no longer default to today — no date stays no date (not urgent).
- Stored `quadrant` is kept synced to the computed placement so task color and
  widget accents follow the Matrix; Settings stats count by displayQuadrant.
- Completed tasks are hidden from the Matrix grid by default (toggle to show).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kutesir
2026-06-15 13:22:25 +03:00
parent fae56cb2a8
commit 4b16e8587d
6 changed files with 121 additions and 38 deletions

View File

@@ -607,6 +607,7 @@ struct CalendarView: View {
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
onEditEvent: { editingEvent = EditableEvent(event: $0) },
onDeleteEvent: { calStore.deleteEvent($0) }
)
@@ -1585,6 +1586,7 @@ struct DayTimelineView: View {
var onMove: ((TaskItem, Quadrant) -> Void)? = nil
var onSetPriority: ((TaskItem, Priority) -> Void)? = nil
var onSetCategory: ((TaskItem, TaskCategory) -> Void)? = nil
var onResetMatrixUrgency: ((TaskItem) -> Void)? = nil
// Calendar events (non-subscription only): edit/delete in the real calendar.
var onEditEvent: ((EKEvent) -> Void)? = nil
var onDeleteEvent: ((EKEvent) -> Void)? = nil
@@ -1747,6 +1749,7 @@ struct DayTimelineView: View {
onMove: { onMove?($0, $1) },
onSetPriority: { onSetPriority?($0, $1) },
onSetCategory: { onSetCategory?($0, $1) },
onResetMatrixUrgency: onResetMatrixUrgency,
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
onEdit: onEdit,
onDelete: { onDelete?($0) }

View File

@@ -5,7 +5,7 @@ struct MatrixView: View {
@EnvironmentObject var taskVM: TaskViewModel
@State private var showAddTask = false
@State private var dropTarget: Quadrant? = nil
@State private var hideCompleted = false
@State private var hideCompleted = true
@State private var showUserGuide = false
@State private var drillQuadrant: Quadrant? = nil
@State private var showDrill = false
@@ -67,6 +67,7 @@ struct MatrixView: View {
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
onDelete: { taskVM.delete($0) },
onTapHeader: { drillQuadrant = .urgent; showDrill = true }
)
@@ -85,6 +86,7 @@ struct MatrixView: View {
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
onDelete: { taskVM.delete($0) },
onTapHeader: { drillQuadrant = .schedule; showDrill = true }
)
@@ -107,6 +109,7 @@ struct MatrixView: View {
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
onDelete: { taskVM.delete($0) },
onTapHeader: { drillQuadrant = .delegate_; showDrill = true }
)
@@ -125,6 +128,7 @@ struct MatrixView: View {
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
onDelete: { taskVM.delete($0) },
onTapHeader: { drillQuadrant = .eliminate; showDrill = true }
)
@@ -188,6 +192,7 @@ struct QuadrantCard: View {
var onMove: ((TaskItem, Quadrant) -> Void)? = nil
var onSetCategory: ((TaskItem, TaskCategory) -> Void)? = nil
var onSetPriority: ((TaskItem, Priority) -> Void)? = nil
var onResetMatrixUrgency: ((TaskItem) -> Void)? = nil
var onDelete: ((TaskItem) -> Void)? = nil
var onTapHeader: (() -> Void)? = nil
@@ -265,6 +270,15 @@ struct QuadrantCard: View {
.font(.system(size: 7, weight: .bold))
.foregroundColor(badgeColor.opacity(0.7))
}
if task.urgencyOverride != nil {
Text("Manual")
.font(AppFonts.mono(7, weight: .bold))
.foregroundColor(badgeColor)
.padding(.horizontal, 4)
.padding(.vertical, 2)
.background(badgeBg)
.clipShape(Capsule())
}
Text(task.title)
.font(AppFonts.sans(12, weight: .medium))
.foregroundColor(task.isComplete ? AppColors.text3(cs) : AppColors.text(cs))
@@ -291,6 +305,7 @@ struct QuadrantCard: View {
onMove: { t, q in withAnimation(KisaniSpring.snappy) { onMove?(t, q) } },
onSetPriority: { onSetPriority?($0, $1) },
onSetCategory: { onSetCategory?($0, $1) },
onResetMatrixUrgency: { t in withAnimation(KisaniSpring.snappy) { onResetMatrixUrgency?(t) } },
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
onDelete: { t in withAnimation(KisaniSpring.snappy) { onDelete?(t) } }
)
@@ -449,6 +464,7 @@ struct QuadrantDetailView: View {
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
onEdit: { editingTask = $0 },
onDelete: { taskVM.delete($0) }
@@ -474,6 +490,7 @@ struct QuadrantDetailView: View {
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
onEdit: { editingTask = $0 },
onDelete: { taskVM.delete($0) }
@@ -500,6 +517,7 @@ struct QuadrantDetailView: View {
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
onEdit: { editingTask = $0 },
onDelete: { taskVM.delete($0) }
@@ -622,12 +640,23 @@ private struct QDTaskRow: View {
Button(action: onTap) {
HStack(alignment: .top) {
Text(task.title)
.font(AppFonts.sans(14, weight: task.isComplete ? .regular : .medium))
.foregroundColor(task.isComplete ? AppColors.text3(cs) : AppColors.text(cs))
.strikethrough(task.isComplete, color: AppColors.text3(cs))
.lineLimit(2)
.frame(maxWidth: .infinity, alignment: .leading)
VStack(alignment: .leading, spacing: 4) {
if task.urgencyOverride != nil {
Text("Manual")
.font(AppFonts.mono(8, weight: .bold))
.foregroundColor(quadrant.color)
.padding(.horizontal, 5)
.padding(.vertical, 2)
.background(quadrant.softColor)
.clipShape(Capsule())
}
Text(task.title)
.font(AppFonts.sans(14, weight: task.isComplete ? .regular : .medium))
.foregroundColor(task.isComplete ? AppColors.text3(cs) : AppColors.text(cs))
.strikethrough(task.isComplete, color: AppColors.text3(cs))
.lineLimit(2)
}
.frame(maxWidth: .infinity, alignment: .leading)
VStack(alignment: .trailing, spacing: 3) {
if let d = task.dueDate {

View File

@@ -1051,7 +1051,7 @@ private struct ProfileStatsSheet: View {
return taskVM.tasks.filter { $0.isComplete && ($0.completedAt ?? .distantPast) >= start }.count
}
private var urgentOpen: Int {
taskVM.tasks.filter { !$0.isComplete && $0.quadrant == .urgent }.count
taskVM.tasks.filter { !$0.isComplete && taskVM.displayQuadrant($0) == .urgent }.count
}
// 7-day task activity
@@ -1140,10 +1140,10 @@ private struct ProfileStatsSheet: View {
// Quadrant breakdown
HStack(spacing: 8) {
QuadStat(label: "Urgent", count: taskVM.tasks.filter { $0.quadrant == .urgent && !$0.isComplete }.count, color: AppColors.accent)
QuadStat(label: "Schedule", count: taskVM.tasks.filter { $0.quadrant == .schedule && !$0.isComplete }.count, color: AppColors.yellow)
QuadStat(label: "Delegate", count: taskVM.tasks.filter { $0.quadrant == .delegate_ && !$0.isComplete }.count, color: AppColors.blue)
QuadStat(label: "Eliminate",count: taskVM.tasks.filter { $0.quadrant == .eliminate && !$0.isComplete }.count, color: AppColors.green)
QuadStat(label: "Urgent", count: taskVM.tasks.filter { !$0.isComplete && taskVM.displayQuadrant($0) == .urgent }.count, color: AppColors.accent)
QuadStat(label: "Schedule", count: taskVM.tasks.filter { !$0.isComplete && taskVM.displayQuadrant($0) == .schedule }.count, color: AppColors.yellow)
QuadStat(label: "Delegate", count: taskVM.tasks.filter { !$0.isComplete && taskVM.displayQuadrant($0) == .delegate_ }.count, color: AppColors.blue)
QuadStat(label: "Eliminate",count: taskVM.tasks.filter { !$0.isComplete && taskVM.displayQuadrant($0) == .eliminate }.count, color: AppColors.green)
}
}
.padding(14).background(AppColors.surface(cs))

View File

@@ -35,6 +35,7 @@ struct TaskMenuItems: View {
var onMove: (TaskItem, Quadrant) -> Void = { _, _ in }
var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in }
var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in }
var onResetMatrixUrgency: ((TaskItem) -> Void)? = nil
var onLiveActivity:(TaskItem) -> Void = { LiveActivityManager.toggle(for: $0) }
var onEdit: ((TaskItem) -> Void)? = nil
var onDelete: (TaskItem) -> Void = { _ in }
@@ -73,6 +74,12 @@ struct TaskMenuItems: View {
}
} label: { Label("Move", systemImage: "arrow.right.square") }
if task.urgencyOverride != nil, let onResetMatrixUrgency {
Button { onResetMatrixUrgency(task) } label: {
Label("Reset Matrix Urgency", systemImage: "arrow.counterclockwise")
}
}
Menu {
ForEach(Priority.allCases) { p in
Button { onSetPriority(task, p) } label: {

View File

@@ -113,7 +113,8 @@ struct TodayView: View {
onSetDate: { taskVM.setDate($0, date: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) }
onSetCategory: { taskVM.setCategory($0, category: $1) },
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) }
)
.padding(.horizontal, 18)
@@ -130,7 +131,8 @@ struct TodayView: View {
onSetDate: { taskVM.setDate($0, date: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) }
onSetCategory: { taskVM.setCategory($0, category: $1) },
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) }
)
.padding(.horizontal, 18)
.padding(.top, 10)
@@ -149,7 +151,8 @@ struct TodayView: View {
onSetDate: { taskVM.setDate($0, date: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) }
onSetCategory: { taskVM.setCategory($0, category: $1) },
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) }
)
}
@@ -166,7 +169,8 @@ struct TodayView: View {
onSetDate: { taskVM.setDate($0, date: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) }
onSetCategory: { taskVM.setCategory($0, category: $1) },
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) }
)
}
@@ -183,7 +187,8 @@ struct TodayView: View {
onSetDate: { taskVM.setDate($0, date: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) }
onSetCategory: { taskVM.setCategory($0, category: $1) },
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) }
)
}
@@ -538,6 +543,7 @@ struct OverdueCard: View {
var onMove: (TaskItem, Quadrant) -> Void = { _, _ in }
var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in }
var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in }
var onResetMatrixUrgency: (TaskItem) -> Void = { _ in }
@State private var expanded = true
@@ -639,6 +645,7 @@ struct OverdueCard: View {
onMove: onMove,
onSetPriority: onSetPriority,
onSetCategory: onSetCategory,
onResetMatrixUrgency: onResetMatrixUrgency,
onLiveActivity: { LiveActivityManager.toggle(for: $0) },
onEdit: onEdit,
onDelete: onDelete
@@ -951,6 +958,7 @@ struct UpcomingSection: View {
var onMove: (TaskItem, Quadrant) -> Void = { _, _ in }
var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in }
var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in }
var onResetMatrixUrgency: (TaskItem) -> Void = { _ in }
@State private var collapsed = false
@State private var showAll = false
@@ -1014,6 +1022,7 @@ struct UpcomingSection: View {
onMove: onMove,
onSetPriority: onSetPriority,
onSetCategory: onSetCategory,
onResetMatrixUrgency: onResetMatrixUrgency,
onLiveActivity: { LiveActivityManager.toggle(for: $0) },
onEdit: onEdit,
onDelete: { task in withAnimation(KisaniSpring.snappy) { onDelete(task) } }
@@ -1737,7 +1746,7 @@ struct AddTaskSheet: View {
let importantRow = (prefilledQuadrant == .urgent || prefilledQuadrant == .schedule)
_selectedPriority = State(initialValue: importantRow ? .high : .none)
var p = NLParsed()
p.date = prefilledDate ?? Calendar.current.startOfDay(for: Date())
p.date = prefilledDate
_parsed = State(initialValue: p)
}
@@ -1755,7 +1764,7 @@ struct AddTaskSheet: View {
.frame(minHeight: 36, maxHeight: 72)
.onChange(of: rawText) { _ in
var p = NLTaskParser.parse(rawText)
if p.date == nil { p.date = Calendar.current.startOfDay(for: Date()) }
if p.date == nil { p.date = prefilledDate }
withAnimation(KisaniSpring.micro) {
parsed = p
if p.isBirthday { selectedCategory = .birthday }