Revert "fix: align calendar sheets with detail pane"
ci / gitleaks (push) Successful in 58s
ci / docker (push) Successful in 5m35s

This reverts commit 6e3e09e8b6.
This commit is contained in:
2026-09-20 18:12:12 +08:00
parent 6e3e09e8b6
commit e6d720b85c
4 changed files with 12 additions and 51 deletions
+4 -40
View File
@@ -200,21 +200,16 @@ test('all bottom destinations expose one active page and desktop layout stays un
expect(desktopBottomGap).toBe(84)
})
test('calendar week focus and sheets keep the approved responsive geometry', async ({ page }) => {
const now = new Date()
const startsAt = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 9).toISOString()
const endsAt = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 10).toISOString()
test('calendar week focus keeps seven usable day controls without page overflow', async ({ page }) => {
await page.route('**/api/v1/calendar-subscriptions', route => route.fulfill({ json: [{ id:'calendar-source', name:'工作', url:'https://example.com/work.ics', color:'#f15a29', enabled:true, refreshed_at:null, last_error:null, stale:false }] }))
await page.route('**/api/v1/calendar-events?*', route => route.fulfill({ json: { events: [{ id:'calendar-event', title:'晨会', starts_at:startsAt, ends_at:endsAt, all_day:false, source_id:'calendar-source', source_name:'工作', color:'#f15a29', description:'同步本周事项', location:'会议室' }], sources: [] } }))
await page.route('**/api/v1/calendar-events?*', route => route.fulfill({ json: { events: [], sources: [] } }))
await page.goto('/')
const viewport = page.viewportSize()!
const mobile = viewport.width <= 930
const mobile = (await page.viewportSize())!.width <= 930
if (mobile) await bottomTab(page, '日历订阅').click()
else await page.locator('.primary-nav').getByRole('button', { name: '日历订阅', exact: true }).click()
const strip = page.getByRole('group', { name: '选择日期' })
await expect(strip).toBeVisible()
await expect(strip.locator('.calendar-week-day')).toHaveCount(7)
await expect(page.locator('.calendar-heading h1')).toHaveCount(0)
const metrics = await strip.evaluate(element => ({
documentOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
stripOverflow: element.scrollWidth > element.clientWidth,
@@ -227,38 +222,7 @@ test('calendar week focus and sheets keep the approved responsive geometry', asy
return { background: style.backgroundColor, color: style.color }
})
expect(selectedColors).toEqual({ background: 'rgb(241, 90, 41)', color: 'rgb(255, 255, 255)' })
if (viewport.width <= 375) expect(metrics.stripOverflow).toBeTruthy()
const shell = page.locator('.shell')
const assertSheetGeometry = async (sheet: ReturnType<typeof page.locator>) => {
await expect(sheet).toBeVisible()
const box = await sheet.boundingBox()
expect(box).not.toBeNull()
expect(await page.evaluate(() => document.documentElement.scrollWidth - innerWidth)).toBe(0)
if (mobile) {
expect(box!.x).toBeCloseTo(0, 0)
expect(box!.width).toBeCloseTo(viewport.width, 0)
const innerOverflow = await sheet.evaluate(element => ({ clientWidth: element.clientWidth, scrollWidth: element.scrollWidth, childrenInside: [...element.querySelectorAll<HTMLElement>('.app-sheet__header,.app-sheet__body,.app-sheet__footer')].every(child => child.getBoundingClientRect().right <= element.getBoundingClientRect().right + 1) }))
expect(innerOverflow.scrollWidth).toBeLessThanOrEqual(innerOverflow.clientWidth)
expect(innerOverflow.childrenInside).toBeTruthy()
} else {
await expect(shell).toHaveClass(/detail-open/)
expect(box!.width).toBeCloseTo(350, 0)
expect(box!.x + box!.width).toBeCloseTo(viewport.width, 0)
expect(box!.height).toBeCloseTo(viewport.height, 0)
}
}
await page.getByRole('button', { name: '管理日历源' }).click()
await page.getByRole('button', { name: '添加日历订阅' }).click()
const form = page.getByRole('dialog', { name: '添加日历源' })
await assertSheetGeometry(form)
await form.getByRole('button', { name: '关闭订阅表单' }).click()
await page.getByRole('button', { name: '关闭日历源' }).click()
await page.locator('[data-event-id="calendar-event"]').click()
const detail = page.getByRole('dialog', { name: '晨会' })
await assertSheetGeometry(detail)
if ((await page.viewportSize())!.width <= 390) expect(metrics.stripOverflow).toBeTruthy()
})
test('settings match the approved paper-ledger geometry and action hierarchy', async ({ page }) => {
+2 -3
View File
@@ -183,7 +183,6 @@ const memoPanel = ref<InstanceType<typeof MemoPanel> | null>(null)
const memoTrash = ref(false)
const memoDetailOpen = ref(false)
const habitDetailOpen = ref(false)
const calendarDetailOpen = ref(false)
const compactLayout = ref(desktopViewportWidth.value <= 930)
const memoShellState = computed(() => deriveMemoShellState({ view: activeView.value, detailOpen: memoDetailOpen.value, compact: compactLayout.value, trash: memoTrash.value }))
const memoBackgroundInert = computed(() => memoShellState.value.backgroundInert)
@@ -1605,7 +1604,7 @@ onUnmounted(() => {
<p v-if="initialized" class="auth-footnote">登录后继续你的清单</p>
</form>
</div>
<div v-else class="shell" :class="{ 'today-active': activeView==='today', 'sidebar-collapsed': sidebarCollapsed, 'detail-open': Boolean(selectedTask) || habitDetailOpen || (activeView==='calendar' && calendarDetailOpen), 'memo-detail-open': activeView==='memos' && memoDetailOpen, 'mobile-sidebar-open': mobileSidebar, 'pane-resizing': Boolean(resizingPane) }" :style="shellStyle">
<div v-else class="shell" :class="{ 'today-active': activeView==='today', 'sidebar-collapsed': sidebarCollapsed, 'detail-open': Boolean(selectedTask) || habitDetailOpen, 'memo-detail-open': activeView==='memos' && memoDetailOpen, 'mobile-sidebar-open': mobileSidebar, 'pane-resizing': Boolean(resizingPane) }" :style="shellStyle">
<div v-if="mobileSidebar || mobileDetail" class="scrim" @click="mobileSidebar=false;mobileDetail=false" />
<aside class="sidebar" :inert="memoBackgroundInert ? true : undefined" :class="{ open: mobileSidebar }" @keydown.esc="sidebarCreateOpen=false;closeArchivedListAction();closeSidebarAction()">
<div class="brand-row"><div class="brand small brand-lockup"><img class="brand-logo" src="/dodo-logo.svg" alt=""><span class="brand-wordmark">dodo</span></div><button class="icon mobile-only" aria-label="关闭菜单" @click="mobileSidebar=false"><X /></button></div>
@@ -1688,7 +1687,7 @@ onUnmounted(() => {
<MvpPanel ref="habitComposer" :key="activeView" :view="activeView as 'habits'|'settings'" :show-completed="showCompleted" :compact-layout="compactLayout" @update:show-completed="showCompleted=$event" @detail="handleHabitDetail" @changed="refreshAll" @notice="toast" @logout="completeLogout" />
</template>
<MemoPanel v-else-if="activeView==='memos'" ref="memoPanel" :request="api" @scope="memoTrash=$event==='trash'" @detail="memoDetailOpen=$event" @notice="toast" />
<CalendarPanel v-else-if="activeView==='calendar'" :compact-layout="compactLayout" @detail="calendarDetailOpen=$event" @notice="toast" />
<CalendarPanel v-else-if="activeView==='calendar'" @notice="toast" />
<CountdownPanel ref="countdownComposer" v-else-if="activeView==='countdowns'" @notice="toast" />
<template v-else>
<section v-if="activeView==='today'" class="today-context" aria-label="今日概览">
+5 -7
View File
@@ -10,8 +10,7 @@ type Subscription = { id:string; name:string; url:string; color:string; enabled:
type CalendarEvent = { id:string; title:string; starts_at:string; ends_at:string; all_day:boolean; source_id:string; source_name:string; color:string; description?:string|null; location?:string|null }
type EventResponse = { events:CalendarEvent[]; sources:Array<{id:string;name:string;stale:boolean}> }
type Form = { name:string; url:string; color:string; enabled:boolean }
const props=withDefaults(defineProps<{compactLayout?:boolean}>(),{compactLayout:true})
const emit=defineEmits<{notice:[message:string];detail:[open:boolean]}>()
const emit=defineEmits<{notice:[message:string]}>()
const subscriptions=ref<Subscription[]>([]),events=ref<CalendarEvent[]>([]),loading=ref(false),error=ref('')
let eventsRequestGeneration=0
const selectedDay=ref(new Date(new Date().getFullYear(),new Date().getMonth(),new Date().getDate())),hiddenSources=ref(new Set<string>())
@@ -55,22 +54,21 @@ async function toggleEnabled(item:Subscription){if(busyId.value)return;busyId.va
async function refresh(item:Subscription){if(busyId.value)return;busyId.value=item.id;error.value='';try{await request(`/calendar-subscriptions/${item.id}/refresh`,{method:'POST'});await Promise.all([loadSubscriptions(),loadEvents()]);emit('notice',`${item.name}已刷新`)}catch(reason){error.value=reason instanceof Error?reason.message:'刷新失败'}finally{busyId.value=''}}
async function remove(item:Subscription){if(busyId.value)return;if(await appDialog.value?.show({title:`删除“${item.name}”?`,description:'该来源的事件也会从日历中移除。',danger:true,confirmText:'删除'})!==true)return;busyId.value=item.id;error.value='';try{await request(`/calendar-subscriptions/${item.id}`,{method:'DELETE'});hiddenSources.value.delete(item.id);await Promise.all([loadSubscriptions(),loadEvents()]);emit('notice','日历订阅已删除')}catch(reason){error.value=reason instanceof Error?reason.message:'删除失败'}finally{busyId.value=''}}
watch(manageOpen,open=>{if(!open)formOpen.value=false})
watch(()=>Boolean(selected.value)||manageOpen.value||formOpen.value,open=>emit('detail',open),{immediate:true})
onMounted(()=>void load())
</script>
<template>
<section class="calendar-view" :class="{loading}">
<header class="calendar-heading"><p>{{filteredEvents.length}} 个日程 · {{subscriptions.length}} 个来源</p><button class="soft-button calendar-manage" aria-label="管理日历源" @click="manageOpen=true"><Settings2/>日历源</button></header>
<header class="calendar-heading"><div><h1>日历订阅</h1><p>{{filteredEvents.length}} 个日程 · {{subscriptions.length}} 个来源</p></div><button class="soft-button calendar-manage" aria-label="管理日历源" @click="manageOpen=true"><Settings2/>日历源</button></header>
<p v-if="error" class="inline-error" role="alert">{{error}}</p>
<div class="calendar-toolbar"><button aria-label="上一周" @click="moveWeek(-1)"><ChevronLeft/></button><button class="calendar-today" aria-label="回到今天" @click="today">今天</button><strong>{{weekLabel}}</strong><button aria-label="下一周" @click="moveWeek(1)"><ChevronRight/></button></div>
<div class="calendar-week-strip" role="group" aria-label="选择日期"><button v-for="day in weekDays" :key="key(day)" class="calendar-week-day" :class="{'is-selected':key(day)===selectedDayKey,'is-today':isToday(day)}" :data-day="key(day)" :aria-label="`${displayDay(key(day))}${dayEventCount(day)?`${dayEventCount(day)}个日程`:',无日程'}`" :aria-pressed="key(day)===selectedDayKey" @click="selectDay(day)"><small>{{weekDayLabel(day)}}</small><b>{{day.getDate()}}</b><i v-if="dayEventCount(day)" aria-hidden="true">{{dayEventCount(day)}}</i></button></div>
<div v-if="subscriptions.length" class="calendar-filters" aria-label="筛选日历源"><label v-for="source in subscriptions" :key="source.id"><input type="checkbox" :aria-label="`筛选${source.name}`" :checked="!hiddenSources.has(source.id)" @change="toggleFilter(source.id)"><i :style="{background:source.color}"/>{{source.name}}</label></div>
<div v-if="visibleEvents.length" class="calendar-agenda"><section><h2>{{selectedDayLabel}} · {{visibleEvents.length}} 个日程</h2><button v-for="event in visibleEvents" :key="eventKey(event)" :data-event-id="event.id" class="calendar-event-row" @click="selected=event"><i :style="{background:eventColor(event)}"/><time>{{displayTime(event)}}</time><span><b>{{eventTitle(event)}}</b><small>{{eventSource(event)}}<template v-if="event.location"> · {{event.location}}</template></small></span><ChevronRight/></button></section></div>
<div v-else-if="!loading" class="calendar-empty"><CalendarDays/><b>这一天还没有日程</b><span>{{subscriptions.length?'可以选择本周其他日期或检查来源筛选':'先添加一个 iCal 日历订阅'}}</span><button v-if="!subscriptions.length" class="primary-small" @click="manageOpen=true;openCreate()">添加日历源</button></div>
<AppSheet :open="Boolean(selected)" :modal="props.compactLayout" inline-target=".shell" variant="detail" panel-class="calendar-event-detail" title-id="calendar-event-title" initial-focus="button[aria-label='关闭日程详情']" :close-on-scrim="props.compactLayout" @close="selected=null"><template v-if="selected"><header class="app-sheet__header calendar-sheet-header"><div><h3 id="calendar-event-title">{{eventTitle(selected)}}</h3><small><i :style="{background:eventColor(selected)}"/>{{eventSource(selected)}}</small></div><button aria-label="关闭日程详情" @click="selected=null"><X/></button></header><div class="app-sheet__body"><dl><div><dt>时间</dt><dd>{{displayDay(localDayKey(eventStart(selected)))}} {{displayTime(selected)}}<template v-if="eventEnd(selected) && !selected.all_day"> {{displayTime({...selected,starts_at:eventEnd(selected)})}}</template></dd></div><div><dt>来源</dt><dd><i :style="{background:eventColor(selected)}"/>{{eventSource(selected)}}</dd></div><div v-if="selected.location"><dt>地点</dt><dd>{{selected.location}}</dd></div></dl><section v-if="selected.description" class="calendar-event-notes"><h4>备注</h4><p>{{selected.description}}</p></section></div></template></AppSheet>
<AppSheet :open="manageOpen&&!formOpen" :modal="props.compactLayout" inline-target=".shell" variant="detail" panel-class="calendar-sources-sheet" title-id="calendar-sources-title" initial-focus="button[aria-label='关闭日历源']" :close-on-scrim="props.compactLayout" @close="manageOpen=false"><header class="app-sheet__header"><h3 id="calendar-sources-title">日历源</h3><button aria-label="关闭日历源" @click="manageOpen=false"><X/></button></header><div class="app-sheet__body"><button class="primary-small calendar-source-add" aria-label="添加日历订阅" @click="openCreate"><Plus/>添加订阅</button><div class="calendar-source-list"><article v-for="source in subscriptions" :key="source.id"><div class="calendar-source-copy"><b><i :style="{background:source.color}"/>{{source.name}}</b><small>{{source.url}}</small><small v-if="source.last_error" class="calendar-source-error" role="alert">{{source.last_error}}</small></div><label class="calendar-source-toggle"><input type="checkbox" :aria-label="`启用${source.name}`" :checked="source.enabled" :disabled="Boolean(busyId)" @change="toggleEnabled(source)"><span>启用</span></label><button :aria-label="`刷新${source.name}`" :disabled="Boolean(busyId)" @click="refresh(source)"><RefreshCw/></button><button :aria-label="`编辑${source.name}`" :disabled="Boolean(busyId)" @click="openEdit(source)"><Pencil/></button><button class="danger-text" :aria-label="`删除${source.name}`" :disabled="Boolean(busyId)" @click="remove(source)"><Trash2/></button></article></div></div></AppSheet>
<AppSheet :open="formOpen" :modal="props.compactLayout" inline-target=".shell" variant="create" panel-class="calendar-subscription-form" title-id="calendar-form-title" initial-focus="input[aria-label='订阅名称']" :busy="busyId==='form'" :close-on-scrim="props.compactLayout" @close="formOpen=false" @submit.prevent="save"><header class="app-sheet__header calendar-sheet-header"><div><h3 id="calendar-form-title">{{editing?'编辑日历源':'添加日历源'}}</h3><small>{{editing?'修改订阅信息':'通过 iCal 地址同步日程'}}</small></div><button type="button" aria-label="关闭订阅表单" @click="formOpen=false"><X/></button></header><div class="app-sheet__body calendar-form-body"><label>名称<input v-model="form.name" aria-label="订阅名称" maxlength="120" required placeholder="例如:工作"></label><label>iCal 地址<input v-model="form.url" aria-label="订阅地址" type="url" required placeholder="https://example.com/calendar.ics"><small>支持公开的 .ics 订阅地址</small></label><div class="calendar-form-appearance"><label>颜色<input v-model="form.color" aria-label="订阅颜色" type="color"></label><label class="calendar-form-toggle"><input v-model="form.enabled" type="checkbox"><span><b>启用日历源</b><small>保存后立即参与同步</small></span></label></div></div><footer class="app-sheet__footer"><button type="button" class="secondary" @click="formOpen=false">取消</button><button type="submit" class="primary-small" :disabled="Boolean(busyId)||!form.name.trim()||!form.url.trim()">保存</button></footer></AppSheet>
<AppSheet :open="Boolean(selected)" variant="detail" panel-class="calendar-event-detail" title-id="calendar-event-title" initial-focus="button[aria-label='关闭日程详情']" @close="selected=null"><template v-if="selected"><header class="app-sheet__header"><h3 id="calendar-event-title">{{eventTitle(selected)}}</h3><button aria-label="关闭日程详情" @click="selected=null"><X/></button></header><div class="app-sheet__body"><dl><div><dt>时间</dt><dd>{{displayDay(localDayKey(eventStart(selected)))}} {{displayTime(selected)}}<template v-if="eventEnd(selected) && !selected.all_day"> {{displayTime({...selected,starts_at:eventEnd(selected)})}}</template></dd></div><div><dt>来源</dt><dd><i :style="{background:eventColor(selected)}"/>{{eventSource(selected)}}</dd></div><div v-if="selected.location"><dt>地点</dt><dd>{{selected.location}}</dd></div></dl><section v-if="selected.description"><h4>备注</h4><p>{{selected.description}}</p></section></div></template></AppSheet>
<AppSheet :open="manageOpen" variant="detail" panel-class="calendar-sources-sheet" title-id="calendar-sources-title" initial-focus="button[aria-label='关闭日历源']" @close="manageOpen=false"><header class="app-sheet__header"><h3 id="calendar-sources-title">日历源</h3><button aria-label="关闭日历源" @click="manageOpen=false"><X/></button></header><div class="app-sheet__body"><button class="primary-small calendar-source-add" aria-label="添加日历订阅" @click="openCreate"><Plus/>添加订阅</button><div class="calendar-source-list"><article v-for="source in subscriptions" :key="source.id"><div class="calendar-source-copy"><b><i :style="{background:source.color}"/>{{source.name}}</b><small>{{source.url}}</small><small v-if="source.last_error" class="calendar-source-error" role="alert">{{source.last_error}}</small></div><label class="calendar-source-toggle"><input type="checkbox" :aria-label="`启用${source.name}`" :checked="source.enabled" :disabled="Boolean(busyId)" @change="toggleEnabled(source)"><span>启用</span></label><button :aria-label="`刷新${source.name}`" :disabled="Boolean(busyId)" @click="refresh(source)"><RefreshCw/></button><button :aria-label="`编辑${source.name}`" :disabled="Boolean(busyId)" @click="openEdit(source)"><Pencil/></button><button class="danger-text" :aria-label="`删除${source.name}`" :disabled="Boolean(busyId)" @click="remove(source)"><Trash2/></button></article></div></div></AppSheet>
<AppSheet :open="formOpen" variant="create" panel-class="calendar-subscription-form" title-id="calendar-form-title" initial-focus="input[aria-label='订阅名称']" :busy="busyId==='form'" @close="formOpen=false" @submit.prevent="save"><header class="app-sheet__header"><h3 id="calendar-form-title">{{editing?'编辑订阅':'添加订阅'}}</h3><button type="button" aria-label="关闭订阅表单" @click="formOpen=false"><X/></button></header><div class="app-sheet__body"><label>名称<input v-model="form.name" aria-label="订阅名称" maxlength="120" required placeholder="例如:工作"></label><label>iCal 地址<input v-model="form.url" aria-label="订阅地址" type="url" required placeholder="https://example.com/calendar.ics"></label><label>颜色<input v-model="form.color" aria-label="订阅颜色" type="color"></label><label class="calendar-form-toggle"><input v-model="form.enabled" type="checkbox">启用此订阅</label></div><footer class="app-sheet__footer"><button type="button" class="secondary" @click="formOpen=false">取消</button><button type="submit" class="primary-small" :disabled="Boolean(busyId)||!form.name.trim()||!form.url.trim()">保存</button></footer></AppSheet>
<AppDialog ref="appDialog"/>
</section>
</template>
File diff suppressed because one or more lines are too long