fix: refine Today mobile hierarchy
ci / gitleaks (push) Successful in 8s
ci / docker (push) Successful in 6m23s

This commit is contained in:
2026-09-19 17:41:50 +08:00
parent 02507ac7a7
commit c17af0fd01
4 changed files with 61 additions and 21 deletions
+31 -8
View File
@@ -51,8 +51,10 @@ test('Today plain-list layout matches the approved responsive geometry', async (
})).toEqual({
media720: width <= 720,
contentWidth: width >= 1440 ? 1080 : width === 721 ? 630 : width - 58,
environmentHeight: width >= 721 ? 55 : width === 375 ? 90 : 93,
environmentHeight: width >= 721 ? 55 : width === 375 ? 79 : 81,
})
await expect(page.locator('.today-heading')).toHaveCount(1)
await expect(page.locator('.today-inline-add')).toHaveCount(1)
await expect(taskRowLocator).toHaveCount(1)
await expect(habitRowLocator).toHaveCount(1)
await expect(page.locator('.today-environment')).toBeVisible()
@@ -69,6 +71,8 @@ test('Today plain-list layout matches the approved responsive geometry', async (
const remaining = document.querySelector<HTMLElement>('.today-remaining')!
const filter = document.querySelector<HTMLElement>('.today-inline-filter')!
const firstSection = document.querySelector<HTMLElement>('.today-section-toggle')!
const heading = document.querySelector<HTMLElement>('.today-heading')!
const inlineAdd = document.querySelector<HTMLElement>('.today-inline-add')!
const main = document.querySelector<HTMLElement>('main.today-main')!
const content = rect('.today-context')
const sectionTitle = firstSection.querySelector<HTMLElement>('.today-section-title')!
@@ -84,8 +88,8 @@ test('Today plain-list layout matches the approved responsive geometry', async (
}))
const taskRow = document.querySelector<HTMLElement>(`#today-tasks .task-row[data-task-id="${taskId}"]`)!
const habitRow = document.querySelector<HTMLElement>(`.today-habit-row[data-habit-id="${habitId}"]`)!
const fab = document.querySelector<HTMLElement>('.unified-fab')!
const orderedElements = [environment, title, remaining, filter, firstSection]
const fab = document.querySelector<HTMLElement>('.unified-fab')
const orderedElements = [environment, heading, firstSection]
const ordered = orderedElements.map((element) => {
const box = element.getBoundingClientRect()
return { left: box.left, right: box.right, top: box.top, bottom: box.bottom, width: box.width, height: box.height }
@@ -136,6 +140,15 @@ test('Today plain-list layout matches the approved responsive geometry', async (
summaries,
styles: {
mainPaddingLeft: getComputedStyle(main).paddingLeft,
contextHeight: content.height,
headingDisplay: getComputedStyle(heading).display,
headingAlignItems: getComputedStyle(heading).alignItems,
filterTop: filter.getBoundingClientRect().top,
filterRight: filter.getBoundingClientRect().right,
inlineAddPosition: getComputedStyle(inlineAdd).position,
inlineAddWidth: inlineAdd.getBoundingClientRect().width,
inlineAddBottom: inlineAdd.getBoundingClientRect().bottom,
viewportHeight: innerHeight,
titleFontSize: getComputedStyle(title).fontSize,
titleFontWeight: getComputedStyle(title).fontWeight,
filterWidth: filter.getBoundingClientRect().width,
@@ -154,8 +167,8 @@ test('Today plain-list layout matches the approved responsive geometry', async (
habitHeight: habitRow.getBoundingClientRect().height,
habitBackground: getComputedStyle(habitRow).backgroundColor,
habitBorderBottom: getComputedStyle(habitRow).borderBottomWidth,
fabShadow: getComputedStyle(fab).boxShadow,
fabIconWidth: fab.querySelector('svg')?.getBoundingClientRect().width,
fabShadow: fab ? getComputedStyle(fab).boxShadow : '',
fabIconWidth: fab?.querySelector('svg')?.getBoundingClientRect().width ?? 0,
},
}
}, { taskId, habitId })
@@ -188,8 +201,18 @@ test('Today plain-list layout matches the approved responsive geometry', async (
expect(metrics.styles.filterVisualHeight).toBeLessThanOrEqual(36)
expect(metrics.styles.filterBackground).toBe('rgba(0, 0, 0, 0)')
expect(metrics.styles.filterBorderRadius).toBe('0px')
expect(metrics.styles.fabShadow).toContain('8px 18px')
expect(metrics.styles.fabIconWidth).toBeCloseTo(30, 0)
expect(metrics.styles.headingDisplay).toBe('grid')
expect(metrics.styles.headingAlignItems).toBe('center')
expect(metrics.styles.filterTop).toBeGreaterThanOrEqual(metrics.ordered[1].top)
expect(metrics.styles.filterTop).toBeLessThanOrEqual(metrics.ordered[1].bottom - metrics.styles.filterHeight)
expect(metrics.styles.filterRight).toBeCloseTo(metrics.content.right, 0)
expect(metrics.styles.inlineAddPosition).toBe('relative')
expect(metrics.styles.inlineAddWidth).toBeCloseTo(metrics.content.width, 0)
expect(metrics.styles.inlineAddBottom).toBeGreaterThanOrEqual(metrics.ordered[metrics.ordered.length - 1].bottom)
expect(metrics.styles.inlineAddBottom).toBeLessThanOrEqual(metrics.styles.viewportHeight + 1)
if (width <= 720) expect(metrics.styles.contextHeight).toBeLessThanOrEqual(210)
expect(metrics.styles.fabShadow).toBe('')
expect(metrics.styles.fabIconWidth).toBe(0)
if (width >= 721) {
expect(metrics.uniqueRows).toHaveLength(1)
expect(metrics.content.width).toBeCloseTo(width >= 1440 ? 1080 : 630, 0)
@@ -214,7 +237,7 @@ test('Today plain-list layout matches the approved responsive geometry', async (
expect(metrics.content.left).toBeCloseTo(29, 0)
expect(metrics.styles.mainPaddingLeft).toBe('29px')
expect(metrics.styles.titleFontSize).toBe('24px')
expect(metrics.environment.height).toBeCloseTo(width === 375 ? 90 : 93, 0)
expect(metrics.environment.height).toBeCloseTo(width === 375 ? 79 : 81, 0)
}
}
if (width >= 721) {
+5 -3
View File
@@ -1708,9 +1708,10 @@ onUnmounted(() => {
<template v-else>
<section v-if="activeView==='today'" class="today-context" aria-label="今日概览">
<TodayEnvironmentStrip :environment="todayEnvironment" :loading="todayEnvironmentLoading" :failed="todayEnvironmentError" />
<h1 class="today-page-title">今天</h1>
<p class="today-remaining">还有 {{ todayTaskRemaining + todayHabitRemaining }} 项待完成</p>
<div class="today-heading">
<div><h1 class="today-page-title">今天</h1><p class="today-remaining">还有 {{ todayTaskRemaining + todayHabitRemaining }} 项待完成</p></div>
<CompletedFilterPill v-model="showCompleted" class="today-inline-filter" />
</div>
</section>
<section v-if="activeView==='tasks'" class="list-page-context">
<div><h1 class="list-page-title" :title="activeName">{{ activeName }}</h1><p class="list-page-summary">{{ taskOpenTotal === null ? '待完成统计暂不可用' : `还有 ${taskOpenTotal} 项待完成` }}</p></div>
@@ -1755,6 +1756,7 @@ onUnmounted(() => {
</div>
</div>
</template>
<button v-if="activeView==='today' && showFloatingAdd" class="today-inline-add" type="button" aria-label="添加任务" @click="openTaskCompose"><Plus/><span>添加任务</span></button>
</main>
<AppSheet v-if="selectedTask" :open="true" :modal="compactLayout" variant="detail" panel-class="detail" title-id="task-detail-title" :close-on-scrim="compactLayout" :busy="taskDetailBusy" @close="closeTaskDetail" @submit.prevent="saveSelectedTaskChanges">
@@ -1789,7 +1791,7 @@ onUnmounted(() => {
</AppSheet>
<nav class="bottom" :inert="memoBackgroundInert ? true : undefined" aria-label="主要导航"><button :class="{active:activeView==='today'}" :aria-current="activeView==='today' ? 'page' : undefined" @click="switchView('today')"><ListTodo/><span>今天</span></button><button :class="{active:activeView==='habits'}" :aria-current="activeView==='habits' ? 'page' : undefined" @click="switchView('habits')"><Repeat2/><span>习惯</span></button><button :class="{active:activeView==='countdowns'}" :aria-current="activeView==='countdowns' ? 'page' : undefined" @click="switchView('countdowns')"><CalendarHeart/><span>倒数日</span></button><button :class="{active:activeView==='settings'}" :aria-current="activeView==='settings' ? 'page' : undefined" @click="switchView('settings')"><Settings/><span>设置</span></button></nav>
<FloatingAddButton v-if="['tasks','today','upcoming','habits','countdowns','memos'].includes(activeView)" :show="showFloatingAdd" :label="activeView==='habits' ? '添加习惯' : activeView==='countdowns' ? '添加倒数日' : activeView==='memos' ? '添加备忘录' : '添加任务'" @activate="activateFloatingAdd" />
<FloatingAddButton v-if="activeView!=='today' && ['tasks','upcoming','habits','countdowns','memos'].includes(activeView)" :show="showFloatingAdd" :label="activeView==='habits' ? '添加习惯' : activeView==='countdowns' ? '添加倒数日' : activeView==='memos' ? '添加备忘录' : '添加任务'" @activate="activateFloatingAdd" />
<AppSheet :open="taskComposeOpen" variant="create" panel-class="task-compose-sheet" title-id="task-compose-title" initial-focus=".task-compose-input" :style="taskComposeStyle" @close="closeTaskCompose" @submit.prevent="submitTaskCompose">
<header class="app-sheet__header"><div><h2 id="task-compose-title">{{ taskComposeTitle }}</h2></div><button class="icon" type="button" aria-label="关闭添加任务" @click="closeTaskCompose"><X/></button></header>
<div class="app-sheet__body">
+12 -6
View File
@@ -49,6 +49,7 @@ describe('Today environment integration', () => {
it('renders the approved plain-list composition without a board or progress tracks', () => {
expect(app).toContain("<main :class=\"{'today-main':activeView==='today','list-main':activeView==='tasks'||activeView==='habits'}\"")
expect(app).toContain('<section v-if="activeView===\'today\'" class="today-context" aria-label="今日概览">')
expect(app).toContain('<div class="today-heading">')
expect(app).toContain('<p class="today-remaining">还有 {{ todayTaskRemaining + todayHabitRemaining }} 项待完成</p>')
expect(app).not.toContain('class="today-board"')
expect(app).not.toContain('today-board-title')
@@ -57,29 +58,34 @@ describe('Today environment integration', () => {
expect(app).not.toContain('today-habit-track')
expect(app).not.toContain('role="progressbar"')
expect(app).toContain("'today-active': activeView==='today'")
expect(css).toContain('.shell.today-active .unified-fab{box-shadow:0 8px 18px rgba(174,65,29,.22)}')
expect(css).not.toContain('\n.unified-fab{box-shadow:0 8px 18px rgba(174,65,29,.22)}')
expect(app).toContain('class="today-inline-add"')
expect(app).toContain("activeView!=='today' && ['tasks','upcoming','habits','countdowns','memos'].includes(activeView)")
expect(css).toContain('.today-inline-add{')
expect(css).toContain('position:relative')
expect(css).toContain('main.today-main{padding-left:max(44px,calc((100% - 1080px)/2));padding-right:max(44px,calc((100% - 1080px)/2))}')
expect(css).toContain('.today-environment{grid-template-columns:repeat(3,minmax(0,1fr));')
expect(css).toContain('@media(max-width:930px){main.today-main{padding-left:max(44px,calc((100% - 630px)/2));padding-right:max(44px,calc((100% - 630px)/2))}}')
expect(css).toContain('@media(max-width:720.98px){main.today-main{padding-left:29px!important;padding-right:29px!important}')
expect(css).toContain('@media(max-width:720.98px){main.today-main{padding-left:29px!important;padding-right:29px!important;padding-bottom:calc(78px + var(--safe-area-bottom))}')
expect(css).toContain('.today-context{margin:0 0 14px;border-bottom:0}')
expect(css).toContain('.today-remaining{margin:0 0 24px;color:var(--muted);font-size:13px}')
expect(css).toContain('.today-heading{display:grid;')
expect(css).toContain('.today-heading .today-remaining{margin:6px 0 0}')
expect(css).not.toContain('.today-board{')
expect(css).not.toContain('.today-board__progress{')
expect(css).not.toContain('.today-track{')
expect(css).not.toContain('.today-track-rail{')
})
it('renders the approved plain-list DOM in prototype order without duplicating the Today title', () => {
it('renders the approved plain-list DOM with the Today title and filter in one heading row', () => {
const main = app.slice(app.indexOf('<main :class='), app.indexOf('</main>'))
const environment = main.indexOf('<TodayEnvironmentStrip')
const heading = main.indexOf('<div class="today-heading">')
const title = main.indexOf('<h1 class="today-page-title">今天</h1>')
const remaining = main.indexOf('<p class="today-remaining">')
const filter = main.indexOf('<CompletedFilterPill v-model="showCompleted" class="today-inline-filter" />')
const overdue = main.indexOf('id="today-overdue-heading"')
expect(environment).toBeGreaterThan(-1)
expect(title).toBeGreaterThan(environment)
expect(heading).toBeGreaterThan(environment)
expect(title).toBeGreaterThan(heading)
expect(remaining).toBeGreaterThan(title)
expect(filter).toBeGreaterThan(remaining)
expect(overdue).toBeGreaterThan(filter)
+12 -3
View File
@@ -152,7 +152,16 @@ main.list-main>.mvp-view>.habit-archive-section{border-top:1px solid #e8e0d5}
@media(max-width:720px){main.list-main{padding-left:29px;padding-right:29px}main.list-main>.list-page-context,main.list-main>.list-search-reveal,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%}.list-page-title{font-size:24px}.list-search-reveal>.task-search-toggle{display:grid;position:relative;justify-self:end}.list-search-reveal:not(.mobile-search-open){min-height:0}.list-search-reveal:not(.mobile-search-open)>.list-search,.list-search-reveal:not(.mobile-search-open)>.list-search-clear{display:none}.list-search kbd{display:none}}
/* Approved Today plain-list prototype parity. */
main.today-main .topbar{margin-bottom:18px}
main.today-main .topbar{margin-bottom:12px}
.today-heading{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;column-gap:16px;margin-top:20px}
.today-heading>div{min-width:0}
.today-heading .today-page-title{margin:0}
.today-heading .today-remaining{margin:6px 0 0}
.today-heading .today-inline-filter{align-self:center;justify-self:end}
.today-inline-add{position:relative;width:100%;height:44px;margin:18px auto 0;display:flex;align-items:center;justify-content:center;gap:7px;border:1px solid #dfc9b2;border-radius:11px;background:var(--surface-raised);color:var(--accent);box-shadow:var(--highlight-inner),0 5px 16px rgba(88,67,42,.1);font-size:13px;font-weight:700}
.today-inline-add svg{width:17px;height:17px}
.today-inline-add:active{transform:scale(.985)}
.today-inline-add:focus-visible{outline:3px solid var(--focus-ring);outline-offset:2px}
.today-environment{grid-template-columns:repeat(3,minmax(0,1fr));align-items:center;gap:0;padding:0 0 18px;border-bottom:1px solid #e8e0d5;color:#8a8178}
.today-environment__item{height:36px;padding:0 0 0 18px;justify-content:flex-start}
.today-environment__item:first-child{padding-left:0}
@@ -166,8 +175,8 @@ main.today-main .topbar{margin-bottom:18px}
.today-main .task-tail{padding-left:12px;font-size:12px}.today-main .habit-row{gap:0}.today-main .habit-main{display:flex;align-items:center;min-width:0;padding:0}
.shell.today-active .unified-fab{box-shadow:0 8px 18px rgba(174,65,29,.22)}.shell.today-active .unified-fab>svg{width:30px;height:30px;stroke-width:1.5}
@media(max-width:930px){main.today-main{padding-left:max(44px,calc((100% - 630px)/2));padding-right:max(44px,calc((100% - 630px)/2))}}
@media(max-width:720.98px){main.today-main{padding-left:29px!important;padding-right:29px!important}.today-environment{grid-template-columns:minmax(0,1fr) minmax(0,1fr);grid-template-rows:21px auto;align-items:stretch;column-gap:0;row-gap:11px;padding-bottom:14px}.today-environment__calendar{grid-column:1/-1;grid-row:1;height:21px!important;padding:0;justify-content:space-between;align-items:start;border-left:0}.today-environment__weather,.today-environment__gold{grid-row:2;height:clamp(42px,calc(25vw - 51.75px),45.75px);padding-top:10px;border-top:1px solid #e8e0d5}.today-environment__weather{grid-column:1;padding-left:0;padding-right:10px;border-left:0}.today-environment__gold{grid-column:2;padding-left:12px}.today-environment__weather strong,.today-environment__gold strong,.today-environment__gold-primary{line-height:16px}.today-environment__item small{line-height:13px}.today-page-title{margin-top:23px;font-size:24px}.today-main .task-row,.today-main .habit-row{height:58px;min-height:58px;max-height:58px}.today-context .completed-filter-pill{width:87px;height:44px;min-height:44px}.today-context .completed-filter-pill__track{width:30px;height:18px}.today-context .completed-filter-pill__thumb{width:14px;height:14px}.today-context .completed-filter-pill[aria-checked="true"] .completed-filter-pill__thumb{transform:translateX(12px)}}
@media(max-width:380px){.today-environment__weather,.today-environment__gold{height:43px;padding-top:7px;padding-bottom:7px}}
@media(max-width:720.98px){main.today-main{padding-left:29px!important;padding-right:29px!important;padding-bottom:calc(78px + var(--safe-area-bottom))}.today-environment{grid-template-columns:minmax(0,1fr) minmax(0,1fr);grid-template-rows:21px auto;align-items:stretch;column-gap:0;row-gap:7px;padding-bottom:10px}.today-environment__calendar{grid-column:1/-1;grid-row:1;height:21px!important;padding:0;justify-content:space-between;align-items:start;border-left:0}.today-environment__weather,.today-environment__gold{grid-row:2;height:clamp(38px,calc(25vw - 55.75px),42px);padding-top:7px;border-top:1px solid #e8e0d5}.today-environment__weather{grid-column:1;padding-left:0;padding-right:10px;border-left:0}.today-environment__gold{grid-column:2;padding-left:12px}.today-environment__weather strong,.today-environment__gold strong,.today-environment__gold-primary{line-height:16px}.today-environment__item small{line-height:13px}.today-heading{margin-top:16px}.today-page-title{font-size:24px}.today-heading .today-remaining{margin-top:4px}.today-main .task-row,.today-main .habit-row{height:58px;min-height:58px;max-height:58px}.today-context .completed-filter-pill{width:87px;height:44px;min-height:44px}.today-context .completed-filter-pill__track{width:30px;height:18px}.today-context .completed-filter-pill__thumb{width:14px;height:14px}.today-context .completed-filter-pill[aria-checked="true"] .completed-filter-pill__thumb{transform:translateX(12px)}.today-inline-add{margin-top:14px}}
@media(max-width:380px){.today-environment__weather,.today-environment__gold{height:40px;padding-top:5px;padding-bottom:5px}}
/* Approved Settings 01: continuous paper ledger. */
main:has(>.mvp-view .settings-sections){background:#fffdf8}