@mediaradar/design-components 0.14.0 → 0.15.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.cts CHANGED
@@ -978,18 +978,29 @@ declare function StackedColumnPlaceholder({ height, width, legendRows, axis, }:
978
978
  * escapes both `overflow` clipping inside scrollable panels and the broken
979
979
  * `fixed` containing block a transformed ancestor creates.
980
980
  * @tokens rounded-control, border-border-default, bg-surface, shadow-overlay,
981
- * text-text-primary, bg-text-primary, text-surface
981
+ * text-text-primary, bg-text-primary, text-surface,
982
+ * duration-(--mr-motion-duration-interactive)
982
983
  * @variant surface - Default. Themed surface/border bubble -- matches the
983
984
  * Recharts chart tooltips this was originally built for.
984
985
  * @variant inverted - Solid dark pill (no border) -- for a tooltip sitting
985
986
  * over a nav rail or other icon-only surface, where the themed surface bubble
986
987
  * doesn't read as clearly against a busy background.
987
988
  * @behavior Shows on hover and on keyboard focus (so it stays reachable
988
- * without a pointer) after `delay` ms (0 = immediate, the default). Hides
989
- * immediately on mouseleave/blur, cancelling a pending delayed show if it
990
- * hasn't fired yet -- except in `interactive` mode, which delays the hide
991
- * briefly so the pointer can travel from the trigger into the tooltip's own
992
- * content. Pass `whenTruncated` to only surface it once the trigger's content
989
+ * without a pointer) after `delay` ms (0 = immediate, the default). Fades in
990
+ * and out (opacity, `ease-in-out`, over the sys
991
+ * `--mr-motion-duration-interactive` token -- 200ms today); the bubble stays
992
+ * mounted through the fade-out and is removed from the DOM once it ends.
993
+ * Under `prefers-reduced-motion: reduce` there is no fade: it appears and is
994
+ * removed instantly. The hide starts on mouseleave/blur, cancelling a pending
995
+ * delayed show if it hasn't fired yet -- except in `interactive` mode, which
996
+ * delays the hide briefly so the pointer can travel from the trigger into the
997
+ * tooltip's own content. Re-hovering (or re-focusing) the trigger while the
998
+ * bubble is still fading out reverses the fade from where it is, without
999
+ * waiting out `delay` again, since it is already on screen; so does moving
1000
+ * the pointer back into an `interactive` bubble. A non-interactive bubble is
1001
+ * `pointer-events-none` throughout, fade-out included, so it never
1002
+ * intercepts clicks meant for what sits under it. Pass `whenTruncated` to
1003
+ * only surface it once the trigger's content
993
1004
  * is actually clipped by ellipsis (`scrollWidth > clientWidth`). `placement`
994
1005
  * 'top'/'bottom' centre (or `align="start"` left-align) on the trigger;
995
1006
  * 'right' centres vertically beside the anchor and 'bottom-end' hangs below,
@@ -1402,9 +1413,20 @@ declare const buttonVariants: (props?: ({
1402
1413
  size?: "icon" | "sm" | "md" | "lg" | null | undefined;
1403
1414
  } & class_variance_authority_types.ClassProp) | undefined) => string;
1404
1415
  interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
1405
- /** Render the button styling onto the single child element instead of a `<button>` (e.g. a router `<Link>`). */
1416
+ /**
1417
+ * Render the button styling onto the single child element instead of a
1418
+ * `<button>` (e.g. a router `<Link>` or an `<a>`). The child must be exactly
1419
+ * one React element; it keeps its own tag, content and semantics.
1420
+ */
1406
1421
  asChild?: boolean;
1407
- /** Shows a spinner in place of the leading edge and disables the button. */
1422
+ /**
1423
+ * Shows a spinner in place of the leading edge and disables the button.
1424
+ * With `asChild` there is no spinner (the child owns its content, and a
1425
+ * spinner belongs inside a real `<button>`): `loading` only sets
1426
+ * `aria-busy` and forwards `disabled` to the child, which only has an
1427
+ * effect on elements that support it -- an `<a>` stays clickable, so block
1428
+ * navigation yourself while loading.
1429
+ */
1408
1430
  loading?: boolean;
1409
1431
  /** Button label and/or icon. With `asChild`, the single element that receives the button styling. */
1410
1432
  children?: ReactNode;
@@ -1430,8 +1452,13 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantPr
1430
1452
  * but Button suppresses it (`focus-visible:outline-hidden focus-visible:ring-0`)
1431
1453
  * and relies on its per-variant hover/brightness treatment instead. `loading`
1432
1454
  * implies `disabled` (and sets `aria-busy`), since a control mid-submit
1433
- * shouldn't accept another click.
1434
- * @a11y Renders a native <button>, so it keeps native keyboard behaviour.
1455
+ * shouldn't accept another click. `asChild` renders only the child element,
1456
+ * through Radix `Slot`, with the button classes, ref and remaining props
1457
+ * merged onto it -- no wrapping `<button>`. With `asChild`, `loading` draws
1458
+ * no spinner; it sets `aria-busy` and forwards `disabled`, which an `<a>`
1459
+ * ignores, so a loading link stays clickable unless the caller blocks it.
1460
+ * @a11y Renders a native <button>, so it keeps native keyboard behaviour
1461
+ * (with `asChild`, the child element's own semantics apply instead).
1435
1462
  * `loading` sets aria-busy, and the spinner is aria-hidden. Icon-only
1436
1463
  * usage needs an aria-label from the caller -- nothing here supplies one.
1437
1464
  * @usage
@@ -1440,6 +1467,7 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantPr
1440
1467
  * <Button variant="secondary" size="sm">Cancel</Button>
1441
1468
  * <Button variant="danger">Delete</Button>
1442
1469
  * <Button variant="primary" loading>Saving…</Button>
1470
+ * <Button asChild variant="secondary"><a href="/reports">View reports</a></Button>
1443
1471
  * ```
1444
1472
  */
1445
1473
  declare const Button: react.ForwardRefExoticComponent<ButtonProps & react.RefAttributes<HTMLButtonElement>>;
package/dist/index.d.ts CHANGED
@@ -978,18 +978,29 @@ declare function StackedColumnPlaceholder({ height, width, legendRows, axis, }:
978
978
  * escapes both `overflow` clipping inside scrollable panels and the broken
979
979
  * `fixed` containing block a transformed ancestor creates.
980
980
  * @tokens rounded-control, border-border-default, bg-surface, shadow-overlay,
981
- * text-text-primary, bg-text-primary, text-surface
981
+ * text-text-primary, bg-text-primary, text-surface,
982
+ * duration-(--mr-motion-duration-interactive)
982
983
  * @variant surface - Default. Themed surface/border bubble -- matches the
983
984
  * Recharts chart tooltips this was originally built for.
984
985
  * @variant inverted - Solid dark pill (no border) -- for a tooltip sitting
985
986
  * over a nav rail or other icon-only surface, where the themed surface bubble
986
987
  * doesn't read as clearly against a busy background.
987
988
  * @behavior Shows on hover and on keyboard focus (so it stays reachable
988
- * without a pointer) after `delay` ms (0 = immediate, the default). Hides
989
- * immediately on mouseleave/blur, cancelling a pending delayed show if it
990
- * hasn't fired yet -- except in `interactive` mode, which delays the hide
991
- * briefly so the pointer can travel from the trigger into the tooltip's own
992
- * content. Pass `whenTruncated` to only surface it once the trigger's content
989
+ * without a pointer) after `delay` ms (0 = immediate, the default). Fades in
990
+ * and out (opacity, `ease-in-out`, over the sys
991
+ * `--mr-motion-duration-interactive` token -- 200ms today); the bubble stays
992
+ * mounted through the fade-out and is removed from the DOM once it ends.
993
+ * Under `prefers-reduced-motion: reduce` there is no fade: it appears and is
994
+ * removed instantly. The hide starts on mouseleave/blur, cancelling a pending
995
+ * delayed show if it hasn't fired yet -- except in `interactive` mode, which
996
+ * delays the hide briefly so the pointer can travel from the trigger into the
997
+ * tooltip's own content. Re-hovering (or re-focusing) the trigger while the
998
+ * bubble is still fading out reverses the fade from where it is, without
999
+ * waiting out `delay` again, since it is already on screen; so does moving
1000
+ * the pointer back into an `interactive` bubble. A non-interactive bubble is
1001
+ * `pointer-events-none` throughout, fade-out included, so it never
1002
+ * intercepts clicks meant for what sits under it. Pass `whenTruncated` to
1003
+ * only surface it once the trigger's content
993
1004
  * is actually clipped by ellipsis (`scrollWidth > clientWidth`). `placement`
994
1005
  * 'top'/'bottom' centre (or `align="start"` left-align) on the trigger;
995
1006
  * 'right' centres vertically beside the anchor and 'bottom-end' hangs below,
@@ -1402,9 +1413,20 @@ declare const buttonVariants: (props?: ({
1402
1413
  size?: "icon" | "sm" | "md" | "lg" | null | undefined;
1403
1414
  } & class_variance_authority_types.ClassProp) | undefined) => string;
1404
1415
  interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
1405
- /** Render the button styling onto the single child element instead of a `<button>` (e.g. a router `<Link>`). */
1416
+ /**
1417
+ * Render the button styling onto the single child element instead of a
1418
+ * `<button>` (e.g. a router `<Link>` or an `<a>`). The child must be exactly
1419
+ * one React element; it keeps its own tag, content and semantics.
1420
+ */
1406
1421
  asChild?: boolean;
1407
- /** Shows a spinner in place of the leading edge and disables the button. */
1422
+ /**
1423
+ * Shows a spinner in place of the leading edge and disables the button.
1424
+ * With `asChild` there is no spinner (the child owns its content, and a
1425
+ * spinner belongs inside a real `<button>`): `loading` only sets
1426
+ * `aria-busy` and forwards `disabled` to the child, which only has an
1427
+ * effect on elements that support it -- an `<a>` stays clickable, so block
1428
+ * navigation yourself while loading.
1429
+ */
1408
1430
  loading?: boolean;
1409
1431
  /** Button label and/or icon. With `asChild`, the single element that receives the button styling. */
1410
1432
  children?: ReactNode;
@@ -1430,8 +1452,13 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantPr
1430
1452
  * but Button suppresses it (`focus-visible:outline-hidden focus-visible:ring-0`)
1431
1453
  * and relies on its per-variant hover/brightness treatment instead. `loading`
1432
1454
  * implies `disabled` (and sets `aria-busy`), since a control mid-submit
1433
- * shouldn't accept another click.
1434
- * @a11y Renders a native <button>, so it keeps native keyboard behaviour.
1455
+ * shouldn't accept another click. `asChild` renders only the child element,
1456
+ * through Radix `Slot`, with the button classes, ref and remaining props
1457
+ * merged onto it -- no wrapping `<button>`. With `asChild`, `loading` draws
1458
+ * no spinner; it sets `aria-busy` and forwards `disabled`, which an `<a>`
1459
+ * ignores, so a loading link stays clickable unless the caller blocks it.
1460
+ * @a11y Renders a native <button>, so it keeps native keyboard behaviour
1461
+ * (with `asChild`, the child element's own semantics apply instead).
1435
1462
  * `loading` sets aria-busy, and the spinner is aria-hidden. Icon-only
1436
1463
  * usage needs an aria-label from the caller -- nothing here supplies one.
1437
1464
  * @usage
@@ -1440,6 +1467,7 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantPr
1440
1467
  * <Button variant="secondary" size="sm">Cancel</Button>
1441
1468
  * <Button variant="danger">Delete</Button>
1442
1469
  * <Button variant="primary" loading>Saving…</Button>
1470
+ * <Button asChild variant="secondary"><a href="/reports">View reports</a></Button>
1443
1471
  * ```
1444
1472
  */
1445
1473
  declare const Button: react.ForwardRefExoticComponent<ButtonProps & react.RefAttributes<HTMLButtonElement>>;
package/dist/index.js CHANGED
@@ -1836,6 +1836,19 @@ import { jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
1836
1836
  function cx6(...parts) {
1837
1837
  return parts.filter(Boolean).join(" ");
1838
1838
  }
1839
+ function exitDurationMs(el) {
1840
+ if (typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return 0;
1841
+ const cs = window.getComputedStyle(el);
1842
+ if (cs.transitionProperty === "none") return 0;
1843
+ const toMs = (v) => {
1844
+ const n = parseFloat(v);
1845
+ if (!Number.isFinite(n)) return 0;
1846
+ return v.trim().endsWith("ms") ? n : n * 1e3;
1847
+ };
1848
+ const durations = cs.transitionDuration.split(",").map(toMs);
1849
+ const delays = cs.transitionDelay.split(",").map(toMs);
1850
+ return Math.max(0, ...durations.map((d, i) => d + (delays[i] ?? delays[0] ?? 0)));
1851
+ }
1839
1852
  function Tooltip({
1840
1853
  label,
1841
1854
  children,
@@ -1855,6 +1868,10 @@ function Tooltip({
1855
1868
  const boxRef = useRef9(null);
1856
1869
  const hideTimer = useRef9(null);
1857
1870
  const showTimer = useRef9(null);
1871
+ const exitTimer = useRef9(null);
1872
+ const [shown, setShown] = useState8(false);
1873
+ const openRef = useRef9(false);
1874
+ const mountedRef = useRef9(false);
1858
1875
  const id = useId();
1859
1876
  const clearHideTimer = () => {
1860
1877
  if (hideTimer.current) {
@@ -1868,10 +1885,17 @@ function Tooltip({
1868
1885
  showTimer.current = null;
1869
1886
  }
1870
1887
  };
1888
+ const clearExitTimer = () => {
1889
+ if (exitTimer.current) {
1890
+ clearTimeout(exitTimer.current);
1891
+ exitTimer.current = null;
1892
+ }
1893
+ };
1871
1894
  useEffect8(
1872
1895
  () => () => {
1873
1896
  clearHideTimer();
1874
1897
  clearShowTimer();
1898
+ clearExitTimer();
1875
1899
  },
1876
1900
  []
1877
1901
  );
@@ -1879,26 +1903,56 @@ function Tooltip({
1879
1903
  const el = anchorRef ? anchorRef.current : ref.current;
1880
1904
  if (!el) return;
1881
1905
  if (whenTruncated && el.scrollWidth <= el.clientWidth) return;
1906
+ clearExitTimer();
1907
+ openRef.current = true;
1882
1908
  const r = el.getBoundingClientRect();
1883
- setBoxWidth(null);
1909
+ if (!mountedRef.current) setBoxWidth(null);
1910
+ mountedRef.current = true;
1884
1911
  setAnchor({ top: r.top, left: r.left, width: r.width, height: r.height, bottom: r.bottom, right: r.right });
1885
1912
  if (anchorRef) el.setAttribute("aria-describedby", id);
1886
1913
  };
1887
1914
  const show = () => {
1888
1915
  clearHideTimer();
1889
- if (delay > 0) {
1916
+ clearShowTimer();
1917
+ if (delay > 0 && !mountedRef.current) {
1890
1918
  showTimer.current = setTimeout(reveal, delay);
1891
1919
  } else {
1892
1920
  reveal();
1893
1921
  }
1894
1922
  };
1895
- const hideNow = () => {
1896
- clearShowTimer();
1897
- clearHideTimer();
1923
+ const unmount = () => {
1924
+ clearExitTimer();
1925
+ if (openRef.current || !mountedRef.current) return;
1926
+ mountedRef.current = false;
1898
1927
  setAnchor(null);
1899
1928
  setBoxWidth(null);
1900
1929
  if (anchorRef?.current) anchorRef.current.removeAttribute("aria-describedby");
1901
1930
  };
1931
+ const hideNow = () => {
1932
+ clearShowTimer();
1933
+ clearHideTimer();
1934
+ openRef.current = false;
1935
+ if (!mountedRef.current) return;
1936
+ setShown(false);
1937
+ clearExitTimer();
1938
+ const ms = boxRef.current ? exitDurationMs(boxRef.current) : 0;
1939
+ if (ms === 0) {
1940
+ unmount();
1941
+ return;
1942
+ }
1943
+ exitTimer.current = setTimeout(unmount, ms + 50);
1944
+ };
1945
+ const keepOpen = () => {
1946
+ clearHideTimer();
1947
+ if (openRef.current || !mountedRef.current) return;
1948
+ clearExitTimer();
1949
+ openRef.current = true;
1950
+ setShown(true);
1951
+ };
1952
+ const onBoxTransitionEnd = (e) => {
1953
+ if (e.target !== e.currentTarget || e.propertyName !== "opacity") return;
1954
+ if (!openRef.current) unmount();
1955
+ };
1902
1956
  const hide = () => {
1903
1957
  clearShowTimer();
1904
1958
  if (!interactive) {
@@ -1928,6 +1982,7 @@ function Tooltip({
1928
1982
  const el = boxRef.current;
1929
1983
  if (!el) return;
1930
1984
  setBoxWidth(el.getBoundingClientRect().width);
1985
+ if (openRef.current) setShown(true);
1931
1986
  }, [anchor, label]);
1932
1987
  const margin = 8;
1933
1988
  let style = { top: 0, left: 0 };
@@ -1955,10 +2010,19 @@ function Tooltip({
1955
2010
  id,
1956
2011
  "data-testid": testIdPrefix && `${testIdPrefix}-tooltip`,
1957
2012
  style: { top: style.top, left: style.left, right: style.right, visibility: boxWidth == null ? "hidden" : "visible" },
1958
- onMouseEnter: interactive ? clearHideTimer : void 0,
2013
+ onMouseEnter: interactive ? keepOpen : void 0,
1959
2014
  onMouseLeave: interactive ? hide : void 0,
2015
+ onTransitionEnd: onBoxTransitionEnd,
1960
2016
  className: cx6(
1961
2017
  "fixed z-[90] w-max max-w-xs whitespace-normal break-words px-2.5 py-1.5 text-xs leading-snug",
2018
+ // Fade in/out. The sys duration token rather than a ref one:
2019
+ // it is what Button's hover/press transition uses, and a
2020
+ // tooltip's fade is the same kind of interactive feedback.
2021
+ // design-tokens has no in-out curve (ease-interactive is the
2022
+ // decelerating "standard" curve), so ease-in-out is Tailwind's.
2023
+ "transition-opacity duration-(--mr-motion-duration-interactive) ease-in-out motion-reduce:transition-none",
2024
+ shown ? "opacity-100" : "opacity-0",
2025
+ // Non-interactive: never a pointer target, fade-out included.
1962
2026
  interactive ? "pointer-events-auto" : "pointer-events-none",
1963
2027
  variant === "inverted" ? "rounded-control bg-text-primary text-surface" : "rounded-control border border-border-default bg-surface text-text-primary shadow-overlay",
1964
2028
  translate
@@ -2698,21 +2762,19 @@ var buttonVariants = cva(
2698
2762
  );
2699
2763
  var Button = forwardRef2(
2700
2764
  ({ className, variant, size, asChild = false, loading = false, disabled, children, ...props }, ref) => {
2701
- const Comp = asChild ? Slot : "button";
2702
- return /* @__PURE__ */ jsxs23(
2703
- Comp,
2704
- {
2705
- className: cn(buttonVariants({ variant, size, className })),
2706
- ref,
2707
- disabled: disabled || loading,
2708
- "aria-busy": loading || void 0,
2709
- ...props,
2710
- children: [
2711
- loading && /* @__PURE__ */ jsx25(Loader22, { className: "size-4 shrink-0 animate-spin", "aria-hidden": "true" }),
2712
- children
2713
- ]
2714
- }
2715
- );
2765
+ const shared = {
2766
+ className: cn(buttonVariants({ variant, size, className })),
2767
+ disabled: disabled || loading,
2768
+ "aria-busy": loading || void 0,
2769
+ ...props
2770
+ };
2771
+ if (asChild) {
2772
+ return /* @__PURE__ */ jsx25(Slot, { ref, ...shared, children });
2773
+ }
2774
+ return /* @__PURE__ */ jsxs23("button", { ref, ...shared, children: [
2775
+ loading && /* @__PURE__ */ jsx25(Loader22, { className: "size-4 shrink-0 animate-spin", "aria-hidden": "true" }),
2776
+ children
2777
+ ] });
2716
2778
  }
2717
2779
  );
2718
2780
  Button.displayName = "Button";