diff --git a/UI-DESIGN.md b/UI-DESIGN.md index eeb582d3..4ecdb6b9 100644 --- a/UI-DESIGN.md +++ b/UI-DESIGN.md @@ -797,6 +797,8 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证 - 待办 AI 评论保留方向选择、流式输出、重新分析和已有结果;来源加载失败通过应用通知提示,并提供就地重试。从待办打开原笔记后,“返回待办”恢复任务选择、搜索、筛选和列表滚动;宽屏焦点返回任务行,窄屏返回待办详情中的返回按钮。 - 笔记详情主区按新到旧连续展示全部记录。打开笔记即在标题下、历史记录上显示空白输入区,保留文字/画布类型选择和保存操作,标题栏与索引均不提供“新增记录”按钮。新增保存成功后重建空白输入区,保留所选类型;自动分析结束也不转入旧记录编辑。旧记录通过显式编辑入口修改,保存使用同一 ID 和返回的修订号。点击索引或当前笔记内的 AI 来源只按 entry ID 滚动定位,不卸载编辑器、不清草稿、不询问离开。空白输入区离开不提示,返回总览、打开其他笔记或改为编辑另一条记录时仍保护未保存修改。 - 打开笔记后页面一级标题仍为“魔法笔记”;笔记标题只在记录流顶部出现一次,使用 `22px` 的可编辑大标题,悬停显示下划线,聚焦显示强调色下划线,保留失焦保存、Enter 与 Escape。标题上方为吸顶的细工具栏,依次放置收起笔记列表、记录索引、条数与更新时间、AI 评论开关,均为带工具提示和可访问名称的图标按钮。记录流左右留白 `24px`(窄于 `600px` 时 `12px`),正文最大宽度 `880px` 并居中;吸顶工具栏不受此限制,始终铺满记录流宽度,AI 开关贴在右边缘,底部分隔线横贯整栏。 +- 桌面笔记详情的列表标题行、正文工具栏与 AI 评论方向标签行顶部对齐,首行统一使用 `40px` 控件高度并垂直居中;AI 区域不叠加面板和表单的顶部留白。 +- 文字记录的新建与编辑区域铺满卡片内沿,格式工具栏紧接头部,输入区域紧接工具栏;编辑器外层不再增加内边距或圆角。正文保留编辑器内部的文字留白,底部操作栏独立保留内边距与顶部分隔线;画布仍保留 `12px` 内容间距。 - 新建记录输入区空闲时收成一行虚线提示框,隐藏类型切换、格式工具栏和保存栏;聚焦、已有草稿、选择画布或出现校验错误时展开,初始约四行并随内容增高。保存成功或切换笔记后恢复为一行。此规则不作用于旧记录编辑器。 - 已保存记录的 AI 分析、编辑、删除为 `28px` 图标按钮,精细指针设备上悬停或键盘聚焦记录时才显示;删除悬停时使用危险色,确认条保持原样。粗指针设备始终显示。笔记内部列表、记录流、AI 区域和文字编辑区在精细指针设备上使用细原生滚动条,轨道透明;强制颜色与粗指针设备保留系统默认。说明、摘要和输入提示使用 `--font-body` 与 `--text-secondary`,时间和短标签保留 `--font-caption`。 - 左侧记录索引默认收起,只在用户展开后记住展开状态;展开宽度默认 `168px`,可拖动右边界在 `140–320px` 内调整栏宽,缩略图随列宽等比适配。开关位于笔记工具栏,提供 `aria-controls`、展开状态和动作提示;收起后完全移除索引列、分隔条、间隙、边框及轨道,不占宽度,隐藏内容不可聚焦。右侧 AI 评论独立显示、滚动和收起,不与索引共享页签。每条记录一个导航项:画布显示真实首页缩略图及总页数,文字显示摘要,两者均显示类型和时间。两侧分别记住桌面展开状态和宽度,共用原有布局偏好;宽布局分隔条共用样式及 `separator` 语义,方向键每次调整 `16px`,Home、End 跳到当前上下限。两侧宽度按可用空间协调限制,为主区保留至少 `300px`,窗口缩小时不覆盖保存的偏好。内容容器不超过 `800px` 时索引默认隐藏,由同一标题栏按钮开闭标题栏下方固定 `168px` 的抽屉,不显示调宽分隔条;标题栏自身保留控件高度,不保留轨道。选择记录或按 Escape 后关闭,Escape 将焦点返回开关。AI 移至底部,高度不超过可用布局的 40% 且最多 `280px`,不显示分隔条。 diff --git a/src/renderer/src/styles.css b/src/renderer/src/styles.css index 6bc9f2a8..12157f73 100644 --- a/src/renderer/src/styles.css +++ b/src/renderer/src/styles.css @@ -1248,6 +1248,7 @@ } .magic-notes-page { + --magic-notes-heading-height: 40px; color-scheme: light; display: flex; min-width: 0; @@ -1323,6 +1324,14 @@ gap: var(--space-2); } +.magic-notes-workspace--notes .magic-notes-pane-heading { + min-height: var(--magic-notes-heading-height); +} + +.magic-notes-workspace--notes .magic-notes-pane-heading :is(.icon-button, .magic-note-tag-manager-button) { + margin-block: 0; +} + .magic-notes-workspace--notes .magic-notes-card-grid { gap: 2px; margin-inline: calc(-1 * var(--space-1)); @@ -1929,7 +1938,16 @@ } .magic-notes-ai-pane { - padding: var(--space-4); + padding: 0 var(--space-4) var(--space-4); +} + +.magic-notes-ai-pane > .magic-notes-ai-controls { + padding-top: 0; +} + +.magic-notes-ai-pane > .magic-notes-ai-controls .inline-help-label { + min-height: var(--magic-notes-heading-height); + margin-bottom: 0; } .magic-todo-source-entry { @@ -2481,7 +2499,7 @@ z-index: 2; top: 0; display: flex; - min-height: 40px; + min-height: var(--magic-notes-heading-height); align-items: center; margin-inline: calc(-1 * var(--magic-note-stream-gutter, var(--space-4))); padding: var(--space-1) var(--magic-note-stream-gutter, var(--space-4)); @@ -2752,9 +2770,17 @@ .magic-note-entry__editor { min-width: 0; +} + +.magic-note-entry--canvas-editing > .magic-note-entry__editor { padding: var(--space-3); } +.magic-note-entry__editor > .magic-note-editor, +.magic-note-composer > .magic-note-editor { + border-radius: 0; +} + .magic-note-entry__delete { display: flex; align-items: center; @@ -2771,7 +2797,8 @@ } .magic-note-entry__editor-actions { - margin-top: var(--space-2); + padding: var(--space-2) var(--space-3); + border-top: 1px solid var(--border-subtle); } .magic-note-editor { diff --git a/tests/magic-notes-layout.electron.test.ts b/tests/magic-notes-layout.electron.test.ts index a9b00a27..5338ad93 100644 --- a/tests/magic-notes-layout.electron.test.ts +++ b/tests/magic-notes-layout.electron.test.ts @@ -30,6 +30,7 @@ it('resizes record columns with native Electron input and preserves desktop widt const fs = require('node:fs'); const assert = require('node:assert/strict'); app.commandLine.appendSwitch('force-device-scale-factor', '1'); + app.commandLine.appendSwitch('disable-features', 'CalculateNativeWinOcclusion'); app.setPath('userData', ${JSON.stringify(join(directory, 'profile'))}); app.whenReady().then(async () => { const win = new BrowserWindow({ show: true, width: 1280, height: 800, useContentSize: true, @@ -126,6 +127,21 @@ it('resizes record columns with native Electron input and preserves desktop widt assert.equal((await rect(saveButton)).width, 0); assert.equal(await js('document.querySelector(".magic-notes-ai-pane")?.checkVisibility() ?? false'), false, 'AI stays closed for notes without comments'); assert.equal((await rect('.magic-notes-index-pane')).width, 0, 'Record index starts collapsed'); + await click('.magic-note-entry button[aria-label="Edit"]'); + await wait('!!document.querySelector(".magic-note-entry__editor .ql-editor")'); + const entryBox = await rect('.magic-note-entry'); + const entryHeader = await rect('.magic-note-entry > header'); + const editToolbar = await rect('.magic-note-entry__editor .ql-toolbar'); + const editInput = await rect('.magic-note-entry__editor .ql-container'); + for (const box of [editToolbar, editInput]) { + assert(Math.abs(box.x - entryBox.x - 1) <= 1 && Math.abs(box.width - entryBox.width + 2) <= 1, 'Editor fills the card inside its border: ' + JSON.stringify({ box, entryBox })); + } + assert(Math.abs(editToolbar.y - entryHeader.y - entryHeader.height) <= 1, 'Toolbar directly follows the entry header'); + assert(Math.abs(editInput.y - editToolbar.y - editToolbar.height) <= 1, 'Input directly follows the toolbar'); + await screenshot('magic-notes-entry-editor'); + await click('.magic-note-entry__editor-actions .secondary-button'); + await wait('!document.querySelector(".magic-note-entry__editor")'); + await js('document.querySelector(".magic-notes-stream-pane").scrollTop = 0'); const focusComposer = async () => { await js('document.querySelector(' + JSON.stringify(editor) + ').focus()'); await settle(); }; await focusComposer(); const emptyHeight = (await rect(editor)).height; @@ -141,6 +157,14 @@ it('resizes record columns with native Electron input and preserves desktop widt const index = '.magic-notes-index-pane', handle = '.magic-notes-index-resize-handle', stream = '.magic-notes-stream-pane'; await click('#magic-notes-index-toggle'); await click('button[aria-controls="magic-notes-ai-pane"]'); + const listHeading = await rect('#magic-library-panel-notes > .magic-notes-pane-heading'); + const detailToolbar = await rect('.magic-note-detail-toolbar'); + const aiHeading = await rect('.magic-notes-ai-pane .inline-help-label'); + for (const heading of [listHeading, aiHeading]) { + assert(Math.abs(heading.y - detailToolbar.y) <= 1, 'Pane headings share a top edge: ' + JSON.stringify({ heading, detailToolbar })); + assert(Math.abs(heading.height - detailToolbar.height) <= 1, 'Pane headings share a row height: ' + JSON.stringify({ heading, detailToolbar })); + } + await screenshot('magic-notes-aligned-headings'); assert.equal((await rect(index)).width, 168); const firstRecord = await rect('.magic-note-record'); assert.equal(firstRecord.x, (await rect(index)).x, 'Thumbnails align with the left pane edge');