Merge develop -> main: full session (KC-51 through KC-71) #28
@@ -1407,3 +1407,47 @@ Pinned the top-level content `VStack` with
|
|||||||
`.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)`.
|
`.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)`.
|
||||||
|
|
||||||
Files: `CalendarView.swift`.
|
Files: `CalendarView.swift`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## KC-49 — Week/3-day dead space persisted after KC-48
|
||||||
|
|
||||||
|
**Status:** Fixed (not build-verified) — supersedes KC-48
|
||||||
|
**Reported by:** User ("DEAD SPACE STILL EXISTS")
|
||||||
|
**Area:** Calendar / Layout
|
||||||
|
|
||||||
|
### Root cause (refined)
|
||||||
|
Pinning only the outer container (`.frame(maxHeight: .infinity)`, KC-48) merely
|
||||||
|
*allows* fill — it doesn't *force* it. The timeline `ScrollView` returned its
|
||||||
|
content size instead of expanding, so the week/3-day root stayed short and the
|
||||||
|
`.bottomTrailing` ZStack floated it down, leaving the day header stranded mid-
|
||||||
|
screen. Day view happened to fill, so it never showed.
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
Force the timeline `ScrollView` to be greedy with
|
||||||
|
`.frame(maxWidth: .infinity, maxHeight: .infinity)`, and pin each timeline
|
||||||
|
view's root `VStack` to fill + top-align. Applied to `weekView` and
|
||||||
|
`threeDayView`.
|
||||||
|
|
||||||
|
Files: `CalendarView.swift`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## KC-50 — Long-press on a timeline event has no actions
|
||||||
|
|
||||||
|
**Status:** Implemented (not build-verified)
|
||||||
|
**Reported by:** User
|
||||||
|
**Area:** Calendar / Tasks
|
||||||
|
|
||||||
|
### Description
|
||||||
|
Long-pressing a task block on the day/week/3-day timeline did nothing — it
|
||||||
|
should offer the same actions as elsewhere (Complete, Snooze, reschedule, etc.).
|
||||||
|
|
||||||
|
### Change
|
||||||
|
`CVTimeBlock` now takes `@ViewBuilder menu` content and shows it via
|
||||||
|
`.contextMenu`. `cvBlockMenu(for:)` builds it for task-backed items: a Complete
|
||||||
|
toggle, a Snooze submenu (15 min / 1 hr / tomorrow), then the shared
|
||||||
|
`TaskMenuItems` (pin, reschedule, move, priority, category, edit, delete).
|
||||||
|
Events (no `taskID`) get no menu.
|
||||||
|
|
||||||
|
Files: `CalendarView.swift`.
|
||||||
|
|||||||
@@ -828,8 +828,10 @@ struct CalendarView: View {
|
|||||||
ScrollView(showsIndicators: false) {
|
ScrollView(showsIndicators: false) {
|
||||||
cvTimeGrid(days: days).padding(.bottom, 40)
|
cvTimeGrid(days: days).padding(.bottom, 40)
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grid layout: the 7 days as cards (2 columns), each listing its schedule.
|
// Grid layout: the 7 days as cards (2 columns), each listing its schedule.
|
||||||
@@ -909,7 +911,9 @@ struct CalendarView: View {
|
|||||||
ScrollView(showsIndicators: false) {
|
ScrollView(showsIndicators: false) {
|
||||||
cvTimeGrid(days: days).padding(.bottom, 40)
|
cvTimeGrid(days: days).padding(.bottom, 40)
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Day
|
// MARK: - Day
|
||||||
@@ -1043,13 +1047,45 @@ struct CalendarView: View {
|
|||||||
draggable: item.taskID != nil && !item.recurring,
|
draggable: item.taskID != nil && !item.recurring,
|
||||||
onReschedule: { newStartMin in
|
onReschedule: { newStartMin in
|
||||||
rescheduleTask(id: item.taskID, on: date, toStartMin: newStartMin)
|
rescheduleTask(id: item.taskID, on: date, toStartMin: newStartMin)
|
||||||
}
|
},
|
||||||
|
menu: { cvBlockMenu(for: item) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Long-press menu for a timeline block. Task items get the full shared menu
|
||||||
|
/// (Complete / Snooze / date / move / edit / delete); events get nothing.
|
||||||
|
@ViewBuilder
|
||||||
|
private func cvBlockMenu(for item: CVTimeItem) -> some View {
|
||||||
|
if let id = item.taskID, let task = taskVM.tasks.first(where: { $0.id == id }) {
|
||||||
|
Button {
|
||||||
|
withAnimation(KisaniSpring.snappy) { taskVM.toggle(task) }
|
||||||
|
} label: {
|
||||||
|
Label(task.isComplete ? "Mark Incomplete" : "Complete", systemImage: "checkmark.circle")
|
||||||
|
}
|
||||||
|
Menu {
|
||||||
|
Button { taskVM.postpone(task, minutes: 15) } label: { Label("15 minutes", systemImage: "clock") }
|
||||||
|
Button { taskVM.postpone(task, minutes: 60) } label: { Label("1 hour", systemImage: "clock") }
|
||||||
|
Button { taskVM.postpone(task, days: 1) } label: { Label("Tomorrow", systemImage: "sun.max") }
|
||||||
|
} label: {
|
||||||
|
Label("Snooze", systemImage: "clock.arrow.circlepath")
|
||||||
|
}
|
||||||
|
TaskMenuItems(
|
||||||
|
task: task,
|
||||||
|
onPin: { taskVM.pin($0) },
|
||||||
|
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||||
|
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
|
||||||
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
|
onEdit: { editingTask = $0 },
|
||||||
|
onDelete: { taskVM.delete($0) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Move a timed task to a new start minute on the same day (drag-to-reschedule).
|
/// Move a timed task to a new start minute on the same day (drag-to-reschedule).
|
||||||
private func rescheduleTask(id: UUID?, on date: Date, toStartMin newStartMin: Int) {
|
private func rescheduleTask(id: UUID?, on date: Date, toStartMin newStartMin: Int) {
|
||||||
guard let id, let task = taskVM.tasks.first(where: { $0.id == id }) else { return }
|
guard let id, let task = taskVM.tasks.first(where: { $0.id == id }) else { return }
|
||||||
@@ -1161,13 +1197,14 @@ struct CVTimeItem: Identifiable {
|
|||||||
// A single timed block on the week/day timeline. Task-backed blocks can be
|
// A single timed block on the week/day timeline. Task-backed blocks can be
|
||||||
// dragged vertically to reschedule (snapped to 15-minute steps); events and
|
// dragged vertically to reschedule (snapped to 15-minute steps); events and
|
||||||
// recurring tasks render fixed.
|
// recurring tasks render fixed.
|
||||||
private struct CVTimeBlock: View {
|
private struct CVTimeBlock<MenuContent: View>: View {
|
||||||
let item: CVTimeItem
|
let item: CVTimeItem
|
||||||
let top: CGFloat
|
let top: CGFloat
|
||||||
let height: CGFloat
|
let height: CGFloat
|
||||||
let hourH: CGFloat
|
let hourH: CGFloat
|
||||||
let draggable: Bool
|
let draggable: Bool
|
||||||
let onReschedule: (Int) -> Void
|
let onReschedule: (Int) -> Void
|
||||||
|
@ViewBuilder let menu: () -> MenuContent
|
||||||
@GestureState private var dragY: CGFloat = 0
|
@GestureState private var dragY: CGFloat = 0
|
||||||
|
|
||||||
private var block: some View {
|
private var block: some View {
|
||||||
@@ -1190,18 +1227,21 @@ private struct CVTimeBlock: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if draggable {
|
Group {
|
||||||
block.gesture(
|
if draggable {
|
||||||
DragGesture(minimumDistance: 6)
|
block.gesture(
|
||||||
.updating($dragY) { value, state, _ in state = value.translation.height }
|
DragGesture(minimumDistance: 6)
|
||||||
.onEnded { value in
|
.updating($dragY) { value, state, _ in state = value.translation.height }
|
||||||
let deltaMin = Int((Double(value.translation.height) / Double(hourH) * 60).rounded() / 15) * 15
|
.onEnded { value in
|
||||||
onReschedule(item.startMin + deltaMin)
|
let deltaMin = Int((Double(value.translation.height) / Double(hourH) * 60).rounded() / 15) * 15
|
||||||
}
|
onReschedule(item.startMin + deltaMin)
|
||||||
)
|
}
|
||||||
} else {
|
)
|
||||||
block
|
} else {
|
||||||
|
block
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.contextMenu { menu() } // empty for events → no menu; task actions otherwise
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user