@nexub/react 5.0.1 → 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.
- package/accessibility.json +1 -1
- package/dist/components/Autocomplete/Autocomplete.d.ts +8 -1
- package/dist/components/Autocomplete/Autocomplete.js +10 -3
- package/dist/components/Breadcrumb/Breadcrumb.d.ts +14 -0
- package/dist/components/Breadcrumb/Breadcrumb.js +30 -2
- package/dist/components/Breadcrumb/index.d.ts +1 -0
- package/dist/components/CommandPalette/CommandPalette.d.ts +5 -0
- package/dist/components/CommandPalette/CommandPalette.js +15 -2
- package/dist/components/Drawer/Drawer.js +7 -2
- package/dist/components/FileUpload/FileUpload.d.ts +14 -0
- package/dist/components/FileUpload/FileUpload.js +64 -37
- package/dist/components/FileUpload/index.d.ts +2 -2
- package/dist/components/FileUpload/index.js +3 -2
- package/dist/components/Toast/Toaster.d.ts +4 -1
- package/dist/components/Toast/Toaster.js +29 -12
- package/dist/components/Toast/index.d.ts +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/styles.css +1 -1
- package/package.json +6 -6
package/accessibility.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "5.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 [
|
|
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
|
-
|
|
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
|
|
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
|
};
|
|
@@ -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) =>
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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__ */
|
|
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:
|
|
83
|
-
/* @__PURE__ */
|
|
84
|
-
|
|
85
|
-
/* @__PURE__ */
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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,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
|
-
|
|
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
|
|
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 =
|
|
51
|
-
if (i < visibleToasts2) stack = offset +
|
|
52
|
-
offset +=
|
|
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" ? "
|
|
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
|
|
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
|
-
|
|
125
|
-
"
|
|
126
|
-
"
|
|
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
|
|
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
|
-
|
|
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,
|