feat: add draggable mobile task composer
ci / docker (push) Successful in 3m26s

This commit is contained in:
2026-09-07 10:52:20 +08:00
parent 86fdcca773
commit d6452fb1c1
5 changed files with 133 additions and 5 deletions
+13 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { calendarModeLabel, countdownDayText, countdownKindLabel, dateKey, defaultView, habitWeek, isHabitComplete, isHabitScheduledToday, isTaskView, nextHabitSwipeValue, numericHabitInputValue, previousHabitSwipeValue, quickTaskFields, shouldToggleRowSwipe } from './mvp-utils'
import { calendarModeLabel, clampFabPosition, countdownDayText, countdownKindLabel, dateKey, defaultView, habitWeek, isFabDrag, isHabitComplete, isHabitScheduledToday, isTaskView, nextHabitSwipeValue, numericHabitInputValue, previousHabitSwipeValue, quickTaskFields, shouldToggleRowSwipe } from './mvp-utils'
describe('MVP view utilities', () => {
it('formats a local date as YYYY-MM-DD', () => {
@@ -81,4 +81,16 @@ describe('MVP view utilities', () => {
expect(shouldToggleRowSwipe(30, 2)).toBe(false)
expect(shouldToggleRowSwipe(80, 60)).toBe(false)
})
it('keeps the draggable mobile add button inside the viewport', () => {
expect(clampFabPosition(-10, -20, 390, 844)).toEqual({ x: 14, y: 14 })
expect(clampFabPosition(500, 900, 390, 844)).toEqual({ x: 324, y: 718 })
expect(clampFabPosition(120, 300, 390, 844)).toEqual({ x: 120, y: 300 })
})
it('distinguishes tapping the add button from dragging it', () => {
expect(isFabDrag(3, 4)).toBe(false)
expect(isFabDrag(8, 0)).toBe(true)
expect(isFabDrag(6, 7)).toBe(true)
})
})
+11
View File
@@ -98,6 +98,17 @@ export function nextTotalAfterLocalTaskAdd(total: number) {
return Math.max(0, Number(total) || 0) + 1
}
export function clampFabPosition(x: number, y: number, viewportWidth: number, viewportHeight: number, size = 52, margin = 14, bottomReserved = 74) {
return {
x: Math.min(Math.max(x, margin), Math.max(margin, viewportWidth - size - margin)),
y: Math.min(Math.max(y, margin), Math.max(margin, viewportHeight - size - bottomReserved)),
}
}
export function isFabDrag(deltaX: number, deltaY: number, threshold = 8) {
return Math.hypot(deltaX, deltaY) >= threshold
}
export function formatApiErrorDetail(detail: unknown): string {
if (typeof detail === 'string') return detail
if (Array.isArray(detail)) {