diff --git a/packages/propel/package.json b/packages/propel/package.json index 15933f2af5a..58322d53d9e 100644 --- a/packages/propel/package.json +++ b/packages/propel/package.json @@ -32,6 +32,7 @@ "./switch": "./dist/switch/index.js", "./table": "./dist/table/index.js", "./tabs": "./dist/tabs/index.js", + "./toast": "./dist/toast/index.js", "./tooltip": "./dist/tooltip/index.js", "./utils": "./dist/utils/index.js" }, diff --git a/packages/propel/src/charts/components/tooltip.tsx b/packages/propel/src/charts/components/tooltip.tsx index f6b518e140c..a2fa06b3bfe 100644 --- a/packages/propel/src/charts/components/tooltip.tsx +++ b/packages/propel/src/charts/components/tooltip.tsx @@ -1,6 +1,5 @@ import React from "react"; import { NameType, Payload, ValueType } from "recharts/types/component/DefaultTooltipContent"; - import { Card, ECardSpacing } from "../../card"; import { cn } from "../../utils/classname"; diff --git a/packages/propel/src/spinners/circular-bar-spinner.tsx b/packages/propel/src/spinners/circular-bar-spinner.tsx new file mode 100644 index 00000000000..3be8af43aad --- /dev/null +++ b/packages/propel/src/spinners/circular-bar-spinner.tsx @@ -0,0 +1,35 @@ +import * as React from "react"; + +interface ICircularBarSpinner extends React.SVGAttributes { + height?: string; + width?: string; + className?: string | undefined; +} + +export const CircularBarSpinner: React.FC = ({ + height = "16px", + width = "16px", + className = "", +}) => ( +
+ + + + + + + + + + + + +
+); diff --git a/packages/propel/src/spinners/circular-spinner.tsx b/packages/propel/src/spinners/circular-spinner.tsx new file mode 100644 index 00000000000..21dc1cb629e --- /dev/null +++ b/packages/propel/src/spinners/circular-spinner.tsx @@ -0,0 +1,33 @@ +import * as React from "react"; +// helpers +import clsx from "clsx"; + +export interface ISpinner extends React.SVGAttributes { + height?: string; + width?: string; + className?: string | undefined; +} + +export const Spinner: React.FC = ({ height = "32px", width = "32px", className = "" }) => ( +
+ + Loading... +
+); diff --git a/packages/propel/src/spinners/index.ts b/packages/propel/src/spinners/index.ts new file mode 100644 index 00000000000..a871a9b77b8 --- /dev/null +++ b/packages/propel/src/spinners/index.ts @@ -0,0 +1,2 @@ +export * from "./circular-spinner"; +export * from "./circular-bar-spinner"; diff --git a/packages/propel/src/toast/index.ts b/packages/propel/src/toast/index.ts new file mode 100644 index 00000000000..da089861b93 --- /dev/null +++ b/packages/propel/src/toast/index.ts @@ -0,0 +1 @@ +export * from "./toast"; diff --git a/packages/propel/src/toast/toast.tsx b/packages/propel/src/toast/toast.tsx new file mode 100644 index 00000000000..0b0955bd4e8 --- /dev/null +++ b/packages/propel/src/toast/toast.tsx @@ -0,0 +1,232 @@ +import * as React from "react"; +import { Toast as BaseToast } from "@base-ui-components/react/toast"; +import { AlertTriangle, CheckCircle2, X, XCircle } from "lucide-react"; +// 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", +} + +type SetToastProps = + | { + type: TOAST_TYPE.LOADING; + title?: string; + } + | { + id?: string | number; + type: Exclude; + title: string; + message?: string; + actionItems?: React.ReactNode; + }; + +type PromiseToastCallback = (data: ToastData) => string; +type ActionItemsPromiseToastCallback = (data: ToastData) => React.ReactNode; + +type PromiseToastData = { + title: string; + message?: PromiseToastCallback; + actionItems?: ActionItemsPromiseToastCallback; +}; + +type PromiseToastOptions = { + loading?: string; + success: PromiseToastData; + error: PromiseToastData; +}; + +export type ToastProps = { + theme: "light" | "dark" | "system"; +}; + +const toastManager = BaseToast.createToastManager(); + +export const Toast = (props: ToastProps) => ( + + + + + + + +); + +const TOAST_DATA = { + [TOAST_TYPE.SUCCESS]: { + icon: , + textColorClassName: "text-toast-text-success", + backgroundColorClassName: "bg-toast-background-success", + borderColorClassName: "border-toast-border-success", + }, + [TOAST_TYPE.ERROR]: { + icon: , + textColorClassName: "text-toast-text-error", + backgroundColorClassName: "bg-toast-background-error", + borderColorClassName: "border-toast-border-error", + }, + [TOAST_TYPE.WARNING]: { + icon: , + textColorClassName: "text-toast-text-warning", + backgroundColorClassName: "bg-toast-background-warning", + borderColorClassName: "border-toast-border-warning", + }, + [TOAST_TYPE.INFO]: { + icon: <>, + textColorClassName: "text-toast-text-info", + backgroundColorClassName: "bg-toast-background-info", + borderColorClassName: "border-toast-border-info", + }, + [TOAST_TYPE.LOADING]: { + icon: , + textColorClassName: "text-toast-text-loading", + backgroundColorClassName: "bg-toast-background-loading", + borderColorClassName: "border-toast-border-loading", + }, +}; +const ToastList = () => { + const { toasts } = BaseToast.useToastManager(); + return toasts.map((toast) => ); +}; + +const 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 ( + { + e.stopPropagation(); + e.preventDefault(); + }} + > + {toastData.type === TOAST_TYPE.LOADING ? ( +
+ {data.icon &&
{data.icon}
} +
+
+ {toastData.title ?? "Loading..."} +
+ + + +
+
+ ) : ( + <> + + + +
+
+ {data.icon &&
{data.icon}
} +
+ + {toastData.title} + + {toastData.message && ( + + {toastData.message} + + )} +
+
+ {toastData.actionItems &&
{toastData.actionItems}
} +
+ + )} +
+ ); +}; + +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 = ( + promise: Promise, + options: PromiseToastOptions +): 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), + }, + }), + }); +}; diff --git a/packages/propel/tsdown.config.ts b/packages/propel/tsdown.config.ts index 8f23c301cb8..9e4d7dbffff 100644 --- a/packages/propel/tsdown.config.ts +++ b/packages/propel/tsdown.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ "src/switch/index.ts", "src/table/index.ts", "src/tabs/index.ts", + "src/toast/index.ts", "src/tooltip/index.ts", "src/utils/index.ts", ],