This commit is contained in:
DCCONSTRUCTIONS
2026-04-18 18:39:25 +03:00
commit 3ba092b60c
4944 changed files with 497564 additions and 0 deletions
@@ -0,0 +1,7 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
export * from "./toast";
@@ -0,0 +1,709 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Toast, ToastStatic, setToast, updateToast, setPromiseToast, TOAST_TYPE } from "./toast";
const meta = {
title: "Components/Toast",
component: Toast,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
args: {
theme: "light",
},
} satisfies Meta<typeof Toast>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Provider: Story = {
render() {
return (
<div>
<Toast theme="light" />
<div className="space-y-2">
<p className="text-gray-600 text-13">
Toast provider is required to display toasts. It should be added to your app root.
</p>
<code className="bg-gray-100 block rounded-sm p-2 text-11">{`<Toast theme="light" />`}</code>
</div>
</div>
);
},
};
export const Success: Story = {
render() {
return (
<>
<Toast theme="light" />
<button
onClick={() =>
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Your changes have been saved successfully.",
})
}
className="rounded-sm bg-success-primary px-4 py-2 text-13 text-on-color hover:bg-success-primary/90"
>
Show Success Toast
</button>
</>
);
},
};
export const Error: Story = {
render() {
return (
<>
<Toast theme="light" />
<button
onClick={() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error",
message: "Something went wrong. Please try again.",
})
}
className="rounded-sm bg-danger-primary px-4 py-2 text-13 text-on-color hover:bg-danger-primary/90"
>
Show Error Toast
</button>
</>
);
},
};
export const Warning: Story = {
render() {
return (
<>
<Toast theme="light" />
<button
onClick={() =>
setToast({
type: TOAST_TYPE.WARNING,
title: "Warning",
message: "This action cannot be undone.",
})
}
className="rounded-sm bg-warning-primary px-4 py-2 text-13 text-on-color hover:bg-warning-primary/90"
>
Show Warning Toast
</button>
</>
);
},
};
export const Info: Story = {
render() {
return (
<>
<Toast theme="light" />
<button
onClick={() =>
setToast({
type: TOAST_TYPE.INFO,
title: "Information",
message: "Here's some helpful information for you.",
})
}
className="rounded-sm bg-accent-primary px-4 py-2 text-13 text-on-color hover:bg-accent-primary/90"
>
Show Info Toast
</button>
</>
);
},
};
export const Loading: Story = {
render() {
return (
<>
<Toast theme="light" />
<button
onClick={() =>
setToast({
type: TOAST_TYPE.LOADING,
title: "Loading...",
})
}
className="rounded-sm border border-subtle bg-layer-2 px-4 py-2 text-13 text-primary hover:bg-layer-1"
>
Show Loading Toast
</button>
</>
);
},
};
export const WithActionItems: Story = {
render() {
return (
<>
<Toast theme="light" />
<button
onClick={() =>
setToast({
type: TOAST_TYPE.SUCCESS,
title: "File uploaded",
message: "Your file has been uploaded successfully.",
actionItems: (
<>
<button className="text-13 font-medium text-primary hover:text-secondary">Button</button>
<button className="text-13 font-medium text-primary hover:text-secondary">Button</button>
</>
),
})
}
className="rounded-sm bg-success-primary px-4 py-2 text-13 text-on-color hover:bg-success-primary/90"
>
Show Toast with Action
</button>
</>
);
},
};
export const UpdateToast: Story = {
render() {
const handleUpdate = () => {
const id = setToast({
type: TOAST_TYPE.LOADING,
title: "Processing...",
});
setTimeout(() => {
updateToast(id, {
type: TOAST_TYPE.SUCCESS,
title: "Complete!",
message: "The operation has finished successfully.",
});
}, 2000);
};
return (
<>
<Toast theme="light" />
<button
onClick={handleUpdate}
className="rounded-sm bg-accent-primary px-4 py-2 text-13 text-on-color hover:bg-accent-primary/90"
>
Update Toast After 2s
</button>
</>
);
},
};
export const PromiseToast: Story = {
render() {
const handlePromise = () => {
const promise = new Promise<{ name?: string; error?: string }>((resolve, reject) => {
setTimeout(() => {
Math.random() > 0.5 ? resolve({ name: "Success data" }) : reject({ error: "Failed" });
}, 2000);
});
setPromiseToast(promise, {
loading: "Processing request...",
success: {
title: "Request completed!",
message: (data) => `Successfully processed: ${data.name}`,
},
error: {
title: "Request failed",
message: (error) => `Error: ${error.error}`,
},
});
};
return (
<>
<Toast theme="light" />
<button
onClick={handlePromise}
className="rounded-sm bg-accent-primary px-4 py-2 text-13 text-on-color hover:bg-accent-primary/90"
>
Show Promise Toast
</button>
</>
);
},
};
export const AllTypes: Story = {
render() {
return (
<>
<Toast theme="light" />
<div className="flex flex-wrap gap-2">
<button
onClick={() =>
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success",
message: "Operation successful",
})
}
className="rounded-sm bg-success-primary px-3 py-2 text-13 text-on-color hover:bg-success-primary/90"
>
Success
</button>
<button
onClick={() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error",
message: "Operation failed",
})
}
className="rounded-sm bg-danger-primary px-3 py-2 text-13 text-on-color hover:bg-danger-primary/90"
>
Error
</button>
<button
onClick={() =>
setToast({
type: TOAST_TYPE.WARNING,
title: "Warning",
message: "Please be careful",
})
}
className="rounded-sm bg-warning-primary px-3 py-2 text-13 text-on-color hover:bg-warning-primary/90"
>
Warning
</button>
<button
onClick={() =>
setToast({
type: TOAST_TYPE.INFO,
title: "Info",
message: "Here's some info",
})
}
className="rounded-sm bg-accent-primary px-3 py-2 text-13 text-on-color hover:bg-accent-primary/90"
>
Info
</button>
<button
onClick={() =>
setToast({
type: TOAST_TYPE.LOADING,
title: "Loading",
})
}
className="rounded-sm border border-subtle bg-layer-2 px-3 py-2 text-13 text-primary hover:bg-layer-1"
>
Loading
</button>
</div>
</>
);
},
};
export const MultipleToasts: Story = {
render() {
return (
<>
<Toast theme="light" />
<button
onClick={() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "First toast",
message: "This is the first toast",
});
setTimeout(() => {
setToast({
type: TOAST_TYPE.INFO,
title: "Second toast",
message: "This is the second toast",
});
}, 500);
setTimeout(() => {
setToast({
type: TOAST_TYPE.WARNING,
title: "Third toast",
message: "This is the third toast",
});
}, 1000);
}}
className="rounded-sm bg-accent-primary px-4 py-2 text-13 text-on-color hover:bg-accent-primary/90"
>
Show Multiple Toasts
</button>
</>
);
},
};
export const TitleOnly: Story = {
render() {
return (
<>
<Toast theme="light" />
<button
onClick={() =>
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Saved!",
})
}
className="rounded-sm bg-success-primary px-4 py-2 text-13 text-on-color hover:bg-success-primary/90"
>
Show Title Only
</button>
</>
);
},
};
export const LongMessage: Story = {
render() {
return (
<>
<Toast theme="light" />
<button
onClick={() =>
setToast({
type: TOAST_TYPE.INFO,
title: "Important Information",
message:
"This is a longer message that provides more detailed information about what happened and what the user should do next.",
})
}
className="rounded-sm bg-accent-primary px-4 py-2 text-13 text-on-color hover:bg-accent-primary/90"
>
Show Long Message
</button>
</>
);
},
};
// ========== Static Variants ==========
export const StaticVariants: Story = {
render() {
return (
<div className="grid grid-cols-1 gap-4">
<div>
<p className="mb-2 text-13 font-medium text-secondary">All toast variants (static):</p>
</div>
<div className="flex flex-col gap-3">
<ToastStatic type={TOAST_TYPE.SUCCESS} title="Success" message="Your changes have been saved successfully." />
<ToastStatic type={TOAST_TYPE.ERROR} title="Error" message="Something went wrong. Please try again." />
<ToastStatic type={TOAST_TYPE.WARNING} title="Warning" message="This action cannot be undone." />
<ToastStatic type={TOAST_TYPE.INFO} title="Information" message="Here's some helpful information for you." />
<ToastStatic type={TOAST_TYPE.LOADING} title="Loading..." />
</div>
</div>
);
},
};
export const StaticSuccess: Story = {
render() {
return (
<ToastStatic
type={TOAST_TYPE.SUCCESS}
title="Changes saved"
message="Your changes have been saved successfully."
/>
);
},
};
export const StaticError: Story = {
render() {
return <ToastStatic type={TOAST_TYPE.ERROR} title="Upload failed" message="Failed to upload file. Try again." />;
},
};
export const StaticWarning: Story = {
render() {
return (
<ToastStatic
type={TOAST_TYPE.WARNING}
title="Unsaved changes"
message="You have unsaved changes. Save before leaving."
/>
);
},
};
export const StaticInfo: Story = {
render() {
return (
<ToastStatic
type={TOAST_TYPE.INFO}
title="New feature available"
message="Check out our latest feature in settings."
/>
);
},
};
export const StaticLoading: Story = {
render() {
return <ToastStatic type={TOAST_TYPE.LOADING} title="Processing your request..." />;
},
};
export const StaticWithActions: Story = {
render() {
return (
<ToastStatic
type={TOAST_TYPE.SUCCESS}
title="File uploaded"
message="Your file has been uploaded successfully."
actionItems={
<>
<button className="text-13 font-medium text-primary hover:text-secondary">Button</button>
<button className="text-13 font-medium text-primary hover:text-secondary">Button</button>
</>
}
/>
);
},
};
export const StaticDarkMode: Story = {
render() {
return (
<div className="flex flex-col gap-3 rounded-lg bg-canvas p-6" data-theme="dark">
<p className="mb-2 text-13 font-medium text-secondary">Toast variants in dark mode:</p>
<ToastStatic
type={TOAST_TYPE.SUCCESS}
title="Success"
message="Operation completed successfully."
theme="dark"
/>
<ToastStatic type={TOAST_TYPE.ERROR} title="Error" message="An error occurred." theme="dark" />
<ToastStatic type={TOAST_TYPE.WARNING} title="Warning" message="Please proceed with caution." theme="dark" />
<ToastStatic type={TOAST_TYPE.INFO} title="Information" message="Here's some useful info." theme="dark" />
<ToastStatic type={TOAST_TYPE.LOADING} title="Loading..." theme="dark" />
</div>
);
},
};
// ========== Design Tokens Documentation ==========
export const DesignTokens: Story = {
render() {
return (
<div className="space-y-6">
<div>
<h2 className="mb-2 text-16 font-semibold text-primary">Toast Design Tokens</h2>
<p className="text-13 text-secondary">
The toast component uses semantic design tokens from the Plane design system.
</p>
</div>
<div>
<h3 className="mb-3 text-14 font-semibold text-primary">Token Mapping by Variant</h3>
<div className="overflow-x-auto">
<table className="w-full rounded-lg border border-subtle text-11">
<thead>
<tr className="bg-layer-1">
<th className="border-b border-subtle px-3 py-2 text-left font-semibold text-primary">Variant</th>
<th className="border-b border-subtle px-3 py-2 text-left font-semibold text-primary">Title Text</th>
<th className="border-b border-subtle px-3 py-2 text-left font-semibold text-primary">Icon BG</th>
<th className="border-b border-subtle px-3 py-2 text-left font-semibold text-primary">Toast BG</th>
<th className="border-b border-subtle px-3 py-2 text-left font-semibold text-primary">Border</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-subtle">
<td className="px-3 py-2 font-medium text-primary">Success</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-primary</code>
</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-success-primary</code>
</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-surface-1</code>
</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">border-subtle</code>
</td>
</tr>
<tr className="border-b border-subtle">
<td className="px-3 py-2 font-medium text-primary">Error</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-primary</code>
</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-danger-primary</code>
</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-surface-1</code>
</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">border-subtle</code>
</td>
</tr>
<tr className="border-b border-subtle">
<td className="px-3 py-2 font-medium text-primary">Warning</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-primary</code>
</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-warning-primary</code>
</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-surface-1</code>
</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">border-subtle</code>
</td>
</tr>
<tr className="border-b border-subtle">
<td className="px-3 py-2 font-medium text-primary">Info</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-primary</code>
</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-accent-primary</code>
</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-surface-1</code>
</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">border-subtle</code>
</td>
</tr>
<tr>
<td className="px-3 py-2 font-medium text-primary">Loading</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-primary</code>
</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-layer-2</code>
</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-surface-1</code>
</td>
<td className="px-3 py-2">
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">border-subtle</code>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<h3 className="mb-3 text-14 font-semibold text-primary">Typography</h3>
<ul className="space-y-2 text-12 text-secondary">
<li>
<span className="font-medium text-primary">Title:</span>{" "}
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-14 font-semibold</code>
</li>
<li>
<span className="font-medium text-primary">Message:</span>{" "}
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-13</code>
</li>
<li>
<span className="font-medium text-primary">Message Color:</span>{" "}
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-secondary</code>
</li>
<li>
<span className="font-medium text-primary">Action Button:</span>{" "}
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-13 font-medium</code>
</li>
</ul>
</div>
<div>
<h3 className="mb-3 text-14 font-semibold text-primary">Dimensions & Styling</h3>
<ul className="space-y-2 text-12 text-secondary">
<li>
<span className="font-medium text-primary">Width:</span> 350px
</li>
<li>
<span className="font-medium text-primary">Padding:</span> 16px (p-4)
</li>
<li>
<span className="font-medium text-primary">Border Radius:</span> 8px (rounded-lg)
</li>
<li>
<span className="font-medium text-primary">Shadow:</span>{" "}
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">shadow-raised-200</code>
</li>
<li>
<span className="font-medium text-primary">Border Width:</span> 1px
</li>
</ul>
</div>
</div>
<div>
<h3 className="mb-3 text-14 font-semibold text-primary">Icon Specifications</h3>
<ul className="space-y-2 text-12 text-secondary">
<li>
<span className="font-medium text-primary">Icon Size:</span> 20x20px
</li>
<li>
<span className="font-medium text-primary">Icon Stroke Width:</span> 2px
</li>
<li>
<span className="font-medium text-primary">Icon Container:</span> 40x40px circular (w-10 h-10
rounded-full)
</li>
<li>
<span className="font-medium text-primary">Icon Color:</span>{" "}
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-on-color</code>
</li>
<li>
<span className="font-medium text-primary">Icon Background:</span>{" "}
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">
bg-{"{"}variant{"}"}-primary
</code>
</li>
<li>
<span className="font-medium text-primary">Close Icon Size:</span> 16x16px
</li>
<li>
<span className="font-medium text-primary">Close Icon Color:</span>{" "}
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">
text-icon-secondary hover:text-icon-tertiary
</code>
</li>
</ul>
</div>
<div className="rounded-lg bg-layer-1 p-4">
<h3 className="mb-2 text-14 font-semibold text-primary">Visual Examples</h3>
<p className="mb-4 text-12 text-secondary">See how the tokens are applied across all toast variants:</p>
<div className="flex flex-col gap-3">
<ToastStatic
type={TOAST_TYPE.SUCCESS}
title="Success"
message="Your changes have been saved successfully."
/>
<ToastStatic type={TOAST_TYPE.ERROR} title="Error" message="Something went wrong. Please try again." />
<ToastStatic type={TOAST_TYPE.WARNING} title="Warning" message="This action cannot be undone." />
<ToastStatic type={TOAST_TYPE.INFO} title="Information" message="Here's some helpful information." />
<ToastStatic type={TOAST_TYPE.LOADING} title="Loading..." />
</div>
</div>
</div>
);
},
};
@@ -0,0 +1,324 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import * as React from "react";
import { Toast as BaseToast } from "@base-ui-components/react/toast";
import { AlertTriangle, CheckIcon, InfoIcon, XIcon } from "lucide-react";
import { CloseIcon } from "../icons/actions/close-icon";
// spinner
import { CircularBarSpinner } from "../spinners/circular-bar-spinner";
import { cn } from "../utils/classname";
export enum TOAST_TYPE {
SUCCESS = "success",
ERROR = "error",
INFO = "info",
WARNING = "warning",
LOADING = "loading",
LOADING_TOAST = "loading-toast",
}
type SetToastProps =
| {
type: TOAST_TYPE.LOADING;
title?: string;
}
| {
id?: string | number;
type: Exclude<TOAST_TYPE, TOAST_TYPE.LOADING>;
title: string;
message?: string;
actionItems?: React.ReactNode;
};
type PromiseToastCallback<ToastData> = (data: ToastData) => string;
type ActionItemsPromiseToastCallback<ToastData> = (data: ToastData) => React.ReactNode;
type PromiseToastData<ToastData> = {
title: string;
message?: PromiseToastCallback<ToastData>;
actionItems?: ActionItemsPromiseToastCallback<ToastData>;
};
type PromiseToastOptions<ToastData> = {
loading?: string;
success: PromiseToastData<ToastData>;
error: PromiseToastData<ToastData>;
};
export type ToastProps = {
theme: "light" | "dark" | "system";
};
const toastManager = BaseToast.createToastManager();
export function Toast(props: ToastProps) {
return (
<BaseToast.Provider toastManager={toastManager}>
<BaseToast.Portal>
<BaseToast.Viewport data-theme={props.theme}>
<ToastList />
</BaseToast.Viewport>
</BaseToast.Portal>
</BaseToast.Provider>
);
}
const TOAST_DATA = {
[TOAST_TYPE.SUCCESS]: {
icon: <CheckIcon width={12} height={12} className="text-on-color" />,
iconBgClassName: "bg-success-primary",
backgroundColorClassName: "!bg-surface-1",
borderColorClassName: "border-subtle",
},
[TOAST_TYPE.ERROR]: {
icon: <XIcon width={12} height={12} className="text-on-color" />,
iconBgClassName: "bg-danger-primary",
backgroundColorClassName: "bg-surface-1",
borderColorClassName: "border-subtle",
},
[TOAST_TYPE.WARNING]: {
icon: <AlertTriangle width={12} height={12} className="text-on-color" />,
iconBgClassName: "bg-warning-primary",
backgroundColorClassName: "bg-surface-1",
borderColorClassName: "border-subtle",
},
[TOAST_TYPE.INFO]: {
icon: <InfoIcon width={12} height={12} className="text-on-color" />,
iconBgClassName: "bg-accent-primary",
backgroundColorClassName: "bg-surface-1",
borderColorClassName: "border-subtle",
},
[TOAST_TYPE.LOADING]: {
icon: <CircularBarSpinner className="text-on-color" />,
iconBgClassName: "bg-layer-2",
backgroundColorClassName: "bg-surface-1",
borderColorClassName: "border-subtle",
},
[TOAST_TYPE.LOADING_TOAST]: {
icon: <CircularBarSpinner className="text-on-color" />,
iconBgClassName: "bg-layer-2",
backgroundColorClassName: "bg-surface-1",
borderColorClassName: "border-subtle",
},
};
function ToastList() {
const { toasts } = BaseToast.useToastManager();
return toasts.map((toast) => <ToastRender key={toast.id} id={toast.id} toast={toast} />);
}
function ToastRender({ id, toast }: { id: React.Key; toast: BaseToast.Root.ToastObject }) {
const toastData = toast.data as SetToastProps;
const type = toastData.type as TOAST_TYPE;
const data = TOAST_DATA[type];
return (
<BaseToast.Root
toast={toast}
key={id}
className={cn(
// Base layout and positioning
"group flex w-[350px] items-center rounded-lg border shadow-raised-200",
"absolute right-3 bottom-3 z-[calc(1000-var(--toast-index))]",
"ease-&lsqb;cubic-bezier(0.22,1,0.36,1)&rsqb; transition-[opacity,transform] duration-500 select-none",
// Default transform with stacking and scaling
"[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+calc(min(var(--toast-index),10)*-10px)))_scale(calc(max(0,1-(var(--toast-index)*0.1))))]",
// Pseudo-element for gap spacing
"after:absolute after:bottom-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-['']",
// State-based opacity
"data-[ending-style]:opacity-0 data-[limited]:opacity-0",
// Starting animation
"data-[starting-style]:[transform:translateY(150%)]",
// Expanded state transform
"data-[expanded]:[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y)))]",
// Swipe direction endings - consolidated
"data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]",
"data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]",
"data-[ending-style]:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]",
"data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]",
// Default ending transform for non-limited toasts
"data-[ending-style]:[&:not([data-limited])]:[transform:translateY(150%)]",
data.backgroundColorClassName,
data.borderColorClassName
)}
style={{
["--gap" as string]: "1rem",
["--offset-y" as string]:
"calc(var(--toast-offset-y) * -1 + (var(--toast-index) * var(--gap) * -1) + var(--toast-swipe-movement-y))",
}}
onMouseDown={(e) => {
e.stopPropagation();
e.preventDefault();
}}
>
<BaseToast.Close className="absolute top-3 right-3 cursor-pointer text-icon-secondary hover:text-icon-tertiary">
<CloseIcon strokeWidth={1.5} width={16} height={16} />
</BaseToast.Close>
<div className="flex w-full items-start gap-2 p-4">
<div className="py-1">
{data.icon && (
<div
className={cn("flex size-4 flex-shrink-0 items-center justify-center rounded-full", data.iconBgClassName)}
>
{data.icon}
</div>
)}
</div>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<BaseToast.Title className="text-h6-medium text-primary">
{toastData.type === TOAST_TYPE.LOADING ? (toastData.title ?? "Loading...") : toastData.title}
</BaseToast.Title>
{toastData.type !== TOAST_TYPE.LOADING && toastData.message && (
<BaseToast.Description className="text-body-xs-regular text-tertiary">
{toastData.message}
</BaseToast.Description>
)}
{toastData.type !== TOAST_TYPE.LOADING && toastData.actionItems && (
<div className="flex items-center gap-2">{toastData.actionItems}</div>
)}
</div>
</div>
</BaseToast.Root>
);
}
// Static toast component for Storybook and documentation
export type ToastStaticProps = {
type: TOAST_TYPE;
title: string;
message?: string;
actionItems?: React.ReactNode;
theme?: "light" | "dark";
};
export function ToastStatic({ type, title, message, actionItems, theme = "light" }: ToastStaticProps) {
const data = TOAST_DATA[type];
return (
<div data-theme={theme} className="inline-block">
<div
className={cn(
// Base layout and positioning
"group flex w-[350px] items-start rounded-lg border border-subtle-1 shadow-overlay-100",
"relative",
data.backgroundColorClassName,
data.borderColorClassName
)}
>
<div className="absolute top-1 right-1 cursor-default text-icon-tertiary">
<CloseIcon strokeWidth={1.5} width={14} height={14} />
</div>
<div className="flex w-full items-start gap-3 p-4">
<div className="py-1">
{data.icon && (
<div
className={cn(
"flex size-4 flex-shrink-0 items-center justify-center rounded-full",
data.iconBgClassName
)}
>
{data.icon}
</div>
)}
</div>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="text-h6-medium text-primary">
{type === TOAST_TYPE.LOADING ? (title ?? "Loading...") : title}
</div>
{type !== TOAST_TYPE.LOADING && message && (
<div className="text-body-xs-regular text-tertiary">{message}</div>
)}
{type !== TOAST_TYPE.LOADING && actionItems && <div className="flex items-center gap-2">{actionItems}</div>}
</div>
</div>
</div>
</div>
);
}
export const setToast = (props: SetToastProps) => {
let toastId: string | undefined;
if (props.type !== TOAST_TYPE.LOADING) {
toastId = toastManager.add({
data: {
type: props.type,
title: props.title,
message: props.message,
actionItems: props.actionItems,
},
});
} else {
toastId = toastManager.add({
data: {
type: props.type,
title: props.title,
},
});
}
return toastId;
};
export const updateToast = (id: string, props: SetToastProps) => {
toastManager.update(id, {
data:
props.type === TOAST_TYPE.LOADING
? {
type: TOAST_TYPE.LOADING,
title: props.title,
}
: {
type: props.type,
title: props.title,
message: props.message,
actionItems: props.actionItems,
},
});
};
export const setPromiseToast = <ToastData,>(
promise: Promise<ToastData>,
options: PromiseToastOptions<ToastData>
): void => {
toastManager.promise(promise, {
loading: {
data: {
title: options.loading ?? "Loading...",
type: TOAST_TYPE.LOADING,
message: undefined,
actionItems: undefined,
},
},
success: (data) => ({
data: {
type: TOAST_TYPE.SUCCESS,
title: options.success.title,
message: options.success.message?.(data),
actionItems: options.success.actionItems?.(data),
},
}),
error: (data) => ({
data: {
type: TOAST_TYPE.ERROR,
title: options.error.title,
message: options.error.message?.(data),
actionItems: options.error.actionItems?.(data),
},
}),
});
};
export const dismissToast = (tId: string) => {
toastManager.close(tId);
};