@nexub/react 7.1.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.
Files changed (34) hide show
  1. package/accessibility.json +1 -1
  2. package/dist/components/Accordion/Accordion.js +1 -1
  3. package/dist/components/AlertDialog/AlertDialog.d.ts +7 -1
  4. package/dist/components/AlertDialog/AlertDialog.js +8 -1
  5. package/dist/components/Avatar/Avatar.js +3 -1
  6. package/dist/components/Breadcrumb/Breadcrumb.js +3 -2
  7. package/dist/components/Card/Card.js +3 -1
  8. package/dist/components/Dialog/Dialog.d.ts +11 -1
  9. package/dist/components/Dialog/Dialog.js +8 -1
  10. package/dist/components/FileUpload/FileUpload.d.ts +8 -1
  11. package/dist/components/FileUpload/FileUpload.js +5 -3
  12. package/dist/components/MediaCard/MediaCard.js +4 -1
  13. package/dist/components/Motion/Collapse.d.ts +5 -4
  14. package/dist/components/Motion/Collapse.js +23 -53
  15. package/dist/components/Motion/motion.types.d.ts +2 -0
  16. package/dist/components/NavigationMenu/NavigationMenu.js +3 -1
  17. package/dist/components/NexubProvider/NexubProvider.d.ts +7 -1
  18. package/dist/components/NexubProvider/NexubProvider.js +3 -1
  19. package/dist/components/PasswordInput/PasswordInput.js +6 -1
  20. package/dist/components/Progress/Progress.js +1 -1
  21. package/dist/components/Tabs/Tabs.js +13 -3
  22. package/dist/components/ThemeProvider/ThemeProvider.js +2 -1
  23. package/dist/hooks/useClassScrollLock.d.ts +13 -0
  24. package/dist/hooks/useClassScrollLock.js +12 -0
  25. package/dist/hooks/useSlidingIndicator.d.ts +1 -1
  26. package/dist/hooks/useSlidingIndicator.js +2 -0
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +3 -0
  29. package/dist/styles.css +1 -1
  30. package/dist/utils/safeHref.d.ts +12 -0
  31. package/dist/utils/safeHref.js +24 -0
  32. package/dist/utils/viewTransition.d.ts +4 -2
  33. package/dist/utils/viewTransition.js +10 -2
  34. package/package.json +11 -6
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "7.1.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
  ) })
@@ -1,5 +1,11 @@
1
1
  import * as React from 'react';
2
2
  import * as RadixAlertDialog from '@radix-ui/react-alert-dialog';
3
+ import { type ScrollLock } from '../../hooks/useClassScrollLock.js';
4
+ export interface AlertDialogRootProps extends RadixAlertDialog.AlertDialogProps {
5
+ /** See Dialog.Root `scrollLock`. @default 'style' (becomes 'class' in 8.0) */
6
+ scrollLock?: ScrollLock;
7
+ }
8
+ declare function Root({ scrollLock, ...props }: AlertDialogRootProps): React.JSX.Element;
3
9
  export interface AlertDialogConfirmProps {
4
10
  /** What the user must type, e.g. the resource name. Matching ignores outer spaces. */
5
11
  text: string;
@@ -16,7 +22,7 @@ declare namespace Confirm {
16
22
  var displayName: string;
17
23
  }
18
24
  export declare const AlertDialog: {
19
- Root: React.FC<RadixAlertDialog.AlertDialogProps>;
25
+ Root: typeof Root;
20
26
  Trigger: React.ForwardRefExoticComponent<RadixAlertDialog.AlertDialogTriggerProps & React.RefAttributes<HTMLButtonElement>>;
21
27
  Portal: React.FC<RadixAlertDialog.AlertDialogPortalProps>;
22
28
  Overlay: React.ForwardRefExoticComponent<RadixAlertDialog.AlertDialogOverlayProps & React.RefAttributes<HTMLDivElement>>;
@@ -7,12 +7,19 @@ import { overlaySurface, scrim } from "../../utils/surface.js";
7
7
  import { buttonVariants } from "../Button/Button.js";
8
8
  import { Input } from "../Input/Input.js";
9
9
  import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
10
+ import { ClassLockOverlay, ScrollLockContext } from "../../hooks/useClassScrollLock.js";
10
11
  const ConfirmContext = React.createContext(null);
11
- const Root = RadixAlertDialog.Root;
12
+ const ALERT_LOCK = { style: { scrollLock: "style", modal: true }, class: { scrollLock: "class", modal: true } };
13
+ function Root({ scrollLock = "style", ...props }) {
14
+ return /* @__PURE__ */ jsx(ScrollLockContext.Provider, { value: ALERT_LOCK[scrollLock], children: /* @__PURE__ */ jsx(RadixAlertDialog.Root, { ...props }) });
15
+ }
12
16
  const Trigger = RadixAlertDialog.Trigger;
13
17
  const Portal = RadixAlertDialog.Portal;
14
18
  const Overlay = React.forwardRef(
15
19
  function AlertDialogOverlay({ className, ...props }, ref) {
20
+ const { scrollLock } = React.useContext(ScrollLockContext);
21
+ if (scrollLock === "class")
22
+ return /* @__PURE__ */ jsx(ClassLockOverlay, { ref, className: cn("fixed inset-0 z-[var(--z-modal)] nx-motion", scrim, className) });
16
23
  return /* @__PURE__ */ jsx(
17
24
  RadixAlertDialog.Overlay,
18
25
  {
@@ -3,6 +3,7 @@ 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";
6
+ import { safeSrc } from "../../utils/safeHref.js";
6
7
  const avatarVariants = cva(
7
8
  "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",
8
9
  {
@@ -38,13 +39,14 @@ const Avatar = React.forwardRef(
38
39
  ({
39
40
  className,
40
41
  size = "md",
41
- src,
42
+ src: srcProp,
42
43
  name,
43
44
  fallback,
44
45
  badge,
45
46
  badgeColor,
46
47
  ...props
47
48
  }, ref) => {
49
+ const src = safeSrc(srcProp);
48
50
  const [failedSrc, setFailedSrc] = React.useState();
49
51
  const showImage = Boolean(src) && failedSrc !== src;
50
52
  const initials = name ? getInitials(name) : null;
@@ -3,6 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { ChevronRight, MoreHorizontal } from "@nexub/icons";
5
5
  import { cn } from "../../utils/cn.js";
6
+ import { safeHref } from "../../utils/safeHref.js";
6
7
  import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
7
8
  import { DropdownMenu } from "../DropdownMenu/DropdownMenu.js";
8
9
  const Root = React.forwardRef(
@@ -84,7 +85,7 @@ const Trail = React.forwardRef(({ items, maxItems = 4, ...props }, ref) => {
84
85
  const collapsed = items.length > keep;
85
86
  const hidden = collapsed ? items.slice(1, items.length - (keep - 1)) : [];
86
87
  const shown = collapsed ? [items[0], ...items.slice(items.length - (keep - 1))] : items;
87
- const crumb = (item, last) => last ? /* @__PURE__ */ jsx(Page, { children: item.label }) : /* @__PURE__ */ jsx(Link, { href: item.href, children: item.label });
88
+ const crumb = (item, last) => last ? /* @__PURE__ */ jsx(Page, { children: item.label }) : /* @__PURE__ */ jsx(Link, { href: safeHref(item.href), children: item.label });
88
89
  return /* @__PURE__ */ jsx(Root, { ref, ...props, children: /* @__PURE__ */ jsx(List, { children: shown.map((item, i) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
89
90
  i > 0 && /* @__PURE__ */ jsx(Separator, {}),
90
91
  /* @__PURE__ */ jsx(Item, { children: crumb(item, i === shown.length - 1) }),
@@ -99,7 +100,7 @@ const Trail = React.forwardRef(({ items, maxItems = 4, ...props }, ref) => {
99
100
  children: /* @__PURE__ */ jsx(MoreHorizontal, { size: "sm", "aria-hidden": "true" })
100
101
  }
101
102
  ),
102
- /* @__PURE__ */ jsx(DropdownMenu.Content, { align: "start", children: hidden.map((h, j) => /* @__PURE__ */ jsx(DropdownMenu.Item, { asChild: true, children: /* @__PURE__ */ jsx("a", { href: h.href, children: h.label }) }, j)) })
103
+ /* @__PURE__ */ jsx(DropdownMenu.Content, { align: "start", children: hidden.map((h, j) => /* @__PURE__ */ jsx(DropdownMenu.Item, { asChild: true, children: /* @__PURE__ */ jsx("a", { href: safeHref(h.href), children: h.label }) }, j)) })
103
104
  ] }) })
104
105
  ] })
105
106
  ] }, i)) }) });
@@ -4,6 +4,7 @@ import * as React from "react";
4
4
  import { Slot } from "@radix-ui/react-slot";
5
5
  import { cva } from "class-variance-authority";
6
6
  import { cn } from "../../utils/cn.js";
7
+ import { safeHref } from "../../utils/safeHref.js";
7
8
  import { glassSurfaceClass } from "../../utils/glass.js";
8
9
  import { usePointerSheen } from "../../hooks/usePointerSheen.js";
9
10
  const cardVariants = cva(
@@ -72,7 +73,7 @@ const Card = React.forwardRef(
72
73
  );
73
74
  Card.displayName = "Card";
74
75
  const CardLink = React.forwardRef(
75
- ({ className, asChild = false, ...props }, ref) => {
76
+ ({ className, asChild = false, href, ...props }, ref) => {
76
77
  const Comp = asChild ? Slot : "a";
77
78
  return /* @__PURE__ */ jsx(
78
79
  Comp,
@@ -83,6 +84,7 @@ const CardLink = React.forwardRef(
83
84
  "text-inherit no-underline outline-none after:absolute after:inset-0 after:rounded-[inherit] after:content-['']",
84
85
  className
85
86
  ),
87
+ href: safeHref(href),
86
88
  ...props
87
89
  }
88
90
  );
@@ -2,12 +2,22 @@ import * as React from 'react';
2
2
  import * as RadixDialog from '@radix-ui/react-dialog';
3
3
  import type { Size } from '../../utils/control.js';
4
4
  import { type SurfaceValue } from '../../utils/glass.js';
5
+ import { type ScrollLock } from '../../hooks/useClassScrollLock.js';
6
+ export interface DialogRootProps extends RadixDialog.DialogProps {
7
+ /**
8
+ * `'class'` locks page scroll with a class on <html> instead of the <style>
9
+ * Radix injects, so the dialog works under a strict `style-src` with no nonce.
10
+ * @default 'style' (becomes 'class' in 8.0)
11
+ */
12
+ scrollLock?: ScrollLock;
13
+ }
14
+ declare function Root({ scrollLock, modal, ...props }: DialogRootProps): React.JSX.Element;
5
15
  interface ContentProps extends RadixDialog.DialogContentProps {
6
16
  size?: Size | 'xl' | 'full';
7
17
  surface?: SurfaceValue;
8
18
  }
9
19
  export declare const Dialog: {
10
- Root: React.FC<RadixDialog.DialogProps>;
20
+ Root: typeof Root;
11
21
  Trigger: React.ForwardRefExoticComponent<RadixDialog.DialogTriggerProps & React.RefAttributes<HTMLButtonElement>>;
12
22
  Portal: React.FC<RadixDialog.DialogPortalProps>;
13
23
  Overlay: React.ForwardRefExoticComponent<RadixDialog.DialogOverlayProps & React.RefAttributes<HTMLDivElement>>;
@@ -5,12 +5,19 @@ import * as RadixDialog from "@radix-ui/react-dialog";
5
5
  import { cn } from "../../utils/cn.js";
6
6
  import { overlaySurface, scrim } from "../../utils/surface.js";
7
7
  import { glassSurfaceClass } from "../../utils/glass.js";
8
- const Root = RadixDialog.Root;
8
+ import { ClassLockOverlay, ScrollLockContext } from "../../hooks/useClassScrollLock.js";
9
+ function Root({ scrollLock = "style", modal = true, ...props }) {
10
+ const lock = React.useMemo(() => ({ scrollLock, modal }), [scrollLock, modal]);
11
+ return /* @__PURE__ */ jsx(ScrollLockContext.Provider, { value: lock, children: /* @__PURE__ */ jsx(RadixDialog.Root, { modal, ...props }) });
12
+ }
9
13
  const Trigger = RadixDialog.Trigger;
10
14
  const Portal = RadixDialog.Portal;
11
15
  const Close = RadixDialog.Close;
12
16
  const Overlay = React.forwardRef(
13
17
  function DialogOverlay({ className, ...props }, ref) {
18
+ const { scrollLock, modal } = React.useContext(ScrollLockContext);
19
+ const cls = cn("fixed inset-0 z-[var(--z-modal)] nx-motion", scrim, className);
20
+ if (scrollLock === "class") return modal ? /* @__PURE__ */ jsx(ClassLockOverlay, { ref, className: cls }) : null;
14
21
  return /* @__PURE__ */ jsx(
15
22
  RadixDialog.Overlay,
16
23
  {
@@ -9,7 +9,9 @@ export declare function formatFileSize(bytes: number, locale: string): string;
9
9
  export declare function fileAccepted(file: File, accept?: string[]): boolean;
10
10
  export interface FileRejection {
11
11
  file: File;
12
- reason: 'type' | 'size' | 'count';
12
+ reason: 'type' | 'size' | 'count' | 'custom';
13
+ /** The message returned by `validate` (reason `'custom'`). */
14
+ message?: string;
13
15
  }
14
16
  export interface FileUploadRootProps extends React.HTMLAttributes<HTMLDivElement> {
15
17
  accept?: string[];
@@ -19,6 +21,11 @@ export interface FileUploadRootProps extends React.HTMLAttributes<HTMLDivElement
19
21
  onFilesChange?: (files: File[]) => void;
20
22
  /** Files turned away by `accept`, `maxSize` or `maxFiles`, from the picker or a drop. */
21
23
  onReject?: (rejections: FileRejection[]) => void;
24
+ /**
25
+ * Your own check, run after `accept` and `maxSize`: return a message to reject
26
+ * the file. Client-side checks are a convenience; validate again on the server.
27
+ */
28
+ validate?: (file: File) => string | null | undefined;
22
29
  }
23
30
  export interface FileUploadItemProps extends React.HTMLAttributes<HTMLLIElement> {
24
31
  index: number;
@@ -27,17 +27,19 @@ function useFileUpload() {
27
27
  return ctx;
28
28
  }
29
29
  const Root = React.forwardRef(
30
- ({ className, accept, maxFiles, maxSize, onFilesChange, onReject, children, ...props }, ref) => {
30
+ ({ className, accept, maxFiles, maxSize, onFilesChange, onReject, validate, children, ...props }, ref) => {
31
31
  const [files, setFiles] = React.useState([]);
32
32
  const [rejections, setRejections] = React.useState([]);
33
33
  const [isDragging, setIsDragging] = React.useState(false);
34
34
  const addFiles = React.useCallback(
35
35
  (newFiles) => {
36
36
  const rejected = [];
37
+ let custom;
37
38
  const next = [...files];
38
39
  for (const file of newFiles) {
39
40
  if (!fileAccepted(file, accept)) rejected.push({ file, reason: "type" });
40
41
  else if (maxSize !== void 0 && file.size > maxSize) rejected.push({ file, reason: "size" });
42
+ else if (custom = validate?.(file) || null) rejected.push({ file, reason: "custom", message: custom });
41
43
  else if (maxFiles !== void 0 && next.length >= maxFiles) rejected.push({ file, reason: "count" });
42
44
  else next.push(file);
43
45
  }
@@ -48,7 +50,7 @@ const Root = React.forwardRef(
48
50
  }
49
51
  if (rejected.length) onReject?.(rejected);
50
52
  },
51
- [files, accept, maxFiles, maxSize, onFilesChange, onReject]
53
+ [files, accept, maxFiles, maxSize, onFilesChange, onReject, validate]
52
54
  );
53
55
  const removeFile = React.useCallback(
54
56
  (index) => {
@@ -126,7 +128,7 @@ const Dropzone = React.forwardRef(
126
128
  }
127
129
  )
128
130
  ] }),
129
- /* @__PURE__ */ jsx("div", { id: errorId, role: "alert", className: "empty:hidden px-4 pb-3 text-start text-sm text-[var(--color-error)]", children: rejections.length > 0 && /* @__PURE__ */ jsx("ul", { children: rejections.map(({ file, reason }, i) => /* @__PURE__ */ jsx("li", { children: reason === "type" ? messages.fileTypeRejected(file.name) : reason === "size" ? messages.fileTooLarge(file.name, formatFileSize(maxSize ?? 0, locale)) : messages.tooManyFiles(maxFiles ?? 0) }, `${file.name}-${i}`)) }) })
131
+ /* @__PURE__ */ jsx("div", { id: errorId, role: "alert", className: "empty:hidden px-4 pb-3 text-start text-sm text-[var(--color-error)]", children: rejections.length > 0 && /* @__PURE__ */ jsx("ul", { children: rejections.map(({ file, reason, message }, i) => /* @__PURE__ */ jsx("li", { children: reason === "custom" ? message : reason === "type" ? messages.fileTypeRejected(file.name) : reason === "size" ? messages.fileTooLarge(file.name, formatFileSize(maxSize ?? 0, locale)) : messages.tooManyFiles(maxFiles ?? 0) }, `${file.name}-${i}`)) }) })
130
132
  ]
131
133
  }
132
134
  )
@@ -1,6 +1,7 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { cn } from "../../utils/cn.js";
4
+ import { safeHref, safeSrc } from "../../utils/safeHref.js";
4
5
  import { glassSurfaceClass } from "../../utils/glass.js";
5
6
  const ratioClass = {
6
7
  "16/9": "aspect-video",
@@ -9,7 +10,9 @@ const ratioClass = {
9
10
  "3/4": "aspect-[3/4]"
10
11
  };
11
12
  const MediaCard = React.forwardRef(
12
- ({ src, alt, ratio = "16/9", title, description, eyebrow, href, className, children, ...props }, ref) => {
13
+ ({ src: srcProp, alt, ratio = "16/9", title, description, eyebrow, href: hrefProp, className, children, ...props }, ref) => {
14
+ const src = safeSrc(srcProp);
15
+ const href = safeHref(hrefProp);
13
16
  const hasCaption = title != null || description != null || eyebrow != null || children != null;
14
17
  return /* @__PURE__ */ jsxs(
15
18
  "div",
@@ -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
@@ -14,6 +14,12 @@ export interface NexubProviderProps {
14
14
  dateLocale?: Locale;
15
15
  /** Overrides for the default English strings. */
16
16
  messages?: Partial<NexubMessages>;
17
+ /**
18
+ * CSP nonce for the `<style>` tags Radix overlays inject at runtime (scroll
19
+ * lock, scrollbar gap). Pass the same per-request nonce as your
20
+ * `style-src-elem` so they work without `'unsafe-inline'`.
21
+ */
22
+ nonce?: string;
17
23
  children?: React.ReactNode;
18
24
  }
19
25
  /**
@@ -21,7 +27,7 @@ export interface NexubProviderProps {
21
27
  * It does not set `dir`/`lang` on <html> — the app does that (as it renders
22
28
  * <html>), so the page and CSS `rtl:` variants agree with this provider.
23
29
  */
24
- export declare function NexubProvider({ locale, dir, dateLocale, messages, children }: NexubProviderProps): React.JSX.Element;
30
+ export declare function NexubProvider({ locale, dir, dateLocale, messages, nonce, children }: NexubProviderProps): React.JSX.Element;
25
31
  /** The active strings: defaults merged with the nearest NexubProvider's overrides. */
26
32
  export declare function useNexubMessages(): NexubMessages;
27
33
  /** The active locale (default 'en'), direction and optional date-fns Locale. */
@@ -2,6 +2,7 @@
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { DirectionProvider } from "@radix-ui/react-direction";
5
+ import { setNonce } from "get-nonce";
5
6
  import { defaultMessages, directionOf } from "@nexub/core";
6
7
  const NexubContext = React.createContext({
7
8
  locale: void 0,
@@ -9,7 +10,8 @@ const NexubContext = React.createContext({
9
10
  dateLocale: void 0,
10
11
  messages: defaultMessages
11
12
  });
12
- function NexubProvider({ locale = "en", dir, dateLocale, messages, children }) {
13
+ function NexubProvider({ locale = "en", dir, dateLocale, messages, nonce, children }) {
14
+ if (nonce) setNonce(nonce);
13
15
  const resolvedDir = dir ?? directionOf(locale);
14
16
  const value = React.useMemo(
15
17
  () => ({ locale, dir: resolvedDir, dateLocale, messages: { ...defaultMessages, ...messages } }),
@@ -14,12 +14,16 @@ const PasswordInput = React.forwardRef(
14
14
  onChange: onVisibleChange
15
15
  });
16
16
  const messages = useNexubMessages();
17
+ const autoId = React.useId();
18
+ const inputId = props.id ?? autoId;
17
19
  const toggleButton = /* @__PURE__ */ jsx(
18
20
  "button",
19
21
  {
20
22
  type: "button",
21
23
  onClick: () => setShowPassword(!showPassword),
22
- "aria-label": showPassword ? messages.hidePassword : messages.showPassword,
24
+ "aria-label": messages.showPassword,
25
+ "aria-pressed": showPassword,
26
+ "aria-controls": inputId,
23
27
  className: cn(
24
28
  "shrink-0 rounded-full text-[var(--color-on-surface-muted)] hover:text-[var(--color-on-surface)] transition-colors"
25
29
  ),
@@ -30,6 +34,7 @@ const PasswordInput = React.forwardRef(
30
34
  Input,
31
35
  {
32
36
  ref,
37
+ id: inputId,
33
38
  autoComplete: "current-password",
34
39
  ...props,
35
40
  type: showPassword ? "text" : "password",
@@ -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.
@@ -2,6 +2,7 @@
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect.js";
5
+ const PRODUCTS = ["leap", "bookub", "payub", "founderub"];
5
6
  const GLASS_MODES = ["clear", "balanced", "tinted", "off"];
6
7
  const ThemeContext = React.createContext(void 0);
7
8
  ThemeContext.displayName = "ThemeContext";
@@ -165,7 +166,7 @@ function useTheme() {
165
166
  const jsString = (s) => JSON.stringify(s).replace(/</g, "\\u003c");
166
167
  function themeScriptSource(storageKey, productStorageKey, glassStorageKey = "nexub-glass", contrastStorageKey) {
167
168
  const contrast = contrastStorageKey ? `var c=localStorage.getItem(${jsString(contrastStorageKey)});if(['standard','medium','high'].indexOf(c)>-1)d.setAttribute('data-contrast',c);` : "";
168
- const product = productStorageKey ? `var b=localStorage.getItem(${jsString(productStorageKey)});if(b&&b!=='base')d.setAttribute('data-product',b);` : "";
169
+ const product = productStorageKey ? `var b=localStorage.getItem(${jsString(productStorageKey)});if(${JSON.stringify(PRODUCTS)}.indexOf(b)>-1)d.setAttribute('data-product',b);` : "";
169
170
  const glass = `var g=localStorage.getItem(${jsString(glassStorageKey)});if(${JSON.stringify(GLASS_MODES)}.indexOf(g)>-1)d.setAttribute('data-glass',g);`;
170
171
  return `try{var d=document.documentElement,t=localStorage.getItem(${jsString(storageKey)});if(t==='light'||t==='dark')d.setAttribute('data-theme',t);${product}${glass}${contrast}}catch(e){}`;
171
172
  }
@@ -1,6 +1,19 @@
1
+ import * as React from 'react';
1
2
  /**
2
3
  * CSP-safe page scroll lock: toggles `nexub-scroll-lock` (from @nexub/themes/base)
3
4
  * on <html> instead of injecting a <style> tag. On while mounted; counted, so
4
5
  * nested overlays (a Sheet and a CommandPalette) share one lock.
5
6
  */
6
7
  export declare function useClassScrollLock(): void;
8
+ export type ScrollLock = 'style' | 'class';
9
+ /**
10
+ * Which scroll lock a modal Root asked for. `'style'` (7.x default) is Radix's
11
+ * react-remove-scroll, which injects a <style> (needs a CSP nonce, see
12
+ * NexubProvider `nonce`); `'class'` is the class lock above, no <style> at all.
13
+ */
14
+ export declare const ScrollLockContext: React.Context<{
15
+ scrollLock: ScrollLock;
16
+ modal: boolean;
17
+ }>;
18
+ /** Plain overlay for scrollLock="class": Radix's RemoveScroll lives in its Overlay. */
19
+ export declare const ClassLockOverlay: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
@@ -13,6 +13,18 @@ function useClassScrollLock() {
13
13
  };
14
14
  }, []);
15
15
  }
16
+ const ScrollLockContext = React.createContext({
17
+ scrollLock: "style",
18
+ modal: true
19
+ });
20
+ const ClassLockOverlay = React.forwardRef(
21
+ function ClassLockOverlay2(props, ref) {
22
+ useClassScrollLock();
23
+ return React.createElement("div", { ref, "data-state": "open", ...props });
24
+ }
25
+ );
16
26
  export {
27
+ ClassLockOverlay,
28
+ ScrollLockContext,
17
29
  useClassScrollLock
18
30
  };
@@ -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
  */