From 56829faa8c116d37a1fb5cd9c96a1a3787961f02 Mon Sep 17 00:00:00 2001 From: Cheryl Kong Date: Wed, 24 Jun 2026 10:25:00 +0100 Subject: [PATCH 01/13] rebase main Signed-off-by: Cheryl Kong --- .../src/react-flow/diagram/Diagram.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx index 145f54a0..6ca66916 100644 --- a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx +++ b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx @@ -195,6 +195,12 @@ export const Diagram = ({ divRef, ref, colorMode = "light" }: DiagramProps) => { > M + setMinimapVisible(!minimapVisible)} + aria-label={minimapVisible ? "Hide minimap" : "Show minimap"} + > + M + From 4ddc2792ac4c53bfc5382902aa108b10b3a9eb17 Mon Sep 17 00:00:00 2001 From: Cheryl Kong Date: Mon, 29 Jun 2026 14:30:07 +0100 Subject: [PATCH 02/13] feat: add toast alert system Signed-off-by: Cheryl Kong --- .../src/components/ui/toast.tsx | 140 ++++++++++++++++++ .../src/diagram-editor/DiagramEditor.tsx | 5 + .../src/hooks/useToast.ts | 73 +++++++++ .../src/i18n/locales/en.ts | 3 + .../src/side-panel/MermaidActions.tsx | 16 +- 5 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 packages/serverless-workflow-diagram-editor/src/components/ui/toast.tsx create mode 100644 packages/serverless-workflow-diagram-editor/src/hooks/useToast.ts diff --git a/packages/serverless-workflow-diagram-editor/src/components/ui/toast.tsx b/packages/serverless-workflow-diagram-editor/src/components/ui/toast.tsx new file mode 100644 index 00000000..c7275982 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/src/components/ui/toast.tsx @@ -0,0 +1,140 @@ +/* + * 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 * as React from "react"; +import { CircleAlert, CircleCheck, Info, TriangleAlert, XIcon } from "lucide-react"; +import { Toast as ToastPrimitive } from "radix-ui"; +import { cn } from "@/lib/utils"; +import { useToast } from "@/hooks/useToast"; + +type ToastProps = { + id: string; + title?: React.ReactNode; + description?: React.ReactNode; + variant?: "default" | "error" | "success" | "warning" | "info"; + open: boolean; +}; + +const TOAST_VARIANT_STYLES_BASE = { + error: { + root: "dec:border-gray-200 dec:bg-white dec:text-gray-900 dec:dark:border-gray-700 dec:dark:bg-[#2d3748] dec:dark:text-gray-100", + close: + "dec:text-gray-600 hover:dec:text-gray-900 dec:dark:text-gray-400 dec:dark:hover:text-gray-100", + icon: CircleAlert, + iconColor: "dec:text-red-500", + borderColor: "dec:border-l-red-500 dec:dark:border-l-red-500", + }, + success: { + root: "dec:border-gray-200 dec:bg-white dec:text-gray-900 dec:dark:border-gray-700 dec:dark:bg-[#2d3748] dec:dark:text-gray-100", + close: + "dec:text-gray-600 hover:dec:text-gray-900 dec:dark:text-gray-400 dec:dark:hover:text-gray-100", + icon: CircleCheck, + iconColor: "dec:text-green-500", + borderColor: "dec:border-l-green-500 dec:dark:border-l-green-500", + }, + warning: { + root: "dec:border-gray-200 dec:bg-white dec:text-gray-900 dec:dark:border-gray-700 dec:dark:bg-[#2d3748] dec:dark:text-gray-100", + close: + "dec:text-gray-600 hover:dec:text-gray-900 dec:dark:text-gray-400 dec:dark:hover:text-gray-100", + icon: TriangleAlert, + iconColor: "dec:text-orange-500", + borderColor: "dec:border-l-orange-500 dec:dark:border-l-orange-500", + }, + info: { + root: "dec:border-gray-200 dec:bg-white dec:text-gray-900 dec:dark:border-gray-700 dec:dark:bg-[#2d3748] dec:dark:text-gray-100", + close: + "dec:text-gray-600 hover:dec:text-gray-900 dec:dark:text-gray-400 dec:dark:hover:text-gray-100", + icon: Info, + iconColor: "dec:text-blue-500", + borderColor: "dec:border-l-blue-500 dec:dark:border-l-blue-500", + }, +} as const; + +const TOAST_VARIANT_STYLES = { + ...TOAST_VARIANT_STYLES_BASE, + default: TOAST_VARIANT_STYLES_BASE.info, +} as const; + +const ToastItem = React.memo( + ({ toast, onDismiss }: { toast: ToastProps; onDismiss: (id: string) => void }) => { + const { id, open, variant = "default", title, description } = toast; + const styles = TOAST_VARIANT_STYLES[variant]; + const Icon = styles.icon; + + return ( + + {Icon && ( +
+ +
+ )} +
+ {title && ( + + {title} + + )} + {description && ( + + {description} + + )} +
+ onDismiss(id)} + className={cn( + "dec:-mt-0.5 dec:rounded-md dec:p-1 dec:opacity-100 dec:transition-colors", + styles.close, + )} + > + + +
+ ); + }, +); + +ToastItem.displayName = "ToastItem"; + +function Toaster() { + const { toasts, dismiss } = useToast(); + + return ( + <> + {toasts.map((toast) => ( + + ))} + + + ); +} + +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..eb8c543f 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,8 @@ 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/toast"; +import { Toast as ToastPrimitive } from "radix-ui"; /** * DiagramEditor component API @@ -130,6 +132,9 @@ export const DiagramEditor = (props: DiagramEditorProps) => { }} + + + ); }; diff --git a/packages/serverless-workflow-diagram-editor/src/hooks/useToast.ts b/packages/serverless-workflow-diagram-editor/src/hooks/useToast.ts new file mode 100644 index 00000000..f5051ce0 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/src/hooks/useToast.ts @@ -0,0 +1,73 @@ +/* + * 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 * as React from "react"; + +type ToastProps = { + id: string; + title?: React.ReactNode; + description?: React.ReactNode; + variant?: "default" | "error" | "success" | "warning" | "info"; + open: boolean; +}; + +type Action = + | { type: "ADD"; toast: Omit } + | { type: "DISMISS"; id: string } + | { type: "REMOVE"; id: string }; + +let toasts: ToastProps[] = []; +let toastCount = 0; +const listeners: Array<(toasts: ToastProps[]) => void> = []; + +function dispatch(action: Action) { + if (action.type === "ADD") { + toasts = [...toasts, { ...action.toast, open: true }]; + setTimeout(() => dispatch({ type: "DISMISS", id: action.toast.id }), 5000); + } else if (action.type === "DISMISS") { + toasts = toasts.map((t) => (t.id === action.id ? { ...t, open: false } : t)); + setTimeout(() => dispatch({ type: "REMOVE", id: action.id }), 300); + } else if (action.type === "REMOVE") { + toasts = toasts.filter((t) => t.id !== action.id); + } + listeners.forEach((listener) => listener(toasts)); +} + +function toast(props: Omit) { + const id = String(toastCount++); + dispatch({ type: "ADD", toast: { ...props, id } }); + return id; +} + +function useToast() { + const [state, setState] = React.useState(toasts); + + React.useEffect(() => { + listeners.push(setState); + return () => { + const index = listeners.indexOf(setState); + if (index > -1) listeners.splice(index, 1); + }; + }, []); + + return { + toasts: state, + show: toast, + dismiss: (id: string) => dispatch({ type: "DISMISS", id }), + }; +} + +export { useToast, toast }; 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 691fb7f8..634dd06f 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 "@/hooks/useToast"; export function MermaidActions({ model }: { model: Specification.Workflow }): React.JSX.Element { const { t } = useI18n(); @@ -51,8 +52,11 @@ 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({ + variant: "error", + title: t("toast.clipboard.error"), + description: error instanceof Error ? error.message : undefined, + }); } }; @@ -66,9 +70,13 @@ export function MermaidActions({ model }: { model: Specification.Workflow }): Re .substring(0, 200); const filename = `${sanitizedName}.mmd`; downloadFile(mermaidCode, filename); + toast({ variant: "success", title: t("toast.download.success") }); } catch (error) { - console.error("Failed to download mermaid file:", error); - // TODO: Create component to show errors to users + toast({ + variant: "error", + title: t("toast.download.error"), + description: error instanceof Error ? error.message : undefined, + }); } }; From adcd90dd92dff16a2f4261e6673804124aade3c2 Mon Sep 17 00:00:00 2001 From: Cheryl Kong Date: Tue, 30 Jun 2026 09:56:31 +0100 Subject: [PATCH 03/13] test: update Mermaid Actions tests Signed-off-by: Cheryl Kong --- .../tests/side-panel/MermaidActions.test.tsx | 77 +++++++++++++++++-- 1 file changed, 70 insertions(+), 7 deletions(-) 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..36880d90 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,8 +24,12 @@ 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 toastHook from "../../src/hooks/useToast"; describe("MermaidActions", () => { + const toastMock = vi.fn(); + const MERMAID_CODE = "mermaid code"; + afterEach(() => { vi.restoreAllMocks(); }); @@ -34,25 +38,84 @@ 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(toastHook, "toast").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({ + variant: "error", + title: 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(toastHook, "toast").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({ + variant: "success", + title: 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(toastHook, "toast").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({ + variant: "error", + title: expect.any(String), + description: "Download error", + }); }); }); From 79b90633006e87abb56065315baba22bf4d2483b Mon Sep 17 00:00:00 2001 From: Cheryl Kong Date: Tue, 30 Jun 2026 10:26:39 +0100 Subject: [PATCH 04/13] add changeset Signed-off-by: Cheryl Kong --- .changeset/toast-alerts.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/toast-alerts.md diff --git a/.changeset/toast-alerts.md b/.changeset/toast-alerts.md new file mode 100644 index 00000000..9a15f828 --- /dev/null +++ b/.changeset/toast-alerts.md @@ -0,0 +1,5 @@ +--- +"@serverlessworkflow/diagram-editor": minor +--- + +add toast notification system From 5363f3f531f971fa4d441cf4aa01fdc265af308c Mon Sep 17 00:00:00 2001 From: Cheryl Kong Date: Tue, 30 Jun 2026 10:43:58 +0100 Subject: [PATCH 05/13] fix copilot complaints Signed-off-by: Cheryl Kong --- .../src/components/ui/toast.tsx | 3 +++ .../src/react-flow/diagram/Diagram.tsx | 6 ------ .../tests/side-panel/MermaidActions.test.tsx | 1 + 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/serverless-workflow-diagram-editor/src/components/ui/toast.tsx b/packages/serverless-workflow-diagram-editor/src/components/ui/toast.tsx index c7275982..da449223 100644 --- a/packages/serverless-workflow-diagram-editor/src/components/ui/toast.tsx +++ b/packages/serverless-workflow-diagram-editor/src/components/ui/toast.tsx @@ -77,6 +77,9 @@ const ToastItem = React.memo( return ( { + if (!nextOpen) onDismiss(id); + }} data-slot="toast" className={cn( "dec:pointer-events-auto dec:relative dec:flex dec:w-full dec:min-w-[320px] dec:max-w-[680px] dec:items-start dec:gap-4 dec:rounded-[14px] dec:border dec:border-l-4 dec:px-7 dec:py-6 dec:shadow-[0_18px_38px_rgba(22,23,24,0.12),0_8px_16px_rgba(22,23,24,0.08)] dec:transition-all dec:data-[state=open]:animate-in dec:data-[state=closed]:animate-out dec:data-[state=closed]:fade-out-80 dec:data-[state=closed]:slide-out-to-left-full dec:data-[state=open]:slide-in-from-top-full", diff --git a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx index 6ca66916..145f54a0 100644 --- a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx +++ b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx @@ -195,12 +195,6 @@ export const Diagram = ({ divRef, ref, colorMode = "light" }: DiagramProps) => { > M - setMinimapVisible(!minimapVisible)} - aria-label={minimapVisible ? "Hide minimap" : "Show minimap"} - > - M - 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 36880d90..4e806e0c 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 @@ -31,6 +31,7 @@ describe("MermaidActions", () => { const MERMAID_CODE = "mermaid code"; afterEach(() => { + toastMock.mockClear(); vi.restoreAllMocks(); }); From 0ce46b675af35b54aab4cae299c4a80eeca30d8a Mon Sep 17 00:00:00 2001 From: Cheryl Kong Date: Thu, 2 Jul 2026 10:46:55 +0100 Subject: [PATCH 06/13] feat: add Sonner Signed-off-by: Cheryl Kong --- .../{toast-alerts.md => toast-alert.md} | 2 +- .../package.json | 2 + .../src/components/ui/sonner.css | 39 +++++ .../src/components/ui/sonner.tsx | 39 +++++ .../src/components/ui/toast.tsx | 143 ------------------ .../src/diagram-editor/DiagramEditor.tsx | 7 +- .../src/hooks/useToast.ts | 73 --------- .../src/side-panel/MermaidActions.tsx | 12 +- .../tests/side-panel/MermaidActions.test.tsx | 28 ++-- pnpm-lock.yaml | 28 ++++ pnpm-workspace.yaml | 2 + 11 files changed, 127 insertions(+), 248 deletions(-) rename .changeset/{toast-alerts.md => toast-alert.md} (54%) create mode 100644 packages/serverless-workflow-diagram-editor/src/components/ui/sonner.css create mode 100644 packages/serverless-workflow-diagram-editor/src/components/ui/sonner.tsx delete mode 100644 packages/serverless-workflow-diagram-editor/src/components/ui/toast.tsx delete mode 100644 packages/serverless-workflow-diagram-editor/src/hooks/useToast.ts diff --git a/.changeset/toast-alerts.md b/.changeset/toast-alert.md similarity index 54% rename from .changeset/toast-alerts.md rename to .changeset/toast-alert.md index 9a15f828..5979db04 100644 --- a/.changeset/toast-alerts.md +++ b/.changeset/toast-alert.md @@ -2,4 +2,4 @@ "@serverlessworkflow/diagram-editor": minor --- -add toast notification system +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..f2f99c82 100644 --- a/packages/serverless-workflow-diagram-editor/package.json +++ b/packages/serverless-workflow-diagram-editor/package.json @@ -48,7 +48,9 @@ "clsx": "catalog:", "elkjs": "catalog:", "js-yaml": "catalog:", + "next-themes": "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..015ab222 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.css @@ -0,0 +1,39 @@ +/* + * 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. + */ + +@layer sonner { + [data-sonner-toaster] { --width: 300px !important; } + [data-sonner-toast] { background: #ffffff !important; border-radius: 10px !important; border: 1px solid #e9edf4 !important; } + [data-sonner-toast][data-type="success"] { border-left: 4px solid #22c55e !important; } + [data-sonner-toast][data-type="error"] { border-left: 4px solid #ef4444 !important; } + [data-sonner-toast][data-type="warning"] { border-left: 4px solid #f97316 !important; } + [data-sonner-toast][data-type="info"] { border-left: 4px solid #3b82f6 !important; } + .dark [data-sonner-toast] { background: #2d3748 !important; color: #ffffff !important; border-top-color: #4a5568 !important; border-right-color: #4a5568 !important; border-bottom-color: #4a5568 !important; } + .dark [data-sonner-toast] [data-title], .dark [data-sonner-toast] [data-description] { color: #ffffff !important; } + [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: #6b7280; + cursor: pointer; + } + .dark [data-sonner-toast] [data-close-button] { color: #ffffff; } +} 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..739da8a4 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.tsx @@ -0,0 +1,39 @@ +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: , + }} + style={ + { + "--normal-bg": "var(--popover)", + "--normal-text": "var(--popover-foreground)", + "--normal-border": "var(--border)", + "--border-radius": "var(--radius)", + } as React.CSSProperties + } + {...props} + /> + ); +}; + +export { Toaster }; diff --git a/packages/serverless-workflow-diagram-editor/src/components/ui/toast.tsx b/packages/serverless-workflow-diagram-editor/src/components/ui/toast.tsx deleted file mode 100644 index da449223..00000000 --- a/packages/serverless-workflow-diagram-editor/src/components/ui/toast.tsx +++ /dev/null @@ -1,143 +0,0 @@ -/* - * 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 * as React from "react"; -import { CircleAlert, CircleCheck, Info, TriangleAlert, XIcon } from "lucide-react"; -import { Toast as ToastPrimitive } from "radix-ui"; -import { cn } from "@/lib/utils"; -import { useToast } from "@/hooks/useToast"; - -type ToastProps = { - id: string; - title?: React.ReactNode; - description?: React.ReactNode; - variant?: "default" | "error" | "success" | "warning" | "info"; - open: boolean; -}; - -const TOAST_VARIANT_STYLES_BASE = { - error: { - root: "dec:border-gray-200 dec:bg-white dec:text-gray-900 dec:dark:border-gray-700 dec:dark:bg-[#2d3748] dec:dark:text-gray-100", - close: - "dec:text-gray-600 hover:dec:text-gray-900 dec:dark:text-gray-400 dec:dark:hover:text-gray-100", - icon: CircleAlert, - iconColor: "dec:text-red-500", - borderColor: "dec:border-l-red-500 dec:dark:border-l-red-500", - }, - success: { - root: "dec:border-gray-200 dec:bg-white dec:text-gray-900 dec:dark:border-gray-700 dec:dark:bg-[#2d3748] dec:dark:text-gray-100", - close: - "dec:text-gray-600 hover:dec:text-gray-900 dec:dark:text-gray-400 dec:dark:hover:text-gray-100", - icon: CircleCheck, - iconColor: "dec:text-green-500", - borderColor: "dec:border-l-green-500 dec:dark:border-l-green-500", - }, - warning: { - root: "dec:border-gray-200 dec:bg-white dec:text-gray-900 dec:dark:border-gray-700 dec:dark:bg-[#2d3748] dec:dark:text-gray-100", - close: - "dec:text-gray-600 hover:dec:text-gray-900 dec:dark:text-gray-400 dec:dark:hover:text-gray-100", - icon: TriangleAlert, - iconColor: "dec:text-orange-500", - borderColor: "dec:border-l-orange-500 dec:dark:border-l-orange-500", - }, - info: { - root: "dec:border-gray-200 dec:bg-white dec:text-gray-900 dec:dark:border-gray-700 dec:dark:bg-[#2d3748] dec:dark:text-gray-100", - close: - "dec:text-gray-600 hover:dec:text-gray-900 dec:dark:text-gray-400 dec:dark:hover:text-gray-100", - icon: Info, - iconColor: "dec:text-blue-500", - borderColor: "dec:border-l-blue-500 dec:dark:border-l-blue-500", - }, -} as const; - -const TOAST_VARIANT_STYLES = { - ...TOAST_VARIANT_STYLES_BASE, - default: TOAST_VARIANT_STYLES_BASE.info, -} as const; - -const ToastItem = React.memo( - ({ toast, onDismiss }: { toast: ToastProps; onDismiss: (id: string) => void }) => { - const { id, open, variant = "default", title, description } = toast; - const styles = TOAST_VARIANT_STYLES[variant]; - const Icon = styles.icon; - - return ( - { - if (!nextOpen) onDismiss(id); - }} - data-slot="toast" - className={cn( - "dec:pointer-events-auto dec:relative dec:flex dec:w-full dec:min-w-[320px] dec:max-w-[680px] dec:items-start dec:gap-4 dec:rounded-[14px] dec:border dec:border-l-4 dec:px-7 dec:py-6 dec:shadow-[0_18px_38px_rgba(22,23,24,0.12),0_8px_16px_rgba(22,23,24,0.08)] dec:transition-all dec:data-[state=open]:animate-in dec:data-[state=closed]:animate-out dec:data-[state=closed]:fade-out-80 dec:data-[state=closed]:slide-out-to-left-full dec:data-[state=open]:slide-in-from-top-full", - styles.root, - styles.borderColor, - )} - > - {Icon && ( -
- -
- )} -
- {title && ( - - {title} - - )} - {description && ( - - {description} - - )} -
- onDismiss(id)} - className={cn( - "dec:-mt-0.5 dec:rounded-md dec:p-1 dec:opacity-100 dec:transition-colors", - styles.close, - )} - > - - -
- ); - }, -); - -ToastItem.displayName = "ToastItem"; - -function Toaster() { - const { toasts, dismiss } = useToast(); - - return ( - <> - {toasts.map((toast) => ( - - ))} - - - ); -} - -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 eb8c543f..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,8 +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/toast"; -import { Toast as ToastPrimitive } from "radix-ui"; +import { Toaster } from "@/components/ui/sonner"; /** * DiagramEditor component API @@ -132,9 +131,7 @@ export const DiagramEditor = (props: DiagramEditorProps) => { }} - - - + ); }; diff --git a/packages/serverless-workflow-diagram-editor/src/hooks/useToast.ts b/packages/serverless-workflow-diagram-editor/src/hooks/useToast.ts deleted file mode 100644 index f5051ce0..00000000 --- a/packages/serverless-workflow-diagram-editor/src/hooks/useToast.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 * as React from "react"; - -type ToastProps = { - id: string; - title?: React.ReactNode; - description?: React.ReactNode; - variant?: "default" | "error" | "success" | "warning" | "info"; - open: boolean; -}; - -type Action = - | { type: "ADD"; toast: Omit } - | { type: "DISMISS"; id: string } - | { type: "REMOVE"; id: string }; - -let toasts: ToastProps[] = []; -let toastCount = 0; -const listeners: Array<(toasts: ToastProps[]) => void> = []; - -function dispatch(action: Action) { - if (action.type === "ADD") { - toasts = [...toasts, { ...action.toast, open: true }]; - setTimeout(() => dispatch({ type: "DISMISS", id: action.toast.id }), 5000); - } else if (action.type === "DISMISS") { - toasts = toasts.map((t) => (t.id === action.id ? { ...t, open: false } : t)); - setTimeout(() => dispatch({ type: "REMOVE", id: action.id }), 300); - } else if (action.type === "REMOVE") { - toasts = toasts.filter((t) => t.id !== action.id); - } - listeners.forEach((listener) => listener(toasts)); -} - -function toast(props: Omit) { - const id = String(toastCount++); - dispatch({ type: "ADD", toast: { ...props, id } }); - return id; -} - -function useToast() { - const [state, setState] = React.useState(toasts); - - React.useEffect(() => { - listeners.push(setState); - return () => { - const index = listeners.indexOf(setState); - if (index > -1) listeners.splice(index, 1); - }; - }, []); - - return { - toasts: state, - show: toast, - dismiss: (id: string) => dispatch({ type: "DISMISS", id }), - }; -} - -export { useToast, toast }; 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 634dd06f..a7dc1559 100644 --- a/packages/serverless-workflow-diagram-editor/src/side-panel/MermaidActions.tsx +++ b/packages/serverless-workflow-diagram-editor/src/side-panel/MermaidActions.tsx @@ -22,7 +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 "@/hooks/useToast"; +import { toast } from "sonner"; export function MermaidActions({ model }: { model: Specification.Workflow }): React.JSX.Element { const { t } = useI18n(); @@ -52,9 +52,7 @@ export function MermaidActions({ model }: { model: Specification.Workflow }): Re copyTimeoutRef.current = null; }, 2000); } catch (error) { - toast({ - variant: "error", - title: t("toast.clipboard.error"), + toast.error(t("toast.clipboard.error"), { description: error instanceof Error ? error.message : undefined, }); } @@ -70,11 +68,9 @@ export function MermaidActions({ model }: { model: Specification.Workflow }): Re .substring(0, 200); const filename = `${sanitizedName}.mmd`; downloadFile(mermaidCode, filename); - toast({ variant: "success", title: t("toast.download.success") }); + toast.success(t("toast.download.success")); } catch (error) { - toast({ - variant: "error", - title: t("toast.download.error"), + toast.error(t("toast.download.error"), { description: error instanceof Error ? error.message : undefined, }); } 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 4e806e0c..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,7 +24,7 @@ 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 toastHook from "../../src/hooks/useToast"; +import * as sonner from "sonner"; describe("MermaidActions", () => { const toastMock = vi.fn(); @@ -57,7 +57,8 @@ describe("MermaidActions", () => { 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(toastHook, "toast").mockImplementation(toastMock); + vi.spyOn(sonner.toast, "error").mockImplementation(toastMock); + vi.spyOn(sonner.toast, "success").mockImplementation(toastMock); renderWithProviders(, { model }); @@ -67,11 +68,7 @@ describe("MermaidActions", () => { await user.click(copyButton); - expect(toastMock).toHaveBeenCalledWith({ - variant: "error", - title: expect.any(String), - description: "Clipboard error", - }); + expect(toastMock).toHaveBeenCalledWith(expect.any(String), { description: "Clipboard error" }); }); it("should call downloadMermaidFile and show success toast when download button is clicked", async () => { @@ -79,7 +76,8 @@ describe("MermaidActions", () => { const { model } = parseWorkflow(WORKFLOW_WITH_METADATA_JSON); const downloadSpy = vi.spyOn(download, "downloadFile").mockImplementation(() => {}); vi.spyOn(core, "exportToMermaid").mockReturnValue(MERMAID_CODE); - vi.spyOn(toastHook, "toast").mockImplementation(toastMock); + vi.spyOn(sonner.toast, "error").mockImplementation(toastMock); + vi.spyOn(sonner.toast, "success").mockImplementation(toastMock); renderWithProviders(, { model }); @@ -90,10 +88,7 @@ describe("MermaidActions", () => { await user.click(downloadButton); expect(downloadSpy).toHaveBeenCalledWith(MERMAID_CODE, "test-wf.mmd"); - expect(toastMock).toHaveBeenCalledWith({ - variant: "success", - title: expect.any(String), - }); + expect(toastMock).toHaveBeenCalledWith(expect.any(String)); }); it("should show error toast when download fails", async () => { @@ -103,7 +98,8 @@ describe("MermaidActions", () => { throw new Error("Download error"); }); vi.spyOn(core, "exportToMermaid").mockReturnValue(MERMAID_CODE); - vi.spyOn(toastHook, "toast").mockImplementation(toastMock); + vi.spyOn(sonner.toast, "error").mockImplementation(toastMock); + vi.spyOn(sonner.toast, "success").mockImplementation(toastMock); renderWithProviders(, { model }); @@ -113,10 +109,6 @@ describe("MermaidActions", () => { await user.click(downloadButton); - expect(toastMock).toHaveBeenCalledWith({ - variant: "error", - title: expect.any(String), - description: "Download error", - }); + expect(toastMock).toHaveBeenCalledWith(expect.any(String), { description: "Download error" }); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f9083fe..62df7c55 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -227,9 +227,15 @@ importers: js-yaml: specifier: 'catalog:' version: 4.2.0 + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) 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: ^2.0.7 + 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) @@ -3220,6 +3226,12 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -3562,6 +3574,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'} @@ -6684,6 +6702,11 @@ snapshots: nanoid@3.3.12: {} + next-themes@0.4.6(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) + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -7117,6 +7140,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..c152fec3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -31,6 +31,7 @@ catalog: "js-yaml": ^4.2.0 jsdom: ^29.1.0 lucide-react: ^1.21.0 + next-themes: ^0.4.6 lint-staged: ^17.0.8 oxfmt: ^0.55.0 oxlint: ^1.70.0 @@ -38,6 +39,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 From d7e6388272ea5c7686bcbe69600b66a43559ce09 Mon Sep 17 00:00:00 2001 From: Cheryl Kong Date: Thu, 2 Jul 2026 11:03:50 +0100 Subject: [PATCH 07/13] fix copilot complaints Signed-off-by: Cheryl Kong --- .../package.json | 1 - .../src/components/ui/sonner.css | 20 +++++++++---------- pnpm-lock.yaml | 19 ++++-------------- pnpm-workspace.yaml | 1 - 4 files changed, 14 insertions(+), 27 deletions(-) diff --git a/packages/serverless-workflow-diagram-editor/package.json b/packages/serverless-workflow-diagram-editor/package.json index f2f99c82..8b16b018 100644 --- a/packages/serverless-workflow-diagram-editor/package.json +++ b/packages/serverless-workflow-diagram-editor/package.json @@ -48,7 +48,6 @@ "clsx": "catalog:", "elkjs": "catalog:", "js-yaml": "catalog:", - "next-themes": "catalog:", "radix-ui": "catalog:", "sonner": "catalog:", "use-sync-external-store": "catalog:" diff --git a/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.css b/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.css index 015ab222..bb1bca6e 100644 --- a/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.css +++ b/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.css @@ -15,15 +15,15 @@ */ @layer sonner { - [data-sonner-toaster] { --width: 300px !important; } - [data-sonner-toast] { background: #ffffff !important; border-radius: 10px !important; border: 1px solid #e9edf4 !important; } - [data-sonner-toast][data-type="success"] { border-left: 4px solid #22c55e !important; } - [data-sonner-toast][data-type="error"] { border-left: 4px solid #ef4444 !important; } - [data-sonner-toast][data-type="warning"] { border-left: 4px solid #f97316 !important; } - [data-sonner-toast][data-type="info"] { border-left: 4px solid #3b82f6 !important; } - .dark [data-sonner-toast] { background: #2d3748 !important; color: #ffffff !important; border-top-color: #4a5568 !important; border-right-color: #4a5568 !important; border-bottom-color: #4a5568 !important; } - .dark [data-sonner-toast] [data-title], .dark [data-sonner-toast] [data-description] { color: #ffffff !important; } - [data-sonner-toast] [data-close-button] { + .dec-root [data-sonner-toaster] { --width: 300px !important; } + .dec-root [data-sonner-toast] { background: #ffffff !important; border-radius: 10px !important; border: 1px solid #e9edf4 !important; } + .dec-root [data-sonner-toast][data-type="success"] { border-left: 4px solid #22c55e !important; } + .dec-root [data-sonner-toast][data-type="error"] { border-left: 4px solid #ef4444 !important; } + .dec-root [data-sonner-toast][data-type="warning"] { border-left: 4px solid #f97316 !important; } + .dec-root [data-sonner-toast][data-type="info"] { border-left: 4px solid #3b82f6 !important; } + .dec-root.dark [data-sonner-toast] { background: #2d3748 !important; color: #ffffff !important; border-top-color: #4a5568 !important; border-right-color: #4a5568 !important; border-bottom-color: #4a5568 !important; } + .dec-root.dark [data-sonner-toast] [data-title], .dec-root.dark [data-sonner-toast] [data-description] { color: #ffffff !important; } + .dec-root [data-sonner-toast] [data-close-button] { position: absolute !important; top: 50% !important; right: 12px !important; @@ -35,5 +35,5 @@ color: #6b7280; cursor: pointer; } - .dark [data-sonner-toast] [data-close-button] { color: #ffffff; } + .dec-root.dark [data-sonner-toast] [data-close-button] { color: #ffffff; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62df7c55..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 @@ -227,14 +230,11 @@ importers: js-yaml: specifier: 'catalog:' version: 4.2.0 - next-themes: - specifier: ^0.4.6 - version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) 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: ^2.0.7 + 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:' @@ -3226,12 +3226,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - next-themes@0.4.6: - resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} - peerDependencies: - react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -6702,11 +6696,6 @@ snapshots: nanoid@3.3.12: {} - next-themes@0.4.6(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) - node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c152fec3..b38b2b2c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -31,7 +31,6 @@ catalog: "js-yaml": ^4.2.0 jsdom: ^29.1.0 lucide-react: ^1.21.0 - next-themes: ^0.4.6 lint-staged: ^17.0.8 oxfmt: ^0.55.0 oxlint: ^1.70.0 From 4d6f0dff9e52252941c1f9df3eceed60949a0eef Mon Sep 17 00:00:00 2001 From: Cheryl Kong Date: Thu, 2 Jul 2026 11:06:23 +0100 Subject: [PATCH 08/13] fix: add license header Signed-off-by: Cheryl Kong --- .../src/components/ui/sonner.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.tsx b/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.tsx index 739da8a4..de31addf 100644 --- a/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.tsx +++ b/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.tsx @@ -1,3 +1,19 @@ +/* + * 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, From 5483b9b037c832653e2df5cff59e2462ed0b1fd3 Mon Sep 17 00:00:00 2001 From: Cheryl Kong Date: Fri, 3 Jul 2026 11:16:46 +0100 Subject: [PATCH 09/13] fix: replace hardcoded colors with css variables in toast Signed-off-by: Cheryl Kong --- .../src/components/ui/sonner.css | 19 +++++++++-------- .../src/components/ui/sonner.tsx | 20 +++++++----------- .../src/styles.css | 21 ++++++++++++++++--- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.css b/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.css index bb1bca6e..c1896d09 100644 --- a/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.css +++ b/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.css @@ -13,16 +13,18 @@ * 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: #ffffff !important; border-radius: 10px !important; border: 1px solid #e9edf4 !important; } - .dec-root [data-sonner-toast][data-type="success"] { border-left: 4px solid #22c55e !important; } - .dec-root [data-sonner-toast][data-type="error"] { border-left: 4px solid #ef4444 !important; } - .dec-root [data-sonner-toast][data-type="warning"] { border-left: 4px solid #f97316 !important; } - .dec-root [data-sonner-toast][data-type="info"] { border-left: 4px solid #3b82f6 !important; } - .dec-root.dark [data-sonner-toast] { background: #2d3748 !important; color: #ffffff !important; border-top-color: #4a5568 !important; border-right-color: #4a5568 !important; border-bottom-color: #4a5568 !important; } - .dec-root.dark [data-sonner-toast] [data-title], .dec-root.dark [data-sonner-toast] [data-description] { color: #ffffff !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; @@ -32,8 +34,7 @@ background: transparent !important; border: none !important; box-shadow: none !important; - color: #6b7280; + color: var(--dec-toast-close-btn); cursor: pointer; } - .dec-root.dark [data-sonner-toast] [data-close-button] { color: #ffffff; } } diff --git a/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.tsx b/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.tsx index de31addf..d5b9e2b6 100644 --- a/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.tsx +++ b/packages/serverless-workflow-diagram-editor/src/components/ui/sonner.tsx @@ -32,21 +32,17 @@ const Toaster = ({ ...props }: ToasterProps) => { className="dec:toaster dec:group" closeButton icons={{ - success: , - info: , - warning: , - error: , + success: ( + + ), + info: , + warning: ( + + ), + error: , loading: , close: , }} - style={ - { - "--normal-bg": "var(--popover)", - "--normal-text": "var(--popover-foreground)", - "--normal-border": "var(--border)", - "--border-radius": "var(--radius)", - } as React.CSSProperties - } {...props} /> ); diff --git a/packages/serverless-workflow-diagram-editor/src/styles.css b/packages/serverless-workflow-diagram-editor/src/styles.css index 486f72d5..85b1d43f 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'; @@ -49,13 +50,27 @@ --dec-error-glow: rgba(239, 68, 68, 0.35); --dec-edge-selected: #aea6a6; --dec-edge-selected-condition: rgb(59 130 246); + --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{ - --dec-error-accent: #f87171; + .dec-root.dark { + --dec-error-accent: #f87171; --dec-error-glow: rgba(248, 113, 113, 0.4); --dec-edge-selected: #aea6a6; --dec-edge-selected-condition: rgb(59 130 246); + --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 { From 12cd317520a293154ae18297a4cff4be7471c7e7 Mon Sep 17 00:00:00 2001 From: lornakelly Date: Tue, 30 Jun 2026 11:18:36 +0100 Subject: [PATCH 10/13] feat: Styling tweaks (#206) Signed-off-by: Cheryl Kong --- .changeset/style-minimap.md | 5 +++++ .../src/react-flow/diagram/Diagram.css | 21 +++++++++++++++++++ .../src/react-flow/diagram/Diagram.tsx | 4 +++- .../src/side-panel/MermaidActions.tsx | 14 +++++++++++-- .../src/styles.css | 18 ++++++++++++++++ 5 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 .changeset/style-minimap.md diff --git a/.changeset/style-minimap.md b/.changeset/style-minimap.md new file mode 100644 index 00000000..cfe20a52 --- /dev/null +++ b/.changeset/style-minimap.md @@ -0,0 +1,5 @@ +--- +"@serverlessworkflow/diagram-editor": patch +--- + +Small styling tweaks to minimap and buttons diff --git a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css index 42d7c676..5b40fff5 100644 --- a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css +++ b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css @@ -65,6 +65,27 @@ min-width: 0 !important; min-height: 0 !important; } + + /* minimap */ + .dec-root .react-flow__minimap{ + --xy-minimap-background-color: var(--dec-minimap-bg); + --xy-minimap-mask-background-color: var(--dec-minimap-mask); + --xy-minimap-mask-stroke-color: var(--dec-minimap-viewport); + --xy-minimap-node-background-color: var(--dec-minimap-node); + --xy-minimap-node-stroke-color: var(--dec-minimap-node-stroke); + --xy-minimap-node-stroke-width: 2; + + border: 1px solid var(--dec-minimap-border); + border-radius:8px; + box-shadow: 0 4px 12px var(--dec-minimap-shadow); + overflow: hidden; +} + + .dec-root .react-flow__minimap-node { + fill: var(--dec-minimap-node); + stroke: var(--dec-minimap-node-stroke); + stroke-width: 2; +} } /* custom nodes */ diff --git a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx index 145f54a0..36b58aee 100644 --- a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx +++ b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx @@ -178,7 +178,9 @@ export const Diagram = ({ divRef, ref, colorMode = "light" }: DiagramProps) => { nodesDraggable={!isReadOnly} nodesConnectable={!isReadOnly} > - {minimapVisible && } + {minimapVisible && ( + + )} 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 a7dc1559..889c6e29 100644 --- a/packages/serverless-workflow-diagram-editor/src/side-panel/MermaidActions.tsx +++ b/packages/serverless-workflow-diagram-editor/src/side-panel/MermaidActions.tsx @@ -78,11 +78,21 @@ export function MermaidActions({ model }: { model: Specification.Workflow }): Re return ( <> - - diff --git a/packages/serverless-workflow-diagram-editor/src/styles.css b/packages/serverless-workflow-diagram-editor/src/styles.css index 85b1d43f..54312fc6 100644 --- a/packages/serverless-workflow-diagram-editor/src/styles.css +++ b/packages/serverless-workflow-diagram-editor/src/styles.css @@ -50,6 +50,15 @@ --dec-error-glow: rgba(239, 68, 68, 0.35); --dec-edge-selected: #aea6a6; --dec-edge-selected-condition: rgb(59 130 246); + + /* minimap */ + --dec-minimap-bg: #f8fafc; + --dec-minimap-mask: rgba(100, 116, 139, 0.25); + --dec-minimap-viewport: rgb(59 130 246); + --dec-minimap-node: #94a3b8; + --dec-minimap-node-stroke: #64748b; + --dec-minimap-border: #cbd5e1; + --dec-minimap-shadow: rgba(15, 23, 42, 0.12); --dec-toast-bg: #ffffff; --dec-toast-border: #e9edf4; --dec-toast-success: #22c55e; @@ -64,6 +73,15 @@ --dec-error-glow: rgba(248, 113, 113, 0.4); --dec-edge-selected: #aea6a6; --dec-edge-selected-condition: rgb(59 130 246); + + /* minimap */ + --dec-minimap-bg: #1a202c; + --dec-minimap-mask: rgba(2, 6, 23, 0.5); + --dec-minimap-viewport: rgb(96 165 250); + --dec-minimap-node: #94a3b8; + --dec-minimap-node-stroke: #cbd5e1; + --dec-minimap-border: #2d3748; + --dec-minimap-shadow: rgba(0, 0, 0, 0.5); --dec-toast-bg: #2d3748; --dec-toast-border: #4a5568; --dec-toast-success: #4ade80; From 2c323fd990892220aa7f48c7d44e038b5c4ffefb Mon Sep 17 00:00:00 2001 From: Handreyrc <47090342+handreyrc@users.noreply.github.com> Date: Tue, 30 Jun 2026 09:00:46 -0400 Subject: [PATCH 11/13] Fix edge labels rendered behind nodes and selected edges (#203) Signed-off-by: Cheryl Kong --- .../src/react-flow/diagram/Diagram.css | 7 ++ .../src/react-flow/diagram/Diagram.tsx | 17 ++-- .../src/react-flow/edges/Edges.tsx | 5 +- .../src/react-flow/zIndexConstants.ts | 43 +++++++++ .../src/styles.css | 3 + .../tests/react-flow/diagram/Diagram.test.tsx | 88 ++++++++++++++---- .../tests/react-flow/edges/Edges.test.tsx | 89 +++++++++++++++++++ 7 files changed, 228 insertions(+), 24 deletions(-) create mode 100644 packages/serverless-workflow-diagram-editor/src/react-flow/zIndexConstants.ts diff --git a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css index 5b40fff5..40f49d07 100644 --- a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css +++ b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css @@ -18,6 +18,13 @@ /* react flow overrides */ @layer react-flow-overrides { + /* Elevate the edge-label renderer container above nodes. + React Flow renders .react-flow__edgelabel-renderer before NodeRenderer + in the DOM, so without an explicit z-index it is painted behind nodes. */ + .dec-root .react-flow__edgelabel-renderer { + z-index: var(--dec-zindex-edge-label-regular); + } + .dec-root .diagram-background { --xy-background-pattern-color: #ccc; background-color: #e5e4e2; diff --git a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx index 36b58aee..a969dfe0 100644 --- a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx +++ b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx @@ -26,6 +26,7 @@ import { useDiagramEditorContext } from "../../store/DiagramEditorContext"; import { buildDiagramElements } from "./diagramBuilder"; import { applyAutoLayout } from "./autoLayout"; import { SidePanelTrigger } from "@/side-panel/SidePanelTrigger"; +import { ZINDEX } from "../zIndexConstants"; const FIT_VIEW_OPTIONS: RF.FitViewOptions = { maxZoom: 1, @@ -33,6 +34,12 @@ const FIT_VIEW_OPTIONS: RF.FitViewOptions = { duration: 400, }; +const applyEdgeZIndex = (edges: T[]): T[] => + edges.map((edge) => ({ + ...edge, + zIndex: edge.selected ? ZINDEX.EDGE_SELECTED : ZINDEX.EDGE_REGULAR, + })); + /** * Diagram component API */ @@ -73,12 +80,7 @@ export const Diagram = ({ divRef, ref, colorMode = "light" }: DiagramProps) => { (changes) => { setEdges((edgesSnapshot) => { const updatedEdges = RF.applyEdgeChanges(changes, edgesSnapshot); - - // Update zIndex for selected edges to bring them to front - return updatedEdges.map((edge) => ({ - ...edge, - zIndex: edge.selected ? 1000 : 0, - })); + return applyEdgeZIndex(updatedEdges); }); }, [setEdges], @@ -107,7 +109,7 @@ export const Diagram = ({ divRef, ref, colorMode = "light" }: DiagramProps) => { // Only update if this effect is still active (not cancelled by cleanup) if (isActive && !abortController?.signal.aborted) { setNodes(nodes); - setEdges(edges); + setEdges(applyEdgeZIndex(edges)); // Queue fitView to run after React updates the DOM fitViewTimeoutId = setTimeout(() => reactFlowInstance.fitView(), 0); @@ -175,6 +177,7 @@ export const Diagram = ({ divRef, ref, colorMode = "light" }: DiagramProps) => { }, }} data-testid={"react-flow-canvas"} + elevateEdgesOnSelect={false} nodesDraggable={!isReadOnly} nodesConnectable={!isReadOnly} > diff --git a/packages/serverless-workflow-diagram-editor/src/react-flow/edges/Edges.tsx b/packages/serverless-workflow-diagram-editor/src/react-flow/edges/Edges.tsx index 5a031e76..40fdb2a0 100644 --- a/packages/serverless-workflow-diagram-editor/src/react-flow/edges/Edges.tsx +++ b/packages/serverless-workflow-diagram-editor/src/react-flow/edges/Edges.tsx @@ -16,6 +16,7 @@ import * as RF from "@xyflow/react"; import type { Point, WayPoints } from "../diagram/autoLayout"; +import { ZINDEX } from "../zIndexConstants"; export enum EdgeTypes { Default = "default", @@ -43,6 +44,7 @@ export type EdgeLabelProps = { targetPosition?: RF.Position; type?: EdgeTypes; data?: BaseEdgeData | undefined; + selected?: boolean; }; export function createPathFromWayPoints( @@ -127,7 +129,7 @@ function getEdgeLabelPosition({ } export function EdgeLabel(props: EdgeLabelProps) { - const { type, data } = props; + const { type, data, selected } = props; const { x, y } = getEdgeLabelPosition(props); return ( @@ -137,6 +139,7 @@ export function EdgeLabel(props: EdgeLabelProps) {
diff --git a/packages/serverless-workflow-diagram-editor/src/react-flow/zIndexConstants.ts b/packages/serverless-workflow-diagram-editor/src/react-flow/zIndexConstants.ts new file mode 100644 index 00000000..b4cda4f3 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/src/react-flow/zIndexConstants.ts @@ -0,0 +1,43 @@ +/* + * 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. + */ + +/** + * Z-index layering constants for diagram edges and labels. + * + * Layering hierarchy (bottom to top): + * 1. Regular edges (0) + * 2. Selected edges (100) + * 3. Edge labels - regular (1000) + * 4. Edge labels - selected (1001) + * + * This ensures: + * - Selected edges appear above regular edges + * - All labels appear above all edges (preventing overlap) + * - Selected edge labels appear above regular labels + */ +export const ZINDEX = { + /** Regular (unselected) edges */ + EDGE_REGULAR: 0, + + /** Selected edges - appear above regular edges but below all labels */ + EDGE_SELECTED: 100, + + /** Regular (unselected) edge labels - appear above all edges */ + EDGE_LABEL_REGULAR: 1000, + + /** Selected edge labels - appear above everything */ + EDGE_LABEL_SELECTED: 1001, +} as const; diff --git a/packages/serverless-workflow-diagram-editor/src/styles.css b/packages/serverless-workflow-diagram-editor/src/styles.css index 54312fc6..25bfaa05 100644 --- a/packages/serverless-workflow-diagram-editor/src/styles.css +++ b/packages/serverless-workflow-diagram-editor/src/styles.css @@ -46,6 +46,9 @@ .dec-root { @apply dec:h-full; + /* Z-index layering for edges and labels */ + --dec-zindex-edge-label-regular: 1000; + --dec-error-accent: #ef4444; --dec-error-glow: rgba(239, 68, 68, 0.35); --dec-edge-selected: #aea6a6; diff --git a/packages/serverless-workflow-diagram-editor/tests/react-flow/diagram/Diagram.test.tsx b/packages/serverless-workflow-diagram-editor/tests/react-flow/diagram/Diagram.test.tsx index fbcc9b55..4628d21d 100644 --- a/packages/serverless-workflow-diagram-editor/tests/react-flow/diagram/Diagram.test.tsx +++ b/packages/serverless-workflow-diagram-editor/tests/react-flow/diagram/Diagram.test.tsx @@ -24,6 +24,7 @@ import { en } from "../../../src/i18n/locales/en"; import { ReactFlowProvider, ReactFlow } from "@xyflow/react"; import * as RF from "@xyflow/react"; import * as autoLayoutModule from "../../../src/react-flow/diagram/autoLayout"; +import { ZINDEX } from "../../../src/react-flow/zIndexConstants"; // Mock ReactFlow to capture props vi.mock("@xyflow/react", async () => { @@ -183,27 +184,51 @@ describe("Diagram Component", () => { }); }); - describe("onEdgesChange with zIndex updates", () => { - it("should provide onEdgesChange callback to ReactFlow", async () => { + it("should disable automatic edge elevation on select", async () => { + renderDiagram({ isReadOnly: false }); + + // Wait for ReactFlow to be called + await waitFor(() => { + expect(ReactFlow).toHaveBeenCalled(); + }); + + // Verify that ReactFlow was called with elevateEdgesOnSelect={false} + const mockReactFlow = vi.mocked(ReactFlow); + const lastCall = mockReactFlow.mock.calls.at(-1); + expect(lastCall).toBeDefined(); + const reactFlowProps = lastCall![0]; + expect(reactFlowProps.elevateEdgesOnSelect).toBe(false); + + await waitFor(() => { + expect(applyAutoLayoutSpy).toHaveBeenCalled(); + }); + }); + + describe("edge z-index management", () => { + it("should apply correct z-index to edges based on selection state", async () => { + applyAutoLayoutSpy.mockResolvedValueOnce({ + nodes: [], + edges: [ + { id: "edge1", source: "n1", target: "n2", selected: false }, + { id: "edge2", source: "n2", target: "n3", selected: false }, + ], + }); + renderDiagram({ isReadOnly: false }); - // Wait for initial render await waitFor(() => { - expect(applyAutoLayoutSpy).toHaveBeenCalled(); + const lastCall = vi.mocked(ReactFlow).mock.calls.at(-1); + expect(lastCall).toBeDefined(); + expect(lastCall![0].edges).toHaveLength(2); }); - // Get the onEdgesChange callback from ReactFlow mock - const mockReactFlow = vi.mocked(ReactFlow); - const lastCall = mockReactFlow.mock.calls.at(-1); - expect(lastCall).toBeDefined(); - const reactFlowProps = lastCall![0]; - const onEdgesChange = reactFlowProps.onEdgesChange; - - expect(onEdgesChange).toBeDefined(); - expect(typeof onEdgesChange).toBe("function"); + // All unselected edges should have zIndex: 0 + const edges = vi.mocked(ReactFlow).mock.calls.at(-1)![0].edges!; + expect(edges.find((e: RF.Edge) => e.id === "edge1")?.zIndex).toBe(ZINDEX.EDGE_REGULAR); + expect(edges.find((e: RF.Edge) => e.id === "edge2")?.zIndex).toBe(ZINDEX.EDGE_REGULAR); }); - it("should apply zIndex correctly when edges are updated", async () => { + it("should elevate selected edge above regular edges but below labels", async () => { applyAutoLayoutSpy.mockResolvedValueOnce({ nodes: [], edges: [ @@ -232,8 +257,39 @@ describe("Diagram Component", () => { await waitFor(() => { const edges = vi.mocked(ReactFlow).mock.calls.at(-1)![0].edges!; - expect(edges.find((e: RF.Edge) => e.id === "edge1")?.zIndex).toBe(1000); - expect(edges.find((e: RF.Edge) => e.id === "edge2")?.zIndex).toBe(1000); + // Selected edges should have elevated z-index (above regular edges, below labels) + expect(edges.find((e: RF.Edge) => e.id === "edge1")?.zIndex).toBe(ZINDEX.EDGE_SELECTED); + expect(edges.find((e: RF.Edge) => e.id === "edge2")?.zIndex).toBe(ZINDEX.EDGE_SELECTED); + }); + }); + + it("should maintain z-index hierarchy: regular edges (0) < selected edges (100) < labels (1000+)", async () => { + applyAutoLayoutSpy.mockResolvedValueOnce({ + nodes: [], + edges: [ + { id: "edge1", source: "n1", target: "n2", selected: true }, + { id: "edge2", source: "n2", target: "n3", selected: false }, + ], + }); + + renderDiagram({ isReadOnly: false }); + + await waitFor(() => { + const lastCall = vi.mocked(ReactFlow).mock.calls.at(-1); + expect(lastCall).toBeDefined(); + const edges = lastCall![0].edges!; + + // Verify z-index hierarchy + const selectedEdge = edges.find((e: RF.Edge) => e.id === "edge1"); + const regularEdge = edges.find((e: RF.Edge) => e.id === "edge2"); + + expect(selectedEdge?.zIndex).toBe(ZINDEX.EDGE_SELECTED); + expect(regularEdge?.zIndex).toBe(ZINDEX.EDGE_REGULAR); + + // Hierarchy in the test + // Regular edges: 0 + // Selected edges: 100 + // Edge labels: 1000+ (tested in Edges.test.tsx) }); }); }); diff --git a/packages/serverless-workflow-diagram-editor/tests/react-flow/edges/Edges.test.tsx b/packages/serverless-workflow-diagram-editor/tests/react-flow/edges/Edges.test.tsx index 7f48f0c5..734376f0 100644 --- a/packages/serverless-workflow-diagram-editor/tests/react-flow/edges/Edges.test.tsx +++ b/packages/serverless-workflow-diagram-editor/tests/react-flow/edges/Edges.test.tsx @@ -27,6 +27,7 @@ import { createPathFromWayPoints, getWayPointsMidpoint, } from "../../../src/react-flow/edges/Edges"; +import { ZINDEX } from "../../../src/react-flow/zIndexConstants"; import * as RF from "@xyflow/react"; describe("React Flow custom edge types", () => { @@ -498,3 +499,91 @@ describe("EdgeLabel positioning", () => { expect(JSON.stringify(result)).toContain(`translate(${labelX}px,${labelY}px)`); }); }); + +describe("EdgeLabel z-index behavior", () => { + it.each([ + { + selected: false, + expectedZIndex: ZINDEX.EDGE_LABEL_REGULAR, + description: "applies regular label z-index when selected=false", + }, + { + selected: true, + expectedZIndex: ZINDEX.EDGE_LABEL_SELECTED, + description: "applies selected label z-index when selected=true", + }, + { + selected: undefined, + expectedZIndex: ZINDEX.EDGE_LABEL_REGULAR, + description: "applies default regular label z-index when selected=undefined", + }, + ])("$description", ({ selected, expectedZIndex }) => { + const result = EdgeLabel({ + sourceX: 0, + sourceY: 0, + targetX: 100, + targetY: 100, + data: { label: "Test Label" }, + selected, + }); + + const resultString = JSON.stringify(result); + expect(resultString).toContain(`"zIndex":${expectedZIndex}`); + }); + + it("applies default regular label z-index when selected prop is not provided", () => { + const result = EdgeLabel({ + sourceX: 0, + sourceY: 0, + targetX: 100, + targetY: 100, + data: { label: "Test Label" }, + }); + + const resultString = JSON.stringify(result); + expect(resultString).toContain(`"zIndex":${ZINDEX.EDGE_LABEL_REGULAR}`); + }); + + describe("integration with edge components", () => { + it.each([ + { component: DefaultEdge, edgeType: "default", edgeClass: "", selected: true }, + { component: ErrorEdge, edgeType: "error", edgeClass: "error", selected: true }, + { component: ConditionEdge, edgeType: "condition", edgeClass: "condition", selected: true }, + { component: DefaultEdge, edgeType: "default", edgeClass: "", selected: false }, + { component: ErrorEdge, edgeType: "error", edgeClass: "error", selected: false }, + { component: ConditionEdge, edgeType: "condition", edgeClass: "condition", selected: false }, + ])( + "$edgeType edge applies selected class when selected=$selected", + ({ component: Component, edgeClass, selected }) => { + const { container } = render( + + + , + ); + + // Verify the edge path is rendered with correct selected class + const pathSelector = edgeClass ? `path.edge-line.${edgeClass}` : "path.edge-line"; + const path = container.querySelector(pathSelector); + expect(path).toBeInTheDocument(); + + if (selected) { + expect(path).toHaveClass("selected"); + } else { + expect(path).not.toHaveClass("selected"); + } + }, + ); + }); +}); From d4cf4571e49f04768a0f4cdf504a304206f05faf Mon Sep 17 00:00:00 2001 From: Kumar Aditya Raj Date: Thu, 2 Jul 2026 19:22:22 +0530 Subject: [PATCH 12/13] Style start/end node components (#202) * start end node styling Signed-off-by: kumaradityaraj * Copilot and team review Signed-off-by: kumaradityaraj * updating pmpl from upstream Signed-off-by: kumaradityaraj * Copilot reviews Signed-off-by: kumaradityaraj * colour fixing Signed-off-by: kumaradityaraj * fixing colour Signed-off-by: kumaradityaraj * node unclickable Signed-off-by: kumaradityaraj * size made small Signed-off-by: kumaradityaraj * running pnpm changeset Signed-off-by: kumaradityaraj * naming changeset file Signed-off-by: kumaradityaraj * removed custom node container Signed-off-by: kumaradityaraj * resolving warnings Signed-off-by: kumaradityaraj --------- Signed-off-by: kumaradityaraj Signed-off-by: Cheryl Kong --- .changeset/style-start-end-node.md | 5 + .../src/react-flow/diagram/Diagram.css | 118 ++++++++++-------- .../src/react-flow/nodes/Nodes.tsx | 39 +++--- 3 files changed, 90 insertions(+), 72 deletions(-) create mode 100644 .changeset/style-start-end-node.md diff --git a/.changeset/style-start-end-node.md b/.changeset/style-start-end-node.md new file mode 100644 index 00000000..238b1c7d --- /dev/null +++ b/.changeset/style-start-end-node.md @@ -0,0 +1,5 @@ +--- +"@serverlessworkflow/diagram-editor": minor +--- + +Styled Start/End nodes as Mermaid-like start/end circles. diff --git a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css index 40f49d07..fb649f6a 100644 --- a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css +++ b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css @@ -74,25 +74,25 @@ } /* minimap */ - .dec-root .react-flow__minimap{ + .dec-root .react-flow__minimap { --xy-minimap-background-color: var(--dec-minimap-bg); --xy-minimap-mask-background-color: var(--dec-minimap-mask); --xy-minimap-mask-stroke-color: var(--dec-minimap-viewport); --xy-minimap-node-background-color: var(--dec-minimap-node); --xy-minimap-node-stroke-color: var(--dec-minimap-node-stroke); --xy-minimap-node-stroke-width: 2; - + border: 1px solid var(--dec-minimap-border); - border-radius:8px; + border-radius: 8px; box-shadow: 0 4px 12px var(--dec-minimap-shadow); overflow: hidden; -} + } .dec-root .react-flow__minimap-node { fill: var(--dec-minimap-node); stroke: var(--dec-minimap-node-stroke); stroke-width: 2; -} + } } /* custom nodes */ @@ -108,44 +108,6 @@ @apply dec:text-white; } - .dec-root .custom-node-container { - @apply dec:rounded-[5px] - dec:bg-white - dec:border dec:border-black - dec:transition-[border,box-shadow] - dec:h-full - dec:w-full; - } - - .dec-root.dark .custom-node-container { - @apply dec:bg-[#2d3748] - dec:border-[#e5e4e2]; - } - - .dec-root .custom-node-container:hover { - @apply dec:hover:border dec:hover:border-black - dec:hover:shadow-[0_0_10px_rgba(0,65,208,0.3)] - dec:hover:h-full - dec:hover:w-full; - } - - .dec-root.dark .custom-node-container:hover { - @apply dec:hover:border-[#5dafd5] - dec:hover:shadow-[0_0_10px_rgba(255,255,255,0.3)]; - } - - .dec-root .custom-node-container.selected { - @apply dec:bg-[#aebbd5] - dec:border dec:border-black - dec:shadow-[0_0_10px_rgba(1,79,248,0.3)]; - } - - .dec-root.dark .custom-node-container.selected { - @apply dec:bg-[#727676] - dec:border-[#4324dc] - dec:shadow-[0_0_10px_rgba(255,255,255,0.3)]; - } - /* Hover + selection are shared by every interactive node (leaf + container) */ .dec-root .dec-leaf-node:hover, .dec-root .dec-container-node:hover { @@ -316,13 +278,21 @@ .dec-root .dec-task-node-badge, .dec-root .dec-task-node-badge-custom { color: color-mix(in srgb, var(--task-node-color) 60%, black); - background-color: color-mix(in srgb, var(--task-node-color) 12%, transparent); + background-color: color-mix( + in srgb, + var(--task-node-color) 12%, + transparent + ); } .dec-root.dark .dec-task-node-badge, .dec-root.dark .dec-task-node-badge-custom { color: color-mix(in srgb, var(--task-node-color) 18%, white); - background-color: color-mix(in srgb, var(--task-node-color) 40%, transparent); + background-color: color-mix( + in srgb, + var(--task-node-color) 40%, + transparent + ); } .dec-root .dec-task-node-badge { @@ -429,7 +399,53 @@ stroke-width: 2.25; } - /* end validation errors */ + /* end validation errors */ + + .dec-root .dec-start-end-node { + position: relative; + display: flex; + justify-content: center; + align-items: center; + } + + .dec-root .dec-start-node, .dec-root .dec-end-node{ + width: 45px; + height: 45px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + position: relative; + + @apply dec:border + dec:border-slate-400 + dec:bg-slate-100; + } + + .dec-root .dec-end-node-inner { + position: absolute; + inset: 6px; + border-radius: 50%; + pointer-events: none; + + @apply dec:border + dec:border-slate-400 + dec:bg-slate-100; + } + + .dec-root.dark .dec-start-node, + .dec-root.dark .dec-end-node { + @apply dec:border + dec:border-slate-600 + dec:bg-slate-800 + dec:text-slate-300; + } + + .dec-root.dark .dec-end-node-inner { + @apply dec:border + dec:border-slate-600 + dec:bg-slate-800; + } } /* custom edges */ @@ -442,7 +458,7 @@ .dec-root .edge-line.error { stroke: var(--dec-error-accent); - @apply dec:[stroke-dasharray:5_5]; + @apply dec:[stroke-dasharray:5_5]; } .dec-root .edge-line.condition { @@ -465,14 +481,14 @@ /* Selected edge shadow effect - lighter for light mode */ .dec-root .edge-line.selected { filter: drop-shadow(0 0 2px rgba(59, 130, 246, 0.8)) - drop-shadow(0 0 6px rgba(59, 130, 246, 0.4)) - drop-shadow(0 0 12px rgba(59, 130, 246, 0.2)); + drop-shadow(0 0 6px rgba(59, 130, 246, 0.4)) + drop-shadow(0 0 12px rgba(59, 130, 246, 0.2)); } .dec-root.dark .edge-line.selected { filter: drop-shadow(0 0 3px rgb(59 130 246)) - drop-shadow(0 0 8px rgb(59 130 246)) - drop-shadow(0 0 16px rgba(59, 130, 246, 0.8)); + drop-shadow(0 0 8px rgb(59 130 246)) + drop-shadow(0 0 16px rgba(59, 130, 246, 0.8)); } /* custom edge labels */ diff --git a/packages/serverless-workflow-diagram-editor/src/react-flow/nodes/Nodes.tsx b/packages/serverless-workflow-diagram-editor/src/react-flow/nodes/Nodes.tsx index 6b338a29..fa8fefbc 100644 --- a/packages/serverless-workflow-diagram-editor/src/react-flow/nodes/Nodes.tsx +++ b/packages/serverless-workflow-diagram-editor/src/react-flow/nodes/Nodes.tsx @@ -188,42 +188,39 @@ function TerminalNodeContent({ id, type }: { id: string; type: TerminalNodeType ); } -// TODO: These props are just a placeholder -interface PlaceholderProps { - id: string; - data: BaseNodeData; - selected: boolean; - type: string; -} - -// TODO: This content is just a placeholder -function PlaceholderContent({ id, data, selected, type }: PlaceholderProps) { +function StartEndNode({ id, type }: { id: string; type: GraphNodeType.Start | GraphNodeType.End }) { + const isStart = type === GraphNodeType.Start; return (
e.stopPropagation()} + onKeyUp={(e) => e.stopPropagation()} + role="presentation" > - -
- {`${type}\n${data.label}`} + {isStart ? ( + + ) : ( + + )} + +
+ {!isStart &&
}
-
); } /* start node */ export type StartNodeType = RF.Node; -export function StartNode({ id, data, selected, type }: RF.NodeProps) { - // TODO: This component is just a placeholder - return ; +export function StartNode({ id, type }: RF.NodeProps) { + return ; } /* end node */ export type EndNodeType = RF.Node; -export function EndNode({ id, data, selected, type }: RF.NodeProps) { - // TODO: This component is just a placeholder - return ; +export function EndNode({ id, type }: RF.NodeProps) { + return ; } /* entry node */ From 3182be7f78e4d8d25357b98de81c21bfdc8a579e Mon Sep 17 00:00:00 2001 From: Cheryl Kong Date: Fri, 3 Jul 2026 11:16:46 +0100 Subject: [PATCH 13/13] fix: replace hardcoded colors with css variables in toast Signed-off-by: Cheryl Kong --- packages/serverless-workflow-diagram-editor/src/styles.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/serverless-workflow-diagram-editor/src/styles.css b/packages/serverless-workflow-diagram-editor/src/styles.css index 25bfaa05..d95ad817 100644 --- a/packages/serverless-workflow-diagram-editor/src/styles.css +++ b/packages/serverless-workflow-diagram-editor/src/styles.css @@ -62,6 +62,8 @@ --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; @@ -85,6 +87,8 @@ --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;