diff --git a/frontend/src/MvpPanel.vue b/frontend/src/MvpPanel.vue index 5491850..463ffdf 100644 --- a/frontend/src/MvpPanel.vue +++ b/frontend/src/MvpPanel.vue @@ -22,6 +22,7 @@ const importPreview = ref(null) const restoreFile = ref(null) const todayKey = ref(dateKey(new Date())) const habitSwipeStart = ref<{ id: string; x: number; y: number } | null>(null) +const habitPointerStart = ref<{ id: string; x: number; y: number } | null>(null) const habitSwipeOffsets = ref>({}) const todayHabits = computed(() => habits.value.filter((item) => isHabitScheduledToday(item, todayKey.value))) let dayRolloverTimer: ReturnType | undefined @@ -73,6 +74,25 @@ function startHabitSwipe(h: Habit, event: TouchEvent) { habitSwipeOffsets.value[h.id] = 0 } } +function startHabitPointer(h: Habit, event: PointerEvent) { + if (busy.value || isInteractiveTarget(event.target)) return + if (event.pointerType === 'touch') return + habitPointerStart.value = { id: h.id, x: event.clientX, y: event.clientY } +} +function moveHabitPointer(h: Habit, event: PointerEvent) { + const start = habitPointerStart.value + if (!start || start.id !== h.id) return + const deltaX = event.clientX - start.x + const deltaY = event.clientY - start.y + const current = Number(logFor(h, todayKey.value)?.value ?? 0) + if (Math.abs(deltaX) > Math.abs(deltaY)) { + const canIncrement = deltaX > 0 && current < (h.target ?? 1) + const canDecrement = deltaX < 0 && current > 0 + if (canIncrement || canDecrement) { + habitSwipeOffsets.value[h.id] = Math.max(-92, Math.min(deltaX, 92)) + } + } +} function moveHabitSwipe(h: Habit, event: TouchEvent) { const start = habitSwipeStart.value const touch = event.touches[0] @@ -121,6 +141,25 @@ async function finishHabitSwipe(h: Habit, event: TouchEvent) { await applyHabitSwipe(h, deltaX) } } +async function finishHabitPointer(h: Habit, event: PointerEvent) { + const start = habitPointerStart.value + if (!start || start.id !== h.id) return + habitPointerStart.value = null + habitSwipeOffsets.value[h.id] = 0 + const deltaX = event.clientX - start.x + const deltaY = event.clientY - start.y + const current = Number(logFor(h, todayKey.value)?.value ?? 0) + const expectedDirection = h.kind === 'numeric' + ? ((deltaX > 0 && current < (h.target ?? 1)) || (deltaX < 0 && current > 0)) + : (isDone(h, todayKey.value) ? deltaX < 0 : deltaX > 0) + if (expectedDirection && shouldToggleRowSwipe(Math.abs(deltaX), deltaY)) { + void applyHabitSwipe(h, deltaX) + } +} +function cancelHabitPointer(h?: Habit) { + habitPointerStart.value = null + if (h) habitSwipeOffsets.value[h.id] = 0 +} function cancelHabitSwipe(h?: Habit) { habitSwipeStart.value = null if (h) habitSwipeOffsets.value[h.id] = 0 @@ -231,7 +270,7 @@ onBeforeUnmount(() => {
-
+
{{ isDone(h, todayKey) ? (h.kind === 'numeric' ? '− 一次' : '↩ 未完成') : (h.kind === 'numeric' ? '+ 一次' : '✓ 打卡') }}
{{ h.name }}{{ habitProgressText(h) }} @@ -256,7 +295,7 @@ onBeforeUnmount(() => {
-
+
{{ isDone(h, todayKey) ? (h.kind === 'numeric' ? '− 一次' : '↩ 未完成') : (h.kind === 'numeric' ? '+ 一次' : '✓ 打卡') }}
{{ h.name }}{{ habitProgressText(h) }} diff --git a/frontend/src/style.test.ts b/frontend/src/style.test.ts index 821b879..612d32d 100644 --- a/frontend/src/style.test.ts +++ b/frontend/src/style.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest' const css = readFileSync('src/style.css', 'utf8') const app = readFileSync('src/App.vue', 'utf8') +const mvpPanel = readFileSync('src/MvpPanel.vue', 'utf8') describe('mobile navigation styles', () => { it('keeps the mobile More sheet visible when it is rendered', () => { @@ -20,6 +21,13 @@ describe('task and habit row decoration', () => { expect(app.match(/@pointercancel="cancelTaskPointer/g)?.length).toBe(2) }) + it('supports pointer dragging for desktop habit rows in Today and Habits views', () => { + expect(mvpPanel.match(/@pointerdown="startHabitPointer/g)?.length).toBe(2) + expect(mvpPanel.match(/@pointermove="moveHabitPointer/g)?.length).toBe(2) + expect(mvpPanel.match(/@pointerup="finishHabitPointer/g)?.length).toBe(2) + expect(mvpPanel.match(/@pointercancel="cancelHabitPointer/g)?.length).toBe(2) + }) + it('does not draw a left border on task or habit rows', () => { expect(css).toContain('.task-row{border-left:0;') expect(css).toContain('.habit-row{border-left:0;')