diff --git a/.changeset/little-buttons-sort.md b/.changeset/little-buttons-sort.md new file mode 100644 index 0000000000..b4a1ab5ae8 --- /dev/null +++ b/.changeset/little-buttons-sort.md @@ -0,0 +1,13 @@ +--- +"@medusajs/ui": major +--- + +feat(ui): Re-work `` and `` based on `sonner`. + +This update contains breaking changes to how toasts work in `@medusajs/ui`. This update has been made to provide a better user experience and to make it easier to use toasts in your Medusa application. + +### BREAKING CHANGES + +The `useToast` hook has been removed. Users should instead use the `toast` function that is exported from the `@medusajs/ui` package. This function can be used to show toasts in your application. For more information on how to use the `toast` function, please refer to the documentation. + +The `Toaster` component is still available but the options for the component have changed. The default position has been changed to `bottom-right`. For more information on the `Toaster` component, please refer to the documentation. diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json index 9f7cf35383..3cf6cd8e36 100644 --- a/packages/admin-next/dashboard/public/locales/en-US/translation.json +++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json @@ -66,6 +66,7 @@ "cancel": "Cancel", "complete": "Complete", "back": "Back", + "close": "Close", "continue": "Continue", "confirm": "Confirm", "edit": "Edit", @@ -283,6 +284,17 @@ "create": { "header": "Create Option" } + }, + "toasts": { + "delete": { + "success": { + "header": "Product deleted", + "description": "{{title}} was successfully deleted." + }, + "error": { + "header": "Failed to delete product" + } + } } }, "collections": { diff --git a/packages/admin-next/dashboard/src/v2-routes/products/product-list/components/product-list-table/product-list-table.tsx b/packages/admin-next/dashboard/src/v2-routes/products/product-list/components/product-list-table/product-list-table.tsx index 1f80bba000..961ca67d65 100644 --- a/packages/admin-next/dashboard/src/v2-routes/products/product-list/components/product-list-table/product-list-table.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/products/product-list/components/product-list-table/product-list-table.tsx @@ -1,6 +1,7 @@ import { PencilSquare, Trash } from "@medusajs/icons" import type { Product } from "@medusajs/medusa" -import { Button, Container, Heading, usePrompt } from "@medusajs/ui" +import { Button, Container, Heading, toast, usePrompt } from "@medusajs/ui" +import { keepPreviousData } from "@tanstack/react-query" import { createColumnHelper } from "@tanstack/react-table" import { useMemo } from "react" import { useTranslation } from "react-i18next" @@ -8,15 +9,15 @@ import { Link, Outlet, useLoaderData } from "react-router-dom" import { ActionMenu } from "../../../../../components/common/action-menu" import { DataTable } from "../../../../../components/table/data-table" +import { + useDeleteProduct, + useProducts, +} from "../../../../../hooks/api/products" import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns" import { useProductTableFilters } from "../../../../../hooks/table/filters/use-product-table-filters" import { useProductTableQuery } from "../../../../../hooks/table/query/use-product-table-query" import { useDataTable } from "../../../../../hooks/use-data-table" import { productsLoader } from "../../loader" -import { - useDeleteProduct, - useProducts, -} from "../../../../../hooks/api/products" const PAGE_SIZE = 20 @@ -34,7 +35,7 @@ export const ProductListTable = () => { }, { initialData, - keepPreviousData: true, + placeholderData: keepPreviousData, } ) @@ -99,7 +100,22 @@ const ProductActions = ({ product }: { product: Product }) => { return } - await mutateAsync() + await mutateAsync(undefined, { + onSuccess: () => { + toast.success(t("products.toasts.delete.success.header"), { + description: t("products.toasts.delete.success.description", { + title: product.title, + }), + dismissLabel: t("actions.close"), + }) + }, + onError: (e) => { + toast.error(t("products.toasts.delete.error.header"), { + description: e.message, + dismissLabel: t("actions.close"), + }) + }, + }) } return ( diff --git a/packages/design-system/ui/package.json b/packages/design-system/ui/package.json index a6d161151d..73d0bf335a 100644 --- a/packages/design-system/ui/package.json +++ b/packages/design-system/ui/package.json @@ -60,19 +60,19 @@ "@types/react-dom": "^18.2.0", "@vitejs/plugin-react": "^4.0.1", "@vitest/coverage-v8": "^0.32.2", - "autoprefixer": "^10.4.17", + "autoprefixer": "^10.4.19", "chromatic": "^6.20.0", "eslint": "^7.32.0", "eslint-plugin-storybook": "^0.6.12", "jsdom": "^22.1.0", - "postcss": "^8.4.33", + "postcss": "^8.4.38", "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", "resize-observer-polyfill": "^1.5.1", "rimraf": "^5.0.1", "storybook": "^7.0.23", - "tailwindcss": "^3.4.1", + "tailwindcss": "^3.4.3", "tsc-alias": "^1.8.7", "typescript": "^5.1.6", "vite": "^4.3.9", @@ -96,7 +96,6 @@ "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", - "@radix-ui/react-toast": "^1.1.4", "@radix-ui/react-tooltip": "^1.0.6", "@react-aria/datepicker": "^3.5.0", "@react-stately/datepicker": "^3.5.0", @@ -108,6 +107,7 @@ "prismjs": "^1.29.0", "react-currency-input-field": "^3.6.11", "react-day-picker": "^8.8.0", + "sonner": "^1.4.41", "tailwind-merge": "^2.2.1" }, "peerDependencies": { diff --git a/packages/design-system/ui/src/components/toast/toast.stories.tsx b/packages/design-system/ui/src/components/toast/toast.stories.tsx index 563390c884..1980b1fe51 100644 --- a/packages/design-system/ui/src/components/toast/toast.stories.tsx +++ b/packages/design-system/ui/src/components/toast/toast.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react" import * as React from "react" -import { Toast, ToastProvider, ToastViewport } from "./toast" +import { Toast } from "./toast" const meta: Meta = { title: "Components/Toast", @@ -10,13 +10,7 @@ const meta: Meta = { layout: "centered", }, render: (args) => { - return ( - - - - - - ) + return }, } @@ -28,7 +22,7 @@ export const Information: Story = { args: { title: "Label", description: "The quick brown fox jumps over a lazy dog.", - open: true, + variant: "info", }, } @@ -37,7 +31,6 @@ export const Warning: Story = { title: "Label", description: "The quick brown fox jumps over a lazy dog.", variant: "warning", - open: true, }, } @@ -46,7 +39,6 @@ export const Error: Story = { title: "Label", description: "The quick brown fox jumps over a lazy dog.", variant: "error", - open: true, }, } @@ -55,7 +47,6 @@ export const Success: Story = { title: "Label", description: "The quick brown fox jumps over a lazy dog.", variant: "success", - open: true, }, } @@ -64,7 +55,6 @@ export const Loading: Story = { title: "Label", description: "The quick brown fox jumps over a lazy dog.", variant: "loading", - open: true, }, } @@ -73,7 +63,6 @@ export const WithAction: Story = { title: "Scheduled meeting", description: "The meeting has been added to your calendar.", variant: "success", - open: true, action: { altText: "Undo adding meeting to calendar", onClick: () => {}, diff --git a/packages/design-system/ui/src/components/toast/toast.tsx b/packages/design-system/ui/src/components/toast/toast.tsx index b4dbd21da4..dbf332e65a 100644 --- a/packages/design-system/ui/src/components/toast/toast.tsx +++ b/packages/design-system/ui/src/components/toast/toast.tsx @@ -1,189 +1,110 @@ -"use client" - -import { - CheckCircleSolid, - ExclamationCircleSolid, - InformationCircleSolid, - Spinner, - XCircleSolid, -} from "@medusajs/icons" -import * as Primitives from "@radix-ui/react-toast" +import { CheckCircleSolid, ExclamationCircleSolid, InformationCircleSolid, Spinner, XCircleSolid } from "@medusajs/icons" import * as React from "react" +import type { ToastAction, ToastVariant } from "@/types" import { clx } from "@/utils/clx" -const ToastProvider = Primitives.Provider -ToastProvider.displayName = "ToastProvider" - -const ToastViewport = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastViewport.displayName = "Toast.Viewport" - -interface ActionProps { - label: string - altText: string - onClick: () => void | Promise -} - -interface ToastProps - extends React.ComponentPropsWithoutRef { - variant?: "info" | "success" | "warning" | "error" | "loading" - title?: string +interface ToastComponentProps { + id: string | number + variant?: ToastVariant + title: string description?: string - action?: ActionProps - disableDismiss?: boolean + action?: ToastAction + onDismiss?: (id?: string | number) => void + dismissLabel?: string } -/** - * This component is based on the [Radix UI Toast](https://www.radix-ui.com/primitives/docs/components/toast) primitives. - * - * @excludeExternal - */ -const Toast = React.forwardRef< - React.ElementRef, - ToastProps ->( - ( - { - className, - /** - * The toast's style. - */ - variant, - /** - * The toast's title. - */ - title, - /** - * The toast's content. - */ - description, - /** - * The actions to show in the toast as buttons. - */ - action, - /** - * Whether to hide the Close button. - */ - disableDismiss = false, - ...props - }: ToastProps, - ref - ) => { - let Icon = undefined +export const Toast = ({ + id, + variant = "info", + title, + description, + action, + onDismiss, + dismissLabel = "Close", +}: ToastComponentProps) => { + const hasActionables = !!action || onDismiss + let icon = undefined - switch (variant) { - case "success": - Icon = - break - case "warning": - Icon = - break - case "error": - Icon = - break - case "loading": - Icon = - break - default: - Icon = - break - } + switch (variant) { + case "success": + icon = + break + case "warning": + icon = + break + case "error": + icon = + break + case "loading": + icon = + break + case "info": + icon = + break + default: + break + } - if (action && !action.altText) { - // eslint-disable-next-line turbo/no-undeclared-env-vars - if (process.env.NODE_ENV === "development") { - console.warn( - "Omitting `altText` from the action is not recommended. Please provide a description for screen readers." - ) - } - } - - return ( - +
-
- {Icon} -
- {title && ( - - {title} - - )} - {description && ( - - {description} - - )} -
-
+ {!!icon && ( + + {icon} + + )}
- {action && ( - <> - { - e.preventDefault() - action.onClick() - }} - type="button" - > - {action.label} - -
- - )} - {!disableDismiss && ( - + {title} + + + {description} + +
+
+ {hasActionables && ( +
+ {!!action && ( + + )} + {onDismiss && ( + )}
- - ) - } -) -Toast.displayName = "Toast" - -type ToastActionElement = ActionProps - -export { - Toast, - ToastProvider, - ToastViewport, - type ToastActionElement, - type ToastProps, -} + )} +
+ ) +} \ No newline at end of file diff --git a/packages/design-system/ui/src/components/toaster/toaster.stories.tsx b/packages/design-system/ui/src/components/toaster/toaster.stories.tsx new file mode 100644 index 0000000000..95eb4bf834 --- /dev/null +++ b/packages/design-system/ui/src/components/toaster/toaster.stories.tsx @@ -0,0 +1,186 @@ +import type { Meta, StoryObj } from "@storybook/react" +import * as React from "react" + +import { Button } from "@/components/button" +import { toast } from "@/utils/toast" +import { Toaster } from "./toaster" + +const meta: Meta = { + title: "Components/Toaster", + component: Toaster, + parameters: { + layout: "centered", + }, + render: (args) => { + const makeMessageToast = () => { + toast("This is a message toast.") + } + + const makeInfoToast = () => { + toast.info("This is an info toast.") + } + + const makeSuccessToast = () => { + toast.success("This is a success toast.") + } + + const makeWarningToast = () => { + toast.warning("This is a warning toast.") + } + + const makeErrorToast = () => { + toast.error("This is an error toast.") + } + + const makeActionToast = () => { + toast.error("This is an error toast with an action.", { + action: { + label: "Retry", + altText: "Retry the request", + onClick: () => { + console.log("Retrying the request...") + }, + }, + }) + } + + const doAsyncStuff = async () => { + return new Promise((resolve, reject) => { + setTimeout(() => { + if (Math.random() > 0.2) { + resolve("Success!") + } else { + reject("Failed!") + } + }, 3000) + }) + } + + const makeAsyncToast = async () => { + toast.promise(doAsyncStuff, { + loading: "Loading...", + success: "Success!", + error: "Failed!", + }) + } + + const makeUpdatingToast = () => { + let id: number | string = Math.random() + + const retry = async () => { + const coinFlip = Math.random() > 0.5 + + console.log("retrying", id) + + toast.loading("Request in progress", { + id: id, + description: "The request is in progress.", + }) + + // wait 3 seconds + await new Promise((resolve) => setTimeout(resolve, 3000)) + + if (coinFlip) { + toast.success("Request succeeded", { + id: id, + description: "The request succeeded.", + }) + } else { + toast.error("Request failed", { + id: id, + description: + "Insert the description here. Can be semi-long and still work.", + action: { + label: "Retry", + altText: "Retry the request", + onClick: retry, + }, + }) + } + } + + toast.error("Request failed", { + id: id, + description: + "Insert the description here. Can be semi-long and still work.", + action: { + label: "Retry", + altText: "Retry the request", + onClick: retry, + }, + }) + } + + return ( +
+
+ + + + + + + + +
+ +
+ ) + }, +} + +export default meta + +type Story = StoryObj + +export const TopRight: Story = { + args: { + position: "top-right", + }, +} + +export const TopCenter: Story = { + args: { + position: "top-center", + }, +} + +export const TopLeft: Story = { + args: { + position: "top-left", + }, +} + +export const BottomRight: Story = { + args: { + position: "bottom-right", + }, +} + +export const BottomCenter: Story = { + args: { + position: "bottom-center", + }, +} + +export const BottomLeft: Story = { + args: { + position: "bottom-left", + }, +} diff --git a/packages/design-system/ui/src/components/toaster/toaster.tsx b/packages/design-system/ui/src/components/toaster/toaster.tsx index d5947b3b49..1c6b51122d 100644 --- a/packages/design-system/ui/src/components/toaster/toaster.tsx +++ b/packages/design-system/ui/src/components/toaster/toaster.tsx @@ -1,20 +1,41 @@ "use client" import * as React from "react" +import { Toaster as Primitive } from "sonner" -import { Toast, ToastProvider, ToastViewport } from "@/components/toast" -import { useToast } from "@/hooks/use-toast" +import { clx } from "@/utils/clx" -const Toaster = () => { - const { toasts } = useToast() +interface ToasterProps + extends Omit< + React.ComponentPropsWithoutRef, + | "richColors" + | "closeButton" + | "icons" + | "theme" + | "invert" + | "loadingIcon" + | "cn" + | "toastOptions" + > {} +const Toaster = ({ + position = "bottom-right", + gap = 12, + offset = 24, + duration, + ...props +}: ToasterProps) => { return ( - - {toasts.map(({ id, ...props }) => { - return - })} - - + ) } diff --git a/packages/design-system/ui/src/hooks/use-toast/index.ts b/packages/design-system/ui/src/hooks/use-toast/index.ts deleted file mode 100644 index 3fe207215e..0000000000 --- a/packages/design-system/ui/src/hooks/use-toast/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./use-toast" diff --git a/packages/design-system/ui/src/hooks/use-toast/use-toast.stories.tsx b/packages/design-system/ui/src/hooks/use-toast/use-toast.stories.tsx deleted file mode 100644 index 3d61c36f4f..0000000000 --- a/packages/design-system/ui/src/hooks/use-toast/use-toast.stories.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react" -import * as React from "react" - -import { Button } from "@/components/button" -import { Toaster } from "@/components/toaster" -import { useToast } from "./use-toast" - -const Demo = () => { - const { toast } = useToast() - - const handleUndo = async () => { - // Wait 3 seconds then resolve with true - const confirmed = await new Promise((resolve) => { - setTimeout(() => { - resolve(true) - }, 3000) - }) - - return confirmed - } - - const handleSchedule = () => { - const onAction = async (fn: (update: any) => void) => { - fn({ - title: "Processing", - description: "Removing post from queue.", - variant: "loading", - disableDismiss: true, - action: undefined, - }) - - const confirmed = await handleUndo().then((confirmed) => { - if (confirmed) { - fn({ - title: "Success", - description: "Your post was successfully removed from the queue.", - variant: "success", - disableDismiss: false, - }) - } else { - fn({ - title: "Error", - description: - "Something went wrong, and your post was not removed from the queue. Try again.", - variant: "error", - disableDismiss: false, - }) - } - }) - } - - const t = toast({ - title: "Scheduled", - description: "Your post has been scheduled for 12:00 PM", - variant: "success", - action: { - label: "Undo", - onClick: () => onAction(t.update), - altText: - "Alternatively, you can undo this action by navigating to the scheduled posts page, and clicking the unschedule button.", - }, - }) - } - - return ( -
- - -
- ) -} - -const meta: Meta = { - title: "Hooks/useToast", - component: Demo, - parameters: { - layout: "centered", - }, -} - -export default meta - -type Story = StoryObj - -export const Default: Story = {} diff --git a/packages/design-system/ui/src/hooks/use-toast/use-toast.tsx b/packages/design-system/ui/src/hooks/use-toast/use-toast.tsx deleted file mode 100644 index e60cd6c667..0000000000 --- a/packages/design-system/ui/src/hooks/use-toast/use-toast.tsx +++ /dev/null @@ -1,188 +0,0 @@ -"use client" - -import * as React from "react" - -import type { ToastActionElement, ToastProps } from "@/components/toast" - -const TOAST_LIMIT = 1 -const TOAST_REMOVE_DELAY = 1000000 - -type ToasterToast = ToastProps & { - id: string - title?: React.ReactNode - description?: React.ReactNode - action?: ToastActionElement -} - -const actionTypes = { - ADD_TOAST: "ADD_TOAST", - UPDATE_TOAST: "UPDATE_TOAST", - DISMISS_TOAST: "DISMISS_TOAST", - REMOVE_TOAST: "REMOVE_TOAST", -} as const - -let count = 0 - -function genId() { - count = (count + 1) % Number.MAX_VALUE - return count.toString() -} - -type ActionType = typeof actionTypes - -type Action = - | { - type: ActionType["ADD_TOAST"] - toast: ToasterToast - } - | { - type: ActionType["UPDATE_TOAST"] - toast: Partial - } - | { - type: ActionType["DISMISS_TOAST"] - toastId?: ToasterToast["id"] - } - | { - type: ActionType["REMOVE_TOAST"] - toastId?: ToasterToast["id"] - } - -interface State { - toasts: ToasterToast[] -} - -const toastTimeouts = new Map>() - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId) - dispatch({ - type: "REMOVE_TOAST", - toastId: toastId, - }) - }, TOAST_REMOVE_DELAY) - - toastTimeouts.set(toastId, timeout) -} - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case "ADD_TOAST": - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - } - - case "UPDATE_TOAST": - return { - ...state, - toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t - ), - } - - case "DISMISS_TOAST": { - const { toastId } = action - - if (toastId) { - addToRemoveQueue(toastId) - } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id) - }) - } - - return { - ...state, - toasts: state.toasts.map((t) => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false, - } - : t - ), - } - } - case "REMOVE_TOAST": - if (action.toastId === undefined) { - return { - ...state, - toasts: [], - } - } - return { - ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId), - } - } -} - -const listeners: Array<(state: State) => void> = [] - -let memoryState: State = { toasts: [] } - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action) - listeners.forEach((listener) => { - listener(memoryState) - }) -} - -type Toast = Omit - -function toast({ ...props }: Toast) { - const id = genId() - - const update = (props: ToasterToast) => - dispatch({ - type: "UPDATE_TOAST", - toast: { ...props, id }, - }) - const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) - - dispatch({ - type: "ADD_TOAST", - toast: { - ...props, - id, - open: true, - onOpenChange: (open) => { - if (!open) dismiss() - }, - }, - }) - - return { - id: id, - dismiss, - update, - } -} - -function useToast() { - const [state, setState] = React.useState(memoryState) - - React.useEffect(() => { - listeners.push(setState) - return () => { - const index = listeners.indexOf(setState) - if (index > -1) { - listeners.splice(index, 1) - } - } - }, [state]) - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - } -} - -export { toast, useToast } diff --git a/packages/design-system/ui/src/index.ts b/packages/design-system/ui/src/index.ts index d77d2a5b90..a013cab65b 100644 --- a/packages/design-system/ui/src/index.ts +++ b/packages/design-system/ui/src/index.ts @@ -40,11 +40,11 @@ export { Tooltip } from "./components/tooltip" // Hooks export { usePrompt } from "./hooks/use-prompt" -export { useToast } from "./hooks/use-toast" export { useToggleState } from "./hooks/use-toggle-state" // Utils export { clx } from "./utils/clx" +export { toast } from "./utils/toast" // Types export * from "./types" diff --git a/packages/design-system/ui/src/types.ts b/packages/design-system/ui/src/types.ts index 74ce771d92..0785720a66 100644 --- a/packages/design-system/ui/src/types.ts +++ b/packages/design-system/ui/src/types.ts @@ -1,5 +1,9 @@ +// Progress types + export type ProgressStatus = "not-started" | "in-progress" | "completed" +// Calendar types + export type DateRange = { /** * The range's start date. @@ -10,3 +14,30 @@ export type DateRange = { */ to?: Date | undefined } + +// Toast types + +export type ToasterPosition = + | "top-left" + | "top-center" + | "top-right" + | "bottom-left" + | "bottom-center" + | "bottom-right" + +export type ToastVariant = + | "info" + | "success" + | "warning" + | "error" + | "loading" + | "message" + +export type ToastActionVariant = "default" | "destructive" + +export type ToastAction = { + label: string + altText: string + onClick: () => void | Promise + variant?: ToastActionVariant +} diff --git a/packages/design-system/ui/src/utils/toast.tsx b/packages/design-system/ui/src/utils/toast.tsx new file mode 100644 index 0000000000..8b4b3b4fb3 --- /dev/null +++ b/packages/design-system/ui/src/utils/toast.tsx @@ -0,0 +1,214 @@ +import { Toast } from "@/components/toast" +import { ToastAction, ToastVariant, ToasterPosition } from "@/types" +import * as React from "react" +import { toast as toastFn } from "sonner" + +interface BaseToastProps { + id?: string | number + position?: ToasterPosition + dismissable?: boolean + dismissLabel?: string + duration?: number +} + +interface ToastProps extends BaseToastProps { + description?: string + action?: ToastAction +} + +function create(variant: ToastVariant, title: string, props: ToastProps = {}) { + return toastFn.custom( + (t) => { + return ( + toastFn.dismiss(props.id || t) : undefined + } + dismissLabel={props.dismissLabel} + variant={variant} + action={props.action} + /> + ) + }, + { + id: props.id, + position: props.position, + dismissible: props.dismissable, + } + ) +} + +function message( + /** + * The title of the toast. + */ + title: string, + /** + * The props of the toast. + */ + props: ToastProps = {} +) { + return create("message", title, props) +} + +function info( + /** + * The title of the toast. + */ title: string, + /** + * The props of the toast. + */ + props: ToastProps = {} +) { + return create("info", title, props) +} + +function error( + /** + * The title of the toast. + */ title: string, + /** + * The props of the toast. + */ + props: ToastProps = {} +) { + return create("error", title, props) +} + +function success( + /** + * The title of the toast. + */ title: string, + /** + * The props of the toast. + */ + props: ToastProps = {} +) { + return create("success", title, props) +} + +function warning( + /** + * The title of the toast. + */ title: string, + /** + * The props of the toast. + */ + props: ToastProps = {} +) { + return create("warning", title, props) +} + +type LoadingToastProps = Omit + +function loading +/** + * The title of the toast. + */( + title: string, + /** + * The props of the toast. + */ + props: ToastProps = {} +) { + return create("loading", title, { ...props, dismissable: false }) +} + +type PromiseStateProps = + | string + | { + title: string + description?: string + } + +interface PromiseToastProps extends BaseToastProps { + loading: PromiseStateProps + success: PromiseStateProps + error: PromiseStateProps +} + +function createUniqueID() { + return Math.random().toString(36).slice(2, 8) +} + +async function promise( + /** + * The promise to be resolved. + */ + promise: Promise | (() => Promise), + /** + * The props of the toast. + */ + props: PromiseToastProps +) { + let id: string | number | undefined = props.id || createUniqueID() + let shouldDismiss = id !== undefined + + id = create( + "loading", + typeof props.loading === "string" ? props.loading : props.loading.title, + { + id: id, + position: props.position, + description: + typeof props.loading === "string" + ? undefined + : props.loading.description, + duration: Infinity, + } + ) + + const p = promise instanceof Promise ? promise : promise() + + p.then(() => { + shouldDismiss = false + create( + "success", + typeof props.success === "string" ? props.success : props.success.title, + { + id: id, + position: props.position, + description: + typeof props.success === "string" + ? undefined + : props.success.description, + } + ) + }) + .catch(() => { + shouldDismiss = false + create( + "error", + typeof props.error === "string" ? props.error : props.error.title, + { + id: id, + position: props.position, + description: + typeof props.error === "string" + ? undefined + : props.error.description, + } + ) + }) + .finally(() => { + if (shouldDismiss) { + toastFn.dismiss(id) + id = undefined + } + }) + + return id +} + +export const toast = Object.assign(message, { + info, + error, + warning, + success, + promise, + loading, + dismiss: toastFn.dismiss, +}) diff --git a/yarn.lock b/yarn.lock index 6fcb2ba11a..cbdc7bdacd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9057,7 +9057,6 @@ __metadata: "@radix-ui/react-slot": ^1.0.2 "@radix-ui/react-switch": ^1.0.3 "@radix-ui/react-tabs": ^1.0.4 - "@radix-ui/react-toast": ^1.1.4 "@radix-ui/react-tooltip": ^1.0.6 "@react-aria/datepicker": ^3.5.0 "@react-stately/datepicker": ^3.5.0 @@ -9078,7 +9077,7 @@ __metadata: "@types/react-dom": ^18.2.0 "@vitejs/plugin-react": ^4.0.1 "@vitest/coverage-v8": ^0.32.2 - autoprefixer: ^10.4.17 + autoprefixer: ^10.4.19 chromatic: ^6.20.0 clsx: ^1.2.1 copy-to-clipboard: ^3.3.3 @@ -9087,7 +9086,7 @@ __metadata: eslint: ^7.32.0 eslint-plugin-storybook: ^0.6.12 jsdom: ^22.1.0 - postcss: ^8.4.33 + postcss: ^8.4.38 prism-react-renderer: ^2.0.6 prismjs: ^1.29.0 prop-types: ^15.8.1 @@ -9097,9 +9096,10 @@ __metadata: react-dom: ^18.2.0 resize-observer-polyfill: ^1.5.1 rimraf: ^5.0.1 + sonner: ^1.4.41 storybook: ^7.0.23 tailwind-merge: ^2.2.1 - tailwindcss: ^3.4.1 + tailwindcss: ^3.4.3 tsc-alias: ^1.8.7 typescript: ^5.1.6 vite: ^4.3.9 @@ -11418,37 +11418,6 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-toast@npm:^1.1.4": - version: 1.1.4 - resolution: "@radix-ui/react-toast@npm:1.1.4" - dependencies: - "@babel/runtime": ^7.13.10 - "@radix-ui/primitive": 1.0.1 - "@radix-ui/react-collection": 1.0.3 - "@radix-ui/react-compose-refs": 1.0.1 - "@radix-ui/react-context": 1.0.1 - "@radix-ui/react-dismissable-layer": 1.0.4 - "@radix-ui/react-portal": 1.0.3 - "@radix-ui/react-presence": 1.0.1 - "@radix-ui/react-primitive": 1.0.3 - "@radix-ui/react-use-callback-ref": 1.0.1 - "@radix-ui/react-use-controllable-state": 1.0.1 - "@radix-ui/react-use-layout-effect": 1.0.1 - "@radix-ui/react-visually-hidden": 1.0.3 - peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - peerDependenciesMeta: - "@types/react": - optional: true - "@types/react-dom": - optional: true - checksum: bb8282ca87820763d3912e11c1693aa414907d08dabd10a07e77ec0ac06a27e9eed76a591fb90dec147454e674c4c57a961b541e2de76c8125ab1aefaab62c2e - languageName: node - linkType: hard - "@radix-ui/react-toggle-group@npm:1.0.4": version: 1.0.4 resolution: "@radix-ui/react-toggle-group@npm:1.0.4" @@ -20976,6 +20945,24 @@ __metadata: languageName: node linkType: hard +"autoprefixer@npm:^10.4.19": + version: 10.4.19 + resolution: "autoprefixer@npm:10.4.19" + dependencies: + browserslist: ^4.23.0 + caniuse-lite: ^1.0.30001599 + fraction.js: ^4.3.7 + normalize-range: ^0.1.2 + picocolors: ^1.0.0 + postcss-value-parser: ^4.2.0 + peerDependencies: + postcss: ^8.1.0 + bin: + autoprefixer: bin/autoprefixer + checksum: fe0178eb8b1da4f15c6535cd329926609b22d1811e047371dccce50563623f8075dd06fb167daff059e4228da651b0bdff6d9b44281541eaf0ce0b79125bfd19 + languageName: node + linkType: hard + "autoprefixer@npm:^9.8.6": version: 9.8.8 resolution: "autoprefixer@npm:9.8.8" @@ -22388,6 +22375,20 @@ __metadata: languageName: node linkType: hard +"browserslist@npm:^4.23.0": + version: 4.23.0 + resolution: "browserslist@npm:4.23.0" + dependencies: + caniuse-lite: ^1.0.30001587 + electron-to-chromium: ^1.4.668 + node-releases: ^2.0.14 + update-browserslist-db: ^1.0.13 + bin: + browserslist: cli.js + checksum: 8e9cc154529062128d02a7af4d8adeead83ca1df8cd9ee65a88e2161039f3d68a4d40fea7353cab6bae4c16182dec2fdd9a1cf7dc2a2935498cee1af0e998943 + languageName: node + linkType: hard + "bs-logger@npm:0.x": version: 0.2.6 resolution: "bs-logger@npm:0.2.6" @@ -22915,6 +22916,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001587, caniuse-lite@npm:^1.0.30001599": + version: 1.0.30001610 + resolution: "caniuse-lite@npm:1.0.30001610" + checksum: 015956a0bf2e3e233da3dc00c5632bbb4d416bcd6ced2f839e33e45b197a856234f97cb046e7427b83d7e3a3d6df314dfab1c86eb9d970970e00ad85a50b4933 + languageName: node + linkType: hard + "capital-case@npm:^1.0.4": version: 1.0.4 resolution: "capital-case@npm:1.0.4" @@ -26256,6 +26264,13 @@ __metadata: languageName: node linkType: hard +"electron-to-chromium@npm:^1.4.668": + version: 1.4.736 + resolution: "electron-to-chromium@npm:1.4.736" + checksum: f3acb515dcb9333b318f9a8ee80a0c3975162da6cc45d9c00152e0de7004b2bfe1246fe6cdf0c41a1b7016780bd4324aff848830aba4227cb55480c1aa32d248 + languageName: node + linkType: hard + "elliptic@npm:^6.5.3": version: 6.5.4 resolution: "elliptic@npm:6.5.4" @@ -35839,6 +35854,15 @@ __metadata: languageName: node linkType: hard +"jiti@npm:^1.21.0": + version: 1.21.0 + resolution: "jiti@npm:1.21.0" + bin: + jiti: bin/jiti.js + checksum: 7f361219fe6c7a5e440d5f1dba4ab763a5538d2df8708cdc22561cf25ea3e44b837687931fca7cdd8cdd9f567300e90be989dd1321650045012d8f9ed6aab07f + languageName: node + linkType: hard + "jmespath@npm:0.16.0": version: 0.16.0 resolution: "jmespath@npm:0.16.0" @@ -42956,6 +42980,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.4.38": + version: 8.4.38 + resolution: "postcss@npm:8.4.38" + dependencies: + nanoid: ^3.3.7 + picocolors: ^1.0.0 + source-map-js: ^1.2.0 + checksum: 955407b8f70cf0c14acf35dab3615899a2a60a26718a63c848cf3c29f2467b0533991b985a2b994430d890bd7ec2b1963e36352b0774a19143b5f591540f7c06 + languageName: node + linkType: hard + "postgres-array@npm:~2.0.0": version: 2.0.0 resolution: "postgres-array@npm:2.0.0" @@ -47357,6 +47392,16 @@ __metadata: languageName: node linkType: hard +"sonner@npm:^1.4.41": + version: 1.4.41 + resolution: "sonner@npm:1.4.41" + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 31467ecab0fcbd5161fb76d158c698841b524686f7f598eecbc2a58d76bc496b5d5242df4350e274575aa3df59428d3aacd534415c968b19fc309c192c443330 + languageName: node + linkType: hard + "sort-keys@npm:^5.0.0": version: 5.0.0 resolution: "sort-keys@npm:5.0.0" @@ -47387,6 +47432,13 @@ __metadata: languageName: node linkType: hard +"source-map-js@npm:^1.2.0": + version: 1.2.0 + resolution: "source-map-js@npm:1.2.0" + checksum: 7e5f896ac10a3a50fe2898e5009c58ff0dc102dcb056ed27a354623a0ece8954d4b2649e1a1b2b52ef2e161d26f8859c7710350930751640e71e374fe2d321a4 + languageName: node + linkType: hard + "source-map-resolve@npm:^0.5.0": version: 0.5.3 resolution: "source-map-resolve@npm:0.5.3" @@ -48872,6 +48924,39 @@ __metadata: languageName: node linkType: hard +"tailwindcss@npm:^3.4.3": + version: 3.4.3 + resolution: "tailwindcss@npm:3.4.3" + dependencies: + "@alloc/quick-lru": ^5.2.0 + arg: ^5.0.2 + chokidar: ^3.5.3 + didyoumean: ^1.2.2 + dlv: ^1.1.3 + fast-glob: ^3.3.0 + glob-parent: ^6.0.2 + is-glob: ^4.0.3 + jiti: ^1.21.0 + lilconfig: ^2.1.0 + micromatch: ^4.0.5 + normalize-path: ^3.0.0 + object-hash: ^3.0.0 + picocolors: ^1.0.0 + postcss: ^8.4.23 + postcss-import: ^15.1.0 + postcss-js: ^4.0.1 + postcss-load-config: ^4.0.1 + postcss-nested: ^6.0.1 + postcss-selector-parser: ^6.0.11 + resolve: ^1.22.2 + sucrase: ^3.32.0 + bin: + tailwind: lib/cli.js + tailwindcss: lib/cli.js + checksum: 11e5546494f2888f693ebaa271b218b3a8e52fe59d7b629e54f2dffd6eaafd5ded2e9f0c37ad04e6a866dffb2b116d91becebad77e1441beee8bf016bb2392f9 + languageName: node + linkType: hard + "tapable@npm:^1.0.0, tapable@npm:^1.1.3": version: 1.1.3 resolution: "tapable@npm:1.1.3"