goey-toast 0.3.0 → 0.4.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/README.md CHANGED
@@ -23,7 +23,8 @@
23
23
  - Dismiss by type filter: `gooeyToast.dismiss({ type: 'error' })`
24
24
  - Dark mode and RTL layout support
25
25
  - Animation presets: smooth, bouncy, subtle, snappy
26
- - Timestamp display on expanded toasts
26
+ - Timestamp display on expanded toasts with optional `showTimestamp` toggle
27
+ - Close button with configurable position (`top-left` / `top-right`)
27
28
  - Countdown progress bar with hover-pause and re-expand
28
29
  - Keyboard dismiss (Escape) and swipe-to-dismiss on mobile
29
30
  - Toast queue with configurable overflow strategy
@@ -164,6 +165,7 @@ Options passed as the second argument to `gooeyToast()` and type-specific method
164
165
  | `timing` | `GooeyToastTimings` | Animation timing overrides |
165
166
  | `spring` | `boolean` | Enable spring/bounce animations (default `true`) |
166
167
  | `bounce` | `number` | Spring intensity from `0.05` (subtle) to `0.8` (dramatic), default `0.4` |
168
+ | `showTimestamp` | `boolean` | Show/hide timestamp in toast header/body (default `true`) |
167
169
  | `showProgress`| `boolean` | Show countdown progress bar |
168
170
  | `onDismiss` | `(id) => void` | Called when toast is dismissed (any reason) |
169
171
  | `onAutoClose` | `(id) => void` | Called only on timer-based auto-dismiss |
@@ -216,11 +218,13 @@ Props for the `<GooeyToaster />` component.
216
218
  | `bounce` | `number` | `0.4` | Spring intensity: `0.05` (subtle) to `0.8` (dramatic) |
217
219
  | `preset` | `AnimationPresetName` | -- | Animation preset for all toasts |
218
220
  | `closeOnEscape` | `boolean` | `true` | Dismiss most recent toast on Escape key |
221
+ | `closeButton` | `boolean \| 'top-left' \| 'top-right'` | `false` | Show close button on hover |
219
222
  | `showProgress` | `boolean` | `false` | Show countdown progress bar on all toasts |
220
223
  | `maxQueue` | `number` | `Infinity` | Maximum queued toasts |
221
224
  | `queueOverflow` | `'drop-oldest' \| 'drop-newest'` | `'drop-oldest'` | Queue overflow strategy |
222
225
  | `dir` | `'ltr' \| 'rtl'` | `'ltr'` | Layout direction |
223
226
  | `swipeToDismiss` | `boolean` | `true` | Enable swipe-to-dismiss on mobile |
227
+ | `showTimestamp` | `boolean` | `true` | Show/hide timestamp on all toasts globally |
224
228
 
225
229
  ### `GooeyPromiseData<T>`
226
230
 
@@ -422,6 +426,28 @@ Press **Escape** to dismiss the most recent toast. Enabled by default; disable w
422
426
 
423
427
  On mobile, swipe toasts to dismiss them. Enabled by default; disable with `swipeToDismiss={false}`.
424
428
 
429
+ ### Close Button
430
+
431
+ Show a close button on hover. Position it `top-left` (default) or `top-right`:
432
+
433
+ ```tsx
434
+ <GooeyToaster closeButton />
435
+ <GooeyToaster closeButton="top-left" />
436
+ <GooeyToaster closeButton="top-right" />
437
+ ```
438
+
439
+ The close button inherits the toast's border and fill color styling. Hidden during the loading phase of promise toasts.
440
+
441
+ ### Hiding Timestamps
442
+
443
+ ```tsx
444
+ // Per-toast: hide timestamp for this toast only
445
+ gooeyToast.success('Saved', { showTimestamp: false })
446
+
447
+ // Globally: hide timestamp for all toasts
448
+ <GooeyToaster showTimestamp={false} />
449
+ ```
450
+
425
451
  ## Exports
426
452
 
427
453
  ```ts
@@ -458,6 +484,10 @@ goey-toast works in all modern browsers that support:
458
484
  - ResizeObserver
459
485
  - `framer-motion` (Chrome, Firefox, Safari, Edge)
460
486
 
487
+ ## See Also
488
+
489
+ - **[gooey-search-tabs](https://github.com/anl331/gooey-search-tabs)** — A morphing search bar with animated tab navigation for React. [Live Demo](https://gooey-search-tabs.vercel.app)
490
+
461
491
  ## License
462
492
 
463
493
  [MIT](./LICENSE)
package/dist/index.cjs CHANGED
@@ -86,6 +86,20 @@ function setGooeyShowProgress(show) {
86
86
  function getGooeyShowProgress() {
87
87
  return _showProgress;
88
88
  }
89
+ var _showTimestamp = true;
90
+ function setGooeyShowTimestamp(show) {
91
+ _showTimestamp = show;
92
+ }
93
+ function getGooeyShowTimestamp() {
94
+ return _showTimestamp;
95
+ }
96
+ var _closeButton = false;
97
+ function setGooeyCloseButton(value) {
98
+ _closeButton = value;
99
+ }
100
+ function getGooeyCloseButton() {
101
+ return _closeButton;
102
+ }
89
103
  var _containerHovered = false;
90
104
  var _hoverSubs = /* @__PURE__ */ new Set();
91
105
  function setContainerHovered(hovered) {
@@ -282,7 +296,9 @@ var styles = {
282
296
  progressWarning: "gooey-progressWarning",
283
297
  progressInfo: "gooey-progressInfo",
284
298
  progressPaused: "gooey-progressPaused",
285
- timestamp: "gooey-timestamp"
299
+ timestamp: "gooey-timestamp",
300
+ closeButton: "gooey-closeButton",
301
+ closeButtonRight: "gooey-closeButtonRight"
286
302
  };
287
303
  var useIsomorphicLayoutEffect = typeof window !== "undefined" ? react.useLayoutEffect : react.useEffect;
288
304
  var phaseIconMap = {
@@ -332,23 +348,23 @@ function registerSonnerObserver(ol, callback) {
332
348
  let entry = observerRegistry.get(ol);
333
349
  if (!entry) {
334
350
  const callbacks = /* @__PURE__ */ new Set();
335
- let applying = false;
336
- const observer = new MutationObserver(() => {
337
- if (applying) return;
338
- applying = true;
339
- requestAnimationFrame(() => {
340
- callbacks.forEach((cb) => cb());
341
- requestAnimationFrame(() => {
342
- applying = false;
343
- });
344
- });
345
- });
346
- observer.observe(ol, {
351
+ const observeOptions = {
347
352
  attributes: true,
348
353
  attributeFilter: ["style", "data-visible"],
349
354
  subtree: true,
350
355
  childList: true
356
+ };
357
+ const observer = new MutationObserver(() => {
358
+ observer.disconnect();
359
+ try {
360
+ for (const cb of callbacks) {
361
+ cb();
362
+ }
363
+ } finally {
364
+ observer.observe(ol, observeOptions);
365
+ }
351
366
  });
367
+ observer.observe(ol, observeOptions);
352
368
  entry = { observer, callbacks };
353
369
  observerRegistry.set(ol, entry);
354
370
  }
@@ -376,17 +392,10 @@ function syncSonnerHeights(wrapperEl, includeOffsets = false) {
376
392
  const h = content ? content.getBoundingClientRect().height : 0;
377
393
  return h > 0 ? h : PH;
378
394
  });
379
- const isExpanded = includeOffsets && toasts[0]?.getAttribute("data-expanded") === "true";
380
- if (isExpanded) {
381
- for (const t of toasts) t.style.setProperty("transition", "none", "important");
382
- }
383
395
  for (let i = 0; i < toasts.length; i++) {
384
396
  toasts[i].style.setProperty("--initial-height", `${heights[i]}px`);
385
397
  }
386
398
  if (!includeOffsets) {
387
- if (isExpanded) {
388
- for (const t of toasts) t.style.removeProperty("transition");
389
- }
390
399
  return;
391
400
  }
392
401
  const gapStr = getComputedStyle(ol).getPropertyValue("--gap").trim();
@@ -402,10 +411,6 @@ function syncSonnerHeights(wrapperEl, includeOffsets = false) {
402
411
  runningOffset += heights[i] + gap;
403
412
  }
404
413
  }
405
- if (isExpanded) {
406
- void ol.offsetHeight;
407
- for (const t of toasts) t.style.removeProperty("transition");
408
- }
409
414
  }
410
415
  function memoizePath(fn) {
411
416
  let lastArgs = null;
@@ -536,10 +541,13 @@ var GooeyToast = ({
536
541
  preset,
537
542
  spring: springProp,
538
543
  bounce: bounceProp,
544
+ showTimestamp: showTimestampProp,
539
545
  showProgress: showProgressProp,
540
546
  toastId
541
547
  }) => {
542
548
  const theme = getGooeyTheme();
549
+ const closeButtonSetting = getGooeyCloseButton();
550
+ const showCloseButton = closeButtonSetting !== false;
543
551
  const fillColor = fillColorProp ?? (theme === "dark" ? "#1a1a1a" : "#ffffff");
544
552
  const position = getGooeyPosition();
545
553
  const dir = getGooeyDir();
@@ -551,6 +559,7 @@ var GooeyToast = ({
551
559
  const useSpring = springProp ?? presetConfig?.spring ?? getGooeySpring();
552
560
  const bounceVal = bounceProp ?? presetConfig?.bounce ?? getGooeyBounce() ?? 0.4;
553
561
  const showProgress = showProgressProp ?? getGooeyShowProgress();
562
+ const showTimestamp = showTimestampProp ?? getGooeyShowTimestamp();
554
563
  const [actionSuccess, setActionSuccess] = react.useState(null);
555
564
  const [dismissing, setDismissing] = react.useState(false);
556
565
  const [progressKey, setProgressKey] = react.useState(0);
@@ -1204,6 +1213,30 @@ var GooeyToast = ({
1204
1213
  )
1205
1214
  }
1206
1215
  ),
1216
+ showCloseButton && effectivePhase !== "loading" && /* @__PURE__ */ jsxRuntime.jsx(
1217
+ "button",
1218
+ {
1219
+ className: `${styles.closeButton}${(isRight ? closeButtonSetting !== "top-right" : closeButtonSetting === "top-right") ? ` ${styles.closeButtonRight}` : ""}`,
1220
+ "aria-label": "Close toast",
1221
+ type: "button",
1222
+ style: {
1223
+ background: fillColor,
1224
+ borderColor: borderColor || "transparent",
1225
+ borderWidth: borderColor ? borderWidth ?? 1.5 : 0,
1226
+ boxShadow: borderColor ? "none" : "0 1px 4px rgba(0, 0, 0, 0.2)",
1227
+ ...isCenter && closeButtonSetting !== "top-right" ? { top: 6, left: -1 } : {}
1228
+ },
1229
+ onClick: (e) => {
1230
+ e.stopPropagation();
1231
+ const id = toastId;
1232
+ if (id != null) sonner.toast.dismiss(id);
1233
+ },
1234
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1235
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
1236
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
1237
+ ] })
1238
+ }
1239
+ ),
1207
1240
  /* @__PURE__ */ jsxRuntime.jsxs(
1208
1241
  "div",
1209
1242
  {
@@ -1213,9 +1246,9 @@ var GooeyToast = ({
1213
1246
  children: [
1214
1247
  /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: headerRef, className: `${styles.header} ${titleColorMap[effectivePhase]}${classNames?.header ? ` ${classNames.header}` : ""}`, children: [
1215
1248
  iconAndTitle,
1216
- !hasDescription && !hasAction && !actionSuccess && /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.timestamp, children: timestampStr })
1249
+ !hasDescription && !hasAction && !actionSuccess && showTimestamp && /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.timestamp, children: timestampStr })
1217
1250
  ] }),
1218
- /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: showBody && hasDescription && !dismissing && /* @__PURE__ */ jsxRuntime.jsxs(
1251
+ /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: showBody && hasDescription && !dismissing && /* @__PURE__ */ jsxRuntime.jsx(
1219
1252
  framerMotion.motion.div,
1220
1253
  {
1221
1254
  className: `${styles.description}${classNames?.description ? ` ${classNames.description}` : ""}`,
@@ -1224,14 +1257,14 @@ var GooeyToast = ({
1224
1257
  animate: { opacity: 1 },
1225
1258
  exit: { opacity: 0 },
1226
1259
  transition: prefersReducedMotion ? { duration: 0.01 } : { duration: 0.35, ease: [0.4, 0, 0.2, 1] },
1227
- children: [
1228
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.timestamp, style: { float: "right", marginLeft: 10, marginTop: 3, paddingLeft: 0 }, children: timestampStr }),
1229
- effectiveDescription
1230
- ]
1260
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "flex-start", gap: "10px" }, children: [
1261
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1, minWidth: 0 }, children: effectiveDescription }),
1262
+ showTimestamp && /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.timestamp, children: timestampStr })
1263
+ ] })
1231
1264
  },
1232
1265
  "description"
1233
1266
  ) }),
1234
- /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: showBody && !hasDescription && hasAction && !dismissing && /* @__PURE__ */ jsxRuntime.jsx(
1267
+ /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: showBody && !hasDescription && hasAction && !dismissing && showTimestamp && /* @__PURE__ */ jsxRuntime.jsx(
1235
1268
  framerMotion.motion.div,
1236
1269
  {
1237
1270
  className: styles.timestamp,
@@ -1397,22 +1430,23 @@ function GooeyToastWrapper({
1397
1430
  spring,
1398
1431
  bounce,
1399
1432
  showProgress,
1433
+ showTimestamp: initialShowTimestamp,
1400
1434
  toastId,
1401
- activeId,
1402
1435
  onDismiss,
1403
1436
  onAutoClose
1404
1437
  }) {
1405
1438
  react.useEffect(() => {
1406
1439
  if (onDismiss || onAutoClose) {
1407
- _toastCallbacks.set(activeId, { onDismiss, onAutoClose });
1440
+ _toastCallbacks.set(toastId, { onDismiss, onAutoClose });
1408
1441
  }
1409
- }, [activeId, onDismiss, onAutoClose]);
1442
+ }, [toastId, onDismiss, onAutoClose]);
1410
1443
  const [title, setTitle] = react.useState(initialTitle);
1411
1444
  const [type, setType] = react.useState(initialType);
1412
1445
  const [phase, setPhase] = react.useState(initialPhase);
1413
1446
  const [description, setDescription] = react.useState(initialDescription);
1414
1447
  const [action, setAction] = react.useState(initialAction);
1415
1448
  const [currentIcon, setCurrentIcon] = react.useState(icon);
1449
+ const [showTimestamp, setShowTimestamp] = react.useState(initialShowTimestamp ?? true);
1416
1450
  react.useEffect(() => {
1417
1451
  const handleUpdate = (opts) => {
1418
1452
  if (opts.title !== void 0) setTitle(opts.title);
@@ -1423,22 +1457,23 @@ function GooeyToastWrapper({
1423
1457
  }
1424
1458
  if (opts.action !== void 0) setAction(opts.action);
1425
1459
  if ("icon" in opts) setCurrentIcon(opts.icon ?? void 0);
1460
+ if (opts.showTimestamp !== void 0) setShowTimestamp(opts.showTimestamp);
1426
1461
  };
1427
- _toastUpdateListeners.set(activeId, handleUpdate);
1462
+ _toastUpdateListeners.set(toastId, handleUpdate);
1428
1463
  return () => {
1429
- _toastUpdateListeners.delete(activeId);
1464
+ _toastUpdateListeners.delete(toastId);
1430
1465
  };
1431
- }, [activeId]);
1466
+ }, [toastId]);
1432
1467
  const mountedRef = react.useRef(true);
1433
1468
  react.useEffect(() => {
1434
1469
  mountedRef.current = true;
1435
1470
  return () => {
1436
1471
  mountedRef.current = false;
1437
1472
  setTimeout(() => {
1438
- if (!mountedRef.current) _onToastDismissed(activeId);
1473
+ if (!mountedRef.current) _onToastDismissed(toastId);
1439
1474
  }, 100);
1440
1475
  };
1441
- }, [activeId]);
1476
+ }, [toastId]);
1442
1477
  return /* @__PURE__ */ jsxRuntime.jsx(ToastErrorBoundary, { children: /* @__PURE__ */ jsxRuntime.jsx(
1443
1478
  GooeyToast,
1444
1479
  {
@@ -1457,6 +1492,7 @@ function GooeyToastWrapper({
1457
1492
  spring,
1458
1493
  bounce,
1459
1494
  showProgress,
1495
+ showTimestamp,
1460
1496
  toastId
1461
1497
  }
1462
1498
  ) });
@@ -1529,7 +1565,9 @@ function PromiseToastWrapper({
1529
1565
  timing: data.timing,
1530
1566
  preset: data.preset,
1531
1567
  spring: data.spring,
1532
- bounce: data.bounce
1568
+ bounce: data.bounce,
1569
+ showTimestamp: data.showTimestamp ?? true,
1570
+ toastId
1533
1571
  }
1534
1572
  ) });
1535
1573
  }
@@ -1558,8 +1596,8 @@ function createGooeyToast(title, type, options) {
1558
1596
  spring: options?.spring,
1559
1597
  bounce: options?.bounce,
1560
1598
  showProgress: options?.showProgress,
1561
- toastId: hasExpandedContent ? toastId : void 0,
1562
- activeId: toastId,
1599
+ showTimestamp: options?.showTimestamp,
1600
+ toastId,
1563
1601
  onDismiss: options?.onDismiss,
1564
1602
  onAutoClose: options?.onAutoClose
1565
1603
  }
@@ -1726,7 +1764,8 @@ function GooeyToaster({
1726
1764
  closeOnEscape = true,
1727
1765
  maxQueue = Infinity,
1728
1766
  queueOverflow = "drop-oldest",
1729
- showProgress = false
1767
+ showProgress = false,
1768
+ showTimestamp = true
1730
1769
  }) {
1731
1770
  const presetConfig = preset ? animationPresets[preset] : void 0;
1732
1771
  const resolvedSpring = spring ?? presetConfig?.spring ?? true;
@@ -1776,6 +1815,12 @@ function GooeyToaster({
1776
1815
  react.useEffect(() => {
1777
1816
  setGooeyShowProgress(showProgress);
1778
1817
  }, [showProgress]);
1818
+ react.useEffect(() => {
1819
+ setGooeyCloseButton(closeButton ?? false);
1820
+ }, [closeButton]);
1821
+ react.useEffect(() => {
1822
+ setGooeyShowTimestamp(showTimestamp);
1823
+ }, [showTimestamp]);
1779
1824
  react.useEffect(() => {
1780
1825
  let expandObs = null;
1781
1826
  let currentOl = null;
@@ -1845,7 +1890,7 @@ function GooeyToaster({
1845
1890
  theme,
1846
1891
  toastOptions: { unstyled: true, ...toastOptions },
1847
1892
  expand,
1848
- closeButton,
1893
+ closeButton: false,
1849
1894
  richColors,
1850
1895
  visibleToasts: 99,
1851
1896
  dir
@@ -1855,8 +1900,10 @@ function GooeyToaster({
1855
1900
  ] });
1856
1901
  }
1857
1902
 
1903
+ exports.GoeyToaster = GooeyToaster;
1858
1904
  exports.GooeyToaster = GooeyToaster;
1859
1905
  exports.animationPresets = animationPresets;
1906
+ exports.goeyToast = gooeyToast;
1860
1907
  exports.gooeyToast = gooeyToast;
1861
1908
  //# sourceMappingURL=index.cjs.map
1862
1909
  //# sourceMappingURL=index.cjs.map