From be84534e776e81d5bf65c30eb18a70c0b4d98452 Mon Sep 17 00:00:00 2001 From: mesalogo Date: Wed, 23 Sep 2026 11:51:17 +0800 Subject: [PATCH] fix: simplify notifications and shorten display time Use compact neutral notification cards with vertically aligned icons, text, and close controls. Dismiss success and info messages after two seconds; keep errors visible until dismissed. Release note: Notifications now use a cleaner compact layout, and success and info messages disappear after two seconds. --- UI-DESIGN.md | 3 +- src/renderer/src/App.test.tsx | 6 ++- src/renderer/src/App.tsx | 2 +- src/renderer/src/styles.css | 47 +++++++++++------------ tests/support/overlay-layering-driver.mjs | 6 +++ 5 files changed, 36 insertions(+), 28 deletions(-) diff --git a/UI-DESIGN.md b/UI-DESIGN.md index aadba5a2..9364dcfd 100644 --- a/UI-DESIGN.md +++ b/UI-DESIGN.md @@ -451,8 +451,9 @@ 应用级通知统一进入全局通知视口,页面不得自行复制通知卡片或在内容流中长期堆放短期消息。 +- 右上角通知使用紧凑的中性表面、弱边框和轻阴影,最大宽度为 `360px`,窄窗口保留两侧各 `16px`。状态色仅用于图标,不铺满卡片;正文直接呈现,通用状态标题仅供屏幕阅读器读取。图标、正文与关闭按钮统一垂直居中,不使用各自的上下偏移。 - 异步操作成功、无需立即处理的信息,以及不属于某个字段的异步失败,使用应用级 `success`、`info` 或 `error` 通知。 -- 成功和信息通知默认在约 4.5 秒后自动消失;错误通知保持可见,直到用户关闭或同一去重键的更新替换它。 +- 成功和信息通知默认在约 2 秒后自动消失;错误通知保持可见,直到用户关闭或同一去重键的更新替换它。 - 通知出现不抢焦点;Modal 打开时,通知关闭按钮纳入当前 Modal 的 Tab 循环。聚焦通知时 Escape 返回原控件或当前 Modal 的可用控件,不关闭 Modal;关闭通知后同样恢复焦点。 打开、关闭或切换 Modal 不重建通知,不重置自动消失计时。 diff --git a/src/renderer/src/App.test.tsx b/src/renderer/src/App.test.tsx index 94af7abd..4f703b30 100644 --- a/src/renderer/src/App.test.tsx +++ b/src/renderer/src/App.test.tsx @@ -10030,7 +10030,11 @@ describe("App", () => { "当前对话已切换到 Continue · 默认模型", ); - act(() => vi.advanceTimersByTime(4_500)); + act(() => vi.advanceTimersByTime(1_999)); + expect(screen.getByRole("status")).toHaveTextContent( + "当前对话已切换到 Continue · 默认模型", + ); + act(() => vi.advanceTimersByTime(1)); expect( screen.queryByText("当前对话已切换到 Continue · 默认模型"), ).not.toBeInTheDocument(); diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index abe2558f..54988039 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -450,7 +450,7 @@ function AppNotificationItem({ } const timeout = window.setTimeout(() => { dispatch({ dismiss: notification.id }); - }, 4_500); + }, 2_000); return () => window.clearTimeout(timeout); }, [dispatch, notification.id, notification.revision, notification.tone]); diff --git a/src/renderer/src/styles.css b/src/renderer/src/styles.css index d9afb55d..4f83f64d 100644 --- a/src/renderer/src/styles.css +++ b/src/renderer/src/styles.css @@ -12479,7 +12479,7 @@ details.settings-section > :not(summary) + :not(summary) { top: 54px; right: var(--space-4); display: grid; - width: min(420px, calc(100vw - 32px)); + width: min(360px, calc(100vw - 32px)); max-height: calc(100vh - 70px); overflow-y: auto; gap: var(--space-2); @@ -12489,30 +12489,20 @@ details.settings-section > :not(summary) + :not(summary) { display: grid; min-width: 0; align-items: center; - padding: var(--space-3) var(--space-4); - border: 1px solid var(--border-default); - border-radius: var(--radius-card); + padding: var(--space-3); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-control); background: var(--surface-raised); - box-shadow: var(--shadow-dialog); - color: var(--text-secondary); + box-shadow: var(--shadow-card); + color: var(--text-primary); font-size: var(--font-body); - grid-template-columns: auto minmax(0, 1fr) auto; - gap: var(--space-3); + line-height: 1.6; + grid-template-columns: 20px minmax(0, 1fr) 28px; + gap: var(--space-2); } -.app-notification--success { - border-color: var(--success); - background: var(--success-subtle); -} - -.app-notification--info { - border-color: var(--accent); - background: var(--accent-subtle); -} - -.app-notification--error { - border-color: var(--danger-border); - background: var(--danger-subtle); +.app-notification > svg { + justify-self: center; } .app-notification--success > svg { @@ -12528,17 +12518,21 @@ details.settings-section > :not(summary) + :not(summary) { } .app-notification > div { - display: grid; min-width: 0; - gap: var(--space-1); } +/* Keep the status label available to screen readers without a repeated heading. */ .app-notification strong { - color: var(--text-primary); - font-size: var(--font-body); + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; } .app-notification span { + display: block; overflow-wrap: anywhere; } @@ -12547,11 +12541,14 @@ details.settings-section > :not(summary) + :not(summary) { width: 28px; height: 28px; flex: 0 0 28px; + padding: 0; + border: 0; place-items: center; border-radius: var(--radius-control); background: transparent; color: var(--text-muted); cursor: pointer; + transition: background var(--motion-fast) ease-out, color var(--motion-fast) ease-out; } .app-notification button:hover { diff --git a/tests/support/overlay-layering-driver.mjs b/tests/support/overlay-layering-driver.mjs index 35ef77ee..a3073fd5 100644 --- a/tests/support/overlay-layering-driver.mjs +++ b/tests/support/overlay-layering-driver.mjs @@ -65,6 +65,12 @@ app.whenReady().then(async () => { const before = await js('document.activeElement.className') await notify('success') assert.equal(await js('document.activeElement.className'), before, 'Notification stole focus') + assert(await js(`Array.from(document.querySelectorAll('.app-notification')).every(card => { + const center = element => { const r = element.getBoundingClientRect(); return r.top + r.height / 2 } + const middle = center(card) + return [card.querySelector(':scope > svg'), card.querySelector('span'), card.querySelector('button svg')] + .every(element => Math.abs(center(element) - middle) <= 1) + })`), 'Notification icon, text and close icon must share the vertical center') await screenshot(`${theme}-${width}-notification`) await click('.app-notification--error button') await click('.app-notification--success button')