draft: toast migration

This commit is contained in:
vamsikrishnamathala
2025-08-19 12:11:08 +05:30
parent 6bf36ad85c
commit 0917ef6f5f

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import { Toaster, toast } from "sonner";
// icons
import { Toast as BaseToast } from "@base-ui-components/react/toast";
import { AlertTriangle, CheckCircle2, X, XCircle } from "lucide-react";
import * as React from "react";
// icons
// spinner
import { CircularBarSpinner } from "../spinners";
// helper
@@ -43,158 +43,207 @@ type PromiseToastOptions<ToastData> = {
error: PromiseToastData<ToastData>;
};
type ToastContentProps = {
toastId: string | number;
icon?: React.ReactNode;
textColorClassName: string;
backgroundColorClassName: string;
borderColorClassName: string;
};
type ToastProps = {
theme: "light" | "dark" | "system";
};
// Global toast manager to allow triggering from anywhere
const toastManager = BaseToast.createToastManager();
export const Toast = (props: ToastProps) => {
const { theme } = props;
return <Toaster visibleToasts={5} gap={16} theme={theme} />;
return (
<BaseToast.Provider toastManager={toastManager} limit={5} timeout={4000}>
<BaseToast.Viewport
render={(vpProps) => (
<div
{...vpProps}
className={cn(
vpProps.className,
"fixed isolate z-[100] bottom-4 right-4 flex w-full max-w-[420px] flex-col gap-3 px-4",
"data-[expanded]:[&>*]:translate-y-[var(--toast-offset-y)]"
)}
>
{vpProps.children}
</div>
)}
>
<ToastList />
</BaseToast.Viewport>
<div data-theme={theme} />
</BaseToast.Provider>
);
};
export const setToast = (props: SetToastProps) => {
const renderToastContent = ({
toastId,
icon,
textColorClassName,
backgroundColorClassName,
borderColorClassName,
}: ToastContentProps) =>
props.type === TOAST_TYPE.LOADING ? (
<div className="flex items-center h-[98px] w-[350px]" data-prevent-outside-click>
<div
onMouseDown={(e) => {
e.stopPropagation();
e.preventDefault();
}}
className={cn("w-full rounded-lg border shadow-sm p-2", backgroundColorClassName, borderColorClassName)}
>
<div className="w-full h-full flex items-center justify-center px-4 py-2">
{icon && <div className="flex items-center justify-center">{icon}</div>}
<div className={cn("w-full flex items-center gap-0.5 pr-1", icon ? "pl-4" : "pl-1")}>
<div className={cn("grow text-sm font-semibold", textColorClassName)}>{props.title ?? "Loading..."}</div>
<div className="flex-shrink-0">
<X
className="text-toast-text-secondary hover:text-toast-text-tertiary cursor-pointer"
strokeWidth={1.5}
width={14}
height={14}
onClick={() => toast.dismiss(toastId)}
/>
function ToastList() {
const { toasts } = BaseToast.useToastManager();
return toasts.map((t) => {
const type =
((t as any).type as TOAST_TYPE) || (((t.data as any) && (t.data as any).tone) as TOAST_TYPE) || TOAST_TYPE.INFO;
const classesByType = (toastType: TOAST_TYPE) => {
switch (toastType) {
case TOAST_TYPE.SUCCESS:
return {
text: "text-toast-text-success",
bg: "bg-toast-background-success",
border: "border-toast-border-success",
icon: <CheckCircle2 width={24} height={24} strokeWidth={1.5} className="text-toast-text-success" />,
};
case TOAST_TYPE.ERROR:
return {
text: "text-toast-text-error",
bg: "bg-toast-background-error",
border: "border-toast-border-error",
icon: <XCircle width={24} height={24} strokeWidth={1.5} className="text-toast-text-error" />,
};
case TOAST_TYPE.WARNING:
return {
text: "text-toast-text-warning",
bg: "bg-toast-background-warning",
border: "border-toast-border-warning",
icon: <AlertTriangle width={24} height={24} strokeWidth={1.5} className="text-toast-text-warning" />,
};
case TOAST_TYPE.LOADING:
return {
text: "text-toast-text-loading",
bg: "bg-toast-background-loading",
border: "border-toast-border-loading",
icon: <CircularBarSpinner className="text-toast-text-tertiary" />,
};
case TOAST_TYPE.INFO:
default:
return {
text: "text-toast-text-info",
bg: "bg-toast-background-info",
border: "border-toast-border-info",
icon: undefined as React.ReactNode | undefined,
};
}
};
const cls = classesByType(type);
const actionItems =
typeof (t.data as any)?.actionItems === "function" ? (t.data as any).actionItems() : (t.data as any)?.actionItems;
return (
<BaseToast.Root
key={t.id}
toast={t}
className={cn(
"[--toast-index:var(--toast-index)] relative group flex flex-col w-[350px] rounded-lg border shadow-custom-shadow-md p-2",
"transition-all duration-300",
"data-[starting-style]:opacity-0 data-[starting-style]:-translate-y-2",
"data-[ending-style]:opacity-0 data-[ending-style]:translate-y-2",
cls.bg,
cls.border
)}
>
{type === TOAST_TYPE.LOADING ? (
<div className="flex items-center h-[98px] w-[350px]" data-prevent-outside-click>
<div className={cn("w-full rounded-lg border shadow-sm p-2", cls.bg, cls.border)}>
<div className="w-full h-full flex items-center justify-center px-4 py-2">
{cls.icon && <div className="flex items-center justify-center">{cls.icon}</div>}
<div className={cn("w-full flex items-center gap-0.5 pr-1", cls.icon ? "pl-4" : "pl-1", cls.text)}>
<div className={cn("grow text-sm font-semibold", cls.text)}>{t.title ?? "Loading..."}</div>
<div className="flex-shrink-0">
<BaseToast.Close
render={
<button
type="button"
aria-label="Close"
className="text-toast-text-secondary hover:text-toast-text-tertiary cursor-pointer"
>
<X strokeWidth={1.5} width={14} height={14} />
</button>
}
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
) : (
<div
data-prevent-outside-click
onMouseDown={(e) => {
e.stopPropagation();
e.preventDefault();
}}
className={cn(
"relative group flex flex-col w-[350px] rounded-lg border shadow-sm p-2",
backgroundColorClassName,
borderColorClassName
)}
>
<X
className="absolute top-2 right-2.5 text-toast-text-secondary hover:text-toast-text-tertiary cursor-pointer"
strokeWidth={1.5}
width={14}
height={14}
onClick={() => toast.dismiss(toastId)}
/>
<div className="w-full flex flex-col gap-2 p-2">
<div className="flex items-center w-full">
{icon && <div className="flex items-center justify-center">{icon}</div>}
<div className={cn("flex flex-col gap-0.5 pr-1", icon ? "pl-4" : "pl-1")}>
<div className={cn("text-sm font-semibold", textColorClassName)}>{props.title}</div>
{props.message && <div className="text-toast-text-secondary text-xs font-medium">{props.message}</div>}
) : (
<div data-prevent-outside-click>
<BaseToast.Close
render={
<button
type="button"
aria-label="Close"
className="absolute top-2 right-2.5 text-toast-text-secondary hover:text-toast-text-tertiary cursor-pointer"
>
<X strokeWidth={1.5} width={14} height={14} />
</button>
}
/>
<div className="w-full flex flex-col gap-2 p-2">
<div className="flex items-center w-full">
{cls.icon && <div className="flex items-center justify-center">{cls.icon}</div>}
<div className={cn("flex flex-col gap-0.5 pr-1", cls.icon ? "pl-4" : "pl-1")}>
<div className={cn("text-sm font-semibold", cls.text)}>{t.title}</div>
{t.description && (
<div className={cn("text-xs font-medium", "text-toast-text-secondary")}>{t.description}</div>
)}
</div>
</div>
{actionItems && <div className="flex items-center pl-[32px]">{actionItems}</div>}
</div>
</div>
{props.actionItems && <div className="flex items-center pl-[32px]">{props.actionItems}</div>}
</div>
</div>
)}
</BaseToast.Root>
);
});
}
switch (props.type) {
case TOAST_TYPE.SUCCESS:
return toast.custom(
(toastId) =>
renderToastContent({
toastId,
icon: <CheckCircle2 width={24} height={24} strokeWidth={1.5} className="text-toast-text-success" />,
textColorClassName: "text-toast-text-success",
backgroundColorClassName: "bg-toast-background-success",
borderColorClassName: "border-toast-border-success",
}),
props.id ? { id: props.id } : {}
);
case TOAST_TYPE.ERROR:
return toast.custom(
(toastId) =>
renderToastContent({
toastId,
icon: <XCircle width={24} height={24} strokeWidth={1.5} className="text-toast-text-error" />,
textColorClassName: "text-toast-text-error",
backgroundColorClassName: "bg-toast-background-error",
borderColorClassName: "border-toast-border-error",
}),
props.id ? { id: props.id } : {}
);
case TOAST_TYPE.WARNING:
return toast.custom(
(toastId) =>
renderToastContent({
toastId,
icon: <AlertTriangle width={24} height={24} strokeWidth={1.5} className="text-toast-text-warning" />,
textColorClassName: "text-toast-text-warning",
backgroundColorClassName: "bg-toast-background-warning",
borderColorClassName: "border-toast-border-warning",
}),
props.id ? { id: props.id } : {}
);
case TOAST_TYPE.INFO:
return toast.custom(
(toastId) =>
renderToastContent({
toastId,
textColorClassName: "text-toast-text-info",
backgroundColorClassName: "bg-toast-background-info",
borderColorClassName: "border-toast-border-info",
}),
props.id ? { id: props.id } : {}
);
case TOAST_TYPE.LOADING:
return toast.custom((toastId) =>
renderToastContent({
toastId,
icon: <CircularBarSpinner className="text-toast-text-tertiary" />,
textColorClassName: "text-toast-text-loading",
backgroundColorClassName: "bg-toast-background-loading",
borderColorClassName: "border-toast-border-loading",
})
);
export const setToast = (props: SetToastProps) => {
if (props.type === TOAST_TYPE.LOADING) {
// Add loading toast (non-dismissable by timeout)
const id = toastManager.add({
title: props.title ?? "Loading...",
type: TOAST_TYPE.LOADING,
timeout: 0,
data: {},
});
return id;
}
const { id, type, title, message, actionItems } = props as Exclude<SetToastProps, { type: TOAST_TYPE.LOADING }>;
if (id !== undefined) {
toastManager.update(String(id), {
title,
description: message,
type,
data: { actionItems },
});
return id;
}
return toastManager.add({
title,
description: message,
type,
data: { actionItems },
});
};
export const setPromiseToast = <ToastData,>(
promise: Promise<ToastData>,
options: PromiseToastOptions<ToastData>
): void => {
// create a loading toast and keep its id for subsequent updates
const tId = setToast({ type: TOAST_TYPE.LOADING, title: options.loading });
// also wire Base UI's promise helper to manage description lifecycle
// using strings/functions per API for loading/success/error
toastManager.promise(promise, {
loading: options.loading ?? "Loading...",
success: options.success.message ? (data: ToastData) => options.success.message!(data) : options.success.title,
error: options.error.message ? (data: ToastData) => options.error.message!(data) : options.error.title,
});
// preserve title + action items behavior exactly like the existing API
promise
.then((data: ToastData) => {
setToast({