fix: keep note capture actions compact and right-aligned
Remove initial generic validation instructions, show size errors beside their fields, and expose capture scope through contextual help. Keep save errors beside actions where space allows and preserve content-sized Save buttons in narrow note workspaces. Release note: Note capture now uses a compact action bar with right-aligned buttons, on-demand scope help, and field-specific validation. Narrow workspaces no longer stretch the Save button.
This commit is contained in:
@@ -55,6 +55,13 @@ describe('compact notes', () => {
|
||||
expect(screen.queryByText('Text to add')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Global')).not.toBeInTheDocument()
|
||||
expect(create).not.toHaveBeenCalled()
|
||||
expect(document.querySelector('.magic-note-panel__footer p')).toBeNull()
|
||||
expect(screen.getByRole('button', { name: 'Add to note' })).toBeDisabled()
|
||||
expect(screen.queryByText(/excluding reasoning/)).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'What gets saved' }))
|
||||
expect(screen.getByRole('tooltip')).toHaveTextContent('excluding reasoning')
|
||||
fireEvent.keyDown(window, { key: 'Escape' })
|
||||
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'New note' }))
|
||||
expect(screen.getByLabelText('Note title')).toHaveValue('Discussion A')
|
||||
expect(document.getElementById('compact-note-validation')).not.toBeInTheDocument()
|
||||
@@ -162,11 +169,28 @@ describe('compact notes', () => {
|
||||
expect(preview).toHaveValue(oversized)
|
||||
expect(screen.getByRole('button', { name: 'Add to note' })).toBeDisabled()
|
||||
expect(screen.getByText(/Each entry supports up to 500 KB/)).toBeVisible()
|
||||
expect(preview).toHaveAccessibleDescription(/Each entry supports up to 500 KB/)
|
||||
expect(document.querySelector('.magic-note-panel__footer p')).toBeNull()
|
||||
fireEvent.change(preview, { target: { value: 'Within capacity' } })
|
||||
expect(screen.getByRole('button', { name: 'Add to note' })).toBeEnabled()
|
||||
expect(create).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps new-note validation beside the title without initial footer instructions', async () => {
|
||||
render(<Harness />)
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'New note' }))
|
||||
const title = screen.getByRole('textbox', { name: 'Note title' })
|
||||
expect(document.querySelector('.magic-note-panel__footer p')).toBeNull()
|
||||
expect(screen.getByRole('button', { name: 'Create note' })).toBeDisabled()
|
||||
fireEvent.change(title, { target: { value: 'x'.repeat(101) } })
|
||||
expect(title).toHaveAccessibleDescription('Use a title of up to 100 characters.')
|
||||
expect(title).toHaveAttribute('aria-invalid', 'true')
|
||||
expect(document.querySelector('.magic-note-panel__footer p')).toBeNull()
|
||||
fireEvent.change(title, { target: { value: 'Valid title' } })
|
||||
expect(screen.queryByText('Use a title of up to 100 characters.')).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Create note' })).toBeEnabled()
|
||||
})
|
||||
|
||||
it('preserves text when the target disappears and requires a new selection', async () => {
|
||||
render(<Harness />)
|
||||
fireEvent.click(screen.getByText('Capture'))
|
||||
|
||||
@@ -7,6 +7,7 @@ import { MagicNoteSource, type OpenMagicNoteSource } from './MagicNoteSource'
|
||||
import type { useMagicNoteDraft } from './use-magic-note-draft'
|
||||
import type { AppNotificationInput } from './notifications'
|
||||
import { SegmentedControl } from './WorkspacePrimitives'
|
||||
import { InlineHelp } from './InlineHelp'
|
||||
import './magic-notes-panel.css'
|
||||
|
||||
const MagicCanvasThumbnail = lazy(async () => ({ default: (await import('./MagicCanvasThumbnail')).MagicCanvasThumbnail }))
|
||||
@@ -149,8 +150,8 @@ export function MagicNotesPanel({ state, active, commentMode, commentFormat = 'c
|
||||
setSelectedNoteId('')
|
||||
requestAnimationFrame(() => document.getElementById('compact-note-search')?.focus())
|
||||
}
|
||||
const validationMessage = tooLong ? t('capture.tooLong') : draft && !valid ? t('capture.required') : undefined
|
||||
const validationId = [validationMessage && 'compact-note-validation', saveError && 'compact-note-save-error'].filter(Boolean).join(' ') || undefined
|
||||
const validationId = tooLong ? 'compact-note-validation' : undefined
|
||||
const titleTooLong = Boolean(draft?.newNote && draft.title.trim().length > 100)
|
||||
const list = (
|
||||
<>
|
||||
<label htmlFor="compact-note-search">{t('capture.search')}</label>
|
||||
@@ -202,13 +203,18 @@ export function MagicNotesPanel({ state, active, commentMode, commentFormat = 'c
|
||||
{draft && !quickDraft ? (
|
||||
<>
|
||||
{draft.source && <div className="magic-note-source">
|
||||
<span>{draft.source.projectName && `${draft.source.projectName} / `}{draft.source.conversationTitle}</span>
|
||||
<p>{t(draft.source.kind === 'message' ? 'capture.messageScope' : 'capture.conversationScope')}</p>
|
||||
<div className="magic-note-panel__source-heading">
|
||||
<span>{draft.source.projectName && `${draft.source.projectName} / `}{draft.source.conversationTitle}</span>
|
||||
<InlineHelp label={t('capture.scopeHelp')}>
|
||||
{t(draft.source.kind === 'message' ? 'capture.messageScope' : 'capture.conversationScope')}
|
||||
</InlineHelp>
|
||||
</div>
|
||||
{draft.incomplete && <p>{t('capture.incomplete')}</p>}
|
||||
</div>}
|
||||
<textarea ref={preview} id="compact-note-text" aria-label={t('capture.content')} value={draft.text} disabled={saving}
|
||||
aria-describedby={validationId} aria-invalid={tooLong || undefined}
|
||||
onChange={event => setDraft({ ...draft, text: event.target.value })} />
|
||||
{tooLong && <p id="compact-note-validation">{t('capture.tooLong')}</p>}
|
||||
<h3>{t('capture.target')}</h3>
|
||||
<SegmentedControl ariaLabel={t('capture.target')} disabled={saving}
|
||||
value={draft.newNote ? 'new' : 'existing'}
|
||||
@@ -218,8 +224,9 @@ export function MagicNotesPanel({ state, active, commentMode, commentFormat = 'c
|
||||
<>
|
||||
<label htmlFor="compact-note-title">{t('capture.title')}</label>
|
||||
<input ref={title} id="compact-note-title" value={draft.title} disabled={saving}
|
||||
aria-describedby={validationId} aria-invalid={draft.title.trim().length > 100 || undefined}
|
||||
aria-describedby={titleTooLong ? 'compact-note-title-error' : undefined} aria-invalid={titleTooLong || undefined}
|
||||
onChange={event => setDraft({ ...draft, title: event.target.value })} />
|
||||
{titleTooLong && <p id="compact-note-title-error">{t('capture.titleTooLong')}</p>}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -264,14 +271,16 @@ export function MagicNotesPanel({ state, active, commentMode, commentFormat = 'c
|
||||
text: event.target.value, initialText: '', title: '', initialTitle: '', targetId: currentDetail.id, newNote: false
|
||||
})} />
|
||||
</>}
|
||||
{validationMessage && <p id="compact-note-validation">{validationMessage}</p>}
|
||||
{saveError && <p role="alert" id="compact-note-save-error">{saveError}</p>}
|
||||
<div className="magic-note-panel__actions">
|
||||
{draft && <button className="secondary-button" type="button" disabled={saving}
|
||||
onClick={() => void cancel()}>{t('actions.cancel')}</button>}
|
||||
<button className="primary-button" type="button" disabled={saving || !valid} onClick={() => void save()}>
|
||||
{t(saving ? 'capture.saving' : draft?.newNote && !draft.text.trim() && !draft.source ? 'actions.createNote' : 'capture.add')}
|
||||
</button>
|
||||
{quickDraft && tooLong && <p id="compact-note-validation">{t('capture.tooLong')}</p>}
|
||||
<div className="magic-note-panel__submit">
|
||||
{saveError && <p role="alert" id="compact-note-save-error">{saveError}</p>}
|
||||
<div className="magic-note-panel__actions">
|
||||
{draft && <button className="secondary-button" type="button" disabled={saving}
|
||||
onClick={() => void cancel()}>{t('actions.cancel')}</button>}
|
||||
<button className="primary-button" type="button" disabled={saving || !valid} onClick={() => void save()}>
|
||||
{t(saving ? 'capture.saving' : draft?.newNote && !draft.text.trim() && !draft.source ? 'actions.createNote' : 'capture.add')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)}
|
||||
|
||||
@@ -13,7 +13,7 @@ export const magicNotes = {
|
||||
conversationScope: 'Saves all user and assistant text at capture time, including earlier history, excluding reasoning, tool logs, images, and attachment files.',
|
||||
incomplete: 'This reply is incomplete. The preview includes its available text.', wait: 'Wait for the reply to finish or stop generation before saving.',
|
||||
tooLong: 'Each entry supports up to 500 KB of text. Shorten the content before saving.',
|
||||
required: 'Enter text and choose a note, or enter a new note title (up to 100 characters).',
|
||||
scopeHelp: 'What gets saved', titleTooLong: 'Use a title of up to 100 characters.',
|
||||
saving: 'Saving...', deletedTarget: 'The target note was deleted. Choose another note; your draft is preserved.',
|
||||
user: 'User', assistant: 'Assistant', loading: 'Loading the full conversation...'
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ export const magicNotes = {
|
||||
conversationScope: '保存范围:触发时完整对话的用户与助手文字正文,不含推理、工具日志、图片和附件文件。',
|
||||
incomplete: '这条回答尚未完成,预览保留已有正文。', wait: '请等待回答完成或停止生成后再保存。',
|
||||
tooLong: '每条记录的文字不能超过 500 KB,请缩减内容后保存。',
|
||||
required: '请输入正文并选择目标笔记,或填写新笔记标题(最多 100 字)。',
|
||||
scopeHelp: '保存内容说明', titleTooLong: '笔记标题最多 100 字。',
|
||||
saving: '正在保存…', deletedTarget: '目标笔记已删除,请重新选择;草稿已保留。',
|
||||
user: '用户', assistant: '助手', loading: '正在读取完整对话…'
|
||||
},
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
.magic-note-panel__preview { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 180px; overflow: auto; }
|
||||
.magic-note-panel__footer { border-top: 1px solid var(--border-subtle); border-bottom: 0; }
|
||||
.magic-note-panel__footer p { margin: 0 0 var(--space-2); overflow-wrap: anywhere; }
|
||||
.magic-note-panel__source-heading { display: flex; align-items: center; gap: var(--space-2); }
|
||||
.magic-note-panel__source-heading > span { min-width: 0; }
|
||||
.magic-note-panel__submit { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
|
||||
.magic-note-panel__submit > p { flex: 1 1 200px; min-width: 0; margin: 0; }
|
||||
.magic-note-panel__submit > .magic-note-panel__actions { margin-left: auto; }
|
||||
.assistant-sidebar__body.assistant-sidebar__body--notes { overflow: hidden; padding: 0; display: flex; flex-direction: column; }
|
||||
.assistant-sidebar__body--notes > .magic-note-panel { flex: 1; }
|
||||
.magic-note-panel .magic-note-panel__footer textarea { min-height: 80px; max-height: 180px; }
|
||||
|
||||
@@ -2467,11 +2467,6 @@
|
||||
}
|
||||
|
||||
@container magic-notes-page (max-width: 720px) {
|
||||
.magic-note-composer > footer {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.magic-note-entry__delete {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user