From a0adc3806c381afed2af08dde0a62980e7e3065b Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Thu, 30 Apr 2026 00:20:19 +0530 Subject: [PATCH 1/7] feat(toast): add leadingIcon support and align styles with Figma MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `leadingIcon` prop to toastManager.add/update and Toast.createToastManager by lifting it onto Base UI's typed `data` slot via a wrapper. - Render the leading icon before the title; color is driven by toast `type` (success/error/warning/info), the toast container itself stays neutral. - Drop typed background/border/text-color overrides so success/error/info/ warning toasts share the default surface — only the icon color changes. - Tighten content alignment: center title-only toasts, top-align when a description is present. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/content/docs/components/toast/demo.ts | 29 ++++++ .../content/docs/components/toast/index.mdx | 10 +- .../content/docs/components/toast/props.ts | 6 ++ .../components/toast/__tests__/toast.test.tsx | 47 +++++++++ .../components/toast/toast-manager.ts | 95 ++++++++++++++++++- .../components/toast/toast-provider.tsx | 24 ++++- .../raystack/components/toast/toast-root.tsx | 14 ++- .../components/toast/toast.module.css | 86 +++++++++-------- packages/raystack/components/toast/toast.tsx | 3 +- 9 files changed, 268 insertions(+), 46 deletions(-) diff --git a/apps/www/src/content/docs/components/toast/demo.ts b/apps/www/src/content/docs/components/toast/demo.ts index 4d5a06714..35a513140 100644 --- a/apps/www/src/content/docs/components/toast/demo.ts +++ b/apps/www/src/content/docs/components/toast/demo.ts @@ -86,6 +86,35 @@ export const descriptionDemo = { ` }; +export const leadingIconDemo = { + type: 'code', + code: ` + + + + + ` +}; + export const actionDemo = { type: 'code', code: ` diff --git a/apps/www/src/content/docs/components/toast/index.mdx b/apps/www/src/content/docs/components/toast/index.mdx index 17962298f..15f590fb4 100644 --- a/apps/www/src/content/docs/components/toast/index.mdx +++ b/apps/www/src/content/docs/components/toast/index.mdx @@ -4,7 +4,7 @@ description: Displays temporary notification messages using Base UI Toast primit source: packages/raystack/components/toast --- -import { preview, basicDemo, typesDemo, descriptionDemo, actionDemo, promiseDemo, positionDemo, updateDemo } from "./demo.ts"; +import { preview, basicDemo, typesDemo, descriptionDemo, leadingIconDemo, actionDemo, promiseDemo, positionDemo, updateDemo } from "./demo.ts"; @@ -55,7 +55,7 @@ Creates a new toast and returns its unique ID. The `toastManager` can be importe ### Type Variants -Use the `type` prop to change the visual styling of the toast. +Use the `type` prop to drive the leading icon color (e.g. green for success, red for error). The toast container itself stays visually neutral regardless of type. @@ -63,6 +63,12 @@ Use the `type` prop to change the visual styling of the toast. +### With Leading Icon + +Pass any React node via `leadingIcon` to render an icon before the title. The icon inherits color from the toast `type`. + + + ### With Action Button Use `actionProps` to render an action button inside the toast. diff --git a/apps/www/src/content/docs/components/toast/props.ts b/apps/www/src/content/docs/components/toast/props.ts index 14ed93021..7ea80cd51 100644 --- a/apps/www/src/content/docs/components/toast/props.ts +++ b/apps/www/src/content/docs/components/toast/props.ts @@ -69,6 +69,12 @@ export interface ToastManagerAddOptions { */ actionProps?: React.ComponentPropsWithoutRef<'button'>; + /** + * Icon rendered before the toast title. Inherits color from the toast type + * (e.g. green for `type: "success"`). + */ + leadingIcon?: React.ReactNode; + /** * Custom data to attach to the toast. */ diff --git a/packages/raystack/components/toast/__tests__/toast.test.tsx b/packages/raystack/components/toast/__tests__/toast.test.tsx index fe49d1220..83db4efe3 100644 --- a/packages/raystack/components/toast/__tests__/toast.test.tsx +++ b/packages/raystack/components/toast/__tests__/toast.test.tsx @@ -208,6 +208,53 @@ describe('Toast', () => { }); }); + describe('Toast leadingIcon', () => { + beforeEach(() => { + renderWithProvider(); + }); + + it('renders leadingIcon when provided', async () => { + act(() => { + toastManager.add({ + title: 'With icon', + leadingIcon: + }); + }); + + expect(await screen.findByText('With icon')).toBeInTheDocument(); + expect(screen.getByTestId('leading-icon')).toBeInTheDocument(); + }); + + it('does not render leading-icon slot when leadingIcon is omitted', async () => { + act(() => { + toastManager.add({ title: 'No icon' }); + }); + + expect(await screen.findByText('No icon')).toBeInTheDocument(); + expect(screen.queryByTestId('leading-icon')).not.toBeInTheDocument(); + }); + + it('forwards leadingIcon through update()', async () => { + let id: string; + act(() => { + id = toastManager.add({ title: 'Initial' }); + }); + expect(await screen.findByText('Initial')).toBeInTheDocument(); + + act(() => { + toastManager.update(id!, { + title: 'Updated', + leadingIcon: + }); + }); + + await waitFor(() => { + expect(screen.getByText('Updated')).toBeInTheDocument(); + expect(screen.getByTestId('updated-icon')).toBeInTheDocument(); + }); + }); + }); + describe('Multiple toasts', () => { beforeEach(() => { renderWithProvider(); diff --git a/packages/raystack/components/toast/toast-manager.ts b/packages/raystack/components/toast/toast-manager.ts index 42678f1a7..586f8873d 100644 --- a/packages/raystack/components/toast/toast-manager.ts +++ b/packages/raystack/components/toast/toast-manager.ts @@ -1,5 +1,98 @@ 'use client'; import { Toast as ToastPrimitive } from '@base-ui/react'; +import type { ReactNode } from 'react'; -export const toastManager = ToastPrimitive.createToastManager(); +export interface ToastData { + leadingIcon?: ReactNode; +} + +type BaseManager = ReturnType< + typeof ToastPrimitive.createToastManager +>; +type BaseAddOptions = Parameters[0]; +type BaseUpdateOptions = Parameters[1]; + +export type ToastAddOptions = Omit & { + /** + * Icon rendered before the toast title. Inherits color from the toast type + * (e.g. green for `type: "success"`). + */ + leadingIcon?: ReactNode; +}; + +export type ToastUpdateOptions = Omit & { + leadingIcon?: ReactNode; +}; + +export interface ToastPromiseOptions { + loading: string | ToastUpdateOptions; + success: + | string + | ToastUpdateOptions + | ((result: Value) => string | ToastUpdateOptions); + error: + | string + | ToastUpdateOptions + | ((error: unknown) => string | ToastUpdateOptions); +} + +export interface ToastManager { + add: (options: ToastAddOptions) => string; + close: (id?: string) => void; + update: (id: string, options: ToastUpdateOptions) => void; + promise: ( + promise: Promise, + options: ToastPromiseOptions + ) => Promise; +} + +/** + * Internal map from public ToastManager wrappers to the underlying Base UI + * manager. Used by `ToastProvider` to forward the correct instance to + * `ToastPrimitive.Provider`. + */ +export const _baseManagerRef = new WeakMap(); + +function lift(options: O) { + const { leadingIcon, ...rest } = options; + if (leadingIcon === undefined) return rest; + return { ...rest, data: { leadingIcon } }; +} + +function liftPromiseOption( + option: string | O | ((arg: A) => string | O) +) { + if (typeof option === 'string') return option; + if (typeof option === 'function') { + const fn = option as (arg: A) => string | O; + return (arg: A) => { + const result = fn(arg); + return typeof result === 'string' ? result : lift(result); + }; + } + return lift(option); +} + +export function createToastManager(): ToastManager { + const base = ToastPrimitive.createToastManager(); + const wrapper: ToastManager = { + add: options => base.add(lift(options) as BaseAddOptions), + close: id => base.close(id), + update: (id, options) => + base.update(id, lift(options) as BaseUpdateOptions), + promise: (promise, { loading, success, error }) => + base.promise(promise, { + // biome-ignore lint/suspicious/noExplicitAny: Base UI accepts these shapes via union + loading: liftPromiseOption(loading) as any, + // biome-ignore lint/suspicious/noExplicitAny: Base UI accepts these shapes via union + success: liftPromiseOption(success) as any, + // biome-ignore lint/suspicious/noExplicitAny: Base UI accepts these shapes via union + error: liftPromiseOption(error) as any + }) + }; + _baseManagerRef.set(wrapper, base); + return wrapper; +} + +export const toastManager = createToastManager(); diff --git a/packages/raystack/components/toast/toast-provider.tsx b/packages/raystack/components/toast/toast-provider.tsx index 82d4772e5..babfac5a0 100644 --- a/packages/raystack/components/toast/toast-provider.tsx +++ b/packages/raystack/components/toast/toast-provider.tsx @@ -3,7 +3,11 @@ import { Toast as ToastPrimitive } from '@base-ui/react'; import { cx } from 'class-variance-authority'; import styles from './toast.module.css'; -import { toastManager } from './toast-manager'; +import { + _baseManagerRef, + toastManager as defaultToastManager, + type ToastManager +} from './toast-manager'; import { ToastRoot } from './toast-root'; export type ToastPosition = @@ -14,12 +18,19 @@ export type ToastPosition = | 'bottom-center' | 'bottom-right'; -export interface ToastProviderProps extends ToastPrimitive.Provider.Props { +export interface ToastProviderProps + extends Omit { /** * Position of the toast viewport on screen. * @default "bottom-right" */ position?: ToastPosition; + /** + * Toast manager instance. Defaults to the singleton exported as + * `toastManager`. Provide a custom one created via + * `Toast.createToastManager()` to scope toasts to this provider. + */ + toastManager?: ToastManager; } function ToastList({ position }: { position: ToastPosition }) { @@ -31,11 +42,18 @@ function ToastList({ position }: { position: ToastPosition }) { export function ToastProvider({ position = 'bottom-right', + toastManager = defaultToastManager, children, ...props }: ToastProviderProps) { + const baseManager = _baseManagerRef.get(toastManager); + if (!baseManager) { + throw new Error( + 'ToastProvider: invalid toastManager. Use `Toast.createToastManager()` from @raystack/apsara to create one.' + ); + } return ( - + {children} - + + {leadingIcon && ( + + )} {toast.title && ( )} - + {toast.actionProps && ( svg { + width: 100%; + height: 100%; +} + +/* Type-based leading icon colors. The toast itself stays visually neutral + across all types — only the leading icon changes color per type. */ +.root[data-type="success"] .leadingIcon { + color: var(--rs-color-foreground-success-primary); +} + +.root[data-type="error"] .leadingIcon { + color: var(--rs-color-foreground-danger-primary); +} + +.root[data-type="warning"] .leadingIcon { + color: var(--rs-color-foreground-attention-primary); +} + +.root[data-type="info"] .leadingIcon { + color: var(--rs-color-foreground-accent-primary); +} + /* ===== Text container (title + description) ===== */ .textContainer { flex: 1; min-width: 0; } +/* ===== Actions (action button + close) ===== */ +.actions { + flex-shrink: 0; +} + /* ===== Title ===== */ .title { - color: inherit; + color: var(--rs-color-foreground-base-primary); font-size: var(--rs-font-size-regular); font-weight: var(--rs-font-weight-medium); line-height: var(--rs-line-height-regular); @@ -264,7 +284,7 @@ /* ===== Description ===== */ .description { - color: inherit; + color: var(--rs-color-foreground-base-secondary); font-size: var(--rs-font-size-small); font-weight: var(--rs-font-weight-regular); line-height: var(--rs-line-height-small); @@ -272,14 +292,6 @@ margin: 0; } -/* Override description color for typed toasts */ -.root[data-type="success"] .description, -.root[data-type="error"] .description, -.root[data-type="warning"] .description, -.root[data-type="info"] .description { - opacity: 0.85; -} - @media (prefers-reduced-motion: no-preference) { .root { transition: diff --git a/packages/raystack/components/toast/toast.tsx b/packages/raystack/components/toast/toast.tsx index ff22ccafa..c6f55aee7 100644 --- a/packages/raystack/components/toast/toast.tsx +++ b/packages/raystack/components/toast/toast.tsx @@ -1,10 +1,11 @@ import { Toast as ToastPrimitive } from '@base-ui/react'; +import { createToastManager } from './toast-manager'; import { ToastProvider } from './toast-provider'; import { ToastRoot } from './toast-root'; export const Toast = Object.assign(ToastRoot, { Provider: ToastProvider, - createToastManager: ToastPrimitive.createToastManager, + createToastManager, useToastManager: ToastPrimitive.useToastManager }); From de3506afa528aa338fdd3566ff8571b9805a0c61 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Thu, 30 Apr 2026 01:45:23 +0530 Subject: [PATCH 2/7] feat(toast): default leading icons per type Map success/error/warning/info/loading toast types to Radix icons (CheckCircled / CrossCircled / ExclamationTriangle / InfoCircled) and the apsara Spinner for loading. Untyped toasts fall back to InfoCircledIcon with the existing base-secondary color. Explicit `leadingIcon` still wins. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../raystack/components/toast/toast-root.tsx | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/raystack/components/toast/toast-root.tsx b/packages/raystack/components/toast/toast-root.tsx index 06bd72847..ab8b26760 100644 --- a/packages/raystack/components/toast/toast-root.tsx +++ b/packages/raystack/components/toast/toast-root.tsx @@ -1,15 +1,32 @@ 'use client'; import { Toast as ToastPrimitive } from '@base-ui/react'; -import { Cross1Icon } from '@radix-ui/react-icons'; +import { + CheckCircledIcon, + Cross1Icon, + CrossCircledIcon, + ExclamationTriangleIcon, + InfoCircledIcon +} from '@radix-ui/react-icons'; import { cx } from 'class-variance-authority'; +import type { ReactNode } from 'react'; import { Button } from '../button'; import { Flex } from '../flex'; import { IconButton } from '../icon-button'; +import { Spinner } from '../spinner'; import styles from './toast.module.css'; import type { ToastData } from './toast-manager'; import type { ToastPosition } from './toast-provider'; +const TOAST_ICONS: Record = { + default: , + success: , + error: , + warning: , + info: , + loading: +}; + type SwipeDirection = 'up' | 'down' | 'left' | 'right'; function getSwipeDirection(position: ToastPosition): SwipeDirection[] { @@ -40,7 +57,12 @@ export function ToastRoot({ }: ToastRootProps) { const swipeDirection = getSwipeDirection(position); const hasDescription = !!toast.description; - const leadingIcon = (toast.data as ToastData | undefined)?.leadingIcon; + const explicitLeadingIcon = (toast.data as ToastData | undefined) + ?.leadingIcon; + const leadingIcon = + explicitLeadingIcon ?? + (toast.type ? TOAST_ICONS[toast.type] : null) ?? + TOAST_ICONS.default; return ( Date: Thu, 30 Apr 2026 01:53:02 +0530 Subject: [PATCH 3/7] fix(toast): match Figma layout and description color - Description text color: base-secondary -> base-primary (Figma node 3594:25050 specifies foreground-base-primary). - Restructure content into a header row (icon + title + actions) followed by a separate description row indented 24px (rs-space-7), matching the Figma column layout instead of stacking title/desc next to the icon. - Header row gets gap=5 between left and actions, gap=3 between icon and title; min-height = rs-space-7 to keep title-only toasts consistent. - Title now always uses .title styling (was swapping to .description styling for title-only toasts). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../raystack/components/toast/toast-root.tsx | 65 +++++++++---------- .../components/toast/toast.module.css | 40 ++++++++---- 2 files changed, 58 insertions(+), 47 deletions(-) diff --git a/packages/raystack/components/toast/toast-root.tsx b/packages/raystack/components/toast/toast-root.tsx index ab8b26760..4063f56ab 100644 --- a/packages/raystack/components/toast/toast-root.tsx +++ b/packages/raystack/components/toast/toast-root.tsx @@ -72,43 +72,42 @@ export function ToastRoot({ data-position={position} {...props} > - - {leadingIcon && ( - - )} - - {toast.title && ( - +
+
+ {leadingIcon && ( + + )} + {toast.title && ( + + {toast.title} + + )} +
+ + {toast.actionProps && ( + } + /> + )} + } > - {toast.title} - - )} - {hasDescription && ( + + + +
+ {hasDescription && ( +
{toast.description} - )} - - - {toast.actionProps && ( - } - /> - )} - } - > - - - +
+ )}
); diff --git a/packages/raystack/components/toast/toast.module.css b/packages/raystack/components/toast/toast.module.css index 55cb6a152..3c1ecadab 100644 --- a/packages/raystack/components/toast/toast.module.css +++ b/packages/raystack/components/toast/toast.module.css @@ -208,17 +208,11 @@ /* ===== Content (stacking visibility) ===== */ .content { display: flex; - align-items: center; + flex-direction: column; gap: var(--rs-space-3); transition: opacity 250ms; } -/* Top-align icon and actions when description is present so the icon - sits next to the title rather than centering against both lines. */ -.content[data-has-description] { - align-items: flex-start; -} - .content[data-behind] { opacity: 0; } @@ -227,6 +221,30 @@ opacity: 1; } +/* ===== Header row (icon + title + actions) ===== */ +.header { + display: flex; + align-items: center; + gap: var(--rs-space-5); + min-height: var(--rs-space-7); + width: 100%; +} + +.headerLeft { + display: flex; + flex: 1 0 0; + align-items: center; + gap: var(--rs-space-3); + min-width: 0; +} + +/* ===== Description row (indented to align under title text) ===== */ +.descriptionRow { + display: flex; + padding-left: var(--rs-space-7); + width: 100%; +} + /* ===== Leading icon ===== */ .leadingIcon { display: inline-flex; @@ -261,12 +279,6 @@ color: var(--rs-color-foreground-accent-primary); } -/* ===== Text container (title + description) ===== */ -.textContainer { - flex: 1; - min-width: 0; -} - /* ===== Actions (action button + close) ===== */ .actions { flex-shrink: 0; @@ -284,7 +296,7 @@ /* ===== Description ===== */ .description { - color: var(--rs-color-foreground-base-secondary); + color: var(--rs-color-foreground-base-primary); font-size: var(--rs-font-size-small); font-weight: var(--rs-font-weight-regular); line-height: var(--rs-line-height-small); From e0664619368625eb805fbfe455d7ae951c0fca57 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Thu, 30 Apr 2026 01:57:09 +0530 Subject: [PATCH 4/7] fix(toast): shrink title to description style for title-only toasts When a toast has a title but no description, render the title with .description style (12px regular) rather than .title style (14px medium) so the toast doesn't look outsized for a single short message. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/raystack/components/toast/toast-root.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/raystack/components/toast/toast-root.tsx b/packages/raystack/components/toast/toast-root.tsx index 4063f56ab..e84314b42 100644 --- a/packages/raystack/components/toast/toast-root.tsx +++ b/packages/raystack/components/toast/toast-root.tsx @@ -81,7 +81,9 @@ export function ToastRoot({ )} {toast.title && ( - + {toast.title} )} From f1792bf4d1c71bbb8ab4282f22b5e7a5b14f6d44 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Thu, 30 Apr 2026 02:12:15 +0530 Subject: [PATCH 5/7] docs(toast): add interactive playground Adds a playground demo with controls for title, description, type, and an actionButton boolean toggle. Empty title/description strings are omitted from the toastManager.add call so users can test partial configurations. Replaces the static preview Demo at the top of the toast docs page with the playground. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/content/docs/components/toast/demo.ts | 49 +++++++++++++++++++ .../content/docs/components/toast/index.mdx | 4 +- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/apps/www/src/content/docs/components/toast/demo.ts b/apps/www/src/content/docs/components/toast/demo.ts index 35a513140..0dd782652 100644 --- a/apps/www/src/content/docs/components/toast/demo.ts +++ b/apps/www/src/content/docs/components/toast/demo.ts @@ -1,5 +1,54 @@ 'use client'; +export const getCode = ( + _updatedProps: Record, + allProps: Record +) => { + const { title, description, type, actionButton } = allProps; + const opts: string[] = []; + if (title && title !== '') opts.push(`title: "${title}"`); + if (description && description !== '') + opts.push(`description: "${description}"`); + if (type && type !== 'default') opts.push(`type: "${type}"`); + if (actionButton) + opts.push(`actionProps: { children: "Action", onClick: () => {} }`); + + const optsStr = opts.length ? `{ ${opts.join(', ')} }` : '{}'; + + return ` + + + `; +}; + +export const playground = { + type: 'playground', + controls: { + title: { + type: 'text', + initialValue: 'Order placed', + defaultValue: '' + }, + description: { + type: 'text', + initialValue: 'Monday, 7 Oct 2024 at 10:20 AM', + defaultValue: '' + }, + type: { + type: 'select', + options: ['default', 'success', 'error', 'warning', 'info', 'loading'], + defaultValue: 'default' + }, + actionButton: { + type: 'checkbox', + defaultValue: false + } + }, + getCode +}; + export const preview = { type: 'code', code: ` diff --git a/apps/www/src/content/docs/components/toast/index.mdx b/apps/www/src/content/docs/components/toast/index.mdx index 15f590fb4..ebd188808 100644 --- a/apps/www/src/content/docs/components/toast/index.mdx +++ b/apps/www/src/content/docs/components/toast/index.mdx @@ -4,9 +4,9 @@ description: Displays temporary notification messages using Base UI Toast primit source: packages/raystack/components/toast --- -import { preview, basicDemo, typesDemo, descriptionDemo, leadingIconDemo, actionDemo, promiseDemo, positionDemo, updateDemo } from "./demo.ts"; +import { playground, basicDemo, typesDemo, descriptionDemo, leadingIconDemo, actionDemo, promiseDemo, positionDemo, updateDemo } from "./demo.ts"; - + ## Anatomy From 3dd36e5db3e1b422d504ed1e64fdb1f3d5dc83cf Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Tue, 5 May 2026 02:37:58 +0530 Subject: [PATCH 6/7] feat: update toast component --- .../src/content/docs/components/toast/demo.ts | 47 +++++- .../content/docs/components/toast/index.mdx | 65 ++++++-- .../content/docs/components/toast/props.ts | 130 +++++++++++++++- .../components/toast/__tests__/toast.test.tsx | 16 ++ .../components/toast/{index.tsx => index.ts} | 2 +- .../components/toast/toast-manager.ts | 147 +++++++++++------- .../components/toast/toast-provider.tsx | 9 +- .../raystack/components/toast/toast-root.tsx | 95 ++++++----- .../components/toast/toast.module.css | 41 ++--- packages/raystack/components/toast/toast.tsx | 6 +- packages/raystack/index.tsx | 2 +- .../raystack/styles/primitives/z-index.css | 5 +- 12 files changed, 411 insertions(+), 154 deletions(-) rename packages/raystack/components/toast/{index.tsx => index.ts} (66%) diff --git a/apps/www/src/content/docs/components/toast/demo.ts b/apps/www/src/content/docs/components/toast/demo.ts index 0dd782652..f946cdfcf 100644 --- a/apps/www/src/content/docs/components/toast/demo.ts +++ b/apps/www/src/content/docs/components/toast/demo.ts @@ -137,7 +137,10 @@ export const descriptionDemo = { export const leadingIconDemo = { type: 'code', - code: ` + tabs: [ + { + name: 'Custom icons', + code: ` ` + }, + { + name: 'No icon', + code: ` + ` + } + ] }; export const actionDemo = { @@ -315,6 +332,34 @@ export const positionDemo = { ] }; +export const hookDemo = { + type: 'code', + code: ` + function HookDemo() { + // Hook usage lives in an inner component so it runs inside the Provider. + function Inner() { + const { add, toasts } = useToastManager(); + return ( + + + Active toasts: {toasts.length} + + ) + } + return ( + + + + ) + }` +}; + export const updateDemo = { type: 'code', code: ` diff --git a/apps/www/src/content/docs/components/toast/index.mdx b/apps/www/src/content/docs/components/toast/index.mdx index ebd188808..9eae7bc25 100644 --- a/apps/www/src/content/docs/components/toast/index.mdx +++ b/apps/www/src/content/docs/components/toast/index.mdx @@ -4,7 +4,7 @@ description: Displays temporary notification messages using Base UI Toast primit source: packages/raystack/components/toast --- -import { playground, basicDemo, typesDemo, descriptionDemo, leadingIconDemo, actionDemo, promiseDemo, positionDemo, updateDemo } from "./demo.ts"; +import { playground, basicDemo, typesDemo, descriptionDemo, leadingIconDemo, actionDemo, promiseDemo, positionDemo, updateDemo, hookDemo } from "./demo.ts"; @@ -38,14 +38,55 @@ Creates a new toast and returns its unique ID. The `toastManager` can be importe -### toastManager methods +### createToastManager() -| Method | Signature | Description | -|--------|-----------|-------------| -| `add` | `(options) => string` | Create a toast, returns its ID | -| `close` | `(id: string) => void` | Close a specific toast by ID | -| `update` | `(id: string, options) => void` | Update an existing toast's properties | -| `promise` | `(promise, { loading, success, error }) => Promise` | Create a toast that tracks a promise lifecycle | +Factory that returns a fresh toast manager. The exported singleton `toastManager` is one such instance created at module load. Pass a custom manager to `` to scope toasts to that provider. + +```tsx +import { Toast, createToastManager } from '@raystack/apsara' + +const manager = createToastManager() + + + + +``` + +The returned object exposes the four manager methods: + + + +#### Update options + +The `update(id, options)` method accepts a partial of the toast — every field optional, used to patch an existing toast in place. + + + +#### Promise options + +The `promise(promise, options)` method drives a toast through the resolve/reject lifecycle of a Promise. + + + +### useToastManager() + +Hook for dispatching toasts from inside a component tree wrapped by ``. Returns the same methods as `createToastManager()` — including first-class `leadingIcon` support — plus a reactive `toasts` array reflecting the currently-displayed toasts. + +```tsx +import { useToastManager } from '@raystack/apsara' +``` + +The hook is exported as a top-level named export (not as `Toast.useToastManager`) so React's rules-of-hooks linting recognizes it correctly. + + + +#### Toast object + +Each entry in `toasts` is a snapshot of a currently-mounted toast. + + + +Prefer the singleton `toastManager` when triggering toasts from non-React code (interceptors, utility functions, event handlers outside the provider). Use `useToastManager()` inside components when you need access to the live `toasts` array, or want the dispatch call colocated with the rest of the component's logic. ## Examples @@ -65,7 +106,7 @@ Use the `type` prop to drive the leading icon color (e.g. green for success, red ### With Leading Icon -Pass any React node via `leadingIcon` to render an icon before the title. The icon inherits color from the toast `type`. +Each toast `type` ships with a sensible default icon. Pass `leadingIcon` to override it with any React node, or pass `leadingIcon: null` to render no icon at all. @@ -93,6 +134,12 @@ Create a toast, then update or close it programmatically using the returned ID. +### Dispatching from a Component (Hook) + +Use `useToastManager()` inside a component to dispatch toasts and/or read the reactive list of active toasts. The component must be a descendant of ``. + + + ## Accessibility - Uses `aria-live` regions for screen reader announcements diff --git a/apps/www/src/content/docs/components/toast/props.ts b/apps/www/src/content/docs/components/toast/props.ts index 7ea80cd51..bbf4a275a 100644 --- a/apps/www/src/content/docs/components/toast/props.ts +++ b/apps/www/src/content/docs/components/toast/props.ts @@ -71,17 +71,137 @@ export interface ToastManagerAddOptions { /** * Icon rendered before the toast title. Inherits color from the toast type - * (e.g. green for `type: "success"`). + * (e.g. green for `type: "success"`). Omit to use the default icon for the + * toast type, pass any React node to override it, or pass `null` to render + * no icon at all. */ leadingIcon?: React.ReactNode; /** - * Custom data to attach to the toast. + * Optional custom ID for the toast. Auto-generated if not provided. */ - data?: Record; + id?: string; +} +/** + * Options for `update(id, options)`. Same shape as `ToastManagerAddOptions`, + * but every field is optional and the toast `id` moves to the first argument. + */ +export interface ToastManagerUpdateOptions { + title?: React.ReactNode; + description?: React.ReactNode; + type?: 'success' | 'error' | 'info' | 'warning' | 'loading'; + timeout?: number; + priority?: 'low' | 'high'; + onClose?: () => void; + onRemove?: () => void; + actionProps?: React.ComponentPropsWithoutRef<'button'>; + leadingIcon?: React.ReactNode; +} + +/** + * Options passed to `promise(promise, options)`. Each lifecycle stage accepts + * a string (used as the toast description), an options object, or — for + * `success`/`error` — a callable that receives the resolved value or + * rejection reason and returns either of the above. + */ +export interface ToastManagerPromiseOptions { /** - * Optional custom ID for the toast. Auto-generated if not provided. + * Toast shown while the promise is pending. Auto-dismiss is disabled. + * + * @remarks `string | ToastManagerUpdateOptions` */ - id?: string; + loading: string | ToastManagerUpdateOptions; + + /** + * Toast shown when the promise resolves. If a function, it receives the + * resolved value and must return a string or update-options object. + * + * @remarks `string | ToastManagerUpdateOptions | ((result: Value) => string | ToastManagerUpdateOptions)` + */ + success: + | string + | ToastManagerUpdateOptions + | ((result: Value) => string | ToastManagerUpdateOptions); + + /** + * Toast shown when the promise rejects. If a function, it receives the + * rejection reason and must return a string or update-options object. + * + * @remarks `string | ToastManagerUpdateOptions | ((error: unknown) => string | ToastManagerUpdateOptions)` + */ + error: + | string + | ToastManagerUpdateOptions + | ((error: unknown) => string | ToastManagerUpdateOptions); +} + +/** + * Live snapshot of an active toast as exposed via `useToastManager().toasts`. + * Read-only — to mutate, use the manager methods (`add`/`update`/`close`). + */ +export interface ToastObject { + id: string; + title?: React.ReactNode; + description?: React.ReactNode; + type?: 'success' | 'error' | 'info' | 'warning' | 'loading'; + timeout?: number; + priority?: 'low' | 'high'; +} + +/** + * The toast manager returned by `createToastManager()` and exported as the + * `toastManager` singleton. + */ +export interface CreateToastManagerReturn { + /** + * Create a new toast and return its ID. The ID can later be passed to + * `update` or `close`. + * + * @remarks `(options: ToastManagerAddOptions) => string` + */ + add: (options: ToastManagerAddOptions) => string; + + /** + * Close a specific toast by ID, or close all visible toasts if no ID is + * given. + * + * @remarks `(id?: string) => void` + */ + close: (id?: string) => void; + + /** + * Update an existing toast in place. Typically used to swap a loading + * toast to a success or error state without dismissing it. + * + * @remarks `(id: string, options: ToastManagerUpdateOptions) => void` + */ + update: (id: string, options: ToastManagerUpdateOptions) => void; + + /** + * Tie a toast's lifecycle to a promise. Renders the `loading` toast + * immediately, then transitions to `success` or `error` when the promise + * settles. Returns the original promise so callers can still `await` it. + * + * @remarks `(promise: Promise, options: ToastManagerPromiseOptions) => Promise` + */ + promise: ( + promise: Promise, + options: ToastManagerPromiseOptions + ) => Promise; +} + +/** + * Return value of the `useToastManager()` hook. Same methods as + * `createToastManager()` plus a reactive `toasts` array driving component + * re-renders. + */ +export interface UseToastManagerReturn extends CreateToastManagerReturn { + /** + * Reactive array of currently-displayed toasts. The hook re-renders the + * caller whenever this list changes. + * + * @remarks `ToastObject[]` + */ + toasts: ToastObject[]; } diff --git a/packages/raystack/components/toast/__tests__/toast.test.tsx b/packages/raystack/components/toast/__tests__/toast.test.tsx index 83db4efe3..290889ed5 100644 --- a/packages/raystack/components/toast/__tests__/toast.test.tsx +++ b/packages/raystack/components/toast/__tests__/toast.test.tsx @@ -234,6 +234,22 @@ describe('Toast', () => { expect(screen.queryByTestId('leading-icon')).not.toBeInTheDocument(); }); + it('renders no icon when leadingIcon is explicitly null, even with a type default', async () => { + act(() => { + toastManager.add({ + title: 'No icon success', + type: 'success', + leadingIcon: null + }); + }); + + const toastEl = await screen.findByText('No icon success'); + const root = toastEl.closest('[data-type="success"]'); + expect(root).toBeInTheDocument(); + // The leading-icon wrapper (the only aria-hidden span in the toast) should not render. + expect(root!.querySelector('[aria-hidden="true"]')).toBeNull(); + }); + it('forwards leadingIcon through update()', async () => { let id: string; act(() => { diff --git a/packages/raystack/components/toast/index.tsx b/packages/raystack/components/toast/index.ts similarity index 66% rename from packages/raystack/components/toast/index.tsx rename to packages/raystack/components/toast/index.ts index e1dea40a3..bcb815d4e 100644 --- a/packages/raystack/components/toast/index.tsx +++ b/packages/raystack/components/toast/index.ts @@ -1,3 +1,3 @@ -export { Toast, toastManager } from './toast'; +export { Toast, toastManager, useToastManager } from './toast'; export type { ToastPosition, ToastProviderProps } from './toast-provider'; export type { ToastRootProps } from './toast-root'; diff --git a/packages/raystack/components/toast/toast-manager.ts b/packages/raystack/components/toast/toast-manager.ts index 586f8873d..55918c37a 100644 --- a/packages/raystack/components/toast/toast-manager.ts +++ b/packages/raystack/components/toast/toast-manager.ts @@ -1,29 +1,34 @@ 'use client'; import { Toast as ToastPrimitive } from '@base-ui/react'; -import type { ReactNode } from 'react'; +import { type ReactNode, useMemo } from 'react'; export interface ToastData { + /** + * Icon rendered before the toast title. Inherits color from the toast type + * (e.g. green for `type: "success"`). + * + * - Omit (or pass `undefined`) to use the default icon for the toast type. + * - Pass any React node to override the icon. + * - Pass `null` to render no icon at all (opt out of type defaults). + */ leadingIcon?: ReactNode; } type BaseManager = ReturnType< typeof ToastPrimitive.createToastManager >; -type BaseAddOptions = Parameters[0]; -type BaseUpdateOptions = Parameters[1]; -export type ToastAddOptions = Omit & { - /** - * Icon rendered before the toast title. Inherits color from the toast type - * (e.g. green for `type: "success"`). - */ - leadingIcon?: ReactNode; -}; +/** Public option shape: hides Base UI's internal storage slot and exposes `leadingIcon` directly. */ +type WithModifiedOptions = Omit & ToastData; -export type ToastUpdateOptions = Omit & { - leadingIcon?: ReactNode; -}; +export type ToastAddOptions = WithModifiedOptions< + Parameters[0] +>; + +export type ToastUpdateOptions = WithModifiedOptions< + Parameters[1] +>; export interface ToastPromiseOptions { loading: string | ToastUpdateOptions; @@ -37,62 +42,94 @@ export interface ToastPromiseOptions { | ((error: unknown) => string | ToastUpdateOptions); } -export interface ToastManager { - add: (options: ToastAddOptions) => string; - close: (id?: string) => void; - update: (id: string, options: ToastUpdateOptions) => void; - promise: ( - promise: Promise, - options: ToastPromiseOptions - ) => Promise; -} - -/** - * Internal map from public ToastManager wrappers to the underlying Base UI - * manager. Used by `ToastProvider` to forward the correct instance to - * `ToastPrimitive.Provider`. - */ -export const _baseManagerRef = new WeakMap(); - function lift(options: O) { const { leadingIcon, ...rest } = options; if (leadingIcon === undefined) return rest; return { ...rest, data: { leadingIcon } }; } -function liftPromiseOption( - option: string | O | ((arg: A) => string | O) +function liftDescriptor(option: string | ToastUpdateOptions) { + return typeof option === 'string' ? option : lift(option); +} + +function liftCallable( + option: + | string + | ToastUpdateOptions + | ((arg: Arg) => string | ToastUpdateOptions) ) { - if (typeof option === 'string') return option; if (typeof option === 'function') { - const fn = option as (arg: A) => string | O; - return (arg: A) => { - const result = fn(arg); - return typeof result === 'string' ? result : lift(result); - }; + return (arg: Arg) => liftDescriptor(option(arg)); } - return lift(option); + return liftDescriptor(option); +} + +function liftPromiseOptions({ + loading, + success, + error +}: ToastPromiseOptions) { + return { + loading: liftDescriptor(loading), + success: liftCallable(success), + error: liftCallable(error) + }; +} + +/** + * Public toast manager. Mirrors the Base UI manager but exposes `leadingIcon` + * as a first-class option on `add`/`update`/`promise`. All other Base UI + * manager members are preserved. + */ +export interface ToastManager + extends Omit { + add: (options: ToastAddOptions) => string; + update: (id: string, options: ToastUpdateOptions) => void; + promise: ( + promise: Promise, + options: ToastPromiseOptions + ) => Promise; } export function createToastManager(): ToastManager { const base = ToastPrimitive.createToastManager(); - const wrapper: ToastManager = { - add: options => base.add(lift(options) as BaseAddOptions), - close: id => base.close(id), - update: (id, options) => - base.update(id, lift(options) as BaseUpdateOptions), - promise: (promise, { loading, success, error }) => - base.promise(promise, { - // biome-ignore lint/suspicious/noExplicitAny: Base UI accepts these shapes via union - loading: liftPromiseOption(loading) as any, - // biome-ignore lint/suspicious/noExplicitAny: Base UI accepts these shapes via union - success: liftPromiseOption(success) as any, - // biome-ignore lint/suspicious/noExplicitAny: Base UI accepts these shapes via union - error: liftPromiseOption(error) as any - }) + return { + ...base, + add: options => base.add(lift(options)), + update: (id, options) => base.update(id, lift(options)), + promise: (promise, options) => + base.promise(promise, liftPromiseOptions(options)) }; - _baseManagerRef.set(wrapper, base); - return wrapper; } export const toastManager = createToastManager(); + +type BaseHookReturn = ReturnType< + typeof ToastPrimitive.useToastManager +>; + +/** + * Reactive view of the active toast list plus the same `leadingIcon`-aware + * `add`/`update`/`promise` API as the standalone manager. Must be used inside + * ``. + */ +export interface UseToastManagerReturn + extends Omit { + add: ToastManager['add']; + update: ToastManager['update']; + promise: ToastManager['promise']; +} + +export function useToastManager(): UseToastManagerReturn { + const base = ToastPrimitive.useToastManager(); + return useMemo( + () => ({ + ...base, + add: options => base.add(lift(options)), + update: (id, options) => base.update(id, lift(options)), + promise: (promise, options) => + base.promise(promise, liftPromiseOptions(options)) + }), + [base] + ); +} diff --git a/packages/raystack/components/toast/toast-provider.tsx b/packages/raystack/components/toast/toast-provider.tsx index babfac5a0..792bc7949 100644 --- a/packages/raystack/components/toast/toast-provider.tsx +++ b/packages/raystack/components/toast/toast-provider.tsx @@ -4,7 +4,6 @@ import { Toast as ToastPrimitive } from '@base-ui/react'; import { cx } from 'class-variance-authority'; import styles from './toast.module.css'; import { - _baseManagerRef, toastManager as defaultToastManager, type ToastManager } from './toast-manager'; @@ -46,14 +45,8 @@ export function ToastProvider({ children, ...props }: ToastProviderProps) { - const baseManager = _baseManagerRef.get(toastManager); - if (!baseManager) { - throw new Error( - 'ToastProvider: invalid toastManager. Use `Toast.createToastManager()` from @raystack/apsara to create one.' - ); - } return ( - + {children} -
-
- {leadingIcon && ( - - )} - {toast.title && ( - - {toast.title} - - )} -
- - {toast.actionProps && ( - } - /> - )} - } + + {leadingIcon && ( + + )} + + - - + {title && ( + + {title} + + )} + + {toast.actionProps && ( + + } + /> + )} + } + > + + + + + {hasBoth && ( + + {toast.description} + + )} -
- {hasDescription && ( -
- - {toast.description} - -
- )} +
); diff --git a/packages/raystack/components/toast/toast.module.css b/packages/raystack/components/toast/toast.module.css index 3c1ecadab..9c2e160cb 100644 --- a/packages/raystack/components/toast/toast.module.css +++ b/packages/raystack/components/toast/toast.module.css @@ -2,7 +2,7 @@ .viewport { --gap: 0.75rem; position: fixed; - z-index: var(--rs-z-index-portal); + z-index: var(--rs-z-index-toast); width: 360px; max-width: calc(100vw - var(--rs-space-10)); outline: none; @@ -207,9 +207,6 @@ /* ===== Content (stacking visibility) ===== */ .content { - display: flex; - flex-direction: column; - gap: var(--rs-space-3); transition: opacity 250ms; } @@ -221,44 +218,34 @@ opacity: 1; } -/* ===== Header row (icon + title + actions) ===== */ -.header { - display: flex; - align-items: center; - gap: var(--rs-space-5); - min-height: var(--rs-space-7); - width: 100%; -} - -.headerLeft { - display: flex; - flex: 1 0 0; - align-items: center; - gap: var(--rs-space-3); +/* ===== Content column (claims remaining space next to the icon) ===== */ +.contentColumn { + flex: 1; min-width: 0; } -/* ===== Description row (indented to align under title text) ===== */ -.descriptionRow { - display: flex; - padding-left: var(--rs-space-7); - width: 100%; +/* ===== Top row (headline + actions) ===== */ +.topRow { + min-height: var(--rs-space-7); } -/* ===== Leading icon ===== */ +/* ===== Leading icon ===== + The wrapper height matches the top row's min-height so the SVG (centered + inside via align/justify) lines up with the headline's vertical center, + even though the wrapper itself is top-aligned in the outer flex. */ .leadingIcon { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: var(--rs-space-5); - height: var(--rs-space-5); + min-height: var(--rs-space-7); color: var(--rs-color-foreground-base-secondary); } .leadingIcon > svg { - width: 100%; - height: 100%; + width: var(--rs-space-5); + height: var(--rs-space-5); } /* Type-based leading icon colors. The toast itself stays visually neutral diff --git a/packages/raystack/components/toast/toast.tsx b/packages/raystack/components/toast/toast.tsx index c6f55aee7..e2fa29be3 100644 --- a/packages/raystack/components/toast/toast.tsx +++ b/packages/raystack/components/toast/toast.tsx @@ -1,12 +1,10 @@ -import { Toast as ToastPrimitive } from '@base-ui/react'; import { createToastManager } from './toast-manager'; import { ToastProvider } from './toast-provider'; import { ToastRoot } from './toast-root'; export const Toast = Object.assign(ToastRoot, { Provider: ToastProvider, - createToastManager, - useToastManager: ToastPrimitive.useToastManager + createToastManager }); -export { toastManager } from './toast-manager'; +export { toastManager, useToastManager } from './toast-manager'; diff --git a/packages/raystack/index.tsx b/packages/raystack/index.tsx index cd849b3b3..49fe90899 100644 --- a/packages/raystack/index.tsx +++ b/packages/raystack/index.tsx @@ -77,7 +77,7 @@ export { ThemeSwitcher, useTheme } from './components/theme-provider'; -export { Toast, toastManager } from './components/toast'; +export { Toast, toastManager, useToastManager } from './components/toast'; export { Toggle } from './components/toggle'; export { Toolbar } from './components/toolbar'; export { Tooltip } from './components/tooltip'; diff --git a/packages/raystack/styles/primitives/z-index.css b/packages/raystack/styles/primitives/z-index.css index ea6381391..f13365e10 100644 --- a/packages/raystack/styles/primitives/z-index.css +++ b/packages/raystack/styles/primitives/z-index.css @@ -1,3 +1,4 @@ :root { - --rs-z-index-portal: 99; -} \ No newline at end of file + --rs-z-index-portal: 99; + --rs-z-index-toast: 999; +} From 75d81032d0e62e3c47c5f87e33b95c68b92785bf Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Tue, 5 May 2026 02:45:12 +0530 Subject: [PATCH 7/7] chore: update migration doc --- apps/www/src/app/examples/combobox/page.tsx | 167 ++++++++++++++++++-- docs/V1-migration.md | 108 ++++++++++--- 2 files changed, 240 insertions(+), 35 deletions(-) diff --git a/apps/www/src/app/examples/combobox/page.tsx b/apps/www/src/app/examples/combobox/page.tsx index da4352c17..c3cee46d3 100644 --- a/apps/www/src/app/examples/combobox/page.tsx +++ b/apps/www/src/app/examples/combobox/page.tsx @@ -1,20 +1,161 @@ 'use client'; -import { Combobox, Flex, Slider } from '@raystack/apsara'; -const Page = () => { +import { + Button, + Combobox, + Dialog, + Flex, + Toast, + toastManager, + useToastManager +} from '@raystack/apsara'; + +const FRUITS = ['Apple', 'Banana', 'Blueberry', 'Grapes', 'Pineapple']; + +function ToastButton({ + label, + source, + type +}: { + label: string; + source: string; + type?: 'success' | 'info' | 'warning'; +}) { + // Demonstrates the hook flavor — works because every button is a + // descendant of in the tree below. + const { add } = useToastManager(); return ( - + add({ + title: `Toast from ${source}`, + description: 'Toasts portal to the top-level provider.', + type + }) + } > - - + {label} + + ); +} + +const Page = () => { + return ( + + +

Combobox + nested dialogs + toast

+

+ Toasts triggered from any depth of nested dialog still render at the + root viewport. Each level has its own combobox and toast button. +

+ + + + + {FRUITS.map(f => ( + + {f} + + ))} + + + + + {/* Singleton flavor — usable from anywhere, including non-React code. */} + + + + }> + Open dialog 1 + + + + Dialog 1 + + Triggers a toast and opens a nested dialog. + + + + + + + + {FRUITS.map(f => ( + + {f} + + ))} + + + + + + + }> + Open dialog 2 + + + + Dialog 2 (nested) + + A toast fired from here still appears at the root + viewport — even though this dialog is portaled. + + + + + + + + + }> + Close + + + + + + + + + }> + Close + + + + + +
+
); }; diff --git a/docs/V1-migration.md b/docs/V1-migration.md index 40996ba86..37500b4a3 100644 --- a/docs/V1-migration.md +++ b/docs/V1-migration.md @@ -1720,7 +1720,9 @@ Unchanged props: `disabled`, `placeholder`, `width`, `value`, `onChange`, `rows` ### Toast -**Exports renamed: `ToastContainer`/`toast` -> `Toast`/`toastManager`** +The library moved from Sonner to Base UI Toast primitives. The flat `toast()` function with chained shortcuts (`toast.success`, `toast.error`, `toast.dismiss`, …) is replaced by a single `toastManager.add(options)` entrypoint, and the standalone `` becomes a context-bearing `` that wraps your tree. + +**Exports renamed: `ToastContainer` / `toast` -> `Toast.Provider` / `toastManager`** ```tsx // ===== BEFORE (Sonner-based) ===== @@ -1731,7 +1733,7 @@ function App() { return ( <> - + ); } @@ -1740,6 +1742,7 @@ function App() { toast('Simple message'); toast.success('Operation complete'); toast.error('Something went wrong'); +toast.message('File deleted', { description: 'You can undo this action' }); toast('File deleted', { duration: 5000, action: { label: 'Undo', onClick: handleUndo }, @@ -1751,55 +1754,116 @@ toast.dismiss(); // dismiss all // ===== AFTER (Base UI-based) ===== import { Toast, toastManager } from '@raystack/apsara'; -// Provider — must wrap app as a context provider +// Provider — wraps the app and supplies context (required for useToastManager) function App() { return ( - + ); } -// Creating toasts +// Creating toasts — single entrypoint, `type` drives styling and default icon toastManager.add({ title: 'Simple message' }); toastManager.add({ title: 'Operation complete', type: 'success' }); toastManager.add({ title: 'Something went wrong', type: 'error' }); +toastManager.add({ title: 'File deleted', description: 'You can undo this action' }); toastManager.add({ title: 'File deleted', + timeout: 5000, actionProps: { children: 'Undo', onClick: handleUndo }, }); -const id = toastManager.add({ title: 'Uploading...' }); +const id = toastManager.add({ title: 'Uploading...', type: 'loading' }); toastManager.close(id); -// no built-in dismiss-all +toastManager.close(); // close all ``` -**Callbacks changed:** +**Provider prop mapping (`` -> ``):** + +| Before | After | Notes | +| --- | --- | --- | +| `duration` | `timeout` | Default auto-dismiss in ms; overridable per toast. | +| `visibleToasts` | `limit` | Max visible toasts (default 3). | +| `position` | `position` | Same accepted values. | +| `theme`, `richColors`, `closeButton`, `expand`, `gap`, `offset` | — | Removed. Theme follows the app's theme provider, the close button is always shown, expand-on-hover is always on. | + +**Toast option mapping (`toast(msg, options)` -> `toastManager.add(options)`):** + +| Before | After | Notes | +| --- | --- | --- | +| First-arg message | `title` | Now part of the options object. | +| `description` | `description` | If `title` is omitted, `description` is promoted into the headline slot so the layout stays single-line. | +| `duration` | `timeout` | `0` disables auto-dismiss (was `Infinity` in sonner). | +| `action: { label, onClick }` | `actionProps: { children, onClick, ...buttonProps }` | Accepts any `