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.
This commit is contained in:
+2
-1
@@ -451,8 +451,9 @@
|
|||||||
|
|
||||||
应用级通知统一进入全局通知视口,页面不得自行复制通知卡片或在内容流中长期堆放短期消息。
|
应用级通知统一进入全局通知视口,页面不得自行复制通知卡片或在内容流中长期堆放短期消息。
|
||||||
|
|
||||||
|
- 右上角通知使用紧凑的中性表面、弱边框和轻阴影,最大宽度为 `360px`,窄窗口保留两侧各 `16px`。状态色仅用于图标,不铺满卡片;正文直接呈现,通用状态标题仅供屏幕阅读器读取。图标、正文与关闭按钮统一垂直居中,不使用各自的上下偏移。
|
||||||
- 异步操作成功、无需立即处理的信息,以及不属于某个字段的异步失败,使用应用级 `success`、`info` 或 `error` 通知。
|
- 异步操作成功、无需立即处理的信息,以及不属于某个字段的异步失败,使用应用级 `success`、`info` 或 `error` 通知。
|
||||||
- 成功和信息通知默认在约 4.5 秒后自动消失;错误通知保持可见,直到用户关闭或同一去重键的更新替换它。
|
- 成功和信息通知默认在约 2 秒后自动消失;错误通知保持可见,直到用户关闭或同一去重键的更新替换它。
|
||||||
- 通知出现不抢焦点;Modal 打开时,通知关闭按钮纳入当前 Modal 的 Tab 循环。聚焦通知时
|
- 通知出现不抢焦点;Modal 打开时,通知关闭按钮纳入当前 Modal 的 Tab 循环。聚焦通知时
|
||||||
Escape 返回原控件或当前 Modal 的可用控件,不关闭 Modal;关闭通知后同样恢复焦点。
|
Escape 返回原控件或当前 Modal 的可用控件,不关闭 Modal;关闭通知后同样恢复焦点。
|
||||||
打开、关闭或切换 Modal 不重建通知,不重置自动消失计时。
|
打开、关闭或切换 Modal 不重建通知,不重置自动消失计时。
|
||||||
|
|||||||
@@ -10030,7 +10030,11 @@ describe("App", () => {
|
|||||||
"当前对话已切换到 Continue · 默认模型",
|
"当前对话已切换到 Continue · 默认模型",
|
||||||
);
|
);
|
||||||
|
|
||||||
act(() => vi.advanceTimersByTime(4_500));
|
act(() => vi.advanceTimersByTime(1_999));
|
||||||
|
expect(screen.getByRole("status")).toHaveTextContent(
|
||||||
|
"当前对话已切换到 Continue · 默认模型",
|
||||||
|
);
|
||||||
|
act(() => vi.advanceTimersByTime(1));
|
||||||
expect(
|
expect(
|
||||||
screen.queryByText("当前对话已切换到 Continue · 默认模型"),
|
screen.queryByText("当前对话已切换到 Continue · 默认模型"),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
|
|||||||
@@ -450,7 +450,7 @@ function AppNotificationItem({
|
|||||||
}
|
}
|
||||||
const timeout = window.setTimeout(() => {
|
const timeout = window.setTimeout(() => {
|
||||||
dispatch({ dismiss: notification.id });
|
dispatch({ dismiss: notification.id });
|
||||||
}, 4_500);
|
}, 2_000);
|
||||||
return () => window.clearTimeout(timeout);
|
return () => window.clearTimeout(timeout);
|
||||||
}, [dispatch, notification.id, notification.revision, notification.tone]);
|
}, [dispatch, notification.id, notification.revision, notification.tone]);
|
||||||
|
|
||||||
|
|||||||
+22
-25
@@ -12479,7 +12479,7 @@ details.settings-section > :not(summary) + :not(summary) {
|
|||||||
top: 54px;
|
top: 54px;
|
||||||
right: var(--space-4);
|
right: var(--space-4);
|
||||||
display: grid;
|
display: grid;
|
||||||
width: min(420px, calc(100vw - 32px));
|
width: min(360px, calc(100vw - 32px));
|
||||||
max-height: calc(100vh - 70px);
|
max-height: calc(100vh - 70px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
@@ -12489,30 +12489,20 @@ details.settings-section > :not(summary) + :not(summary) {
|
|||||||
display: grid;
|
display: grid;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: var(--space-3) var(--space-4);
|
padding: var(--space-3);
|
||||||
border: 1px solid var(--border-default);
|
border: 1px solid var(--border-subtle);
|
||||||
border-radius: var(--radius-card);
|
border-radius: var(--radius-control);
|
||||||
background: var(--surface-raised);
|
background: var(--surface-raised);
|
||||||
box-shadow: var(--shadow-dialog);
|
box-shadow: var(--shadow-card);
|
||||||
color: var(--text-secondary);
|
color: var(--text-primary);
|
||||||
font-size: var(--font-body);
|
font-size: var(--font-body);
|
||||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
line-height: 1.6;
|
||||||
gap: var(--space-3);
|
grid-template-columns: 20px minmax(0, 1fr) 28px;
|
||||||
|
gap: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-notification--success {
|
.app-notification > svg {
|
||||||
border-color: var(--success);
|
justify-self: center;
|
||||||
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--success > svg {
|
.app-notification--success > svg {
|
||||||
@@ -12528,17 +12518,21 @@ details.settings-section > :not(summary) + :not(summary) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-notification > div {
|
.app-notification > div {
|
||||||
display: grid;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
gap: var(--space-1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Keep the status label available to screen readers without a repeated heading. */
|
||||||
.app-notification strong {
|
.app-notification strong {
|
||||||
color: var(--text-primary);
|
position: absolute;
|
||||||
font-size: var(--font-body);
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip-path: inset(50%);
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-notification span {
|
.app-notification span {
|
||||||
|
display: block;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12547,11 +12541,14 @@ details.settings-section > :not(summary) + :not(summary) {
|
|||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
flex: 0 0 28px;
|
flex: 0 0 28px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
border-radius: var(--radius-control);
|
border-radius: var(--radius-control);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: background var(--motion-fast) ease-out, color var(--motion-fast) ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-notification button:hover {
|
.app-notification button:hover {
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ app.whenReady().then(async () => {
|
|||||||
const before = await js('document.activeElement.className')
|
const before = await js('document.activeElement.className')
|
||||||
await notify('success')
|
await notify('success')
|
||||||
assert.equal(await js('document.activeElement.className'), before, 'Notification stole focus')
|
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 screenshot(`${theme}-${width}-notification`)
|
||||||
await click('.app-notification--error button')
|
await click('.app-notification--error button')
|
||||||
await click('.app-notification--success button')
|
await click('.app-notification--success button')
|
||||||
|
|||||||
Reference in New Issue
Block a user