feat: refine task list visual details
This commit is contained in:
@@ -1368,9 +1368,9 @@ onUnmounted(() => {
|
||||
<button v-if="activeView!=='trash'" class="drag-handle task-drag-handle" :disabled="Boolean(query) || 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 v-if="activeView!=='trash'" 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" :aria-expanded="!collapsedTaskIds.has(node.task.id)" @click="activeView==='trash'?undefined:selectTaskUnlessSwiped(node.task, true)" @keydown.enter="activeView==='trash'?undefined:selectTaskUnlessSwiped(node.task, true)" @keydown.space.prevent="activeView==='trash'?undefined:selectTaskUnlessSwiped(node.task, true)"><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>
|
||||
<span v-if="node.task.due_at" class="task-tail"><TaskDueDisplay :due-at="node.task.due_at" :due-has-time="node.task.due_has_time" :completed="node.task.completed" :now-ms="taskDueNowMs" /></span><span v-if="activeView==='trash'" class="task-actions"><button class="restore" @click.stop="restoreTask(node.task)"><ArchiveRestore/>恢复</button><button class="icon danger ghost" aria-label="永久删除" @click.stop="purgeTask(node.task)"><X/></button></span><span v-else class="task-actions"><button class="icon ghost" aria-label="删除任务" @click.stop="removeTask(node.task)"><Trash2/></button></span>
|
||||
<span v-if="node.task.due_at" class="task-tail"><TaskDueDisplay :due-at="node.task.due_at" :due-has-time="node.task.due_has_time" :completed="node.task.completed" :now-ms="taskDueNowMs" /></span><span v-if="activeView==='trash'" class="task-actions"><button class="restore" @click.stop="restoreTask(node.task)"><ArchiveRestore/>恢复</button><button class="icon danger ghost" aria-label="永久删除" @click.stop="purgeTask(node.task)"><X/></button></span><span v-else class="task-actions"><button class="icon ghost task-detail-trigger" aria-label="打开任务详情" @click.stop="selectTask(node.task)"><Ellipsis/></button></span>
|
||||
</article>
|
||||
<article v-if="!collapsedTaskIds.has(node.task.id)" v-for="subtask in node.subtasks" :key="subtask.id" :data-task-id="subtask.id" class="task-row subtask swipeable" :class="{done:subtask.completed,'just-completed': justCompletedTaskIds.has(subtask.id),'completion-exiting':completionExitingTaskIds.has(subtask.id),ready:Math.abs(taskSwipeOffsets[subtask.id] ?? 0) >= 64,reordering:taskReorder?.id===subtask.id,'reorder-target':taskReorderTarget===subtask.id}" :style="{ '--swipe-x': `${taskSwipeOffsets[subtask.id] ?? 0}px`, '--reorder-y': `${taskReorder?.id === subtask.id ? taskReorder.offsetY : 0}px` }" @pointerdown="startTaskPointer(subtask, $event)" @pointermove="moveTaskPointer(subtask, $event)" @pointerup="finishTaskPointer(subtask, $event)" @pointercancel="cancelTaskPointer(subtask)" @touchstart.passive="startTaskSwipe(subtask, $event)" @touchmove.passive="moveTaskSwipe(subtask, $event)" @touchend="finishTaskSwipe(subtask, $event)" @touchcancel="cancelTaskSwipe(subtask)"><button v-if="activeView!=='trash'" class="drag-handle task-drag-handle" aria-label="上下拖动子任务排序" title="上下拖动排序" @pointerdown.stop="startTaskReorder(subtask, $event)" @pointermove.stop="moveTaskReorder(subtask, $event)" @pointerup.stop="finishTaskReorder(subtask, $event)" @pointercancel.stop="cancelTaskReorder"><GripVertical/></button><button v-if="activeView!=='trash'" class="task-check" :aria-label="subtask.completed ? `重新打开${subtask.title}` : `完成${subtask.title}`" :aria-pressed="subtask.completed" @click.stop="toggle(subtask)"><span class="task-check-mark" :class="`p${subtask.priority}`"><Check v-if="subtask.completed" /></span></button><div class="task-main" role="button" tabindex="0" @click="activeView==='trash'?undefined:selectTaskUnlessSwiped(subtask)" @keydown.enter="activeView==='trash'?undefined:selectTaskUnlessSwiped(subtask)" @keydown.space.prevent="activeView==='trash'?undefined:selectTaskUnlessSwiped(subtask)"><strong :title="subtask.title">{{subtask.title}}</strong></div><span v-if="subtask.due_at" class="task-tail"><TaskDueDisplay :due-at="subtask.due_at" :due-has-time="subtask.due_has_time" :completed="subtask.completed" :now-ms="taskDueNowMs" /></span></article>
|
||||
<article v-if="!collapsedTaskIds.has(node.task.id)" v-for="subtask in node.subtasks" :key="subtask.id" :data-task-id="subtask.id" class="task-row subtask swipeable" :class="{done:subtask.completed,'just-completed': justCompletedTaskIds.has(subtask.id),'completion-exiting':completionExitingTaskIds.has(subtask.id),ready:Math.abs(taskSwipeOffsets[subtask.id] ?? 0) >= 64,reordering:taskReorder?.id===subtask.id,'reorder-target':taskReorderTarget===subtask.id}" :style="{ '--swipe-x': `${taskSwipeOffsets[subtask.id] ?? 0}px`, '--reorder-y': `${taskReorder?.id === subtask.id ? taskReorder.offsetY : 0}px` }" @pointerdown="startTaskPointer(subtask, $event)" @pointermove="moveTaskPointer(subtask, $event)" @pointerup="finishTaskPointer(subtask, $event)" @pointercancel="cancelTaskPointer(subtask)" @touchstart.passive="startTaskSwipe(subtask, $event)" @touchmove.passive="moveTaskSwipe(subtask, $event)" @touchend="finishTaskSwipe(subtask, $event)" @touchcancel="cancelTaskSwipe(subtask)"><button v-if="activeView!=='trash'" class="drag-handle task-drag-handle" aria-label="上下拖动子任务排序" title="上下拖动排序" @pointerdown.stop="startTaskReorder(subtask, $event)" @pointermove.stop="moveTaskReorder(subtask, $event)" @pointerup.stop="finishTaskReorder(subtask, $event)" @pointercancel.stop="cancelTaskReorder"><GripVertical/></button><button v-if="activeView!=='trash'" class="task-check" :aria-label="subtask.completed ? `重新打开${subtask.title}` : `完成${subtask.title}`" :aria-pressed="subtask.completed" @click.stop="toggle(subtask)"><span class="task-check-mark" :class="`p${subtask.priority}`"><Check v-if="subtask.completed" /></span></button><div class="task-main" role="button" tabindex="0" @click="activeView==='trash'?undefined:selectTaskUnlessSwiped(subtask)" @keydown.enter="activeView==='trash'?undefined:selectTaskUnlessSwiped(subtask)" @keydown.space.prevent="activeView==='trash'?undefined:selectTaskUnlessSwiped(subtask)"><strong :title="subtask.title">{{subtask.title}}</strong></div><span v-if="subtask.due_at" class="task-tail"><TaskDueDisplay :due-at="subtask.due_at" :due-has-time="subtask.due_has_time" :completed="subtask.completed" :now-ms="taskDueNowMs" /></span><span v-if="activeView!=='trash'" class="task-actions"><button class="icon ghost task-detail-trigger" aria-label="打开任务详情" @click.stop="selectTask(subtask)"><Ellipsis/></button></span></article>
|
||||
</template>
|
||||
<div v-if="!visibleTasks.length&&!loading" class="empty"><ListTodo/><b>{{ query ? '没有匹配的任务' : hiddenCompletedTaskCount > 0 ? '已完成任务已隐藏' : '这里还很安静' }}</b><span>{{query?'换个关键词试试':hiddenCompletedTaskCount > 0 ? '开启“显示已完成”即可查看。':'写下第一件想完成的小事吧'}}</span><button v-if="activeView==='today' && !query && hiddenCompletedTaskCount === 0" class="soft-button empty-action" @click="openTaskCompose"><Plus/>添加今天任务</button></div>
|
||||
</section>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -43,7 +43,7 @@ describe('unified task due display', () => {
|
||||
expect(ordinaryChildren).not.toContain('restoreTask(subtask)')
|
||||
expect(ordinaryChildren).not.toContain('purgeTask(subtask)')
|
||||
expect(ordinaryChildren).not.toContain('v-if="activeView===\'trash\'" class="task-actions"')
|
||||
expect(app).toContain('<span v-else class="task-actions"><button class="icon ghost"')
|
||||
expect(app).toContain('<span v-else class="task-actions"><button class="icon ghost task-detail-trigger"')
|
||||
expect(app).toContain('<span v-if="node.task.priority" class="priority"')
|
||||
expect(app).toContain('v-if="node.subtasks.length" class="meta"')
|
||||
expect(app).toContain('<span class="meta-item"><ListChecks/>')
|
||||
@@ -155,11 +155,21 @@ describe('settings sessions and audit activity', () => {
|
||||
})
|
||||
|
||||
describe('solid cream material system', () => {
|
||||
it('defines three opaque cream surfaces, warm borders, restrained depth, and the radius scale', () => {
|
||||
expect(css).toContain('--surface-canvas:#f6f0e5')
|
||||
expect(css).toContain('--surface-base:#fff9ef')
|
||||
it('defines the approved opaque cream surfaces, text colors, warm border, and status colors', () => {
|
||||
expect(css).toContain('--surface-canvas:#f3ecdf')
|
||||
expect(css).toContain('--surface-base:#fdfaf3')
|
||||
expect(css).toContain('--surface-raised:#fffdf8')
|
||||
expect(css).toContain('--border-cream:#e5d7c3')
|
||||
expect(css).toContain('--text-primary:#302a24')
|
||||
expect(css).toContain('--text-secondary:#655b50')
|
||||
expect(css).toContain('--border-cream:#e4d5c0')
|
||||
expect(css).toContain('--accent:#f15a29')
|
||||
expect(css).toContain('--success:#687e61')
|
||||
expect(css).toContain('--radius-list:15px')
|
||||
expect(css).toContain('font-family:-apple-system,BlinkMacSystemFont,"Avenir Next","PingFang SC"')
|
||||
expect(css).toContain('font-variant-numeric:tabular-nums')
|
||||
expect(css).toContain('.habit-progress-bar::-webkit-progress-value{background:var(--success)')
|
||||
expect(css).toContain('.habit-progress-bar::-moz-progress-bar{background:var(--success)')
|
||||
expect(css).not.toContain('linear-gradient(90deg,#f7a066,var(--accent))')
|
||||
expect(css).toContain('--highlight-inner:inset 0 1px 0 #fff')
|
||||
expect(css).toContain('--radius-control:11px')
|
||||
expect(css).toContain('--radius-card:14px')
|
||||
@@ -199,6 +209,38 @@ describe('solid cream material system', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('approved UI detail direction', () => {
|
||||
it('uses one More detail entry for ordinary parent and child rows while preserving Trash and overdue actions', () => {
|
||||
const ordinaryRows = app.slice(app.indexOf('<section class="task-list"'), app.indexOf('</section>', app.indexOf('<section class="task-list"')))
|
||||
expect(ordinaryRows.match(/aria-label="打开任务详情"/g)).toHaveLength(2)
|
||||
expect(ordinaryRows).toContain('@click.stop="selectTask(node.task)"><Ellipsis/>')
|
||||
expect(ordinaryRows).toContain('@click.stop="selectTask(subtask)"><Ellipsis/>')
|
||||
expect(ordinaryRows).not.toContain('aria-label="删除任务"')
|
||||
expect(ordinaryRows).toContain('v-if="activeView===\'trash\'" class="task-actions"')
|
||||
expect(ordinaryRows).toContain('restoreTask(node.task)')
|
||||
expect(ordinaryRows).toContain('purgeTask(node.task)')
|
||||
const overdue = app.slice(app.indexOf('<section v-if="overdueTaskTree.length"'), app.indexOf('<h3 id="today-tasks"'))
|
||||
expect(overdue).not.toContain('aria-label="打开任务详情"')
|
||||
})
|
||||
|
||||
it('keeps the due tail immediately before the More detail entry', () => {
|
||||
expect(app).toContain('</span><span v-else class="task-actions"><button class="icon ghost task-detail-trigger" aria-label="打开任务详情" @click.stop="selectTask(node.task)"><Ellipsis/></button></span>')
|
||||
expect(app).toContain('</span><span v-if="activeView!==\'trash\'" class="task-actions"><button class="icon ghost task-detail-trigger" aria-label="打开任务详情" @click.stop="selectTask(subtask)"><Ellipsis/></button></span>')
|
||||
expect(css).toContain('.task-detail-trigger:hover{color:var(--text-primary);background:#fff7eb}')
|
||||
})
|
||||
|
||||
it('keeps task details on a 12px rhythm with fixed labels and right-aligned controls', () => {
|
||||
expect(css).toContain('.detail-form{min-width:0;grid-template-columns:minmax(0,1fr);padding:19px;display:grid;gap:12px}')
|
||||
expect(css).toContain('.detail-form>label{grid-template-columns:80px 1fr;align-items:center}')
|
||||
expect(css).toContain('.task-detail-field-input{width:190px!important;max-width:100%;justify-self:end}')
|
||||
})
|
||||
|
||||
it('keeps refresh neutral and at least 44px', () => {
|
||||
expect(css).toContain('.topbar-refresh{width:44px;height:44px;min-width:44px;border-radius:50%;color:var(--text-secondary)}')
|
||||
expect(css).not.toContain('.topbar-refresh:hover:not(:disabled){background:#fff7eb;color:var(--accent)}')
|
||||
})
|
||||
})
|
||||
|
||||
describe('completion feedback motion', () => {
|
||||
it('delays removal only when a completed row will become hidden', () => {
|
||||
expect(shouldAnimateCompletionExit({ completing: true, showCompleted: false })).toBe(true)
|
||||
@@ -601,7 +643,7 @@ describe('task and habit row decoration', () => {
|
||||
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:linear-gradient')
|
||||
expect(css).toContain('.habit-progress-bar::-webkit-progress-value{background:var(--success)')
|
||||
})
|
||||
|
||||
it('restores the current page and list from local storage', () => {
|
||||
|
||||
Reference in New Issue
Block a user