react-snackbar-glass 0.0.0-stage → 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.
package/dist/index.cjs ADDED
@@ -0,0 +1,967 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var reactDom = require('react-dom');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ // src/core/createSnackbarStore.ts
8
+ var EMPTY = Object.freeze([]);
9
+ var DEFAULT_CONFIG = {
10
+ position: "bottom-right",
11
+ duration: 5e3,
12
+ dismissible: true,
13
+ progress: true,
14
+ tone: "glass",
15
+ size: "md",
16
+ maxVisible: 3
17
+ };
18
+ var sequence = 0;
19
+ function createId() {
20
+ sequence += 1;
21
+ const random = Math.random().toString(36).slice(2, 7);
22
+ return `prs-${Date.now().toString(36)}-${sequence.toString(36)}-${random}`;
23
+ }
24
+ function normalizeDuration(value, fallback) {
25
+ if (value === void 0) return fallback;
26
+ if (value === null) return Infinity;
27
+ if (!Number.isFinite(value) || value <= 0) return Infinity;
28
+ return Math.max(1, Math.round(value));
29
+ }
30
+ function isPlainOptions(value) {
31
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !react.isValidElement(value) && !("$$typeof" in value);
32
+ }
33
+ function contentToOptions(content) {
34
+ if (isPlainOptions(content)) return content;
35
+ return { message: content };
36
+ }
37
+ function resolveContent(content, value) {
38
+ const resolved = typeof content === "function" ? content(value) : content;
39
+ return contentToOptions(resolved);
40
+ }
41
+ function createSnackbarStore(initialConfig = {}) {
42
+ let config = {
43
+ ...DEFAULT_CONFIG,
44
+ ...initialConfig.position !== void 0 ? { position: initialConfig.position } : null,
45
+ ...initialConfig.duration !== void 0 ? {
46
+ duration: normalizeDuration(
47
+ initialConfig.duration,
48
+ DEFAULT_CONFIG.duration
49
+ )
50
+ } : null,
51
+ ...initialConfig.dismissible !== void 0 ? { dismissible: initialConfig.dismissible } : null,
52
+ ...initialConfig.progress !== void 0 ? { progress: initialConfig.progress } : null,
53
+ ...initialConfig.tone !== void 0 ? { tone: initialConfig.tone } : null,
54
+ ...initialConfig.size !== void 0 ? { size: initialConfig.size } : null,
55
+ ...initialConfig.maxVisible !== void 0 ? { maxVisible: Math.max(1, Math.floor(initialConfig.maxVisible)) } : null
56
+ };
57
+ let records = EMPTY;
58
+ const listeners = /* @__PURE__ */ new Set();
59
+ const timers = /* @__PURE__ */ new Map();
60
+ const shown = /* @__PURE__ */ new Set();
61
+ const pauseReasons = /* @__PURE__ */ new Map();
62
+ let pageHidden = false;
63
+ const emit = () => {
64
+ for (const listener of listeners) listener();
65
+ };
66
+ const find = (id) => records.find((record) => record.id === id);
67
+ const replace = (id, record) => {
68
+ records = records.map((current) => current.id === id ? record : current);
69
+ };
70
+ const cleanup = (id) => {
71
+ timers.delete(id);
72
+ shown.delete(id);
73
+ pauseReasons.delete(id);
74
+ };
75
+ const stopTimer = (timer) => {
76
+ if (timer.handle !== null) {
77
+ clearTimeout(timer.handle);
78
+ timer.handle = null;
79
+ }
80
+ if (timer.startedAt !== null) {
81
+ timer.remaining = Math.max(0, timer.remaining - (Date.now() - timer.startedAt));
82
+ timer.startedAt = null;
83
+ }
84
+ };
85
+ const schedule = (id, timer) => {
86
+ timer.startedAt = Date.now();
87
+ timer.handle = setTimeout(() => {
88
+ timer.handle = null;
89
+ timer.startedAt = null;
90
+ timer.remaining = 0;
91
+ dismiss(id, "timeout");
92
+ }, Math.max(0, timer.remaining));
93
+ };
94
+ const startTimer = (id) => {
95
+ const record = find(id);
96
+ if (!record || record.phase !== "visible" || !shown.has(id) || record.duration === Infinity || pageHidden || (pauseReasons.get(id)?.size ?? 0) > 0) {
97
+ return;
98
+ }
99
+ const existing = timers.get(id);
100
+ if (existing) {
101
+ if (existing.handle === null && existing.startedAt === null) {
102
+ schedule(id, existing);
103
+ }
104
+ return;
105
+ }
106
+ const timer = {
107
+ remaining: record.duration,
108
+ startedAt: null,
109
+ handle: null
110
+ };
111
+ timers.set(id, timer);
112
+ schedule(id, timer);
113
+ };
114
+ const resetTimer = (id) => {
115
+ const timer = timers.get(id);
116
+ if (timer) {
117
+ if (timer.handle !== null) clearTimeout(timer.handle);
118
+ timers.delete(id);
119
+ }
120
+ startTimer(id);
121
+ };
122
+ const setPaused = (id, paused) => {
123
+ const record = find(id);
124
+ if (!record || record.paused === paused) return;
125
+ replace(id, { ...record, paused });
126
+ emit();
127
+ };
128
+ const pause = (id, reason = "manual") => {
129
+ const record = find(id);
130
+ if (!record || record.phase !== "visible") return;
131
+ let reasons = pauseReasons.get(id);
132
+ if (!reasons) {
133
+ reasons = /* @__PURE__ */ new Set();
134
+ pauseReasons.set(id, reasons);
135
+ }
136
+ if (reasons.has(reason)) return;
137
+ reasons.add(reason);
138
+ const timer = timers.get(id);
139
+ if (timer) stopTimer(timer);
140
+ setPaused(id, true);
141
+ };
142
+ const resume = (id, reason = "manual") => {
143
+ const reasons = pauseReasons.get(id);
144
+ if (!reasons || !reasons.has(reason)) return;
145
+ reasons.delete(reason);
146
+ if (reasons.size > 0) return;
147
+ pauseReasons.delete(id);
148
+ setPaused(id, false);
149
+ startTimer(id);
150
+ };
151
+ const dismiss = (id, reason = "manual") => {
152
+ const record = find(id);
153
+ if (!record || record.phase === "exiting") return;
154
+ const timer = timers.get(id);
155
+ if (timer) {
156
+ stopTimer(timer);
157
+ timers.delete(id);
158
+ }
159
+ if (!shown.has(id)) {
160
+ records = records.filter((current) => current.id !== id);
161
+ cleanup(id);
162
+ emit();
163
+ record.onClose?.(reason, id);
164
+ return;
165
+ }
166
+ pauseReasons.delete(id);
167
+ replace(id, {
168
+ ...record,
169
+ phase: "exiting",
170
+ closeReason: reason,
171
+ paused: false
172
+ });
173
+ emit();
174
+ record.onClose?.(reason, id);
175
+ };
176
+ const dismissAll = (reason = "dismissAll") => {
177
+ const callbacks = [];
178
+ const remaining = [];
179
+ for (const record of records) {
180
+ if (record.phase === "exiting") {
181
+ remaining.push(record);
182
+ continue;
183
+ }
184
+ const timer = timers.get(record.id);
185
+ if (timer) {
186
+ stopTimer(timer);
187
+ timers.delete(record.id);
188
+ }
189
+ if (!shown.has(record.id)) {
190
+ cleanup(record.id);
191
+ callbacks.push(() => record.onClose?.(reason, record.id));
192
+ continue;
193
+ }
194
+ pauseReasons.delete(record.id);
195
+ remaining.push({
196
+ ...record,
197
+ phase: "exiting",
198
+ closeReason: reason,
199
+ paused: false
200
+ });
201
+ callbacks.push(() => record.onClose?.(reason, record.id));
202
+ }
203
+ records = remaining;
204
+ emit();
205
+ for (const callback of callbacks) callback();
206
+ };
207
+ const show = (options) => {
208
+ const id = options.id ?? createId();
209
+ const existing = find(id);
210
+ if (existing) {
211
+ update(id, options);
212
+ const current = find(id);
213
+ if (current && current.phase === "exiting") {
214
+ records = records.map(
215
+ (record2) => record2.id === id ? { ...record2, phase: "visible", closeReason: void 0, paused: false } : record2
216
+ );
217
+ pauseReasons.delete(id);
218
+ emit();
219
+ startTimer(id);
220
+ }
221
+ return id;
222
+ }
223
+ const record = {
224
+ id,
225
+ title: options.title,
226
+ message: options.message,
227
+ variant: options.variant ?? "neutral",
228
+ tone: options.tone ?? config.tone,
229
+ size: options.size ?? config.size,
230
+ duration: normalizeDuration(options.duration, config.duration),
231
+ position: options.position ?? config.position,
232
+ icon: options.icon,
233
+ action: options.action ?? null,
234
+ dismissible: options.dismissible ?? config.dismissible,
235
+ progress: options.progress ?? config.progress,
236
+ className: options.className,
237
+ style: options.style,
238
+ createdAt: Date.now(),
239
+ phase: "visible",
240
+ paused: false,
241
+ closeReason: void 0,
242
+ onOpen: options.onOpen,
243
+ onClose: options.onClose
244
+ };
245
+ records = [...records, record];
246
+ emit();
247
+ return id;
248
+ };
249
+ const update = (id, options) => {
250
+ const record = find(id);
251
+ if (!record) return;
252
+ const next = {
253
+ ...record,
254
+ ...options.title !== void 0 ? { title: options.title } : null,
255
+ ...options.message !== void 0 ? { message: options.message } : null,
256
+ ...options.variant !== void 0 ? { variant: options.variant } : null,
257
+ ...options.tone !== void 0 ? { tone: options.tone } : null,
258
+ ...options.size !== void 0 ? { size: options.size } : null,
259
+ ...options.position !== void 0 ? { position: options.position } : null,
260
+ ...options.icon !== void 0 ? { icon: options.icon } : null,
261
+ ...options.action !== void 0 ? { action: options.action } : null,
262
+ ...options.dismissible !== void 0 ? { dismissible: options.dismissible } : null,
263
+ ...options.progress !== void 0 ? { progress: options.progress } : null,
264
+ ...options.className !== void 0 ? { className: options.className } : null,
265
+ ...options.style !== void 0 ? { style: options.style } : null,
266
+ ...options.onOpen !== void 0 ? { onOpen: options.onOpen } : null,
267
+ ...options.onClose !== void 0 ? { onClose: options.onClose } : null,
268
+ ...options.duration !== void 0 ? { duration: normalizeDuration(options.duration, record.duration) } : null
269
+ };
270
+ const durationChanged = next.duration !== record.duration;
271
+ replace(id, next);
272
+ emit();
273
+ if (durationChanged && next.phase === "visible" && shown.has(id)) {
274
+ if (next.duration === Infinity) {
275
+ const timer = timers.get(id);
276
+ if (timer) {
277
+ stopTimer(timer);
278
+ timers.delete(id);
279
+ }
280
+ } else {
281
+ resetTimer(id);
282
+ }
283
+ }
284
+ };
285
+ const notifyShown = (id) => {
286
+ if (shown.has(id)) return;
287
+ const record = find(id);
288
+ if (!record || record.phase !== "visible") return;
289
+ shown.add(id);
290
+ record.onOpen?.(id);
291
+ startTimer(id);
292
+ };
293
+ const notifyExited = (id) => {
294
+ const record = find(id);
295
+ if (!record || record.phase !== "exiting") return;
296
+ cleanup(id);
297
+ records = records.filter((current) => current.id !== id);
298
+ emit();
299
+ };
300
+ const setPageHidden = (hidden) => {
301
+ if (pageHidden === hidden) return;
302
+ pageHidden = hidden;
303
+ for (const record of records) {
304
+ if (hidden) {
305
+ pause(record.id, "page");
306
+ } else {
307
+ resume(record.id, "page");
308
+ startTimer(record.id);
309
+ }
310
+ }
311
+ };
312
+ const setConfig = (patch) => {
313
+ config = {
314
+ ...config,
315
+ ...patch.position !== void 0 ? { position: patch.position } : null,
316
+ ...patch.duration !== void 0 ? {
317
+ duration: normalizeDuration(patch.duration, config.duration)
318
+ } : null,
319
+ ...patch.dismissible !== void 0 ? { dismissible: patch.dismissible } : null,
320
+ ...patch.progress !== void 0 ? { progress: patch.progress } : null,
321
+ ...patch.tone !== void 0 ? { tone: patch.tone } : null,
322
+ ...patch.size !== void 0 ? { size: patch.size } : null,
323
+ ...patch.maxVisible !== void 0 ? { maxVisible: Math.max(1, Math.floor(patch.maxVisible)) } : null
324
+ };
325
+ };
326
+ const promise = (input, messages, options = {}) => {
327
+ const loadingOptions = contentToOptions(messages.loading ?? "Loading\u2026");
328
+ const id = options.id ?? createId();
329
+ show({
330
+ dismissible: false,
331
+ ...options,
332
+ ...loadingOptions,
333
+ id,
334
+ duration: loadingOptions.duration !== void 0 ? loadingOptions.duration : null,
335
+ variant: loadingOptions.variant ?? "loading",
336
+ progress: loadingOptions.progress ?? false
337
+ });
338
+ const settle = (content, value, variant) => {
339
+ const next = resolveContent(content, value);
340
+ update(id, {
341
+ ...next,
342
+ variant: next.variant ?? variant,
343
+ duration: next.duration !== void 0 ? next.duration : options.duration !== void 0 ? options.duration : 4e3
344
+ });
345
+ };
346
+ return input.then(
347
+ (value) => {
348
+ settle(messages.success, value, "success");
349
+ return value;
350
+ },
351
+ (error) => {
352
+ settle(messages.error, error, "error");
353
+ throw error;
354
+ }
355
+ );
356
+ };
357
+ return {
358
+ show,
359
+ update,
360
+ dismiss,
361
+ dismissAll,
362
+ promise,
363
+ subscribe(listener) {
364
+ listeners.add(listener);
365
+ return () => listeners.delete(listener);
366
+ },
367
+ getSnapshot: () => records,
368
+ getServerSnapshot: () => EMPTY,
369
+ getConfig: () => ({ ...config }),
370
+ setConfig,
371
+ pause,
372
+ resume,
373
+ notifyShown,
374
+ notifyExited,
375
+ setPageHidden
376
+ };
377
+ }
378
+ var strokeProps = {
379
+ fill: "none",
380
+ stroke: "currentColor",
381
+ strokeWidth: 1.7,
382
+ strokeLinecap: "round",
383
+ strokeLinejoin: "round"
384
+ };
385
+ var BellIcon = /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 20 20", width: "18", height: "18", "aria-hidden": "true", children: [
386
+ /* @__PURE__ */ jsxRuntime.jsx(
387
+ "path",
388
+ {
389
+ ...strokeProps,
390
+ d: "M5.2 8.1a4.8 4.8 0 0 1 9.6 0c0 3.1 1.3 4.3 1.3 4.3H3.9s1.3-1.2 1.3-4.3Z"
391
+ }
392
+ ),
393
+ /* @__PURE__ */ jsxRuntime.jsx("path", { ...strokeProps, d: "M8.2 15.2a1.9 1.9 0 0 0 3.6 0" })
394
+ ] });
395
+ var CheckIcon = /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 20 20", width: "18", height: "18", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { ...strokeProps, d: "M4.6 10.4 8 13.8l7.4-7.6", strokeWidth: 2 }) });
396
+ var ErrorIcon = /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 20 20", width: "18", height: "18", "aria-hidden": "true", children: [
397
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { ...strokeProps, cx: "10", cy: "10", r: "6.6" }),
398
+ /* @__PURE__ */ jsxRuntime.jsx("path", { ...strokeProps, d: "M10 6.4v4.2" }),
399
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "10", cy: "13.5", r: "0.95", fill: "currentColor" })
400
+ ] });
401
+ var WarningIcon = /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 20 20", width: "18", height: "18", "aria-hidden": "true", children: [
402
+ /* @__PURE__ */ jsxRuntime.jsx(
403
+ "path",
404
+ {
405
+ ...strokeProps,
406
+ d: "M8.6 3.6a1.6 1.6 0 0 1 2.8 0l5.2 9.6a1.6 1.6 0 0 1-1.4 2.4H4.8a1.6 1.6 0 0 1-1.4-2.4l5.2-9.6Z"
407
+ }
408
+ ),
409
+ /* @__PURE__ */ jsxRuntime.jsx("path", { ...strokeProps, d: "M10 7.6v3.2" }),
410
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "10", cy: "13.3", r: "0.95", fill: "currentColor" })
411
+ ] });
412
+ var InfoIcon = /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 20 20", width: "18", height: "18", "aria-hidden": "true", children: [
413
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { ...strokeProps, cx: "10", cy: "10", r: "6.6" }),
414
+ /* @__PURE__ */ jsxRuntime.jsx("path", { ...strokeProps, d: "M10 9.1v4.2" }),
415
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "10", cy: "6.5", r: "0.95", fill: "currentColor" })
416
+ ] });
417
+ var SpinnerIcon = /* @__PURE__ */ jsxRuntime.jsxs(
418
+ "svg",
419
+ {
420
+ viewBox: "0 0 20 20",
421
+ width: "18",
422
+ height: "18",
423
+ "aria-hidden": "true",
424
+ className: "prs-icon-spin",
425
+ children: [
426
+ /* @__PURE__ */ jsxRuntime.jsx(
427
+ "circle",
428
+ {
429
+ ...strokeProps,
430
+ cx: "10",
431
+ cy: "10",
432
+ r: "6.4",
433
+ opacity: "0.22",
434
+ strokeWidth: 2
435
+ }
436
+ ),
437
+ /* @__PURE__ */ jsxRuntime.jsx(
438
+ "path",
439
+ {
440
+ ...strokeProps,
441
+ d: "M10 3.6a6.4 6.4 0 0 1 6.4 6.4",
442
+ strokeWidth: 2
443
+ }
444
+ )
445
+ ]
446
+ }
447
+ );
448
+ var SparkIcon = /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 20 20", width: "18", height: "18", "aria-hidden": "true", children: [
449
+ /* @__PURE__ */ jsxRuntime.jsx(
450
+ "path",
451
+ {
452
+ ...strokeProps,
453
+ d: "M10 2.9c.55 3.9 2.2 5.55 6.1 6.1-3.9.55-5.55 2.2-6.1 6.1-.55-3.9-2.2-5.55-6.1-6.1C7.8 8.45 9.45 6.8 10 2.9Z"
454
+ }
455
+ ),
456
+ /* @__PURE__ */ jsxRuntime.jsx("path", { ...strokeProps, d: "M15.2 13.4c.2 1.4.8 2 2.2 2.2-1.4.2-2 .8-2.2 2.2-.2-1.4-.8-2-2.2-2.2 1.4-.2 2-.8 2.2-2.2Z" })
457
+ ] });
458
+ var defaultIcons = {
459
+ neutral: BellIcon,
460
+ success: CheckIcon,
461
+ error: ErrorIcon,
462
+ warning: WarningIcon,
463
+ info: InfoIcon,
464
+ loading: SpinnerIcon,
465
+ custom: SparkIcon
466
+ };
467
+ var DefaultCloseIcon = /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx(
468
+ "path",
469
+ {
470
+ fill: "none",
471
+ stroke: "currentColor",
472
+ strokeWidth: "1.6",
473
+ strokeLinecap: "round",
474
+ d: "M4.2 4.2l7.6 7.6M11.8 4.2l-7.6 7.6"
475
+ }
476
+ ) });
477
+ function SnackbarItem({
478
+ record,
479
+ store,
480
+ icons,
481
+ closeIcon,
482
+ closeLabel,
483
+ pauseOnHover,
484
+ pauseOnFocus,
485
+ swipeToDismiss,
486
+ swipeThreshold
487
+ }) {
488
+ const [dragX, setDragX] = react.useState(0);
489
+ const [dragging, setDragging] = react.useState(false);
490
+ const [actionPending, setActionPending] = react.useState(false);
491
+ const gesture = react.useRef(null);
492
+ const dragXRef = react.useRef(0);
493
+ const draggingRef = react.useRef(false);
494
+ const suppressClick = react.useRef(false);
495
+ const surfaceRef = react.useRef(null);
496
+ const { id, phase, action, title, message, variant, duration } = record;
497
+ react.useEffect(() => {
498
+ store.notifyShown(id);
499
+ }, [store, id]);
500
+ react.useEffect(() => {
501
+ if (phase !== "exiting") return;
502
+ const timeout = setTimeout(() => store.notifyExited(id), 400);
503
+ return () => clearTimeout(timeout);
504
+ }, [phase, store, id]);
505
+ const resetDrag = () => {
506
+ dragXRef.current = 0;
507
+ draggingRef.current = false;
508
+ setDragX(0);
509
+ setDragging(false);
510
+ };
511
+ const pauseForDrag = () => {
512
+ store.pause(id, "drag");
513
+ };
514
+ const resumeForDrag = () => {
515
+ store.resume(id, "drag");
516
+ };
517
+ const handlePointerDown = (event) => {
518
+ suppressClick.current = false;
519
+ if (!swipeToDismiss || phase === "exiting") return;
520
+ if (event.pointerType === "mouse" && event.button !== 0) return;
521
+ const target = event.target;
522
+ if (target.closest(
523
+ 'button, a, input, textarea, select, [role="button"], [data-prs-no-swipe]'
524
+ )) {
525
+ return;
526
+ }
527
+ gesture.current = {
528
+ pointerId: event.pointerId,
529
+ startX: event.clientX,
530
+ startY: event.clientY,
531
+ axis: "pending"
532
+ };
533
+ };
534
+ const handlePointerMove = (event) => {
535
+ const current = gesture.current;
536
+ if (!current) return;
537
+ const dx = event.clientX - current.startX;
538
+ const dy = event.clientY - current.startY;
539
+ if (current.axis === "pending") {
540
+ if (Math.abs(dx) < 6 && Math.abs(dy) < 6) return;
541
+ if (Math.abs(dy) > Math.abs(dx)) {
542
+ gesture.current = null;
543
+ return;
544
+ }
545
+ current.axis = "x";
546
+ draggingRef.current = true;
547
+ setDragging(true);
548
+ pauseForDrag();
549
+ surfaceRef.current?.setPointerCapture?.(event.pointerId);
550
+ }
551
+ if (event.cancelable) event.preventDefault();
552
+ const absolute = Math.abs(dx);
553
+ const damped = absolute > 96 ? 96 + (absolute - 96) * 0.32 : absolute;
554
+ const next = Math.sign(dx) * damped;
555
+ dragXRef.current = next;
556
+ setDragX(next);
557
+ if (absolute > 8) suppressClick.current = true;
558
+ };
559
+ const finishGesture = (event) => {
560
+ const current = gesture.current;
561
+ if (!current) return;
562
+ if (draggingRef.current) {
563
+ const shouldDismiss = Math.abs(dragXRef.current) >= swipeThreshold;
564
+ resetDrag();
565
+ resumeForDrag();
566
+ if (surfaceRef.current?.hasPointerCapture?.(current.pointerId)) {
567
+ surfaceRef.current.releasePointerCapture(current.pointerId);
568
+ }
569
+ if (shouldDismiss) {
570
+ suppressClick.current = true;
571
+ store.dismiss(id, "swipe");
572
+ }
573
+ } else {
574
+ resetDrag();
575
+ }
576
+ gesture.current = null;
577
+ if (event.type === "pointercancel") resumeForDrag();
578
+ };
579
+ const handleLostPointerCapture = () => {
580
+ if (!draggingRef.current) return;
581
+ resetDrag();
582
+ resumeForDrag();
583
+ gesture.current = null;
584
+ };
585
+ const handleAnimationEnd = (event) => {
586
+ if (event.target !== event.currentTarget) return;
587
+ if (phase === "exiting") store.notifyExited(id);
588
+ };
589
+ const handleAction = async () => {
590
+ if (!action || actionPending) return;
591
+ const result = action.onClick?.(id);
592
+ if (result && typeof result.then === "function") {
593
+ setActionPending(true);
594
+ try {
595
+ await result;
596
+ } finally {
597
+ setActionPending(false);
598
+ }
599
+ }
600
+ if (action.closeOnClick !== false) store.dismiss(id, "action");
601
+ };
602
+ const handleBlur = (event) => {
603
+ if (!pauseOnFocus) return;
604
+ if (!event.currentTarget.contains(event.relatedTarget)) {
605
+ store.resume(id, "focus");
606
+ }
607
+ };
608
+ const resolvedIcon = record.icon === void 0 ? icons?.[variant] ?? defaultIcons[variant] : record.icon;
609
+ const showIcon = record.icon !== false;
610
+ const showProgress = record.progress && Number.isFinite(duration) && duration > 0;
611
+ const style = {
612
+ ...record.style,
613
+ "--prs-drag-x": `${dragX}px`,
614
+ "--prs-drag-opacity": String(
615
+ 1 - Math.min(Math.abs(dragX) / 300, 0.55)
616
+ )
617
+ };
618
+ return /* @__PURE__ */ jsxRuntime.jsx(
619
+ "div",
620
+ {
621
+ className: `prs-item${record.className ? ` ${record.className}` : ""}`,
622
+ "data-variant": variant,
623
+ "data-tone": record.tone,
624
+ "data-size": record.size,
625
+ "data-state": phase,
626
+ "data-paused": record.paused ? "true" : "false",
627
+ "data-dragging": dragging ? "true" : "false",
628
+ "data-has-icon": showIcon ? "true" : "false",
629
+ "data-dismissible": record.dismissible ? "true" : "false",
630
+ "data-prs-id": record.id,
631
+ "data-position": record.position,
632
+ style,
633
+ role: variant === "error" ? "alert" : "status",
634
+ "aria-live": variant === "error" ? "assertive" : "polite",
635
+ "aria-atomic": "true",
636
+ "aria-busy": actionPending || variant === "loading" ? true : void 0,
637
+ onPointerEnter: () => {
638
+ if (pauseOnHover) store.pause(id, "hover");
639
+ },
640
+ onPointerLeave: () => {
641
+ if (pauseOnHover) store.resume(id, "hover");
642
+ },
643
+ onFocus: () => {
644
+ if (pauseOnFocus) store.pause(id, "focus");
645
+ },
646
+ onBlur: handleBlur,
647
+ onKeyDown: (event) => {
648
+ if (event.key === "Escape" && record.dismissible) {
649
+ event.stopPropagation();
650
+ store.dismiss(id, "escape");
651
+ }
652
+ },
653
+ onAnimationEnd: handleAnimationEnd,
654
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
655
+ "div",
656
+ {
657
+ ref: surfaceRef,
658
+ className: "prs-item__surface",
659
+ onClickCapture: (event) => {
660
+ if (!suppressClick.current) return;
661
+ suppressClick.current = false;
662
+ event.preventDefault();
663
+ event.stopPropagation();
664
+ },
665
+ onPointerDown: handlePointerDown,
666
+ onPointerMove: handlePointerMove,
667
+ onPointerUp: finishGesture,
668
+ onPointerCancel: finishGesture,
669
+ onLostPointerCapture: handleLostPointerCapture,
670
+ children: [
671
+ showIcon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "prs-item__icon", "aria-hidden": "true", children: resolvedIcon }) : null,
672
+ /* @__PURE__ */ jsxRuntime.jsxs(
673
+ "div",
674
+ {
675
+ className: "prs-item__content",
676
+ "data-has-title": title !== void 0 && title !== null ? "true" : "false",
677
+ children: [
678
+ title !== void 0 && title !== null ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "prs-item__title", children: title }) : null,
679
+ message !== void 0 && message !== null ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "prs-item__message", children: message }) : null,
680
+ action ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "prs-item__actions", children: /* @__PURE__ */ jsxRuntime.jsxs(
681
+ "button",
682
+ {
683
+ type: "button",
684
+ className: "prs-action",
685
+ onClick: handleAction,
686
+ disabled: actionPending,
687
+ "aria-busy": actionPending || void 0,
688
+ children: [
689
+ actionPending ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "prs-action__spinner", "aria-hidden": "true" }) : null,
690
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: action.label })
691
+ ]
692
+ }
693
+ ) }) : null
694
+ ]
695
+ }
696
+ ),
697
+ record.dismissible ? /* @__PURE__ */ jsxRuntime.jsx(
698
+ "button",
699
+ {
700
+ type: "button",
701
+ className: "prs-close",
702
+ "aria-label": closeLabel,
703
+ onClick: () => store.dismiss(id, "manual"),
704
+ children: closeIcon ?? DefaultCloseIcon
705
+ }
706
+ ) : null,
707
+ showProgress ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "prs-progress", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx(
708
+ "span",
709
+ {
710
+ className: "prs-progress__bar",
711
+ style: { animationDuration: `${duration}ms` }
712
+ },
713
+ duration
714
+ ) }) : null
715
+ ]
716
+ }
717
+ )
718
+ }
719
+ );
720
+ }
721
+ var POSITIONS = [
722
+ "top-left",
723
+ "top-center",
724
+ "top-right",
725
+ "bottom-left",
726
+ "bottom-center",
727
+ "bottom-right"
728
+ ];
729
+ function SnackbarHost({
730
+ store,
731
+ maxVisible = 3,
732
+ offset = 20,
733
+ gap = 12,
734
+ width = 384,
735
+ theme = "auto",
736
+ container,
737
+ pauseOnHover = true,
738
+ pauseOnFocus = true,
739
+ pauseOnDocumentHidden = true,
740
+ swipeToDismiss = true,
741
+ swipeThreshold = 88,
742
+ icons,
743
+ closeIcon,
744
+ closeLabel = "Dismiss notification",
745
+ regionLabel = "Notifications",
746
+ className,
747
+ style,
748
+ zIndex = 2147483e3,
749
+ dir
750
+ }) {
751
+ const records = react.useSyncExternalStore(
752
+ store.subscribe,
753
+ store.getSnapshot,
754
+ store.getServerSnapshot
755
+ );
756
+ const [portalNode, setPortalNode] = react.useState(null);
757
+ const regionRefs = react.useRef(/* @__PURE__ */ new Map());
758
+ const topsRef = react.useRef(/* @__PURE__ */ new Map());
759
+ react.useEffect(() => {
760
+ if (typeof document === "undefined") return;
761
+ const target = typeof container === "function" ? container() : container ?? document.body;
762
+ setPortalNode(target);
763
+ return () => setPortalNode(null);
764
+ }, [container]);
765
+ react.useEffect(() => {
766
+ if (!pauseOnDocumentHidden || typeof document === "undefined") return;
767
+ const sync = () => store.setPageHidden(document.visibilityState === "hidden");
768
+ sync();
769
+ document.addEventListener("visibilitychange", sync);
770
+ return () => {
771
+ document.removeEventListener("visibilitychange", sync);
772
+ store.setPageHidden(false);
773
+ };
774
+ }, [store, pauseOnDocumentHidden]);
775
+ react.useLayoutEffect(() => {
776
+ if (!portalNode) {
777
+ topsRef.current.clear();
778
+ return;
779
+ }
780
+ if (typeof window === "undefined") return;
781
+ const reduced = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
782
+ const next = /* @__PURE__ */ new Map();
783
+ for (const region of regionRefs.current.values()) {
784
+ const regionTop = region.getBoundingClientRect().top;
785
+ const nodes = region.querySelectorAll("[data-prs-id]");
786
+ for (const node of nodes) {
787
+ const id = node.dataset.prsId;
788
+ if (!id) continue;
789
+ const top = regionTop + node.offsetTop;
790
+ next.set(id, top);
791
+ const previous = topsRef.current.get(id);
792
+ if (reduced || previous === void 0 || node.dataset.state === "exiting" || typeof node.animate !== "function") {
793
+ continue;
794
+ }
795
+ const delta = previous - top;
796
+ if (Math.abs(delta) < 0.5) continue;
797
+ node.animate(
798
+ [
799
+ { transform: `translate3d(0, ${delta}px, 0)` },
800
+ { transform: "translate3d(0, 0, 0)" }
801
+ ],
802
+ {
803
+ duration: 320,
804
+ easing: "cubic-bezier(0.22, 1, 0.36, 1)"
805
+ }
806
+ );
807
+ }
808
+ }
809
+ topsRef.current = next;
810
+ });
811
+ if (!portalNode) return null;
812
+ const byPosition = /* @__PURE__ */ new Map();
813
+ for (const record of records) {
814
+ const bucket = byPosition.get(record.position);
815
+ if (bucket) bucket.push(record);
816
+ else byPosition.set(record.position, [record]);
817
+ }
818
+ const limit = Math.max(1, Math.floor(maxVisible));
819
+ const rootStyle = {
820
+ "--prs-width": `${width}px`,
821
+ "--prs-offset": `${offset}px`,
822
+ "--prs-gap": `${gap}px`,
823
+ "--prs-z-index": String(zIndex),
824
+ ...style
825
+ };
826
+ return reactDom.createPortal(
827
+ /* @__PURE__ */ jsxRuntime.jsx(
828
+ "div",
829
+ {
830
+ className: `prs-root${className ? ` ${className}` : ""}`,
831
+ "data-prs-theme": theme,
832
+ dir,
833
+ style: rootStyle,
834
+ children: POSITIONS.map((position) => {
835
+ const items = byPosition.get(position);
836
+ if (!items || items.length === 0) return null;
837
+ const visible = items.slice(0, limit);
838
+ return /* @__PURE__ */ jsxRuntime.jsx(
839
+ "section",
840
+ {
841
+ ref: (node) => {
842
+ if (node) regionRefs.current.set(position, node);
843
+ else regionRefs.current.delete(position);
844
+ },
845
+ className: "prs-region",
846
+ "data-position": position,
847
+ "aria-label": regionLabel,
848
+ children: visible.map((record) => /* @__PURE__ */ jsxRuntime.jsx(
849
+ SnackbarItem,
850
+ {
851
+ record,
852
+ store,
853
+ icons,
854
+ closeIcon,
855
+ closeLabel,
856
+ pauseOnHover,
857
+ pauseOnFocus,
858
+ swipeToDismiss,
859
+ swipeThreshold
860
+ },
861
+ record.id
862
+ ))
863
+ },
864
+ position
865
+ );
866
+ })
867
+ }
868
+ ),
869
+ portalNode
870
+ );
871
+ }
872
+ var SnackbarContext = react.createContext(null);
873
+ function SnackbarProvider({
874
+ children,
875
+ store: providedStore,
876
+ position,
877
+ duration,
878
+ dismissible,
879
+ progress,
880
+ tone,
881
+ size,
882
+ maxVisible,
883
+ ...hostProps
884
+ }) {
885
+ const store = react.useMemo(
886
+ () => providedStore ?? createSnackbarStore({
887
+ position,
888
+ duration,
889
+ dismissible,
890
+ progress,
891
+ tone,
892
+ size
893
+ }),
894
+ // The store is intentionally created once per provider instance.
895
+ // Subsequent config changes are pushed below.
896
+ [providedStore]
897
+ );
898
+ react.useEffect(() => {
899
+ store.setConfig({
900
+ position,
901
+ duration,
902
+ dismissible,
903
+ progress,
904
+ tone,
905
+ size,
906
+ maxVisible
907
+ });
908
+ }, [store, position, duration, dismissible, progress, tone, size, maxVisible]);
909
+ const api = react.useMemo(() => {
910
+ const preset = (variant) => (message, options = {}) => store.show({ ...options, message, variant });
911
+ return {
912
+ show: store.show,
913
+ update: store.update,
914
+ dismiss: store.dismiss,
915
+ dismissAll: store.dismissAll,
916
+ promise: store.promise,
917
+ success: preset("success"),
918
+ error: preset("error"),
919
+ warning: preset("warning"),
920
+ info: preset("info"),
921
+ neutral: preset("neutral"),
922
+ custom: preset("custom"),
923
+ loading: (message, options = {}) => store.show({
924
+ ...options,
925
+ message,
926
+ variant: "loading",
927
+ duration: options.duration !== void 0 ? options.duration : null
928
+ }),
929
+ store
930
+ };
931
+ }, [store]);
932
+ const value = react.useMemo(() => ({ store, api }), [store, api]);
933
+ return /* @__PURE__ */ jsxRuntime.jsxs(SnackbarContext.Provider, { value, children: [
934
+ children,
935
+ /* @__PURE__ */ jsxRuntime.jsx(
936
+ SnackbarHost,
937
+ {
938
+ store,
939
+ maxVisible: maxVisible ?? store.getConfig().maxVisible,
940
+ ...hostProps
941
+ }
942
+ )
943
+ ] });
944
+ }
945
+ function useSnackbarContext() {
946
+ const context = react.useContext(SnackbarContext);
947
+ if (!context) {
948
+ throw new Error(
949
+ "useSnackbar must be used inside a <SnackbarProvider>. Render the provider above the component that displays snackbars."
950
+ );
951
+ }
952
+ return context;
953
+ }
954
+ function useSnackbar() {
955
+ return useSnackbarContext().api;
956
+ }
957
+ function useSnackbarStore() {
958
+ return useSnackbarContext().store;
959
+ }
960
+
961
+ exports.SnackbarHost = SnackbarHost;
962
+ exports.SnackbarProvider = SnackbarProvider;
963
+ exports.createSnackbarStore = createSnackbarStore;
964
+ exports.useSnackbar = useSnackbar;
965
+ exports.useSnackbarStore = useSnackbarStore;
966
+ //# sourceMappingURL=index.cjs.map
967
+ //# sourceMappingURL=index.cjs.map