diff --git a/.changeset/toast-notification-removed.md b/.changeset/toast-notification-removed.md
new file mode 100644
index 00000000..33829dd7
--- /dev/null
+++ b/.changeset/toast-notification-removed.md
@@ -0,0 +1,5 @@
+---
+"@openworkflowspec/diagram-editor": minor
+---
+
+Toast notification removed for side panel buttons
diff --git a/packages/open-workflow-diagram-editor/package.json b/packages/open-workflow-diagram-editor/package.json
index 3e5285e5..d77b79e8 100644
--- a/packages/open-workflow-diagram-editor/package.json
+++ b/packages/open-workflow-diagram-editor/package.json
@@ -52,7 +52,6 @@
"html-to-image": "catalog:",
"js-yaml": "catalog:",
"radix-ui": "catalog:",
- "sonner": "catalog:",
"use-sync-external-store": "catalog:"
},
"devDependencies": {
diff --git a/packages/open-workflow-diagram-editor/src/components/ui/sonner.css b/packages/open-workflow-diagram-editor/src/components/ui/sonner.css
deleted file mode 100644
index 661b1784..00000000
--- a/packages/open-workflow-diagram-editor/src/components/ui/sonner.css
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright 2021-Present The Open 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/open-workflow-diagram-editor/src/components/ui/sonner.tsx b/packages/open-workflow-diagram-editor/src/components/ui/sonner.tsx
deleted file mode 100644
index e8de5262..00000000
--- a/packages/open-workflow-diagram-editor/src/components/ui/sonner.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2021-Present The Open 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/open-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx b/packages/open-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx
index dafad7fe..572d0d07 100644
--- a/packages/open-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx
+++ b/packages/open-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx
@@ -27,7 +27,6 @@ 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";
/**
* Imperative handle exposed by `DiagramEditor` via `ref`.
@@ -184,7 +183,6 @@ export const DiagramEditor = React.forwardRef
-
);
},
diff --git a/packages/open-workflow-diagram-editor/src/i18n/locales/en.ts b/packages/open-workflow-diagram-editor/src/i18n/locales/en.ts
index 4dbe7c83..008634d9 100644
--- a/packages/open-workflow-diagram-editor/src/i18n/locales/en.ts
+++ b/packages/open-workflow-diagram-editor/src/i18n/locales/en.ts
@@ -43,7 +43,9 @@ export const en = {
"sidebar.exportMermaid.copy": "Copy Mermaid Code",
"sidebar.exportMermaid.download": "Download as Mermaid File",
"sidebar.exportMermaid.copied": "Copied!",
+ "sidebar.export.downloaded": "Downloaded",
"sidebar.exportPng.download": "Download as PNG",
+ "sidebar.exportPng.downloading": "Downloading...",
"aria.minimap.hide": "Hide minimap",
"aria.minimap.show": "Show minimap",
"aria.badge": "Badge:",
@@ -53,9 +55,6 @@ export const en = {
"aria.panel.exportActions": "Export actions",
"workflowError.autoLayout.title": "Layout Error",
"workflowError.autoLayout.message": "Failed to apply auto-layout to the diagram.",
- "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/open-workflow-diagram-editor/src/lib/download.ts b/packages/open-workflow-diagram-editor/src/lib/download.ts
index 6daeb23c..fe67999f 100644
--- a/packages/open-workflow-diagram-editor/src/lib/download.ts
+++ b/packages/open-workflow-diagram-editor/src/lib/download.ts
@@ -24,9 +24,7 @@ export function downloadFile(content: string, filename: string, mimeType = "text
const link = document.createElement("a");
link.href = url;
link.download = filename;
- document.body.appendChild(link);
link.click();
- document.body.removeChild(link);
setTimeout(() => {
URL.revokeObjectURL(url);
}, 100);
diff --git a/packages/open-workflow-diagram-editor/src/side-panel/WorkflowActions.tsx b/packages/open-workflow-diagram-editor/src/side-panel/WorkflowActions.tsx
index acc83562..b0e3e61c 100644
--- a/packages/open-workflow-diagram-editor/src/side-panel/WorkflowActions.tsx
+++ b/packages/open-workflow-diagram-editor/src/side-panel/WorkflowActions.tsx
@@ -16,7 +16,7 @@
import * as React from "react";
import { useI18n } from "@openworkflowspec/i18n";
-import { ClipboardPen, Download, ClipboardCheck, FileImage } from "lucide-react";
+import { ClipboardPen, Download, ClipboardCheck, FileImage, Import } from "lucide-react";
import { useReactFlow, useStore } from "@xyflow/react";
import { Button } from "@/components/ui/button";
import { exportToMermaid } from "@/core";
@@ -26,12 +26,13 @@ import { exportDiagramAsPng } from "@/lib/exportPng";
import { sanitizeFilename } from "@/lib/utils";
import { useDiagramEditorContext } from "@/store/DiagramEditorContext";
import type { Specification } from "@openworkflowspec/sdk";
-import { toast } from "sonner";
export function WorkflowActions({ model }: { model: Specification.Workflow }): React.JSX.Element {
const { t } = useI18n();
const [isCopied, setIsCopied] = React.useState(false);
+ const [downloadedType, setDownloadedType] = React.useState<"mermaid" | "png" | null>(null);
const copyTimeoutRef = React.useRef | null>(null);
+ const downloadTimeoutRef = React.useRef | null>(null);
const reactFlowInstance = useReactFlow();
const diagramDomNode = useStore((s) => s.domNode);
const { isExporting, setIsExporting } = useDiagramEditorContext();
@@ -41,6 +42,10 @@ export function WorkflowActions({ model }: { model: Specification.Workflow }): R
if (copyTimeoutRef.current) {
clearTimeout(copyTimeoutRef.current);
}
+
+ if (downloadTimeoutRef.current) {
+ clearTimeout(downloadTimeoutRef.current);
+ }
};
}, []);
@@ -59,9 +64,10 @@ export function WorkflowActions({ model }: { model: Specification.Workflow }): R
copyTimeoutRef.current = null;
}, 2000);
} catch (error) {
- toast.error(t("toast.clipboard.error"), {
- description: error instanceof Error ? error.message : undefined,
- });
+ console.error(
+ "Failed to copy Mermaid code:",
+ error instanceof Error ? error.message : undefined,
+ );
}
};
@@ -70,11 +76,21 @@ export function WorkflowActions({ model }: { model: Specification.Workflow }): R
const mermaidCode = exportToMermaid(model);
const filename = `${sanitizeFilename(model.document?.name)}.mmd`;
downloadFile(mermaidCode, filename);
- toast.success(t("toast.download.success"));
+ setDownloadedType("mermaid");
+
+ if (downloadTimeoutRef.current) {
+ clearTimeout(downloadTimeoutRef.current);
+ }
+
+ downloadTimeoutRef.current = setTimeout(() => {
+ setDownloadedType(null);
+ downloadTimeoutRef.current = null;
+ }, 2000);
} catch (error) {
- toast.error(t("toast.download.error"), {
- description: error instanceof Error ? error.message : undefined,
- });
+ console.error(
+ "Failed to download Mermaid file:",
+ error instanceof Error ? error.message : undefined,
+ );
}
};
@@ -86,11 +102,22 @@ export function WorkflowActions({ model }: { model: Specification.Workflow }): R
`${sanitizeFilename(model.document?.name)}.png`,
diagramDomNode,
);
- toast.success(t("toast.download.success"));
+
+ setDownloadedType("png");
+
+ if (downloadTimeoutRef.current) {
+ clearTimeout(downloadTimeoutRef.current);
+ }
+
+ downloadTimeoutRef.current = setTimeout(() => {
+ setDownloadedType(null);
+ downloadTimeoutRef.current = null;
+ }, 2000);
} catch (error) {
- toast.error(t("toast.download.error"), {
- description: error instanceof Error ? error.message : undefined,
- });
+ console.error(
+ "Failed to export diagram as PNG:",
+ error instanceof Error ? error.message : undefined,
+ );
} finally {
setIsExporting(false);
}
@@ -113,8 +140,10 @@ export function WorkflowActions({ model }: { model: Specification.Workflow }): R
size="sm"
className="dec:cursor-pointer"
>
-
- {t("sidebar.exportMermaid.download")}
+ {downloadedType === "mermaid" ? : }
+ {downloadedType === "mermaid"
+ ? t("sidebar.export.downloaded")
+ : t("sidebar.exportMermaid.download")}
>
);
diff --git a/packages/open-workflow-diagram-editor/src/styles.css b/packages/open-workflow-diagram-editor/src/styles.css
index a0bed771..7d28037f 100644
--- a/packages/open-workflow-diagram-editor/src/styles.css
+++ b/packages/open-workflow-diagram-editor/src/styles.css
@@ -15,15 +15,28 @@
*/
/* layer order (Priority: lowest -> highest) */
+
+@import 'tailwindcss' prefix(dec);
+@import './components/ui/shadcn.css';
+
+@layer base {
+ .dec-root,
+ .dec-root *,
+ .dec-root::before,
+ .dec-root::after,
+ .dec-root::backdrop,
+ .dec-root *::before,
+ .dec-root *::after,
+ .dec-root *::backdrop {
+ --tw-border-style: solid;
+ }
+}
+
@layer base,
side-panel,
react-flow-overrides,
custom-nodes,
- custom-edge-labels,
- sonner;
-
-@import 'tailwindcss' prefix(dec);
-@import './components/ui/shadcn.css';
+ custom-edge-labels;
@custom-variant dark (&:where(.dec-root.dark, .dec-root.dark *));
diff --git a/packages/open-workflow-diagram-editor/tests/lib/download.test.ts b/packages/open-workflow-diagram-editor/tests/lib/download.test.ts
index d113d092..f33ef7ca 100644
--- a/packages/open-workflow-diagram-editor/tests/lib/download.test.ts
+++ b/packages/open-workflow-diagram-editor/tests/lib/download.test.ts
@@ -24,8 +24,6 @@ describe("downloadFile", () => {
it("creates and triggers file download", () => {
const mockClick = vi.fn();
- const mockAppendChild = vi.fn();
- const mockRemoveChild = vi.fn();
const mockElement = {
click: mockClick,
href: "",
@@ -35,8 +33,6 @@ describe("downloadFile", () => {
vi.spyOn(document, "createElement").mockReturnValue(
mockElement as unknown as HTMLAnchorElement,
);
- vi.spyOn(document.body, "appendChild").mockImplementation(mockAppendChild);
- vi.spyOn(document.body, "removeChild").mockImplementation(mockRemoveChild);
vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:mock-url");
vi.spyOn(URL, "revokeObjectURL").mockImplementation(vi.fn());
@@ -44,8 +40,8 @@ describe("downloadFile", () => {
downloadFile(testCode, "test.mmd");
expect(document.createElement).toHaveBeenCalledWith("a");
+ expect(mockElement.href).toBe("blob:mock-url");
+ expect(mockElement.download).toBe("test.mmd");
expect(mockClick).toHaveBeenCalled();
- expect(mockAppendChild).toHaveBeenCalled();
- expect(mockRemoveChild).toHaveBeenCalled();
});
});
diff --git a/packages/open-workflow-diagram-editor/tests/side-panel/WorkflowActions.test.tsx b/packages/open-workflow-diagram-editor/tests/side-panel/WorkflowActions.test.tsx
index 6eb63bd7..3ad31c01 100644
--- a/packages/open-workflow-diagram-editor/tests/side-panel/WorkflowActions.test.tsx
+++ b/packages/open-workflow-diagram-editor/tests/side-panel/WorkflowActions.test.tsx
@@ -25,7 +25,6 @@ import * as clipboard from "../../src/lib/clipboard";
import * as core from "../../src/core";
import * as download from "../../src/lib/download";
import * as exportPng from "../../src/lib/exportPng";
-import * as sonner from "sonner";
describe("WorkflowActions", () => {
const toastMock = vi.fn();
@@ -58,8 +57,6 @@ describe("WorkflowActions", () => {
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 });
@@ -69,7 +66,7 @@ describe("WorkflowActions", () => {
await user.click(copyButton);
- expect(toastMock).toHaveBeenCalledWith(expect.any(String), { description: "Clipboard error" });
+ expect(clipboard.copyToClipboard).toHaveBeenCalledWith(MERMAID_CODE);
});
it("should call downloadMermaidFile and show success toast when download button is clicked", async () => {
@@ -77,8 +74,6 @@ describe("WorkflowActions", () => {
const { model } = parseWorkflow(WORKFLOW_WITH_METADATA_JSON);
const downloadSpy = vi.spyOn(download, "downloadFile").mockImplementation(() => {});
vi.spyOn(core, "exportToMermaid").mockReturnValue(MERMAID_CODE);
- vi.spyOn(sonner.toast, "error").mockImplementation(toastMock);
- vi.spyOn(sonner.toast, "success").mockImplementation(toastMock);
renderWithProviders(, { model });
@@ -89,18 +84,16 @@ describe("WorkflowActions", () => {
await user.click(downloadButton);
expect(downloadSpy).toHaveBeenCalledWith(MERMAID_CODE, "test-wf.mmd");
- expect(toastMock).toHaveBeenCalledWith(expect.any(String));
+ expect(screen.getByRole("button", { name: "Downloaded" })).toBeInTheDocument();
});
- it("should show error toast when download fails", async () => {
+ it("should handle download failure", 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 });
@@ -110,7 +103,7 @@ describe("WorkflowActions", () => {
await user.click(downloadButton);
- expect(toastMock).toHaveBeenCalledWith(expect.any(String), { description: "Download error" });
+ expect(download.downloadFile).toHaveBeenCalledWith(MERMAID_CODE, "test-wf.mmd");
});
it("should disable the PNG button when isExporting is true", () => {
@@ -118,16 +111,17 @@ describe("WorkflowActions", () => {
renderWithProviders(, { model, isExporting: true });
- expect(screen.getByRole("button", { name: /Download as PNG/i })).toBeDisabled();
+ const pngButton = screen.getByRole("button", {
+ name: "Downloading...",
+ });
+ expect(pngButton).toBeDisabled();
});
- it("should call exportDiagramAsPng with sanitized filename and show success toast", async () => {
+ it("should call exportDiagramAsPng with sanitized filename and show downloaded state", async () => {
const user = userEvent.setup();
const { model } = parseWorkflow(WORKFLOW_WITH_METADATA_JSON);
const exportSpy = vi.spyOn(exportPng, "exportDiagramAsPng").mockResolvedValue(undefined);
const setIsExporting = vi.fn();
- vi.spyOn(sonner.toast, "error").mockImplementation(toastMock);
- vi.spyOn(sonner.toast, "success").mockImplementation(toastMock);
renderWithProviders(, { model, setIsExporting });
@@ -135,24 +129,20 @@ describe("WorkflowActions", () => {
await vi.waitFor(() => expect(exportSpy).toHaveBeenCalled());
expect(exportSpy).toHaveBeenCalledWith(expect.anything(), "test-wf.png", null);
- expect(toastMock).toHaveBeenCalledWith(expect.any(String));
+ expect(screen.getByRole("button", { name: "Downloaded" })).toBeInTheDocument();
expect(setIsExporting).toHaveBeenCalledWith(true);
expect(setIsExporting).toHaveBeenLastCalledWith(false);
});
- it("should show error toast and still reset isExporting when PNG export fails", async () => {
+ it("should reset isExporting when PNG export fails", async () => {
const user = userEvent.setup();
const { model } = parseWorkflow(WORKFLOW_WITH_METADATA_JSON);
vi.spyOn(exportPng, "exportDiagramAsPng").mockRejectedValue(new Error("Export failed"));
const setIsExporting = vi.fn();
- vi.spyOn(sonner.toast, "error").mockImplementation(toastMock);
- vi.spyOn(sonner.toast, "success").mockImplementation(toastMock);
renderWithProviders(, { model, setIsExporting });
await user.click(screen.getByRole("button", { name: /Download as PNG/i }));
await vi.waitFor(() => expect(setIsExporting).toHaveBeenLastCalledWith(false));
-
- expect(toastMock).toHaveBeenCalledWith(expect.any(String), { description: "Export failed" });
});
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 811b2361..c325e1ea 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -132,9 +132,6 @@ catalogs:
rimraf:
specifier: ^6.1.3
version: 6.1.3
- sonner:
- specifier: ^2.0.8
- version: 2.0.8
storybook:
specifier: ^10.6.0
version: 10.6.0
@@ -349,9 +346,6 @@ importers:
radix-ui:
specifier: 'catalog:'
version: 1.6.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- sonner:
- specifier: 'catalog:'
- version: 2.0.8(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
use-sync-external-store:
specifier: 'catalog:'
version: 1.7.0(react@19.3.0)
@@ -3732,16 +3726,6 @@ packages:
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
- sonner@2.0.8:
- resolution: {integrity: sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==}
- peerDependencies:
- '@types/react': ^18.0.0 || ^19.0.0
- react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
- react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -7102,13 +7086,6 @@ snapshots:
sisteransi@1.0.5: {}
- sonner@2.0.8(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
- dependencies:
- react: 19.3.0
- react-dom: 19.3.0(react@19.3.0)
- optionalDependencies:
- '@types/react': 19.3.0
-
source-map-js@1.2.1: {}
source-map@0.6.1: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index d3d94c06..6e18b6c7 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -44,7 +44,6 @@ catalog:
react: ^19.3.0
react-dom: ^19.3.0
rimraf: ^6.1.3
- sonner: ^2.0.8
storybook: ^10.6.0
syncpack: ^15.3.3
tailwindcss: ^4.3.3