@nexub/react 5.0.0 → 5.1.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": "5.0.0",
2
+ "version": "5.1.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 on every pull request; a new violation fails the build.",
@@ -10,9 +10,16 @@ export interface AutocompleteProps {
10
10
  placeholder?: string;
11
11
  emptyText?: string;
12
12
  loading?: boolean;
13
+ /** Controlled input text. */
14
+ inputValue?: string;
15
+ /**
16
+ * Called as the user types. When set, the options are taken as already
17
+ * filtered (fetched for this text), so the built-in filtering is off.
18
+ */
19
+ onInputChange?: (text: string) => void;
13
20
  className?: string;
14
21
  }
15
- export declare function Autocomplete({ options, value, onValueChange, placeholder, emptyText, loading, className, }: AutocompleteProps): React.JSX.Element;
22
+ export declare function Autocomplete({ options, value, onValueChange, placeholder, emptyText, loading, inputValue, onInputChange, className, }: AutocompleteProps): React.JSX.Element;
16
23
  export declare namespace Autocomplete {
17
24
  var displayName: string;
18
25
  }
@@ -10,17 +10,24 @@ function Autocomplete({
10
10
  placeholder,
11
11
  emptyText,
12
12
  loading = false,
13
+ inputValue,
14
+ onInputChange,
13
15
  className
14
16
  }) {
15
17
  const messages = useNexubMessages();
16
18
  const selectedLabel = options.find((o) => o.value === value)?.label ?? "";
17
- const [search, setSearch] = React.useState(selectedLabel);
19
+ const [innerSearch, setInnerSearch] = React.useState(selectedLabel);
18
20
  const [prevValue, setPrevValue] = React.useState(value);
21
+ const search = inputValue ?? innerSearch;
22
+ const setSearch = (text) => {
23
+ setInnerSearch(text);
24
+ onInputChange?.(text);
25
+ };
19
26
  if (value !== prevValue) {
20
27
  setPrevValue(value);
21
- setSearch(selectedLabel);
28
+ setInnerSearch(selectedLabel);
22
29
  }
23
- return /* @__PURE__ */ jsxs(Combobox.Root, { className, children: [
30
+ return /* @__PURE__ */ jsxs(Combobox.Root, { className, shouldFilter: !onInputChange, children: [
24
31
  /* @__PURE__ */ jsx(Combobox.Input, { placeholder: placeholder ?? messages.searchPlaceholder, value: search, onValueChange: setSearch }),
25
32
  /* @__PURE__ */ jsx(Combobox.List, { children: loading ? /* @__PURE__ */ jsx("div", { className: "py-6 text-center text-sm text-[var(--color-on-surface-muted)]", children: messages.loading }) : /* @__PURE__ */ jsxs(Fragment, { children: [
26
33
  /* @__PURE__ */ jsx(Combobox.Empty, { children: emptyText ?? messages.noResults }),
@@ -1,4 +1,17 @@
1
1
  import * as React from 'react';
2
+ export interface BreadcrumbTrailItem {
3
+ label: React.ReactNode;
4
+ href?: string;
5
+ }
6
+ export interface BreadcrumbTrailProps extends React.HTMLAttributes<HTMLElement> {
7
+ /** The path, root first. The last item is the current page. */
8
+ items: BreadcrumbTrailItem[];
9
+ /**
10
+ * Most items shown. Longer trails keep the first item and the last
11
+ * `maxItems - 1`, and put the rest in a "More" menu. Default 4.
12
+ */
13
+ maxItems?: number;
14
+ }
2
15
  export declare const Breadcrumb: {
3
16
  Root: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLElement> & React.RefAttributes<HTMLElement>>;
4
17
  List: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLOListElement> & React.RefAttributes<HTMLOListElement>>;
@@ -18,4 +31,5 @@ export declare const Breadcrumb: {
18
31
  ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>): React.JSX.Element;
19
32
  displayName: string;
20
33
  };
34
+ Trail: React.ForwardRefExoticComponent<BreadcrumbTrailProps & React.RefAttributes<HTMLElement>>;
21
35
  };
@@ -1,9 +1,10 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ 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
6
  import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
7
+ import { DropdownMenu } from "../DropdownMenu/DropdownMenu.js";
7
8
  const Root = React.forwardRef(
8
9
  ({ className, ...props }, ref) => /* @__PURE__ */ jsx("nav", { ref, "aria-label": useNexubMessages().breadcrumb, className: cn("", className), ...props })
9
10
  );
@@ -77,7 +78,34 @@ const Ellipsis = ({ className, ...props }) => /* @__PURE__ */ jsxs(
77
78
  }
78
79
  );
79
80
  Ellipsis.displayName = "Breadcrumb.Ellipsis";
80
- const Breadcrumb = { Root, List, Item, Link, Page, Separator, Ellipsis };
81
+ const Trail = React.forwardRef(({ items, maxItems = 4, ...props }, ref) => {
82
+ const messages = useNexubMessages();
83
+ const keep = Math.max(2, maxItems);
84
+ const collapsed = items.length > keep;
85
+ const hidden = collapsed ? items.slice(1, items.length - (keep - 1)) : [];
86
+ 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
+ return /* @__PURE__ */ jsx(Root, { ref, ...props, children: /* @__PURE__ */ jsx(List, { children: shown.map((item, i) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
89
+ i > 0 && /* @__PURE__ */ jsx(Separator, {}),
90
+ /* @__PURE__ */ jsx(Item, { children: crumb(item, i === shown.length - 1) }),
91
+ i === 0 && collapsed && /* @__PURE__ */ jsxs(Fragment, { children: [
92
+ /* @__PURE__ */ jsx(Separator, {}),
93
+ /* @__PURE__ */ jsx(Item, { children: /* @__PURE__ */ jsxs(DropdownMenu.Root, { children: [
94
+ /* @__PURE__ */ jsx(
95
+ DropdownMenu.Trigger,
96
+ {
97
+ "aria-label": messages.breadcrumbMore,
98
+ className: "nx-state-layer flex h-9 w-9 items-center justify-center rounded-shape-control",
99
+ children: /* @__PURE__ */ jsx(MoreHorizontal, { size: "sm", "aria-hidden": "true" })
100
+ }
101
+ ),
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
+ ] }) })
104
+ ] })
105
+ ] }, i)) }) });
106
+ });
107
+ Trail.displayName = "Breadcrumb.Trail";
108
+ const Breadcrumb = { Root, List, Item, Link, Page, Separator, Ellipsis, Trail };
81
109
  export {
82
110
  Breadcrumb
83
111
  };
@@ -1 +1,2 @@
1
1
  export { Breadcrumb } from './Breadcrumb.js';
2
+ export type { BreadcrumbTrailItem, BreadcrumbTrailProps } from './Breadcrumb.js';
@@ -3,6 +3,21 @@ import { jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Command } from "cmdk";
5
5
  import { cn } from "../../utils/cn.js";
6
+ function scrollWithinList(el) {
7
+ if (!el) return;
8
+ el.scrollIntoView = () => {
9
+ const list = el.closest("[cmdk-list]");
10
+ if (!list) return;
11
+ const l = list.getBoundingClientRect();
12
+ const r = el.getBoundingClientRect();
13
+ if (r.top < l.top) list.scrollTop -= l.top - r.top;
14
+ else if (r.bottom > l.bottom) list.scrollTop += r.bottom - l.bottom;
15
+ };
16
+ }
17
+ function setRef(ref, value) {
18
+ if (typeof ref === "function") ref(value);
19
+ else if (ref) ref.current = value;
20
+ }
6
21
  const Root = React.forwardRef(
7
22
  ({ className, ...props }, ref) => /* @__PURE__ */ jsx(
8
23
  Command,
@@ -57,7 +72,10 @@ const Group = React.forwardRef(
57
72
  ({ className, ...props }, ref) => /* @__PURE__ */ jsx(
58
73
  Command.Group,
59
74
  {
60
- ref,
75
+ ref: (el) => {
76
+ scrollWithinList(el?.querySelector("[cmdk-group-heading]"));
77
+ setRef(ref, el);
78
+ },
61
79
  className: cn(
62
80
  "overflow-hidden p-1 text-[var(--color-on-surface)] [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-[var(--color-on-surface-muted)]",
63
81
  className
@@ -71,7 +89,10 @@ const Item = React.forwardRef(
71
89
  ({ className, ...props }, ref) => /* @__PURE__ */ jsx(
72
90
  Command.Item,
73
91
  {
74
- ref,
92
+ ref: (el) => {
93
+ scrollWithinList(el);
94
+ setRef(ref, el);
95
+ },
75
96
  className: cn(
76
97
  "relative flex cursor-default select-none items-center rounded-[10px] px-2.5 py-2 text-body-sm outline-none",
77
98
  // Highlight = the on-surface state layer at hover opacity.
@@ -12,6 +12,11 @@ export interface CommandPaletteRootProps extends RadixDialog.DialogProps {
12
12
  scrollLock?: 'style' | 'class';
13
13
  /** `false` when the items are already filtered (e.g. async search results). @default true */
14
14
  shouldFilter?: boolean;
15
+ /**
16
+ * A letter that toggles the palette with Cmd (macOS) or Ctrl, anywhere on
17
+ * the page, e.g. `'k'` for Cmd+K / Ctrl+K. Off by default.
18
+ */
19
+ shortcut?: string;
15
20
  }
16
21
  export declare const CommandPalette: {
17
22
  Root: React.ForwardRefExoticComponent<CommandPaletteRootProps & React.RefAttributes<HTMLDivElement>>;
@@ -8,17 +8,30 @@ import { cn } from "../../utils/cn.js";
8
8
  import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
9
9
  import { overlaySurface, scrim } from "../../utils/surface.js";
10
10
  import { useClassScrollLock } from "../../hooks/useClassScrollLock.js";
11
+ import { useControllableState } from "../../hooks/useControllableState.js";
11
12
  const overlayClasses = cn("fixed inset-0 z-[var(--z-modal)] nx-motion", scrim);
12
13
  function ClassLockOverlay() {
13
14
  useClassScrollLock();
14
15
  return /* @__PURE__ */ jsx("div", { "data-state": "open", className: overlayClasses });
15
16
  }
16
17
  const Root = React.forwardRef(
17
- ({ children, label: labelProp, scrollLock = "style", shouldFilter, ...props }, ref) => {
18
+ ({ children, label: labelProp, scrollLock = "style", shouldFilter, shortcut, open: openProp, defaultOpen, onOpenChange, ...props }, ref) => {
18
19
  const messages = useNexubMessages();
20
+ const [open = false, setOpen] = useControllableState({ value: openProp, defaultValue: defaultOpen, onChange: onOpenChange });
21
+ React.useEffect(() => {
22
+ if (!shortcut) return;
23
+ const onKeyDown = (e) => {
24
+ if (e.defaultPrevented || e.altKey || e.shiftKey || !(e.metaKey || e.ctrlKey)) return;
25
+ if (e.key.toLowerCase() !== shortcut.toLowerCase()) return;
26
+ e.preventDefault();
27
+ setOpen(!open);
28
+ };
29
+ document.addEventListener("keydown", onKeyDown);
30
+ return () => document.removeEventListener("keydown", onKeyDown);
31
+ }, [shortcut, open, setOpen]);
19
32
  const label = labelProp ?? messages.commandPalette;
20
33
  const returnFocusRef = React.useRef(null);
21
- return /* @__PURE__ */ jsx(RadixDialog.Root, { ...props, children: /* @__PURE__ */ jsxs(RadixDialog.Portal, { children: [
34
+ return /* @__PURE__ */ jsx(RadixDialog.Root, { open, onOpenChange: setOpen, ...props, children: /* @__PURE__ */ jsxs(RadixDialog.Portal, { children: [
22
35
  scrollLock === "class" ? /* @__PURE__ */ jsx(ClassLockOverlay, {}) : /* @__PURE__ */ jsx(RadixDialog.Overlay, { className: overlayClasses }),
23
36
  /* @__PURE__ */ jsxs(
24
37
  RadixDialog.Content,
@@ -105,11 +105,16 @@ const Description = React.forwardRef(
105
105
  );
106
106
  Description.displayName = "Drawer.Description";
107
107
  const Header = React.forwardRef(
108
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { ref, className: cn("flex flex-col gap-1.5 p-6", className), ...props })
108
+ ({ className, ...props }, ref) => (
109
+ // Sticky on solid panels: long content scrolls under the header and footer,
110
+ // which inherit the panel background. Glass panels keep one frosted layer
111
+ // (a second translucent layer reads as an opaque band), so they don't stick.
112
+ /* @__PURE__ */ jsx("div", { ref, className: cn("sticky top-0 z-10 flex flex-col gap-1.5 bg-inherit p-6 [[data-material=glass]_&]:static [[data-material=glass]_&]:bg-transparent", className), ...props })
113
+ )
109
114
  );
110
115
  Header.displayName = "Drawer.Header";
111
116
  const Footer = React.forwardRef(
112
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { ref, className: cn("mt-auto flex flex-col gap-2 p-6", className), ...props })
117
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { ref, className: cn("sticky bottom-0 z-10 mt-auto flex flex-col gap-2 bg-inherit p-6 [[data-material=glass]_&]:static [[data-material=glass]_&]:bg-transparent", className), ...props })
113
118
  );
114
119
  Footer.displayName = "Drawer.Footer";
115
120
  const Drawer = {
@@ -1,10 +1,24 @@
1
1
  import * as React from 'react';
2
2
  /** Localised file size ("12.3 kB", "1,5 Mo"), 1024-based as before. */
3
3
  export declare function formatFileSize(bytes: number, locale: string): string;
4
+ /**
5
+ * Whether `file` matches an `accept` list: MIME types (`image/png`), wildcards
6
+ * (`image/*`) and extensions (`.pdf`), like the native input. A UX filter only:
7
+ * the server must still check what it receives.
8
+ */
9
+ export declare function fileAccepted(file: File, accept?: string[]): boolean;
10
+ export interface FileRejection {
11
+ file: File;
12
+ reason: 'type' | 'size' | 'count';
13
+ }
4
14
  export interface FileUploadRootProps extends React.HTMLAttributes<HTMLDivElement> {
5
15
  accept?: string[];
6
16
  maxFiles?: number;
17
+ /** Largest file in bytes. Bigger files are rejected with a message. */
18
+ maxSize?: number;
7
19
  onFilesChange?: (files: File[]) => void;
20
+ /** Files turned away by `accept`, `maxSize` or `maxFiles`, from the picker or a drop. */
21
+ onReject?: (rejections: FileRejection[]) => void;
8
22
  }
9
23
  export declare const FileUpload: {
10
24
  Root: React.ForwardRefExoticComponent<FileUploadRootProps & React.RefAttributes<HTMLDivElement>>;
@@ -8,6 +8,17 @@ function formatFileSize(bytes, locale) {
8
8
  const [value, unit] = bytes < 1024 ? [bytes, "byte"] : bytes < 1024 * 1024 ? [bytes / 1024, "kilobyte"] : [bytes / (1024 * 1024), "megabyte"];
9
9
  return new Intl.NumberFormat(locale, { style: "unit", unit, unitDisplay: "short", maximumFractionDigits: 1 }).format(value);
10
10
  }
11
+ function fileAccepted(file, accept) {
12
+ if (!accept?.length) return true;
13
+ const name = file.name.toLowerCase();
14
+ const type = file.type.toLowerCase();
15
+ return accept.some((raw) => {
16
+ const a = raw.trim().toLowerCase();
17
+ if (a.startsWith(".")) return name.endsWith(a);
18
+ if (a.endsWith("/*")) return type.startsWith(a.slice(0, -1));
19
+ return type === a;
20
+ });
21
+ }
11
22
  const FileUploadContext = React.createContext(null);
12
23
  function useFileUpload() {
13
24
  const ctx = React.useContext(FileUploadContext);
@@ -15,32 +26,41 @@ function useFileUpload() {
15
26
  return ctx;
16
27
  }
17
28
  const Root = React.forwardRef(
18
- ({ className, accept, maxFiles, onFilesChange, children, ...props }, ref) => {
29
+ ({ className, accept, maxFiles, maxSize, onFilesChange, onReject, children, ...props }, ref) => {
19
30
  const [files, setFiles] = React.useState([]);
31
+ const [rejections, setRejections] = React.useState([]);
20
32
  const [isDragging, setIsDragging] = React.useState(false);
21
33
  const addFiles = React.useCallback(
22
34
  (newFiles) => {
23
- setFiles((prev) => {
24
- const next = [...prev, ...newFiles].slice(0, maxFiles);
35
+ const rejected = [];
36
+ const next = [...files];
37
+ for (const file of newFiles) {
38
+ if (!fileAccepted(file, accept)) rejected.push({ file, reason: "type" });
39
+ else if (maxSize !== void 0 && file.size > maxSize) rejected.push({ file, reason: "size" });
40
+ else if (maxFiles !== void 0 && next.length >= maxFiles) rejected.push({ file, reason: "count" });
41
+ else next.push(file);
42
+ }
43
+ setRejections(rejected);
44
+ if (next.length !== files.length) {
45
+ setFiles(next);
25
46
  onFilesChange?.(next);
26
- return next;
27
- });
47
+ }
48
+ if (rejected.length) onReject?.(rejected);
28
49
  },
29
- [maxFiles, onFilesChange]
50
+ [files, accept, maxFiles, maxSize, onFilesChange, onReject]
30
51
  );
31
52
  const removeFile = React.useCallback(
32
53
  (index) => {
33
- setFiles((prev) => {
34
- const next = prev.filter((_, i) => i !== index);
35
- onFilesChange?.(next);
36
- return next;
37
- });
54
+ const next = files.filter((_, i) => i !== index);
55
+ setFiles(next);
56
+ setRejections([]);
57
+ onFilesChange?.(next);
38
58
  },
39
- [onFilesChange]
59
+ [files, onFilesChange]
40
60
  );
41
61
  const contextValue = React.useMemo(
42
- () => ({ accept, maxFiles, onFilesChange, files, addFiles, removeFile, isDragging, setIsDragging }),
43
- [accept, maxFiles, onFilesChange, files, addFiles, removeFile, isDragging, setIsDragging]
62
+ () => ({ accept, maxFiles, maxSize, onFilesChange, files, rejections, addFiles, removeFile, isDragging, setIsDragging }),
63
+ [accept, maxFiles, maxSize, onFilesChange, files, rejections, addFiles, removeFile, isDragging, setIsDragging]
44
64
  );
45
65
  return /* @__PURE__ */ jsx(FileUploadContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx("div", { ref, className: cn("flex flex-col gap-4", className), ...props, children }) });
46
66
  }
@@ -48,8 +68,10 @@ const Root = React.forwardRef(
48
68
  Root.displayName = "FileUpload.Root";
49
69
  const Dropzone = React.forwardRef(
50
70
  ({ className, children, ...props }, ref) => {
51
- const { accept, addFiles, isDragging, setIsDragging } = useFileUpload();
71
+ const { accept, maxFiles, maxSize, rejections, addFiles, isDragging, setIsDragging } = useFileUpload();
52
72
  const messages = useNexubMessages();
73
+ const { locale } = useLocale();
74
+ const errorId = React.useId();
53
75
  const inputRef = React.useRef(null);
54
76
  function handleDrop(e) {
55
77
  e.preventDefault();
@@ -61,7 +83,7 @@ const Dropzone = React.forwardRef(
61
83
  // A <label> around the native file input: click, Enter and Space open the
62
84
  // picker natively, the visible text is the accessible name, and the
63
85
  // focus ring follows the input's keyboard focus onto the drop zone.
64
- /* @__PURE__ */ jsx(
86
+ /* @__PURE__ */ jsxs(
65
87
  "div",
66
88
  {
67
89
  ref,
@@ -79,28 +101,32 @@ const Dropzone = React.forwardRef(
79
101
  className
80
102
  ),
81
103
  ...props,
82
- children: /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer flex-col items-center justify-center gap-2 p-8", children: [
83
- /* @__PURE__ */ jsx(Upload, { size: "lg", className: "text-[var(--color-on-surface-muted)]", "aria-hidden": "true" }),
84
- children ?? /* @__PURE__ */ jsxs("span", { className: "text-sm text-[var(--color-on-surface-muted)]", children: [
85
- /* @__PURE__ */ jsx("span", { className: "font-semibold text-[var(--color-primary)]", children: messages.clickToUpload }),
86
- " ",
87
- messages.orDragAndDrop
88
- ] }),
89
- /* @__PURE__ */ jsx(
90
- "input",
91
- {
92
- ref: inputRef,
93
- type: "file",
94
- className: "sr-only",
95
- multiple: true,
96
- accept: accept?.join(","),
97
- onChange: (e) => {
98
- if (e.target.files) addFiles(Array.from(e.target.files));
99
- e.target.value = "";
104
+ children: [
105
+ /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer flex-col items-center justify-center gap-2 p-8", children: [
106
+ /* @__PURE__ */ jsx(Upload, { size: "lg", className: "text-[var(--color-on-surface-muted)]", "aria-hidden": "true" }),
107
+ children ?? /* @__PURE__ */ jsxs("span", { className: "text-sm text-[var(--color-on-surface-muted)]", children: [
108
+ /* @__PURE__ */ jsx("span", { className: "font-semibold text-[var(--color-primary)]", children: messages.clickToUpload }),
109
+ " ",
110
+ messages.orDragAndDrop
111
+ ] }),
112
+ /* @__PURE__ */ jsx(
113
+ "input",
114
+ {
115
+ ref: inputRef,
116
+ type: "file",
117
+ className: "sr-only",
118
+ multiple: true,
119
+ accept: accept?.join(","),
120
+ onChange: (e) => {
121
+ if (e.target.files) addFiles(Array.from(e.target.files));
122
+ e.target.value = "";
123
+ },
124
+ "aria-describedby": rejections.length ? errorId : void 0
100
125
  }
101
- }
102
- )
103
- ] })
126
+ )
127
+ ] }),
128
+ /* @__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}`)) }) })
129
+ ]
104
130
  }
105
131
  )
106
132
  );
@@ -152,5 +178,6 @@ Item.displayName = "FileUpload.Item";
152
178
  const FileUpload = { Root, Dropzone, List, Item };
153
179
  export {
154
180
  FileUpload,
181
+ fileAccepted,
155
182
  formatFileSize
156
183
  };
@@ -1,2 +1,2 @@
1
- export { FileUpload } from './FileUpload.js';
2
- export type { FileUploadRootProps } from './FileUpload.js';
1
+ export { FileUpload, fileAccepted } from './FileUpload.js';
2
+ export type { FileUploadRootProps, FileRejection } from './FileUpload.js';
@@ -1,4 +1,5 @@
1
- import { FileUpload } from "./FileUpload.js";
1
+ import { FileUpload, fileAccepted } from "./FileUpload.js";
2
2
  export {
3
- FileUpload
3
+ FileUpload,
4
+ fileAccepted
4
5
  };
@@ -1,5 +1,8 @@
1
1
  import * as React from 'react';
2
+ export type ToasterPosition = 'top-start' | 'top-center' | 'top-end' | 'bottom-start' | 'bottom-center' | 'bottom-end';
2
3
  export interface ToasterProps {
4
+ /** Where the stack sits. Default `top-end`; start/end follow the reading direction. */
5
+ position?: ToasterPosition;
3
6
  /** Always show the toasts as a list instead of a collapsed stack. */
4
7
  expand?: boolean;
5
8
  /** How many toasts are visible at once; older ones are visually hidden. Default 3. */
@@ -15,7 +18,7 @@ export interface ToasterProps {
15
18
  * (--nx-toast-index / --nx-toast-offset per toast, --nx-front-height and
16
19
  * --nx-stack-height on the viewport), never a style attribute (strict CSP).
17
20
  */
18
- export declare function Toaster({ expand, visibleToasts }: ToasterProps): React.JSX.Element;
21
+ export declare function Toaster({ expand, visibleToasts, position }: ToasterProps): React.JSX.Element;
19
22
  export declare namespace Toaster {
20
23
  var displayName: string;
21
24
  }
@@ -15,7 +15,19 @@ const toneMap = {
15
15
  neutral: ""
16
16
  };
17
17
  const GAP = 8;
18
- function Toaster({ expand = false, visibleToasts = 3 }) {
18
+ const vertical = {
19
+ top: { viewport: "top-0", toast: "top-4 origin-bottom", enter: "[--nx-y:calc(-100%-1rem)]" },
20
+ bottom: { viewport: "bottom-0 [--nx-dir:-1]", toast: "bottom-4 origin-top", enter: "[--nx-y:calc(100%+1rem)]" }
21
+ };
22
+ const horizontal = {
23
+ start: { viewport: "start-0", enter: "[--nx-x:calc(-100%-1rem)] rtl:[--nx-x:calc(100%+1rem)]" },
24
+ center: { viewport: "inset-x-0 mx-auto", enter: "" },
25
+ end: { viewport: "end-0", enter: "[--nx-x:calc(100%+1rem)] rtl:[--nx-x:calc(-100%-1rem)]" }
26
+ };
27
+ function Toaster({ expand = false, visibleToasts = 3, position = "top-end" }) {
28
+ const [side, align] = position.split("-");
29
+ const v = vertical[side];
30
+ const h = horizontal[align];
19
31
  const toasts = useToastStore();
20
32
  const messages = useNexubMessages();
21
33
  const { dir } = useLocale();
@@ -44,12 +56,12 @@ function Toaster({ expand = false, visibleToasts = 3 }) {
44
56
  const el = ol.querySelector(`[data-toast-id="${id}"]`);
45
57
  if (!el) return;
46
58
  if (i === 0 || expanded2 || !heights.current.has(id)) heights.current.set(id, el.offsetHeight);
47
- const h = heights.current.get(id);
59
+ const h2 = heights.current.get(id);
48
60
  el.style.setProperty("--nx-toast-index", String(i));
49
61
  el.style.setProperty("--nx-toast-offset", `${offset}px`);
50
- if (i === 0) front = h;
51
- if (i < visibleToasts2) stack = offset + h;
52
- offset += h + GAP;
62
+ if (i === 0) front = h2;
63
+ if (i < visibleToasts2) stack = offset + h2;
64
+ offset += h2 + GAP;
53
65
  });
54
66
  ol.style.setProperty("--nx-front-height", `${front}px`);
55
67
  ol.style.setProperty("--nx-stack-height", `${expanded2 ? stack : front}px`);
@@ -92,7 +104,7 @@ function Toaster({ expand = false, visibleToasts = 3 }) {
92
104
  setFocused(false);
93
105
  }
94
106
  }, [order.length]);
95
- return /* @__PURE__ */ jsxs(RadixToast.Provider, { swipeDirection: dir === "rtl" ? "left" : "right", children: [
107
+ return /* @__PURE__ */ jsxs(RadixToast.Provider, { swipeDirection: align === "start" === (dir === "rtl") ? "right" : "left", children: [
96
108
  toasts.map((t) => {
97
109
  const i = order.indexOf(t.id);
98
110
  const action = t.action ?? (t.undo && { label: t.undo.label ?? messages.undo, onClick: t.undo.onUndo });
@@ -118,12 +130,13 @@ function Toaster({ expand = false, visibleToasts = 3 }) {
118
130
  timers.current.add(timer);
119
131
  },
120
132
  className: cn(
121
- "group pointer-events-auto absolute inset-x-4 top-4 flex items-center justify-between gap-3 overflow-hidden rounded-shape-container bg-[var(--color-inverse-surface)] py-3 ps-4 pe-2 text-[var(--color-inverse-on-surface)] shadow-elevation-2",
133
+ "group pointer-events-auto absolute inset-x-4 flex items-center justify-between gap-3 overflow-hidden rounded-shape-container bg-[var(--color-inverse-surface)] py-3 ps-4 pe-2 text-[var(--color-inverse-on-surface)] shadow-elevation-2",
122
134
  // Stack: older toasts sit behind, scaled from their bottom edge so it peeks out.
123
135
  // Transition-only timing: duration-*/ease-* utilities would also retime nx-motion's enter/exit keyframes.
124
- "origin-bottom transition-[transform,opacity] [transition-duration:var(--motion-spring-spatial-default-duration)] [transition-timing-function:var(--motion-spring-spatial-default-easing)]",
125
- "translate-y-[calc(var(--nx-toast-index,0)*var(--nx-toast-step))] scale-[calc(1_-_var(--nx-toast-index,0)*var(--nx-toast-shrink))]",
126
- "group-data-[expanded=true]/toaster:translate-y-[var(--nx-toast-offset,0px)] group-data-[expanded=true]/toaster:scale-100",
136
+ v.toast,
137
+ "transition-[transform,opacity] [transition-duration:var(--motion-spring-spatial-default-duration)] [transition-timing-function:var(--motion-spring-spatial-default-easing)]",
138
+ "translate-y-[calc(var(--nx-toast-index,0)*var(--nx-toast-step)*var(--nx-dir,1))] scale-[calc(1_-_var(--nx-toast-index,0)*var(--nx-toast-shrink))]",
139
+ "group-data-[expanded=true]/toaster:translate-y-[calc(var(--nx-toast-offset,0px)*var(--nx-dir,1))] group-data-[expanded=true]/toaster:scale-100",
127
140
  // Collapsing settles faster than expanding.
128
141
  "group-data-[expanded=false]/toaster:[transition-duration:var(--motion-spring-effects-fast-duration)] group-data-[expanded=false]/toaster:[transition-timing-function:var(--motion-spring-effects-fast-easing)]",
129
142
  "group-data-[expanded=false]/toaster:data-[front=false]:h-[var(--nx-front-height)]",
@@ -135,7 +148,8 @@ function Toaster({ expand = false, visibleToasts = 3 }) {
135
148
  "motion-reduce:[transition-property:opacity] motion-reduce:group-data-[expanded=false]/toaster:data-[front=false]:pointer-events-none motion-reduce:group-data-[expanded=false]/toaster:data-[front=false]:opacity-0",
136
149
  "data-[state=closed]:duration-effects-fast",
137
150
  "data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none",
138
- "nx-motion [--nx-x:calc(100%+1rem)] rtl:[--nx-x:calc(-100%-1rem)]",
151
+ "nx-motion",
152
+ align === "center" ? v.enter : h.enter,
139
153
  toneMap[t.tone ?? "neutral"]
140
154
  ),
141
155
  duration: t.duration ?? 5e3,
@@ -168,8 +182,11 @@ function Toaster({ expand = false, visibleToasts = 3 }) {
168
182
  ref: viewportRef,
169
183
  label: messages.notifications,
170
184
  "data-expanded": expanded,
185
+ "data-position": position,
171
186
  className: cn(
172
- "group/toaster fixed top-0 end-0 z-[var(--z-toast,2000)] max-h-screen w-full p-4 sm:max-w-sm",
187
+ v.viewport,
188
+ h.viewport,
189
+ "group/toaster fixed z-[var(--z-toast,2000)] max-h-screen w-full p-4 sm:max-w-sm",
173
190
  // Collapsed: the front toast's height (older edges peek below); expanded: the whole list.
174
191
  "h-[calc(var(--nx-stack-height,0px)+2rem)]",
175
192
  "[--nx-toast-step:10px] [--nx-toast-shrink:0.05] motion-reduce:[--nx-toast-step:0px] motion-reduce:[--nx-toast-shrink:0]"
@@ -1,5 +1,5 @@
1
1
  export { Toast } from './Toast.js';
2
2
  export { Toaster } from './Toaster.js';
3
- export type { ToasterProps } from './Toaster.js';
3
+ export type { ToasterProps, ToasterPosition } from './Toaster.js';
4
4
  export { toast, useToastStore } from './useToast.js';
5
5
  export type { ToastItem, ToastTone, UndoableToastOptions } from './useToast.js';
package/dist/index.d.ts CHANGED
@@ -182,7 +182,7 @@ export type { ToolbarRootProps } from './components/Toolbar/index.js';
182
182
  export { Toast } from './components/Toast/index.js';
183
183
  export { Toaster } from './components/Toast/index.js';
184
184
  export { toast, useToastStore } from './components/Toast/index.js';
185
- export type { ToastItem, ToasterProps, UndoableToastOptions } from './components/Toast/index.js';
185
+ export type { ToastItem, ToasterProps, ToasterPosition, UndoableToastOptions } from './components/Toast/index.js';
186
186
  export { TopNav } from './components/TopNav/index.js';
187
187
  export type { TopNavItemProps, TopNavRootProps } from './components/TopNav/index.js';
188
188
  export { Sidebar } from './components/Sidebar/index.js';
@@ -191,6 +191,7 @@ export { AppShell } from './components/AppShell/index.js';
191
191
  export { PageHeader } from './components/PageHeader/index.js';
192
192
  export type { PageHeaderProps } from './components/PageHeader/index.js';
193
193
  export { Breadcrumb } from './components/Breadcrumb/index.js';
194
+ export type { BreadcrumbTrailItem, BreadcrumbTrailProps } from './components/Breadcrumb/index.js';
194
195
  export { Pagination } from './components/Pagination/index.js';
195
196
  export type { PaginationProps } from './components/Pagination/index.js';
196
197
  export { CommandPalette } from './components/CommandPalette/index.js';
@@ -201,8 +202,8 @@ export type { DataTableProps } from './components/DataTable/index.js';
201
202
  export { EmptyState } from './components/EmptyState/index.js';
202
203
  export { ErrorState } from './components/ErrorState/index.js';
203
204
  export type { ErrorStateProps } from './components/ErrorState/index.js';
204
- export { FileUpload } from './components/FileUpload/index.js';
205
- export type { FileUploadRootProps } from './components/FileUpload/index.js';
205
+ export { FileUpload, fileAccepted } from './components/FileUpload/index.js';
206
+ export type { FileUploadRootProps, FileRejection } from './components/FileUpload/index.js';
206
207
  export { DatePicker } from './components/DatePicker/index.js';
207
208
  export type { DatePickerProps } from './components/DatePicker/index.js';
208
209
  export { DateRangePicker } from './components/DateRangePicker/index.js';
package/dist/index.js CHANGED
@@ -115,7 +115,7 @@ import { Table } from "./components/Table/index.js";
115
115
  import { DataTable } from "./components/DataTable/index.js";
116
116
  import { EmptyState } from "./components/EmptyState/index.js";
117
117
  import { ErrorState } from "./components/ErrorState/index.js";
118
- import { FileUpload } from "./components/FileUpload/index.js";
118
+ import { FileUpload, fileAccepted } from "./components/FileUpload/index.js";
119
119
  import { DatePicker } from "./components/DatePicker/index.js";
120
120
  import { DateRangePicker } from "./components/DateRangePicker/index.js";
121
121
  import { ThemeProvider, ThemeScript, useTheme } from "./components/ThemeProvider/index.js";
@@ -271,6 +271,7 @@ export {
271
271
  containerVariants,
272
272
  defaultMessageStrings,
273
273
  defaultMessages,
274
+ fileAccepted,
274
275
  headingVariants,
275
276
  iconButtonVariants,
276
277
  inputVariants,