@assure-one/design-system 1.13.0 → 1.14.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.d.ts CHANGED
@@ -2320,12 +2320,20 @@ interface ToastAction {
2320
2320
  }
2321
2321
  /** Options accepted by `toast()` and (partially) by `update()`. */
2322
2322
  interface ToastOptions {
2323
- title: React.ReactNode;
2323
+ title?: React.ReactNode;
2324
2324
  description?: React.ReactNode;
2325
2325
  variant?: ToastVariant;
2326
2326
  /** Auto-dismiss after N ms. `0` = sticky. `loading` defaults to sticky. */
2327
2327
  duration?: number;
2328
2328
  action?: ToastAction;
2329
+ /**
2330
+ * Rich-variant escape hatch. When provided, the toast renders `content`
2331
+ * inside its surface instead of the title/chip/countdown layout — the
2332
+ * content owns its own structure and dismissal. Pass a render function to
2333
+ * receive `close` (e.g. a review queue that dismisses itself once empty).
2334
+ * Rich-content toasts are sticky by default (no auto-dismiss countdown).
2335
+ */
2336
+ content?: React.ReactNode | ((close: () => void) => React.ReactNode);
2329
2337
  /**
2330
2338
  * When set, a new toast with the same key updates the existing one in place
2331
2339
  * instead of stacking a duplicate (e.g. repeated "Saving…" feedback).
package/dist/index.js CHANGED
@@ -9493,19 +9493,17 @@ function ToastProvider({ children }) {
9493
9493
  setToasts((prev) => prev.filter((t) => t.id !== id));
9494
9494
  }, []);
9495
9495
  const addToast = useCallback((options) => {
9496
- const variant = options.variant ?? "default";
9497
- const duration = options.duration ?? (variant === "loading" ? 0 : DEFAULT_DURATION);
9498
9496
  const id = `toast-${++toastCount}`;
9499
9497
  setToasts((prev) => {
9500
9498
  if (options.dedupeKey) {
9501
9499
  const existing = prev.find((t) => t.dedupeKey === options.dedupeKey);
9502
9500
  if (existing) {
9503
9501
  return prev.map(
9504
- (t) => t.dedupeKey === options.dedupeKey ? { ...t, ...options, variant, duration, seq: t.seq + 1 } : t
9502
+ (t) => t.dedupeKey === options.dedupeKey ? { ...t, ...options, seq: t.seq + 1 } : t
9505
9503
  );
9506
9504
  }
9507
9505
  }
9508
- return [...prev, { id, seq: 0, ...options, variant, duration }];
9506
+ return [...prev, { id, seq: 0, ...options }];
9509
9507
  });
9510
9508
  return id;
9511
9509
  }, []);
@@ -9513,9 +9511,8 @@ function ToastProvider({ children }) {
9513
9511
  setToasts(
9514
9512
  (prev) => prev.map((t) => {
9515
9513
  if (t.id !== id) return t;
9516
- const variant = patch.variant ?? t.variant;
9517
9514
  const duration = patch.duration ?? (patch.variant && patch.variant !== "loading" ? DEFAULT_DURATION : t.duration);
9518
- return { ...t, ...patch, variant, duration, seq: t.seq + 1 };
9515
+ return { ...t, ...patch, duration, seq: t.seq + 1 };
9519
9516
  })
9520
9517
  );
9521
9518
  }, []);
@@ -9556,79 +9553,89 @@ function ToastProvider({ children }) {
9556
9553
  ] });
9557
9554
  }
9558
9555
  var variantConfig = {
9559
- default: { chip: "bg-pro-bg", icon: "text-pro-fg", bar: "bg-pro-fg", Icon: InfoCircleSolidIcon },
9556
+ default: {
9557
+ chip: "bg-pro-bg",
9558
+ icon: "text-pro-fg",
9559
+ stroke: "stroke-pro-fg",
9560
+ ring: "ring-pro-fg/15",
9561
+ Icon: InfoCircleSolidIcon
9562
+ },
9560
9563
  success: {
9561
9564
  chip: "bg-success-bg",
9562
9565
  icon: "text-success-fg",
9563
- bar: "bg-success-fg",
9566
+ stroke: "stroke-success-fg",
9567
+ ring: "ring-success-fg/15",
9564
9568
  Icon: CheckCircleSolidIcon
9565
9569
  },
9566
9570
  destructive: {
9567
9571
  chip: "bg-danger-bg",
9568
9572
  icon: "text-danger-fg",
9569
- bar: "bg-danger-fg",
9573
+ stroke: "stroke-danger-fg",
9574
+ ring: "ring-danger-fg/15",
9570
9575
  Icon: XCircleSolidIcon
9571
9576
  },
9572
9577
  warning: {
9573
9578
  chip: "bg-warning-bg",
9574
9579
  icon: "text-warning-fg",
9575
- bar: "bg-warning-fg",
9580
+ stroke: "stroke-warning-fg",
9581
+ ring: "ring-warning-fg/20",
9576
9582
  Icon: AlertTriangleSolidIcon
9577
9583
  },
9578
9584
  info: {
9579
9585
  chip: "bg-info/12",
9580
9586
  icon: "text-info",
9581
- bar: "bg-info",
9587
+ stroke: "stroke-info",
9588
+ ring: "ring-info/15",
9582
9589
  Icon: InfoCircleSolidIcon
9583
9590
  },
9584
9591
  loading: {
9585
9592
  chip: "bg-pro-bg",
9586
9593
  icon: "text-pro-fg",
9587
- bar: "bg-pro-fg",
9594
+ stroke: "stroke-pro-fg",
9595
+ ring: "ring-pro-fg/15",
9588
9596
  Icon: LoaderIcon,
9589
9597
  spin: true
9590
9598
  }
9591
9599
  };
9600
+ var RING_RADIUS2 = 11;
9601
+ var RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS2;
9592
9602
  var SWIPE_DISMISS_PX = 72;
9593
9603
  function ToastItem({ toast, onDismiss }) {
9594
- const [exiting, setExiting] = useState(false);
9604
+ const timerRef = useRef(void 0);
9605
+ const remainingRef = useRef(toast.duration ?? DEFAULT_DURATION);
9606
+ const startedAtRef = useRef(0);
9595
9607
  const cardRef = useRef(null);
9596
- const barRef = useRef(null);
9597
- const pausedRef = useRef(false);
9598
- const duration = toast.duration;
9599
- const showProgress = duration > 0;
9608
+ const [exiting, setExiting] = useState(false);
9609
+ const [paused, setPaused] = useState(false);
9610
+ const hasContent = toast.content != null;
9611
+ const duration = toast.duration ?? DEFAULT_DURATION;
9612
+ const isLoading = (toast.variant ?? "default") === "loading";
9613
+ const autoDismiss = duration > 0 && !hasContent && !isLoading;
9614
+ const isError = (toast.variant ?? "default") === "destructive";
9600
9615
  const close = useCallback(() => {
9601
9616
  setExiting(true);
9602
9617
  setTimeout(() => onDismiss(toast.id), 150);
9603
9618
  }, [toast.id, onDismiss]);
9604
9619
  useEffect(() => {
9605
- if (!showProgress) return;
9606
- let raf = 0;
9607
- let remaining = duration;
9608
- let last = performance.now();
9609
- const tick = (now) => {
9610
- const dt = now - last;
9611
- last = now;
9612
- if (!pausedRef.current) {
9613
- remaining -= dt;
9614
- if (barRef.current) {
9615
- barRef.current.style.width = `${Math.max(0, Math.min(100, (duration - remaining) / duration * 100))}%`;
9616
- }
9617
- if (remaining <= 0) {
9618
- close();
9619
- return;
9620
- }
9621
- }
9622
- raf = requestAnimationFrame(tick);
9620
+ remainingRef.current = toast.duration ?? DEFAULT_DURATION;
9621
+ }, [toast.seq, toast.duration]);
9622
+ useEffect(() => {
9623
+ if (!autoDismiss || exiting || paused) return;
9624
+ startedAtRef.current = Date.now();
9625
+ timerRef.current = setTimeout(close, remainingRef.current);
9626
+ return () => {
9627
+ clearTimeout(timerRef.current);
9628
+ remainingRef.current = Math.max(
9629
+ 0,
9630
+ remainingRef.current - (Date.now() - startedAtRef.current)
9631
+ );
9623
9632
  };
9624
- raf = requestAnimationFrame(tick);
9625
- return () => cancelAnimationFrame(raf);
9626
- }, [duration, showProgress, close, toast.seq]);
9633
+ }, [autoDismiss, exiting, paused, close, toast.seq]);
9627
9634
  const dragRef = useRef(null);
9628
9635
  const onPointerDown = (e) => {
9629
- if (e.target.closest("button")) return;
9636
+ if (e.target.closest("button, a, input, textarea")) return;
9630
9637
  dragRef.current = { startX: e.clientX, dx: 0 };
9631
- pausedRef.current = true;
9638
+ setPaused(true);
9632
9639
  cardRef.current?.setPointerCapture(e.pointerId);
9633
9640
  };
9634
9641
  const onPointerMove = (e) => {
@@ -9649,101 +9656,151 @@ function ToastItem({ toast, onDismiss }) {
9649
9656
  }
9650
9657
  cardRef.current.style.transform = "";
9651
9658
  cardRef.current.style.opacity = "";
9652
- pausedRef.current = false;
9659
+ setPaused(false);
9653
9660
  };
9654
- const { chip, icon, bar, Icon: Icon3, spin } = variantConfig[toast.variant];
9655
- return (
9656
- // Pointer/hover handlers add pause-on-hover + swipe-to-dismiss as progressive
9657
- // enhancement only — keyboard/AT users dismiss via the explicit Dismiss
9658
- // button below, and the live region (role=status) still announces content.
9659
- // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
9660
- /* @__PURE__ */ jsxs(
9661
- "div",
9662
- {
9663
- ref: cardRef,
9664
- role: "status",
9665
- "data-state": exiting ? "closed" : "open",
9666
- onPointerDown,
9667
- onPointerMove,
9668
- onPointerUp: endDrag,
9669
- onPointerCancel: endDrag,
9670
- onMouseEnter: () => {
9671
- pausedRef.current = true;
9672
- },
9673
- onMouseLeave: () => {
9674
- if (!dragRef.current) pausedRef.current = false;
9675
- },
9676
- onFocusCapture: () => {
9677
- pausedRef.current = true;
9678
- },
9679
- onBlurCapture: () => {
9680
- if (!dragRef.current) pausedRef.current = false;
9681
- },
9682
- className: cn(
9683
- "bg-surface border-rule text-fg rounded-card shadow-card pointer-events-auto relative w-80 touch-none overflow-hidden border p-4",
9684
- "font-body select-none",
9685
- // Unified data-state animation (spec §5.27) — same model as Dialog/Tooltip:
9686
- // open → fade+slide in over --duration-overlay (200ms), ease-out-quart
9687
- // close → fade+slide out over --duration-fast (150ms), ease-in
9688
- "data-[state=open]:animate-in data-[state=open]:slide-in-from-right-8 data-[state=open]:fade-in-0",
9689
- "data-[state=open]:duration-[var(--duration-overlay)] data-[state=open]:ease-[var(--ease-out-quart)]",
9690
- "data-[state=closed]:animate-out data-[state=closed]:slide-out-to-right-8 data-[state=closed]:fade-out-0",
9691
- "data-[state=closed]:duration-[var(--duration-fast)] data-[state=closed]:ease-[var(--ease-in)]",
9692
- "motion-reduce:animate-none"
9661
+ const { chip, icon, stroke, ring, Icon: Icon3, spin } = variantConfig[toast.variant ?? "default"];
9662
+ return /* @__PURE__ */ jsx(
9663
+ "div",
9664
+ {
9665
+ ref: cardRef,
9666
+ role: hasContent ? "group" : isError ? "alert" : "status",
9667
+ "aria-atomic": hasContent ? void 0 : true,
9668
+ "data-state": exiting ? "closed" : "open",
9669
+ onPointerEnter: () => setPaused(true),
9670
+ onPointerLeave: () => {
9671
+ if (!dragRef.current) setPaused(false);
9672
+ },
9673
+ onPointerDown,
9674
+ onPointerMove,
9675
+ onPointerUp: endDrag,
9676
+ onPointerCancel: endDrag,
9677
+ onFocusCapture: () => setPaused(true),
9678
+ onBlurCapture: () => {
9679
+ if (!dragRef.current) setPaused(false);
9680
+ },
9681
+ className: cn(
9682
+ "bg-surface border-rule text-fg rounded-card shadow-card pointer-events-auto relative w-80 max-w-[calc(100vw-2rem)] touch-none overflow-hidden border",
9683
+ // Title-mode padding; rich content owns its own internal padding.
9684
+ hasContent ? "" : "p-4",
9685
+ "font-body select-none",
9686
+ // Tactile lift on hover/focus — signals the card is interactive and
9687
+ // that its countdown is held. Shadow only (not motion), so it needs no
9688
+ // reduced-motion fallback; the named property keeps us off transition-all.
9689
+ "transition-shadow duration-[var(--duration-fast)] ease-[var(--ease-out-quart)]",
9690
+ "hover:shadow-lg focus-within:shadow-lg motion-reduce:transition-none",
9691
+ // Unified data-state animation (spec §5.27) — same model as Dialog/Tooltip,
9692
+ // with a subtle zoom so the toast *pops* in rather than just appearing:
9693
+ // open → fade + slide-from-right + zoom-from-95 over --duration-normal (250ms), ease-out-quart
9694
+ // close → fade + slide-to-right + zoom-to-95 over --duration-fast (150ms), ease-in
9695
+ "data-[state=open]:animate-in data-[state=open]:slide-in-from-right-8 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
9696
+ "data-[state=open]:duration-[var(--duration-normal)] data-[state=open]:ease-[var(--ease-out-quart)]",
9697
+ "data-[state=closed]:animate-out data-[state=closed]:slide-out-to-right-8 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
9698
+ "data-[state=closed]:duration-[var(--duration-fast)] data-[state=closed]:ease-[var(--ease-in)]",
9699
+ "motion-reduce:animate-none"
9700
+ ),
9701
+ children: hasContent ? typeof toast.content === "function" ? toast.content(close) : toast.content : /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
9702
+ /* @__PURE__ */ jsx(
9703
+ "span",
9704
+ {
9705
+ className: cn(
9706
+ // Same-hue inset ring gives the tinted chip a crafted edge instead
9707
+ // of a flat fill — definition without a heavy border.
9708
+ "inline-flex size-8 shrink-0 items-center justify-center rounded-[9px] ring-1 ring-inset",
9709
+ // Staggered pop — chip scales in ~80ms after the card lands so the
9710
+ // status glyph "arrives". motion-safe only; reduced motion shows it
9711
+ // at rest with no entrance.
9712
+ "motion-safe:animate-[toast-chip-in_280ms_var(--ease-out-quart)_80ms_both]",
9713
+ chip,
9714
+ icon,
9715
+ ring
9716
+ ),
9717
+ "aria-hidden": "true",
9718
+ children: /* @__PURE__ */ jsx(Icon3, { className: cn("size-5", spin && "animate-spin motion-reduce:animate-none") })
9719
+ }
9693
9720
  ),
9694
- children: [
9695
- /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
9696
- /* @__PURE__ */ jsx(
9697
- "span",
9698
- {
9699
- className: cn(
9700
- "inline-flex size-8 shrink-0 items-center justify-center rounded-[9px]",
9701
- chip,
9702
- icon
9703
- ),
9704
- "aria-hidden": "true",
9705
- children: /* @__PURE__ */ jsx(Icon3, { className: cn("size-5", spin && "animate-spin motion-reduce:animate-none") })
9706
- }
9721
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
9722
+ toast.title && /* @__PURE__ */ jsx("p", { className: "text-fg text-sm leading-tight font-semibold", children: toast.title }),
9723
+ toast.description && /* @__PURE__ */ jsx("p", { className: "text-fg-3 mt-0.5 text-sm leading-snug", children: toast.description }),
9724
+ toast.action && /* @__PURE__ */ jsx(
9725
+ "button",
9726
+ {
9727
+ type: "button",
9728
+ onClick: () => {
9729
+ toast.action?.onClick();
9730
+ close();
9731
+ },
9732
+ className: cn(
9733
+ "text-pro-fg mt-2 text-sm font-semibold",
9734
+ "transition-colors duration-[var(--duration-fast)] ease-[var(--ease-out-quart)] hover:underline motion-reduce:transition-none",
9735
+ "focus-visible:[box-shadow:var(--shadow-focus-ring)] rounded-sm focus-visible:outline-none"
9736
+ ),
9737
+ children: toast.action.label
9738
+ }
9739
+ )
9740
+ ] }),
9741
+ /* @__PURE__ */ jsxs(
9742
+ "button",
9743
+ {
9744
+ type: "button",
9745
+ onClick: close,
9746
+ className: cn(
9747
+ // size-7 click target with a -m-1 offset so the glyph keeps its
9748
+ // top-right optical position while the hit area (and ring) grow.
9749
+ "text-fg-4 hover:text-fg-2 hover:bg-overlay-hover relative -mt-1 -mr-1 inline-flex size-7 shrink-0 items-center justify-center rounded-full",
9750
+ "transition-colors duration-[var(--duration-fast)] ease-[var(--ease-out-quart)] motion-reduce:transition-none",
9751
+ "focus-visible:[box-shadow:var(--shadow-focus-ring)] focus-visible:outline-none"
9707
9752
  ),
9708
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
9709
- /* @__PURE__ */ jsx("p", { className: "text-fg text-sm leading-tight font-semibold", children: toast.title }),
9710
- toast.description && /* @__PURE__ */ jsx("p", { className: "text-fg-3 mt-0.5 text-sm leading-snug", children: toast.description }),
9711
- toast.action && /* @__PURE__ */ jsx(
9712
- "button",
9753
+ "aria-label": "Dismiss",
9754
+ children: [
9755
+ autoDismiss && // Countdown drains as a ring hugging the dismiss button — the timer
9756
+ // and the close affordance share one spot. Pauses in lockstep with
9757
+ // the JS timer (animation-play-state). Hidden under reduced motion;
9758
+ // the timer still governs dismissal.
9759
+ /* @__PURE__ */ jsxs(
9760
+ "svg",
9713
9761
  {
9714
- type: "button",
9715
- onClick: () => {
9716
- toast.action?.onClick();
9717
- close();
9718
- },
9719
- className: cn(
9720
- "text-pro-fg mt-2 text-sm font-semibold",
9721
- "transition-colors duration-[var(--duration-fast)] ease-[var(--ease-out-quart)] hover:underline motion-reduce:transition-none",
9722
- "focus-visible:[box-shadow:var(--shadow-focus-ring)] rounded-sm focus-visible:outline-none"
9723
- ),
9724
- children: toast.action.label
9762
+ viewBox: "0 0 28 28",
9763
+ className: "pointer-events-none absolute inset-0 size-7 -rotate-90 motion-reduce:hidden",
9764
+ "aria-hidden": "true",
9765
+ children: [
9766
+ /* @__PURE__ */ jsx(
9767
+ "circle",
9768
+ {
9769
+ cx: "14",
9770
+ cy: "14",
9771
+ r: RING_RADIUS2,
9772
+ fill: "none",
9773
+ strokeWidth: "2",
9774
+ className: "stroke-rule"
9775
+ }
9776
+ ),
9777
+ /* @__PURE__ */ jsx(
9778
+ "circle",
9779
+ {
9780
+ cx: "14",
9781
+ cy: "14",
9782
+ r: RING_RADIUS2,
9783
+ fill: "none",
9784
+ strokeWidth: "2",
9785
+ strokeLinecap: "round",
9786
+ className: cn("opacity-90", stroke),
9787
+ style: {
9788
+ strokeDasharray: RING_CIRCUMFERENCE,
9789
+ "--toast-ring-circ": RING_CIRCUMFERENCE,
9790
+ animation: `toast-ring-drain ${duration}ms linear forwards`,
9791
+ animationPlayState: paused ? "paused" : "running"
9792
+ }
9793
+ }
9794
+ )
9795
+ ]
9725
9796
  }
9726
- )
9727
- ] }),
9728
- /* @__PURE__ */ jsx(
9729
- "button",
9730
- {
9731
- type: "button",
9732
- onClick: close,
9733
- className: cn(
9734
- "text-fg-4 hover:text-fg-2 rounded-icon -mr-1 inline-flex size-5 shrink-0 items-center justify-center",
9735
- "transition-colors duration-[var(--duration-fast)] ease-[var(--ease-out-quart)] motion-reduce:transition-none",
9736
- "focus-visible:[box-shadow:var(--shadow-focus-ring)] focus-visible:outline-none"
9737
- ),
9738
- "aria-label": "Dismiss",
9739
- children: /* @__PURE__ */ jsx(XIcon, { className: "size-3.5", "aria-hidden": "true" })
9740
- }
9741
- )
9742
- ] }),
9743
- showProgress && /* @__PURE__ */ jsx("div", { className: "bg-rule absolute inset-x-0 bottom-0 h-1", "aria-hidden": "true", children: /* @__PURE__ */ jsx("div", { ref: barRef, className: cn("h-full", bar), style: { width: "0%" } }) })
9744
- ]
9745
- }
9746
- )
9797
+ ),
9798
+ /* @__PURE__ */ jsx(XIcon, { className: "relative size-3.5", "aria-hidden": "true" })
9799
+ ]
9800
+ }
9801
+ )
9802
+ ] }) })
9803
+ }
9747
9804
  );
9748
9805
  }
9749
9806
  var itemVariants = cva(