goey-toast 0.2.0 → 0.2.1

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 CHANGED
@@ -29,6 +29,29 @@ function setGoeyBounce(bounce) {
29
29
  function getGoeyBounce() {
30
30
  return _bounce;
31
31
  }
32
+ var _visibleToasts = 3;
33
+ function setGoeyVisibleToasts(n) {
34
+ _visibleToasts = n;
35
+ }
36
+ function getGoeyVisibleToasts() {
37
+ return _visibleToasts;
38
+ }
39
+ var _containerHovered = false;
40
+ var _hoverSubs = /* @__PURE__ */ new Set();
41
+ function setContainerHovered(hovered) {
42
+ if (_containerHovered === hovered) return;
43
+ _containerHovered = hovered;
44
+ _hoverSubs.forEach((cb) => cb(hovered));
45
+ }
46
+ function getContainerHovered() {
47
+ return _containerHovered;
48
+ }
49
+ function subscribeContainerHovered(cb) {
50
+ _hoverSubs.add(cb);
51
+ return () => {
52
+ _hoverSubs.delete(cb);
53
+ };
54
+ }
32
55
  function GoeyToaster({
33
56
  position = "bottom-right",
34
57
  duration,
@@ -53,6 +76,43 @@ function GoeyToaster({
53
76
  react.useEffect(() => {
54
77
  setGoeyBounce(bounce);
55
78
  }, [bounce]);
79
+ react.useEffect(() => {
80
+ setGoeyVisibleToasts(visibleToasts ?? 3);
81
+ }, [visibleToasts]);
82
+ react.useEffect(() => {
83
+ let expandObs = null;
84
+ let currentOl = null;
85
+ const syncFromExpanded = (ol) => {
86
+ const anyExpanded = ol.querySelector('[data-sonner-toast][data-expanded="true"]') !== null;
87
+ setContainerHovered(anyExpanded);
88
+ };
89
+ const attach = (ol) => {
90
+ if (ol === currentOl) return;
91
+ expandObs?.disconnect();
92
+ currentOl = ol;
93
+ expandObs = new MutationObserver(() => syncFromExpanded(ol));
94
+ expandObs.observe(ol, { attributes: true, attributeFilter: ["data-expanded"], subtree: true });
95
+ syncFromExpanded(ol);
96
+ };
97
+ const el = document.querySelector("[data-sonner-toaster]");
98
+ if (el) attach(el);
99
+ const bodyObs = new MutationObserver(() => {
100
+ const found = document.querySelector("[data-sonner-toaster]");
101
+ if (found) {
102
+ attach(found);
103
+ } else if (currentOl) {
104
+ expandObs?.disconnect();
105
+ currentOl = null;
106
+ setContainerHovered(false);
107
+ }
108
+ });
109
+ bodyObs.observe(document.body, { childList: true, subtree: true });
110
+ return () => {
111
+ bodyObs.disconnect();
112
+ expandObs?.disconnect();
113
+ setContainerHovered(false);
114
+ };
115
+ }, []);
56
116
  react.useEffect(() => {
57
117
  if (process.env.NODE_ENV !== "development") return;
58
118
  const el = document.createElement("div");
@@ -83,7 +143,7 @@ function GoeyToaster({
83
143
  expand,
84
144
  closeButton,
85
145
  richColors,
86
- visibleToasts,
146
+ visibleToasts: 99,
87
147
  dir
88
148
  }
89
149
  );
@@ -303,7 +363,7 @@ function registerSonnerObserver(ol, callback) {
303
363
  });
304
364
  observer.observe(ol, {
305
365
  attributes: true,
306
- attributeFilter: ["style"],
366
+ attributeFilter: ["style", "data-visible"],
307
367
  subtree: true,
308
368
  childList: true
309
369
  });
@@ -319,7 +379,7 @@ function registerSonnerObserver(ol, callback) {
319
379
  }
320
380
  };
321
381
  }
322
- function syncSonnerHeights(wrapperEl) {
382
+ function syncSonnerHeights(wrapperEl, includeOffsets = false) {
323
383
  if (!wrapperEl) return;
324
384
  const li = wrapperEl.closest("[data-sonner-toast]");
325
385
  if (!li?.parentElement) return;
@@ -327,12 +387,42 @@ function syncSonnerHeights(wrapperEl) {
327
387
  const toasts = Array.from(
328
388
  ol.querySelectorAll(":scope > [data-sonner-toast]")
329
389
  );
330
- for (const t of toasts) {
390
+ if (toasts.length === 0) return;
391
+ const heights = toasts.map((t) => {
392
+ if (t.getAttribute("data-visible") === "false") return 0;
331
393
  const content = t.firstElementChild;
332
- const height = content ? content.getBoundingClientRect().height : 0;
333
- if (height > 0) {
334
- t.style.setProperty("--initial-height", `${height}px`);
394
+ const h = content ? content.getBoundingClientRect().height : 0;
395
+ return h > 0 ? h : PH;
396
+ });
397
+ const isExpanded = includeOffsets && toasts[0]?.getAttribute("data-expanded") === "true";
398
+ if (isExpanded) {
399
+ for (const t of toasts) t.style.setProperty("transition", "none", "important");
400
+ }
401
+ for (let i = 0; i < toasts.length; i++) {
402
+ toasts[i].style.setProperty("--initial-height", `${heights[i]}px`);
403
+ }
404
+ if (!includeOffsets) {
405
+ if (isExpanded) {
406
+ for (const t of toasts) t.style.removeProperty("transition");
335
407
  }
408
+ return;
409
+ }
410
+ const gapStr = getComputedStyle(ol).getPropertyValue("--gap").trim();
411
+ const gap = parseFloat(gapStr) || 14;
412
+ let runningOffset = 0;
413
+ for (let i = toasts.length - 1; i >= 0; i--) {
414
+ if (toasts[i].getAttribute("data-visible") === "false") {
415
+ toasts[i].style.setProperty("--offset", "0px");
416
+ continue;
417
+ }
418
+ toasts[i].style.setProperty("--offset", `${runningOffset}px`);
419
+ if (i > 0) {
420
+ runningOffset += heights[i] + gap;
421
+ }
422
+ }
423
+ if (isExpanded) {
424
+ void ol.offsetHeight;
425
+ for (const t of toasts) t.style.removeProperty("transition");
336
426
  }
337
427
  }
338
428
  function morphPath(pw, bw, th, t) {
@@ -461,6 +551,8 @@ var GoeyToast = ({
461
551
  const [dismissing, setDismissing] = react.useState(false);
462
552
  const [hovered, setHovered] = react.useState(false);
463
553
  const hoveredRef = react.useRef(false);
554
+ const containerHoveredRef = react.useRef(getContainerHovered());
555
+ const [containerHovered, setContainerHoveredState] = react.useState(getContainerHovered());
464
556
  const collapsingRef = react.useRef(false);
465
557
  const preDismissRef = react.useRef(false);
466
558
  const collapseEndTime = react.useRef(0);
@@ -489,6 +581,12 @@ var GoeyToast = ({
489
581
  react.useEffect(() => {
490
582
  dimsRef.current = dims;
491
583
  }, [dims]);
584
+ react.useEffect(() => {
585
+ return subscribeContainerHovered((h) => {
586
+ containerHoveredRef.current = h;
587
+ setContainerHoveredState(h);
588
+ });
589
+ }, []);
492
590
  const flush = react.useCallback(() => {
493
591
  const { pw: p, bw: b, th: h } = aDims.current;
494
592
  if (p <= 0 || b <= 0 || h <= 0) return;
@@ -749,6 +847,7 @@ var GoeyToast = ({
749
847
  th: targetDims.th + (savedDims.th - targetDims.th) * t
750
848
  };
751
849
  flush();
850
+ syncSonnerHeights(wrapperRef.current, true);
752
851
  },
753
852
  onComplete: () => {
754
853
  morphTRef.current = 0;
@@ -757,6 +856,7 @@ var GoeyToast = ({
757
856
  collapseEndTime.current = Date.now();
758
857
  aDims.current = { ...targetDims };
759
858
  flush();
859
+ syncSonnerHeights(wrapperRef.current, true);
760
860
  setShowBody(false);
761
861
  }
762
862
  });
@@ -777,10 +877,15 @@ var GoeyToast = ({
777
877
  const displayMs = timing?.displayDuration ?? DEFAULT_DISPLAY_DURATION;
778
878
  const fullDelay = displayMs - expandDelayMs - collapseMs;
779
879
  if (fullDelay <= 0) return;
780
- if (hoveredRef.current) return;
880
+ if (hoveredRef.current || containerHoveredRef.current) return;
781
881
  const delay = remainingRef.current ?? fullDelay;
782
882
  timerStartRef.current = Date.now();
783
883
  const timer = setTimeout(() => {
884
+ if (hoveredRef.current || containerHoveredRef.current) {
885
+ const elapsed = Date.now() - timerStartRef.current;
886
+ remainingRef.current = Math.max(0, delay - elapsed);
887
+ return;
888
+ }
784
889
  remainingRef.current = null;
785
890
  expandedDimsRef.current = { ...aDims.current };
786
891
  collapsingRef.current = true;
@@ -792,15 +897,15 @@ var GoeyToast = ({
792
897
  clearTimeout(timer);
793
898
  const elapsed = Date.now() - timerStartRef.current;
794
899
  const remaining = delay - elapsed;
795
- if (remaining > 0 && hoveredRef.current) {
900
+ if (remaining > 0 && (hoveredRef.current || containerHoveredRef.current)) {
796
901
  remainingRef.current = remaining;
797
902
  }
798
903
  };
799
- }, [showBody, actionSuccess, dismissing, prefersReducedMotion, hovered]);
904
+ }, [showBody, actionSuccess, dismissing, prefersReducedMotion, hovered, containerHovered]);
800
905
  const canExpand = hasDescription || hasAction;
801
906
  const reExpandingRef = react.useRef(false);
802
907
  react.useEffect(() => {
803
- if (!hovered || !canExpand || !dismissing) return;
908
+ if (!hovered && !containerHovered || !canExpand || !dismissing) return;
804
909
  morphCtrl.current?.stop();
805
910
  collapsingRef.current = false;
806
911
  preDismissRef.current = false;
@@ -823,27 +928,30 @@ var GoeyToast = ({
823
928
  th: startDims.th + (target.th - startDims.th) * t
824
929
  };
825
930
  flush();
931
+ syncSonnerHeights(wrapperRef.current, true);
826
932
  },
827
933
  onComplete: () => {
828
934
  morphTRef.current = 1;
829
935
  aDims.current = { ...dimsRef.current };
830
936
  reExpandingRef.current = false;
831
937
  flush();
832
- syncSonnerHeights(wrapperRef.current);
938
+ syncSonnerHeights(wrapperRef.current, true);
833
939
  }
834
940
  });
835
941
  });
836
942
  return () => {
837
943
  morphCtrl.current?.stop();
838
944
  };
839
- }, [hovered, dismissing, canExpand]);
945
+ }, [hovered, containerHovered, dismissing, canExpand]);
840
946
  react.useEffect(() => {
841
- if (!toastId || !dismissing || showBody || hovered) return;
947
+ if (!toastId || !dismissing || showBody) return;
842
948
  const t = setTimeout(() => {
843
- if (!hoveredRef.current) sonner.toast.dismiss(toastId);
949
+ if (!hoveredRef.current && !containerHoveredRef.current) {
950
+ sonner.toast.dismiss(toastId);
951
+ }
844
952
  }, 800);
845
953
  return () => clearTimeout(t);
846
- }, [dismissing, showBody, hovered, toastId]);
954
+ }, [dismissing, showBody, toastId]);
847
955
  react.useEffect(() => {
848
956
  if (!toastId || !actionSuccess || showBody) return;
849
957
  const t = setTimeout(() => sonner.toast.dismiss(toastId), 1200);
@@ -863,7 +971,7 @@ var GoeyToast = ({
863
971
  morphTRef.current = 1;
864
972
  aDims.current = { ...dimsRef.current };
865
973
  flush();
866
- syncSonnerHeights(wrapperRef.current);
974
+ syncSonnerHeights(wrapperRef.current, true);
867
975
  return;
868
976
  }
869
977
  const raf = requestAnimationFrame(() => {
@@ -882,12 +990,13 @@ var GoeyToast = ({
882
990
  th: startDims.th + (target.th - startDims.th) * t
883
991
  };
884
992
  flush();
993
+ syncSonnerHeights(wrapperRef.current, true);
885
994
  },
886
995
  onComplete: () => {
887
996
  morphTRef.current = 1;
888
997
  aDims.current = { ...dimsRef.current };
889
998
  flush();
890
- syncSonnerHeights(wrapperRef.current);
999
+ syncSonnerHeights(wrapperRef.current, true);
891
1000
  }
892
1001
  });
893
1002
  });
@@ -937,9 +1046,26 @@ var GoeyToast = ({
937
1046
  if (!wrapper) return;
938
1047
  const ol = wrapper.closest("[data-sonner-toast]")?.parentElement;
939
1048
  if (!ol) return;
940
- return registerSonnerObserver(ol, () => {
941
- syncSonnerHeights(wrapper);
1049
+ const unregister = registerSonnerObserver(ol, () => {
1050
+ syncSonnerHeights(wrapper, true);
1051
+ });
1052
+ const expandObs = new MutationObserver((mutations) => {
1053
+ for (const m of mutations) {
1054
+ if (m.type === "attributes" && m.attributeName === "data-expanded" && m.target.getAttribute("data-expanded") === "true") {
1055
+ syncSonnerHeights(wrapper, true);
1056
+ break;
1057
+ }
1058
+ }
1059
+ });
1060
+ expandObs.observe(ol, {
1061
+ attributes: true,
1062
+ attributeFilter: ["data-expanded"],
1063
+ subtree: true
942
1064
  });
1065
+ return () => {
1066
+ unregister();
1067
+ expandObs.disconnect();
1068
+ };
943
1069
  }, []);
944
1070
  const handleActionClick = react.useCallback(() => {
945
1071
  if (!effectiveAction) return;
@@ -1067,6 +1193,20 @@ var ToastErrorBoundary = class extends react.Component {
1067
1193
  }
1068
1194
  };
1069
1195
  var DEFAULT_EXPANDED_DURATION = 4e3;
1196
+ var _activeIds = /* @__PURE__ */ new Set();
1197
+ var _queue = [];
1198
+ function _processQueue() {
1199
+ const max = getGoeyVisibleToasts();
1200
+ while (_queue.length > 0 && _activeIds.size < max) {
1201
+ const next = _queue.shift();
1202
+ _activeIds.add(next.id);
1203
+ next.create();
1204
+ }
1205
+ }
1206
+ function _onToastDismissed(id) {
1207
+ if (!_activeIds.delete(id)) return;
1208
+ _processQueue();
1209
+ }
1070
1210
  function GoeyToastWrapper({
1071
1211
  initialPhase,
1072
1212
  title,
@@ -1081,8 +1221,19 @@ function GoeyToastWrapper({
1081
1221
  timing,
1082
1222
  spring,
1083
1223
  bounce,
1084
- toastId
1224
+ toastId,
1225
+ activeId
1085
1226
  }) {
1227
+ const mountedRef = react.useRef(true);
1228
+ react.useEffect(() => {
1229
+ mountedRef.current = true;
1230
+ return () => {
1231
+ mountedRef.current = false;
1232
+ setTimeout(() => {
1233
+ if (!mountedRef.current) _onToastDismissed(activeId);
1234
+ }, 100);
1235
+ };
1236
+ }, [activeId]);
1086
1237
  return /* @__PURE__ */ jsxRuntime.jsx(ToastErrorBoundary, { children: /* @__PURE__ */ jsxRuntime.jsx(
1087
1238
  GoeyToast,
1088
1239
  {
@@ -1112,6 +1263,16 @@ function PromiseToastWrapper({
1112
1263
  const [title, setTitle] = react.useState(data.loading);
1113
1264
  const [description, setDescription] = react.useState(data.description?.loading);
1114
1265
  const [action, setAction] = react.useState(void 0);
1266
+ const mountedRef = react.useRef(true);
1267
+ react.useEffect(() => {
1268
+ mountedRef.current = true;
1269
+ return () => {
1270
+ mountedRef.current = false;
1271
+ setTimeout(() => {
1272
+ if (!mountedRef.current) _onToastDismissed(toastId);
1273
+ }, 100);
1274
+ };
1275
+ }, [toastId]);
1115
1276
  react.useEffect(() => {
1116
1277
  const resetDuration = (hasExpandedContent) => {
1117
1278
  const baseDuration = data.timing?.displayDuration ?? (hasExpandedContent ? DEFAULT_EXPANDED_DURATION : void 0);
@@ -1164,31 +1325,55 @@ function createGoeyToast(title, type, options) {
1164
1325
  const baseDuration = options?.timing?.displayDuration ?? options?.duration ?? (options?.description ? DEFAULT_EXPANDED_DURATION : void 0);
1165
1326
  const duration = hasExpandedContent ? Infinity : baseDuration;
1166
1327
  const toastId = options?.id ?? Math.random().toString(36).slice(2);
1167
- return sonner.toast.custom(
1168
- () => /* @__PURE__ */ jsxRuntime.jsx(
1169
- GoeyToastWrapper,
1328
+ const create = () => {
1329
+ sonner.toast.custom(
1330
+ () => /* @__PURE__ */ jsxRuntime.jsx(
1331
+ GoeyToastWrapper,
1332
+ {
1333
+ initialPhase: type,
1334
+ title,
1335
+ type,
1336
+ description: options?.description,
1337
+ action: options?.action,
1338
+ icon: options?.icon,
1339
+ classNames: options?.classNames,
1340
+ fillColor: options?.fillColor,
1341
+ borderColor: options?.borderColor,
1342
+ borderWidth: options?.borderWidth,
1343
+ timing: options?.timing,
1344
+ spring: options?.spring,
1345
+ bounce: options?.bounce,
1346
+ toastId: hasExpandedContent ? toastId : void 0,
1347
+ activeId: toastId
1348
+ }
1349
+ ),
1170
1350
  {
1171
- initialPhase: type,
1172
- title,
1173
- type,
1174
- description: options?.description,
1175
- action: options?.action,
1176
- icon: options?.icon,
1177
- classNames: options?.classNames,
1178
- fillColor: options?.fillColor,
1179
- borderColor: options?.borderColor,
1180
- borderWidth: options?.borderWidth,
1181
- timing: options?.timing,
1182
- spring: options?.spring,
1183
- bounce: options?.bounce,
1184
- toastId: hasExpandedContent ? toastId : void 0
1351
+ duration,
1352
+ id: toastId
1185
1353
  }
1186
- ),
1187
- {
1188
- duration,
1189
- id: toastId
1354
+ );
1355
+ };
1356
+ if (_activeIds.size < getGoeyVisibleToasts()) {
1357
+ _activeIds.add(toastId);
1358
+ create();
1359
+ } else {
1360
+ _queue.push({ id: toastId, create });
1361
+ }
1362
+ return toastId;
1363
+ }
1364
+ function dismissGoeyToast(id) {
1365
+ if (id != null) {
1366
+ const idx = _queue.findIndex((q) => q.id === id);
1367
+ if (idx !== -1) {
1368
+ _queue.splice(idx, 1);
1369
+ return;
1190
1370
  }
1191
- );
1371
+ sonner.toast.dismiss(id);
1372
+ } else {
1373
+ _queue.length = 0;
1374
+ _activeIds.clear();
1375
+ sonner.toast.dismiss();
1376
+ }
1192
1377
  }
1193
1378
  var goeyToast = Object.assign(
1194
1379
  (title, options) => createGoeyToast(title, "default", options),
@@ -1199,12 +1384,21 @@ var goeyToast = Object.assign(
1199
1384
  info: (title, options) => createGoeyToast(title, "info", options),
1200
1385
  promise: (promise, data) => {
1201
1386
  const id = Math.random().toString(36).slice(2);
1202
- return sonner.toast.custom(() => /* @__PURE__ */ jsxRuntime.jsx(PromiseToastWrapper, { promise, data, toastId: id }), {
1203
- id,
1204
- duration: data.timing?.displayDuration != null || data.description ? Infinity : void 0
1205
- });
1387
+ const create = () => {
1388
+ sonner.toast.custom(() => /* @__PURE__ */ jsxRuntime.jsx(PromiseToastWrapper, { promise, data, toastId: id }), {
1389
+ id,
1390
+ duration: data.timing?.displayDuration != null || data.description ? Infinity : void 0
1391
+ });
1392
+ };
1393
+ if (_activeIds.size < getGoeyVisibleToasts()) {
1394
+ _activeIds.add(id);
1395
+ create();
1396
+ } else {
1397
+ _queue.push({ id, create });
1398
+ }
1399
+ return id;
1206
1400
  },
1207
- dismiss: sonner.toast.dismiss
1401
+ dismiss: dismissGoeyToast
1208
1402
  }
1209
1403
  );
1210
1404