feat: 动效补齐动效审计五处缺口——视图切换淡入上移、AppSheet抽屉开合对称、新建任务落行反馈、翻页结果淡入、数字习惯+1脉冲
ci / gitleaks (push) Successful in 9s
ci / docker (push) Successful in 4m5s

This commit is contained in:
2026-09-24 14:19:18 +08:00
parent 3b6cd70d5b
commit 35f9b566ce
7 changed files with 70 additions and 9 deletions
+26 -5
View File
@@ -143,6 +143,13 @@ const markTaskJustCompleted = createCompletionPulse(
(id) => { justCompletedTaskIds.value = new Set(justCompletedTaskIds.value).add(id) },
(id) => { const next = new Set(justCompletedTaskIds.value); next.delete(id); justCompletedTaskIds.value = next },
)
const justAddedTaskIds = ref(new Set<string>())
const markTaskJustAdded = createCompletionPulse(
(id) => { justAddedTaskIds.value = new Set(justAddedTaskIds.value).add(id) },
(id) => { const next = new Set(justAddedTaskIds.value); next.delete(id); justAddedTaskIds.value = next },
)
const viewSwap = ref(false)
const listSwap = ref(false)
const navigationLoaded = ref(false)
const taskSwipeStart = ref<{ id: string; x: number; y: number } | null>(null)
const taskPointerStart = ref<{ id: string; x: number; y: number } | null>(null)
@@ -325,9 +332,10 @@ async function submitTaskCompose() {
description: composeDescription.value,
...recurrencePayload,
}) }) as Promise<Task>,
() => {
(created) => {
if (taskComposeGeneration.value === composeGeneration) taskComposeOpen.value = false
toast('任务已添加')
markTaskJustAdded(created.id)
},
(reason) => {
if (taskComposeGeneration.value !== composeGeneration) return
@@ -1658,11 +1666,17 @@ async function scrollToTaskPageStart() {
await nextTick()
taskListElement.value?.scrollIntoView({ block: 'start' })
}
async function swapListResults() {
listSwap.value = false
await nextTick()
listSwap.value = true
}
async function previousPage() {
if (page.value <= 1 || loading.value) return
page.value -= 1
if (activeView.value === 'trash') await loadTrash()
else if (isTaskView(activeView.value)) await loadAll()
await swapListResults()
await scrollToTaskPageStart()
}
async function nextPage() {
@@ -1670,6 +1684,7 @@ async function nextPage() {
page.value += 1
if (activeView.value === 'trash') await loadTrash()
else if (isTaskView(activeView.value)) await loadAll()
await swapListResults()
await scrollToTaskPageStart()
}
@@ -1682,6 +1697,12 @@ function reconcileDesktopPaneWidths() {
}
watch([selectedTask, habitDetailOpen, calendarDetailOpen, sidebarCollapsed], reconcileDesktopPaneWidths)
watch(activeView, async () => {
listSwap.value = false
viewSwap.value = false
await nextTick()
viewSwap.value = true
})
function handlePaneResizeBlur() { stopPaneResize() }
@@ -1802,7 +1823,7 @@ onUnmounted(() => {
</aside>
<div v-if="!sidebarCollapsed" class="pane-resizer pane-resizer--sidebar" role="separator" aria-label="调整菜单栏宽度" aria-orientation="vertical" aria-valuemin="190" :aria-valuemax="sidebarMaxWidth" :aria-valuenow="sidebarWidth" tabindex="0" @pointerdown="startPaneResize('sidebar',$event)" @lostpointercapture="handlePaneLostPointerCapture" @keydown="resizePaneWithKeyboard('sidebar',$event)" />
<main :class="{'today-main':activeView==='today','list-main':activeView==='tasks'||activeView==='upcoming'||activeView==='habits'}">
<main :class="{'today-main':activeView==='today','list-main':activeView==='tasks'||activeView==='upcoming'||activeView==='habits'}" :data-view-swap="viewSwap ? '' : null">
<header class="topbar" :class="{'settings-topbar':activeView==='settings'}" :inert="memoBackgroundInert ? true : undefined">
<button class="icon" :aria-label="(sidebarCollapsed ? '展开' : '收起') + '菜单'" :aria-expanded="sidebarCollapsed ? 'false' : 'true'" @click="toggleSidebar"><Menu /></button>
<div v-if="!['today','tasks','upcoming','trash','habits','settings','focus'].includes(activeView)" class="topbar-title"><h1 :title="activeName">{{ activeName }}</h1></div>
@@ -1842,7 +1863,7 @@ onUnmounted(() => {
<span class="trash-page-count">共 {{totalTasks}} 项</span>
</section>
<div v-if="activeView==='tasks' || activeView==='upcoming'" id="task-list-heading" class="list-section-heading"><span id="task-list-title" class="list-section-title">任务</span><span class="list-section-count">{{ totalTasks }}</span><button v-if="activeView==='tasks' && taskReorderAvailable" class="list-section-action" type="button" :aria-pressed="taskReorderMode" @click="taskReorderMode=!taskReorderMode;cancelTaskReorder()">{{ taskReorderMode ? '完成' : '调整顺序' }}</button></div>
<section v-if="activeView==='trash' && visibleTasks.length" ref="taskListElement" class="trash-groups" aria-label="回收站任务分组">
<section v-if="activeView==='trash' && visibleTasks.length" ref="taskListElement" class="trash-groups" :class="{'list-swap': listSwap}" aria-label="回收站任务分组">
<section v-for="group in trashGroups" :key="group.key" class="trash-group" :aria-labelledby="`trash-group-${group.key}`">
<header class="trash-group-heading"><h2 :id="`trash-group-${group.key}`">{{group.label}}</h2><span>{{group.nodes.length}}</span></header>
<div class="task-list plain-list trash-list">
@@ -1854,10 +1875,10 @@ onUnmounted(() => {
</div>
</section>
</section>
<section v-else :id="activeView==='today' ? 'today-tasks' : undefined" class="task-list plain-list" :class="{loading, 'is-collapsed': todaySectionCollapse.tasks}" :role="activeView==='today' ? 'region' : undefined" :aria-labelledby="activeView==='today' ? 'today-tasks-heading' : (activeView==='tasks' || activeView==='upcoming') ? 'task-list-title' : undefined" ref="taskListElement">
<section v-else :id="activeView==='today' ? 'today-tasks' : undefined" class="task-list plain-list" :class="{loading, 'is-collapsed': todaySectionCollapse.tasks, 'list-swap': listSwap}" :role="activeView==='today' ? 'region' : undefined" :aria-labelledby="activeView==='today' ? 'today-tasks-heading' : (activeView==='tasks' || activeView==='upcoming') ? 'task-list-title' : undefined" ref="taskListElement">
<div class="today-collapse-inner plain-list">
<template v-for="node in taskTree" :key="node.task.id">
<article :data-task-id="node.task.id" class="task-row swipeable" :class="{done:node.task.completed,'filter-reveal':completedFilterReveal&&node.task.completed,'just-completed': justCompletedTaskIds.has(node.task.id),'completion-exiting':completionExitingTaskIds.has(node.task.id),selected:selectedTask?.id===node.task.id,ready:Math.abs(taskSwipeOffsets[node.task.id] ?? 0) >= 64,reordering:taskReorder?.id===node.task.id,'reorder-target':taskReorderTarget===node.task.id}" :style="{ '--swipe-x': `${taskSwipeOffsets[node.task.id] ?? 0}px`, '--reorder-y': `${taskReorder?.id === node.task.id ? taskReorder.offsetY : 0}px` }" @pointerdown="startTaskPointer(node.task, $event)" @pointermove="moveTaskPointer(node.task, $event)" @pointerup="finishTaskPointer(node.task, $event)" @pointercancel="cancelTaskPointer(node.task)" @touchstart.passive="startTaskSwipe(node.task, $event)" @touchmove.passive="moveTaskSwipe(node.task, $event)" @touchend="finishTaskSwipe(node.task, $event)" @touchcancel="cancelTaskSwipe(node.task)">
<article :data-task-id="node.task.id" class="task-row swipeable" :class="{done:node.task.completed,'filter-reveal':completedFilterReveal&&node.task.completed,'just-completed': justCompletedTaskIds.has(node.task.id),'completion-exiting':completionExitingTaskIds.has(node.task.id),'just-added':justAddedTaskIds.has(node.task.id),selected:selectedTask?.id===node.task.id,ready:Math.abs(taskSwipeOffsets[node.task.id] ?? 0) >= 64,reordering:taskReorder?.id===node.task.id,'reorder-target':taskReorderTarget===node.task.id}" :style="{ '--swipe-x': `${taskSwipeOffsets[node.task.id] ?? 0}px`, '--reorder-y': `${taskReorder?.id === node.task.id ? taskReorder.offsetY : 0}px` }" @pointerdown="startTaskPointer(node.task, $event)" @pointermove="moveTaskPointer(node.task, $event)" @pointerup="finishTaskPointer(node.task, $event)" @pointercancel="cancelTaskPointer(node.task)" @touchstart.passive="startTaskSwipe(node.task, $event)" @touchmove.passive="moveTaskSwipe(node.task, $event)" @touchend="finishTaskSwipe(node.task, $event)" @touchcancel="cancelTaskSwipe(node.task)">
<button v-if="taskReorderMode" class="drag-handle task-drag-handle" :disabled="totalPages > 1" aria-label="上下拖动任务排序" title="上下拖动排序" @pointerdown.stop="startTaskReorder(node.task, $event)" @pointermove.stop="moveTaskReorder(node.task, $event)" @pointerup.stop="finishTaskReorder(node.task, $event)" @pointercancel.stop="cancelTaskReorder"><GripVertical/></button>
<button class="task-check" :aria-label="node.task.completed ? `重新打开${node.task.title}` : `完成${node.task.title}`" :aria-pressed="node.task.completed" @click.stop="toggle(node.task)"><span class="task-check-mark" :class="`p${node.task.priority}`"><Check v-if="node.task.completed" /></span></button>
<div class="task-main" role="button" tabindex="0" @click="selectTaskUnlessSwiped(node.task)" @keydown.enter="selectTaskUnlessSwiped(node.task)" @keydown.space.prevent="selectTaskUnlessSwiped(node.task)"><strong :title="node.task.title">{{node.task.title}}</strong><span v-if="node.subtasks.length" class="meta"><span class="meta-item"><ListChecks/>{{node.subtasks.filter(t=>t.completed).length}}/{{node.subtasks.length}}</span></span><span v-if="node.task.priority" class="priority" :class="`p${node.task.priority}`">{{['','低','中','高'][node.task.priority]}}</span></div>
+8 -2
View File
@@ -127,6 +127,11 @@ const markHabitJustCompleted = createCompletionPulse(
(id) => { justCompletedHabitIds.value = new Set(justCompletedHabitIds.value).add(id) },
(id) => { const next = new Set(justCompletedHabitIds.value); next.delete(id); justCompletedHabitIds.value = next },
)
const countBumpedHabitIds = ref(new Set<string>())
const markHabitCountBump = createCompletionPulse(
(id) => { countBumpedHabitIds.value = new Set(countBumpedHabitIds.value).add(id) },
(id) => { const next = new Set(countBumpedHabitIds.value); next.delete(id); countBumpedHabitIds.value = next },
)
const todayHabits = computed(() => habits.value.filter((item) => isHabitScheduledToday(item, todayKey.value)))
const visibleTodayHabits = computed(() => props.showCompleted ? todayHabits.value : todayHabits.value.filter((item) => !isDone(item, todayKey.value) || completionExitingHabitIds.value.has(item.id)))
const visibleHabits = computed(() => props.showCompleted ? habits.value : habits.value.filter((item) => !isDone(item, todayKey.value) || completionExitingHabitIds.value.has(item.id)))
@@ -304,6 +309,7 @@ async function mutateHabitValue(h: Habit, next: number | boolean, previous: numb
await syncHabitHistoryToday(h, day)
if (!ownership.current()) return
await finishHabitCompletion(h, wasDone, next, animateExit, ownership.current)
if (ownership.current() && h.kind === 'numeric' && Number(next) > Number(previous) && !(!wasDone && isHabitComplete(h.kind, next, h.target ??1))) markHabitCountBump(h.id)
if (ownership.current()) emit('notice', notice)
},
error: (rollback, reason, ownership) => {
@@ -832,7 +838,7 @@ onBeforeUnmount(() => {
<!-- 今日习惯:只展示“今天该做”的习惯,复用正式习惯行样式 -->
<div v-if="view === 'today-habits' && (habits.length || !busy)" class="habit-list plain-list today-habit-list">
<article v-for="h in visibleTodayHabits" :key="h.id" :data-habit-id="h.id" class="habit-row today-habit-row swipeable" :class="{ done: isDone(h, todayKey), selected:selectedHabit?.id===h.id, 'just-completed': justCompletedHabitIds.has(h.id), 'completion-exiting': completionExitingHabitIds.has(h.id), ready: Math.abs(habitSwipeOffsets[h.id] ?? 0) >= 64 }" :style="{ '--swipe-x': `${habitSwipeOffsets[h.id] ?? 0}px` }" @pointerdown="startHabitPointer(h, $event)" @pointermove="moveHabitPointer(h, $event)" @pointerup="finishHabitPointer(h, $event)" @pointercancel="cancelHabitPointer(h)" @touchstart.passive="startHabitSwipe(h, $event)" @touchmove.passive="moveHabitSwipe(h, $event)" @touchend="finishHabitSwipe(h, $event)" @touchcancel="cancelHabitSwipe(h)">
<article v-for="h in visibleTodayHabits" :key="h.id" :data-habit-id="h.id" class="habit-row today-habit-row swipeable" :class="{ done: isDone(h, todayKey), selected:selectedHabit?.id===h.id, 'just-completed': justCompletedHabitIds.has(h.id), 'completion-exiting': completionExitingHabitIds.has(h.id), 'count-bump': countBumpedHabitIds.has(h.id), ready: Math.abs(habitSwipeOffsets[h.id] ?? 0) >= 64 }" :style="{ '--swipe-x': `${habitSwipeOffsets[h.id] ?? 0}px` }" @pointerdown="startHabitPointer(h, $event)" @pointermove="moveHabitPointer(h, $event)" @pointerup="finishHabitPointer(h, $event)" @pointercancel="cancelHabitPointer(h)" @touchstart.passive="startHabitSwipe(h, $event)" @touchmove.passive="moveHabitSwipe(h, $event)" @touchend="finishHabitSwipe(h, $event)" @touchcancel="cancelHabitSwipe(h)">
<button class="task-check habit-check" :disabled="!habitAction(h).writable" :aria-disabled="!habitAction(h).writable" :title="habitAction(h).reason" :aria-label="habitAction(h).reason || (isDone(h, todayKey) ? `减少${h.name}一次` : `完成${h.name}一次`)" :aria-pressed="isDone(h, todayKey)" @click.stop="toggleHabitFromButton(h)"><span class="task-check-mark"><Check v-if="isDone(h, todayKey)" /></span></button>
<div class="habit-main" role="button" tabindex="0" :aria-label="`查看习惯详情:${h.name}`" @click="openHabitDetail(h, $event.currentTarget as HTMLElement)" @keydown.enter.prevent="openHabitDetail(h, $event.currentTarget as HTMLElement)" @keydown.space.prevent="openHabitDetail(h, $event.currentTarget as HTMLElement)">
<span class="habit-name" :title="h.name">{{ h.name }}</span>
@@ -865,7 +871,7 @@ onBeforeUnmount(() => {
<div v-if="view === 'habits'" class="list-section-heading habit-section-heading"><span class="list-section-title">今日习惯</span><span class="list-section-count">{{ todayHabitSummary.total }}</span><button v-if="habitReorderAvailable" class="list-section-action" type="button" :aria-pressed="habitReorderMode" @click="habitReorderMode=!habitReorderMode;cancelHabitReorder()">{{ habitReorderMode ? '完成' : '调整顺序' }}</button></div>
<!-- 习惯列表支持整行滑动记录。 -->
<div v-if="view === 'habits'" class="habit-list plain-list">
<article v-for="h in visibleHabits" :key="h.id" :data-habit-id="h.id" class="habit-row habit-row--full swipeable" :class="{ done: isDone(h, todayKey), selected:selectedHabit?.id===h.id, 'just-completed': justCompletedHabitIds.has(h.id), 'completion-exiting': completionExitingHabitIds.has(h.id), ready: Math.abs(habitSwipeOffsets[h.id] ?? 0) >= 64, reordering: habitReorder?.id === h.id, 'reorder-target': habitReorderTarget === h.id }" :style="{ '--swipe-x': `${habitSwipeOffsets[h.id] ?? 0}px`, '--reorder-y': `${habitReorder?.id === h.id ? habitReorder.offsetY : 0}px` }" @pointerdown="startHabitPointer(h, $event)" @pointermove="moveHabitPointer(h, $event)" @pointerup="finishHabitPointer(h, $event)" @pointercancel="cancelHabitPointer(h)" @touchstart.passive="startHabitSwipe(h, $event)" @touchmove.passive="moveHabitSwipe(h, $event)" @touchend="finishHabitSwipe(h, $event)" @touchcancel="cancelHabitSwipe(h)">
<article v-for="h in visibleHabits" :key="h.id" :data-habit-id="h.id" class="habit-row habit-row--full swipeable" :class="{ done: isDone(h, todayKey), selected:selectedHabit?.id===h.id, 'just-completed': justCompletedHabitIds.has(h.id), 'completion-exiting': completionExitingHabitIds.has(h.id), 'count-bump': countBumpedHabitIds.has(h.id), ready: Math.abs(habitSwipeOffsets[h.id] ?? 0) >= 64, reordering: habitReorder?.id === h.id, 'reorder-target': habitReorderTarget === h.id }" :style="{ '--swipe-x': `${habitSwipeOffsets[h.id] ?? 0}px`, '--reorder-y': `${habitReorder?.id === h.id ? habitReorder.offsetY : 0}px` }" @pointerdown="startHabitPointer(h, $event)" @pointermove="moveHabitPointer(h, $event)" @pointerup="finishHabitPointer(h, $event)" @pointercancel="cancelHabitPointer(h)" @touchstart.passive="startHabitSwipe(h, $event)" @touchmove.passive="moveHabitSwipe(h, $event)" @touchend="finishHabitSwipe(h, $event)" @touchcancel="cancelHabitSwipe(h)">
<button v-if="habitReorderMode && habitReorderAvailable" class="drag-handle habit-drag-handle" aria-label="上下拖动习惯排序" title="上下拖动排序" @pointerdown.stop="startHabitReorder(h, $event)" @pointermove.stop="moveHabitReorder(h, $event)" @pointerup.stop="finishHabitReorder(h, $event)" @pointercancel.stop="cancelHabitReorder"><GripVertical/></button>
<button class="task-check habit-check" :disabled="!habitAction(h).writable" :aria-disabled="!habitAction(h).writable" :title="habitAction(h).reason" :aria-label="habitAction(h).reason || (isDone(h, todayKey) ? `减少${h.name}一次` : `完成${h.name}一次`)" :aria-pressed="isDone(h, todayKey)" @click.stop="toggleHabitFromButton(h)"><span class="task-check-mark"><Check v-if="isDone(h, todayKey)" /></span></button>
<div class="habit-main" role="button" tabindex="0" :aria-label="`查看习惯详情:${h.name}`" @click="openHabitDetail(h, $event.currentTarget as HTMLElement)" @keydown.enter.prevent="openHabitDetail(h, $event.currentTarget as HTMLElement)" @keydown.space.prevent="openHabitDetail(h, $event.currentTarget as HTMLElement)">
+1
View File
@@ -280,5 +280,6 @@ describe('AppSheet', () => {
expect(source).toContain(`:key="'app-sheet-modal'"`)
expect(source).toContain(`:key="'app-sheet-inline'"`)
expect(source).toContain(`:key="'app-sheet-bare'"`)
expect(source).toContain('<Transition name="app-sheet-mask">')
})
})
+1 -1
View File
@@ -92,7 +92,7 @@ onBeforeUnmount(deactivate)
<template>
<Teleport v-if="modal" :key="'app-sheet-modal'" :to="overlayRoot()">
<Transition name="app-sheet">
<Transition name="app-sheet-mask">
<div v-if="open" class="app-overlay app-sheet-mask" :style="{ zIndex: layerZIndex }" :aria-busy="busy || undefined" @click="scrimClose">
<component :is="$attrs.onSubmit ? 'form' : 'section'" ref="panel" class="app-sheet" :class="[`app-sheet--${variant}`, panelClass]" role="dialog" aria-modal="true" :aria-labelledby="titleId" :aria-describedby="descriptionId" :aria-label="label" tabindex="-1" v-bind="$attrs" @keydown="keydown">
<slot />
+13
View File
@@ -278,11 +278,24 @@ main:has(>.mvp-view .settings-sections) .settings-data>.inline-error{margin:var(
@media(max-width:600px){
.app-sheet-mask-enter-active .app-sheet{transition:transform 210ms cubic-bezier(.2,.8,.3,1),opacity 210ms cubic-bezier(.2,.8,.3,1)}
.app-sheet-mask-enter-from .app-sheet{transform:translateY(16px);opacity:0}
.app-sheet-mask-leave-active .app-sheet{transition:transform 160ms cubic-bezier(.2,.8,.3,1),opacity 160ms cubic-bezier(.2,.8,.3,1)}
.app-sheet-mask-leave-to .app-sheet{transform:translateY(16px);opacity:0}
}
/* Restrained motion: rows newly revealed by the completed filter fade in (opacity only - rows own transform for swipe/reorder). */
.task-row.filter-reveal{animation:completed-row-reveal 170ms ease-out}
@keyframes completed-row-reveal{from{opacity:0}to{opacity:1}}
/* Restrained motion: motion audit gaps 2026-09-24 - event-driven only, transform/opacity, <=210ms, cubic-bezier(.2,.8,.3,1). */
main[data-view-swap]{animation:view-enter 140ms cubic-bezier(.2,.8,.3,1)}
@keyframes view-enter{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
.task-row.just-added{animation:added-row-in 200ms cubic-bezier(.2,.8,.3,1)}
@keyframes added-row-in{from{opacity:0;transform:translate(var(--swipe-x),var(--reorder-y,0px)) translateY(4px)}to{opacity:1;transform:translate(var(--swipe-x),var(--reorder-y,0px))}}
.task-list.list-swap,.trash-groups.list-swap{animation:results-fade 100ms ease-out}
@keyframes results-fade{from{opacity:0}to{opacity:1}}
.habit-row.count-bump .task-check-mark{animation:count-bump .15s cubic-bezier(.2,.8,.3,1)}
@keyframes count-bump{0%{transform:scale(1)}50%{transform:scale(1.3)}100%{transform:scale(1)}}
@media(prefers-reduced-motion:reduce){main[data-view-swap]{animation:none}.task-row.just-added{animation:none}.task-list.list-swap,.trash-groups.list-swap{animation:none}.habit-row.count-bump .task-check-mark{animation:none}.app-sheet-mask-enter-active,.app-sheet-mask-leave-active,.app-sheet-mask-enter-active .app-sheet,.app-sheet-mask-leave-active .app-sheet{transition:none}}
/* 触控目标:小控件命中区扩展到 44px(视觉尺寸不变) */
.markdown-mode-switch button{position:relative}
+20
View File
@@ -484,6 +484,26 @@ describe('completion feedback motion', () => {
expect(css).toContain('@media(prefers-reduced-motion:reduce){.task-row.just-completed,.habit-row.just-completed,.task-row.just-completed .task-check-mark,.habit-row.just-completed .task-check-mark{animation:none}.task-row.completion-exiting,.habit-row.completion-exiting{animation:none}}')
})
it('fills the five motion-audit gaps with bounded, event-driven animations', () => {
expect(css).toContain('main[data-view-swap]{animation:view-enter 140ms cubic-bezier(.2,.8,.3,1)}')
expect(css).toContain('@keyframes view-enter{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}')
expect(app).toContain(':data-view-swap="viewSwap ? \'\' : null"')
expect(app).toContain('watch(activeView, async () => {')
expect(appSheet).toContain('<Transition name="app-sheet-mask">')
expect(css).toContain('.app-sheet-mask-leave-active .app-sheet{transition:transform 160ms cubic-bezier(.2,.8,.3,1),opacity 160ms cubic-bezier(.2,.8,.3,1)}')
expect(css).toContain('.app-sheet-mask-leave-to .app-sheet{transform:translateY(16px);opacity:0}')
expect(css).toContain('.task-row.just-added{animation:added-row-in 200ms cubic-bezier(.2,.8,.3,1)}')
expect(app).toContain("'just-added':justAddedTaskIds.has(node.task.id)")
expect(app).toContain('markTaskJustAdded(created.id)')
expect(css).toContain('.task-list.list-swap,.trash-groups.list-swap{animation:results-fade 100ms ease-out}')
expect(app).toContain("'list-swap': listSwap")
expect(app).toContain('await swapListResults()')
expect(css).toContain('.habit-row.count-bump .task-check-mark{animation:count-bump .15s cubic-bezier(.2,.8,.3,1)}')
expect(mvpPanel).toContain('markHabitCountBump(h.id)')
expect(mvpPanel).toContain("'count-bump': countBumpedHabitIds.has(h.id)")
expect(css).toContain('@media(prefers-reduced-motion:reduce){main[data-view-swap]{animation:none}.task-row.just-added{animation:none}.task-list.list-swap,.trash-groups.list-swap{animation:none}.habit-row.count-bump .task-check-mark{animation:none}.app-sheet-mask-enter-active,.app-sheet-mask-leave-active,.app-sheet-mask-enter-active .app-sheet,.app-sheet-mask-leave-active .app-sheet{transition:none}}')
})
it('stacks the desktop calendar copy to align with weather and gold', () => {
expect(css).toContain('@media(min-width:721px){.today-environment__calendar{flex-direction:column;align-items:flex-start;gap:0}.today-environment__calendar small{margin-top:var(--space-2);font-size:11px}}')
})
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/lib/utils.ts","./src/app.vue"],"version":"5.9.3"}
{"root":["./src/calendarintegration.test.ts","./src/calendarpanel.test.ts","./src/countdownpanel.test.ts","./src/memointegration.test.ts","./src/memopanel.test.ts","./src/pomodorointegration.test.ts","./src/todayenvironment.test.ts","./src/todaysections.test.ts","./src/current-view-task-reconciliation.test.ts","./src/failurehint.test.ts","./src/failurehint.ts","./src/habit-async-reliability.test.ts","./src/main.ts","./src/markdown-it-task-lists.d.ts","./src/refresh-search-removal.test.ts","./src/style.test.ts","./src/task-async-reliability.test.ts","./src/visual-polish.test.ts","./src/vite-env.d.ts","./src/api/backups.test.ts","./src/api/backups.ts","./src/api/errors.ts","./src/api/http.ts","./src/api/index.ts","./src/components/appsheet.test.ts","./src/components/calendarpicker.integration.test.ts","./src/components/calendarpicker.test.ts","./src/components/completedfilterpill.test.ts","./src/components/floatingaddbutton.test.ts","./src/components/memoeditor.test.ts","./src/components/memorow.test.ts","./src/components/taskduedisplay.test.ts","./src/components/todayenvironmentstrip.test.ts","./src/composables/useoverlaystack.ts","./src/lib/app-shell-state.test.ts","./src/lib/app-shell-state.ts","./src/lib/archived-list-menu.test.ts","./src/lib/archived-list-menu.ts","./src/lib/backup-preflight-state.test.ts","./src/lib/backup-preflight-state.ts","./src/lib/calendar-picker.test.ts","./src/lib/calendar-picker.ts","./src/lib/completion-motion.ts","./src/lib/csrf.test.ts","./src/lib/csrf.ts","./src/lib/desktop-shell-resize.test.ts","./src/lib/desktop-shell-resize.ts","./src/lib/list-drag.test.ts","./src/lib/list-drag.ts","./src/lib/mvp-utils.test.ts","./src/lib/mvp-utils.ts","./src/lib/performance.test.ts","./src/lib/pomodoro-timer.test.ts","./src/lib/pomodoro-timer.ts","./src/lib/task-due-clock.test.ts","./src/lib/task-due-clock.ts","./src/lib/task-due.test.ts","./src/lib/task-due.ts","./src/lib/task-open-total.ts","./src/lib/task-utils.test.ts","./src/lib/task-utils.ts","./src/lib/today-section-collapse.test.ts","./src/lib/today-section-collapse.ts","./src/lib/ui-bugfixes.test.ts","./src/lib/utils.ts","./src/app.vue","./src/calendarpanel.vue","./src/countdownpanel.vue","./src/memopanel.vue","./src/mvppanel.vue","./src/pomodoropanel.vue","./src/components/appdialog.vue","./src/components/appsheet.vue","./src/components/calendarpicker.vue","./src/components/completedfilterpill.vue","./src/components/floatingaddbutton.vue","./src/components/memoeditor.vue","./src/components/memorow.vue","./src/components/taskduedisplay.vue","./src/components/todayenvironmentstrip.vue"],"version":"5.9.3"}