"use client"
import type { CSSProperties, ReactNode } from "react"
import { Toast as ToastPrimitive } from "@base-ui/react/toast"
import * as stylex from "@stylexjs/stylex"
import type { StyleXStyles } from "@stylexjs/stylex"
import { clsx } from "clsx"
import { CircleCheck, Info, OctagonX, TriangleAlert, X } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Spinner } from "@/components/ui/spinner"
import { VStack } from "@/components/ui/stack"
import { stateStyles } from "@/lib/state-styles"
import { breakpoints } from "@/theme/breakpoints.stylex"
import { states } from "@/theme/states"
import {
color,
duration,
ease,
layer,
leading,
radius,
shadow,
space,
text,
} from "@/theme/tokens.stylex"
const toast = ToastPrimitive.createToastManager()
const styles = stylex.create({
viewport: {
position: "fixed",
zIndex: layer.popup,
bottom: space.s4,
right: space.s4,
left: { default: space.s4, [breakpoints.sm]: "auto" },
width: { default: "auto", [breakpoints.sm]: "100%" },
marginInline: { default: "auto", [breakpoints.sm]: 0 },
maxWidth: "24rem",
pointerEvents: "none",
outlineStyle: "none",
},
stackOrder: {
zIndex: "calc(1000 - var(--toast-index))",
},
root: {
position: "absolute",
right: 0,
bottom: 0,
width: "100%",
height: "var(--carbon-toast-height)",
transformOrigin: "bottom",
borderRadius: radius.xl,
boxShadow: {
default: shadow.popupLifted,
":focus-visible": `0 0 0 1px ${color.ring}, 0 0 0 3px color-mix(in oklab, ${color.ring} 30%, transparent), ${shadow.popupLifted}`,
},
userSelect: "none",
pointerEvents: "auto",
willChange: "transform",
"--carbon-toast-gap": space.s3,
"--carbon-toast-peek": space.s3,
"--carbon-toast-height": "var(--toast-frontmost-height, var(--toast-height))",
"--carbon-toast-scale": "max(0, 1 - var(--toast-index) * 0.1)",
"--carbon-toast-shrink": "calc(1 - var(--carbon-toast-scale))",
"--carbon-toast-offset-y":
"calc(-1 * var(--toast-offset-y) - var(--toast-index) * var(--carbon-toast-gap) + var(--toast-swipe-movement-y))",
transform:
"translateX(var(--toast-swipe-movement-x)) translateY(calc(var(--toast-swipe-movement-y) - var(--toast-index) * var(--carbon-toast-peek) - var(--carbon-toast-shrink) * var(--carbon-toast-height))) scale(var(--carbon-toast-scale))",
transitionProperty: "transform, opacity, height",
transitionDuration: {
default: `${duration.stack}, ${duration.stack}, 150ms`,
"@media (prefers-reduced-motion: reduce)": "0s",
},
transitionTimingFunction: `${ease.decelerate}, ${ease.standard}, ${ease.standard}`,
"::after": {
content: '""',
position: "absolute",
top: "100%",
left: 0,
width: "100%",
height: "calc(var(--carbon-toast-gap) + 1px)",
},
},
expanded: {
height: "var(--toast-height)",
transform: "translateX(var(--toast-swipe-movement-x)) translateY(var(--carbon-toast-offset-y))",
},
limited: { opacity: 0 },
starting: { transform: "translateY(150%)" },
content: {
display: "flex",
height: "100%",
alignItems: "center",
gap: space.s3,
overflow: "hidden",
padding: space.s4,
transitionProperty: "opacity",
transitionDuration: {
default: duration.stackFade,
"@media (prefers-reduced-motion: reduce)": "0s",
},
transitionTimingFunction: ease.decelerate,
},
behind: { opacity: 0 },
text: { minWidth: 0 },
title: { fontSize: text.sm, lineHeight: leading.sm, fontWeight: 500 },
description: { fontSize: text.sm, lineHeight: leading.sm, color: color.mutedForeground },
action: { flexShrink: 0 },
close: {
flexShrink: 0,
color: { default: color.mutedForeground, ":hover": color.foreground },
},
icon: { display: "flex", flexShrink: 0, "--carbon-icon-size": space.s4 },
iconError: { color: color.destructive },
})
const exits = stylex.create({
none: { transform: "translateY(150%)" },
down: { transform: "translateY(calc(var(--toast-swipe-movement-y) + 150%))" },
up: { transform: "translateY(calc(var(--toast-swipe-movement-y) - 150%))" },
left: {
transform:
"translateX(calc(var(--toast-swipe-movement-x) - 150%)) translateY(var(--carbon-toast-offset-y))",
},
right: {
transform:
"translateX(calc(var(--toast-swipe-movement-x) + 150%)) translateY(var(--carbon-toast-offset-y))",
},
})
type Overrides = { xstyle?: StyleXStyles; className?: string }
type StateOverrides = Overrides & { style?: CSSProperties }
function ToastProvider(props: ToastPrimitive.Provider.Props) {
return <ToastPrimitive.Provider {...props} />
}
function ToastPortal(props: ToastPrimitive.Portal.Props) {
return <ToastPrimitive.Portal data-slot="toast-portal" {...props} />
}
type ToastViewportProps = Omit<ToastPrimitive.Viewport.Props, "className"> & Overrides
function ToastViewport({ xstyle, className, ...props }: ToastViewportProps) {
const sx = stylex.props(styles.viewport, xstyle)
return (
<ToastPrimitive.Viewport
data-slot="toast-viewport"
{...sx}
{...props}
className={clsx(sx.className, className)}
/>
)
}
type ToastProps = Omit<ToastPrimitive.Root.Props, "className" | "style"> & StateOverrides
function Toast({ xstyle, style, className, ...props }: ToastProps) {
return (
<ToastPrimitive.Root
data-slot="toast"
{...props}
{...stateStyles(
(state) => [
states.popupSurface,
styles.root,
styles.stackOrder,
state.expanded && styles.expanded,
state.transitionStatus === "starting" && styles.starting,
state.transitionStatus === "ending" &&
!state.limited &&
exits[state.swipeDirection ?? "none"],
state.limited && styles.limited,
xstyle,
],
{ className, style },
)}
/>
)
}
type ToastContentProps = Omit<ToastPrimitive.Content.Props, "className" | "style"> & StateOverrides
function ToastContent({ xstyle, style, className, ...props }: ToastContentProps) {
return (
<ToastPrimitive.Content
data-slot="toast-content"
{...props}
{...stateStyles(
(state) => [styles.content, state.behind && !state.expanded && styles.behind, xstyle],
{ className, style },
)}
/>
)
}
type ToastTitleProps = Omit<ToastPrimitive.Title.Props, "className"> & Overrides
function ToastTitle({ xstyle, className, ...props }: ToastTitleProps) {
const sx = stylex.props(styles.title, xstyle)
return (
<ToastPrimitive.Title
data-slot="toast-title"
{...sx}
{...props}
className={clsx(sx.className, className)}
/>
)
}
type ToastDescriptionProps = Omit<ToastPrimitive.Description.Props, "className"> & Overrides
function ToastDescription({ xstyle, className, ...props }: ToastDescriptionProps) {
const sx = stylex.props(styles.description, xstyle)
return (
<ToastPrimitive.Description
data-slot="toast-description"
{...sx}
{...props}
className={clsx(sx.className, className)}
/>
)
}
function ToastAction({
render = <Button variant="secondary" size="sm" xstyle={styles.action} />,
...props
}: ToastPrimitive.Action.Props) {
return <ToastPrimitive.Action data-slot="toast-action" render={render} {...props} />
}
function ToastClose({
render = <Button variant="ghost" size="icon-sm" xstyle={styles.close} />,
children,
...props
}: ToastPrimitive.Close.Props) {
return (
<ToastPrimitive.Close data-slot="toast-close" aria-label="Close" render={render} {...props}>
{children ?? <X aria-hidden="true" />}
</ToastPrimitive.Close>
)
}
const icons: Record<string, ReactNode> = {
success: <CircleCheck aria-hidden="true" />,
info: <Info aria-hidden="true" />,
warning: <TriangleAlert aria-hidden="true" />,
error: <OctagonX aria-hidden="true" {...stylex.props(styles.iconError)} />,
loading: <Spinner aria-hidden="true" />,
}
function ToastIcon({ type }: { type: string | undefined }) {
const icon = type ? icons[type] : null
if (!icon) return null
const sx = stylex.props(styles.icon)
return (
<span data-slot="toast-icon" {...sx} className={clsx("carbon-icons", sx.className)}>
{icon}
</span>
)
}
function ToastList() {
const { toasts } = ToastPrimitive.useToastManager()
return toasts.map((item) => (
<Toast key={item.id} toast={item}>
<ToastContent>
<ToastIcon type={item.type} />
<VStack gap={1} grow xstyle={styles.text}>
<ToastTitle />
<ToastDescription />
</VStack>
<ToastAction />
<ToastClose />
</ToastContent>
</Toast>
))
}
function Toaster({ children, toastManager = toast, ...props }: ToastPrimitive.Provider.Props) {
return (
<ToastProvider toastManager={toastManager} {...props}>
{children}
<ToastPortal>
<ToastViewport>
<ToastList />
</ToastViewport>
</ToastPortal>
</ToastProvider>
)
}
const createToastManager = ToastPrimitive.createToastManager
const useToastManager = ToastPrimitive.useToastManager
export {
Toast,
ToastAction,
ToastClose,
ToastContent,
ToastDescription,
ToastIcon,
ToastPortal,
ToastProvider,
ToastTitle,
ToastViewport,
Toaster,
createToastManager,
toast,
useToastManager,
}