@nexub/react 7.2.0 → 7.3.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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "7.2.0",
2
+ "version": "7.3.0",
3
3
  "standard": "WCAG 2.2 Level AA",
4
4
  "method": {
5
5
  "automated": "axe-core rules tagged wcag2a, wcag2aa, wcag21a, wcag21aa and wcag22aa, run on every Storybook story in light and dark, and on open popups (menus, listboxes, dialogs), on every pull request; a new violation fails the build.",
@@ -33,7 +33,7 @@ const Trigger = React.forwardRef(
33
33
  ...props,
34
34
  children: [
35
35
  children,
36
- /* @__PURE__ */ jsx(ChevronDown, { size: "sm", className: "shrink-0 transition-transform duration-200", "aria-hidden": "true" })
36
+ /* @__PURE__ */ jsx(ChevronDown, { size: "sm", className: "shrink-0 transition-transform duration-spatial-fast ease-spatial-fast", "aria-hidden": "true" })
37
37
  ]
38
38
  }
39
39
  ) })
@@ -4,6 +4,7 @@ export interface CollapseProps extends Omit<MotionBaseProps, 'easing'>, React.HT
4
4
  /**
5
5
  * Legacy: set it to use `--duration-*` + `--ease-{value}` instead of the
6
6
  * spatial spring for the height transition.
7
+ * @deprecated since 7.3, removed in 8.0 (see MotionBaseProps.easing).
7
8
  */
8
9
  easing?: MotionBaseProps['easing'];
9
10
  /**
@@ -15,10 +16,10 @@ export interface CollapseProps extends Omit<MotionBaseProps, 'easing'>, React.HT
15
16
  as?: React.ElementType;
16
17
  }
17
18
  /**
18
- * Collapse — animates `max-height` to reveal / hide content on the spatial
19
- * spring. Reduced motion snaps the height instead of animating it.
20
- * Height is measured from the content ref so you don't need to
21
- * provide an explicit height.
19
+ * Collapse — reveals / hides content on the spatial spring by animating a
20
+ * one-row grid from `0fr` to `1fr`, so the browser sizes it: no height is
21
+ * measured in JS and content that grows while open just grows. Reduced motion
22
+ * snaps instead of animating.
22
23
  *
23
24
  * ```tsx
24
25
  * <Collapse in={isOpen}>
@@ -13,63 +13,30 @@ const Collapse = React.forwardRef(function Collapse2({
13
13
  as: Tag = "div",
14
14
  ...rest
15
15
  }, ref) {
16
- const innerRef = React.useRef(null);
17
16
  const reduced = usePrefersReducedMotion();
18
17
  const legacy = easing !== void 0;
19
- const settleMs = duration === "instant" || reduced ? 0 : legacy ? DURATION_MS[duration] : SPRING_MS.spatial[SPRING_SPEED[duration]];
20
- const [height, setHeight] = React.useState(visible ? "auto" : 0);
21
- const [overflow, setOverflow] = React.useState(
22
- visible ? "visible" : "hidden"
23
- );
18
+ const instant = duration === "instant" || reduced;
19
+ const settleMs = instant ? 0 : legacy ? DURATION_MS[duration] : SPRING_MS.spatial[SPRING_SPEED[duration]];
24
20
  const [mounted, setMounted] = React.useState(unmountOnExit ? visible : true);
21
+ const [expanded, setExpanded] = React.useState(visible);
22
+ const [settled, setSettled] = React.useState(true);
25
23
  const prevVisible = React.useRef(visible);
26
24
  React.useEffect(() => {
27
- const changed = prevVisible.current !== visible;
25
+ if (prevVisible.current === visible) return;
28
26
  prevVisible.current = visible;
29
- if (!changed) return;
30
- let raf1 = 0;
31
- let raf2 = 0;
32
- if (visible) {
33
- setMounted(true);
34
- raf1 = requestAnimationFrame(() => {
35
- const el = innerRef.current;
36
- if (el) {
37
- setHeight(el.scrollHeight);
38
- setOverflow("hidden");
39
- }
40
- });
41
- } else {
42
- const el = innerRef.current;
43
- if (el) {
44
- setHeight(el.scrollHeight);
45
- setOverflow("hidden");
46
- raf1 = requestAnimationFrame(() => {
47
- raf2 = requestAnimationFrame(() => {
48
- setHeight(typeof collapsedHeight === "number" ? collapsedHeight : 0);
49
- });
50
- });
51
- }
27
+ setSettled(false);
28
+ if (!visible) {
29
+ setExpanded(false);
30
+ return;
52
31
  }
53
- return () => {
54
- cancelAnimationFrame(raf1);
55
- cancelAnimationFrame(raf2);
56
- };
57
- }, [visible, collapsedHeight]);
32
+ setMounted(true);
33
+ const raf = requestAnimationFrame(() => setExpanded(true));
34
+ return () => cancelAnimationFrame(raf);
35
+ }, [visible]);
58
36
  const finish = React.useCallback(() => {
59
- if (visible) {
60
- setHeight("auto");
61
- setOverflow("visible");
62
- } else if (unmountOnExit) {
63
- setMounted(false);
64
- }
37
+ setSettled(true);
38
+ if (!visible && unmountOnExit) setMounted(false);
65
39
  }, [visible, unmountOnExit]);
66
- const handleTransitionEnd = React.useCallback(
67
- (e) => {
68
- if (e.target !== e.currentTarget || e.propertyName !== "max-height") return;
69
- finish();
70
- },
71
- [finish]
72
- );
73
40
  const firstRender = React.useRef(true);
74
41
  React.useEffect(() => {
75
42
  if (firstRender.current) {
@@ -80,20 +47,23 @@ const Collapse = React.forwardRef(function Collapse2({
80
47
  return () => clearTimeout(t);
81
48
  }, [visible, settleMs, finish]);
82
49
  if (!mounted) return null;
83
- const maxHeight = height === "auto" ? "none" : `${height}px`;
50
+ const overflow = expanded && settled ? "visible" : "hidden";
84
51
  return /* @__PURE__ */ jsx(
85
52
  Tag,
86
53
  {
87
54
  ref,
88
55
  style: {
89
- maxHeight,
56
+ display: "grid",
57
+ gridTemplateRows: expanded ? "1fr" : "0fr",
90
58
  overflow,
91
- transition: duration === "instant" || reduced ? "none" : legacy ? `max-height var(--duration-${duration}) var(--ease-${easing})` : spring("max-height", "spatial", SPRING_SPEED[duration]),
59
+ transition: instant ? "none" : legacy ? `grid-template-rows var(--duration-${duration}) var(--ease-${easing})` : spring("grid-template-rows", "spatial", SPRING_SPEED[duration]),
92
60
  ...style
93
61
  },
94
- onTransitionEnd: handleTransitionEnd,
62
+ onTransitionEnd: (e) => {
63
+ if (e.target === e.currentTarget && e.propertyName === "grid-template-rows") finish();
64
+ },
95
65
  ...rest,
96
- children: /* @__PURE__ */ jsx("div", { ref: innerRef, children })
66
+ children: /* @__PURE__ */ jsx("div", { style: { minHeight: collapsedHeight, overflow }, children })
97
67
  }
98
68
  );
99
69
  });
@@ -12,6 +12,8 @@ export interface MotionBaseProps {
12
12
  /**
13
13
  * Legacy: set it to use the old `--duration-*` + `--ease-{value}` tokens
14
14
  * instead of springs. Omit it to ride the spring tokens.
15
+ * @deprecated since 7.3: the `--duration-*` / `--ease-*` tokens (tokens/motion.json) are
16
+ * retired in favour of the spring tokens. Removed in 8.0.
15
17
  */
16
18
  easing?: MotionEasing;
17
19
  /**
@@ -50,7 +50,7 @@ const Trigger = React.forwardRef(
50
50
  ChevronDown,
51
51
  {
52
52
  size: "xs",
53
- className: "relative ms-1 transition duration-300 group-data-[state=open]:rotate-180",
53
+ className: "relative ms-1 transition-transform duration-spatial-fast ease-spatial-fast group-data-[state=open]:rotate-180",
54
54
  "aria-hidden": "true"
55
55
  }
56
56
  )
@@ -96,6 +96,8 @@ const Viewport = React.forwardRef(
96
96
  "nx-motion nx-pop origin-top relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden",
97
97
  floatingSurface,
98
98
  "md:w-[var(--radix-navigation-menu-viewport-width)]",
99
+ // Resize between items on the spatial spring instead of snapping.
100
+ "transition-[width,height] duration-spatial-default ease-spatial-default motion-reduce:transition-none",
99
101
  className
100
102
  ),
101
103
  ...props
@@ -20,7 +20,7 @@ const progressTrackVariants = cva(
20
20
  }
21
21
  );
22
22
  const progressFillVariants = cva(
23
- "h-full rounded-full transition-all duration-300 ease-in-out",
23
+ "h-full rounded-full transition-all duration-spatial-default ease-spatial-default",
24
24
  {
25
25
  variants: {
26
26
  tone: {
@@ -6,7 +6,7 @@ import { cn } from "../../utils/cn.js";
6
6
  import { useMergedRefs } from "../../hooks/useMergedRefs.js";
7
7
  import { useSlidingIndicator } from "../../hooks/useSlidingIndicator.js";
8
8
  const Root = React.forwardRef(
9
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx(RadixTabs.Root, { ref, className: cn("flex flex-col gap-3", className), ...props })
9
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx(RadixTabs.Root, { ref, className: cn("flex flex-col gap-3 data-[orientation=vertical]:flex-row", className), ...props })
10
10
  );
11
11
  Root.displayName = "Tabs.Root";
12
12
  const List = React.forwardRef(
@@ -18,8 +18,12 @@ const List = React.forwardRef(
18
18
  {
19
19
  ref: useMergedRefs(ref, forwardedRef),
20
20
  className: cn(
21
- "relative flex w-full items-stretch justify-start overflow-x-auto",
21
+ "group/tabs relative flex w-full items-stretch justify-start overflow-x-auto",
22
22
  "border-b border-[var(--color-outline-variant)]",
23
+ // Vertical: a column with the rule on the inline-end side.
24
+ "data-[orientation=vertical]:w-auto data-[orientation=vertical]:shrink-0 data-[orientation=vertical]:flex-col",
25
+ "data-[orientation=vertical]:overflow-x-visible data-[orientation=vertical]:overflow-y-auto",
26
+ "data-[orientation=vertical]:border-b-0 data-[orientation=vertical]:border-e",
23
27
  className
24
28
  ),
25
29
  ...props,
@@ -32,7 +36,10 @@ const List = React.forwardRef(
32
36
  className: cn(
33
37
  "pointer-events-none absolute bottom-0 left-0 h-[3px] rounded-t-full bg-[var(--color-primary)]",
34
38
  "w-[var(--nx-ind-w,0px)] translate-x-[var(--nx-ind-x,0px)] opacity-[var(--nx-ind-o,0)]",
35
- "[[data-nx-indicator=ready]>&]:transition-[transform,width] [[data-nx-indicator=ready]>&]:duration-spatial-default [[data-nx-indicator=ready]>&]:ease-spatial-default"
39
+ "group-data-[orientation=vertical]/tabs:top-0 group-data-[orientation=vertical]/tabs:bottom-auto group-data-[orientation=vertical]/tabs:left-auto group-data-[orientation=vertical]/tabs:end-0",
40
+ "group-data-[orientation=vertical]/tabs:h-[var(--nx-ind-h,0px)] group-data-[orientation=vertical]/tabs:w-[3px] group-data-[orientation=vertical]/tabs:rounded-none group-data-[orientation=vertical]/tabs:rounded-s-full",
41
+ "group-data-[orientation=vertical]/tabs:translate-x-0 group-data-[orientation=vertical]/tabs:translate-y-[var(--nx-ind-y,0px)]",
42
+ "[[data-nx-indicator=ready]>&]:transition-[transform,width,height] [[data-nx-indicator=ready]>&]:duration-spatial-default [[data-nx-indicator=ready]>&]:ease-spatial-default"
36
43
  )
37
44
  }
38
45
  )
@@ -52,6 +59,7 @@ const Trigger = React.forwardRef(
52
59
  "nx-state-layer relative inline-flex h-control-md items-center justify-center whitespace-nowrap rounded-t-[var(--shape-field)] px-4 text-label-lg select-none",
53
60
  "text-[var(--color-on-surface-variant)] transition-colors duration-effects-fast ease-effects-fast",
54
61
  "data-[state=active]:text-[var(--color-primary)]",
62
+ "group-data-[orientation=vertical]/tabs:justify-start group-data-[orientation=vertical]/tabs:rounded-none group-data-[orientation=vertical]/tabs:rounded-s-[var(--shape-field)]",
55
63
  "disabled:pointer-events-none disabled:text-[var(--color-text-disabled)]",
56
64
  className
57
65
  ),
@@ -66,6 +74,8 @@ const Content = React.forwardRef(
66
74
  {
67
75
  ref,
68
76
  className: cn(
77
+ // Crossfade in on the effects spring (@starting-style, @nexub/themes/base).
78
+ "nx-tab-panel",
69
79
  className,
70
80
  // Radix hides inactive panels with the `hidden` attribute, which any
71
81
  // display class (`flex`, `md:grid`…) overrides. !important always wins.
@@ -1,7 +1,7 @@
1
1
  import type * as React from 'react';
2
2
  /**
3
3
  * One indicator that slides to the active item (SegmentedControl thumb, Tabs
4
- * underline). Writes --nx-ind-x / -w / -o (opacity) on the (position: relative) root;
4
+ * underline). Writes --nx-ind-x / -w / -y / -h / -o (opacity) on the (position: relative) root;
5
5
  * CSS transitions do the motion. Sets data-nx-indicator="ready" one frame after
6
6
  * the first measure so the indicator doesn't animate in on mount.
7
7
  */
@@ -10,6 +10,8 @@ function useSlidingIndicator(ref, activeSelector) {
10
10
  if (!el) return;
11
11
  root.style.setProperty("--nx-ind-x", `${el.offsetLeft}px`);
12
12
  root.style.setProperty("--nx-ind-w", `${el.offsetWidth}px`);
13
+ root.style.setProperty("--nx-ind-y", `${el.offsetTop}px`);
14
+ root.style.setProperty("--nx-ind-h", `${el.offsetHeight}px`);
13
15
  };
14
16
  update();
15
17
  const frame = requestAnimationFrame(() => root.dataset.nxIndicator = "ready");