@nexub/react 7.2.0 → 7.4.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.
Files changed (34) hide show
  1. package/accessibility.json +1 -1
  2. package/dist/components/Accordion/Accordion.js +1 -1
  3. package/dist/components/Avatar/Avatar.d.ts +3 -0
  4. package/dist/components/Avatar/Avatar.js +28 -5
  5. package/dist/components/Avatar/index.d.ts +1 -1
  6. package/dist/components/Button/Button.d.ts +2 -1
  7. package/dist/components/Button/Button.js +37 -7
  8. package/dist/components/DataTable/DataTable.d.ts +5 -1
  9. package/dist/components/DataTable/DataTable.js +6 -2
  10. package/dist/components/Motion/Collapse.d.ts +5 -4
  11. package/dist/components/Motion/Collapse.js +23 -53
  12. package/dist/components/Motion/motion.types.d.ts +2 -0
  13. package/dist/components/NavigationMenu/NavigationMenu.js +3 -1
  14. package/dist/components/Progress/Progress.d.ts +4 -0
  15. package/dist/components/Progress/Progress.js +74 -21
  16. package/dist/components/Skeleton/Skeleton.d.ts +9 -0
  17. package/dist/components/Skeleton/Skeleton.js +18 -1
  18. package/dist/components/Skeleton/index.d.ts +2 -2
  19. package/dist/components/Skeleton/index.js +2 -1
  20. package/dist/components/Tabs/Tabs.js +13 -3
  21. package/dist/components/Toast/Toaster.js +68 -23
  22. package/dist/components/Toast/index.d.ts +1 -1
  23. package/dist/components/Toast/useToast.d.ts +18 -1
  24. package/dist/components/Toast/useToast.js +14 -0
  25. package/dist/hooks/useAsyncAction.d.ts +27 -0
  26. package/dist/hooks/useAsyncAction.js +36 -0
  27. package/dist/hooks/useSlidingIndicator.d.ts +1 -1
  28. package/dist/hooks/useSlidingIndicator.js +2 -0
  29. package/dist/index.d.ts +6 -3
  30. package/dist/index.js +4 -1
  31. package/dist/styles.css +1 -1
  32. package/dist/utils/viewTransition.d.ts +4 -2
  33. package/dist/utils/viewTransition.js +10 -2
  34. package/package.json +6 -6
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "7.2.0",
2
+ "version": "7.4.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
  ) })
@@ -3,11 +3,14 @@ import { type VariantProps } from 'class-variance-authority';
3
3
  export declare const avatarVariants: (props?: ({
4
4
  size?: "xs" | "sm" | "md" | "lg" | "xl" | null | undefined;
5
5
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
6
+ export type AvatarStatus = 'online' | 'away' | 'busy' | 'offline';
6
7
  export interface AvatarProps extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof avatarVariants> {
7
8
  src?: string;
8
9
  name?: string;
9
10
  fallback?: React.ReactNode;
10
11
  badge?: boolean;
11
12
  badgeColor?: string;
13
+ /** Presence dot, added to the accessible name ("Ada Lovelace, Away"). Replaces `badge` when both are set. */
14
+ status?: AvatarStatus;
12
15
  }
13
16
  export declare const Avatar: React.ForwardRefExoticComponent<AvatarProps & React.RefAttributes<HTMLSpanElement>>;
@@ -4,6 +4,7 @@ import * as React from "react";
4
4
  import { cva } from "class-variance-authority";
5
5
  import { cn } from "../../utils/cn.js";
6
6
  import { safeSrc } from "../../utils/safeHref.js";
7
+ import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
7
8
  const avatarVariants = cva(
8
9
  "relative inline-flex items-center justify-center shrink-0 rounded-full overflow-hidden bg-[var(--color-surface-container-high)] text-[var(--color-on-surface-muted)] font-medium select-none",
9
10
  {
@@ -28,6 +29,12 @@ const badgeSizeMap = {
28
29
  lg: "h-3 w-3",
29
30
  xl: "h-3.5 w-3.5"
30
31
  };
32
+ const statusColor = {
33
+ online: "bg-[var(--color-success)]",
34
+ away: "bg-[var(--color-warning)]",
35
+ busy: "bg-[var(--color-error)]",
36
+ offline: "bg-[var(--color-outline)]"
37
+ };
31
38
  function getInitials(name) {
32
39
  const parts = name.trim().split(/\s+/);
33
40
  const first = parts[0] ?? "";
@@ -44,19 +51,30 @@ const Avatar = React.forwardRef(
44
51
  fallback,
45
52
  badge,
46
53
  badgeColor,
54
+ status,
47
55
  ...props
48
56
  }, ref) => {
49
57
  const src = safeSrc(srcProp);
50
58
  const [failedSrc, setFailedSrc] = React.useState();
59
+ const [pendingSrc, setPendingSrc] = React.useState();
60
+ const hideUntilLoaded = React.useCallback(
61
+ (img) => {
62
+ if (img && !img.complete) setPendingSrc(img.getAttribute("src") ?? void 0);
63
+ },
64
+ []
65
+ );
66
+ const messages = useNexubMessages();
67
+ const statusLabel = status ? messages.presence[status] : void 0;
68
+ const label = [name, statusLabel].filter(Boolean).join(", ") || void 0;
51
69
  const showImage = Boolean(src) && failedSrc !== src;
52
70
  const initials = name ? getInitials(name) : null;
53
- const labelled = Boolean(name || props["aria-label"] || props["aria-labelledby"]);
71
+ const labelled = Boolean(label || props["aria-label"] || props["aria-labelledby"]);
54
72
  return /* @__PURE__ */ jsxs(
55
73
  "span",
56
74
  {
57
75
  ref,
58
76
  className: cn(avatarVariants({ size }), className),
59
- "aria-label": name ?? void 0,
77
+ "aria-label": label,
60
78
  role: labelled ? "img" : void 0,
61
79
  "aria-hidden": labelled ? void 0 : true,
62
80
  ...props,
@@ -66,7 +84,12 @@ const Avatar = React.forwardRef(
66
84
  {
67
85
  src,
68
86
  alt: name ?? "",
69
- className: "h-full w-full object-cover",
87
+ ref: hideUntilLoaded,
88
+ className: cn(
89
+ "h-full w-full object-cover transition-opacity duration-effects-default ease-effects-default",
90
+ pendingSrc === src && "opacity-0"
91
+ ),
92
+ onLoad: () => setPendingSrc(void 0),
70
93
  onError: () => setFailedSrc(src)
71
94
  }
72
95
  ) : initials ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: initials }) : fallback ? fallback : (
@@ -82,13 +105,13 @@ const Avatar = React.forwardRef(
82
105
  }
83
106
  )
84
107
  ),
85
- badge && /* @__PURE__ */ jsx(
108
+ (badge || status) && /* @__PURE__ */ jsx(
86
109
  "span",
87
110
  {
88
111
  className: cn(
89
112
  "absolute bottom-0 end-0 rounded-full ring-2 ring-[var(--color-surface)]",
90
113
  badgeSizeMap[size ?? "md"],
91
- badgeColor ?? "bg-[var(--color-success)]"
114
+ status ? statusColor[status] : badgeColor ?? "bg-[var(--color-success)]"
92
115
  ),
93
116
  "aria-hidden": "true"
94
117
  }
@@ -1,2 +1,2 @@
1
1
  export { Avatar, avatarVariants } from './Avatar.js';
2
- export type { AvatarProps } from './Avatar.js';
2
+ export type { AvatarProps, AvatarStatus } from './Avatar.js';
@@ -14,7 +14,8 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
14
14
  /** Async feedback. `loading` blocks clicks (aria-disabled, focus kept),
15
15
  * `success` draws a check, `error` shakes once. Not applied with `asChild`. */
16
16
  status?: ButtonStatus;
17
- /** Alias for `status="loading"`. */
17
+ /** Alias for `status="loading"`. A `type="submit"` button also shows loading
18
+ * while its parent `<form action>` is pending (React 19 `useFormStatus`). */
18
19
  loading?: boolean;
19
20
  /** Announced while loading. @default 'Loading' */
20
21
  loadingLabel?: string;
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
+ import { useFormStatus } from "react-dom";
4
5
  import { Slot } from "@radix-ui/react-slot";
5
6
  import { Loader2 } from "@nexub/icons";
6
7
  import { cva } from "class-variance-authority";
@@ -67,6 +68,22 @@ const buttonVariants = cva(
67
68
  }
68
69
  );
69
70
  const CheckIcon = () => /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { className: "nx-check-draw", pathLength: 1, d: "M20 6 9 17l-5-5" }) });
71
+ const SPINNER_DELAY = 150;
72
+ const SPINNER_MIN = 400;
73
+ function useSpinner(busy) {
74
+ const [shown, setShown] = React.useState(busy);
75
+ const shownAt = React.useRef(0);
76
+ React.useEffect(() => {
77
+ if (shown) shownAt.current = Date.now();
78
+ }, [shown]);
79
+ React.useEffect(() => {
80
+ if (busy === shown) return;
81
+ const wait = busy ? SPINNER_DELAY : Math.max(0, SPINNER_MIN - (Date.now() - shownAt.current));
82
+ const t = setTimeout(() => setShown(busy), wait);
83
+ return () => clearTimeout(t);
84
+ }, [busy, shown]);
85
+ return shown;
86
+ }
70
87
  const Button = React.forwardRef(
71
88
  ({
72
89
  className,
@@ -89,7 +106,9 @@ const Button = React.forwardRef(
89
106
  children,
90
107
  ...props
91
108
  }, ref) => {
92
- const status = statusProp ?? (loading ? "loading" : "idle");
109
+ const formPending = useFormStatus().pending && props.type === "submit";
110
+ const status = statusProp ?? (loading || formPending ? "loading" : "idle");
111
+ const spinner = useSpinner(status === "loading");
93
112
  const handlePointerMove = usePointerSheen(magnetic, onPointerMove);
94
113
  const sharedProps = {
95
114
  className: cn(
@@ -111,7 +130,7 @@ const Button = React.forwardRef(
111
130
  }
112
131
  const busy = status === "loading";
113
132
  const swapLabel = status === "success" && successLabel !== void 0;
114
- const overlay = busy ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin", "aria-hidden": "true" }) : status === "success" && !swapLabel ? /* @__PURE__ */ jsx(CheckIcon, {}) : null;
133
+ const overlay = spinner ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin", "aria-hidden": "true" }) : status === "success" && !swapLabel ? /* @__PURE__ */ jsx(CheckIcon, {}) : null;
115
134
  const announcement = status === "loading" ? loadingLabel : status === "success" ? successLabel ?? "Done" : status === "error" ? errorLabel : "";
116
135
  const button = /* @__PURE__ */ jsxs(
117
136
  "button",
@@ -130,14 +149,25 @@ const Button = React.forwardRef(
130
149
  "data-status": status === "idle" ? void 0 : status,
131
150
  ...sharedProps,
132
151
  children: [
133
- /* @__PURE__ */ jsx("span", { className: cn("contents", overlay && "text-transparent [&_img]:invisible [&_svg]:invisible"), children: swapLabel ? /* @__PURE__ */ jsxs(Fragment, { children: [
134
- /* @__PURE__ */ jsx(CheckIcon, {}),
135
- successLabel
136
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
152
+ successLabel === void 0 ? /* @__PURE__ */ jsxs("span", { className: cn("contents", overlay && "text-transparent [&_img]:invisible [&_svg]:invisible"), children: [
137
153
  leadingIcon,
138
154
  children,
139
155
  trailingIcon
140
- ] }) }),
156
+ ] }) : (
157
+ // Both labels share one grid cell, so the button is as wide as the wider one
158
+ // whichever shows; the hidden one keeps its space.
159
+ /* @__PURE__ */ jsxs("span", { className: cn("inline-grid", overlay && "text-transparent [&_img]:invisible [&_svg]:invisible"), children: [
160
+ /* @__PURE__ */ jsxs("span", { className: cn("col-start-1 row-start-1 inline-flex items-center justify-center gap-2", swapLabel && "invisible"), "aria-hidden": swapLabel || void 0, children: [
161
+ leadingIcon,
162
+ children,
163
+ trailingIcon
164
+ ] }),
165
+ /* @__PURE__ */ jsxs("span", { className: cn("col-start-1 row-start-1 inline-flex items-center justify-center gap-2", !swapLabel && "invisible"), "aria-hidden": !swapLabel || void 0, children: [
166
+ swapLabel ? /* @__PURE__ */ jsx(CheckIcon, {}) : /* @__PURE__ */ jsx("span", { className: "h-4 w-4" }),
167
+ successLabel
168
+ ] })
169
+ ] })
170
+ ),
141
171
  overlay && /* @__PURE__ */ jsx("span", { className: "absolute inset-0 flex items-center justify-center", "aria-hidden": "true", children: overlay })
142
172
  ]
143
173
  }
@@ -91,5 +91,9 @@ export interface DataTableProps<TData> extends React.HTMLAttributes<HTMLDivEleme
91
91
  onRowOrderChange?: (data: TData[]) => void;
92
92
  /** Names a row in "Move ‹label›" and the move announcements. @default its position */
93
93
  getRowLabel?: (row: TData) => string;
94
+ /** Shows skeleton rows (one page's worth) and sets aria-busy on the table. */
95
+ loading?: boolean;
96
+ /** Shown in a full-width row when there are no rows. @default messages.noResults */
97
+ emptyState?: React.ReactNode;
94
98
  }
95
- export declare function DataTable<TData>({ columns, data, enableSorting, enableRowSelection, selectionColumn, rowSelection: rowSelectionProp, onRowSelectionChange, getRowId, bulkActions, density: densityProp, defaultDensity, onDensityChange, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, viewOptions, toolbar, enableColumnResizing, columnSizing: columnSizingProp, onColumnSizingChange, enableColumnReordering, columnOrder: columnOrderProp, onColumnOrderChange, columnPinning: columnPinningProp, onColumnPinningChange, maxHeight, grid, rowWindow, pagination, getSubRows, expanded: expandedProp, defaultExpanded, onExpandedChange, onRowOrderChange, getRowLabel, caption, captionHidden, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: DataTableProps<TData>): React.JSX.Element;
99
+ export declare function DataTable<TData>({ columns, data, enableSorting, enableRowSelection, selectionColumn, rowSelection: rowSelectionProp, onRowSelectionChange, getRowId, bulkActions, density: densityProp, defaultDensity, onDensityChange, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, viewOptions, toolbar, enableColumnResizing, columnSizing: columnSizingProp, onColumnSizingChange, enableColumnReordering, columnOrder: columnOrderProp, onColumnOrderChange, columnPinning: columnPinningProp, onColumnPinningChange, maxHeight, grid, rowWindow, pagination, getSubRows, expanded: expandedProp, defaultExpanded, onExpandedChange, onRowOrderChange, getRowLabel, loading, emptyState, caption, captionHidden, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: DataTableProps<TData>): React.JSX.Element;
@@ -23,6 +23,7 @@ import { Button } from "../Button/Button.js";
23
23
  import { DropdownMenu } from "../DropdownMenu/DropdownMenu.js";
24
24
  import { createGridNavigation } from "./gridNavigation.js";
25
25
  import { useReorder } from "../Sortable/reorder.js";
26
+ import { Skeleton } from "../Skeleton/Skeleton.js";
26
27
  const alignText = {
27
28
  start: "text-start",
28
29
  end: "text-end",
@@ -170,6 +171,8 @@ function DataTable({
170
171
  onExpandedChange,
171
172
  onRowOrderChange,
172
173
  getRowLabel,
174
+ loading = false,
175
+ emptyState,
173
176
  caption,
174
177
  captionHidden = false,
175
178
  className,
@@ -429,6 +432,7 @@ function DataTable({
429
432
  "aria-label": ariaLabel,
430
433
  "aria-labelledby": ariaLabelledBy,
431
434
  "aria-rowcount": rowWin.virtual ? rows.length + 1 : void 0,
435
+ "aria-busy": loading || void 0,
432
436
  role: grid ? "grid" : void 0,
433
437
  onKeyDown: grid ? gridNav.onKeyDown : void 0,
434
438
  onFocus: grid ? gridNav.onFocus : void 0,
@@ -528,7 +532,7 @@ function DataTable({
528
532
  header.id
529
533
  );
530
534
  }) }, headerGroup.id)) }),
531
- /* @__PURE__ */ jsx(TableBody, { children: rows.length ? /* @__PURE__ */ jsxs(Fragment, { children: [
535
+ /* @__PURE__ */ jsx(TableBody, { children: loading ? Array.from({ length: pagination?.pageSize ?? 5 }, (_, i) => /* @__PURE__ */ jsx(TableRow, { "data-loading": "", className: "hover:bg-transparent", children: table.getVisibleLeafColumns().map((c) => /* @__PURE__ */ jsx(TableCell, { className: cn(compact && "py-1.5"), children: /* @__PURE__ */ jsx(Skeleton, {}) }, c.id)) }, i)) : rows.length ? /* @__PURE__ */ jsxs(Fragment, { children: [
532
536
  rowWin.before > 0 && /* @__PURE__ */ jsx(Spacer, { height: rowWin.before }),
533
537
  rowWin.indexes.map((i) => {
534
538
  const row = rows[i];
@@ -564,7 +568,7 @@ function DataTable({
564
568
  {
565
569
  colSpan: table.getVisibleLeafColumns().length,
566
570
  className: "h-24 text-center text-[var(--color-on-surface-muted)]",
567
- children: messages.noResults
571
+ children: emptyState ?? messages.noResults
568
572
  }
569
573
  ) }) })
570
574
  ]
@@ -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
@@ -13,5 +13,9 @@ export interface ProgressProps extends Omit<React.HTMLAttributes<HTMLDivElement>
13
13
  max?: number;
14
14
  tone?: ToneValue;
15
15
  animated?: boolean;
16
+ /** `circular` draws a ring; same value/max/indeterminate semantics. @default 'linear' */
17
+ variant?: 'linear' | 'circular';
18
+ /** Linear only: splits the bar into N equal steps, filled up to the value. */
19
+ segments?: number;
16
20
  }
17
21
  export declare const Progress: React.ForwardRefExoticComponent<ProgressProps & React.RefAttributes<HTMLDivElement>>;
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx } from "react/jsx-runtime";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { cva } from "class-variance-authority";
5
5
  import { cn } from "../../utils/cn.js";
@@ -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: {
@@ -43,6 +43,18 @@ const progressFillVariants = cva(
43
43
  }
44
44
  }
45
45
  );
46
+ const ringTone = {
47
+ neutral: "stroke-[var(--color-outline)]",
48
+ info: "stroke-[var(--color-info)]",
49
+ success: "stroke-[var(--color-success)]",
50
+ warning: "stroke-[var(--color-warning)]",
51
+ danger: "stroke-[var(--color-error)]",
52
+ leap: "stroke-[var(--color-leap)]",
53
+ bookub: "stroke-[var(--color-bookub)]",
54
+ payub: "stroke-[var(--color-payub)]",
55
+ founderub: "stroke-[var(--color-founderub)]"
56
+ };
57
+ const ringSize = { sm: "size-6", md: "size-10", lg: "size-16" };
46
58
  const Progress = React.forwardRef(
47
59
  ({
48
60
  className,
@@ -51,6 +63,8 @@ const Progress = React.forwardRef(
51
63
  value,
52
64
  max = 100,
53
65
  animated = false,
66
+ variant = "linear",
67
+ segments,
54
68
  ...props
55
69
  }, ref) => {
56
70
  const isIndeterminate = value === void 0 || value === null;
@@ -60,31 +74,70 @@ const Progress = React.forwardRef(
60
74
  useIsomorphicLayoutEffect(() => {
61
75
  fillRef.current?.style.setProperty("width", `${percentage}%`);
62
76
  }, [percentage]);
63
- return /* @__PURE__ */ jsx(
64
- "div",
65
- {
66
- ref,
67
- role: "progressbar",
68
- "aria-valuenow": isIndeterminate ? void 0 : clampedValue,
69
- "aria-valuemin": 0,
70
- "aria-valuemax": max,
71
- className: cn(progressTrackVariants({ size }), className),
72
- ...props,
73
- children: /* @__PURE__ */ jsx(
74
- "div",
77
+ const aria = {
78
+ role: "progressbar",
79
+ "aria-valuenow": isIndeterminate ? void 0 : clampedValue,
80
+ "aria-valuemin": 0,
81
+ "aria-valuemax": max
82
+ };
83
+ if (variant === "circular") {
84
+ return /* @__PURE__ */ jsx("div", { ref, ...aria, className: cn("inline-flex", ringSize[size ?? "md"], className), ...props, children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 36 36", className: cn("size-full -rotate-90", isIndeterminate && "animate-spin motion-reduce:animate-none"), "aria-hidden": "true", children: [
85
+ /* @__PURE__ */ jsx("circle", { cx: "18", cy: "18", r: "15", fill: "none", strokeWidth: "4", className: "stroke-[var(--color-surface-container-high)]" }),
86
+ /* @__PURE__ */ jsx(
87
+ "circle",
75
88
  {
76
- className: cn(
77
- progressFillVariants({ tone }),
78
- animated && "animate-pulse",
79
- isIndeterminate && "animate-[nx-progress-indeterminate_1.4s_ease-in-out_infinite]"
80
- ),
81
- ref: fillRef,
89
+ cx: "18",
90
+ cy: "18",
91
+ r: "15",
92
+ fill: "none",
93
+ strokeWidth: "4",
94
+ strokeLinecap: "round",
95
+ pathLength: 100,
96
+ strokeDasharray: `${isIndeterminate ? 25 : percentage} 100`,
97
+ className: cn("transition-all duration-spatial-default ease-spatial-default", ringTone[tone]),
82
98
  "data-part": "fill",
83
99
  "data-indeterminate": isIndeterminate || void 0
84
100
  }
85
101
  )
102
+ ] }) });
103
+ }
104
+ if (segments && segments > 0 && !isIndeterminate) {
105
+ const filled = Math.round(percentage / 100 * segments);
106
+ return /* @__PURE__ */ jsx(
107
+ "div",
108
+ {
109
+ ref,
110
+ ...aria,
111
+ className: cn(progressTrackVariants({ size }), "flex gap-1 bg-transparent", className),
112
+ ...props,
113
+ children: Array.from({ length: segments }, (_, i) => /* @__PURE__ */ jsx(
114
+ "div",
115
+ {
116
+ "data-part": "segment",
117
+ "data-filled": i < filled || void 0,
118
+ className: cn(
119
+ "flex-1",
120
+ i < filled ? progressFillVariants({ tone }) : "rounded-full bg-[var(--color-surface-container-high)]"
121
+ )
122
+ },
123
+ i
124
+ ))
125
+ }
126
+ );
127
+ }
128
+ return /* @__PURE__ */ jsx("div", { ref, ...aria, className: cn(progressTrackVariants({ size }), className), ...props, children: /* @__PURE__ */ jsx(
129
+ "div",
130
+ {
131
+ className: cn(
132
+ progressFillVariants({ tone }),
133
+ animated && "animate-pulse",
134
+ isIndeterminate && "animate-[nx-progress-indeterminate_1.4s_ease-in-out_infinite]"
135
+ ),
136
+ ref: fillRef,
137
+ "data-part": "fill",
138
+ "data-indeterminate": isIndeterminate || void 0
86
139
  }
87
- );
140
+ ) });
88
141
  }
89
142
  );
90
143
  Progress.displayName = "Progress";
@@ -10,5 +10,14 @@ export interface SkeletonProps extends React.HTMLAttributes<HTMLSpanElement>, Va
10
10
  width?: string | number;
11
11
  height?: string | number;
12
12
  animated?: boolean;
13
+ /** Effect when `animated`: `pulse` fades, `shimmer` sweeps a highlight across. Both stop under reduced motion. @default 'pulse' */
14
+ animation?: 'pulse' | 'shimmer';
13
15
  }
14
16
  export declare const Skeleton: React.ForwardRefExoticComponent<SkeletonProps & React.RefAttributes<HTMLSpanElement>>;
17
+ export interface SkeletonTextProps extends React.HTMLAttributes<HTMLSpanElement>, Pick<SkeletonProps, 'animated' | 'animation'> {
18
+ /** Number of lines; the last is shorter. @default 3 */
19
+ lines?: number;
20
+ }
21
+ /** Placeholder paragraph. Each line is one line-height tall (inherited from the
22
+ * surrounding text style), so swapping in the real text doesn't shift layout. */
23
+ export declare const SkeletonText: React.ForwardRefExoticComponent<SkeletonTextProps & React.RefAttributes<HTMLSpanElement>>;
@@ -29,6 +29,7 @@ const Skeleton = React.forwardRef(
29
29
  width,
30
30
  height,
31
31
  animated = true,
32
+ animation = "pulse",
32
33
  style,
33
34
  ...props
34
35
  }, ref) => {
@@ -40,7 +41,11 @@ const Skeleton = React.forwardRef(
40
41
  {
41
42
  ref,
42
43
  "aria-hidden": "true",
43
- className: cn(skeletonVariants({ shape, animated }), className),
44
+ className: cn(
45
+ skeletonVariants({ shape, animated: animated && animation === "pulse" }),
46
+ animated && animation === "shimmer" && "nx-skeleton-shimmer",
47
+ className
48
+ ),
44
49
  style: sizeStyle || style ? { ...sizeStyle, ...style } : void 0,
45
50
  ...props
46
51
  }
@@ -48,7 +53,19 @@ const Skeleton = React.forwardRef(
48
53
  }
49
54
  );
50
55
  Skeleton.displayName = "Skeleton";
56
+ const SkeletonText = React.forwardRef(
57
+ ({ className, lines = 3, animated, animation, ...props }, ref) => /* @__PURE__ */ jsx("span", { ref, "aria-hidden": "true", className: cn("block", className), ...props, children: Array.from({ length: lines }, (_, i) => /* @__PURE__ */ jsx("span", { className: "flex h-[1lh] items-center", children: /* @__PURE__ */ jsx(
58
+ Skeleton,
59
+ {
60
+ animated,
61
+ animation,
62
+ className: i === lines - 1 && lines > 1 ? "w-3/5" : void 0
63
+ }
64
+ ) }, i)) })
65
+ );
66
+ SkeletonText.displayName = "SkeletonText";
51
67
  export {
52
68
  Skeleton,
69
+ SkeletonText,
53
70
  skeletonVariants
54
71
  };
@@ -1,2 +1,2 @@
1
- export { Skeleton, skeletonVariants } from './Skeleton.js';
2
- export type { SkeletonProps } from './Skeleton.js';
1
+ export { Skeleton, SkeletonText, skeletonVariants } from './Skeleton.js';
2
+ export type { SkeletonProps, SkeletonTextProps } from './Skeleton.js';
@@ -1,5 +1,6 @@
1
- import { Skeleton, skeletonVariants } from "./Skeleton.js";
1
+ import { Skeleton, SkeletonText, skeletonVariants } from "./Skeleton.js";
2
2
  export {
3
3
  Skeleton,
4
+ SkeletonText,
4
5
  skeletonVariants
5
6
  };