@monetr/notify 1.0.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.
@@ -0,0 +1,343 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { DRAG_DAMPEN_MAX, DRAG_DISMISS_DISTANCE, DRAG_VELOCITY_THRESHOLD, DRAG_VELOCITY_WINDOW_MS, EXIT_TRANSITION_MS, SWIPE_EXIT_MS } from "./constants.js";
4
+ import { dampenValue } from "./helpers.js";
5
+ const initialDrag = {
6
+ startX: 0,
7
+ startY: 0,
8
+ currentX: 0,
9
+ currentY: 0,
10
+ dragging: false,
11
+ sampleAt: 0,
12
+ sampleX: 0,
13
+ sampleY: 0
14
+ };
15
+ function isDismissAllowed(anchor, axis, sign) {
16
+ if ('y' === axis) return sign === ('top' === anchor.vertical ? -1 : 1);
17
+ if ('left' === anchor.horizontal) return -1 === sign;
18
+ if ('right' === anchor.horizontal) return 1 === sign;
19
+ return true;
20
+ }
21
+ const PIP_VARIANTS = [
22
+ 'success',
23
+ 'error',
24
+ 'warning',
25
+ 'info'
26
+ ];
27
+ function Notification(props) {
28
+ const { instance, index, stackHovered, iconNode, dispatch } = props;
29
+ const { key, anchorOrigin, autoHideDuration, disableWindowBlurListener, action, variant, message, state, swipeDirection } = instance;
30
+ const elementRef = useRef(null);
31
+ const dragRef = useRef({
32
+ ...initialDrag
33
+ });
34
+ const elapsedRef = useRef(0);
35
+ const lastResumeRef = useRef(null);
36
+ const swipeAxisRef = useRef(null);
37
+ const onCloseRef = useRef(instance.onClose);
38
+ useEffect(()=>{
39
+ onCloseRef.current = instance.onClose;
40
+ });
41
+ const [dragOffset, setDragOffset] = useState({
42
+ x: 0,
43
+ y: 0
44
+ });
45
+ const [windowBlurred, setWindowBlurred] = useState(false);
46
+ useEffect(()=>{
47
+ if ('entering' !== state) return;
48
+ let raf2 = 0;
49
+ const raf1 = requestAnimationFrame(()=>{
50
+ raf2 = requestAnimationFrame(()=>{
51
+ dispatch({
52
+ type: 'set-state',
53
+ key,
54
+ state: 'visible'
55
+ });
56
+ });
57
+ });
58
+ return ()=>{
59
+ cancelAnimationFrame(raf1);
60
+ cancelAnimationFrame(raf2);
61
+ };
62
+ }, [
63
+ state,
64
+ key,
65
+ dispatch
66
+ ]);
67
+ useEffect(()=>{
68
+ if (disableWindowBlurListener) return;
69
+ const onBlur = ()=>setWindowBlurred(true);
70
+ const onFocus = ()=>setWindowBlurred(false);
71
+ window.addEventListener('blur', onBlur);
72
+ window.addEventListener('focus', onFocus);
73
+ return ()=>{
74
+ window.removeEventListener('blur', onBlur);
75
+ window.removeEventListener('focus', onFocus);
76
+ };
77
+ }, [
78
+ disableWindowBlurListener
79
+ ]);
80
+ const effectivelyPaused = 'paused' === state || stackHovered || windowBlurred;
81
+ useEffect(()=>{
82
+ if ('visible' !== state || null === autoHideDuration || effectivelyPaused) {
83
+ if (null !== lastResumeRef.current) {
84
+ elapsedRef.current += Date.now() - lastResumeRef.current;
85
+ lastResumeRef.current = null;
86
+ }
87
+ return;
88
+ }
89
+ lastResumeRef.current = Date.now();
90
+ const remaining = Math.max(0, autoHideDuration - elapsedRef.current);
91
+ const timeoutId = window.setTimeout(()=>{
92
+ onCloseRef.current?.(null, 'timeout', key);
93
+ dispatch({
94
+ type: 'dismiss',
95
+ key
96
+ });
97
+ }, remaining);
98
+ return ()=>{
99
+ window.clearTimeout(timeoutId);
100
+ if (null !== lastResumeRef.current) {
101
+ elapsedRef.current += Date.now() - lastResumeRef.current;
102
+ lastResumeRef.current = null;
103
+ }
104
+ };
105
+ }, [
106
+ state,
107
+ autoHideDuration,
108
+ effectivelyPaused,
109
+ key,
110
+ dispatch
111
+ ]);
112
+ useEffect(()=>{
113
+ if ('visible' !== state || null === autoHideDuration || effectivelyPaused) return;
114
+ let raf = 0;
115
+ const tick = ()=>{
116
+ const el = elementRef.current;
117
+ if (!el) return;
118
+ const live = null !== lastResumeRef.current ? Date.now() - lastResumeRef.current : 0;
119
+ const total = elapsedRef.current + live;
120
+ const progress = Math.min(1, total / autoHideDuration);
121
+ el.style.setProperty('--monetr-notification-progress', String(progress));
122
+ if (progress < 1 && null !== lastResumeRef.current) raf = requestAnimationFrame(tick);
123
+ };
124
+ raf = requestAnimationFrame(tick);
125
+ return ()=>cancelAnimationFrame(raf);
126
+ }, [
127
+ state,
128
+ autoHideDuration,
129
+ effectivelyPaused
130
+ ]);
131
+ useEffect(()=>{
132
+ if ('exiting' !== state && 'swiped-out' !== state) return;
133
+ const fallback = window.setTimeout(()=>dispatch({
134
+ type: 'remove',
135
+ key
136
+ }), 'swiped-out' === state ? SWIPE_EXIT_MS + 100 : EXIT_TRANSITION_MS + 100);
137
+ return ()=>window.clearTimeout(fallback);
138
+ }, [
139
+ state,
140
+ key,
141
+ dispatch
142
+ ]);
143
+ const handlePointerDown = useCallback((event)=>{
144
+ if (0 !== index) return;
145
+ if ('visible' !== state && 'entering' !== state && 'paused' !== state) return;
146
+ const target = event.target;
147
+ if (target?.closest('button, a, input, textarea, select, [role="button"]')) return;
148
+ const now = performance.now();
149
+ dragRef.current = {
150
+ startX: event.clientX,
151
+ startY: event.clientY,
152
+ currentX: event.clientX,
153
+ currentY: event.clientY,
154
+ dragging: true,
155
+ sampleAt: now,
156
+ sampleX: event.clientX,
157
+ sampleY: event.clientY
158
+ };
159
+ try {
160
+ event.currentTarget.setPointerCapture(event.pointerId);
161
+ } catch {}
162
+ }, [
163
+ index,
164
+ state
165
+ ]);
166
+ const handlePointerMove = useCallback((event)=>{
167
+ const drag = dragRef.current;
168
+ if (!drag.dragging) return;
169
+ drag.currentX = event.clientX;
170
+ drag.currentY = event.clientY;
171
+ const now = performance.now();
172
+ if (now - drag.sampleAt > DRAG_VELOCITY_WINDOW_MS) {
173
+ drag.sampleAt = now;
174
+ drag.sampleX = event.clientX;
175
+ drag.sampleY = event.clientY;
176
+ }
177
+ const dx = event.clientX - drag.startX;
178
+ const dy = event.clientY - drag.startY;
179
+ const ax = Math.abs(dx);
180
+ const ay = Math.abs(dy);
181
+ if (ax >= ay) {
182
+ const sign = Math.sign(dx) || 1;
183
+ if (isDismissAllowed(anchorOrigin, 'x', sign)) setDragOffset({
184
+ x: dx,
185
+ y: 0
186
+ });
187
+ else {
188
+ const damp = Math.min(DRAG_DAMPEN_MAX, Math.max(0, dampenValue(ax)));
189
+ setDragOffset({
190
+ x: damp * sign,
191
+ y: 0
192
+ });
193
+ }
194
+ } else {
195
+ const sign = Math.sign(dy) || 1;
196
+ if (isDismissAllowed(anchorOrigin, 'y', sign)) setDragOffset({
197
+ x: 0,
198
+ y: dy
199
+ });
200
+ else {
201
+ const damp = Math.min(DRAG_DAMPEN_MAX, Math.max(0, dampenValue(ay)));
202
+ setDragOffset({
203
+ x: 0,
204
+ y: damp * sign
205
+ });
206
+ }
207
+ }
208
+ }, [
209
+ anchorOrigin
210
+ ]);
211
+ const handlePointerUp = useCallback(()=>{
212
+ const drag = dragRef.current;
213
+ if (!drag.dragging) return;
214
+ drag.dragging = false;
215
+ const dx = drag.currentX - drag.startX;
216
+ const dy = drag.currentY - drag.startY;
217
+ const ax = Math.abs(dx);
218
+ const ay = Math.abs(dy);
219
+ const elapsed = Math.max(1, performance.now() - drag.sampleAt);
220
+ const vx = Math.abs(drag.currentX - drag.sampleX) / elapsed;
221
+ const vy = Math.abs(drag.currentY - drag.sampleY) / elapsed;
222
+ const checkAxis = (axis, delta, vel)=>{
223
+ const sign = Math.sign(delta) || 1;
224
+ if (!isDismissAllowed(anchorOrigin, axis, sign)) return null;
225
+ if (Math.abs(delta) >= DRAG_DISMISS_DISTANCE || vel >= DRAG_VELOCITY_THRESHOLD) return {
226
+ axis,
227
+ direction: sign
228
+ };
229
+ return null;
230
+ };
231
+ const outcome = ax >= ay ? checkAxis('x', dx, vx) ?? checkAxis('y', dy, vy) : checkAxis('y', dy, vy) ?? checkAxis('x', dx, vx);
232
+ if (outcome) {
233
+ swipeAxisRef.current = outcome.axis;
234
+ onCloseRef.current?.(null, 'swipe', key);
235
+ dispatch({
236
+ type: 'swipe',
237
+ key,
238
+ direction: outcome.direction
239
+ });
240
+ return;
241
+ }
242
+ setDragOffset({
243
+ x: 0,
244
+ y: 0
245
+ });
246
+ }, [
247
+ anchorOrigin,
248
+ key,
249
+ dispatch
250
+ ]);
251
+ const handlePointerCancel = useCallback(()=>{
252
+ dragRef.current.dragging = false;
253
+ setDragOffset({
254
+ x: 0,
255
+ y: 0
256
+ });
257
+ }, []);
258
+ const handleDismiss = useCallback((event)=>{
259
+ event.stopPropagation();
260
+ onCloseRef.current?.(event, 'instructed', key);
261
+ dispatch({
262
+ type: 'dismiss',
263
+ key
264
+ });
265
+ }, [
266
+ key,
267
+ dispatch
268
+ ]);
269
+ const handleAction = useCallback((event)=>{
270
+ event.stopPropagation();
271
+ onCloseRef.current?.(event, 'instructed', key);
272
+ dispatch({
273
+ type: 'dismiss',
274
+ key
275
+ });
276
+ }, [
277
+ key,
278
+ dispatch
279
+ ]);
280
+ const renderedAction = null == action ? null : 'function' == typeof action ? action(key) : action;
281
+ const cssVars = {
282
+ '--monetr-stack-index': String(index),
283
+ '--monetr-drag-x': `${dragOffset.x}px`,
284
+ '--monetr-drag-y': `${dragOffset.y}px`
285
+ };
286
+ const showPip = !iconNode && PIP_VARIANTS.includes(variant);
287
+ return /*#__PURE__*/ jsxs("li", {
288
+ "aria-atomic": "true",
289
+ "aria-live": 'error' === variant || 'warning' === variant ? 'assertive' : 'polite',
290
+ "data-dragging": dragRef.current.dragging ? 'true' : 'false',
291
+ "data-monetr-notification": "",
292
+ "data-monetr-notification-anchor": `${anchorOrigin.vertical}-${anchorOrigin.horizontal}`,
293
+ "data-monetr-notification-variant": variant,
294
+ "data-stack-hovered-self": stackHovered ? 'true' : 'false',
295
+ "data-stack-index": index,
296
+ "data-state": state,
297
+ "data-swipe-axis": swipeAxisRef.current ?? void 0,
298
+ "data-swipe-direction": 0 !== swipeDirection ? String(swipeDirection) : void 0,
299
+ onPointerCancel: handlePointerCancel,
300
+ onPointerDown: handlePointerDown,
301
+ onPointerMove: handlePointerMove,
302
+ onPointerUp: handlePointerUp,
303
+ ref: elementRef,
304
+ role: 'error' === variant || 'warning' === variant ? 'alert' : 'status',
305
+ style: cssVars,
306
+ children: [
307
+ iconNode ? /*#__PURE__*/ jsx("span", {
308
+ "data-monetr-notification-icon-slot": "",
309
+ children: iconNode
310
+ }) : null,
311
+ showPip ? /*#__PURE__*/ jsx("span", {
312
+ "aria-hidden": "true",
313
+ "data-monetr-notification-pip": ""
314
+ }) : null,
315
+ /*#__PURE__*/ jsx("span", {
316
+ "data-monetr-notification-message": "",
317
+ children: message
318
+ }),
319
+ renderedAction ? /*#__PURE__*/ jsx("button", {
320
+ "data-monetr-notification-action": "",
321
+ onClick: handleAction,
322
+ type: "button",
323
+ children: renderedAction
324
+ }) : null,
325
+ /*#__PURE__*/ jsx("button", {
326
+ "aria-label": "Dismiss",
327
+ "data-monetr-notification-dismiss": "",
328
+ "data-testid": 0 === index ? 'notification-dismiss' : void 0,
329
+ onClick: handleDismiss,
330
+ type: "button",
331
+ children: "\xd7"
332
+ }),
333
+ null !== autoHideDuration ? /*#__PURE__*/ jsx("div", {
334
+ "aria-hidden": "true",
335
+ "data-monetr-notification-progress-track": "",
336
+ children: /*#__PURE__*/ jsx("div", {
337
+ "data-monetr-notification-progress-bar-fill": ""
338
+ })
339
+ }) : null
340
+ ]
341
+ });
342
+ }
343
+ export { Notification };
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { AnchorOrigin, NotifierAction, SnackbarItem, VariantType } from './types';
3
+ interface NotifierProps {
4
+ anchor: AnchorOrigin;
5
+ instances: SnackbarItem[];
6
+ iconVariant?: Partial<Record<VariantType, ReactNode>>;
7
+ dispatch: (action: NotifierAction) => void;
8
+ }
9
+ export declare function Notifier(props: NotifierProps): JSX.Element;
10
+ export {};
@@ -0,0 +1,28 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Notification } from "./notification.js";
4
+ function Notifier(props) {
5
+ const { anchor, instances, iconVariant, dispatch } = props;
6
+ const [hovered, setHovered] = useState(false);
7
+ return /*#__PURE__*/ jsx("ol", {
8
+ "data-monetr-notifier-stack": "",
9
+ "data-stack-anchor": `${anchor.vertical}-${anchor.horizontal}`,
10
+ "data-stack-empty": 0 === instances.length ? 'true' : 'false',
11
+ "data-stack-hovered": hovered ? 'true' : 'false',
12
+ onPointerEnter: ()=>setHovered(true),
13
+ onPointerLeave: ()=>setHovered(false),
14
+ style: {
15
+ listStyle: 'none',
16
+ padding: 0,
17
+ margin: 0
18
+ },
19
+ children: instances.map((inst, index)=>/*#__PURE__*/ jsx(Notification, {
20
+ dispatch: dispatch,
21
+ iconNode: iconVariant?.[inst.variant],
22
+ index: index,
23
+ instance: inst,
24
+ stackHovered: hovered
25
+ }, inst.key))
26
+ });
27
+ }
28
+ export { Notifier };
@@ -0,0 +1,2 @@
1
+ import type { SnackbarProviderProps } from './types';
2
+ export declare function SnackbarProvider(props: SnackbarProviderProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,122 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { SnackbarContext } from "./context.js";
4
+ import { findOverflowKeys, initialQueue, isLiveState, makeInstance, providerDefaults, queueReducer } from "./use-queue.js";
5
+ const LazyNotifierRoot = /*#__PURE__*/ lazy(()=>import("./renderer.js"));
6
+ function SnackbarProvider(props) {
7
+ const { children, prefetch = 'idle', iconVariant, container, anchorOrigin, autoHideDuration, maxSnack } = props;
8
+ const defaults = useMemo(()=>providerDefaults({
9
+ anchorOrigin,
10
+ autoHideDuration,
11
+ maxSnack
12
+ }), [
13
+ anchorOrigin?.vertical,
14
+ anchorOrigin?.horizontal,
15
+ anchorOrigin,
16
+ autoHideDuration,
17
+ maxSnack
18
+ ]);
19
+ const queueRef = useRef(initialQueue);
20
+ const [, setVersion] = useState(0);
21
+ const dispatch = useCallback((action)=>{
22
+ queueRef.current = queueReducer(queueRef.current, action);
23
+ setVersion((v)=>v + 1);
24
+ }, []);
25
+ const prefetchedRef = useRef(false);
26
+ const [active, setActiveState] = useState(false);
27
+ const activeRef = useRef(false);
28
+ const setActive = useCallback(()=>{
29
+ if (!activeRef.current) {
30
+ activeRef.current = true;
31
+ setActiveState(true);
32
+ }
33
+ }, []);
34
+ const enqueueSnackbar = useCallback((message, options)=>{
35
+ if (options?.key !== void 0) {
36
+ const existing = queueRef.current.find((i)=>i.key === options.key);
37
+ if (existing) return existing.key;
38
+ }
39
+ const item = makeInstance(message, options, defaults);
40
+ const dropKeys = findOverflowKeys(queueRef.current, item, defaults.maxSnack);
41
+ for (const key of dropKeys){
42
+ const dropped = queueRef.current.find((i)=>i.key === key);
43
+ dropped?.onClose?.(null, 'maxsnack', key);
44
+ }
45
+ dispatch({
46
+ type: 'enqueue',
47
+ item,
48
+ dropKeys
49
+ });
50
+ setActive();
51
+ return item.key;
52
+ }, [
53
+ defaults,
54
+ setActive,
55
+ dispatch
56
+ ]);
57
+ const closeSnackbar = useCallback((key)=>{
58
+ if (void 0 === key) {
59
+ for (const item of queueRef.current)if (isLiveState(item.state)) item.onClose?.(null, 'instructed', item.key);
60
+ dispatch({
61
+ type: 'close-all'
62
+ });
63
+ return;
64
+ }
65
+ const item = queueRef.current.find((i)=>i.key === key);
66
+ if (item && isLiveState(item.state)) item.onClose?.(null, 'instructed', key);
67
+ dispatch({
68
+ type: 'dismiss',
69
+ key
70
+ });
71
+ }, [
72
+ dispatch
73
+ ]);
74
+ const api = useMemo(()=>({
75
+ enqueueSnackbar,
76
+ closeSnackbar
77
+ }), [
78
+ enqueueSnackbar,
79
+ closeSnackbar
80
+ ]);
81
+ useEffect(()=>{
82
+ if ('never' === prefetch || "u" < typeof window || prefetchedRef.current) return;
83
+ const run = ()=>{
84
+ prefetchedRef.current = true;
85
+ import("./renderer.js");
86
+ };
87
+ if ('mount' === prefetch) return void run();
88
+ const w = window;
89
+ const ric = w.requestIdleCallback;
90
+ const cic = w.cancelIdleCallback;
91
+ if (ric) {
92
+ const id = ric(run);
93
+ return ()=>{
94
+ if (cic) cic(id);
95
+ };
96
+ }
97
+ const id = window.setTimeout(run, 200);
98
+ return ()=>window.clearTimeout(id);
99
+ }, [
100
+ prefetch
101
+ ]);
102
+ const rendererDispatch = useCallback((a)=>dispatch(a), [
103
+ dispatch
104
+ ]);
105
+ return /*#__PURE__*/ jsxs(SnackbarContext.Provider, {
106
+ value: api,
107
+ children: [
108
+ children,
109
+ active ? /*#__PURE__*/ jsx(Suspense, {
110
+ fallback: null,
111
+ children: /*#__PURE__*/ jsx(LazyNotifierRoot, {
112
+ container: container,
113
+ dispatch: rendererDispatch,
114
+ iconVariant: iconVariant,
115
+ maxSnack: defaults.maxSnack,
116
+ queue: queueRef.current
117
+ })
118
+ }) : null
119
+ ]
120
+ });
121
+ }
122
+ export { SnackbarProvider };
@@ -0,0 +1,4 @@
1
+ import type { NotifierRootProps } from './types';
2
+ import './style.css';
3
+ declare function NotifierRoot(props: NotifierRootProps): JSX.Element | null;
4
+ export default NotifierRoot;
@@ -0,0 +1,72 @@
1
+ import { Fragment, jsx } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import { Notifier } from "./notifier.js";
5
+ import { anchorKey } from "./use-queue.js";
6
+ import "./style.css";
7
+ const ALL_ANCHORS = [
8
+ {
9
+ vertical: 'top',
10
+ horizontal: 'left'
11
+ },
12
+ {
13
+ vertical: 'top',
14
+ horizontal: 'center'
15
+ },
16
+ {
17
+ vertical: 'top',
18
+ horizontal: 'right'
19
+ },
20
+ {
21
+ vertical: 'bottom',
22
+ horizontal: 'left'
23
+ },
24
+ {
25
+ vertical: 'bottom',
26
+ horizontal: 'center'
27
+ },
28
+ {
29
+ vertical: 'bottom',
30
+ horizontal: 'right'
31
+ }
32
+ ];
33
+ function NotifierRoot(props) {
34
+ const { queue, dispatch, iconVariant, container } = props;
35
+ const [target, setTarget] = useState(null);
36
+ useEffect(()=>{
37
+ if (container) setTarget(container);
38
+ else if ("u" > typeof document) setTarget(document.body);
39
+ }, [
40
+ container
41
+ ]);
42
+ const grouped = useMemo(()=>{
43
+ const map = new Map();
44
+ for (const item of queue){
45
+ const k = anchorKey(item.anchorOrigin);
46
+ const list = map.get(k);
47
+ if (list) list.push(item);
48
+ else map.set(k, [
49
+ item
50
+ ]);
51
+ }
52
+ return map;
53
+ }, [
54
+ queue
55
+ ]);
56
+ if (!target) return null;
57
+ return /*#__PURE__*/ jsx(Fragment, {
58
+ children: ALL_ANCHORS.map((anchor)=>{
59
+ const k = anchorKey(anchor);
60
+ const items = grouped.get(k) ?? [];
61
+ if (0 === items.length) return null;
62
+ return /*#__PURE__*/ createPortal(/*#__PURE__*/ jsx(Notifier, {
63
+ anchor: anchor,
64
+ dispatch: dispatch,
65
+ iconVariant: iconVariant,
66
+ instances: items
67
+ }), target, k);
68
+ })
69
+ });
70
+ }
71
+ const renderer = NotifierRoot;
72
+ export default renderer;