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 +199 -0
- package/dist/adapters/sonner.cjs +126 -0
- package/dist/adapters/sonner.cjs.map +1 -0
- package/dist/adapters/sonner.d.cts +6 -0
- package/dist/adapters/sonner.d.ts +6 -0
- package/dist/adapters/sonner.js +121 -0
- package/dist/adapters/sonner.js.map +1 -0
- package/dist/index.cjs +486 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +481 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.cjs +169 -0
- package/dist/internal.cjs.map +1 -0
- package/dist/internal.d.cts +14 -0
- package/dist/internal.d.ts +14 -0
- package/dist/internal.js +166 -0
- package/dist/internal.js.map +1 -0
- package/dist/react.cjs +71 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +35 -0
- package/dist/react.d.ts +35 -0
- package/dist/react.js +61 -0
- package/dist/react.js.map +1 -0
- package/dist/types-CsPmpcbL.d.cts +149 -0
- package/dist/types-CsPmpcbL.d.ts +149 -0
- package/package.json +80 -0
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,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"]}
|