@nexub/react 5.0.1 → 5.2.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,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
  };
@@ -0,0 +1,35 @@
1
+ import * as React from 'react';
2
+ export interface MultiSelectOption {
3
+ value: string;
4
+ label: string;
5
+ }
6
+ export interface MultiSelectProps {
7
+ options: MultiSelectOption[];
8
+ value?: string[];
9
+ defaultValue?: string[];
10
+ onValueChange?: (value: string[]) => void;
11
+ placeholder?: string;
12
+ emptyText?: string;
13
+ loading?: boolean;
14
+ /** Controlled search text. */
15
+ inputValue?: string;
16
+ /** Called as the user types; options are then shown as given (no built-in filtering). */
17
+ onInputChange?: (text: string) => void;
18
+ /** Adds a "Create “text”" option when the text matches no option exactly. */
19
+ onCreate?: (text: string) => void;
20
+ /** Chips shown before the rest collapse into "+N more". Default 3. */
21
+ maxVisibleChips?: number;
22
+ disabled?: boolean;
23
+ className?: string;
24
+ id?: string;
25
+ 'aria-label'?: string;
26
+ 'aria-labelledby'?: string;
27
+ }
28
+ /**
29
+ * Pick several options: chips in the field, a filterable list below.
30
+ * Backspace in an empty field removes the last chip.
31
+ */
32
+ export declare function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder, emptyText, loading, inputValue, onInputChange, onCreate, maxVisibleChips, disabled, className, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, }: MultiSelectProps): React.JSX.Element;
33
+ export declare namespace MultiSelect {
34
+ var displayName: string;
35
+ }
@@ -0,0 +1,142 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Check, Plus } from "@nexub/icons";
5
+ import { cn } from "../../utils/cn.js";
6
+ import { useControllableState } from "../../hooks/useControllableState.js";
7
+ import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
8
+ import { Chip } from "../Chip/Chip.js";
9
+ import { Combobox } from "../Combobox/Combobox.js";
10
+ import { ComboboxLoading, ComboboxPopover } from "../Combobox/ComboboxPopover.js";
11
+ function MultiSelect({
12
+ options,
13
+ value: valueProp,
14
+ defaultValue,
15
+ onValueChange,
16
+ placeholder,
17
+ emptyText,
18
+ loading = false,
19
+ inputValue,
20
+ onInputChange,
21
+ onCreate,
22
+ maxVisibleChips = 3,
23
+ disabled,
24
+ className,
25
+ id,
26
+ "aria-label": ariaLabel,
27
+ "aria-labelledby": ariaLabelledBy
28
+ }) {
29
+ const messages = useNexubMessages();
30
+ const [value = [], setValue] = useControllableState({
31
+ value: valueProp,
32
+ defaultValue: defaultValue ?? [],
33
+ onChange: onValueChange
34
+ });
35
+ const [innerSearch, setInnerSearch] = React.useState("");
36
+ const [open, setOpen] = React.useState(false);
37
+ const search = inputValue ?? innerSearch;
38
+ const setSearch = (text2) => {
39
+ setInnerSearch(text2);
40
+ onInputChange?.(text2);
41
+ };
42
+ const [labels, setLabels] = React.useState(() => /* @__PURE__ */ new Map());
43
+ const missing = options.some((o) => labels.get(o.value) !== o.label);
44
+ if (missing) setLabels(new Map([...labels, ...options.map((o) => [o.value, o.label])]));
45
+ const labelOf = (v) => labels.get(v) ?? options.find((o) => o.value === v)?.label ?? v;
46
+ const toggle = (v) => setValue(value.includes(v) ? value.filter((x) => x !== v) : [...value, v]);
47
+ const remove = (v) => setValue(value.filter((x) => x !== v));
48
+ const text = search.trim();
49
+ const canCreate = !!onCreate && !loading && text !== "" && !options.some((o) => o.label.toLowerCase() === text.toLowerCase());
50
+ const hiddenCount = Math.max(0, value.length - maxVisibleChips);
51
+ const chips = /* @__PURE__ */ jsxs(Fragment, { children: [
52
+ value.slice(0, maxVisibleChips).map((v) => /* @__PURE__ */ jsx(
53
+ Chip,
54
+ {
55
+ size: "sm",
56
+ onRemove: disabled ? void 0 : () => remove(v),
57
+ removeLabel: messages.removeOption(labelOf(v)),
58
+ children: labelOf(v)
59
+ },
60
+ v
61
+ )),
62
+ hiddenCount > 0 && /* @__PURE__ */ jsxs(Chip, { size: "sm", children: [
63
+ "+",
64
+ messages.moreCount(hiddenCount)
65
+ ] })
66
+ ] });
67
+ return /* @__PURE__ */ jsx(
68
+ Combobox.Root,
69
+ {
70
+ label: ariaLabel,
71
+ className: cn("overflow-visible rounded-none bg-transparent", className),
72
+ shouldFilter: !onInputChange,
73
+ children: /* @__PURE__ */ jsx(
74
+ ComboboxPopover,
75
+ {
76
+ open: open && !disabled,
77
+ onOpenChange: setOpen,
78
+ before: chips,
79
+ disabled,
80
+ input: {
81
+ id,
82
+ "aria-label": ariaLabel,
83
+ "aria-labelledby": ariaLabelledBy,
84
+ placeholder: value.length ? void 0 : placeholder ?? messages.searchPlaceholder,
85
+ value: search,
86
+ onValueChange: (t) => {
87
+ setSearch(t);
88
+ setOpen(true);
89
+ },
90
+ onKeyDown: (e) => {
91
+ if (e.key === "Backspace" && search === "" && value.length) remove(value[value.length - 1]);
92
+ }
93
+ },
94
+ children: /* @__PURE__ */ jsx(Combobox.List, { "aria-multiselectable": "true", children: loading ? /* @__PURE__ */ jsx(ComboboxLoading, {}) : /* @__PURE__ */ jsxs(Fragment, { children: [
95
+ !canCreate && /* @__PURE__ */ jsx(Combobox.Empty, { children: emptyText ?? messages.noResults }),
96
+ options.map((opt) => {
97
+ const checked = value.includes(opt.value);
98
+ return /* @__PURE__ */ jsxs(
99
+ Combobox.Item,
100
+ {
101
+ value: opt.label,
102
+ keywords: [opt.value],
103
+ "aria-checked": checked,
104
+ onSelect: () => {
105
+ toggle(opt.value);
106
+ if (search) setSearch("");
107
+ },
108
+ className: "gap-2",
109
+ children: [
110
+ /* @__PURE__ */ jsx(Check, { size: "sm", "aria-hidden": "true", className: cn(!checked && "invisible") }),
111
+ opt.label
112
+ ]
113
+ },
114
+ opt.value
115
+ );
116
+ }),
117
+ canCreate && /* @__PURE__ */ jsxs(
118
+ Combobox.Item,
119
+ {
120
+ forceMount: true,
121
+ value: "\0",
122
+ onSelect: () => {
123
+ onCreate(text);
124
+ setSearch("");
125
+ },
126
+ className: "gap-2",
127
+ children: [
128
+ /* @__PURE__ */ jsx(Plus, { size: "sm", "aria-hidden": "true" }),
129
+ messages.createOption(text)
130
+ ]
131
+ }
132
+ )
133
+ ] }) })
134
+ }
135
+ )
136
+ }
137
+ );
138
+ }
139
+ MultiSelect.displayName = "MultiSelect";
140
+ export {
141
+ MultiSelect
142
+ };
@@ -0,0 +1,2 @@
1
+ export { MultiSelect } from './MultiSelect.js';
2
+ export type { MultiSelectProps, MultiSelectOption } from './MultiSelect.js';
@@ -0,0 +1,4 @@
1
+ import { MultiSelect } from "./MultiSelect.js";
2
+ export {
3
+ MultiSelect
4
+ };
@@ -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
@@ -160,6 +160,8 @@ export { Select } from './components/Select/index.js';
160
160
  export { Combobox } from './components/Combobox/index.js';
161
161
  export { Autocomplete } from './components/Autocomplete/index.js';
162
162
  export type { AutocompleteOption, AutocompleteProps } from './components/Autocomplete/index.js';
163
+ export { MultiSelect } from './components/MultiSelect/index.js';
164
+ export type { MultiSelectOption, MultiSelectProps } from './components/MultiSelect/index.js';
163
165
  export { Switch } from './components/Switch/index.js';
164
166
  export type { SwitchProps } from './components/Switch/index.js';
165
167
  export { Checkbox } from './components/Checkbox/index.js';
@@ -182,7 +184,7 @@ export type { ToolbarRootProps } from './components/Toolbar/index.js';
182
184
  export { Toast } from './components/Toast/index.js';
183
185
  export { Toaster } from './components/Toast/index.js';
184
186
  export { toast, useToastStore } from './components/Toast/index.js';
185
- export type { ToastItem, ToasterProps, UndoableToastOptions } from './components/Toast/index.js';
187
+ export type { ToastItem, ToasterProps, ToasterPosition, UndoableToastOptions } from './components/Toast/index.js';
186
188
  export { TopNav } from './components/TopNav/index.js';
187
189
  export type { TopNavItemProps, TopNavRootProps } from './components/TopNav/index.js';
188
190
  export { Sidebar } from './components/Sidebar/index.js';
@@ -191,6 +193,7 @@ export { AppShell } from './components/AppShell/index.js';
191
193
  export { PageHeader } from './components/PageHeader/index.js';
192
194
  export type { PageHeaderProps } from './components/PageHeader/index.js';
193
195
  export { Breadcrumb } from './components/Breadcrumb/index.js';
196
+ export type { BreadcrumbTrailItem, BreadcrumbTrailProps } from './components/Breadcrumb/index.js';
194
197
  export { Pagination } from './components/Pagination/index.js';
195
198
  export type { PaginationProps } from './components/Pagination/index.js';
196
199
  export { CommandPalette } from './components/CommandPalette/index.js';
@@ -201,8 +204,8 @@ export type { DataTableProps } from './components/DataTable/index.js';
201
204
  export { EmptyState } from './components/EmptyState/index.js';
202
205
  export { ErrorState } from './components/ErrorState/index.js';
203
206
  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';
207
+ export { FileUpload, fileAccepted } from './components/FileUpload/index.js';
208
+ export type { FileUploadRootProps, FileRejection } from './components/FileUpload/index.js';
206
209
  export { DatePicker } from './components/DatePicker/index.js';
207
210
  export type { DatePickerProps } from './components/DatePicker/index.js';
208
211
  export { DateRangePicker } from './components/DateRangePicker/index.js';
package/dist/index.js CHANGED
@@ -89,6 +89,7 @@ import { Drawer, Sheet } from "./components/Drawer/index.js";
89
89
  import { Select } from "./components/Select/index.js";
90
90
  import { Combobox } from "./components/Combobox/index.js";
91
91
  import { Autocomplete } from "./components/Autocomplete/index.js";
92
+ import { MultiSelect } from "./components/MultiSelect/index.js";
92
93
  import { Switch } from "./components/Switch/index.js";
93
94
  import { Checkbox } from "./components/Checkbox/index.js";
94
95
  import { RadioGroup } from "./components/RadioGroup/index.js";
@@ -115,7 +116,7 @@ import { Table } from "./components/Table/index.js";
115
116
  import { DataTable } from "./components/DataTable/index.js";
116
117
  import { EmptyState } from "./components/EmptyState/index.js";
117
118
  import { ErrorState } from "./components/ErrorState/index.js";
118
- import { FileUpload } from "./components/FileUpload/index.js";
119
+ import { FileUpload, fileAccepted } from "./components/FileUpload/index.js";
119
120
  import { DatePicker } from "./components/DatePicker/index.js";
120
121
  import { DateRangePicker } from "./components/DateRangePicker/index.js";
121
122
  import { ThemeProvider, ThemeScript, useTheme } from "./components/ThemeProvider/index.js";
@@ -201,6 +202,7 @@ export {
201
202
  MessageActions,
202
203
  MessageBubble,
203
204
  MoneyInput,
205
+ MultiSelect,
204
206
  NavigationMenu,
205
207
  NavigationRail,
206
208
  NexubProvider,
@@ -271,6 +273,7 @@ export {
271
273
  containerVariants,
272
274
  defaultMessageStrings,
273
275
  defaultMessages,
276
+ fileAccepted,
274
277
  headingVariants,
275
278
  iconButtonVariants,
276
279
  inputVariants,