import type { JSX } from "@solidjs/web"; import { type Accessor, createContext, createEffect, useContext, } from "solid-js"; import { type MaybeAccessor, readOption } from "../internals/maybe-accessor"; import { SNAPSHOT_KEYS, type Snapshottable, snapshotKeys, } from "../internals/observability/snapshot"; import type { ToastManager, ToastManagerEvent } from "./create-toast-manager"; import { createToastStore, type ToastStore } from "./toast-store"; import type { ToastManagerAddOptions, ToastManagerPromiseOptions, ToastManagerUpdateOptions, ToastObject, } from "./use-toast-manager"; export type { ToastObject }; /** * Options for {@link createToast}. * * The queue is internally owned — `timeout` / `limit` configure it but the * consumer does not hold signals for them. Unlike `createTabs`/etc the toast * primitive owns internal state because the queue IS the implementation (it * is not a consumer-controlled value; the consumer drives it via `add` / `close` * / `update` imperatives). */ export interface CreateToastOptions { /** * Default auto-dismiss duration in milliseconds. `0` disables auto-dismiss. * @default 5000 */ timeout?: MaybeAccessor; /** * Maximum number of visible toasts. Oldest toasts beyond the limit are * marked `limited` so consumers can animate them out. * @default 3 */ limit?: MaybeAccessor; /** * Optional external manager created via {@link createToastManager}. Events * emitted by the manager are forwarded into this primitive's queue, letting * code outside the component tree add / close / update toasts. */ toastManager?: MaybeAccessor; } /** * Public API returned by {@link createToast}. */ export interface ToastApi { /** Reactive accessor for the current toast queue. */ toasts: Accessor[]>; /** Whether any toast is hovered or focused. */ expanded: Accessor; /** Whether the viewport is hovering. */ hovering: Accessor; /** Whether the viewport is focused. */ focused: Accessor; /** Whether the queue is empty. */ isEmpty: Accessor; /** Add a toast and return its id. */ add: (options: ToastManagerAddOptions) => string; /** Close a toast by id, or all toasts when id is omitted. */ close: (id?: string) => void; /** Update an existing toast in place. */ update: (id: string, options: ToastManagerUpdateOptions) => void; /** Add a promise-backed toast that transitions through loading → success/error. */ promise: ( value: Promise, options: ToastManagerPromiseOptions, ) => Promise; /** * The underlying store. Passed into `` so the * existing wrapper components (`Toast.Provider`, `Toast.Viewport`, etc.) * can compose this primitive. */ readonly store: ToastStore; /** Context provider — pair with {@link useToast}. */ Provider: (props: { children: JSX.Element }) => JSX.Element; } const ToastContext = createContext(null); /** * Read the nearest {@link ToastApi} provided by `c.Provider`. Throws if * called outside a Provider. */ export function useToast(): ToastApi { const ctx = useContext(ToastContext); if (ctx == null) { throw new Error( "Base UI: useToast() must be called inside a .", ); } return ctx; } /** * Solid-native headless toast primitive. Owns the queue, timers, and * lifecycle bookkeeping. The consumer drives the queue via `add` / `close` / * `update` / `promise` imperatives and reads reactive state (`toasts`, * `expanded`, `hovering`, `focused`, `isEmpty`) through accessors. * * Internal state IS appropriate here: the queue is implementation detail * (not a consumer-controlled value). `timeout` / `limit` configure the store * but the consumer never holds the backing signals. * * Callbacks (`onValueChange` etc.) are not part of this surface because there * is no discrete "value" the consumer needs to mirror — the consumer observes * the queue reactively via `toasts()`. The `onClose` / `onRemove` hooks on * individual `ToastObject`s serve per-toast lifecycle needs. */ export function createToast(options: CreateToastOptions = {}): ToastApi { const initialTimeout = readOption(options.timeout) ?? 5000; const initialLimit = readOption(options.limit) ?? 3; const store = createToastStore({ timeout: initialTimeout, limit: initialLimit, }); // Sync timeout/limit into the store when the accessor values change. // Split compute/apply (RFC 02) so reads and writes stay in separate scopes. createEffect( () => readOption(options.timeout) ?? 5000, (next: number) => { store.setTimeoutOption(next); }, ); createEffect( () => readOption(options.limit) ?? 3, (next: number) => { store.setLimitOption(next); }, ); // Forward external manager events into the store. Returns cleanup so the // apply phase can unsubscribe without needing `onCleanup` (the apply runs // in an unowned scope where `onCleanup` would trip NO_OWNER_CLEANUP). createEffect( () => readOption(options.toastManager), (manager: ToastManager | undefined) => { if (!manager) return undefined; const unsubscribe = manager[" subscribe"]((event: ToastManagerEvent) => { const { action, options: opts } = event; const id = opts.id as string | undefined; if ( action === "promise" && opts.promise && "loading" in opts && "success" in opts && "error" in opts ) { const promiseOptions = { loading: opts.loading as ToastManagerPromiseOptions< unknown, object >["loading"], success: opts.success as ToastManagerPromiseOptions< unknown, object >["success"], error: opts.error as ToastManagerPromiseOptions< unknown, object >["error"], setPromise: opts.setPromise, }; store.promiseToast(opts.promise, promiseOptions); } else if (action === "update" && id) { store.updateToast( id, opts as Parameters[1], ); } else if (action === "close") { store.closeToast(id); } else { store.addToast(opts as Parameters[0]); } }); return unsubscribe; }, ); const api: ToastApi = { toasts: store.toasts as Accessor[]>, expanded: store.expanded, hovering: store.hovering, focused: store.focused, isEmpty: store.isEmpty, add(addOptions) { return store.addToast(addOptions); }, close(id) { store.closeToast(id); }, update(id, updateOptions) { store.updateToast(id, updateOptions); }, promise(value, promiseOptions) { return store.promiseToast(value, promiseOptions); }, get store() { return store; }, Provider: (props) => ( {props.children} ), }; (api as ToastApi & Snapshottable)[SNAPSHOT_KEYS] = TOAST_SNAPSHOT_KEYS; return api; } const TOAST_SNAPSHOT_KEYS = snapshotKeys( "toasts", "expanded", "hovering", "focused", "isEmpty", );