diff --git a/packages/ui/stories/toast.stories.tsx b/packages/ui/stories/toast.stories.tsx
index 58ebeaa7f2..90916e5b4d 100644
--- a/packages/ui/stories/toast.stories.tsx
+++ b/packages/ui/stories/toast.stories.tsx
@@ -111,3 +111,130 @@ export const ConfirmQueued: Story = {
await expect(opener).toHaveFocus();
},
};
+
+// The diagnostics affordance the provider wires onto every error toast — and
+// the branch where opening it also fails, so the provider raises its own
+// failure toast instead of swallowing the rejection.
+const DIAGNOSTICS_ERROR_ACTION = {
+ label: '查看详情',
+ failureTitle: '无法打开诊断',
+ failureDescription: '日志服务未响应,请稍后重试。',
+ onClick: async () => {
+ throw new Error('diagnostics unavailable');
+ },
+};
+
+// Real trigger: any failed operation surfaced through `toast.error` (a failed
+// save, an offline sync, an unauthorised request — this surface has one error
+// affordance, so they all share its shape). A bare `error` variant never
+// exercises the action button or its own failure path.
+export const ErrorWithDiagnostics: Story = {
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ render: () => {
+ const toast = useToast();
+ useEffect(() => {
+ toast.toast({
+ title: '保存失败',
+ description: '网络中断,更改未同步。',
+ variant: 'error',
+ duration: 0,
+ });
+ }, [toast]);
+ return
;
+ },
+ play: async () => {
+ const page = within(document.body);
+ // The error toast carries the provider's diagnostics action…
+ const diagnostics = await page.findByRole('button', { name: '查看详情' });
+ await userEvent.click(diagnostics);
+ // …and when that action itself fails, the provider surfaces its own
+ // failure toast rather than dropping the rejection on the floor.
+ await page.findByText('无法打开诊断');
+ },
+};
+
+function DestructiveConfirmExample() {
+ const toast = useToast();
+ return (
+
+
+ );
+}
+
+// Real trigger: an irreversible action (delete a project / task). The confirm
+// button wears the destructive variant and initial focus rests on cancel, so
+// the dangerous path is never the default.
+export const DestructiveConfirm: Story = {
+ render: () => ,
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const page = within(document.body);
+ await userEvent.click(canvas.getByRole('button', { name: '删除项目' }));
+ const dialog = await page.findByRole('alertdialog', { name: '删除项目?' });
+ await expect(within(dialog).getByRole('button', { name: '删除' })).toBeInTheDocument();
+ // The least-destructive choice holds initial focus.
+ await expect(within(dialog).getByRole('button', { name: '取消' })).toHaveFocus();
+ },
+};
+
+const LONG_TOAST_TITLE =
+ '同步到远程工作区时发生了一个需要你注意的、标题本身就相当冗长的问题';
+const LONG_TOAST_DESCRIPTION =
+ '远程主机在多次重试后仍未确认写入:连接在握手阶段被重置,随后的退避重试也接连超时。' +
+ '本地更改已安全保留,你可以稍后重新同步;这段说明刻意写得很长,用来检验 toast 卡片在多行文本下是否仍然清晰,而不会把操作区挤出可视范围。';
+
+// Very long title and body: the card must wrap legibly rather than clip text
+// or shove the layout off-screen.
+export const LongContent: Story = {
+ render: () => {
+ const toast = useToast();
+ useEffect(() => {
+ toast.toast({
+ title: LONG_TOAST_TITLE,
+ description: LONG_TOAST_DESCRIPTION,
+ variant: 'warning',
+ duration: 0,
+ });
+ }, [toast]);
+ return ;
+ },
+};
+
+// Many notifications at once — a burst of background results stacking up.
+// Exercises the stack's density and whatever cap or scroll the layer applies,
+// which a single seeded toast never shows.
+export const ManyStacked: Story = {
+ render: () => {
+ const toast = useToast();
+ useEffect(() => {
+ for (let i = 1; i <= 8; i += 1) {
+ toast.toast({
+ title: `后台任务 ${i} 已完成`,
+ description: `第 ${i} 条通知的说明文字。`,
+ variant: VARIANTS[i % VARIANTS.length]!,
+ duration: 0,
+ });
+ }
+ }, [toast]);
+ return ;
+ },
+};