diff --git a/package.json b/package.json
index 7c6c1a33..461a6d9d 100644
--- a/package.json
+++ b/package.json
@@ -61,6 +61,7 @@
"react-aria-components": "1.16.0",
"react-resizable-panels": "2.1.7",
"react-stately": "3.37.0",
+ "react-toastify": "^11.1.0",
"react-virtualized": "9.22.6",
"react-pdf": "10.1.0"
},
diff --git a/src/components/toast/ToastContent.tsx b/src/components/toast/ToastContent.tsx
new file mode 100644
index 00000000..5e158357
--- /dev/null
+++ b/src/components/toast/ToastContent.tsx
@@ -0,0 +1,49 @@
+import { ReactNode } from "react";
+import clsx from "clsx";
+import { ToastAction, ToastType } from "./types";
+
+type ToastContentProps = {
+ message: ReactNode;
+ icon?: ReactNode;
+ type?: ToastType;
+ actions?: ToastAction[];
+ progress?: number;
+};
+
+export const ToastContent = ({
+ message,
+ icon,
+ actions,
+ progress,
+}: ToastContentProps) => {
+ const hasActions = actions && actions.length > 0;
+
+ return (
+
+ {icon &&
{icon}}
+
+ {message}
+ {progress !== undefined && (
+ {progress}%
+ )}
+
+ {hasActions && (
+
+ {actions.map((action) => (
+
+ ))}
+
+ )}
+
+ );
+};
diff --git a/src/components/toast/ToastExtendedContent.tsx b/src/components/toast/ToastExtendedContent.tsx
new file mode 100644
index 00000000..2d4f6653
--- /dev/null
+++ b/src/components/toast/ToastExtendedContent.tsx
@@ -0,0 +1,123 @@
+import { useState } from "react";
+import clsx from "clsx";
+import { FileIcon } from ":/components/preview/icons/FileIcon";
+import { CircleCheckFilled } from "../icon/icons/CircleCheckFilled";
+import { Loader } from "../icon/icons/Loader";
+import { Info } from "../icon/icons/Info";
+import { ChevronDown } from "../icon/icons/ChevronDown";
+import { XMark } from "../icon/icons/XMark";
+import { ToastExtendedItem, ToastExtendedOptions } from "./types";
+
+type ToastExtendedContentProps = Omit<
+ ToastExtendedOptions,
+ "autoClose" | "containerId"
+>;
+
+const ToastExtendedItemRow = ({ item }: { item: ToastExtendedItem }) => {
+ return (
+
+
+ {item.icon ?? (
+
+ )}
+
+
+ {item.title}
+ {item.size && (
+ {item.size}
+ )}
+
+
+ {item.status === "completed" ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
+
+export const ToastExtendedContent = ({
+ items,
+ summary,
+ progress,
+ onClose,
+ onInfoClick,
+}: ToastExtendedContentProps) => {
+ const [expanded, setExpanded] = useState(true);
+
+ return (
+
+
+
+
+ {items.map((item) => (
+
+ ))}
+
+
+
+
+
+ {summary}
+ {progress !== undefined && (
+
+ {progress}%
+
+ )}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/toast/ToastProvider.tsx b/src/components/toast/ToastProvider.tsx
new file mode 100644
index 00000000..7a2283b0
--- /dev/null
+++ b/src/components/toast/ToastProvider.tsx
@@ -0,0 +1,36 @@
+import { ToastContainer, Slide } from "react-toastify";
+
+export type ToastProviderProps = {
+ position?:
+ | "top-right"
+ | "top-center"
+ | "top-left"
+ | "bottom-right"
+ | "bottom-center"
+ | "bottom-left";
+ stacked?: boolean;
+ containerId?: string;
+};
+
+export const ToastProvider = ({
+ position = "bottom-left",
+ stacked = true,
+ containerId,
+}: ToastProviderProps) => {
+ return (
+
+ );
+};
diff --git a/src/components/toast/index.scss b/src/components/toast/index.scss
new file mode 100644
index 00000000..2fc5ee76
--- /dev/null
+++ b/src/components/toast/index.scss
@@ -0,0 +1,420 @@
+.c__toast-container {
+ --toastify-toast-width: 576px;
+ --toastify-container-width: 576px;
+
+ padding: 0;
+
+ &,
+ &[data-stacked="true"] {
+ width: 576px;
+ max-width: calc(100vw - 32px);
+ }
+
+ .c__toast {
+ display: flex;
+ align-items: center;
+ box-sizing: border-box;
+ min-height: 48px;
+ padding: var(--c--globals--spacings--xs, 8px);
+ border-radius: var(--c--globals--spacings--xs, 8px);
+ border: 1px solid var(--c--contextuals--border--semantic--neutral--tertiary);
+ background: var(--c--contextuals--background--surface--primary);
+ box-shadow: 0 6px 18px 0 rgba(0, 0, 0, 0.08);
+ margin-bottom: var(--c--globals--spacings--xs, 8px);
+ width: 100%;
+ max-width: 100%;
+ word-break: normal;
+
+ &.Toastify__toast--info {
+ border-color: var(--c--contextuals--border--semantic--info--tertiary);
+ background: var(--c--contextuals--background--semantic--info--tertiary);
+ }
+
+ &.Toastify__toast--success {
+ border-color: var(--c--contextuals--border--semantic--success--tertiary);
+ background: var(
+ --c--contextuals--background--semantic--success--tertiary
+ );
+ }
+
+ &.Toastify__toast--warning {
+ border-color: var(--c--contextuals--border--semantic--warning--tertiary);
+ background: var(
+ --c--contextuals--background--semantic--warning--tertiary
+ );
+ }
+
+ &.Toastify__toast--error {
+ border-color: var(--c--contextuals--border--semantic--error--tertiary);
+ background: var(--c--contextuals--background--semantic--error--tertiary);
+ }
+ }
+}
+
+.c__toast-content {
+ display: flex;
+ align-items: center;
+ padding: 0 var(--c--globals--spacings--3xs, 4px);
+ gap: 10px;
+ flex: 1;
+ min-width: 0;
+ min-height: 32px;
+ font-family: var(--c--globals--font--families--base);
+}
+
+.c__toast-content__icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ color: var(--c--contextuals--content--semantic--brand--primary);
+
+ .Toastify__toast--info & {
+ color: var(--c--contextuals--content--semantic--info--primary);
+ }
+
+ .Toastify__toast--success & {
+ color: var(--c--contextuals--content--semantic--success--primary);
+ }
+
+ .Toastify__toast--warning & {
+ color: var(--c--contextuals--content--semantic--warning--primary);
+ }
+
+ .Toastify__toast--error & {
+ color: var(--c--contextuals--content--semantic--error--primary);
+ }
+}
+
+.c__toast-content__message {
+ flex: 1 1 auto;
+ min-width: 0;
+ font-size: var(--c--globals--font--sizes--sm, 14px);
+ line-height: var(--c--globals--line-heights--sm, 18px);
+}
+
+.c__toast-content__text {
+ font-weight: 500;
+ color: var(--c--contextuals--content--semantic--brand--primary);
+
+ .Toastify__toast--info & {
+ color: var(--c--contextuals--content--semantic--info--primary);
+ }
+
+ .Toastify__toast--success & {
+ color: var(--c--contextuals--content--semantic--success--primary);
+ }
+
+ .Toastify__toast--warning & {
+ color: var(--c--contextuals--content--semantic--warning--primary);
+ }
+
+ .Toastify__toast--error & {
+ color: var(--c--contextuals--content--semantic--error--primary);
+ }
+}
+
+.c__toast-content__progress {
+ font-weight: 400;
+ margin-left: 10px;
+ white-space: nowrap;
+ color: var(--c--contextuals--content--semantic--brand--secondary);
+
+ .Toastify__toast--info & {
+ color: var(--c--contextuals--content--semantic--info--secondary);
+ }
+
+ .Toastify__toast--success & {
+ color: var(--c--contextuals--content--semantic--success--secondary);
+ }
+
+ .Toastify__toast--warning & {
+ color: var(--c--contextuals--content--semantic--warning--secondary);
+ }
+
+ .Toastify__toast--error & {
+ color: var(--c--contextuals--content--semantic--error--secondary);
+ }
+}
+
+.c__toast-content__actions {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ flex-shrink: 0;
+ margin-left: auto;
+}
+
+.c__toast-content__action {
+ cursor: pointer;
+ display: flex;
+ height: 32px;
+ min-width: 32px;
+ padding: 0 var(--c--globals--spacings--xs, 8px);
+ justify-content: center;
+ align-items: center;
+ border: 1px solid transparent;
+ background: transparent;
+ font-family: inherit;
+ font-size: var(--c--globals--font--sizes--base, 16px);
+ font-weight: 500;
+ line-height: var(--c--globals--line-heights--md, 22px);
+ color: var(--c--contextuals--content--semantic--brand--tertiary);
+ border-radius: var(--c--globals--spacings--3xs, 4px);
+
+ .Toastify__toast--info & {
+ color: var(--c--contextuals--content--semantic--info--tertiary);
+ }
+
+ .Toastify__toast--success & {
+ color: var(--c--contextuals--content--semantic--success--tertiary);
+ }
+
+ .Toastify__toast--warning & {
+ color: var(--c--contextuals--content--semantic--warning--tertiary);
+ }
+
+ .Toastify__toast--error & {
+ color: var(--c--contextuals--content--semantic--error--tertiary);
+ }
+
+ &:hover {
+ background: var(--c--contextuals--background--semantic--brand--tertiary);
+ }
+
+ .Toastify__toast--info &:hover {
+ background: var(--c--contextuals--background--semantic--info--secondary);
+ }
+
+ .Toastify__toast--success &:hover {
+ background: var(--c--contextuals--background--semantic--success--secondary);
+ }
+
+ .Toastify__toast--warning &:hover {
+ background: var(--c--contextuals--background--semantic--warning--secondary);
+ }
+
+ .Toastify__toast--error &:hover {
+ background: var(--c--contextuals--background--semantic--error--secondary);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--c--contextuals--border--semantic--brand--primary);
+ outline-offset: 2px;
+ }
+}
+
+.c__toast.c__toast--extended {
+ min-height: auto;
+ padding: var(--c--globals--spacings--xs, 8px);
+ border: 1px solid var(--c--contextuals--border--semantic--brand--tertiary);
+ background: var(--c--contextuals--background--semantic--brand--tertiary);
+ border-radius: var(--c--globals--spacings--xs, 8px);
+ box-shadow: 0 6px 18px 0 rgba(0, 0, 145, 0.05);
+ align-items: stretch;
+}
+
+.c__toast-extended {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ width: 100%;
+ font-family: var(--c--globals--font--families--base);
+ transition: gap 0.2s ease;
+
+ &--expanded {
+ gap: 10px;
+ }
+}
+
+.c__toast-extended__list-wrapper {
+ display: grid;
+ grid-template-rows: 0fr;
+ transition: grid-template-rows 0.2s ease;
+
+ &--expanded {
+ grid-template-rows: 1fr;
+ }
+}
+
+.c__toast-extended__list-inner {
+ min-height: 0;
+ overflow: hidden;
+}
+
+.c__toast-extended__list {
+ display: flex;
+ flex-direction: column;
+ margin: 0;
+ padding: 0 var(--c--globals--spacings--xs, 8px);
+ list-style: none;
+ background: var(--c--contextuals--background--surface--primary);
+ border-radius: var(--c--globals--spacings--3xs, 4px);
+ overflow: hidden;
+}
+
+.c__toast-extended__item {
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: var(--c--globals--spacings--xs, 8px);
+ height: 40px;
+ padding: var(--c--globals--spacings--3xs, 4px)
+ var(--c--globals--spacings--xxs, 6px);
+ min-width: 0;
+
+ &:not(:last-child)::after {
+ content: "";
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: var(--c--contextuals--border--surface--primary);
+ }
+}
+
+.c__toast-extended__item-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ width: 24px;
+ height: 32px;
+}
+
+.c__toast-extended__item-content {
+ display: flex;
+ align-items: center;
+ gap: var(--c--globals--spacings--xs, 8px);
+ flex: 1 1 0;
+ min-width: 0;
+ font-size: var(--c--globals--font--sizes--sm, 14px);
+ line-height: var(--c--globals--line-heights--sm, 18px);
+}
+
+.c__toast-extended__item-title {
+ font-weight: 500;
+ color: var(--c--contextuals--content--semantic--brand--primary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.c__toast-extended__item-size {
+ font-weight: 400;
+ color: var(--c--contextuals--content--semantic--brand--secondary);
+ white-space: nowrap;
+ flex-shrink: 0;
+}
+
+.c__toast-extended__item-status {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ margin-left: auto;
+ color: var(--c--contextuals--content--semantic--brand--tertiary);
+}
+
+.c__toast-extended__item-loader {
+ animation: c__toast-extended-spin 1s linear infinite;
+}
+
+@keyframes c__toast-extended-spin {
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+.c__toast-extended__footer {
+ display: flex;
+ align-items: center;
+ height: 32px;
+ min-height: 32px;
+ padding: 0;
+}
+
+.c__toast-extended__summary {
+ display: flex;
+ align-items: center;
+ gap: var(--c--globals--spacings--3xs, 4px);
+ flex: 0 1 auto;
+ min-width: 0;
+ padding-left: var(--c--globals--spacings--3xs, 4px);
+ font-size: var(--c--globals--font--sizes--sm, 14px);
+ line-height: var(--c--globals--line-heights--sm, 18px);
+}
+
+.c__toast-extended__summary-text {
+ font-weight: 500;
+ color: var(--c--contextuals--content--semantic--brand--primary);
+}
+
+.c__toast-extended__summary-progress {
+ font-weight: 400;
+ color: var(--c--contextuals--content--semantic--brand--tertiary);
+}
+
+.c__toast-extended__summary-info {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ color: var(--c--contextuals--content--semantic--brand--tertiary);
+ .c__toast-extended__footer
+ &:focus-visible {
+ outline: 2px solid var(--c--contextuals--border--semantic--brand--primary);
+ outline-offset: 2px;
+ border-radius: var(--c--globals--spacings--3xs, 4px);
+ }
+}
+
+.c__toast-extended__footer-actions {
+ display: flex;
+ align-items: center;
+ gap: var(--c--globals--spacings--3xs, 4px);
+ flex-shrink: 0;
+ margin-left: auto;
+}
+
+.c__toast-extended__footer-action {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 32px;
+ height: 32px;
+ padding: 0 var(--c--globals--spacings--3xs, 4px);
+ border: 1px solid transparent;
+ border-radius: var(--c--globals--spacings--3xs, 4px);
+ background: transparent;
+ cursor: pointer;
+ color: var(--c--contextuals--content--semantic--brand--tertiary);
+
+ &:hover {
+ background: var(--c--contextuals--background--semantic--brand--tertiary);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--c--contextuals--border--semantic--brand--primary);
+ outline-offset: 2px;
+ }
+
+ &--toggle {
+ transition: transform 0.2s ease;
+
+ &[aria-expanded="false"] {
+ transform: rotate(180deg);
+ }
+ }
+
+ &--toggle svg,
+ &--close svg {
+ width: 24px;
+ height: 24px;
+ flex-shrink: 0;
+ }
+}
diff --git a/src/components/toast/index.ts b/src/components/toast/index.ts
new file mode 100644
index 00000000..ff1ef565
--- /dev/null
+++ b/src/components/toast/index.ts
@@ -0,0 +1,11 @@
+export { toast } from "./toast";
+export { ToastProvider } from "./ToastProvider";
+export type { ToastProviderProps } from "./ToastProvider";
+export type {
+ ToastOptions,
+ ToastAction,
+ ToastType,
+ ToastExtendedOptions,
+ ToastExtendedItem,
+ ToastExtendedItemStatus,
+} from "./types";
diff --git a/src/components/toast/toast.stories.tsx b/src/components/toast/toast.stories.tsx
new file mode 100644
index 00000000..54a6efd6
--- /dev/null
+++ b/src/components/toast/toast.stories.tsx
@@ -0,0 +1,387 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { Title, Description, Controls, Stories } from "@storybook/blocks";
+import { ToastProvider } from "./ToastProvider";
+import { toast } from "./toast";
+import { ArrowRight } from "../icon/icons/ArrowRight";
+
+const meta: Meta = {
+ title: "Components/Toast",
+ component: ToastProvider,
+ tags: ["autodocs"],
+ parameters: {
+ docs: {
+ page: () => (
+ <>
+
+
+
+
+ >
+ ),
+ description: {
+ component: `The Toast component displays brief notifications to the user. It wraps \`react-toastify\` with the design system's tokens and typography.
+
+## Installation
+
+Add the \`ToastProvider\` at the root of your app:
+
+\`\`\`tsx
+import { ToastProvider } from "@gouvfr-lasuite/ui-kit";
+
+function App() {
+ return (
+ <>
+
+
+ >
+ );
+}
+\`\`\`
+
+## Basic usage
+
+\`\`\`tsx
+import { toast } from "@gouvfr-lasuite/ui-kit";
+
+// Simple toast
+toast.info("Document moved successfully.");
+
+// Toast with icon, progress, and actions
+toast.info("Document 1 moved to Document 2", {
+ icon: ,
+ progress: 15,
+ actions: [
+ { label: "See", onClick: () => navigate("/document/2") },
+ { label: "Cancel", onClick: () => undoMove() },
+ ],
+});
+\`\`\`
+
+## API
+
+| Method | Description |
+|--------|-------------|
+| \`toast(message, options?)\` | Default toast |
+| \`toast.info(message, options?)\` | Info variant (blue) |
+| \`toast.success(message, options?)\` | Success variant (green) |
+| \`toast.warning(message, options?)\` | Warning variant (orange) |
+| \`toast.error(message, options?)\` | Error variant (red) |
+| \`toast.dismiss(toastId?)\` | Dismiss a toast (or all) |
+| \`toast.update(toastId, options)\` | Update an existing toast |
+| \`toast.extended(options)\` | Extended toast with file list and summary |
+
+## ToastOptions
+
+| Prop | Type | Description |
+|------|------|-------------|
+| \`icon\` | \`ReactNode\` | Icon displayed on the left |
+| \`type\` | \`"info" \\| "success" \\| "warning" \\| "error"\` | Toast variant |
+| \`actions\` | \`ToastAction[]\` | Action buttons (See, Cancel, etc.) |
+| \`progress\` | \`number\` | Progress percentage shown next to message |
+| \`autoClose\` | \`number \\| false\` | Auto-dismiss delay in ms (default: 5000) |
+
+## ToastAction
+
+| Prop | Type | Description |
+|------|------|-------------|
+| \`label\` | \`string\` | Button text |
+| \`onClick\` | \`() => void\` | Callback on click |
+
+## Dismissing from an action
+
+\`\`\`tsx
+const id = toast.info("File moved", {
+ actions: [
+ { label: "Cancel", onClick: () => {
+ undoMove();
+ toast.dismiss(id);
+ }},
+ ],
+});
+\`\`\`
+
+## Extended toast (file transfers)
+
+Use \`toast.extended()\` to display a list of files with transfer status. The toast is **expanded by default**; the user can collapse/expand the list via the chevron (handled internally). Wire \`onClose\` to dismiss the toast.
+
+\`\`\`tsx
+import { toast } from "@gouvfr-lasuite/ui-kit";
+
+const id = toast.extended({
+ summary: "3 documents in transfer",
+ progress: 15,
+ items: [
+ {
+ id: "1",
+ title: "Presentation on Monet",
+ size: "12 GB",
+ mimetype: "application/vnd.ms-powerpoint",
+ status: "completed",
+ },
+ {
+ id: "2",
+ title: "Flower wallpaper – Uploading",
+ mimetype: "image/jpeg",
+ status: "loading",
+ },
+ {
+ id: "3",
+ title: "Seminar Logistics",
+ size: "2 MB",
+ mimetype: "application/pdf",
+ status: "completed",
+ },
+ ],
+ onClose: () => toast.dismiss(id),
+ onInfoClick: () => showTransferDetails(),
+ autoClose: false,
+});
+\`\`\`
+
+To update progress as uploads complete, use \`toast.update()\` with a new \`ToastExtendedContent\` render (or re-call \`toast.extended\` after dismissing the previous one).
+
+## ToastExtendedOptions
+
+| Prop | Type | Required | Description |
+|------|------|----------|-------------|
+| \`items\` | \`ToastExtendedItem[]\` | Yes | List of files to display |
+| \`summary\` | \`string\` | No | Footer text (e.g. "3 documents in transfer") |
+| \`progress\` | \`number\` | No | Global progress % shown next to summary |
+| \`onClose\` | \`() => void\` | No | Called when user clicks the X button |
+| \`onInfoClick\` | \`() => void\` | No | Called when user clicks the info icon |
+| \`autoClose\` | \`number \\| false\` | No | Auto-dismiss delay in ms (default: \`false\`) |
+| \`containerId\` | \`string\` | No | Target a specific ToastContainer (e.g. Storybook) |
+
+## ToastExtendedItem
+
+| Prop | Type | Required | Description |
+|------|------|----------|-------------|
+| \`id\` | \`string\` | No | Unique key for the row |
+| \`title\` | \`string\` | Yes | File name or label |
+| \`size\` | \`string\` | No | File size (e.g. "12 GB", "2 MB") |
+| \`mimetype\` | \`string\` | No | MIME type for automatic file icon |
+| \`status\` | \`"completed" \\| "loading"\` | Yes | Shows checkmark or spinner |
+| \`icon\` | \`ReactNode\` | No | Custom icon instead of mime-based FileIcon |
+`,
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ name: "Info with actions",
+ render: () => {
+ const containerId = "story-info-actions";
+ return (
+ <>
+
+
+ >
+ );
+ },
+};
+
+export const WithProgress: Story = {
+ name: "With progress",
+ render: () => {
+ const containerId = "story-progress";
+ return (
+ <>
+
+
+ >
+ );
+ },
+};
+
+export const Success: Story = {
+ render: () => {
+ const containerId = "story-success";
+ return (
+ <>
+
+
+ >
+ );
+ },
+};
+
+export const Warning: Story = {
+ render: () => {
+ const containerId = "story-warning";
+ return (
+ <>
+
+
+ >
+ );
+ },
+};
+
+export const Error: Story = {
+ render: () => {
+ const containerId = "story-error";
+ return (
+ <>
+
+
+ >
+ );
+ },
+};
+
+export const Stacked: Story = {
+ name: "Multiple stacked",
+ render: () => {
+ const containerId = "story-stacked";
+ let count = 0;
+ return (
+ <>
+
+
+ >
+ );
+ },
+};
+
+export const Extended: Story = {
+ name: "Extended",
+ render: () => {
+ const containerId = "story-extended";
+ return (
+ <>
+
+
+ >
+ );
+ },
+};
diff --git a/src/components/toast/toast.tsx b/src/components/toast/toast.tsx
new file mode 100644
index 00000000..c831bcfd
--- /dev/null
+++ b/src/components/toast/toast.tsx
@@ -0,0 +1,54 @@
+import { ReactNode } from "react";
+import { toast as reactToastify, Id } from "react-toastify";
+import { ToastContent } from "./ToastContent";
+import { ToastExtendedContent } from "./ToastExtendedContent";
+import { ToastExtendedOptions, ToastOptions } from "./types";
+
+export const toast = (message: ReactNode, options?: ToastOptions): Id => {
+ return reactToastify(
+ ,
+ {
+ type: options?.type ?? "default",
+ autoClose: options?.autoClose ?? 5000,
+ icon: false,
+ containerId: options?.containerId,
+ },
+ );
+};
+
+toast.info = (message: ReactNode, options?: Omit): Id =>
+ toast(message, { ...options, type: "info" });
+
+toast.success = (
+ message: ReactNode,
+ options?: Omit,
+): Id => toast(message, { ...options, type: "success" });
+
+toast.warning = (
+ message: ReactNode,
+ options?: Omit,
+): Id => toast(message, { ...options, type: "warning" });
+
+toast.error = (
+ message: ReactNode,
+ options?: Omit,
+): Id => toast(message, { ...options, type: "error" });
+
+toast.extended = (options: ToastExtendedOptions): Id => {
+ return reactToastify(, {
+ type: "default",
+ autoClose: options.autoClose ?? false,
+ icon: false,
+ containerId: options.containerId,
+ className: "c__toast c__toast--extended",
+ });
+};
+
+toast.update = reactToastify.update;
+toast.dismiss = reactToastify.dismiss;
diff --git a/src/components/toast/types.ts b/src/components/toast/types.ts
new file mode 100644
index 00000000..8c03568c
--- /dev/null
+++ b/src/components/toast/types.ts
@@ -0,0 +1,38 @@
+import { ReactNode } from "react";
+
+export type ToastType = "info" | "success" | "warning" | "error";
+
+export type ToastAction = {
+ label: string;
+ onClick: () => void;
+};
+
+export type ToastOptions = {
+ icon?: ReactNode;
+ type?: ToastType;
+ actions?: ToastAction[];
+ progress?: number;
+ autoClose?: number | false;
+ containerId?: string;
+};
+
+export type ToastExtendedItemStatus = "completed" | "loading";
+
+export type ToastExtendedItem = {
+ id?: string;
+ title: string;
+ size?: string;
+ mimetype?: string;
+ status: ToastExtendedItemStatus;
+ icon?: ReactNode;
+};
+
+export type ToastExtendedOptions = {
+ items: ToastExtendedItem[];
+ summary?: string;
+ progress?: number;
+ onClose?: () => void;
+ onInfoClick?: () => void;
+ autoClose?: number | false;
+ containerId?: string;
+};
diff --git a/src/index.ts b/src/index.ts
index 36ca7f0c..2e7be177 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -23,6 +23,7 @@ export * from "./components/tabs";
export * from "./components/tree-view";
export * from "./components/form/label/label";
export * from "./components/badge";
+export * from "./components/toast";
export * from "./components/icon";
export * from "./components/help-menu";
export * from "./components/feedback-form";
diff --git a/src/library.scss b/src/library.scss
index 2f11ab7c..029652aa 100644
--- a/src/library.scss
+++ b/src/library.scss
@@ -38,6 +38,7 @@
@use "./components/share/modal/share-modal";
@use "./components/modal";
@use "./components/badge";
+@use "./components/toast";
@use "./components/icon";
@use "./components/help-menu";
@use "./components/feedback-form";
diff --git a/yarn.lock b/yarn.lock
index d6c64330..36932b33 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5177,7 +5177,7 @@ cliui@^7.0.2:
strip-ansi "^6.0.0"
wrap-ansi "^7.0.0"
-clsx@2.1.1, clsx@^2.0.0:
+clsx@2.1.1, clsx@^2.0.0, clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
@@ -8034,6 +8034,13 @@ react-style-singleton@^2.2.2, react-style-singleton@^2.2.3:
get-nonce "^1.0.0"
tslib "^2.0.0"
+react-toastify@^11.1.0:
+ version "11.1.0"
+ resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-11.1.0.tgz#3d3c73a44d5cac868ee9a52e0f90dd706532c4a4"
+ integrity sha512-e9h23x3phN0wbFeB6yovmWp7lobzV4CaCH0LO8nVP6H7Y+3GbcLpIzMm9dJhcp1RXbpyfvjgpfXqO80QAmn7sg==
+ dependencies:
+ clsx "^2.1.1"
+
react-virtualized@9.22.6:
version "9.22.6"
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.6.tgz#3ae2aa69eca61cf3af332e2f9d6b4aa5638786d5"