style: redesign sidebar as quiet index
ci / gitleaks (push) Successful in 58s
ci / docker (push) Successful in 4m32s

This commit is contained in:
2026-09-17 16:48:23 +08:00
parent 40162aaa54
commit 73aca652f1
3 changed files with 54 additions and 10 deletions
+2 -2
View File
@@ -1463,9 +1463,9 @@ onUnmounted(() => {
<div class="folders">
<div v-for="folder in folders" :key="folder.id" class="folder-block" :data-folder-id="folder.id">
<div class="folder-row" :class="{'list-drop-target':listDrag&&listDropFolderId===folder.id&&!listReorderTarget}" @pointermove="listDrag&&resolveListDrop($event)"><button :title="folder.name" :aria-label="folder.name" @click="toggleFolder(folder.id)"><ChevronDown v-if="expandedFolders.has(folder.id)"/><ChevronRight v-else/><Folder/><span>{{folder.name}}</span></button><span class="row-actions"><button aria-label="打开文件夹操作" :aria-expanded="sidebarAction?.item.id===folder.id" @click="openSidebarAction('folders',folder)"><Ellipsis/></button></span></div>
<div v-for="list in lists.filter(l=>l.folder_id===folder.id && !l.is_inbox)" v-show="expandedFolders.has(folder.id)" :key="list.id" :data-list-id="list.id" class="list-row" :class="{active:activeView==='tasks'&&activeList===list.id,'list-dragging':listDrag?.id===list.id,'list-reorder-target':listReorderTarget===list.id&&listDrag?.id!==list.id}" :style="{'--list-drag-y':`${listDrag?.id===list.id?listDrag.offsetY:0}px`}" @pointermove="moveListDrag(list,$event)" @pointerup="finishListDrag(list,$event)" @pointercancel="cancelListDrag"><button class="list-drag-handle" aria-label="拖动清单排序或移动到文件夹" title="长按拖动清单" @pointerdown.stop="startListHandlePress(list,$event)" @pointermove.stop="moveListHandle(list,$event)" @pointerup.stop="finishListDrag(list,$event)" @pointercancel.stop="cancelListDrag"><GripVertical/></button><button class="list-row-main draggable-list-row-main" :title="list.name" :aria-label="list.name" @click="selectListUnlessDragged(list)"><i/><span>{{list.name}}</span></button><span class="row-actions"><button aria-label="打开清单操作" :aria-expanded="sidebarAction?.item.id===list.id" @click="openSidebarAction('lists',list)"><Ellipsis/></button></span></div>
<div v-for="list in lists.filter(l=>l.folder_id===folder.id && !l.is_inbox)" v-show="expandedFolders.has(folder.id)" :key="list.id" :data-list-id="list.id" class="list-row" :class="{active:activeView==='tasks'&&activeList===list.id,'list-dragging':listDrag?.id===list.id,'list-reorder-target':listReorderTarget===list.id&&listDrag?.id!==list.id}" :style="{'--list-drag-y':`${listDrag?.id===list.id?listDrag.offsetY:0}px`}" @pointermove="moveListDrag(list,$event)" @pointerup="finishListDrag(list,$event)" @pointercancel="cancelListDrag"><button class="list-drag-handle" aria-label="拖动清单排序或移动到文件夹" title="长按拖动清单" @pointerdown.stop="startListHandlePress(list,$event)" @pointermove.stop="moveListHandle(list,$event)" @pointerup.stop="finishListDrag(list,$event)" @pointercancel.stop="cancelListDrag"><GripVertical/></button><button class="list-row-main draggable-list-row-main" :title="list.name" :aria-label="list.name" @click="selectListUnlessDragged(list)"><span>{{list.name}}</span></button><span class="row-actions"><button aria-label="打开清单操作" :aria-expanded="sidebarAction?.item.id===list.id" @click="openSidebarAction('lists',list)"><Ellipsis/></button></span></div>
</div>
<div v-for="list in lists.filter(l=>!l.folder_id&&!l.is_inbox)" :key="list.id" :data-list-id="list.id" class="list-row" :class="{active:activeView==='tasks'&&activeList===list.id,'list-dragging':listDrag?.id===list.id,'list-reorder-target':listReorderTarget===list.id&&listDrag?.id!==list.id}" :style="{'--list-drag-y':`${listDrag?.id===list.id?listDrag.offsetY:0}px`}" @pointermove="moveListDrag(list,$event)" @pointerup="finishListDrag(list,$event)" @pointercancel="cancelListDrag"><button class="list-drag-handle" aria-label="拖动清单排序或移动到文件夹" title="长按拖动清单" @pointerdown.stop="startListHandlePress(list,$event)" @pointermove.stop="moveListHandle(list,$event)" @pointerup.stop="finishListDrag(list,$event)" @pointercancel.stop="cancelListDrag"><GripVertical/></button><button class="list-row-main draggable-list-row-main" :title="list.name" :aria-label="list.name" @click="selectListUnlessDragged(list)"><i/><span>{{list.name}}</span></button><span class="row-actions"><button aria-label="打开清单操作" :aria-expanded="sidebarAction?.item.id===list.id" @click="openSidebarAction('lists',list)"><Ellipsis/></button></span></div>
<div v-for="list in lists.filter(l=>!l.folder_id&&!l.is_inbox)" :key="list.id" :data-list-id="list.id" class="list-row" :class="{active:activeView==='tasks'&&activeList===list.id,'list-dragging':listDrag?.id===list.id,'list-reorder-target':listReorderTarget===list.id&&listDrag?.id!==list.id}" :style="{'--list-drag-y':`${listDrag?.id===list.id?listDrag.offsetY:0}px`}" @pointermove="moveListDrag(list,$event)" @pointerup="finishListDrag(list,$event)" @pointercancel="cancelListDrag"><button class="list-drag-handle" aria-label="拖动清单排序或移动到文件夹" title="长按拖动清单" @pointerdown.stop="startListHandlePress(list,$event)" @pointermove.stop="moveListHandle(list,$event)" @pointerup.stop="finishListDrag(list,$event)" @pointercancel.stop="cancelListDrag"><GripVertical/></button><button class="list-row-main draggable-list-row-main" :title="list.name" :aria-label="list.name" @click="selectListUnlessDragged(list)"><span>{{list.name}}</span></button><span class="row-actions"><button aria-label="打开清单操作" :aria-expanded="sidebarAction?.item.id===list.id" @click="openSidebarAction('lists',list)"><Ellipsis/></button></span></div>
<div class="archived-lists">
<button ref="archivedListsToggle" class="archived-lists-toggle" :class="{ empty: archivedLists.length === 0 }" :aria-expanded="archivedLists.length > 0 && archivedListsExpanded" aria-controls="archived-task-lists" :disabled="archivedLists.length === 0" @click="toggleArchivedLists"><ChevronRight :class="{ expanded: archivedListsExpanded }"/><span>已归档 {{ archivedLists.length }}</span></button>
<div id="archived-task-lists" v-show="archivedListsExpanded" class="archived-list-items">
File diff suppressed because one or more lines are too long
+48 -4
View File
@@ -192,7 +192,7 @@ describe('solid cream material system', () => {
it('uses opaque cream surfaces for the shell, navigation, overlays, and feedback', () => {
expect(css).toContain('.shell{background:var(--surface-base)}')
expect(css).toContain('.sidebar{background:var(--surface-canvas)')
expect(css).toContain('.sidebar{background:#f7efe3')
expect(css).toContain('main{background:var(--surface-base)}')
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)')
@@ -1241,8 +1241,8 @@ describe('desktop task detail disclosure', () => {
expect(app).toContain('@click="closeTaskDetail"')
expect(app).toContain('function closeTaskDetail()')
expect(app).not.toContain('<div v-else class="paper">')
expect(css).toContain('.shell{height:100vh;display:grid;grid-template-columns:238px minmax(430px,1fr) 0;')
expect(css).toContain('.shell.detail-open{grid-template-columns:238px minmax(430px,1fr) 350px}')
expect(css).toContain('.shell{height:100vh;display:grid;grid-template-columns:236px minmax(430px,1fr) 0;')
expect(css).toContain('.shell.detail-open{grid-template-columns:236px minmax(430px,1fr) 350px}')
expect(css).toContain('.shell.sidebar-collapsed.detail-open{grid-template-columns:0 minmax(430px,1fr) 350px}')
expect(css).toContain('transition:grid-template-columns .22s ease')
})
@@ -1283,7 +1283,7 @@ describe('sidebar information hierarchy', () => {
it('uses a compact brand row, restrained active marker and a single list creation trigger', () => {
expect(app).toContain('class="mini-icon list-create-trigger" aria-label="新建清单或文件夹"')
expect(app).not.toContain('aria-label="新建文件夹" @click="createFolder"')
expect(css).toContain('.brand-row{height:64px;')
expect(css).toContain('.brand-row{height:68px;')
expect(app).toContain('class="brand brand-lockup"')
expect(app).toContain('class="brand small brand-lockup"')
expect(app).toContain('class="center loading-brand"')
@@ -1330,6 +1330,37 @@ describe('sidebar information hierarchy', () => {
})
})
describe('quiet index sidebar parity', () => {
it('matches the approved desktop and mobile sidebar geometry and material', () => {
expect(css).toContain('.shell{height:100vh;display:grid;grid-template-columns:236px minmax(430px,1fr) 0;')
expect(css).toContain('.shell.detail-open{grid-template-columns:236px minmax(430px,1fr) 350px}')
expect(css).toContain('.sidebar{border-right:1px solid #e4d5c3;background:#f7efe3;')
expect(css).toContain('.brand-row{height:68px;')
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('@media(max-width:1050px) and (min-width:931px){.shell.detail-open{grid-template-columns:236px minmax(400px,1fr) minmax(280px,30vw)}')
expect(css).not.toContain('grid-template-columns:220px')
expect(css).toContain('@media(max-width:930px){.shell')
expect(css).toContain('left:0;width:236px;max-width:86vw;')
})
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::before,.list-row.active::before,.sidebar-management button.active::before{')
expect(css).toContain('width:3px;height:18px;')
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('.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}')
})
it('removes decorative markers from both ordinary list row renderers', () => {
expect(app.match(/class="list-row-main draggable-list-row-main"/g)).toHaveLength(2)
expect(app).not.toMatch(/class="list-row-main draggable-list-row-main"[^>]*><i\s*\/>/)
expect(css).not.toContain('.list-row i')
})
})
describe('sidebar layout', () => {
it('switches to the compact layout before the desktop grid can be clipped', () => {
expect(css).toContain('@media(max-width:930px){.shell')
@@ -1365,6 +1396,19 @@ describe('sidebar layout', () => {
expect(css).toContain('.row-actions{display:flex;position:absolute;right:0;opacity:0;pointer-events:none;')
})
it('keeps folder touch targets at least 44px high', () => {
const folderButtonRule = css.match(/\.folder-row>button,\.list-row-main\{([^}]*)\}/)?.[1]
expect(folderButtonRule).toContain('min-height:44px')
})
it('keeps the final mobile row-action touch target at least 44px high', () => {
const rowActionRules = [...css.matchAll(/\.row-actions button\{([^}]*)\}/g)]
const finalRowActionRule = rowActionRules.at(-1)?.[1]
expect(finalRowActionRule).toContain('min-height:44px')
})
it('exposes full list and folder names to assistive labels and native tooltips', () => {
expect(app).toContain(':title="folder.name" :aria-label="folder.name"')
expect(app).toContain(':title="list.name" :aria-label="list.name"')