style: add habit row state feedback
ci / gitleaks (push) Successful in 7s
ci / docker (push) Successful in 4m14s

This commit is contained in:
2026-09-19 14:53:02 +08:00
parent 462bf00b57
commit 02507ac7a7
4 changed files with 31 additions and 23 deletions
+11 -1
View File
@@ -33,7 +33,9 @@ function bottomTab(page: Page, name: string) {
}
async function openSidebarView(page: Page, name: string) {
if ((page.viewportSize()?.width ?? 0) <= 930) {
await page.locator('.topbar').getByRole('button', { name: /展开菜单|收起菜单/ }).click()
}
const target = page.locator('.sidebar').getByRole('button', { name, exact: true })
await expect(target).toBeInViewport()
await target.click()
@@ -72,7 +74,8 @@ test('boolean habits complete in place while paused and unscheduled rows explain
expect(pause.ok(), await pause.text()).toBeTruthy()
await page.goto('/')
await bottomTab(page, '习惯').click()
if ((page.viewportSize()?.width ?? 0) <= 930) await bottomTab(page, '习惯').click()
else await page.locator('.sidebar').getByRole('button', { name: '习惯', exact: true }).click()
const booleanRow = page.locator('.habit-row').filter({ hasText: booleanName })
const booleanCheck = booleanRow.getByRole('button', { name: `完成${booleanName}一次` })
const checkGeometry = await booleanCheck.evaluate((element) => {
@@ -87,7 +90,14 @@ test('boolean habits complete in place while paused and unscheduled rows explain
expect(checkGeometry.height).toBeGreaterThanOrEqual(44)
await booleanCheck.click()
await expect(booleanRow).toHaveClass(/done/)
await expect(booleanRow).toHaveCSS('background-color', 'rgb(251, 247, 240)')
await expect(booleanRow.getByRole('button', { name: `减少${booleanName}一次` })).toHaveAttribute('aria-pressed', 'true')
await booleanRow.locator('.habit-main').click()
await expect(booleanRow).toHaveClass(/selected/)
await expect(booleanRow).toHaveCSS('background-color', 'rgb(255, 244, 229)')
await expect(page.getByRole('dialog', { name: booleanName })).toBeVisible()
await page.getByRole('button', { name: '关闭习惯详情' }).click()
await expect(booleanRow).not.toHaveClass(/selected/)
await page.reload()
const persistedBooleanRow = page.locator('.habit-row').filter({ hasText: booleanName })
await expect(persistedBooleanRow).toHaveClass(/done/)
+2 -2
View File
@@ -808,7 +808,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), '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), 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>
@@ -840,7 +840,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), '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), 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)">
+10 -12
View File
File diff suppressed because one or more lines are too long
+7 -7
View File
@@ -153,14 +153,14 @@ describe('approved five-detail polish', () => {
}
})
it('gives task rows distinct hover, selected, completed, and overdue feedback while habits stay quiet', () => {
expect(css).toContain('@media(hover:hover) and (pointer:fine){.plain-list>.task-row:not(.task-row--trash):hover{background:#fff8ee}}')
expect(css).toContain('@media(hover:hover) and (pointer:fine){.task-row:hover:not(.reordering):not(.task-row--trash){transform:translate(calc(var(--swipe-x) + 2px),var(--reorder-y,0px))}}')
expect(css).toContain('.plain-list>.task-row:not(.task-row--trash).selected,.plain-list>.task-row:not(.task-row--trash).selected.done,.plain-list>.task-row:not(.task-row--trash).selected.overdue-task{background:#fff4e5}')
expect(css).toContain('.plain-list>.task-row:not(.task-row--trash).selected::before{background:var(--accent)}')
expect(css).toContain('.plain-list>.task-row:not(.task-row--trash).done{background:#fbf7f0}')
it('gives task and habit rows distinct hover, selected, and completed feedback', () => {
expect(css).toContain('@media(hover:hover) and (pointer:fine){.plain-list>.task-row:not(.task-row--trash):hover,.plain-list>.habit-row:hover{background:#fff8ee}}')
expect(css).toContain('@media(hover:hover) and (pointer:fine){.task-row:hover:not(.reordering):not(.task-row--trash),.habit-row:hover:not(.reordering){transform:translate(calc(var(--swipe-x) + 2px),var(--reorder-y,0px))}}')
expect(css).toContain('.plain-list>.task-row:not(.task-row--trash).selected,.plain-list>.task-row:not(.task-row--trash).selected.done,.plain-list>.task-row:not(.task-row--trash).selected.overdue-task,.plain-list>.habit-row.selected,.plain-list>.habit-row.selected.done{background:#fff4e5}')
expect(css).toContain('.plain-list>.task-row:not(.task-row--trash).selected::before,.plain-list>.habit-row.selected::before{background:var(--accent)}')
expect(css).toContain('.plain-list>.task-row:not(.task-row--trash).done,.plain-list>.habit-row.done{background:#fbf7f0}')
expect(css).toContain('.plain-list>.task-row:not(.task-row--trash).overdue-task{background:#fff7f4}')
expect(css).toContain('.plain-list>.habit-row:hover,.plain-list>.habit-row.done{background:transparent}')
expect(habits.match(/selected:selectedHabit\?\.id===h\.id/g)?.length).toBe(2)
})
it('renders memo rows with title and time only at a fixed compact height', () => {