gracefulerrors 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,199 @@
1
+ # gracefulerrors
2
+
3
+ gracefulerrors is a TypeScript library for turning technical errors into consistent user-facing experiences. It provides a framework-agnostic core engine, a React SDK, and a Sonner adapter.
4
+
5
+ ## Features
6
+
7
+ - Normalize raw errors into a shared internal format
8
+ - Route errors to `toast`, `modal`, `inline`, or `silent`
9
+ - Register typed error codes with per-code UI behavior
10
+ - Dedupe, queue, and limit concurrent notifications
11
+ - Integrate with React through a provider, hooks, and an error boundary
12
+ - Render with Sonner or a custom adapter
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install gracefulerrors
18
+ ```
19
+
20
+ If you use the React SDK or the Sonner adapter, install the matching peer dependencies in your app:
21
+
22
+ ```bash
23
+ npm install react react-dom sonner
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```ts
29
+ import { createErrorEngine } from 'gracefulerrors'
30
+
31
+ type AppCode = 'AUTH_FAILED' | 'PAYMENT_FAILED'
32
+
33
+ const engine = createErrorEngine<AppCode>({
34
+ registry: {
35
+ AUTH_FAILED: {
36
+ ui: 'toast',
37
+ uiOptions: {
38
+ severity: 'error',
39
+ },
40
+ message: 'Your session expired. Please sign in again.',
41
+ },
42
+ PAYMENT_FAILED: {
43
+ ui: 'modal',
44
+ message: 'Payment could not be processed.',
45
+ },
46
+ },
47
+ fallback: {
48
+ ui: 'toast',
49
+ message: 'Something went wrong.',
50
+ },
51
+ })
52
+
53
+ engine.handle({
54
+ code: 'AUTH_FAILED',
55
+ message: '401 Unauthorized',
56
+ })
57
+ ```
58
+
59
+ ## Fetch Wrapper
60
+
61
+ `createFetch` wraps the native `fetch` and forwards failures to the engine.
62
+
63
+ ```ts
64
+ import { createErrorEngine, createFetch } from 'gracefulerrors'
65
+
66
+ const engine = createErrorEngine({
67
+ registry: {
68
+ NETWORK_ERROR: { ui: 'toast', message: 'Network error' },
69
+ },
70
+ })
71
+
72
+ const apiFetch = createFetch(engine, { mode: 'handle' })
73
+
74
+ const response = await apiFetch('/api/profile')
75
+ ```
76
+
77
+ Supported modes:
78
+
79
+ - `throw` returns the original failure after notifying the engine
80
+ - `handle` swallows handled failures and returns `undefined`
81
+ - `silent` leaves non-OK responses and thrown errors to the caller without notifying the engine
82
+
83
+ ## React Integration
84
+
85
+ ```tsx
86
+ import { ErrorEngineProvider, useErrorEngine } from 'gracefulerrors/react'
87
+ import { createErrorEngine } from 'gracefulerrors'
88
+
89
+ const engine = createErrorEngine({
90
+ registry: {
91
+ NETWORK_ERROR: { ui: 'toast', message: 'Network error' },
92
+ },
93
+ })
94
+
95
+ function SaveButton() {
96
+ const errorEngine = useErrorEngine()
97
+
98
+ return (
99
+ <button
100
+ onClick={() => {
101
+ errorEngine?.handle({ code: 'NETWORK_ERROR', message: 'Request failed' })
102
+ }}
103
+ >
104
+ Save
105
+ </button>
106
+ )
107
+ }
108
+
109
+ export function App() {
110
+ return (
111
+ <ErrorEngineProvider engine={engine}>
112
+ <SaveButton />
113
+ </ErrorEngineProvider>
114
+ )
115
+ }
116
+ ```
117
+
118
+ `gracefulerrors/react` also exports:
119
+
120
+ - `useFieldError(field)` for inline error state
121
+ - `ErrorBoundaryWithEngine` for catching runtime errors and forwarding them to the engine
122
+
123
+ ## Sonner Adapter
124
+
125
+ ```tsx
126
+ import { SonnerToaster, createSonnerAdapter } from 'gracefulerrors/sonner'
127
+ import { createErrorEngine } from 'gracefulerrors'
128
+
129
+ const engine = createErrorEngine({
130
+ registry: {
131
+ SERVER_ERROR: {
132
+ ui: 'toast',
133
+ severity: 'error',
134
+ message: 'Server error. Please try again.',
135
+ },
136
+ },
137
+ renderer: createSonnerAdapter(),
138
+ })
139
+
140
+ export function Root() {
141
+ return (
142
+ <>
143
+ <SonnerToaster />
144
+ {/* your app */}
145
+ </>
146
+ )
147
+ }
148
+ ```
149
+
150
+ ## Public API
151
+
152
+ Main entry:
153
+
154
+ - `createErrorEngine`
155
+ - `createFetch`
156
+ - `mergeRegistries`
157
+ - `builtInNormalizer`
158
+
159
+ Type exports are available from the root package as well.
160
+
161
+ Additional entry points:
162
+
163
+ - `gracefulerrors/react`
164
+ - `gracefulerrors/sonner`
165
+ - `gracefulerrors/internal` for internal testing and low-level integration only
166
+
167
+ ## Configuration Highlights
168
+
169
+ Common engine options include:
170
+
171
+ - `registry`: typed error-to-UI mapping
172
+ - `fallback`: default UI choice when no registry entry matches
173
+ - `normalizer` / `normalizers`: custom normalization pipeline
174
+ - `fingerprint`: dedupe key strategy
175
+ - `dedupeWindow`: deduplication window in milliseconds
176
+ - `maxConcurrent` and `maxQueue`: notification throughput control
177
+ - `aggregation`: group bursts of the same UI type
178
+ - `routingStrategy`: dynamic override before registry resolution
179
+ - `transform`: post-normalization shaping or suppression
180
+ - `onError`, `onNormalized`, `onRouted`, `onFallback`, `onSuppressed`, `onDropped`: lifecycle hooks
181
+ - `renderer`: custom rendering adapter
182
+
183
+ ## Package Notes
184
+
185
+ - Package format: ESM and CJS via conditional exports
186
+ - Runtime peers: `react`, `react-dom`, and `sonner` are optional peer dependencies
187
+ - Current version: `0.1.0`
188
+
189
+ ## Development
190
+
191
+ ```bash
192
+ npm run build
193
+ npm run lint
194
+ npm run typecheck
195
+ ```
196
+
197
+ ## License
198
+
199
+ MIT
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ var sonner = require('sonner');
4
+ var client = require('react-dom/client');
5
+ var react = require('react');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ // src/adapters/sonner.tsx
9
+ function resolveMessage(entry, error) {
10
+ if (typeof entry.message === "function") return entry.message(error);
11
+ return entry.message;
12
+ }
13
+ function ModalDialog({
14
+ message,
15
+ dismissible,
16
+ onDismiss
17
+ }) {
18
+ react.useEffect(() => {
19
+ const handleKeyDown = (e) => {
20
+ if (e.key === "Escape") onDismiss();
21
+ };
22
+ document.addEventListener("keydown", handleKeyDown);
23
+ return () => document.removeEventListener("keydown", handleKeyDown);
24
+ }, [onDismiss]);
25
+ return /* @__PURE__ */ jsxRuntime.jsx(
26
+ "div",
27
+ {
28
+ style: {
29
+ position: "fixed",
30
+ inset: 0,
31
+ backgroundColor: "rgba(0,0,0,0.5)",
32
+ display: "flex",
33
+ alignItems: "center",
34
+ justifyContent: "center",
35
+ zIndex: 9999
36
+ },
37
+ onClick: dismissible ? onDismiss : void 0,
38
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
39
+ "div",
40
+ {
41
+ style: {
42
+ background: "white",
43
+ borderRadius: 8,
44
+ padding: 24,
45
+ minWidth: 300,
46
+ maxWidth: 500
47
+ },
48
+ onClick: (e) => e.stopPropagation(),
49
+ children: [
50
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "0 0 16px" }, children: message }),
51
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onDismiss, children: "Dismiss" })
52
+ ]
53
+ }
54
+ )
55
+ }
56
+ );
57
+ }
58
+ function createSonnerAdapter() {
59
+ const activeToastIds = /* @__PURE__ */ new Map();
60
+ function render(intent, lifecycle) {
61
+ switch (intent.ui) {
62
+ case "toast": {
63
+ const message = resolveMessage(intent.entry, intent.error) ?? intent.error.message ?? "An error occurred";
64
+ const opts = intent.entry.uiOptions ?? {};
65
+ const severity = opts.severity ?? "error";
66
+ const severityMap = {
67
+ error: sonner.toast.error,
68
+ warning: sonner.toast.warning,
69
+ info: sonner.toast.info,
70
+ success: sonner.toast.success
71
+ };
72
+ const toastFn = severityMap[severity] ?? sonner.toast.error;
73
+ const id = toastFn(message, {
74
+ position: opts.position ?? "top-right",
75
+ icon: opts.icon,
76
+ duration: opts.duration ?? 4e3
77
+ });
78
+ activeToastIds.set(intent.error.code, id);
79
+ break;
80
+ }
81
+ case "modal": {
82
+ const message = resolveMessage(intent.entry, intent.error) ?? intent.error.message ?? "An error occurred";
83
+ const opts = intent.entry.uiOptions ?? {};
84
+ const dismissible = opts.dismissible !== false;
85
+ const container = document.createElement("div");
86
+ document.body.appendChild(container);
87
+ const root = client.createRoot(container);
88
+ const dismiss = () => {
89
+ root.unmount();
90
+ if (document.body.contains(container)) {
91
+ document.body.removeChild(container);
92
+ }
93
+ lifecycle.onDismiss?.();
94
+ };
95
+ root.render(
96
+ /* @__PURE__ */ jsxRuntime.jsx(ModalDialog, { message, dismissible, onDismiss: dismiss })
97
+ );
98
+ break;
99
+ }
100
+ case "inline":
101
+ return;
102
+ case "silent":
103
+ return;
104
+ }
105
+ }
106
+ function clear(code) {
107
+ const id = activeToastIds.get(code);
108
+ if (id !== void 0) {
109
+ sonner.toast.dismiss(id);
110
+ activeToastIds.delete(code);
111
+ }
112
+ }
113
+ function clearAll() {
114
+ sonner.toast.dismiss();
115
+ activeToastIds.clear();
116
+ }
117
+ return { render, clear, clearAll };
118
+ }
119
+
120
+ Object.defineProperty(exports, "SonnerToaster", {
121
+ enumerable: true,
122
+ get: function () { return sonner.Toaster; }
123
+ });
124
+ exports.createSonnerAdapter = createSonnerAdapter;
125
+ //# sourceMappingURL=sonner.cjs.map
126
+ //# sourceMappingURL=sonner.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/sonner.tsx"],"names":["useEffect","jsx","jsxs","toast","createRoot"],"mappings":"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","file":"sonner.cjs","sourcesContent":["import { toast, Toaster } from 'sonner'\nimport { createRoot } from 'react-dom/client'\nimport { useEffect } from 'react'\nimport type { RendererAdapter, RenderIntent, ErrorRegistryEntryFull, AppError } from '../types'\n\nfunction resolveMessage<TCode extends string = string>(entry: ErrorRegistryEntryFull<TCode>, error: AppError<TCode>): string | undefined {\n if (typeof entry.message === 'function') return entry.message(error)\n return entry.message\n}\n\n// ---------------------------------------------------------------------------\n// Modal component\n// ---------------------------------------------------------------------------\n\nfunction ModalDialog({\n message,\n dismissible,\n onDismiss,\n}: {\n message: string\n dismissible: boolean\n onDismiss: () => void\n}) {\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') onDismiss()\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => document.removeEventListener('keydown', handleKeyDown)\n }, [onDismiss])\n\n return (\n <div\n style={{\n position: 'fixed',\n inset: 0,\n backgroundColor: 'rgba(0,0,0,0.5)',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n zIndex: 9999,\n }}\n onClick={dismissible ? onDismiss : undefined}\n >\n <div\n style={{\n background: 'white',\n borderRadius: 8,\n padding: 24,\n minWidth: 300,\n maxWidth: 500,\n }}\n onClick={e => e.stopPropagation()}\n >\n <p style={{ margin: '0 0 16px' }}>{message}</p>\n <button onClick={onDismiss}>Dismiss</button>\n </div>\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// createSonnerAdapter\n// ---------------------------------------------------------------------------\n\nexport function createSonnerAdapter(): RendererAdapter {\n const activeToastIds = new Map<string, string | number>()\n\n function render<TCode extends string = string>(intent: RenderIntent<TCode>, lifecycle: { onDismiss?: () => void }): void {\n switch (intent.ui) {\n case 'toast': {\n const message =\n resolveMessage(intent.entry, intent.error) ??\n intent.error.message ??\n 'An error occurred'\n\n type ToastOpts = {\n position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center'\n severity?: 'info' | 'warning' | 'error' | 'success'\n icon?: string\n duration?: number\n }\n const opts = (intent.entry.uiOptions ?? {}) as ToastOpts\n const severity = opts.severity ?? 'error'\n\n const severityMap: Record<string, typeof toast.error> = {\n error: toast.error,\n warning: toast.warning,\n info: toast.info,\n success: toast.success,\n }\n const toastFn = severityMap[severity] ?? toast.error\n\n const id = toastFn(message, {\n position: opts.position ?? 'top-right',\n icon: opts.icon,\n duration: opts.duration ?? 4000,\n })\n\n activeToastIds.set(intent.error.code as string, id)\n break\n }\n\n case 'modal': {\n const message =\n resolveMessage(intent.entry, intent.error) ??\n intent.error.message ??\n 'An error occurred'\n const opts = intent.entry.uiOptions ?? {}\n const dismissible = (opts as { dismissible?: boolean }).dismissible !== false\n\n const container = document.createElement('div')\n document.body.appendChild(container)\n const root = createRoot(container)\n\n const dismiss = () => {\n root.unmount()\n if (document.body.contains(container)) {\n document.body.removeChild(container)\n }\n lifecycle.onDismiss?.()\n }\n\n root.render(\n <ModalDialog message={message} dismissible={dismissible} onDismiss={dismiss} />\n )\n break\n }\n\n case 'inline':\n return\n\n case 'silent':\n return\n }\n }\n\n function clear(code: string): void {\n const id = activeToastIds.get(code)\n if (id !== undefined) {\n toast.dismiss(id)\n activeToastIds.delete(code)\n }\n }\n\n function clearAll(): void {\n toast.dismiss()\n activeToastIds.clear()\n }\n\n return { render, clear, clearAll }\n}\n\nexport { Toaster as SonnerToaster }\n"]}
@@ -0,0 +1,6 @@
1
+ export { Toaster as SonnerToaster } from 'sonner';
2
+ import { e as RendererAdapter } from '../types-CsPmpcbL.cjs';
3
+
4
+ declare function createSonnerAdapter(): RendererAdapter;
5
+
6
+ export { createSonnerAdapter };
@@ -0,0 +1,6 @@
1
+ export { Toaster as SonnerToaster } from 'sonner';
2
+ import { e as RendererAdapter } from '../types-CsPmpcbL.js';
3
+
4
+ declare function createSonnerAdapter(): RendererAdapter;
5
+
6
+ export { createSonnerAdapter };
@@ -0,0 +1,121 @@
1
+ import { toast } from 'sonner';
2
+ export { Toaster as SonnerToaster } from 'sonner';
3
+ import { createRoot } from 'react-dom/client';
4
+ import { useEffect } from 'react';
5
+ import { jsx, jsxs } from 'react/jsx-runtime';
6
+
7
+ // src/adapters/sonner.tsx
8
+ function resolveMessage(entry, error) {
9
+ if (typeof entry.message === "function") return entry.message(error);
10
+ return entry.message;
11
+ }
12
+ function ModalDialog({
13
+ message,
14
+ dismissible,
15
+ onDismiss
16
+ }) {
17
+ useEffect(() => {
18
+ const handleKeyDown = (e) => {
19
+ if (e.key === "Escape") onDismiss();
20
+ };
21
+ document.addEventListener("keydown", handleKeyDown);
22
+ return () => document.removeEventListener("keydown", handleKeyDown);
23
+ }, [onDismiss]);
24
+ return /* @__PURE__ */ jsx(
25
+ "div",
26
+ {
27
+ style: {
28
+ position: "fixed",
29
+ inset: 0,
30
+ backgroundColor: "rgba(0,0,0,0.5)",
31
+ display: "flex",
32
+ alignItems: "center",
33
+ justifyContent: "center",
34
+ zIndex: 9999
35
+ },
36
+ onClick: dismissible ? onDismiss : void 0,
37
+ children: /* @__PURE__ */ jsxs(
38
+ "div",
39
+ {
40
+ style: {
41
+ background: "white",
42
+ borderRadius: 8,
43
+ padding: 24,
44
+ minWidth: 300,
45
+ maxWidth: 500
46
+ },
47
+ onClick: (e) => e.stopPropagation(),
48
+ children: [
49
+ /* @__PURE__ */ jsx("p", { style: { margin: "0 0 16px" }, children: message }),
50
+ /* @__PURE__ */ jsx("button", { onClick: onDismiss, children: "Dismiss" })
51
+ ]
52
+ }
53
+ )
54
+ }
55
+ );
56
+ }
57
+ function createSonnerAdapter() {
58
+ const activeToastIds = /* @__PURE__ */ new Map();
59
+ function render(intent, lifecycle) {
60
+ switch (intent.ui) {
61
+ case "toast": {
62
+ const message = resolveMessage(intent.entry, intent.error) ?? intent.error.message ?? "An error occurred";
63
+ const opts = intent.entry.uiOptions ?? {};
64
+ const severity = opts.severity ?? "error";
65
+ const severityMap = {
66
+ error: toast.error,
67
+ warning: toast.warning,
68
+ info: toast.info,
69
+ success: toast.success
70
+ };
71
+ const toastFn = severityMap[severity] ?? toast.error;
72
+ const id = toastFn(message, {
73
+ position: opts.position ?? "top-right",
74
+ icon: opts.icon,
75
+ duration: opts.duration ?? 4e3
76
+ });
77
+ activeToastIds.set(intent.error.code, id);
78
+ break;
79
+ }
80
+ case "modal": {
81
+ const message = resolveMessage(intent.entry, intent.error) ?? intent.error.message ?? "An error occurred";
82
+ const opts = intent.entry.uiOptions ?? {};
83
+ const dismissible = opts.dismissible !== false;
84
+ const container = document.createElement("div");
85
+ document.body.appendChild(container);
86
+ const root = createRoot(container);
87
+ const dismiss = () => {
88
+ root.unmount();
89
+ if (document.body.contains(container)) {
90
+ document.body.removeChild(container);
91
+ }
92
+ lifecycle.onDismiss?.();
93
+ };
94
+ root.render(
95
+ /* @__PURE__ */ jsx(ModalDialog, { message, dismissible, onDismiss: dismiss })
96
+ );
97
+ break;
98
+ }
99
+ case "inline":
100
+ return;
101
+ case "silent":
102
+ return;
103
+ }
104
+ }
105
+ function clear(code) {
106
+ const id = activeToastIds.get(code);
107
+ if (id !== void 0) {
108
+ toast.dismiss(id);
109
+ activeToastIds.delete(code);
110
+ }
111
+ }
112
+ function clearAll() {
113
+ toast.dismiss();
114
+ activeToastIds.clear();
115
+ }
116
+ return { render, clear, clearAll };
117
+ }
118
+
119
+ export { createSonnerAdapter };
120
+ //# sourceMappingURL=sonner.js.map
121
+ //# sourceMappingURL=sonner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/sonner.tsx"],"names":[],"mappings":"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","file":"sonner.js","sourcesContent":["import { toast, Toaster } from 'sonner'\nimport { createRoot } from 'react-dom/client'\nimport { useEffect } from 'react'\nimport type { RendererAdapter, RenderIntent, ErrorRegistryEntryFull, AppError } from '../types'\n\nfunction resolveMessage<TCode extends string = string>(entry: ErrorRegistryEntryFull<TCode>, error: AppError<TCode>): string | undefined {\n if (typeof entry.message === 'function') return entry.message(error)\n return entry.message\n}\n\n// ---------------------------------------------------------------------------\n// Modal component\n// ---------------------------------------------------------------------------\n\nfunction ModalDialog({\n message,\n dismissible,\n onDismiss,\n}: {\n message: string\n dismissible: boolean\n onDismiss: () => void\n}) {\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') onDismiss()\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => document.removeEventListener('keydown', handleKeyDown)\n }, [onDismiss])\n\n return (\n <div\n style={{\n position: 'fixed',\n inset: 0,\n backgroundColor: 'rgba(0,0,0,0.5)',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n zIndex: 9999,\n }}\n onClick={dismissible ? onDismiss : undefined}\n >\n <div\n style={{\n background: 'white',\n borderRadius: 8,\n padding: 24,\n minWidth: 300,\n maxWidth: 500,\n }}\n onClick={e => e.stopPropagation()}\n >\n <p style={{ margin: '0 0 16px' }}>{message}</p>\n <button onClick={onDismiss}>Dismiss</button>\n </div>\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// createSonnerAdapter\n// ---------------------------------------------------------------------------\n\nexport function createSonnerAdapter(): RendererAdapter {\n const activeToastIds = new Map<string, string | number>()\n\n function render<TCode extends string = string>(intent: RenderIntent<TCode>, lifecycle: { onDismiss?: () => void }): void {\n switch (intent.ui) {\n case 'toast': {\n const message =\n resolveMessage(intent.entry, intent.error) ??\n intent.error.message ??\n 'An error occurred'\n\n type ToastOpts = {\n position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center'\n severity?: 'info' | 'warning' | 'error' | 'success'\n icon?: string\n duration?: number\n }\n const opts = (intent.entry.uiOptions ?? {}) as ToastOpts\n const severity = opts.severity ?? 'error'\n\n const severityMap: Record<string, typeof toast.error> = {\n error: toast.error,\n warning: toast.warning,\n info: toast.info,\n success: toast.success,\n }\n const toastFn = severityMap[severity] ?? toast.error\n\n const id = toastFn(message, {\n position: opts.position ?? 'top-right',\n icon: opts.icon,\n duration: opts.duration ?? 4000,\n })\n\n activeToastIds.set(intent.error.code as string, id)\n break\n }\n\n case 'modal': {\n const message =\n resolveMessage(intent.entry, intent.error) ??\n intent.error.message ??\n 'An error occurred'\n const opts = intent.entry.uiOptions ?? {}\n const dismissible = (opts as { dismissible?: boolean }).dismissible !== false\n\n const container = document.createElement('div')\n document.body.appendChild(container)\n const root = createRoot(container)\n\n const dismiss = () => {\n root.unmount()\n if (document.body.contains(container)) {\n document.body.removeChild(container)\n }\n lifecycle.onDismiss?.()\n }\n\n root.render(\n <ModalDialog message={message} dismissible={dismissible} onDismiss={dismiss} />\n )\n break\n }\n\n case 'inline':\n return\n\n case 'silent':\n return\n }\n }\n\n function clear(code: string): void {\n const id = activeToastIds.get(code)\n if (id !== undefined) {\n toast.dismiss(id)\n activeToastIds.delete(code)\n }\n }\n\n function clearAll(): void {\n toast.dismiss()\n activeToastIds.clear()\n }\n\n return { render, clear, clearAll }\n}\n\nexport { Toaster as SonnerToaster }\n"]}