From 21c02e0d287805ec9bfc246d3da77c7ae789b547 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 8 Oct 2026 19:46:28 +0000 Subject: [PATCH 1/4] feat: add Toast component Co-authored-by: Brandon McConnell --- packages/components/src/components/index.ts | 1 + .../components/src/components/toast/index.ts | 2 + .../src/components/toast/toast.stories.tsx | 111 ++++++++++++ .../components/src/components/toast/toast.tsx | 165 ++++++++++++++++++ .../components/src/constants/selectors.ts | 1 + packages/components/src/icons/index.tsx | 42 +++++ 6 files changed, 322 insertions(+) create mode 100644 packages/components/src/components/toast/index.ts create mode 100644 packages/components/src/components/toast/toast.stories.tsx create mode 100644 packages/components/src/components/toast/toast.tsx diff --git a/packages/components/src/components/index.ts b/packages/components/src/components/index.ts index 755742f6..b6b93bc6 100644 --- a/packages/components/src/components/index.ts +++ b/packages/components/src/components/index.ts @@ -16,6 +16,7 @@ export * from "./search"; export * from "./steps"; export * from "./tabs"; export * from "./tile"; +export * from "./toast"; export * from "./tooltip"; export * from "./tree"; export * from "./update"; diff --git a/packages/components/src/components/toast/index.ts b/packages/components/src/components/toast/index.ts new file mode 100644 index 00000000..3d1110b1 --- /dev/null +++ b/packages/components/src/components/toast/index.ts @@ -0,0 +1,2 @@ +export type { ToastAction, ToastProps, ToastVariant } from "./toast"; +export { Toast } from "./toast"; diff --git a/packages/components/src/components/toast/toast.stories.tsx b/packages/components/src/components/toast/toast.stories.tsx new file mode 100644 index 00000000..94c47466 --- /dev/null +++ b/packages/components/src/components/toast/toast.stories.tsx @@ -0,0 +1,111 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Toast } from "./toast"; + +const meta: Meta = { + title: "Components/Toast", + component: Toast, + parameters: { + layout: "padded", + }, + tags: ["autodocs"], + argTypes: { + variant: { + control: "select", + options: ["info", "warning", "note", "tip", "check", "danger"], + description: "Predefined toast variant", + }, + action: { + control: "object", + description: + "Optional call to action. Pressing it runs onClick and closes the toast", + }, + dismissible: { + control: "boolean", + description: "Show a close button that dismisses the toast", + }, + onDismiss: { + action: "dismissed", + description: "Called when the close button is pressed", + }, + dismissLabel: { + control: "text", + description: "Accessible label for the close button", + }, + className: { + control: "text", + description: "Additional CSS classes for the toast", + }, + }, +}; + +export default meta; +type Story = StoryObj; + +const action = { label: "Continue", onClick: () => undefined }; + +export const Warning: Story = { + args: { + variant: "warning", + children: "Your unsaved changes will be discarded.", + action, + }, +}; + +export const AllVariants: Story = { + render: () => ( +
+ {(["info", "warning", "note", "tip", "check", "danger"] as const).map( + (variant) => ( + + A toast with the {variant} variant. + + ) + )} +
+ ), +}; + +export const LongText: Story = { + args: { + variant: "warning", + children: + "Long messages wrap beside the icon while the action and close button stay centered on the first line of text, even when the message runs onto several lines.", + action, + }, +}; + +export const Narrow: Story = { + render: () => ( +
+ + In a narrow container, the action and close button move below the + message. + +
+ ), +}; + +export const EdgeToEdge: Story = { + args: { + variant: "warning", + children: "Remove the rounding to run the toast edge to edge.", + action, + className: "rounded-none", + }, +}; + +export const NotDismissible: Story = { + args: { + variant: "warning", + children: "Only the action can close this toast.", + action, + dismissible: false, + }, +}; + +export const DismissOnly: Story = { + args: { + variant: "info", + children: "A toast without an action.", + }, +}; diff --git a/packages/components/src/components/toast/toast.tsx b/packages/components/src/components/toast/toast.tsx new file mode 100644 index 00000000..bc2c16d0 --- /dev/null +++ b/packages/components/src/components/toast/toast.tsx @@ -0,0 +1,165 @@ +import { cn } from "cn"; +import { type ReactNode, useState } from "react"; +import { Classes } from "@/constants/selectors"; +import { + CheckIcon, + DangerIcon, + InfoIcon, + NoteIcon, + TipIcon, + WarningFilledIcon, + XIcon, +} from "@/icons"; + +type ToastVariant = "info" | "warning" | "note" | "tip" | "check" | "danger"; + +type ToastAction = { + label: string; + onClick: () => void; +}; + +type ToastProps = { + children: ReactNode; + variant?: ToastVariant; + action?: ToastAction; + dismissible?: boolean; + onDismiss?: () => void; + dismissLabel?: string; + className?: string; +}; + +const variantConfig = { + info: { + icon: InfoIcon, + className: "bg-stone-600/20", + iconClassName: "text-stone-700 dark:text-stone-300", + }, + warning: { + icon: WarningFilledIcon, + className: "bg-orange-600/20", + iconClassName: "text-orange-700 dark:text-orange-300", + }, + note: { + icon: NoteIcon, + className: "bg-blue-600/20", + iconClassName: "text-blue-700 dark:text-blue-300", + }, + tip: { + icon: TipIcon, + className: "bg-green-600/20", + iconClassName: "text-green-700 dark:text-green-300", + }, + check: { + icon: CheckIcon, + className: "bg-green-600/20", + iconClassName: "text-green-700 dark:text-green-300", + }, + danger: { + icon: DangerIcon, + className: "bg-red-600/20", + iconClassName: "text-red-700 dark:text-red-300", + }, +}; + +const FOCUS_RING_CLASSNAME = + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary dark:focus-visible:ring-primary-light"; + +const LineCenter = ({ + className, + children, +}: { + className?: string; + children: ReactNode; +}) => ( +
+ {children} +
+); + +const Toast = ({ + children, + variant = "info", + action, + dismissible = true, + onDismiss, + dismissLabel = "Dismiss", + className, +}: ToastProps) => { + const [open, setOpen] = useState(true); + + if (!open) { + return null; + } + + const config = variantConfig[variant]; + const IconComponent = config.icon; + + return ( +
+ + +
+
+ {children} +
+ {(action || dismissible) && ( + + {action && ( + + )} + {dismissible && ( + + )} + + )} +
+
+ ); +}; + +export { Toast }; +export type { ToastAction, ToastProps, ToastVariant }; diff --git a/packages/components/src/constants/selectors.ts b/packages/components/src/constants/selectors.ts index 49989598..c11a876c 100644 --- a/packages/components/src/constants/selectors.ts +++ b/packages/components/src/constants/selectors.ts @@ -18,6 +18,7 @@ const _classes = { Tabs: "tabs", TabIcon: "tab-icon", Update: "update", + Toast: "toast", Tooltip: "tooltip", Panel: "panel", APISection: "api-section", diff --git a/packages/components/src/icons/index.tsx b/packages/components/src/icons/index.tsx index 326b7dfe..cb91fc6f 100644 --- a/packages/components/src/icons/index.tsx +++ b/packages/components/src/icons/index.tsx @@ -332,6 +332,46 @@ const CopyButtonIcon = ({ ); }; +const WarningFilledIcon = ({ + className, + ...props +}: SVGProps) => { + return ( + + ); +}; + +const XIcon = ({ className, ...props }: SVGProps) => { + return ( + + ); +}; + export { ActiveCopyButtonIcon, CopyButtonIcon, @@ -346,4 +386,6 @@ export { TipIcon, DangerIcon, WarningIcon, + WarningFilledIcon, + XIcon, }; From 1edaa20626872f6bce8fb98d0df0c891736f2bc8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 8 Oct 2026 20:19:46 +0000 Subject: [PATCH 2/4] fix(toast): match the design's sizes, icons, and variants Co-authored-by: Brandon McConnell --- .../src/components/toast/toast.stories.tsx | 14 +++--- .../components/src/components/toast/toast.tsx | 46 +++++++------------ packages/components/src/icons/index.tsx | 41 +++++++++++------ 3 files changed, 48 insertions(+), 53 deletions(-) diff --git a/packages/components/src/components/toast/toast.stories.tsx b/packages/components/src/components/toast/toast.stories.tsx index 94c47466..936a36ad 100644 --- a/packages/components/src/components/toast/toast.stories.tsx +++ b/packages/components/src/components/toast/toast.stories.tsx @@ -11,7 +11,7 @@ const meta: Meta = { argTypes: { variant: { control: "select", - options: ["info", "warning", "note", "tip", "check", "danger"], + options: ["info", "check", "warning", "danger"], description: "Predefined toast variant", }, action: { @@ -54,13 +54,11 @@ export const Warning: Story = { export const AllVariants: Story = { render: () => (
- {(["info", "warning", "note", "tip", "check", "danger"] as const).map( - (variant) => ( - - A toast with the {variant} variant. - - ) - )} + {(["info", "check", "warning", "danger"] as const).map((variant) => ( + + A toast with the {variant} variant. + + ))}
), }; diff --git a/packages/components/src/components/toast/toast.tsx b/packages/components/src/components/toast/toast.tsx index bc2c16d0..8ec24a64 100644 --- a/packages/components/src/components/toast/toast.tsx +++ b/packages/components/src/components/toast/toast.tsx @@ -2,16 +2,13 @@ import { cn } from "cn"; import { type ReactNode, useState } from "react"; import { Classes } from "@/constants/selectors"; import { - CheckIcon, - DangerIcon, - InfoIcon, - NoteIcon, - TipIcon, - WarningFilledIcon, + CircleCheckFilledIcon, + CircleWarningFilledIcon, + TriangleWarningFilledIcon, XIcon, } from "@/icons"; -type ToastVariant = "info" | "warning" | "note" | "tip" | "check" | "danger"; +type ToastVariant = "info" | "check" | "warning" | "danger"; type ToastAction = { label: string; @@ -30,32 +27,22 @@ type ToastProps = { const variantConfig = { info: { - icon: InfoIcon, - className: "bg-stone-600/20", - iconClassName: "text-stone-700 dark:text-stone-300", - }, - warning: { - icon: WarningFilledIcon, - className: "bg-orange-600/20", - iconClassName: "text-orange-700 dark:text-orange-300", - }, - note: { - icon: NoteIcon, + icon: CircleWarningFilledIcon, className: "bg-blue-600/20", iconClassName: "text-blue-700 dark:text-blue-300", }, - tip: { - icon: TipIcon, - className: "bg-green-600/20", - iconClassName: "text-green-700 dark:text-green-300", - }, check: { - icon: CheckIcon, + icon: CircleCheckFilledIcon, className: "bg-green-600/20", iconClassName: "text-green-700 dark:text-green-300", }, + warning: { + icon: TriangleWarningFilledIcon, + className: "bg-orange-600/20", + iconClassName: "text-orange-700 dark:text-orange-300", + }, danger: { - icon: DangerIcon, + icon: CircleWarningFilledIcon, className: "bg-red-600/20", iconClassName: "text-red-700 dark:text-red-300", }, @@ -98,7 +85,7 @@ const Toast = ({
@@ -124,7 +110,7 @@ const Toast = ({ {action && (