diff --git a/.changeset/toast-alert.md b/.changeset/toast-alert.md
new file mode 100644
index 00000000..5979db04
--- /dev/null
+++ b/.changeset/toast-alert.md
@@ -0,0 +1,5 @@
+---
+"@serverlessworkflow/diagram-editor": minor
+---
+
+add toast alert system through shadcn Sonner
diff --git a/packages/serverless-workflow-diagram-editor/package.json b/packages/serverless-workflow-diagram-editor/package.json
index 98e3422b..8b16b018 100644
--- a/packages/serverless-workflow-diagram-editor/package.json
+++ b/packages/serverless-workflow-diagram-editor/package.json
@@ -49,6 +49,7 @@
"elkjs": "catalog:",
"js-yaml": "catalog:",
"radix-ui": "catalog:",
+ "sonner": "catalog:",
"use-sync-external-store": "catalog:"
},
"devDependencies": {
diff --git a/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.css b/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.css
new file mode 100644
index 00000000..c1896d09
--- /dev/null
+++ b/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.css
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ @reference "../../styles.css";
+
+@layer sonner {
+ /* sonner defaults unlayered, so these overrides need !important */
+ .dec-root [data-sonner-toaster] { --width: 300px !important; }
+ .dec-root [data-sonner-toast] { background: var(--dec-toast-bg) !important; border-radius: 10px !important; border: 1px solid var(--dec-toast-border) !important; }
+ .dec-root [data-sonner-toast][data-type="success"] { border-left: 4px solid var(--dec-toast-success) !important; }
+ .dec-root [data-sonner-toast][data-type="error"] { border-left: 4px solid var(--dec-error-accent) !important; }
+ .dec-root [data-sonner-toast][data-type="warning"] { border-left: 4px solid var(--dec-toast-warning) !important; }
+ .dec-root [data-sonner-toast][data-type="info"] { border-left: 4px solid var(--dec-toast-info) !important; }
+ .dec-root [data-sonner-toast] [data-title], .dec-root [data-sonner-toast] [data-description] { color: var(--dec-toast-text) !important; }
+ .dec-root [data-sonner-toast] [data-close-button] {
+ position: absolute !important;
+ top: 50% !important;
+ right: 12px !important;
+ left: auto !important;
+ transform: translateY(-50%) !important;
+ background: transparent !important;
+ border: none !important;
+ box-shadow: none !important;
+ color: var(--dec-toast-close-btn);
+ cursor: pointer;
+ }
+}
diff --git a/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.tsx b/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.tsx
new file mode 100644
index 00000000..d5b9e2b6
--- /dev/null
+++ b/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021-Present The Serverless Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ CircleCheckIcon,
+ InfoIcon,
+ Loader2Icon,
+ OctagonXIcon,
+ TriangleAlertIcon,
+ XIcon,
+} from "lucide-react";
+import { Toaster as Sonner, type ToasterProps } from "sonner";
+import "./sonner.css";
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ return (
+
+ ),
+ info: ,
+ warning: (
+
+ ),
+ error: ,
+ loading: ,
+ close: ,
+ }}
+ {...props}
+ />
+ );
+};
+
+export { Toaster };
diff --git a/packages/serverless-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx b/packages/serverless-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx
index 4521b121..8271c802 100644
--- a/packages/serverless-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx
+++ b/packages/serverless-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx
@@ -27,6 +27,7 @@ import { useResolvedColorMode } from "../hooks/useResolvedColorMode";
import { SidebarProvider } from "@/components/ui/sidebar";
import { SidePanel } from "@/side-panel/SidePanel";
import { DiagramEditorErrorBoundary } from "./error-pages/DiagramEditorErrorBoundary";
+import { Toaster } from "@/components/ui/sonner";
/**
* DiagramEditor component API
@@ -130,6 +131,7 @@ export const DiagramEditor = (props: DiagramEditorProps) => {
}}
+
);
};
diff --git a/packages/serverless-workflow-diagram-editor/src/i18n/locales/en.ts b/packages/serverless-workflow-diagram-editor/src/i18n/locales/en.ts
index bdd2d201..401cd0ab 100644
--- a/packages/serverless-workflow-diagram-editor/src/i18n/locales/en.ts
+++ b/packages/serverless-workflow-diagram-editor/src/i18n/locales/en.ts
@@ -50,6 +50,9 @@ export const en = {
"aria.panel.workflowInfo": "Workflow information panel",
"aria.panel.content": "Panel content",
"aria.panel.exportActions": "Export actions",
+ "toast.clipboard.error": "Failed to copy",
+ "toast.download.success": "Download started",
+ "toast.download.error": "Download failed",
} as const;
export type TranslationKeys = keyof typeof en;
diff --git a/packages/serverless-workflow-diagram-editor/src/side-panel/MermaidActions.tsx b/packages/serverless-workflow-diagram-editor/src/side-panel/MermaidActions.tsx
index a74ed7af..889c6e29 100644
--- a/packages/serverless-workflow-diagram-editor/src/side-panel/MermaidActions.tsx
+++ b/packages/serverless-workflow-diagram-editor/src/side-panel/MermaidActions.tsx
@@ -22,6 +22,7 @@ import { exportToMermaid } from "@/core";
import { copyToClipboard } from "@/lib/clipboard";
import { downloadFile } from "@/lib/download";
import type { Specification } from "@serverlessworkflow/sdk";
+import { toast } from "sonner";
export function MermaidActions({ model }: { model: Specification.Workflow }): React.JSX.Element {
const { t } = useI18n();
@@ -51,8 +52,9 @@ export function MermaidActions({ model }: { model: Specification.Workflow }): Re
copyTimeoutRef.current = null;
}, 2000);
} catch (error) {
- console.error("Failed to copy mermaid code:", error);
- // TODO: Create component to show errors to users
+ toast.error(t("toast.clipboard.error"), {
+ description: error instanceof Error ? error.message : undefined,
+ });
}
};
@@ -66,9 +68,11 @@ export function MermaidActions({ model }: { model: Specification.Workflow }): Re
.substring(0, 200);
const filename = `${sanitizedName}.mmd`;
downloadFile(mermaidCode, filename);
+ toast.success(t("toast.download.success"));
} catch (error) {
- console.error("Failed to download mermaid file:", error);
- // TODO: Create component to show errors to users
+ toast.error(t("toast.download.error"), {
+ description: error instanceof Error ? error.message : undefined,
+ });
}
};
diff --git a/packages/serverless-workflow-diagram-editor/src/styles.css b/packages/serverless-workflow-diagram-editor/src/styles.css
index 53090fa0..d95ad817 100644
--- a/packages/serverless-workflow-diagram-editor/src/styles.css
+++ b/packages/serverless-workflow-diagram-editor/src/styles.css
@@ -19,7 +19,8 @@
side-panel,
react-flow-overrides,
custom-nodes,
- custom-edge-labels;
+ custom-edge-labels,
+ sonner;
@import 'tailwindcss' prefix(dec);
@import './components/ui/shadcn.css';
@@ -61,6 +62,15 @@
--dec-minimap-node-stroke: #64748b;
--dec-minimap-border: #cbd5e1;
--dec-minimap-shadow: rgba(15, 23, 42, 0.12);
+
+ /* toast */
+ --dec-toast-bg: #ffffff;
+ --dec-toast-border: #e9edf4;
+ --dec-toast-success: #22c55e;
+ --dec-toast-warning: #f97316;
+ --dec-toast-info: #3b82f6;
+ --dec-toast-close-btn: #6b7280;
+ --dec-toast-text: #1f2328;
}
.dec-root.dark {
@@ -77,6 +87,15 @@
--dec-minimap-node-stroke: #cbd5e1;
--dec-minimap-border: #2d3748;
--dec-minimap-shadow: rgba(0, 0, 0, 0.5);
+
+ /* toast */
+ --dec-toast-bg: #2d3748;
+ --dec-toast-border: #4a5568;
+ --dec-toast-success: #4ade80;
+ --dec-toast-warning: #fb923c;
+ --dec-toast-info: #60a5fa;
+ --dec-toast-close-btn: #ffffff;
+ --dec-toast-text: #ffffff;
}
.dec-root .dec-diagram-content {
diff --git a/packages/serverless-workflow-diagram-editor/tests/side-panel/MermaidActions.test.tsx b/packages/serverless-workflow-diagram-editor/tests/side-panel/MermaidActions.test.tsx
index fbd5b0cc..8d3f7978 100644
--- a/packages/serverless-workflow-diagram-editor/tests/side-panel/MermaidActions.test.tsx
+++ b/packages/serverless-workflow-diagram-editor/tests/side-panel/MermaidActions.test.tsx
@@ -24,9 +24,14 @@ import { WORKFLOW_WITH_METADATA_JSON } from "../fixtures/workflows";
import * as clipboard from "../../src/lib/clipboard";
import * as core from "../../src/core";
import * as download from "../../src/lib/download";
+import * as sonner from "sonner";
describe("MermaidActions", () => {
+ const toastMock = vi.fn();
+ const MERMAID_CODE = "mermaid code";
+
afterEach(() => {
+ toastMock.mockClear();
vi.restoreAllMocks();
});
@@ -34,25 +39,76 @@ describe("MermaidActions", () => {
const user = userEvent.setup();
const { model } = parseWorkflow(WORKFLOW_WITH_METADATA_JSON);
const copySpy = vi.spyOn(clipboard, "copyToClipboard").mockResolvedValue(undefined);
- vi.spyOn(core, "exportToMermaid").mockReturnValue("mermaid code");
+ vi.spyOn(core, "exportToMermaid").mockReturnValue(MERMAID_CODE);
renderWithProviders(, { model });
- const copyButton = screen.getByText(/Copy Mermaid Code/i);
+
+ const copyButton = screen.getByRole("button", {
+ name: /Copy Mermaid Code/i,
+ });
+
+ await user.click(copyButton);
+
+ expect(copySpy).toHaveBeenCalledWith(MERMAID_CODE);
+ });
+
+ it("should show error toast when clipboard copy fails", async () => {
+ const user = userEvent.setup();
+ const { model } = parseWorkflow(WORKFLOW_WITH_METADATA_JSON);
+ vi.spyOn(clipboard, "copyToClipboard").mockRejectedValue(new Error("Clipboard error"));
+ vi.spyOn(core, "exportToMermaid").mockReturnValue(MERMAID_CODE);
+ vi.spyOn(sonner.toast, "error").mockImplementation(toastMock);
+ vi.spyOn(sonner.toast, "success").mockImplementation(toastMock);
+
+ renderWithProviders(, { model });
+
+ const copyButton = screen.getByRole("button", {
+ name: /Copy Mermaid Code/i,
+ });
+
await user.click(copyButton);
- expect(copySpy).toHaveBeenCalledWith("mermaid code");
+ expect(toastMock).toHaveBeenCalledWith(expect.any(String), { description: "Clipboard error" });
});
- it("should call downloadMermaidFile when download button is clicked", async () => {
+ it("should call downloadMermaidFile and show success toast when download button is clicked", async () => {
const user = userEvent.setup();
const { model } = parseWorkflow(WORKFLOW_WITH_METADATA_JSON);
const downloadSpy = vi.spyOn(download, "downloadFile").mockImplementation(() => {});
- vi.spyOn(core, "exportToMermaid").mockReturnValue("mermaid code");
+ vi.spyOn(core, "exportToMermaid").mockReturnValue(MERMAID_CODE);
+ vi.spyOn(sonner.toast, "error").mockImplementation(toastMock);
+ vi.spyOn(sonner.toast, "success").mockImplementation(toastMock);
renderWithProviders(, { model });
- const downloadButton = screen.getByText(/Download as Mermaid File/i);
+
+ const downloadButton = screen.getByRole("button", {
+ name: /Download as Mermaid File/i,
+ });
+
+ await user.click(downloadButton);
+
+ expect(downloadSpy).toHaveBeenCalledWith(MERMAID_CODE, "test-wf.mmd");
+ expect(toastMock).toHaveBeenCalledWith(expect.any(String));
+ });
+
+ it("should show error toast when download fails", async () => {
+ const user = userEvent.setup();
+ const { model } = parseWorkflow(WORKFLOW_WITH_METADATA_JSON);
+ vi.spyOn(download, "downloadFile").mockImplementation(() => {
+ throw new Error("Download error");
+ });
+ vi.spyOn(core, "exportToMermaid").mockReturnValue(MERMAID_CODE);
+ vi.spyOn(sonner.toast, "error").mockImplementation(toastMock);
+ vi.spyOn(sonner.toast, "success").mockImplementation(toastMock);
+
+ renderWithProviders(, { model });
+
+ const downloadButton = screen.getByRole("button", {
+ name: /Download as Mermaid File/i,
+ });
+
await user.click(downloadButton);
- expect(downloadSpy).toHaveBeenCalledWith("mermaid code", "test-wf.mmd");
+ expect(toastMock).toHaveBeenCalledWith(expect.any(String), { description: "Download error" });
});
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3f9083fe..e1cd3904 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -117,6 +117,9 @@ catalogs:
rimraf:
specifier: ^6.1.3
version: 6.1.3
+ sonner:
+ specifier: ^2.0.7
+ version: 2.0.7
storybook:
specifier: ^10.4.6
version: 10.4.6
@@ -230,6 +233,9 @@ importers:
radix-ui:
specifier: 'catalog:'
version: 1.6.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ sonner:
+ specifier: 'catalog:'
+ version: 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
use-sync-external-store:
specifier: 'catalog:'
version: 1.6.0(react@19.2.7)
@@ -3562,6 +3568,12 @@ packages:
resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==}
engines: {node: '>=20'}
+ sonner@2.0.7:
+ resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
+ peerDependencies:
+ react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -7117,6 +7129,11 @@ snapshots:
ansi-styles: 6.2.3
is-fullwidth-code-point: 5.1.0
+ sonner@2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
+ dependencies:
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+
source-map-js@1.2.1: {}
source-map@0.6.1: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 095aab45..b38b2b2c 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -38,6 +38,7 @@ catalog:
react: ^19.2.7
react-dom: ^19.2.7
rimraf: ^6.1.3
+ sonner: ^2.0.7
storybook: ^10.4.6
syncpack: ^15.3.2
tailwindcss: ^4.3.1