feat: unify design tokens, five font weights, radius scale, restrained motion, AA contrast
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
+15
-8
@@ -111,6 +111,7 @@ const markdownPreview = ref(false)
|
|||||||
const taskNoteEditor = ref<HTMLTextAreaElement | null>(null)
|
const taskNoteEditor = ref<HTMLTextAreaElement | null>(null)
|
||||||
const SHOW_COMPLETED_STORAGE_KEY = 'dodo.show-completed'
|
const SHOW_COMPLETED_STORAGE_KEY = 'dodo.show-completed'
|
||||||
const showCompleted = ref(readStoredBoolean(window.localStorage, SHOW_COMPLETED_STORAGE_KEY, true))
|
const showCompleted = ref(readStoredBoolean(window.localStorage, SHOW_COMPLETED_STORAGE_KEY, true))
|
||||||
|
const completedFilterReveal = ref(false)
|
||||||
const TODAY_SECTION_COLLAPSE_KEY = 'dodo.today-section-collapse.v1'
|
const TODAY_SECTION_COLLAPSE_KEY = 'dodo.today-section-collapse.v1'
|
||||||
const todaySectionCollapse = ref<TodaySectionCollapse>(readTodaySectionCollapse(window.localStorage, TODAY_SECTION_COLLAPSE_KEY))
|
const todaySectionCollapse = ref<TodaySectionCollapse>(readTodaySectionCollapse(window.localStorage, TODAY_SECTION_COLLAPSE_KEY))
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
@@ -449,6 +450,10 @@ watch(taskReorderAvailable, () => {
|
|||||||
})
|
})
|
||||||
watch(showCompleted, (value) => {
|
watch(showCompleted, (value) => {
|
||||||
writeStoredBoolean(window.localStorage, SHOW_COMPLETED_STORAGE_KEY, value)
|
writeStoredBoolean(window.localStorage, SHOW_COMPLETED_STORAGE_KEY, value)
|
||||||
|
if (value) {
|
||||||
|
completedFilterReveal.value = true
|
||||||
|
window.setTimeout(() => { completedFilterReveal.value = false }, 500)
|
||||||
|
}
|
||||||
if (isTaskView(activeView.value)) { page.value = 1; loadAll() }
|
if (isTaskView(activeView.value)) { page.value = 1; loadAll() }
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1819,15 +1824,15 @@ onUnmounted(() => {
|
|||||||
<CompletedFilterPill v-model="showCompleted" class="list-inline-filter" />
|
<CompletedFilterPill v-model="showCompleted" class="list-inline-filter" />
|
||||||
</section>
|
</section>
|
||||||
<template v-if="activeView==='today'">
|
<template v-if="activeView==='today'">
|
||||||
<section v-if="overdueTaskTree.length" class="overdue-section today-collapsible-section">
|
<section v-if="overdueTaskTree.length" class="overdue-section today-collapsible-section" :class="{'is-collapsed': todaySectionCollapse.overdue}">
|
||||||
<button id="today-overdue-heading" class="today-section-toggle" type="button" :aria-expanded="!todaySectionCollapse.overdue" aria-controls="today-overdue" @click="toggleTodaySection('overdue')"><span class="today-section-title">逾期</span><span class="today-section-summary">{{overdueTaskTree.length}}</span><span class="today-section-chevron" aria-hidden="true">{{ todaySectionCollapse.overdue ? '›' : '⌄' }}</span></button>
|
<button id="today-overdue-heading" class="today-section-toggle" type="button" :aria-expanded="!todaySectionCollapse.overdue" aria-controls="today-overdue" @click="toggleTodaySection('overdue')"><span class="today-section-title">逾期</span><span class="today-section-summary">{{overdueTaskTree.length}}</span><span class="today-section-chevron" :class="{open: !todaySectionCollapse.overdue}" aria-hidden="true">›</span></button>
|
||||||
<div v-show="!todaySectionCollapse.overdue" id="today-overdue" class="task-list plain-list overdue-list" role="region" aria-labelledby="today-overdue-heading">
|
<div id="today-overdue" class="task-list plain-list overdue-list" role="region" aria-labelledby="today-overdue-heading">
|
||||||
<template v-for="node in overdueTaskTree" :key="`overdue-${node.task.id}`">
|
<template v-for="node in overdueTaskTree" :key="`overdue-${node.task.id}`">
|
||||||
<article :data-task-id="node.task.id" class="task-row overdue-task swipeable" :class="{'just-completed':justCompletedTaskIds.has(node.task.id),'completion-exiting':completionExitingTaskIds.has(node.task.id),ready:Math.abs(taskSwipeOffsets[node.task.id] ?? 0) >= 64}" :style="{ '--swipe-x': `${taskSwipeOffsets[node.task.id] ?? 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 class="task-check" :aria-label="`完成${node.task.title}`" :aria-pressed="false" @click.stop="toggle(node.task)"><span class="task-check-mark" :class="`p${node.task.priority}`"></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></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></article>
|
<article :data-task-id="node.task.id" class="task-row overdue-task swipeable" :class="{'just-completed':justCompletedTaskIds.has(node.task.id),'completion-exiting':completionExitingTaskIds.has(node.task.id),ready:Math.abs(taskSwipeOffsets[node.task.id] ?? 0) >= 64}" :style="{ '--swipe-x': `${taskSwipeOffsets[node.task.id] ?? 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 class="task-check" :aria-label="`完成${node.task.title}`" :aria-pressed="false" @click.stop="toggle(node.task)"><span class="task-check-mark" :class="`p${node.task.priority}`"></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></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></article>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<button id="today-tasks-heading" class="today-section-toggle today-section-anchor" type="button" :aria-expanded="!todaySectionCollapse.tasks" aria-controls="today-tasks" @click="toggleTodaySection('tasks')"><span class="today-section-title">今天</span><span class="today-section-summary">{{totalTasks}}</span><span class="today-section-chevron" aria-hidden="true">{{ todaySectionCollapse.tasks ? '›' : '⌄' }}</span></button>
|
<button id="today-tasks-heading" class="today-section-toggle today-section-anchor" type="button" :aria-expanded="!todaySectionCollapse.tasks" aria-controls="today-tasks" @click="toggleTodaySection('tasks')"><span class="today-section-title">今天</span><span class="today-section-summary">{{totalTasks}}</span><span class="today-section-chevron" :class="{open: !todaySectionCollapse.tasks}" aria-hidden="true">›</span></button>
|
||||||
</template>
|
</template>
|
||||||
<section v-if="activeView==='trash'" class="trash-page-context">
|
<section v-if="activeView==='trash'" class="trash-page-context">
|
||||||
<div><h1 ref="trashPageTitle" class="trash-page-title" tabindex="-1">回收站</h1><p class="trash-page-summary">删除的任务保留在这里,可整组恢复或永久删除。</p></div>
|
<div><h1 ref="trashPageTitle" class="trash-page-title" tabindex="-1">回收站</h1><p class="trash-page-summary">删除的任务保留在这里,可整组恢复或永久删除。</p></div>
|
||||||
@@ -1846,9 +1851,10 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
<section v-else :id="activeView==='today' ? 'today-tasks' : undefined" class="task-list plain-list" :class="{loading}" v-show="activeView!=='today' || !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}" :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">
|
<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,'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),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 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>
|
<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>
|
<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>
|
||||||
@@ -1857,11 +1863,12 @@ onUnmounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
<div v-if="activeView==='today' && hiddenCompletedTaskCount > 0 && !visibleTasks.length && !loading" class="today-filtered-empty-note"><span>已隐藏已完成任务</span><button type="button" class="link" @click="showCompleted=true">显示</button></div>
|
<div v-if="activeView==='today' && hiddenCompletedTaskCount > 0 && !visibleTasks.length && !loading" class="today-filtered-empty-note"><span>已隐藏已完成任务</span><button type="button" class="link" @click="showCompleted=true">显示</button></div>
|
||||||
<div v-else-if="!visibleTasks.length&&!loading" class="empty"><ListTodo/><b>{{ activeView==='trash' ? '回收站是空的' : hiddenCompletedTaskCount > 0 ? '已完成任务已隐藏' : '这里还很安静' }}</b><span>{{activeView==='trash' ? '删除的任务会显示在这里' : hiddenCompletedTaskCount > 0 ? '开启“显示已完成”即可查看。':'写下第一件想完成的小事吧'}}</span></div>
|
<div v-else-if="!visibleTasks.length&&!loading" class="empty"><ListTodo/><b>{{ activeView==='trash' ? '回收站是空的' : hiddenCompletedTaskCount > 0 ? '已完成任务已隐藏' : '这里还很安静' }}</b><span>{{activeView==='trash' ? '删除的任务会显示在这里' : hiddenCompletedTaskCount > 0 ? '开启“显示已完成”即可查看。':'写下第一件想完成的小事吧'}}</span></div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<nav v-if="totalPages > 1 && (activeView!=='today' || !todaySectionCollapse.tasks)" class="pager" aria-label="任务分页"><button class="pager-button pager-button--previous" :disabled="page<=1 || loading" aria-label="上一页" @click="previousPage"><ChevronLeft aria-hidden="true"/><span>上一页</span></button><span class="pager-status" aria-live="polite"><strong>{{page}} / {{totalPages}}</strong><span>共 {{ totalTasks }} 项</span></span><button class="pager-button pager-button--next" :disabled="page>=totalPages || loading" aria-label="下一页" @click="nextPage"><span>下一页</span><ChevronRight aria-hidden="true"/></button></nav>
|
<nav v-if="totalPages > 1 && (activeView!=='today' || !todaySectionCollapse.tasks)" class="pager" aria-label="任务分页"><button class="pager-button pager-button--previous" :disabled="page<=1 || loading" aria-label="上一页" @click="previousPage"><ChevronLeft aria-hidden="true"/><span>上一页</span></button><span class="pager-status" aria-live="polite"><strong>{{page}} / {{totalPages}}</strong><span>共 {{ totalTasks }} 项</span></span><button class="pager-button pager-button--next" :disabled="page>=totalPages || loading" aria-label="下一页" @click="nextPage"><span>下一页</span><ChevronRight aria-hidden="true"/></button></nav>
|
||||||
<div v-if="activeView==='today'" class="today-habits-section today-section-anchor">
|
<div v-if="activeView==='today'" class="today-habits-section today-section-anchor">
|
||||||
<button id="today-habits-heading" class="today-section-toggle" type="button" :aria-expanded="!todaySectionCollapse.habits" aria-controls="today-habits" @click="toggleTodaySection('habits')"><span class="today-section-title">习惯</span><span class="today-section-summary">{{todayHabitTotal}}</span><span class="today-section-chevron" aria-hidden="true">{{ todaySectionCollapse.habits ? '›' : '⌄' }}</span></button>
|
<button id="today-habits-heading" class="today-section-toggle" type="button" :aria-expanded="!todaySectionCollapse.habits" aria-controls="today-habits" @click="toggleTodaySection('habits')"><span class="today-section-title">习惯</span><span class="today-section-summary">{{todayHabitTotal}}</span><span class="today-section-chevron" :class="{open: !todaySectionCollapse.habits}" aria-hidden="true">›</span></button>
|
||||||
<div v-show="!todaySectionCollapse.habits" id="today-habits" role="region" aria-labelledby="today-habits-heading">
|
<div id="today-habits" :class="{'is-collapsed': todaySectionCollapse.habits}" role="region" aria-labelledby="today-habits-heading">
|
||||||
<MvpPanel ref="habitComposer" view="today-habits" :show-completed="showCompleted" :compact-layout="compactLayout" @detail="handleHabitDetail" @changed="refreshAll" @notice="toast" @summary="updateTodayHabitSummary" />
|
<MvpPanel ref="habitComposer" view="today-habits" :show-completed="showCompleted" :compact-layout="compactLayout" @detail="handleHabitDetail" @changed="refreshAll" @notice="toast" @summary="updateTodayHabitSummary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,14 +36,14 @@ describe('Today independent collapsible sections', () => {
|
|||||||
expect(app.match(/class="today-section-toggle(?: today-section-anchor)?"/g)).toHaveLength(3)
|
expect(app.match(/class="today-section-toggle(?: today-section-anchor)?"/g)).toHaveLength(3)
|
||||||
expect(app).toContain('<span class="today-section-summary">{{totalTasks}}</span>')
|
expect(app).toContain('<span class="today-section-summary">{{totalTasks}}</span>')
|
||||||
expect(app).not.toContain('<span class="today-section-summary">{{taskTree.length}} 项</span>')
|
expect(app).not.toContain('<span class="today-section-summary">{{taskTree.length}} 项</span>')
|
||||||
expect(app).toContain("{{ todaySectionCollapse.overdue ? '›' : '⌄' }}")
|
expect(app).toContain('<span class="today-section-chevron" :class="{open: !todaySectionCollapse.overdue}" aria-hidden="true">›</span>')
|
||||||
expect(app).not.toContain('<ChevronRight v-if="todaySectionCollapse.overdue"')
|
expect(app).not.toContain('<ChevronRight v-if="todaySectionCollapse.overdue"')
|
||||||
expect(css).toMatch(/\.today-section-toggle\{[^}]*min-height:44px/)
|
expect(css).toMatch(/\.today-section-toggle\{[^}]*min-height:44px/)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps the habit panel mounted while collapsed and omits an empty overdue section', () => {
|
it('keeps the habit panel mounted while collapsed and omits an empty overdue section', () => {
|
||||||
expect(app).toContain('<section v-if="overdueTaskTree.length" class="overdue-section today-collapsible-section">')
|
expect(app).toContain('<section v-if="overdueTaskTree.length" class="overdue-section today-collapsible-section" :class="{\'is-collapsed\': todaySectionCollapse.overdue}">')
|
||||||
expect(app).toContain('<div v-show="!todaySectionCollapse.habits" id="today-habits"')
|
expect(app).toContain('<div id="today-habits" :class="{\'is-collapsed\': todaySectionCollapse.habits}" role="region"')
|
||||||
const habits = app.slice(app.indexOf('id="today-habits-heading"'), app.indexOf('</div>', app.indexOf('<MvpPanel ref="habitComposer" view="today-habits"')) + 6)
|
const habits = app.slice(app.indexOf('id="today-habits-heading"'), app.indexOf('</div>', app.indexOf('<MvpPanel ref="habitComposer" view="today-habits"')) + 6)
|
||||||
expect(habits).toContain('<MvpPanel ref="habitComposer" view="today-habits"')
|
expect(habits).toContain('<MvpPanel ref="habitComposer" view="today-habits"')
|
||||||
expect(habits).not.toContain('v-if="!todaySectionCollapse.habits"')
|
expect(habits).not.toContain('v-if="!todaySectionCollapse.habits"')
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -74,11 +74,13 @@ onBeforeUnmount(deactivate)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport v-if="modal" :to="overlayRoot()">
|
<Teleport v-if="modal" :to="overlayRoot()">
|
||||||
|
<Transition name="app-sheet">
|
||||||
<div v-if="open" class="app-overlay app-sheet-mask" :style="{ zIndex: layerZIndex }" :aria-busy="busy || undefined" @click="scrimClose">
|
<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">
|
<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 />
|
<slot />
|
||||||
</component>
|
</component>
|
||||||
</div>
|
</div>
|
||||||
|
</Transition>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<Teleport v-else-if="inlineTarget" :to="inlineTarget">
|
<Teleport v-else-if="inlineTarget" :to="inlineTarget">
|
||||||
<component v-if="open" :is="$attrs.onSubmit ? 'form' : 'section'" ref="panel" class="app-sheet" :class="[`app-sheet--${variant}`, panelClass]" role="dialog" :aria-labelledby="titleId" :aria-describedby="descriptionId" :aria-label="label" :aria-busy="busy || undefined" v-bind="$attrs">
|
<component v-if="open" :is="$attrs.onSubmit ? 'form' : 'section'" ref="panel" class="app-sheet" :class="[`app-sheet--${variant}`, panelClass]" role="dialog" :aria-labelledby="titleId" :aria-describedby="descriptionId" :aria-label="label" :aria-busy="busy || undefined" v-bind="$attrs">
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ describe('add-task CalendarPicker integration', () => {
|
|||||||
|
|
||||||
it('uses a fixed desktop popover and opaque mobile bottom sheet above task compose', () => {
|
it('uses a fixed desktop popover and opaque mobile bottom sheet above task compose', () => {
|
||||||
expect(css).toContain('.calendar-picker-layer{position:fixed;z-index:90;inset:0;pointer-events:none}')
|
expect(css).toContain('.calendar-picker-layer{position:fixed;z-index:90;inset:0;pointer-events:none}')
|
||||||
expect(css).toContain('.calendar-picker{position:fixed;width:328px;background:#fffdf8;border:1px solid #ded6ca;')
|
expect(css).toContain('.calendar-picker{position:fixed;width:328px;background:var(--surface-raised);border:1px solid var(--calendar-picker-border);')
|
||||||
expect(css).toContain('.calendar-picker__grid button.selected{background:var(--accent);color:#fff}')
|
expect(css).toContain('.calendar-picker__grid button.selected{background:var(--accent);color:var(--white)}')
|
||||||
expect(css).toContain('@media(max-width:600px){.calendar-picker-layer{pointer-events:auto;background:rgba(45,38,31,.3);display:flex;align-items:flex-end}')
|
expect(css).toContain('@media(max-width:600px){.calendar-picker-layer{pointer-events:auto;background:rgba(45,38,31,.3);display:flex;align-items:flex-end}')
|
||||||
expect(css).toContain('height:min(520px,72dvh)')
|
expect(css).toContain('height:min(520px,72dvh)')
|
||||||
expect(css).not.toContain('.calendar-picker{backdrop-filter')
|
expect(css).not.toContain('.calendar-picker{backdrop-filter')
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const draft = ref('')
|
|||||||
const focusedDate = ref(new Date())
|
const focusedDate = ref(new Date())
|
||||||
const visibleMonth = ref(new Date())
|
const visibleMonth = ref(new Date())
|
||||||
const compact = ref(false)
|
const compact = ref(false)
|
||||||
|
const monthDir = ref(0)
|
||||||
const position = ref({ left: 12, top: 12 })
|
const position = ref({ left: 12, top: 12 })
|
||||||
const weekdays = ['一', '二', '三', '四', '五', '六', '日']
|
const weekdays = ['一', '二', '三', '四', '五', '六', '日']
|
||||||
const title = computed(() => new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: 'long' }).format(visibleMonth.value))
|
const title = computed(() => new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: 'long' }).format(visibleMonth.value))
|
||||||
@@ -22,6 +23,12 @@ function choose(value: string) {
|
|||||||
draft.value = value
|
draft.value = value
|
||||||
const date = parseLocalDate(value)!
|
const date = parseLocalDate(value)!
|
||||||
focusedDate.value = date
|
focusedDate.value = date
|
||||||
|
aimMonth(date)
|
||||||
|
}
|
||||||
|
function aimMonth(date: Date) {
|
||||||
|
const prev = visibleMonth.value
|
||||||
|
const delta = (date.getFullYear() - prev.getFullYear()) * 12 + (date.getMonth() - prev.getMonth())
|
||||||
|
if (delta !== 0) monthDir.value = delta > 0 ? 1 : -1
|
||||||
visibleMonth.value = new Date(date.getFullYear(), date.getMonth(), 1)
|
visibleMonth.value = new Date(date.getFullYear(), date.getMonth(), 1)
|
||||||
}
|
}
|
||||||
function finish() { emit('update:modelValue', draft.value); close() }
|
function finish() { emit('update:modelValue', draft.value); close() }
|
||||||
@@ -33,7 +40,7 @@ function changeMonth(amount: number) {
|
|||||||
}
|
}
|
||||||
function focusDay(date: Date) {
|
function focusDay(date: Date) {
|
||||||
focusedDate.value = date
|
focusedDate.value = date
|
||||||
visibleMonth.value = new Date(date.getFullYear(), date.getMonth(), 1)
|
aimMonth(date)
|
||||||
nextTick(() => dialog.value?.querySelector<HTMLElement>(`[data-date="${formatLocalDate(date)}"]`)?.focus())
|
nextTick(() => dialog.value?.querySelector<HTMLElement>(`[data-date="${formatLocalDate(date)}"]`)?.focus())
|
||||||
}
|
}
|
||||||
function onGridKey(event: KeyboardEvent) {
|
function onGridKey(event: KeyboardEvent) {
|
||||||
@@ -90,6 +97,7 @@ onBeforeUnmount(() => { document.removeEventListener('pointerdown', onOutside);
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
|
<Transition name="calendar-picker">
|
||||||
<div v-if="open" class="calendar-picker-layer" :class="{compact}" @click.self="compact && close()">
|
<div v-if="open" class="calendar-picker-layer" :class="{compact}" @click.self="compact && close()">
|
||||||
<section ref="dialog" class="calendar-picker" :class="{compact}" :style="dialogStyle" role="dialog" aria-modal="true" aria-labelledby="calendar-picker-title" @keydown="onDialogKey">
|
<section ref="dialog" class="calendar-picker" :class="{compact}" :style="dialogStyle" role="dialog" aria-modal="true" aria-labelledby="calendar-picker-title" @keydown="onDialogKey">
|
||||||
<header class="calendar-picker__header">
|
<header class="calendar-picker__header">
|
||||||
@@ -99,7 +107,7 @@ onBeforeUnmount(() => { document.removeEventListener('pointerdown', onOutside);
|
|||||||
<button v-if="compact" class="calendar-picker__close" type="button" aria-label="关闭日期选择器" @click="close"><X/></button>
|
<button v-if="compact" class="calendar-picker__close" type="button" aria-label="关闭日期选择器" @click="close"><X/></button>
|
||||||
</header>
|
</header>
|
||||||
<div class="calendar-picker__weekdays" aria-hidden="true"><span v-for="day in weekdays" :key="day">{{ day }}</span></div>
|
<div class="calendar-picker__weekdays" aria-hidden="true"><span v-for="day in weekdays" :key="day">{{ day }}</span></div>
|
||||||
<div class="calendar-picker__grid" role="grid" aria-label="日期" @keydown="onGridKey">
|
<div class="calendar-picker__grid" :key="title" :class="monthDir > 0 ? 'shift-next' : monthDir < 0 ? 'shift-prev' : ''" role="grid" aria-label="日期" @keydown="onGridKey">
|
||||||
<button v-for="day in days" :key="day.value" type="button" role="gridcell" :data-date="day.value" :class="{outside:!day.inMonth,today:day.isToday,selected:day.isSelected}" :aria-label="day.value" :aria-selected="day.isSelected" :aria-current="day.isToday?'date':undefined" :tabindex="day.value===formatLocalDate(focusedDate)?0:-1" @focus="focusedDate=day.date" @click="choose(day.value)">{{ day.date.getDate() }}</button>
|
<button v-for="day in days" :key="day.value" type="button" role="gridcell" :data-date="day.value" :class="{outside:!day.inMonth,today:day.isToday,selected:day.isSelected}" :aria-label="day.value" :aria-selected="day.isSelected" :aria-current="day.isToday?'date':undefined" :tabindex="day.value===formatLocalDate(focusedDate)?0:-1" @focus="focusedDate=day.date" @click="choose(day.value)">{{ day.date.getDate() }}</button>
|
||||||
</div>
|
</div>
|
||||||
<footer class="calendar-picker__footer">
|
<footer class="calendar-picker__footer">
|
||||||
@@ -111,5 +119,6 @@ onBeforeUnmount(() => { document.removeEventListener('pointerdown', onOutside);
|
|||||||
</footer>
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
</Transition>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -130,10 +130,10 @@ describe('TodayEnvironmentStrip', () => {
|
|||||||
it('uses the selected landing-page typography and full-height separators', () => {
|
it('uses the selected landing-page typography and full-height separators', () => {
|
||||||
const css = readFileSync(resolve(process.cwd(), 'src/style.css'), 'utf8')
|
const css = readFileSync(resolve(process.cwd(), 'src/style.css'), 'utf8')
|
||||||
expect(css).toContain('.today-environment{grid-column:1/-1;position:relative;min-width:0;display:grid;grid-template-columns:minmax(0,1.25fr) minmax(0,1fr) minmax(0,1fr);align-items:stretch')
|
expect(css).toContain('.today-environment{grid-column:1/-1;position:relative;min-width:0;display:grid;grid-template-columns:minmax(0,1.25fr) minmax(0,1fr) minmax(0,1fr);align-items:stretch')
|
||||||
expect(css).toContain('color:#302a24;font-size:23px;line-height:1.15;font-weight:760')
|
expect(css).toContain('color:var(--text-primary);font-size:23px;line-height:1.15;font-weight:800')
|
||||||
expect(css).toContain('margin-top:6px;color:#655b50;font-size:12px;font-weight:450')
|
expect(css).toContain('margin-top:6px;color:var(--text-secondary);font-size:12px;font-weight:400')
|
||||||
expect(css).toContain('.today-environment__eyeline{color:#8b8275;font-size:11px')
|
expect(css).toContain('.today-environment__eyeline{color:var(--text-secondary);font-size:11px')
|
||||||
expect(css).toContain('.today-environment__item+.today-environment__item{border-left:1px solid var(--line)}')
|
expect(css).toContain('.today-environment__item+.today-environment__item{border-left:1px solid var(--border-cream)}')
|
||||||
expect(css).not.toContain('.today-environment.has-attention:before')
|
expect(css).not.toContain('.today-environment.has-attention:before')
|
||||||
expect(css).not.toMatch(/\.today-environment[^}]*gradient|\.today-environment[^}]*box-shadow|\.today-environment[^}]*backdrop-filter/)
|
expect(css).not.toMatch(/\.today-environment[^}]*gradient|\.today-environment[^}]*box-shadow|\.today-environment[^}]*backdrop-filter/)
|
||||||
})
|
})
|
||||||
@@ -143,8 +143,8 @@ describe('TodayEnvironmentStrip', () => {
|
|||||||
expect(css).not.toContain('.today-track-head{')
|
expect(css).not.toContain('.today-track-head{')
|
||||||
expect(css).toContain('@media(max-width:720px){.today-environment{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);grid-template-rows:auto auto}')
|
expect(css).toContain('@media(max-width:720px){.today-environment{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);grid-template-rows:auto auto}')
|
||||||
expect(css).toContain('.today-environment__calendar{grid-column:1/-1;grid-row:1;display:flex;flex-direction:row;align-items:baseline;justify-content:space-between')
|
expect(css).toContain('.today-environment__calendar{grid-column:1/-1;grid-row:1;display:flex;flex-direction:row;align-items:baseline;justify-content:space-between')
|
||||||
expect(css).toContain('.today-environment__weather{grid-column:1;grid-row:2;border-top:1px solid var(--line);border-left:0!important}')
|
expect(css).toContain('.today-environment__weather{grid-column:1;grid-row:2;border-top:1px solid var(--border-cream);border-left:0!important}')
|
||||||
expect(css).toContain('.today-environment__gold{grid-column:2;grid-row:2;border-top:1px solid var(--line)}')
|
expect(css).toContain('.today-environment__gold{grid-column:2;grid-row:2;border-top:1px solid var(--border-cream)}')
|
||||||
expect(css).toContain('.today-environment__weather,.today-environment__gold{padding:13px 10px;display:flex;flex-direction:column;')
|
expect(css).toContain('.today-environment__weather,.today-environment__gold{padding:13px 10px;display:flex;flex-direction:column;')
|
||||||
expect(css).toContain('.today-environment__calendar-main--desktop,.today-environment__gold-date--desktop{display:none}')
|
expect(css).toContain('.today-environment__calendar-main--desktop,.today-environment__gold-date--desktop{display:none}')
|
||||||
expect(css).toContain('.today-environment__calendar-main--mobile,.today-environment__gold-date--mobile{display:inline}')
|
expect(css).toContain('.today-environment__calendar-main--mobile,.today-environment__gold-date--mobile{display:inline}')
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
.memo-panel{position:relative;min-height:calc(100vh - 130px)}.shell.memo-detail-open main{padding-right:372px}.memo-panel__main{display:grid;gap:14px}.memo-toolbar{display:flex;align-items:center;gap:12px}.memo-scope{display:flex;gap:4px;padding:3px;border:1px solid var(--border-cream);border-radius:12px;background:var(--surface-raised)}.memo-scope button{min-height:44px;display:inline-flex;align-items:center;gap:6px;border:0;border-radius:9px;background:transparent;padding:0 13px}.memo-scope button[aria-selected="true"]{background:var(--accent-soft);color:#b7421e;font-weight:700}.memo-list{display:grid;gap:0;border:1px solid var(--border-cream);border-radius:var(--radius-list);background:var(--surface-raised);overflow:hidden}.memo-list.refreshing{opacity:.62}.memo-row{width:100%;height:74px;min-height:74px;max-height:76px;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:12px;border:0;background:var(--surface-raised);padding:9px 15px;text-align:left}.memo-row+.memo-row{border-top:1px solid var(--border-cream)}.memo-row:hover,.memo-row.active{background:#fff7eb}.memo-row strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.memo-row time{grid-column:2;grid-row:1;align-self:center;color:var(--muted);font-size:11px}.memo-state{min-height:240px;display:grid;place-items:center;align-content:center;gap:9px;color:var(--muted);text-align:center}.memo-state svg{width:30px;height:30px;color:var(--accent)}.memo-load-more{justify-self:center;min-width:132px;min-height:44px}.memo-error,.memo-editor__error{color:var(--danger);background:#fff0ec;border-radius:10px;padding:10px 12px}.memo-editor>.memo-editor{display:contents}.memo-editor{width:350px;position:fixed;z-index:42;right:0;top:0;bottom:0;display:flex;flex-direction:column;border-left:1px solid var(--border-cream);background:var(--surface-raised);box-shadow:var(--shadow-raised)}.memo-editor header,.memo-editor footer{min-height:64px;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 16px;border-bottom:1px solid var(--border-cream)}.memo-editor header span{font-size:12px;font-weight:750;letter-spacing:.06em;color:var(--muted)}.memo-editor header button{width:44px;height:44px;display:grid;place-items:center;border:0;border-radius:10px;background:transparent}.memo-editor__fields{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;display:grid;align-content:start;gap:14px;padding:18px}.memo-editor__fields label{display:grid;gap:7px;color:var(--text-secondary);font-size:12px;font-weight:700}.memo-editor__fields input,.memo-editor__fields textarea{width:100%;border:1px solid var(--border-cream);border-radius:11px;background:#fff;padding:12px;outline:0}.memo-editor__fields textarea{resize:vertical;line-height:1.65}.memo-editor__fields input:focus,.memo-editor__fields textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--focus-ring)}.memo-field{display:grid;gap:7px}.memo-field__label{display:flex;align-items:center;justify-content:space-between;gap:10px;color:var(--text-secondary);font-size:12px;font-weight:700}.memo-markdown-field{display:grid;gap:7px;min-width:0}.memo-markdown-editor{min-width:0}.memo-markdown-editor .markdown-toolbar{max-width:100%}.memo-markdown-editor textarea{min-height:250px;resize:vertical}.memo-markdown-preview{min-height:250px;max-height:none;width:100%;overflow-x:hidden}.memo-markdown-preview pre{max-width:100%;overflow-x:auto}.memo-editor footer{border-top:1px solid var(--border-cream);border-bottom:0}.memo-editor footer button{min-height:44px}.memo-editor-scrim{display:none}
|
.memo-panel{position:relative;min-height:calc(100vh - 130px)}.shell.memo-detail-open main{padding-right:372px}.memo-panel__main{display:grid;gap:14px}.memo-toolbar{display:flex;align-items:center;gap:12px}.memo-scope{display:flex;gap:4px;padding:3px;border:1px solid var(--border-cream);border-radius:var(--radius-control);background:var(--surface-raised)}.memo-scope button{min-height:44px;display:inline-flex;align-items:center;gap:6px;border:0;border-radius:var(--radius-control);background:transparent;padding:0 13px}.memo-scope button[aria-selected="true"]{background:var(--accent-soft);color:var(--accent-ink);font-weight:700}.memo-list{display:grid;gap:0;border:1px solid var(--border-cream);border-radius:var(--radius-list);background:var(--surface-raised);overflow:hidden}.memo-list.refreshing{opacity:.62}.memo-row{width:100%;height:74px;min-height:74px;max-height:76px;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:12px;border:0;background:var(--surface-raised);padding:9px 15px;text-align:left}.memo-row+.memo-row{border-top:1px solid var(--border-cream)}.memo-row:hover,.memo-row.active{background:var(--surface-hover)}.memo-row strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.memo-row time{grid-column:2;grid-row:1;align-self:center;color:var(--text-secondary);font-size:11px}.memo-state{min-height:240px;display:grid;place-items:center;align-content:center;gap:9px;color:var(--text-secondary);text-align:center}.memo-state svg{width:30px;height:30px;color:var(--accent)}.memo-load-more{justify-self:center;min-width:132px;min-height:44px}.memo-error,.memo-editor__error{color:var(--danger);background:var(--error-wash);border-radius:var(--radius-control);padding:10px 12px}.memo-editor>.memo-editor{display:contents}.memo-editor{width:350px;position:fixed;z-index:42;right:0;top:0;bottom:0;display:flex;flex-direction:column;border-left:1px solid var(--border-cream);background:var(--surface-raised);box-shadow:var(--shadow-raised)}.memo-editor header,.memo-editor footer{min-height:64px;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 16px;border-bottom:1px solid var(--border-cream)}.memo-editor header span{font-size:12px;font-weight:700;letter-spacing:.06em;color:var(--text-secondary)}.memo-editor header button{width:44px;height:44px;display:grid;place-items:center;border:0;border-radius:var(--radius-control);background:transparent}.memo-editor__fields{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;display:grid;align-content:start;gap:14px;padding:18px}.memo-editor__fields label{display:grid;gap:7px;color:var(--text-secondary);font-size:12px;font-weight:700}.memo-editor__fields input,.memo-editor__fields textarea{width:100%;border:1px solid var(--border-cream);border-radius:var(--radius-control);background:var(--white);padding:12px;outline:0}.memo-editor__fields textarea{resize:vertical;line-height:1.65}.memo-editor__fields input:focus,.memo-editor__fields textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--focus-ring)}.memo-field{display:grid;gap:7px}.memo-field__label{display:flex;align-items:center;justify-content:space-between;gap:10px;color:var(--text-secondary);font-size:12px;font-weight:700}.memo-markdown-field{display:grid;gap:7px;min-width:0}.memo-markdown-editor{min-width:0}.memo-markdown-editor .markdown-toolbar{max-width:100%}.memo-markdown-editor textarea{min-height:250px;resize:vertical}.memo-markdown-preview{min-height:250px;max-height:none;width:100%;overflow-x:hidden}.memo-markdown-preview pre{max-width:100%;overflow-x:auto}.memo-editor footer{border-top:1px solid var(--border-cream);border-bottom:0}.memo-editor footer button{min-height:44px}.memo-editor-scrim{display:none}
|
||||||
@media(max-width:930px){.shell.memo-detail-open main{padding:20px 17px 112px}.memo-panel{min-height:calc(100dvh - 150px)}.memo-toolbar{display:block}.memo-scope{min-width:0}.memo-scope button{flex:1;justify-content:center;padding-inline:9px}.memo-row{height:74px;min-height:74px;max-height:76px}.memo-editor-scrim{display:block;position:fixed;z-index:41;inset:0;background:var(--scrim)}.memo-editor{position:fixed;z-index:42;left:0;right:0;top:auto;bottom:0;width:100%;max-width:100%;height:min(92dvh,820px);overflow-x:hidden;border:1px solid var(--border-cream);border-bottom:0;border-radius:22px 22px 0 0;transition:transform .22s ease}.memo-editor__fields{padding:16px}.memo-editor footer{padding-bottom:max(10px,env(safe-area-inset-bottom))}}
|
@media(max-width:930px){.shell.memo-detail-open main{padding:20px 17px 112px}.memo-panel{min-height:calc(100dvh - 150px)}.memo-toolbar{display:block}.memo-scope{min-width:0}.memo-scope button{flex:1;justify-content:center;padding-inline:9px}.memo-row{height:74px;min-height:74px;max-height:76px}.memo-editor-scrim{display:block;position:fixed;z-index:41;inset:0;background:var(--scrim)}.memo-editor{position:fixed;z-index:42;left:0;right:0;top:auto;bottom:0;width:100%;max-width:100%;height:min(92dvh,820px);overflow-x:hidden;border:1px solid var(--border-cream);border-bottom:0;border-radius:22px 22px 0 0;transition:transform .22s ease}.memo-editor__fields{padding:16px}.memo-editor footer{padding-bottom:max(10px,env(safe-area-inset-bottom))}}
|
||||||
@media(prefers-reduced-motion:reduce){.memo-editor,.memo-row,.memo-list{transition:none!important}}
|
@media(prefers-reduced-motion:reduce){.memo-editor,.memo-row,.memo-list{transition:none!important}}
|
||||||
|
|||||||
+127
-114
File diff suppressed because one or more lines are too long
+39
-39
@@ -64,7 +64,7 @@ describe('unified task due display', () => {
|
|||||||
|
|
||||||
it('scopes content metadata and keeps date semantics without a visible calendar icon', () => {
|
it('scopes content metadata and keeps date semantics without a visible calendar icon', () => {
|
||||||
expect(css).toContain('.task-due--overdue{color:var(--danger)}')
|
expect(css).toContain('.task-due--overdue{color:var(--danger)}')
|
||||||
expect(css).toContain('.task-due--neutral,.task-due--completed{color:var(--muted)}')
|
expect(css).toContain('.task-due--neutral,.task-due--completed{color:var(--text-secondary)}')
|
||||||
expect(css).toContain('.task-main{border:0;background:transparent;flex:1;min-width:0;')
|
expect(css).toContain('.task-main{border:0;background:transparent;flex:1;min-width:0;')
|
||||||
expect(css).toContain('.meta-item{display:flex;align-items:center;gap:3px}')
|
expect(css).toContain('.meta-item{display:flex;align-items:center;gap:3px}')
|
||||||
expect(css).not.toContain('.meta span{display:flex;')
|
expect(css).not.toContain('.meta span{display:flex;')
|
||||||
@@ -79,22 +79,22 @@ describe('unified task due display', () => {
|
|||||||
|
|
||||||
describe('approved cream solid button system', () => {
|
describe('approved cream solid button system', () => {
|
||||||
it('gives semantic action buttons a compact desktop size and a touch-safe mobile size', () => {
|
it('gives semantic action buttons a compact desktop size and a touch-safe mobile size', () => {
|
||||||
expect(css).toContain(':is(.primary,.primary-small,.soft-button,.secondary,.restore,.file-button,.danger-button){min-height:40px;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 15px;border-radius:10px;font-size:13px;font-weight:700;line-height:1;white-space:nowrap;transition:transform .14s ease,background-color .14s ease,border-color .14s ease,box-shadow .14s ease,color .14s ease}')
|
expect(css).toContain(':is(.primary,.primary-small,.soft-button,.secondary,.restore,.file-button,.danger-button){min-height:40px;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 15px;border-radius:var(--radius-control);font-size:13px;font-weight:700;line-height:1;white-space:nowrap;transition:transform .14s ease,background-color .14s ease,border-color .14s ease,box-shadow .14s ease,color .14s ease}')
|
||||||
expect(css).toContain(':is(.primary,.primary-small,.soft-button,.secondary,.restore,.file-button,.danger-button)>svg{width:16px;height:16px;flex:0 0 auto}')
|
expect(css).toContain(':is(.primary,.primary-small,.soft-button,.secondary,.restore,.file-button,.danger-button)>svg{width:16px;height:16px;flex:0 0 auto}')
|
||||||
expect(css).toContain('@media(min-width:931px){main:has(>.mvp-view .settings-sections) :is(.soft-button,.primary-small,.danger-button,.file-button),.numeric-action .soft-button,.habit-history__message .soft-button,.backup-preflight .danger-button,.app-sheet__danger :is(.soft-button,.primary-small,.danger-button,.file-button,.secondary,.restore),.purge-list-dialog :is(.secondary,.danger-button){min-height:40px}}')
|
expect(css).toContain('@media(min-width:931px){main:has(>.mvp-view .settings-sections) :is(.soft-button,.primary-small,.danger-button,.file-button),.numeric-action .soft-button,.habit-history__message .soft-button,.backup-preflight .danger-button,.app-sheet__danger :is(.soft-button,.primary-small,.danger-button,.file-button,.secondary,.restore),.purge-list-dialog :is(.secondary,.danger-button){min-height:40px}}')
|
||||||
expect(css).toContain('@media(max-width:930px){:is(.primary,.primary-small,.soft-button,.secondary,.restore,.file-button,.danger-button){min-height:44px;padding-inline:16px;gap:7px}:is(.primary,.primary-small,.soft-button,.secondary,.restore,.file-button,.danger-button)>svg{width:17px;height:17px}}')
|
expect(css).toContain('@media(max-width:930px){:is(.primary,.primary-small,.soft-button,.secondary,.restore,.file-button,.danger-button){min-height:44px;padding-inline:16px;gap:7px}:is(.primary,.primary-small,.soft-button,.secondary,.restore,.file-button,.danger-button)>svg{width:17px;height:17px}}')
|
||||||
expect(css).toContain(':is(.primary,.primary-small){border:1px solid var(--accent);background:var(--accent);color:#fff;box-shadow:inset 0 1px 0 rgba(255,255,255,.36),0 4px 10px rgba(241,90,41,.20)}')
|
expect(css).toContain(':is(.primary,.primary-small){border:1px solid var(--accent);background:var(--accent);color:var(--white);box-shadow:inset 0 1px 0 rgba(255,255,255,.36),0 4px 10px rgba(241,90,41,.20)}')
|
||||||
expect(css).toContain(':is(.soft-button,.secondary,.restore,.file-button){border:1px solid #ddd0c0;background:#fffaf3;color:#51483f;box-shadow:inset 0 1px 0 #fff,0 2px 5px rgba(83,62,42,.06)}')
|
expect(css).toContain(':is(.soft-button,.secondary,.restore,.file-button){border:1px solid var(--soft-button-border-2);background:var(--row-hover);color:var(--soft-button-ink);box-shadow:inset 0 1px 0 var(--white),0 2px 5px rgba(83,62,42,.06)}')
|
||||||
expect(css).toContain('.danger-button{border:1px solid #e2b7ac;background:#fff8f5;color:#b83c2c;box-shadow:inset 0 1px 0 #fff,0 2px 5px rgba(105,48,34,.05)}')
|
expect(css).toContain('.danger-button{border:1px solid var(--danger-button-border-3);background:var(--danger-btn-bg);color:var(--danger-button-ink);box-shadow:inset 0 1px 0 var(--white),0 2px 5px rgba(105,48,34,.05)}')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('provides restrained interaction, focus, and disabled states without resizing buttons', () => {
|
it('provides restrained interaction, focus, and disabled states without resizing buttons', () => {
|
||||||
expect(css).toContain(':is(.primary,.primary-small):hover:not(:disabled){background:#dd4b1e;border-color:#dd4b1e}')
|
expect(css).toContain(':is(.primary,.primary-small):hover:not(:disabled){background:var(--accent-press);border-color:var(--accent-press)}')
|
||||||
expect(css).toContain(':is(.soft-button,.secondary,.restore,.file-button):hover:not(:disabled):not(.disabled){background:#f8eddf;border-color:#d5c5b2}')
|
expect(css).toContain(':is(.soft-button,.secondary,.restore,.file-button):hover:not(:disabled):not(.disabled){background:var(--soft-button-bg);border-color:var(--soft-button-border)}')
|
||||||
expect(css).toContain(':is(.primary,.primary-small,.soft-button,.secondary,.restore,.file-button,.danger-button):active:not(:disabled):not(.disabled){transform:translateY(1px) scale(.985)}')
|
expect(css).toContain(':is(.primary,.primary-small,.soft-button,.secondary,.restore,.file-button,.danger-button):active:not(:disabled):not(.disabled){transform:translateY(1px) scale(.985)}')
|
||||||
expect(css).toContain(':is(.primary,.primary-small,.soft-button,.secondary,.restore,.file-button,.danger-button):focus-visible{outline:3px solid rgba(241,90,41,.24);outline-offset:3px}')
|
expect(css).toContain(':is(.primary,.primary-small,.soft-button,.secondary,.restore,.file-button,.danger-button):focus-visible{outline:3px solid rgba(241,90,41,.24);outline-offset:3px}')
|
||||||
expect(css).toContain(':is(.primary,.primary-small,.soft-button,.secondary,.restore,.file-button,.danger-button):disabled,.file-button.disabled{background:#ebe2d8;border-color:#ded3c7;color:#aaa095;box-shadow:none;cursor:not-allowed;opacity:1}')
|
expect(css).toContain(':is(.primary,.primary-small,.soft-button,.secondary,.restore,.file-button,.danger-button):disabled,.file-button.disabled{background:var(--disabled-bg);border-color:var(--disabled-border);color:var(--disabled-ink);box-shadow:none;cursor:not-allowed;opacity:1}')
|
||||||
expect(css).toContain('main:has(>.mvp-view .settings-sections) :is(.settings-data>.settings-row:first-of-type .soft-button:disabled,.backup-preflight .danger-button:disabled,.password-form .primary-small:disabled){background:#ebe2d8;border-color:#ded3c7;color:#aaa095;box-shadow:none;opacity:1}')
|
expect(css).toContain('main:has(>.mvp-view .settings-sections) :is(.settings-data>.settings-row:first-of-type .soft-button:disabled,.backup-preflight .danger-button:disabled,.password-form .primary-small:disabled){background:var(--disabled-bg);border-color:var(--disabled-border);color:var(--disabled-ink);box-shadow:none;opacity:1}')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses a real disabled file-picker button and keeps its input out of layout', () => {
|
it('uses a real disabled file-picker button and keeps its input out of layout', () => {
|
||||||
@@ -170,12 +170,12 @@ describe('approved Settings 01 paper ledger', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('uses the selected continuous-paper geometry on desktop and mobile', () => {
|
it('uses the selected continuous-paper geometry on desktop and mobile', () => {
|
||||||
expect(css).toContain('main:has(>.mvp-view .settings-sections){background:#fffdf8}')
|
expect(css).toContain('main:has(>.mvp-view .settings-sections){background:var(--surface-raised)}')
|
||||||
expect(css).toContain('.settings-sections{width:min(100%,900px);margin:0 auto;display:grid;gap:0;padding:34px 0 64px}')
|
expect(css).toContain('.settings-sections{width:min(100%,900px);margin:0 auto;display:grid;gap:0;padding:34px 0 64px}')
|
||||||
expect(css).toContain('.settings-heading h1{font-size:34px;line-height:1.1;font-weight:700;')
|
expect(css).toContain('.settings-heading h1{font-size:34px;line-height:1.1;font-weight:700;')
|
||||||
expect(css).toContain('.settings-group{min-width:0;background:transparent;border:0;border-radius:0;box-shadow:none;overflow:visible;padding-top:25px}')
|
expect(css).toContain('.settings-group{min-width:0;background:transparent;border:0;border-radius:0;box-shadow:none;overflow:visible;padding-top:25px}')
|
||||||
expect(css).toContain('.settings-group>header{height:44px;padding:0;display:flex;align-items:center;border-bottom:1px solid #e8e0d5}')
|
expect(css).toContain('.settings-group>header{height:44px;padding:0;display:flex;align-items:center;border-bottom:1px solid var(--border-hairline)}')
|
||||||
expect(css).toContain('.settings-row{min-height:62px;padding:6px 0;border-top:0;border-bottom:1px solid #e8e0d5;')
|
expect(css).toContain('.settings-row{min-height:62px;padding:6px 0;border-top:0;border-bottom:1px solid var(--border-hairline);')
|
||||||
expect(css).toContain('.settings-topbar{margin-bottom:0}')
|
expect(css).toContain('.settings-topbar{margin-bottom:0}')
|
||||||
expect(css).not.toContain('.topbar>.settings-refresh{grid-area:filter}')
|
expect(css).not.toContain('.topbar>.settings-refresh{grid-area:filter}')
|
||||||
expect(css).toContain('@media(max-width:720px){main:has(>.mvp-view .settings-sections){padding-left:29px;padding-right:29px;padding-bottom:calc(102px + env(safe-area-inset-bottom))}')
|
expect(css).toContain('@media(max-width:720px){main:has(>.mvp-view .settings-sections){padding-left:29px;padding-right:29px;padding-bottom:calc(102px + env(safe-area-inset-bottom))}')
|
||||||
@@ -185,8 +185,8 @@ describe('approved Settings 01 paper ledger', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('keeps controls and password errors aligned with the approved form contract', () => {
|
it('keeps controls and password errors aligned with the approved form contract', () => {
|
||||||
expect(css).toContain('main:has(>.mvp-view .settings-sections) :is(.soft-button,.primary-small,.danger-button,.file-button){border-radius:10px}')
|
expect(css).toContain('main:has(>.mvp-view .settings-sections) :is(.soft-button,.primary-small,.danger-button,.file-button){border-radius:var(--radius-control)}')
|
||||||
expect(css).toContain('main:has(>.mvp-view .settings-sections) :is(.settings-data>.settings-row:first-of-type .soft-button,.backup-preflight .danger-button,.password-form .primary-small){background:#f15a29;border-color:#f15a29;color:#fff;box-shadow:inset 0 1px 0 rgba(255,255,255,.36),0 4px 10px rgba(241,90,41,.20)}')
|
expect(css).toContain('main:has(>.mvp-view .settings-sections) :is(.settings-data>.settings-row:first-of-type .soft-button,.backup-preflight .danger-button,.password-form .primary-small){background:var(--accent);border-color:var(--accent);color:var(--white);box-shadow:inset 0 1px 0 rgba(255,255,255,.36),0 4px 10px rgba(241,90,41,.20)}')
|
||||||
expect(css).toContain('main:has(>.mvp-view .settings-sections) .danger-text{min-height:44px;border:0;background:transparent;color:var(--danger)}')
|
expect(css).toContain('main:has(>.mvp-view .settings-sections) .danger-text{min-height:44px;border:0;background:transparent;color:var(--danger)}')
|
||||||
expect(css).toContain('.password-form.settings-form{width:100%;display:grid;grid-template-columns:repeat(3,minmax(0,1fr)) auto;gap:10px}')
|
expect(css).toContain('.password-form.settings-form{width:100%;display:grid;grid-template-columns:repeat(3,minmax(0,1fr)) auto;gap:10px}')
|
||||||
expect(css).toContain('.password-form.settings-form .inline-error{grid-column:1/-1;margin:0}')
|
expect(css).toContain('.password-form.settings-form .inline-error{grid-column:1/-1;margin:0}')
|
||||||
@@ -265,14 +265,14 @@ describe('solid cream material system', () => {
|
|||||||
expect(css).toContain('--text-secondary:#655b50')
|
expect(css).toContain('--text-secondary:#655b50')
|
||||||
expect(css).toContain('--border-cream:#e4d5c0')
|
expect(css).toContain('--border-cream:#e4d5c0')
|
||||||
expect(css).toContain('--accent:#f15a29')
|
expect(css).toContain('--accent:#f15a29')
|
||||||
expect(css).toContain('--success:#687e61')
|
expect(css).toContain('--success:#5f705a')
|
||||||
expect(css).toContain('--radius-list:15px')
|
expect(css).toContain('--radius-list:15px')
|
||||||
expect(css).toContain('font-family:-apple-system,BlinkMacSystemFont,"Avenir Next","PingFang SC"')
|
expect(css).toContain('font-family:-apple-system,BlinkMacSystemFont,"Avenir Next","PingFang SC"')
|
||||||
expect(css).toContain('font-variant-numeric:tabular-nums')
|
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::-webkit-progress-value{background:var(--success)')
|
||||||
expect(css).toContain('.habit-progress-bar::-moz-progress-bar{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).not.toContain('linear-gradient(90deg,#f7a066,var(--accent))')
|
||||||
expect(css).toContain('--highlight-inner:inset 0 1px 0 #fff')
|
expect(css).toContain('--highlight-inner:inset 0 1px 0 var(--white)')
|
||||||
expect(css).toContain('--radius-control:11px')
|
expect(css).toContain('--radius-control:11px')
|
||||||
expect(css).toContain('--radius-card:14px')
|
expect(css).toContain('--radius-card:14px')
|
||||||
expect(css).toContain('--radius-panel:20px')
|
expect(css).toContain('--radius-panel:20px')
|
||||||
@@ -287,13 +287,13 @@ describe('solid cream material system', () => {
|
|||||||
|
|
||||||
it('uses opaque cream surfaces for the shell, navigation, overlays, and feedback', () => {
|
it('uses opaque cream surfaces for the shell, navigation, overlays, and feedback', () => {
|
||||||
expect(css).toContain('.shell{background:var(--surface-base)}')
|
expect(css).toContain('.shell{background:var(--surface-base)}')
|
||||||
expect(css).toContain('.sidebar{background:#f7efe3')
|
expect(css).toContain('.sidebar{background:var(--surface-canvas)')
|
||||||
expect(css).toContain('main{background:var(--surface-base)}')
|
expect(css).toContain('main{background:var(--surface-base)}')
|
||||||
expect(css).toContain('.detail,.bottom{background:var(--surface-raised)')
|
expect(css).toContain('.detail,.bottom{background:var(--surface-raised)')
|
||||||
expect(css).toContain('.app-sheet,.calendar-picker,.sidebar-popover,.archived-row-actions{background:var(--surface-raised)')
|
expect(css).toContain('.app-sheet,.calendar-picker,.sidebar-popover,.archived-row-actions{background:var(--surface-raised)')
|
||||||
expect(css).toContain('.toast{background:#3b342c')
|
expect(css).toContain('.toast{background:var(--surface-inverse)')
|
||||||
expect(css).toContain('.error-toast{background:var(--danger)')
|
expect(css).toContain('.error-toast{background:var(--danger)')
|
||||||
expect(css).toContain('.app-sheet-mask.app-sheet-mask{background:var(--sheet-scrim)')
|
expect(css).toContain('.app-sheet-mask.app-sheet-mask{background:var(--scrim)')
|
||||||
expect(css).toContain('--scrim:rgba(45,38,31,.38)')
|
expect(css).toContain('--scrim:rgba(45,38,31,.38)')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -392,13 +392,13 @@ describe('approved UI detail direction', () => {
|
|||||||
it('matches the approved paper-flow geometry and material', () => {
|
it('matches the approved paper-flow geometry and material', () => {
|
||||||
expect(css).toContain('.shell.detail-open{grid-template-columns:var(--sidebar-width,236px) minmax(330px,1fr) var(--detail-width,350px)}')
|
expect(css).toContain('.shell.detail-open{grid-template-columns:var(--sidebar-width,236px) minmax(330px,1fr) var(--detail-width,350px)}')
|
||||||
expect(css).toContain('@media(max-width:1050px) and (min-width:931px){main{padding-inline:24px}')
|
expect(css).toContain('@media(max-width:1050px) and (min-width:931px){main{padding-inline:24px}')
|
||||||
expect(css).toContain('.detail{min-width:0;border-left:1px solid var(--line);background:#fffdf8;overflow:hidden;display:flex;flex-direction:column}')
|
expect(css).toContain('.detail{min-width:0;border-left:1px solid var(--border-cream);background:var(--surface-raised);overflow:hidden;display:flex;flex-direction:column}')
|
||||||
expect(css).toContain('.detail-head{height:58px;flex:0 0 58px;display:flex;align-items:center;justify-content:space-between;padding:0 18px;')
|
expect(css).toContain('.detail-head{height:58px;flex:0 0 58px;display:flex;align-items:center;justify-content:space-between;padding:0 18px;')
|
||||||
expect(css).toContain('.detail-form{min-width:0;border:0;margin:0;grid-template-columns:minmax(0,1fr);padding:18px;display:grid;gap:18px;overflow-y:auto;')
|
expect(css).toContain('.detail-form{min-width:0;border:0;margin:0;grid-template-columns:minmax(0,1fr);padding:18px;display:grid;gap:18px;overflow-y:auto;')
|
||||||
expect(css).toContain('.detail-title textarea{min-width:0;min-height:62px;padding:0;border:0;background:transparent;box-shadow:none;resize:none;outline:none;font-size:20px;line-height:27px;')
|
expect(css).toContain('.detail-title textarea{min-width:0;min-height:62px;padding:0;border:0;background:transparent;box-shadow:none;resize:none;outline:none;font-size:20px;line-height:27px;')
|
||||||
expect(css).toContain('.task-detail-arrangement{display:grid;gap:13px;padding-bottom:18px;border-bottom:1px solid #e4dbcf}')
|
expect(css).toContain('.task-detail-arrangement{display:grid;gap:13px;padding-bottom:18px;border-bottom:1px solid var(--border-section)}')
|
||||||
expect(css).toContain('.task-detail-date-time{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:10px}')
|
expect(css).toContain('.task-detail-date-time{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:10px}')
|
||||||
expect(css).toContain('.task-detail-field-input{width:100%!important;min-height:44px;border-radius:10px}')
|
expect(css).toContain('.task-detail-field-input{width:100%!important;min-height:44px;border-radius:var(--radius-control)}')
|
||||||
expect(css).toContain('.detail-actions{position:sticky;bottom:0;z-index:3;min-height:69px;')
|
expect(css).toContain('.detail-actions{position:sticky;bottom:0;z-index:3;min-height:69px;')
|
||||||
expect(css).toContain('.detail-save{min-width:88px}')
|
expect(css).toContain('.detail-save{min-width:88px}')
|
||||||
expect(css).toContain('.subtask-detail,.after-completion-fields,.repeat-custom-fields{width:100%;max-width:100%;min-width:0;border:0;border-radius:0;box-shadow:none;background:transparent;overflow:visible}')
|
expect(css).toContain('.subtask-detail,.after-completion-fields,.repeat-custom-fields{width:100%;max-width:100%;min-width:0;border:0;border-radius:0;box-shadow:none;background:transparent;overflow:visible}')
|
||||||
@@ -484,7 +484,7 @@ describe('completion feedback motion', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('stacks the desktop calendar copy to align with weather and gold', () => {
|
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:2px;font-size:10.5px}}')
|
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:2px;font-size:11px}}')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses the approved internal-standard login composition and material tokens', () => {
|
it('uses the approved internal-standard login composition and material tokens', () => {
|
||||||
@@ -658,8 +658,8 @@ describe('mobile sheet contract', () => {
|
|||||||
expect(mvpPanel).toContain('panel-class="habit-detail-sheet"')
|
expect(mvpPanel).toContain('panel-class="habit-detail-sheet"')
|
||||||
expect(countdownPanel).toContain('panel-class="countdown-detail-sheet"')
|
expect(countdownPanel).toContain('panel-class="countdown-detail-sheet"')
|
||||||
expect(countdownPanel).toContain('panel-class="countdown-modal"')
|
expect(countdownPanel).toContain('panel-class="countdown-modal"')
|
||||||
expect(css).toContain('--sheet-radius:20px;--sheet-scrim:rgba(45,38,31,.4)')
|
expect(css).toContain('--scrim:rgba(45,38,31,.38);--radius-control:11px')
|
||||||
expect(css).toContain('.app-sheet-mask.app-sheet-mask{background:var(--sheet-scrim)')
|
expect(css).toContain('.app-sheet-mask.app-sheet-mask{background:var(--scrim)')
|
||||||
expect(css).toContain('.app-sheet__header{min-height:64px;')
|
expect(css).toContain('.app-sheet__header{min-height:64px;')
|
||||||
expect(css).toContain('.app-sheet__body{min-height:0;overflow-y:auto;')
|
expect(css).toContain('.app-sheet__body{min-height:0;overflow-y:auto;')
|
||||||
expect(css).toContain('.app-sheet__footer{position:sticky;bottom:0;')
|
expect(css).toContain('.app-sheet__footer{position:sticky;bottom:0;')
|
||||||
@@ -669,14 +669,14 @@ describe('mobile sheet contract', () => {
|
|||||||
it('keeps the danger zone reserved for archived habit deletion', () => {
|
it('keeps the danger zone reserved for archived habit deletion', () => {
|
||||||
expect(mvpPanel).toContain('v-if="selectedHabit.archived_at" class="app-sheet__danger habit-detail-archived-actions"')
|
expect(mvpPanel).toContain('v-if="selectedHabit.archived_at" class="app-sheet__danger habit-detail-archived-actions"')
|
||||||
expect(mvpPanel).toContain('deleteHabit(selectedHabit)')
|
expect(mvpPanel).toContain('deleteHabit(selectedHabit)')
|
||||||
expect(css).toContain('.app-sheet__danger{border-top:1px solid #f1d4cd;')
|
expect(css).toContain('.app-sheet__danger{border-top:1px solid var(--app-sheet-danger-border);')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('mobile list row language', () => {
|
describe('mobile list row language', () => {
|
||||||
it('uses one quiet bordered row surface for tasks, habits, and countdowns on mobile', () => {
|
it('uses one quiet bordered row surface for tasks, habits, and countdowns on mobile', () => {
|
||||||
expect(css).toContain('@media(max-width:930px){.task-row,.habit-row,.countdown-row{')
|
expect(css).toContain('@media(max-width:930px){.task-row,.habit-row,.countdown-row{')
|
||||||
expect(css).toMatch(/@media\(max-width:930px\)\{\.task-row,\.habit-row,\.countdown-row\{[^}]*background:#fff;[^}]*border:1px solid var\(--line\);[^}]*border-radius:13px;[^}]*box-shadow:none/)
|
expect(css).toMatch(/@media\(max-width:930px\)\{\.task-row,\.habit-row,\.countdown-row\{[^}]*background:var\(--white\);[^}]*border:1px solid var\(--border-cream\);[^}]*border-radius:13px;[^}]*box-shadow:none/)
|
||||||
expect(css).toContain('.task-list,.habit-list,.countdown-timeline{display:grid;gap:8px}')
|
expect(css).toContain('.task-list,.habit-list,.countdown-timeline{display:grid;gap:8px}')
|
||||||
expect(css).toContain('.countdown-row{grid-template-columns:minmax(0,1fr) 72px;')
|
expect(css).toContain('.countdown-row{grid-template-columns:minmax(0,1fr) 72px;')
|
||||||
expect(css).not.toContain('grid-template-columns:44px minmax(0,1fr) 72px')
|
expect(css).not.toContain('grid-template-columns:44px minmax(0,1fr) 72px')
|
||||||
@@ -1053,7 +1053,7 @@ describe('task and habit row decoration', () => {
|
|||||||
const desktopMain = css.slice(css.indexOf('main{container-type:inline-size;'), css.indexOf('.topbar{'))
|
const desktopMain = css.slice(css.indexOf('main{container-type:inline-size;'), css.indexOf('.topbar{'))
|
||||||
expect(desktopMain).toContain('overflow:auto')
|
expect(desktopMain).toContain('overflow:auto')
|
||||||
expect(css).toContain('@media(min-width:931px){main{scrollbar-width:none}main::-webkit-scrollbar{display:none}}')
|
expect(css).toContain('@media(min-width:931px){main{scrollbar-width:none}main::-webkit-scrollbar{display:none}}')
|
||||||
expect(css).toContain('.detail{min-width:0;border-left:1px solid var(--line);background:#faf7f0;overflow:auto}')
|
expect(css).toContain('.detail{min-width:0;border-left:1px solid var(--border-cream);background:var(--detail-bg);overflow:auto}')
|
||||||
expect(css).not.toContain('@media(max-width:930px){main{scrollbar-width:none}')
|
expect(css).not.toContain('@media(max-width:930px){main{scrollbar-width:none}')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1202,8 +1202,8 @@ describe('task and habit row decoration', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('uses a gentle completion treatment instead of a discarded-task strike', () => {
|
it('uses a gentle completion treatment instead of a discarded-task strike', () => {
|
||||||
expect(css).toContain('.habit-row.done .habit-name{color:#756c61;')
|
expect(css).toContain('.habit-row.done .habit-name{color:var(--text-secondary);')
|
||||||
expect(css).toContain('text-decoration-color:#d8c8bb')
|
expect(css).toContain('text-decoration-color:var(--habit-row-strike)')
|
||||||
expect(css).toContain('text-decoration-thickness:1px')
|
expect(css).toContain('text-decoration-thickness:1px')
|
||||||
expect(css).not.toContain('.habit-row.done .habit-name{color:var(--accent);text-decoration:line-through')
|
expect(css).not.toContain('.habit-row.done .habit-name{color:var(--accent);text-decoration:line-through')
|
||||||
})
|
})
|
||||||
@@ -1217,7 +1217,7 @@ describe('task and habit row decoration', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('uses a continuous bottom divider instead of card borders', () => {
|
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).toMatch(/\.plain-list-row,\.plain-list>\.task-row,\.plain-list>\.habit-row\{[^}]*border:0;[^}]*border-bottom:1px solid var\(--border-hairline\)/)
|
||||||
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-top:/)
|
||||||
expect(css).not.toMatch(/\.plain-list-row,\.plain-list>\.task-row,\.plain-list>\.habit-row\{[^}]*border-right:/)
|
expect(css).not.toMatch(/\.plain-list-row,\.plain-list>\.task-row,\.plain-list>\.habit-row\{[^}]*border-right:/)
|
||||||
})
|
})
|
||||||
@@ -1256,7 +1256,7 @@ describe('approved habit safety and U2 title hierarchy', () => {
|
|||||||
expect(css).toContain('.settings-sections{width:min(100%,900px);')
|
expect(css).toContain('.settings-sections{width:min(100%,900px);')
|
||||||
expect(css).toContain('.settings-row{min-height:62px;')
|
expect(css).toContain('.settings-row{min-height:62px;')
|
||||||
expect(css).toContain('.backup-preflight .danger-button{min-height:44px}')
|
expect(css).toContain('.backup-preflight .danger-button{min-height:44px}')
|
||||||
expect(css).toContain('.backup-preflight.invalid{background:#fff2ef;')
|
expect(css).toContain('.backup-preflight.invalid{background:var(--error-wash);')
|
||||||
expect(mvpPanel).toContain("v-if=\"restoreMode==='replace'\" class=\"restore-replace-warning\"")
|
expect(mvpPanel).toContain("v-if=\"restoreMode==='replace'\" class=\"restore-replace-warning\"")
|
||||||
expect(mvpPanel).toContain('替换恢复会覆盖当前数据,请先导出完整备份。')
|
expect(mvpPanel).toContain('替换恢复会覆盖当前数据,请先导出完整备份。')
|
||||||
})
|
})
|
||||||
@@ -1344,7 +1344,7 @@ describe('habit detail paper-flow redesign', () => {
|
|||||||
|
|
||||||
it('matches the paper-flow surface and action hierarchy at desktop and touch widths', () => {
|
it('matches the paper-flow surface and action hierarchy at desktop and touch widths', () => {
|
||||||
expect(css).toContain('/* Approved habit detail 01: paper flow. */')
|
expect(css).toContain('/* Approved habit detail 01: paper flow. */')
|
||||||
expect(css).toMatch(/\.habit-detail-hero\{[^}]*padding:[^;}]*;[^}]*border-bottom:1px solid #e4dbcf/)
|
expect(css).toMatch(/\.habit-detail-hero\{[^}]*padding:[^;}]*;[^}]*border-bottom:1px solid var\(--border-section\)/)
|
||||||
expect(css).toMatch(/\.habit-detail-hero strong\{[^}]*font-size:28px/)
|
expect(css).toMatch(/\.habit-detail-hero strong\{[^}]*font-size:28px/)
|
||||||
expect(css).toMatch(/\.habit-detail-progress-row\{[^}]*min-height:66px;[^}]*grid-template-columns:minmax\(0,1fr\) auto/)
|
expect(css).toMatch(/\.habit-detail-progress-row\{[^}]*min-height:66px;[^}]*grid-template-columns:minmax\(0,1fr\) auto/)
|
||||||
expect(css).toMatch(/\.habit-detail-meta-row\{[^}]*min-height:52px;[^}]*grid-template-columns:86px minmax\(0,1fr\)/)
|
expect(css).toMatch(/\.habit-detail-meta-row\{[^}]*min-height:52px;[^}]*grid-template-columns:86px minmax\(0,1fr\)/)
|
||||||
@@ -1367,7 +1367,7 @@ describe('task detail layout', () => {
|
|||||||
expect(app).toContain('<div class="task-detail-date-time">')
|
expect(app).toContain('<div class="task-detail-date-time">')
|
||||||
expect(app).toContain('v-model="selectedDueDate" class="task-detail-due-input task-detail-field-input"')
|
expect(app).toContain('v-model="selectedDueDate" class="task-detail-due-input task-detail-field-input"')
|
||||||
expect(app.match(/class="task-detail-field-input"/g)).toHaveLength(2)
|
expect(app.match(/class="task-detail-field-input"/g)).toHaveLength(2)
|
||||||
expect(css).toContain('.task-detail-field-input{width:100%!important;min-height:44px;border-radius:10px}')
|
expect(css).toContain('.task-detail-field-input{width:100%!important;min-height:44px;border-radius:var(--radius-control)}')
|
||||||
expect(css).toContain('.task-detail-due-input{padding-inline:9px!important}')
|
expect(css).toContain('.task-detail-due-input{padding-inline:9px!important}')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1485,7 +1485,7 @@ describe('unified floating add interaction', () => {
|
|||||||
expect(floatingAdd).toContain('<Plus />')
|
expect(floatingAdd).toContain('<Plus />')
|
||||||
expect(floatingAdd).not.toContain('fab-cat')
|
expect(floatingAdd).not.toContain('fab-cat')
|
||||||
expect(floatingAdd).not.toContain('pupilOffset')
|
expect(floatingAdd).not.toContain('pupilOffset')
|
||||||
expect(css).toContain('.unified-fab{display:grid;place-items:center;position:fixed;z-index:60;right:20px;bottom:22px;width:56px;height:56px;padding:0;border:0;border-radius:50%;background:var(--accent);color:#fff;')
|
expect(css).toContain('.unified-fab{display:grid;place-items:center;position:fixed;z-index:60;right:20px;bottom:22px;width:56px;height:56px;padding:0;border:0;border-radius:50%;background:var(--accent);color:var(--white);')
|
||||||
expect(css).toContain('.unified-fab>svg{width:25px;height:25px}')
|
expect(css).toContain('.unified-fab>svg{width:25px;height:25px}')
|
||||||
expect(css).not.toContain('.fab-cat__')
|
expect(css).not.toContain('.fab-cat__')
|
||||||
})
|
})
|
||||||
@@ -1628,7 +1628,7 @@ describe('quiet index sidebar parity', () => {
|
|||||||
it('matches the approved desktop and mobile sidebar geometry and material', () => {
|
it('matches the approved desktop and mobile sidebar geometry and material', () => {
|
||||||
expect(css).toContain('.shell{height:100vh;display:grid;grid-template-columns:var(--sidebar-width,236px) minmax(330px,1fr) 0;')
|
expect(css).toContain('.shell{height:100vh;display:grid;grid-template-columns:var(--sidebar-width,236px) minmax(330px,1fr) 0;')
|
||||||
expect(css).toContain('.shell.detail-open{grid-template-columns:var(--sidebar-width,236px) minmax(330px,1fr) var(--detail-width,350px)}')
|
expect(css).toContain('.shell.detail-open{grid-template-columns:var(--sidebar-width,236px) minmax(330px,1fr) var(--detail-width,350px)}')
|
||||||
expect(css).toContain('.sidebar{border-right:1px solid #e4d5c3;background:#f7efe3;')
|
expect(css).toContain('.sidebar{border-right:1px solid var(--border-cream);background:var(--surface-canvas);')
|
||||||
expect(css).toContain('.brand-row{height:68px;')
|
expect(css).toContain('.brand-row{height:68px;')
|
||||||
expect(css).toContain('border-bottom:1px solid rgba(222,205,185,.75)')
|
expect(css).toContain('border-bottom:1px solid rgba(222,205,185,.75)')
|
||||||
expect(css).toContain('.primary-nav{display:grid;padding:10px 11px 8px;gap:2px}')
|
expect(css).toContain('.primary-nav{display:grid;padding:10px 11px 8px;gap:2px}')
|
||||||
@@ -1639,13 +1639,13 @@ describe('quiet index sidebar parity', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('matches the approved quiet navigation and list hierarchy', () => {
|
it('matches the approved quiet navigation and list hierarchy', () => {
|
||||||
expect(css).toContain('.primary-nav button.active{background:#f9e2d5;color:#b63c1b;')
|
expect(css).toContain('.primary-nav button.active{background:var(--nav-active-bg);color:var(--accent-ink);')
|
||||||
expect(css).toContain('.primary-nav button.active::before,.list-row.active::before,.sidebar-management button.active::before{')
|
expect(css).toContain('.primary-nav button.active::before,.list-row.active::before,.sidebar-management button.active::before{')
|
||||||
expect(css).toContain('width:3px;height:18px;')
|
expect(css).toContain('width:3px;height:18px;')
|
||||||
expect(css).toContain('.section-title{padding:14px 15px 5px 20px;')
|
expect(css).toContain('.section-title{padding:14px 15px 5px 20px;')
|
||||||
expect(css).toContain('.folders{flex:1;min-height:0;padding:0 10px;')
|
expect(css).toContain('.folders{flex:1;min-height:0;padding:0 10px;')
|
||||||
expect(css).toContain('.folder-row>button,.list-row-main{flex:1;min-width:0;min-height:44px;border:0;background:transparent;padding:0 8px;')
|
expect(css).toContain('.folder-row>button,.list-row-main{flex:1;min-width:0;min-height:44px;border:0;background:transparent;padding:0 8px;')
|
||||||
expect(css).toContain('.list-row.active{background:#fff8ef;color:#bc431f;box-shadow:inset 0 0 0 1px #ead5c4}')
|
expect(css).toContain('.list-row.active{background:var(--list-row-bg);color:var(--accent-ink);box-shadow:inset 0 0 0 1px var(--list-row-shadow)}')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('removes decorative markers from both ordinary list row renderers', () => {
|
it('removes decorative markers from both ordinary list row renderers', () => {
|
||||||
@@ -1818,7 +1818,7 @@ describe('original circular floating add button', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('uses the original orange circle visuals and restrained motion', () => {
|
it('uses the original orange circle visuals and restrained motion', () => {
|
||||||
expect(css).toContain('.unified-fab{display:grid;place-items:center;position:fixed;z-index:60;right:20px;bottom:22px;width:56px;height:56px;padding:0;border:0;border-radius:50%;background:var(--accent);color:#fff;')
|
expect(css).toContain('.unified-fab{display:grid;place-items:center;position:fixed;z-index:60;right:20px;bottom:22px;width:56px;height:56px;padding:0;border:0;border-radius:50%;background:var(--accent);color:var(--white);')
|
||||||
expect(css).toContain('.unified-fab.dragging{transform:scale(1.06);')
|
expect(css).toContain('.unified-fab.dragging{transform:scale(1.06);')
|
||||||
expect(css).toContain('.unified-fab.snapping{box-shadow:0 10px 24px rgba(241,90,41,.32)}')
|
expect(css).toContain('.unified-fab.snapping{box-shadow:0 10px 24px rgba(241,90,41,.32)}')
|
||||||
expect(css).toContain('.unified-fab:focus-visible{outline:3px solid var(--focus-ring);outline-offset:3px}')
|
expect(css).toContain('.unified-fab:focus-visible{outline:3px solid var(--focus-ring);outline-offset:3px}')
|
||||||
|
|||||||
@@ -1 +1,17 @@
|
|||||||
.today-section-toggle{width:100%;min-height:44px;display:flex;align-items:center;padding:0;border:0;border-bottom:1px solid #e8e0d5;background:transparent;color:var(--text-primary);text-align:left;border-radius:0}.today-section-toggle:hover{background:transparent}.today-section-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.today-section-title{display:flex;align-items:center;font-weight:700;font-size:13px;letter-spacing:.02em}.today-section-summary{margin-left:auto;color:var(--muted);font-size:12px;font-weight:400}.today-section-chevron{width:14px;margin-left:3px;color:var(--muted);font-size:13px;line-height:1;text-align:right}.today-collapsible-section,.today-habits-section{min-width:0}.today-habits-section.today-section-anchor{scroll-margin-top:18px}
|
.today-section-toggle{width:100%;min-height:44px;display:flex;align-items:center;padding:0;border:0;border-bottom:1px solid var(--border-hairline);background:transparent;color:var(--text-primary);text-align:left;border-radius:0}.today-section-toggle:hover{background:transparent}.today-section-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.today-section-title{display:flex;align-items:center;font-weight:700;font-size:13px;letter-spacing:.02em}.today-section-summary{margin-left:auto;color:var(--text-secondary);font-size:12px;font-weight:400}.today-section-chevron{width:14px;margin-left:3px;color:var(--text-secondary);font-size:13px;line-height:1;text-align:right}.today-collapsible-section,.today-habits-section{min-width:0}.today-habits-section.today-section-anchor{scroll-margin-top:18px}
|
||||||
|
|
||||||
|
|
||||||
|
/* Restrained motion: paper unfold/put-away for the three Today sections (grid-rows 0fr<->1fr 180ms). */
|
||||||
|
#today-tasks{grid-template-rows:1fr;transition:grid-template-rows 180ms ease-out}
|
||||||
|
#today-tasks.is-collapsed{grid-template-rows:0fr}
|
||||||
|
#today-tasks>.today-collapse-inner{min-height:0;overflow:hidden}
|
||||||
|
.overdue-section{display:grid;grid-template-rows:auto 1fr;transition:grid-template-rows 180ms ease-out}
|
||||||
|
.overdue-section.is-collapsed{grid-template-rows:auto 0fr}
|
||||||
|
#today-overdue{min-height:0;overflow:hidden}
|
||||||
|
.overdue-section.is-collapsed #today-overdue{margin-bottom:0}
|
||||||
|
#today-habits{display:grid;grid-template-rows:1fr;transition:grid-template-rows 180ms ease-out}
|
||||||
|
#today-habits.is-collapsed{grid-template-rows:0fr}
|
||||||
|
#today-habits>*{min-height:0;overflow:hidden}
|
||||||
|
#today-tasks.is-collapsed>.today-collapse-inner,.overdue-section.is-collapsed>#today-overdue,#today-habits.is-collapsed>*{visibility:hidden;transition:visibility 0s linear 180ms}
|
||||||
|
.today-section-chevron{transition:transform 160ms ease-out}
|
||||||
|
.today-section-chevron.open{transform:rotate(90deg)}
|
||||||
|
|||||||
@@ -93,9 +93,9 @@ describe('approved five-detail polish', () => {
|
|||||||
expect(app).toContain('<span class="pager-status" aria-live="polite"><strong>{{page}} / {{totalPages}}</strong><span>共 {{ totalTasks }} 项</span></span>')
|
expect(app).toContain('<span class="pager-status" aria-live="polite"><strong>{{page}} / {{totalPages}}</strong><span>共 {{ totalTasks }} 项</span></span>')
|
||||||
expect(app).toContain('class="pager-button pager-button--next"')
|
expect(app).toContain('class="pager-button pager-button--next"')
|
||||||
expect(app).toContain('<span>下一页</span><ChevronRight aria-hidden="true"/>')
|
expect(app).toContain('<span>下一页</span><ChevronRight aria-hidden="true"/>')
|
||||||
expect(css).toContain('.pager{min-height:56px;display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);align-items:center;gap:8px;margin:8px 0 14px;border-top:1px solid #e8e0d5;background:transparent}')
|
expect(css).toContain('.pager{min-height:56px;display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);align-items:center;gap:8px;margin:8px 0 14px;border-top:1px solid var(--border-hairline);background:transparent}')
|
||||||
expect(css).toContain('.pager-button{min-width:0;min-height:44px;padding:0 8px;display:flex;align-items:center;justify-content:center;gap:6px;border:0;border-radius:9px;background:transparent;color:#b7421e;font-size:13px;font-weight:650;box-shadow:none;transition:transform .14s ease,background-color .14s ease,color .14s ease}')
|
expect(css).toContain('.pager-button{min-width:0;min-height:44px;padding:0 8px;display:flex;align-items:center;justify-content:center;gap:6px;border:0;border-radius:var(--radius-control);background:transparent;color:var(--accent-ink);font-size:13px;font-weight:600;box-shadow:none;transition:transform .14s ease,background-color .14s ease,color .14s ease}')
|
||||||
expect(css).toContain('.pager-status{min-width:72px;display:grid;justify-items:center;gap:1px;color:var(--muted);font-size:11px;font-variant-numeric:tabular-nums}')
|
expect(css).toContain('.pager-status{min-width:72px;display:grid;justify-items:center;gap:1px;color:var(--text-secondary);font-size:11px;font-variant-numeric:tabular-nums}')
|
||||||
expect(css).toContain('@media(max-width:390px){.pager-button{padding:0 4px}.pager{grid-template-columns:minmax(0,1fr) 72px minmax(0,1fr);gap:2px}}')
|
expect(css).toContain('@media(max-width:390px){.pager-button{padding:0 4px}.pager{grid-template-columns:minmax(0,1fr) 72px minmax(0,1fr);gap:2px}}')
|
||||||
expect(app).toContain("if (activeView.value === 'upcoming') { openParams.set('due_from', isoAtLocalDayOffset(0)); openParams.set('due_to', isoAtLocalDayOffset(8)) }")
|
expect(app).toContain("if (activeView.value === 'upcoming') { openParams.set('due_from', isoAtLocalDayOffset(0)); openParams.set('due_to', isoAtLocalDayOffset(8)) }")
|
||||||
expect(app).toContain("new Date(task.due_at) >= startOfLocalDay(0) && new Date(task.due_at) < startOfLocalDay(8)")
|
expect(app).toContain("new Date(task.due_at) >= startOfLocalDay(0) && new Date(task.due_at) < startOfLocalDay(8)")
|
||||||
@@ -109,7 +109,7 @@ describe('approved five-detail polish', () => {
|
|||||||
expect(app).toContain('class="trash-groups"')
|
expect(app).toContain('class="trash-groups"')
|
||||||
expect(css).toContain('.list-page-context{width:min(100%,630px);margin:0 auto 0;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:start;gap:0 16px}')
|
expect(css).toContain('.list-page-context{width:min(100%,630px);margin:0 auto 0;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:start;gap:0 16px}')
|
||||||
expect(css).toContain('.list-page-title{margin:0;font-size:34px;line-height:1.15;font-weight:700;letter-spacing:-.035em}')
|
expect(css).toContain('.list-page-title{margin:0;font-size:34px;line-height:1.15;font-weight:700;letter-spacing:-.035em}')
|
||||||
expect(css).toContain('.list-page-summary{margin:8px 0 18px;color:var(--muted);font-size:13px}')
|
expect(css).toContain('.list-page-summary{margin:8px 0 18px;color:var(--text-secondary);font-size:13px}')
|
||||||
expect(css).toContain('@media(min-width:1440px){main.list-main>.list-page-context')
|
expect(css).toContain('@media(min-width:1440px){main.list-main>.list-page-context')
|
||||||
expect(css).toContain('@media(max-width:720px){main.list-main{padding-left:29px;padding-right:29px}main.list-main>.list-page-context,main.list-main>.list-section-heading,main.list-main>.task-list,main.list-main>.list-page-meta,main.list-main>.pager,main.list-main>.mvp-view{width:100%}')
|
expect(css).toContain('@media(max-width:720px){main.list-main{padding-left:29px;padding-right:29px}main.list-main>.list-page-context,main.list-main>.list-section-heading,main.list-main>.task-list,main.list-main>.list-page-meta,main.list-main>.pager,main.list-main>.mvp-view{width:100%}')
|
||||||
expect(css).toContain('@media(min-width:721px) and (max-width:1439px){main.list-main>.list-page-context,main.list-main>.list-section-heading,main.list-main>.task-list,main.list-main>.list-page-meta,main.list-main>.pager,main.list-main>.mvp-view{width:min(100%,630px)}')
|
expect(css).toContain('@media(min-width:721px) and (max-width:1439px){main.list-main>.list-page-context,main.list-main>.list-section-heading,main.list-main>.task-list,main.list-main>.list-page-meta,main.list-main>.pager,main.list-main>.mvp-view{width:min(100%,630px)}')
|
||||||
@@ -118,9 +118,9 @@ describe('approved five-detail polish', () => {
|
|||||||
expect(css).not.toContain('top:-62px')
|
expect(css).not.toContain('top:-62px')
|
||||||
expect(css).toContain('.today-context .completed-filter-pill:hover:not(:disabled),.today-context .completed-filter-pill:active:not(:disabled),.list-inline-filter:hover:not(:disabled),.list-inline-filter:active:not(:disabled),.habit-inline-filter:hover:not(:disabled),.habit-inline-filter:active:not(:disabled){background:transparent;border-color:transparent}')
|
expect(css).toContain('.today-context .completed-filter-pill:hover:not(:disabled),.today-context .completed-filter-pill:active:not(:disabled),.list-inline-filter:hover:not(:disabled),.list-inline-filter:active:not(:disabled),.habit-inline-filter:hover:not(:disabled),.habit-inline-filter:active:not(:disabled){background:transparent;border-color:transparent}')
|
||||||
expect(css).toContain('.today-inline-filter,.list-inline-filter,.habit-inline-filter{-webkit-tap-highlight-color:transparent}')
|
expect(css).toContain('.today-inline-filter,.list-inline-filter,.habit-inline-filter{-webkit-tap-highlight-color:transparent}')
|
||||||
expect(css).toContain('.list-inline-filter .completed-filter-pill__track,.habit-inline-filter .completed-filter-pill__track{width:31px;height:18px;background:#d8d0c5}')
|
expect(css).toContain('.list-inline-filter .completed-filter-pill__track,.habit-inline-filter .completed-filter-pill__track{width:31px;height:18px;background:var(--list-inline-filter-bg)}')
|
||||||
expect(css).toContain('.list-inline-filter .completed-filter-pill__thumb,.habit-inline-filter .completed-filter-pill__thumb{width:14px;height:14px}')
|
expect(css).toContain('.list-inline-filter .completed-filter-pill__thumb,.habit-inline-filter .completed-filter-pill__thumb{width:14px;height:14px}')
|
||||||
expect(css).toContain('.list-section-heading{width:min(100%,630px);min-height:44px;margin:0 auto;display:flex;align-items:center;border-bottom:1px solid #e8e0d5}')
|
expect(css).toContain('.list-section-heading{width:min(100%,630px);min-height:44px;margin:0 auto;display:flex;align-items:center;border-bottom:1px solid var(--border-hairline)}')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses approved Today-sourced page headers for full Habits and keeps archive lazy section', () => {
|
it('uses approved Today-sourced page headers for full Habits and keeps archive lazy section', () => {
|
||||||
@@ -139,7 +139,7 @@ describe('approved five-detail polish', () => {
|
|||||||
expect(css).toContain('.habit-section-heading{margin-top:0}')
|
expect(css).toContain('.habit-section-heading{margin-top:0}')
|
||||||
expect(css).toContain('main.list-main>.mvp-view{gap:0}')
|
expect(css).toContain('main.list-main>.mvp-view{gap:0}')
|
||||||
expect(css).toContain('main.list-main>.mvp-view>.habit-section-heading{width:100%}')
|
expect(css).toContain('main.list-main>.mvp-view>.habit-section-heading{width:100%}')
|
||||||
expect(css).toContain('main.list-main>.mvp-view>.habit-archive-section{margin-top:24px;padding-top:12px;border-top:1px solid #e8e0d5}')
|
expect(css).toContain('main.list-main>.mvp-view>.habit-archive-section{margin-top:24px;padding-top:12px;border-top:1px solid var(--border-hairline)}')
|
||||||
expect(css).toContain('.habit-archive-section{display:grid;gap:10px}')
|
expect(css).toContain('.habit-archive-section{display:grid;gap:10px}')
|
||||||
expect(css).toContain('.habit-archive-toggle{width:100%;min-height:44px;display:grid;grid-template-columns:28px minmax(0,1fr) 28px;align-items:center;gap:8px;padding:0 4px;border:0;background:transparent;box-shadow:none')
|
expect(css).toContain('.habit-archive-toggle{width:100%;min-height:44px;display:grid;grid-template-columns:28px minmax(0,1fr) 28px;align-items:center;gap:8px;padding:0 4px;border:0;background:transparent;box-shadow:none')
|
||||||
expect(css).toContain('.archived-habits{margin-top:2px;')
|
expect(css).toContain('.archived-habits{margin-top:2px;')
|
||||||
@@ -154,7 +154,7 @@ describe('approved five-detail polish', () => {
|
|||||||
expect(habits).toContain('class=\"habit-list plain-list\"')
|
expect(habits).toContain('class=\"habit-list plain-list\"')
|
||||||
expect(habits).toContain('class=\"habit-row habit-row--full swipeable\"')
|
expect(habits).toContain('class=\"habit-row habit-row--full swipeable\"')
|
||||||
expect(css).toContain('.plain-list{display:grid;gap:0;background:transparent;border:0;border-radius:0;box-shadow:none;overflow:visible}')
|
expect(css).toContain('.plain-list{display:grid;gap:0;background:transparent;border:0;border-radius:0;box-shadow:none;overflow:visible}')
|
||||||
expect(css).toContain('.plain-list-row,.plain-list>.task-row,.plain-list>.habit-row{height:58px;min-height:58px;max-height:58px;background:transparent;border:0;border-bottom:1px solid #e8e0d5;border-radius:0;box-shadow:none}')
|
expect(css).toContain('.plain-list-row,.plain-list>.task-row,.plain-list>.habit-row{height:58px;min-height:58px;max-height:58px;background:transparent;border:0;border-bottom:1px solid var(--border-hairline);border-radius:0;box-shadow:none}')
|
||||||
expect(css).toContain('.task-row{grid-template-columns:44px minmax(0,1fr) auto}')
|
expect(css).toContain('.task-row{grid-template-columns:44px minmax(0,1fr) auto}')
|
||||||
expect(css).toContain('.habit-row{grid-template-columns:44px minmax(0,1fr) auto;grid-template-rows:18px 4px;align-content:center;row-gap:5px}')
|
expect(css).toContain('.habit-row{grid-template-columns:44px minmax(0,1fr) auto;grid-template-rows:18px 4px;align-content:center;row-gap:5px}')
|
||||||
expect(css).toContain('.habit-row:has(>.habit-progress-bar)>.habit-main,.habit-row:has(>.habit-progress-bar)>.habit-progress-bar{transform:translateY(3px)}')
|
expect(css).toContain('.habit-row:has(>.habit-progress-bar)>.habit-main,.habit-row:has(>.habit-progress-bar)>.habit-progress-bar{transform:translateY(3px)}')
|
||||||
@@ -178,12 +178,12 @@ describe('approved five-detail polish', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('gives task and habit rows distinct hover, selected, and completed feedback', () => {
|
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){.plain-list>.task-row:not(.task-row--trash):hover,.plain-list>.habit-row:hover{background:var(--event-hover)}}')
|
||||||
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('@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,.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:var(--plain-list-bg-2)}')
|
||||||
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).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).done,.plain-list>.habit-row.done{background:var(--plain-list-bg)}')
|
||||||
expect(css).toContain('.plain-list>.task-row:not(.task-row--trash).overdue-task{background:#fff7f4}')
|
expect(css).toContain('.plain-list>.task-row:not(.task-row--trash).overdue-task{background:var(--plain-list-bg-3)}')
|
||||||
expect(habits.match(/selected:selectedHabit\?\.id===h\.id/g)?.length).toBe(2)
|
expect(habits.match(/selected:selectedHabit\?\.id===h\.id/g)?.length).toBe(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ import tailwindcss from '@tailwindcss/vite'
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue(), tailwindcss()],
|
plugins: [vue(), tailwindcss()],
|
||||||
server: { proxy: { '/api': 'http://localhost:8781', '/health': 'http://localhost:8781' } },
|
server: { proxy: { '/api': 'http://localhost:8781', '/health': 'http://localhost:8781' } },
|
||||||
test: { environment: 'jsdom' },
|
test: { environment: 'jsdom', setupFiles: ['./vitest.setup.ts'] },
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Synchronous requestAnimationFrame so Vue <Transition> leave (double-rAF)
|
||||||
|
// settles in the same tick as close(), preserving tests' "dialog removed
|
||||||
|
// immediately" contract. Production keeps real browser frames.
|
||||||
|
globalThis.requestAnimationFrame = ((cb: FrameRequestCallback) => {
|
||||||
|
cb(Date.now())
|
||||||
|
return 1
|
||||||
|
}) as typeof globalThis.requestAnimationFrame
|
||||||
|
|
||||||
|
globalThis.cancelAnimationFrame = (() => {}) as typeof globalThis.cancelAnimationFrame
|
||||||
|
|
||||||
|
export {}
|
||||||
Reference in New Issue
Block a user