fix: refine Magic Notes editing layout

The Magic Notes editor had ambiguous font-size labels, cramped pane spacing, and a fixed short writing area. The toolbar now groups controls and uses numeric sizes, the pane and empty states are balanced, and the editor can be resized vertically from a taller default.

Release note: 优化魔法笔记编辑体验:字号改为数字、工具栏与分栏布局更清晰,输入区域支持纵向拖拽并默认显示更多内容。
This commit is contained in:
mesalogo
2026-08-17 10:33:19 +08:00
parent ddcc6dd1d9
commit f5ee9b2198
7 changed files with 258 additions and 145 deletions
+12 -2
View File
@@ -4,8 +4,8 @@ import type { MagicNoteRichContent } from '../../shared/magic-notes-contracts'
import { MagicNoteEditor } from './MagicNoteEditor'
describe('MagicNoteEditor', () => {
it('exposes font size, text color, and attachment controls', () => {
render(
it('exposes numeric font sizes, text color, and attachment controls', () => {
const { container } = render(
<MagicNoteEditor
ariaLabel="笔记正文"
onChange={vi.fn()}
@@ -13,7 +13,17 @@ describe('MagicNoteEditor', () => {
/>
)
expect(
screen.getByRole('toolbar', { name: '笔记格式工具栏' })
).toBeInTheDocument()
expect(container.querySelectorAll('.ql-formats')).toHaveLength(5)
expect(screen.getByLabelText('字体大小')).toBeInTheDocument()
expect(
Array.from(
container.querySelectorAll('.ql-size .ql-picker-item'),
(item) => item.getAttribute('data-label')
)
).toEqual(['12', '14', '18', '24'])
expect(screen.getByLabelText('字体颜色')).toBeInTheDocument()
expect(
screen.getByRole('button', { name: '上传视频或附件' })
+16 -1
View File
@@ -454,7 +454,13 @@ export function MagicNoteEditor({
}
}}
>
<div ref={toolbarRef} className="magic-note-editor__toolbar">
<div
ref={toolbarRef}
aria-label={t('editor.toolbarLabel')}
className="magic-note-editor__toolbar"
role="toolbar"
>
<span className="ql-formats">
<select
aria-label={t('editor.paragraphStyle')}
className="ql-header"
@@ -475,6 +481,8 @@ export function MagicNoteEditor({
<option value="large">{t('editor.fontSizeLarge')}</option>
<option value="huge">{t('editor.fontSizeHuge')}</option>
</select>
</span>
<span className="ql-formats">
<select
aria-label={t('editor.textColor')}
className="ql-color"
@@ -500,6 +508,8 @@ export function MagicNoteEditor({
className="ql-strike"
type="button"
/>
</span>
<span className="ql-formats">
<button
aria-label={t('editor.todoList')}
className="ql-list"
@@ -518,6 +528,8 @@ export function MagicNoteEditor({
type="button"
value="ordered"
/>
</span>
<span className="ql-formats">
<button
aria-label={t('editor.blockquote')}
className="ql-blockquote"
@@ -541,6 +553,8 @@ export function MagicNoteEditor({
>
<Paperclip aria-hidden="true" size={16} />
</button>
</span>
<span className="ql-formats">
<button
aria-label={t('editor.undo')}
type="button"
@@ -555,6 +569,7 @@ export function MagicNoteEditor({
>
</button>
</span>
</div>
<div ref={editorRef} className="magic-note-editor__content" />
<input
+19 -3
View File
@@ -377,7 +377,7 @@ describe('MagicNotesWorkspace', () => {
).toBeInTheDocument()
})
it('keeps history editing contained and uses standard delete buttons', async () => {
it('keeps history editing contained and de-emphasizes note deletion', async () => {
const { container } = render(
<MagicNotesWorkspace onNotify={onNotify} />
)
@@ -389,8 +389,11 @@ describe('MagicNotesWorkspace', () => {
const deleteEntry = screen.getByRole('button', {
name: '删除记录'
})
expect(deleteNote).toHaveClass('danger-button', 'danger-button--quiet')
expect(deleteNote).toHaveTextContent('删除笔记')
expect(deleteNote).toHaveClass(
'icon-button',
'magic-note-detail-header__delete'
)
expect(deleteNote).toHaveTextContent('')
expect(deleteEntry).toHaveClass('danger-button', 'danger-button--quiet')
expect(deleteEntry).toHaveTextContent('删除记录')
@@ -445,6 +448,17 @@ describe('MagicNotesWorkspace', () => {
expect(pane).toBeVisible()
})
it('shows a concise empty state before a note has AI comments', async () => {
get.mockResolvedValueOnce(alternateDetail(noteId, detail.title))
render(<MagicNotesWorkspace onNotify={onNotify} />)
expect(await screen.findByText('暂无 AI 评论')).toBeInTheDocument()
expect(
screen.getByText('写完一句并停止输入 5 秒后,评论会显示在这里。')
).toBeInTheDocument()
})
it('resizes the AI comments pane with pointer and keyboard controls', async () => {
const originalInnerWidth = window.innerWidth
Object.defineProperty(window, 'innerWidth', {
@@ -466,6 +480,8 @@ describe('MagicNotesWorkspace', () => {
releasePointerCapture: { value: releasePointerCapture }
})
expect(separator).toHaveAttribute('aria-valuenow', '280')
fireEvent.pointerDown(separator, {
button: 0,
clientX: 800,
+26 -16
View File
@@ -62,7 +62,7 @@ type ValidationTarget =
| 'new-entry'
| 'edit-entry'
const defaultAiPaneWidth = 300
const defaultAiPaneWidth = 280
const minimumAiPaneWidth = 240
const maximumAiPaneWidth = 520
const magicNotesListPaneWidth = 220
@@ -1611,12 +1611,12 @@ export function MagicNotesWorkspace({
</button>
<button
aria-label={t('actions.deleteNote')}
className="danger-button danger-button--quiet"
className="icon-button magic-note-detail-header__delete"
title={t('actions.deleteNote')}
type="button"
onClick={() => setDeletingNote(true)}
>
<Trash2 aria-hidden="true" size={14} />
{t('actions.deleteNote')}
</button>
</div>
</header>
@@ -2128,14 +2128,18 @@ export function MagicNotesWorkspace({
) : null}
{libraryView === 'todos' ? (
!selectedTodo ? (
<p className="magic-notes-muted">
{t('comments.selectTodo')}
</p>
<EmptyState
description={t('comments.selectTodo')}
icon={<Bot size={20} />}
title={t('comments.emptyTitle')}
/>
) : selectedTodo.comments.length === 0 ? (
!liveAnalysis && (
<p className="magic-notes-muted">
{t('comments.analyzeTodoHint')}
</p>
<EmptyState
description={t('comments.analyzeTodoHint')}
icon={<Bot size={20} />}
title={t('comments.emptyTitle')}
/>
)
) : (
<div className="magic-notes-ai-feed">
@@ -2150,20 +2154,26 @@ export function MagicNotesWorkspace({
</div>
)
) : !detail ? (
<p className="magic-notes-muted">
{t('comments.selectNote')}
</p>
<EmptyState
description={t('comments.selectNote')}
icon={<Bot size={20} />}
title={t('comments.emptyTitle')}
/>
) : aiEntries.length === 0 &&
draftAnalyses.length === 0 &&
!draftAnalysisRunning &&
!liveAnalysis ? (
<p className="magic-notes-muted">
{commentMode === 'immediate'
<EmptyState
description={
commentMode === 'immediate'
? t('comments.immediateHint')
: commentMode === 'after-save-auto'
? t('comments.autoHint')
: t('comments.manualHint')}
</p>
: t('comments.manualHint')
}
icon={<Bot size={20} />}
title={t('comments.emptyTitle')}
/>
) : (
<div className="magic-notes-ai-feed">
{draftAnalysisRunning && !liveAnalysis && (
@@ -85,8 +85,7 @@ export const magicNotes = {
emptySelectionDescription:
'Select a note on the left, or create a note to start writing.',
newEntryLabel: 'New entry content',
composerImmediateHint:
'Press Enter, then stop typing for 5 seconds for AI to comment on the draft',
composerImmediateHint: 'AI comments after you stop typing for 5 seconds',
composerRichTextHint:
'Supports rich text and pasting or dropping local images, videos, and attachments',
emptyEntries: 'No entries yet. Write the first entry above.',
@@ -122,10 +121,11 @@ export const magicNotes = {
comments: {
paneLabel: 'AI comments',
closePane: 'Close AI comments pane',
emptyTitle: 'No AI comments yet',
directionLabel: 'Comment direction',
directionAriaLabel: 'AI comment direction',
directionHelp:
'Direction changes apply only to the next comment and do not alter existing content. Change the comment format in Settings.',
'Applies only to the next comment. Change the format in Settings.',
directions: {
general: 'General feedback',
expand: 'Expand writing',
@@ -144,7 +144,7 @@ export const magicNotes = {
'Choose “Analyze with AI” in the to-do details to show comments here.',
selectNote: 'Select a note to show AI comments.',
immediateHint:
'Finish a sentence, press Enter, and stop typing for 5 seconds to show comments here.',
'Finish a sentence and stop typing for 5 seconds to show comments here.',
autoHint: 'AI comments automatically after you save an entry.',
manualHint:
'Choose “Analyze with AI” on an entry to show comments here.'
@@ -154,6 +154,7 @@ export const magicNotes = {
aiPaneWidth: 'AI comments pane, {{width}} pixels'
},
editor: {
toolbarLabel: 'Note formatting toolbar',
imageReadFailed: 'Could not read the image',
fileReadFailed: 'Could not read the file',
maxImages: 'Each entry can contain up to {{count}} images',
@@ -173,10 +174,10 @@ export const magicNotes = {
heading3: 'Heading 3',
body: 'Body',
fontSize: 'Font size',
fontSizeSmall: 'Small',
fontSizeNormal: 'Normal',
fontSizeLarge: 'Large',
fontSizeHuge: 'Extra large',
fontSizeSmall: '12',
fontSizeNormal: '14',
fontSizeLarge: '18',
fontSizeHuge: '24',
textColor: 'Text color',
bold: 'Bold',
italic: 'Italic',
@@ -79,7 +79,7 @@ export const magicNotes = {
emptySelectionTitle: '还没有选择笔记',
emptySelectionDescription: '从左侧选择笔记,或新建一篇笔记开始记录。',
newEntryLabel: '新记录内容',
composerImmediateHint: '按回车并停止输入 5 秒后AI 评论当前草稿',
composerImmediateHint: '停止输入 5 秒后自动评论',
composerRichTextHint: '支持富文本,可粘贴或拖入本地图片、视频和附件',
emptyEntries: '还没有记录,在上方写下第一条内容。',
editEntryLabel: '编辑记录内容',
@@ -113,9 +113,10 @@ export const magicNotes = {
comments: {
paneLabel: 'AI 评论',
closePane: '关闭 AI 评论面板',
emptyTitle: '暂无 AI 评论',
directionLabel: '评论方向',
directionAriaLabel: 'AI 评论方向',
directionHelp: '评论方向更改仅用于下一次评论,不会改动已生成的内容;评论形式可在设置中心修改。',
directionHelp: '仅影响下一次评论;评论形式可在设置中心修改。',
directions: {
general: '综合点评',
expand: '扩展写作',
@@ -132,7 +133,7 @@ export const magicNotes = {
selectTodo: '选择待办后显示 AI 评论。',
analyzeTodoHint: '点击待办详情中的“AI 分析”,评论会显示在这里。',
selectNote: '选择笔记后显示 AI 评论。',
immediateHint: '写完一句后按回车,停止输入 5 秒,评论会显示在这里。',
immediateHint: '写完一句停止输入 5 秒,评论会显示在这里。',
autoHint: '保存记录后,AI 会自动评论。',
manualHint: '在记录上点击“AI 分析”,评论会显示在这里。'
},
@@ -141,6 +142,7 @@ export const magicNotes = {
aiPaneWidth: 'AI 评论栏 {{width}} 像素'
},
editor: {
toolbarLabel: '笔记格式工具栏',
imageReadFailed: '图片读取失败',
fileReadFailed: '文件读取失败',
maxImages: '每条记录最多包含 {{count}} 张图片',
@@ -158,10 +160,10 @@ export const magicNotes = {
heading3: '标题 3',
body: '正文',
fontSize: '字体大小',
fontSizeSmall: '小号',
fontSizeNormal: '常规',
fontSizeLarge: '大号',
fontSizeHuge: '特大',
fontSizeSmall: '12',
fontSizeNormal: '14',
fontSizeLarge: '18',
fontSizeHuge: '24',
textColor: '字体颜色',
bold: '粗体',
italic: '斜体',
+64 -5
View File
@@ -526,7 +526,7 @@
container: speech-model-list / inline-size;
grid-template-columns:
220px minmax(300px, 1fr) 9px
var(--magic-notes-ai-width, 300px);
var(--magic-notes-ai-width, 280px);
}
.magic-notes-layout--ai-hidden {
@@ -544,7 +544,11 @@
width: 9px;
min-width: 9px;
padding: 0;
background: var(--surface-subtle);
background: linear-gradient(
to right,
var(--surface-raised) 0 4px,
var(--surface-subtle) 4px 100%
);
cursor: col-resize;
touch-action: none;
}
@@ -872,7 +876,7 @@
}
.magic-notes-stream-pane {
padding: var(--space-6);
padding: var(--space-4) var(--space-6) var(--space-6);
background: var(--surface-raised);
}
@@ -1030,9 +1034,19 @@
.magic-note-detail-header > div {
display: flex;
align-items: center;
gap: var(--space-1);
}
.magic-note-detail-header__delete {
color: var(--text-muted);
}
.magic-note-detail-header__delete:hover {
background: var(--danger-subtle);
color: var(--danger);
}
.magic-note-delete-confirmation {
display: flex;
align-items: center;
@@ -1090,6 +1104,16 @@
gap: var(--space-3);
}
.magic-note-composer > footer > span {
min-width: 0;
}
.magic-note-composer > footer > .primary-button {
min-width: 80px;
flex-shrink: 0;
white-space: nowrap;
}
.magic-note-entry-stream {
display: grid;
gap: var(--space-3);
@@ -1151,10 +1175,28 @@
}
.magic-note-editor__toolbar.ql-toolbar.ql-snow {
display: flex;
align-items: center;
flex-wrap: wrap;
padding: var(--space-2);
border: 0;
border-bottom: 1px solid var(--border-subtle);
background: var(--surface-subtle);
gap: var(--space-1);
}
.magic-note-editor__toolbar.ql-toolbar.ql-snow .ql-formats {
display: inline-flex;
align-items: center;
padding-right: var(--space-1);
border-right: 1px solid var(--border-subtle);
margin: 0;
}
.magic-note-editor__toolbar.ql-toolbar.ql-snow
.ql-formats:last-child {
padding-right: 0;
border-right: 0;
}
.magic-note-editor__toolbar.ql-toolbar button,
@@ -1170,18 +1212,35 @@
}
.magic-note-editor__content.ql-container.ql-snow {
min-height: 132px;
min-height: 178px;
overflow: hidden;
border: 0;
color: var(--text-primary);
font-family: inherit;
font-size: 14px;
resize: vertical;
}
.magic-note-editor__content .ql-editor {
min-height: 132px;
min-height: 178px;
line-height: 1.65;
}
.magic-note-editor .ql-size-small,
.magic-note-content .ql-size-small {
font-size: 12px;
}
.magic-note-editor .ql-size-large,
.magic-note-content .ql-size-large {
font-size: 18px;
}
.magic-note-editor .ql-size-huge,
.magic-note-content .ql-size-huge {
font-size: 24px;
}
.magic-note-editor .ql-editor img,
.magic-note-content .ql-editor img {
display: block;