refactor: unify task and habit list rows
ci / gitleaks (push) Successful in 10s
ci / docker (push) Successful in 3m45s

This commit is contained in:
2026-09-18 06:25:13 +08:00
parent e4e1d96b30
commit a4c1ac5978
8 changed files with 219 additions and 108 deletions
+31 -36
View File
@@ -42,30 +42,23 @@ describe('unified task due display', () => {
expect(app).toContain('<span class="meta-item"><ListChecks/>')
})
it('uses a right-aligned desktop tail and a compact two-line mobile tail without empty slots', () => {
it('uses a right-aligned single-line tail without empty slots', () => {
expect(css).toContain('.task-tail{flex:0 1 auto;min-width:0;max-width:')
expect(css).toContain('text-align:right;white-space:nowrap')
expect(css).toContain('.task-actions{display:flex;align-items:center;flex:0 0 auto}')
expect(css).toContain('.task-row>.drag-handle,.task-row>.task-check,.task-actions>*{flex-shrink:0}')
expect(css).toContain('@media(max-width:930px){.task-tail{flex:0 1 auto;max-width:132px;')
expect(css).toContain('.task-row:has(>.task-tail .task-due--timed){height:66px;max-height:68px;}')
expect(css).toContain('.task-row:has(>.task-tail .task-due--timed) .task-main{padding:4px 0}')
expect(css).toContain('.task-due--timed .task-due__text{display:grid;justify-items:end;overflow:hidden;text-overflow:ellipsis;')
expect(css).toContain('.task-due--timed .task-due__absolute,.task-due--timed .task-due__relative{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}')
expect(css).toContain('.task-due--timed .task-due__separator{display:none}')
expect(css).toContain('.plain-list .task-due--timed .task-due__text{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.2}')
expect(css).toContain('.plain-list .task-due--timed .task-due__separator{display:inline}')
expect(css).not.toContain('.task-main .meta{display:flex;flex-wrap:wrap;')
expect(css).toContain('.task-check{width:44px;height:44px;')
expect(css).toContain('.icon,.ghost{min-width:44px;min-height:44px;')
expect(css).toMatch(/\.restore\{[^}]*min-height:44px/)
})
it('keeps date-only rows compact on mobile while preserving the timed two-line layout', () => {
expect(css).toContain('@media(max-width:930px){.task-tail{flex:0 1 auto;max-width:132px;')
it('overrides the legacy timed mobile presentation inside every plain list', () => {
expect(css).toContain('.task-tail:has(.task-due--timed){flex:0 1 76px;max-width:76px}')
expect(css).toContain('.task-row:has(>.task-tail .task-due--timed){height:66px;max-height:68px;}')
expect(css).not.toContain('.task-row:has(>.task-tail){height:66px;max-height:68px;}')
expect(css).toContain('.task-due--timed .task-due__text{display:grid;justify-items:end;')
expect(css).toContain('.task-due--timed .task-due__separator{display:none}')
expect(css).toContain('.plain-list .task-due--timed .task-due__text{display:block;')
expect(css).toContain('.plain-list .task-due--timed .task-due__separator{display:inline}')
})
it('scopes content metadata and keeps date semantics without a visible calendar icon', () => {
@@ -247,7 +240,7 @@ describe('approved UI detail direction', () => {
})
it('opens Today habit bodies with keyboard parity while check clicks stay isolated', () => {
const todayRows = mvpPanel.slice(mvpPanel.indexOf('class="habit-list today-habit-list"'), mvpPanel.indexOf('<!-- 完整习惯列表 -->'))
const todayRows = mvpPanel.slice(mvpPanel.indexOf('class="habit-list plain-list today-habit-list"'), mvpPanel.indexOf('<!-- 完整习惯列表 -->'))
expect(todayRows).toContain('role="button" tabindex="0"')
expect(todayRows).toContain('@click="openHabitDetail(h, $event.currentTarget as HTMLElement)"')
expect(todayRows).toContain('@keydown.enter.prevent="openHabitDetail(h, $event.currentTarget as HTMLElement)"')
@@ -758,29 +751,24 @@ describe('task and habit row decoration', () => {
expect(toggleBlock).not.toContain('await loadHabits()')
})
it('places numeric habit progress at the far right instead of under the title', () => {
expect(mvpPanel.match(/<small v-if="habitProgressText\(h\)" class="habit-progress">/g)?.length).toBe(2)
it('keeps habit status in the right-side tail and outside the title', () => {
expect(mvpPanel.match(/class="habit-row-meta"/g)?.length).toBe(2)
expect(mvpPanel).not.toContain('class="habit-progress"')
expect(mvpPanel).not.toContain('<span><span class="habit-name">{{ h.name }}</span><small')
expect(css).toContain('.habit-progress{margin-left:auto;')
})
it('keeps Today focused and adds the weekly grid only to the full habits view', () => {
expect(mvpPanel.match(/class="habit-week"/g)?.length).toBe(1)
expect(mvpPanel).toContain('v-for="cell in h.cells"')
expect(mvpPanel).toContain('class="habit-week-cell"')
expect(mvpPanel).toContain('class="habit-week-day"')
expect(mvpPanel).not.toMatch(/today-habit-list[\s\S]*?class="habit-week"[\s\S]*?<!-- 完整习惯列表 -->/)
it('keeps weekly history out of both active list renderers', () => {
expect(mvpPanel).not.toContain('class="habit-week"')
expect(mvpPanel).not.toContain('v-for="cell in h.cells"')
expect(mvpPanel).not.toMatch(/today-habit-list[\s\S]*?habit-drag-handle[\s\S]*?<!-- 完整习惯列表 -->/)
})
it('uses numeric progress as the main feedback and omits it for boolean habits', () => {
expect(mvpPanel.match(/<progress v-if="h.kind === 'numeric'" class="habit-progress-bar"/g)?.length).toBe(2)
expect(mvpPanel.match(/:value="habitProgressValue\(h\)" :max="habitProgressMax\(h\)"/g)?.length).toBe(2)
expect(mvpPanel.match(/:aria-label="`\$\{h.name\}进度:\$\{habitProgressText\(h\)\}`"/g)?.length).toBe(2)
expect(css).toContain('.habit-progress{margin-left:auto;')
expect(css).toContain('font-size:16px')
expect(css).toContain('.habit-progress-bar{grid-column:1/-1;')
expect(css).toContain('.habit-progress-bar::-webkit-progress-value{background:var(--success)')
it('uses the same visible text status tail in Today and full habits', () => {
expect(mvpPanel).not.toContain('<progress v-if="h.kind === \'numeric\'" class="habit-progress-bar"')
expect(mvpPanel.match(/class="habit-row-meta"/g)?.length).toBe(2)
expect(mvpPanel.match(/:aria-label="habitRowStatus\(h\)"/g)?.length).toBe(2)
expect(mvpPanel).not.toContain('class="habit-state-note"')
expect(css).toContain('.habit-row-meta{')
})
it('restores the current page and list from local storage', () => {
@@ -988,11 +976,18 @@ describe('task and habit row decoration', () => {
expect(css).not.toContain('.habit-row.done .habit-name{color:var(--accent);text-decoration:line-through')
})
it('keeps habit cards borderless so the rounded left edge has no visual gap', () => {
expect(css).toMatch(/\.habit-row\{border:0;/)
expect(css).not.toMatch(/\.habit-row\{[^}]*border-top:/)
expect(css).not.toMatch(/\.habit-row\{[^}]*border-right:/)
expect(css).not.toMatch(/\.habit-row\{[^}]*border-bottom:/)
it('keeps every plain-list container completely cardless', () => {
expect(css).toContain('.plain-list{display:grid;gap:0;background:transparent;border:0;border-radius:0;box-shadow:none;overflow:visible}')
expect(css).not.toMatch(/\.plain-list\{[^}]*padding:/)
expect(css).not.toMatch(/\.plain-list\{[^}]*border:(?!0)/)
expect(css).not.toMatch(/\.plain-list\{[^}]*border-radius:(?!0)/)
expect(css).not.toMatch(/\.plain-list\{[^}]*box-shadow:(?!none)/)
})
it('uses a continuous bottom divider instead of card borders', () => {
expect(css).toMatch(/\.plain-list-row,\.plain-list>\.task-row,\.plain-list>\.habit-row\{[^}]*border:0;[^}]*border-bottom:1px solid #e8e0d5/)
expect(css).not.toMatch(/\.plain-list-row,\.plain-list>\.task-row,\.plain-list>\.habit-row\{[^}]*border-top:/)
expect(css).not.toMatch(/\.plain-list-row,\.plain-list>\.task-row,\.plain-list>\.habit-row\{[^}]*border-right:/)
})
it('keeps task rows borderless on the left and habit rows fully borderless', () => {