feat: Update toast design (#8018)
**What** - Updates the Toast component and `toast` util to match the latest design. - Updates every usage of `toast` as `dismissableLabel` is no longer a valid prop, as we now render a X mark instead of a text button.
This commit is contained in:
committed by
GitHub
parent
24fc6befd2
commit
6e613f4f50
@@ -105,7 +105,7 @@
|
||||
"react-aria": "^3.33.1",
|
||||
"react-currency-input-field": "^3.6.11",
|
||||
"react-stately": "^3.31.1",
|
||||
"sonner": "^1.4.41",
|
||||
"sonner": "^1.5.0",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"upgrade": "^1.1.0",
|
||||
"yarn": "^1.22.22"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Keyboard } from "@medusajs/icons"
|
||||
import { Kbd } from "../kbd"
|
||||
import { Toast } from "./toast"
|
||||
|
||||
const meta: Meta<typeof Toast> = {
|
||||
@@ -58,6 +60,13 @@ export const Loading: Story = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Message: Story = {
|
||||
args: {
|
||||
title: <span>Next time hit <Kbd>G</Kbd> then <Kbd>O</Kbd> to go to orders.</span>,
|
||||
icon: <Keyboard className="text-ui-fg-subtle" />,
|
||||
}
|
||||
}
|
||||
|
||||
export const WithAction: Story = {
|
||||
args: {
|
||||
title: "Scheduled meeting",
|
||||
@@ -70,3 +79,10 @@ export const WithAction: Story = {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const NoDescription: Story = {
|
||||
args: {
|
||||
title: "Label",
|
||||
variant: "info",
|
||||
},
|
||||
}
|
||||
@@ -1,17 +1,26 @@
|
||||
import { CheckCircleSolid, ExclamationCircleSolid, InformationCircleSolid, Spinner, XCircleSolid } from "@medusajs/icons"
|
||||
import {
|
||||
CheckCircleSolid,
|
||||
ExclamationCircleSolid,
|
||||
InformationCircleSolid,
|
||||
Spinner,
|
||||
XCircleSolid,
|
||||
XMark,
|
||||
} from "@medusajs/icons"
|
||||
import * as React from "react"
|
||||
|
||||
import type { ToastAction, ToastVariant } from "@/types"
|
||||
import { clx } from "@/utils/clx"
|
||||
import { toast } from "sonner"
|
||||
import { IconButton } from "../icon-button"
|
||||
|
||||
interface ToastComponentProps {
|
||||
id: string | number
|
||||
variant?: ToastVariant
|
||||
title: string
|
||||
description?: string
|
||||
title: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastAction
|
||||
onDismiss?: (id?: string | number) => void
|
||||
dismissLabel?: string
|
||||
icon?: React.ReactNode
|
||||
dismissable?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,7 +33,7 @@ export const Toast = ({
|
||||
id,
|
||||
/**
|
||||
* @ignore
|
||||
*
|
||||
*
|
||||
* @privateRemarks
|
||||
* As the Toast component is created using
|
||||
* the `toast` utility functions, the variant is inferred
|
||||
@@ -33,11 +42,18 @@ export const Toast = ({
|
||||
variant = "info",
|
||||
/**
|
||||
* @ignore
|
||||
*
|
||||
*
|
||||
* @privateRemarks
|
||||
* The `toast` utility functions accept this as a parameter.
|
||||
*/
|
||||
title,
|
||||
/**
|
||||
* @ignore
|
||||
*
|
||||
* @privateRemarks
|
||||
* The `toast` utility functions accept this as a parameter.
|
||||
*/
|
||||
icon: _icon,
|
||||
/**
|
||||
* The toast's text.
|
||||
*/
|
||||
@@ -48,48 +64,43 @@ export const Toast = ({
|
||||
action,
|
||||
/**
|
||||
* @ignore
|
||||
*
|
||||
*
|
||||
* @privateRemarks
|
||||
* The `toast` utility functions don't allow
|
||||
* passing this prop.
|
||||
* The `toast` utility functions accept this as a parameter.
|
||||
*/
|
||||
onDismiss,
|
||||
/**
|
||||
* The label of the dismiss button, if available.
|
||||
*/
|
||||
dismissLabel = "Close",
|
||||
dismissable = true,
|
||||
}: ToastComponentProps) => {
|
||||
const hasActionables = !!action || onDismiss
|
||||
let icon = undefined
|
||||
let icon = _icon
|
||||
|
||||
switch (variant) {
|
||||
case "success":
|
||||
icon = <CheckCircleSolid className="text-ui-tag-green-icon" />
|
||||
break
|
||||
case "warning":
|
||||
icon = <ExclamationCircleSolid className="text-ui-tag-orange-icon" />
|
||||
break
|
||||
case "error":
|
||||
icon = <XCircleSolid className="text-ui-tag-red-icon" />
|
||||
break
|
||||
case "loading":
|
||||
icon = <Spinner className="text-ui-tag-blue-icon animate-spin" />
|
||||
break
|
||||
case "info":
|
||||
icon = <InformationCircleSolid className="text-ui-fg-base" />
|
||||
break
|
||||
default:
|
||||
break
|
||||
if (!_icon) {
|
||||
switch (variant) {
|
||||
case "success":
|
||||
icon = <CheckCircleSolid className="text-ui-tag-green-icon" />
|
||||
break
|
||||
case "warning":
|
||||
icon = <ExclamationCircleSolid className="text-ui-tag-orange-icon" />
|
||||
break
|
||||
case "error":
|
||||
icon = <XCircleSolid className="text-ui-tag-red-icon" />
|
||||
break
|
||||
case "loading":
|
||||
icon = <Spinner className="text-ui-tag-blue-icon animate-spin" />
|
||||
break
|
||||
case "info":
|
||||
icon = <InformationCircleSolid className="text-ui-fg-base" />
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="shadow-elevation-flyout bg-ui-bg-base flex w-fit min-w-[360px] max-w-[440px] overflow-hidden rounded-lg">
|
||||
<div className="shadow-elevation-flyout bg-ui-bg-component flex w-fit min-w-[360px] max-w-[440px] gap-x-3 overflow-hidden rounded-lg p-3">
|
||||
<div
|
||||
className={clx("grid flex-1 items-start gap-3 py-3 pl-3", {
|
||||
"border-r pr-3": hasActionables,
|
||||
"pr-6": !hasActionables,
|
||||
className={clx("grid flex-1 items-center gap-x-2", {
|
||||
"grid-cols-[20px_1fr]": !!icon,
|
||||
"grid-cols-1": !icon,
|
||||
"items-start": !!description,
|
||||
})}
|
||||
>
|
||||
{!!icon && (
|
||||
@@ -97,29 +108,26 @@ export const Toast = ({
|
||||
{icon}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="txt-compact-small-plus text-ui-fg-base">
|
||||
{title}
|
||||
</span>
|
||||
<span className="txt-small text-ui-fg-subtle text-pretty">
|
||||
{description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{hasActionables && (
|
||||
<div
|
||||
className={clx("grid grid-cols-1", {
|
||||
"grid-rows-2": !!action && onDismiss,
|
||||
"grid-rows-1": !action || !onDismiss,
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col gap-y-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="txt-compact-small-plus text-ui-fg-base">
|
||||
{title}
|
||||
</span>
|
||||
{description && (
|
||||
<span className="txt-small text-ui-fg-subtle text-pretty">
|
||||
{description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!!action && (
|
||||
<button
|
||||
type="button"
|
||||
className={clx(
|
||||
"txt-compact-small-plus text-ui-fg-base bg-ui-bg-base hover:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed flex items-center justify-center px-4 transition-colors",
|
||||
"txt-compact-small-plus text-ui-fg-base bg-ui-bg-base flex w-fit items-center rounded-[4px] transition-colors",
|
||||
"focus-visible:shadow-borders-focus",
|
||||
"hover:text-ui-fg-subtle",
|
||||
"disabled:text-ui-fg-disabled",
|
||||
{
|
||||
"border-ui-border-base border-b": onDismiss,
|
||||
"text-ui-fg-error": action.variant === "destructive",
|
||||
}
|
||||
)}
|
||||
@@ -128,19 +136,18 @@ export const Toast = ({
|
||||
{action.label}
|
||||
</button>
|
||||
)}
|
||||
{onDismiss && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onDismiss(id)}
|
||||
className={clx(
|
||||
"txt-compact-small-plus text-ui-fg-subtle bg-ui-bg-base hover:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed flex items-center justify-center px-4 transition-colors"
|
||||
)}
|
||||
>
|
||||
{dismissLabel}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!!dismissable && (
|
||||
<IconButton
|
||||
size="2xsmall"
|
||||
variant="transparent"
|
||||
type="button"
|
||||
onClick={() => toast.dismiss(id)}
|
||||
>
|
||||
<XMark />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,45 @@
|
||||
import { Toast } from "@/components/toast"
|
||||
import { ToastAction, ToastVariant, ToasterPosition } from "@/types"
|
||||
import * as React from "react"
|
||||
import { toast as toastFn } from "sonner"
|
||||
import { ExternalToast, toast as toastFn } from "sonner"
|
||||
|
||||
interface BaseToastProps {
|
||||
id?: string | number
|
||||
position?: ToasterPosition
|
||||
dismissable?: boolean
|
||||
dismissLabel?: string
|
||||
duration?: number
|
||||
dismissable?: boolean
|
||||
icon?: React.ReactNode
|
||||
}
|
||||
|
||||
interface ToastProps extends BaseToastProps {
|
||||
description?: string
|
||||
description?: React.ReactNode
|
||||
action?: ToastAction
|
||||
}
|
||||
|
||||
function create(variant: ToastVariant, title: string, props: ToastProps = {}) {
|
||||
return toastFn.custom(
|
||||
(t) => {
|
||||
return (
|
||||
<Toast
|
||||
id={props.id || t}
|
||||
title={title}
|
||||
description={props.description}
|
||||
onDismiss={
|
||||
props.dismissable ? () => toastFn.dismiss(props.id || t) : undefined
|
||||
}
|
||||
dismissLabel={props.dismissLabel}
|
||||
variant={variant}
|
||||
action={props.action}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: props.id,
|
||||
position: props.position,
|
||||
dismissible: props.dismissable,
|
||||
}
|
||||
)
|
||||
function create(variant: ToastVariant, title: React.ReactNode, props: ToastProps = {}) {
|
||||
const external: ExternalToast = {
|
||||
position: props.position,
|
||||
duration: props.duration,
|
||||
dismissible: props.dismissable,
|
||||
}
|
||||
|
||||
if (props.id) {
|
||||
external.id = props.id
|
||||
}
|
||||
|
||||
return toastFn.custom((t) => {
|
||||
return (
|
||||
<Toast
|
||||
id={t}
|
||||
title={title}
|
||||
description={props.description}
|
||||
dismissable={props.dismissable}
|
||||
variant={variant}
|
||||
action={props.action}
|
||||
icon={props.icon}
|
||||
/>
|
||||
)
|
||||
}, external)
|
||||
}
|
||||
|
||||
function message(
|
||||
@@ -54,6 +55,12 @@ function message(
|
||||
return create("message", title, props)
|
||||
}
|
||||
|
||||
function custom() {
|
||||
return create("message", "Custom",)
|
||||
}
|
||||
|
||||
interface VariantToastProps extends Omit<ToastProps, "icon"> {}
|
||||
|
||||
function info(
|
||||
/**
|
||||
* The title of the toast.
|
||||
@@ -61,7 +68,7 @@ function info(
|
||||
/**
|
||||
* The props of the toast.
|
||||
*/
|
||||
props: ToastProps = {}
|
||||
props: VariantToastProps = {}
|
||||
) {
|
||||
return create("info", title, props)
|
||||
}
|
||||
@@ -73,7 +80,7 @@ function error(
|
||||
/**
|
||||
* The props of the toast.
|
||||
*/
|
||||
props: ToastProps = {}
|
||||
props: VariantToastProps = {}
|
||||
) {
|
||||
return create("error", title, props)
|
||||
}
|
||||
@@ -85,7 +92,7 @@ function success(
|
||||
/**
|
||||
* The props of the toast.
|
||||
*/
|
||||
props: ToastProps = {}
|
||||
props: VariantToastProps = { }
|
||||
) {
|
||||
return create("success", title, props)
|
||||
}
|
||||
@@ -97,22 +104,19 @@ function warning(
|
||||
/**
|
||||
* The props of the toast.
|
||||
*/
|
||||
props: ToastProps = {}
|
||||
props: VariantToastProps = {}
|
||||
) {
|
||||
return create("warning", title, props)
|
||||
}
|
||||
|
||||
type LoadingToastProps = Omit<ToastProps, "dismissable" | "dismissLabel">
|
||||
|
||||
function loading
|
||||
/**
|
||||
* The title of the toast.
|
||||
*/(
|
||||
title: string,
|
||||
function loading(
|
||||
/**
|
||||
* The title of the toast.
|
||||
*/ title: string,
|
||||
/**
|
||||
* The props of the toast.
|
||||
*/
|
||||
props: ToastProps = {}
|
||||
props: VariantToastProps = {}
|
||||
) {
|
||||
return create("loading", title, { ...props, dismissable: false })
|
||||
}
|
||||
@@ -124,7 +128,7 @@ type PromiseStateProps =
|
||||
description?: string
|
||||
}
|
||||
|
||||
interface PromiseToastProps extends BaseToastProps {
|
||||
interface PromiseToastProps extends Omit<BaseToastProps, "icon"> {
|
||||
loading: PromiseStateProps
|
||||
success: PromiseStateProps
|
||||
error: PromiseStateProps
|
||||
@@ -158,6 +162,7 @@ async function promise<TData>(
|
||||
? undefined
|
||||
: props.loading.description,
|
||||
duration: Infinity,
|
||||
dismissable: false,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user