@nexub/react 5.4.0 → 5.6.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 (31) hide show
  1. package/accessibility.json +34 -1
  2. package/dist/components/AlertDialog/AlertDialog.d.ts +17 -0
  3. package/dist/components/AlertDialog/AlertDialog.js +40 -5
  4. package/dist/components/AlertDialog/index.d.ts +1 -0
  5. package/dist/components/DateRangePicker/DateRangePicker.d.ts +17 -0
  6. package/dist/components/DateRangePicker/DateRangePicker.js +123 -71
  7. package/dist/components/DateRangePicker/index.d.ts +2 -1
  8. package/dist/components/DateRangePicker/index.js +3 -1
  9. package/dist/components/EmptyState/EmptyStatePreset.d.ts +14 -0
  10. package/dist/components/EmptyState/EmptyStatePreset.js +23 -0
  11. package/dist/components/EmptyState/index.d.ts +2 -0
  12. package/dist/components/EmptyState/index.js +3 -1
  13. package/dist/components/FileUpload/FileUpload.d.ts +10 -3
  14. package/dist/components/FileUpload/FileUpload.js +13 -4
  15. package/dist/components/FileUpload/index.d.ts +1 -1
  16. package/dist/components/FilterBuilder/FilterBuilder.d.ts +50 -0
  17. package/dist/components/FilterBuilder/FilterBuilder.js +164 -0
  18. package/dist/components/FilterBuilder/index.d.ts +2 -0
  19. package/dist/components/FilterBuilder/index.js +8 -0
  20. package/dist/components/OTPInput/OTPInput.d.ts +28 -0
  21. package/dist/components/OTPInput/OTPInput.js +107 -0
  22. package/dist/components/OTPInput/index.d.ts +2 -0
  23. package/dist/components/OTPInput/index.js +4 -0
  24. package/dist/components/SavedViews/SavedViews.d.ts +26 -0
  25. package/dist/components/SavedViews/SavedViews.js +65 -0
  26. package/dist/components/SavedViews/index.d.ts +2 -0
  27. package/dist/components/SavedViews/index.js +4 -0
  28. package/dist/index.d.ts +12 -3
  29. package/dist/index.js +14 -1
  30. package/dist/styles.css +1 -1
  31. package/package.json +6 -6
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.4.0",
2
+ "version": "5.6.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.",
@@ -399,6 +399,17 @@
399
399
  "keyboard": [],
400
400
  "screenReader": []
401
401
  },
402
+ {
403
+ "slug": "filter-builder",
404
+ "name": "Filter Builder",
405
+ "package": "@nexub/react",
406
+ "automated": "pass",
407
+ "exceptions": [],
408
+ "keyboard": [],
409
+ "screenReader": [
410
+ "Field, condition and value controls are labelled; remove buttons say what they remove."
411
+ ]
412
+ },
402
413
  {
403
414
  "slug": "filter-chip",
404
415
  "name": "Filter Chip",
@@ -740,6 +751,19 @@
740
751
  "Under reduced motion digits change instantly."
741
752
  ]
742
753
  },
754
+ {
755
+ "slug": "otp-input",
756
+ "name": "OTP Input",
757
+ "package": "@nexub/react",
758
+ "automated": "pass",
759
+ "exceptions": [],
760
+ "keyboard": [
761
+ "Type: Fills the box and moves to the next.",
762
+ "Backspace: Clears the box, or the previous one when empty.",
763
+ "ArrowLeft / ArrowRight: Moves between boxes."
764
+ ],
765
+ "screenReader": []
766
+ },
743
767
  {
744
768
  "slug": "pagination",
745
769
  "name": "Pagination",
@@ -839,6 +863,15 @@
839
863
  "keyboard": [],
840
864
  "screenReader": []
841
865
  },
866
+ {
867
+ "slug": "saved-views",
868
+ "name": "Saved Views",
869
+ "package": "@nexub/react",
870
+ "automated": "pass",
871
+ "exceptions": [],
872
+ "keyboard": [],
873
+ "screenReader": []
874
+ },
842
875
  {
843
876
  "slug": "scroll-area",
844
877
  "name": "Scroll Area",
@@ -1,5 +1,20 @@
1
1
  import * as React from 'react';
2
2
  import * as RadixAlertDialog from '@radix-ui/react-alert-dialog';
3
+ export interface AlertDialogConfirmProps {
4
+ /** What the user must type, e.g. the resource name. Matching ignores outer spaces. */
5
+ text: string;
6
+ /** The label; defaults to "Type ‹text› to confirm". */
7
+ label?: React.ReactNode;
8
+ className?: string;
9
+ }
10
+ /**
11
+ * For critical, irreversible actions: the Action button stays disabled until
12
+ * `text` is typed. Place it inside AlertDialog.Content.
13
+ */
14
+ declare function Confirm({ text, label, className }: AlertDialogConfirmProps): React.JSX.Element;
15
+ declare namespace Confirm {
16
+ var displayName: string;
17
+ }
3
18
  export declare const AlertDialog: {
4
19
  Root: React.FC<RadixAlertDialog.AlertDialogProps>;
5
20
  Trigger: React.ForwardRefExoticComponent<RadixAlertDialog.AlertDialogTriggerProps & React.RefAttributes<HTMLButtonElement>>;
@@ -12,4 +27,6 @@ export declare const AlertDialog: {
12
27
  Footer: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
13
28
  Action: React.ForwardRefExoticComponent<RadixAlertDialog.AlertDialogActionProps & React.RefAttributes<HTMLButtonElement>>;
14
29
  Cancel: React.ForwardRefExoticComponent<RadixAlertDialog.AlertDialogCancelProps & React.RefAttributes<HTMLButtonElement>>;
30
+ Confirm: typeof Confirm;
15
31
  };
32
+ export {};
@@ -1,10 +1,13 @@
1
1
  "use client";
2
- import { jsx } from "react/jsx-runtime";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import * as RadixAlertDialog from "@radix-ui/react-alert-dialog";
5
5
  import { cn } from "../../utils/cn.js";
6
6
  import { overlaySurface, scrim } from "../../utils/surface.js";
7
7
  import { buttonVariants } from "../Button/Button.js";
8
+ import { Input } from "../Input/Input.js";
9
+ import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
10
+ const ConfirmContext = React.createContext(null);
8
11
  const Root = RadixAlertDialog.Root;
9
12
  const Trigger = RadixAlertDialog.Trigger;
10
13
  const Portal = RadixAlertDialog.Portal;
@@ -27,7 +30,10 @@ const Overlay = React.forwardRef(
27
30
  Overlay.displayName = "AlertDialog.Overlay";
28
31
  const Content = React.forwardRef(
29
32
  function AlertDialogContent({ className, children, ...props }, ref) {
30
- return /* @__PURE__ */ jsx(
33
+ const [required, setRequired] = React.useState(null);
34
+ const [typed, setTyped] = React.useState("");
35
+ const confirm = React.useMemo(() => ({ required, setRequired, typed, setTyped }), [required, typed]);
36
+ return /* @__PURE__ */ jsx(ConfirmContext.Provider, { value: confirm, children: /* @__PURE__ */ jsx(
31
37
  RadixAlertDialog.Content,
32
38
  {
33
39
  ref,
@@ -41,7 +47,7 @@ const Content = React.forwardRef(
41
47
  ...props,
42
48
  children
43
49
  }
44
- );
50
+ ) });
45
51
  }
46
52
  );
47
53
  Content.displayName = "AlertDialog.Content";
@@ -82,11 +88,14 @@ const Footer = React.forwardRef(
82
88
  );
83
89
  Footer.displayName = "AlertDialog.Footer";
84
90
  const Action = React.forwardRef(
85
- function AlertDialogAction({ className, ...props }, ref) {
91
+ function AlertDialogAction({ className, disabled, ...props }, ref) {
92
+ const confirm = React.useContext(ConfirmContext);
93
+ const blocked = !!confirm?.required && confirm.typed.trim() !== confirm.required;
86
94
  return /* @__PURE__ */ jsx(
87
95
  RadixAlertDialog.Action,
88
96
  {
89
97
  ref,
98
+ disabled: disabled || blocked,
90
99
  className: cn(
91
100
  // The destructive confirm: same as <Button variant="destructive">.
92
101
  buttonVariants({ variant: "destructive" }),
@@ -114,6 +123,31 @@ const Cancel = React.forwardRef(
114
123
  }
115
124
  );
116
125
  Cancel.displayName = "AlertDialog.Cancel";
126
+ function Confirm({ text, label, className }) {
127
+ const confirm = React.useContext(ConfirmContext);
128
+ const t = useNexubMessages();
129
+ const id = React.useId();
130
+ const setRequired = confirm?.setRequired;
131
+ React.useEffect(() => {
132
+ setRequired?.(text);
133
+ return () => setRequired?.(null);
134
+ }, [text, setRequired]);
135
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-2", className), children: [
136
+ /* @__PURE__ */ jsx("label", { htmlFor: id, className: "text-body-sm text-[var(--color-on-surface)]", children: label ?? t.typeToConfirm(text) }),
137
+ /* @__PURE__ */ jsx(
138
+ Input,
139
+ {
140
+ id,
141
+ value: confirm?.typed ?? "",
142
+ onChange: (e) => confirm?.setTyped(e.target.value),
143
+ autoComplete: "off",
144
+ autoCapitalize: "off",
145
+ spellCheck: false
146
+ }
147
+ )
148
+ ] });
149
+ }
150
+ Confirm.displayName = "AlertDialog.Confirm";
117
151
  const AlertDialog = {
118
152
  Root,
119
153
  Trigger,
@@ -125,7 +159,8 @@ const AlertDialog = {
125
159
  Header,
126
160
  Footer,
127
161
  Action,
128
- Cancel
162
+ Cancel,
163
+ Confirm
129
164
  };
130
165
  export {
131
166
  AlertDialog
@@ -1 +1,2 @@
1
1
  export { AlertDialog } from './AlertDialog.js';
2
+ export type { AlertDialogConfirmProps } from './AlertDialog.js';
@@ -2,6 +2,13 @@ import * as React from 'react';
2
2
  import { type DateRange } from 'react-day-picker';
3
3
  import type { Locale } from 'date-fns';
4
4
  export type { DateRange };
5
+ export interface DateRangePreset {
6
+ label: string;
7
+ /** `today` is the current date in the picker's time zone (midnight, local Date). */
8
+ range: (today: Date) => DateRange;
9
+ }
10
+ /** Today's calendar date in `timeZone`, as a local midnight Date. */
11
+ export declare function todayIn(timeZone?: string, now?: Date): Date;
5
12
  export interface DateRangePickerProps {
6
13
  value?: DateRange;
7
14
  onChange?: (range: DateRange | undefined) => void;
@@ -16,5 +23,15 @@ export interface DateRangePickerProps {
16
23
  'aria-label'?: string;
17
24
  /** date-fns Locale for the calendar and trigger text; wins over NexubProvider's `dateLocale`. */
18
25
  locale?: Locale;
26
+ /**
27
+ * Quick ranges beside the calendar. `true` shows Today, Last 7 days,
28
+ * Last 30 days, Month to date and Last month; or pass your own.
29
+ */
30
+ presets?: boolean | DateRangePreset[];
31
+ /**
32
+ * IANA time zone the dates mean, e.g. 'Europe/London'. Shown under the
33
+ * calendar, and presets count "today" there.
34
+ */
35
+ timeZone?: string;
19
36
  }
20
37
  export declare const DateRangePicker: React.ForwardRefExoticComponent<DateRangePickerProps & React.RefAttributes<HTMLButtonElement>>;
@@ -5,10 +5,22 @@ import * as RadixPopover from "@radix-ui/react-popover";
5
5
  import { DayPicker } from "react-day-picker";
6
6
  import { CalendarDays } from "@nexub/icons";
7
7
  import { cn } from "../../utils/cn.js";
8
- import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
8
+ import { useLocale, useNexubMessages } from "../NexubProvider/NexubProvider.js";
9
9
  import { useCalendarLocale } from "../DatePicker/DatePicker.js";
10
10
  import { field, fieldFocusVisible } from "../../utils/control.js";
11
11
  import { floatingSurface } from "../../utils/surface.js";
12
+ function todayIn(timeZone, now = /* @__PURE__ */ new Date()) {
13
+ if (!timeZone) return new Date(now.getFullYear(), now.getMonth(), now.getDate());
14
+ const parts = new Intl.DateTimeFormat("en-US", {
15
+ timeZone,
16
+ year: "numeric",
17
+ month: "numeric",
18
+ day: "numeric"
19
+ }).formatToParts(now);
20
+ const get = (t) => Number(parts.find((p) => p.type === t)?.value);
21
+ return new Date(get("year"), get("month") - 1, get("day"));
22
+ }
23
+ const addDays = (d, n) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
12
24
  function formatRange(range, placeholder, formatDate) {
13
25
  if (!range) return placeholder;
14
26
  if (range.from && range.to) {
@@ -51,79 +63,119 @@ const calendarClassNames = {
51
63
  disabled: "",
52
64
  hidden: "invisible"
53
65
  };
54
- const DateRangePicker = React.forwardRef(function DateRangePicker2({
55
- value,
56
- onChange,
57
- placeholder: placeholderProp,
58
- disabled = false,
59
- minDate,
60
- maxDate,
61
- className,
62
- id,
63
- "aria-label": ariaLabel,
64
- locale: localeProp
65
- }, ref) {
66
- const [open, setOpen] = React.useState(false);
67
- const messages = useNexubMessages();
68
- const calendar = useCalendarLocale(localeProp);
69
- const placeholder = placeholderProp ?? messages.pickDateRange;
70
- const display = formatRange(value, placeholder, calendar.format);
71
- const label = ariaLabel ? `${ariaLabel}: ${display}` : display;
72
- return /* @__PURE__ */ jsxs(RadixPopover.Root, { open, onOpenChange: setOpen, children: [
73
- /* @__PURE__ */ jsx(RadixPopover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
74
- "button",
66
+ const DateRangePicker = React.forwardRef(
67
+ function DateRangePicker2({
68
+ value,
69
+ onChange,
70
+ placeholder: placeholderProp,
71
+ disabled = false,
72
+ minDate,
73
+ maxDate,
74
+ className,
75
+ id,
76
+ "aria-label": ariaLabel,
77
+ locale: localeProp,
78
+ presets: presetsProp,
79
+ timeZone
80
+ }, ref) {
81
+ const [open, setOpen] = React.useState(false);
82
+ const messages = useNexubMessages();
83
+ const { locale } = useLocale();
84
+ const presets = presetsProp === true ? [
85
+ { label: messages.today, range: (t) => ({ from: t, to: t }) },
86
+ { label: messages.last7Days, range: (t) => ({ from: addDays(t, -6), to: t }) },
87
+ { label: messages.last30Days, range: (t) => ({ from: addDays(t, -29), to: t }) },
75
88
  {
76
- ref,
77
- id,
78
- disabled,
79
- "aria-label": label,
80
- className: cn(
81
- "inline-flex h-control-md w-full items-center justify-start gap-2 px-3.5 text-body-md",
82
- field,
83
- fieldFocusVisible,
84
- // Open reads like focus: the 2px primary border (as Select).
85
- "data-[state=open]:border-[var(--color-primary)] data-[state=open]:shadow-[inset_0_0_0_1px_var(--color-primary)]",
86
- "disabled:cursor-not-allowed disabled:border-[var(--color-outline-variant)] disabled:bg-[var(--color-surface-container-low)] disabled:text-[var(--color-text-disabled)]",
87
- !value && "text-[var(--color-on-surface-muted)]",
88
- className
89
- ),
90
- children: [
91
- /* @__PURE__ */ jsx(CalendarDays, { size: "sm", "aria-hidden": "true" }),
92
- display
93
- ]
94
- }
95
- ) }),
96
- /* @__PURE__ */ jsx(RadixPopover.Portal, { children: /* @__PURE__ */ jsx(
97
- RadixPopover.Content,
89
+ label: messages.monthToDate,
90
+ range: (t) => ({ from: new Date(t.getFullYear(), t.getMonth(), 1), to: t })
91
+ },
98
92
  {
99
- align: "start",
100
- className: cn(
101
- "z-[var(--z-popover)] outline-none",
102
- floatingSurface,
103
- "nx-motion nx-pop origin-[var(--radix-popover-content-transform-origin)]"
104
- ),
105
- children: /* @__PURE__ */ jsx(
106
- DayPicker,
107
- {
108
- mode: "range",
109
- selected: value,
110
- defaultMonth: value?.from,
111
- onSelect: onChange,
112
- numberOfMonths: 2,
113
- startMonth: minDate,
114
- endMonth: maxDate,
115
- autoFocus: true,
116
- showOutsideDays: false,
117
- className: "relative p-3",
118
- classNames: calendarClassNames,
119
- ...calendar.dayPickerProps
120
- }
121
- )
93
+ label: messages.lastMonth,
94
+ range: (t) => ({
95
+ from: new Date(t.getFullYear(), t.getMonth() - 1, 1),
96
+ to: new Date(t.getFullYear(), t.getMonth(), 0)
97
+ })
122
98
  }
123
- ) })
124
- ] });
125
- });
99
+ ] : presetsProp || [];
100
+ const zoneName = timeZone ? new Intl.DateTimeFormat(locale, { timeZone, timeZoneName: "shortOffset" }).formatToParts(/* @__PURE__ */ new Date()).find((p) => p.type === "timeZoneName")?.value ?? timeZone : void 0;
101
+ const calendar = useCalendarLocale(localeProp);
102
+ const placeholder = placeholderProp ?? messages.pickDateRange;
103
+ const display = formatRange(value, placeholder, calendar.format);
104
+ const label = ariaLabel ? `${ariaLabel}: ${display}` : display;
105
+ return /* @__PURE__ */ jsxs(RadixPopover.Root, { open, onOpenChange: setOpen, children: [
106
+ /* @__PURE__ */ jsx(RadixPopover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
107
+ "button",
108
+ {
109
+ ref,
110
+ id,
111
+ disabled,
112
+ "aria-label": label,
113
+ className: cn(
114
+ "inline-flex h-control-md w-full items-center justify-start gap-2 px-3.5 text-body-md",
115
+ field,
116
+ fieldFocusVisible,
117
+ // Open reads like focus: the 2px primary border (as Select).
118
+ "data-[state=open]:border-[var(--color-primary)] data-[state=open]:shadow-[inset_0_0_0_1px_var(--color-primary)]",
119
+ "disabled:cursor-not-allowed disabled:border-[var(--color-outline-variant)] disabled:bg-[var(--color-surface-container-low)] disabled:text-[var(--color-text-disabled)]",
120
+ !value && "text-[var(--color-on-surface-muted)]",
121
+ className
122
+ ),
123
+ children: [
124
+ /* @__PURE__ */ jsx(CalendarDays, { size: "sm", "aria-hidden": "true" }),
125
+ display
126
+ ]
127
+ }
128
+ ) }),
129
+ /* @__PURE__ */ jsx(RadixPopover.Portal, { children: /* @__PURE__ */ jsx(
130
+ RadixPopover.Content,
131
+ {
132
+ align: "start",
133
+ className: cn(
134
+ "z-[var(--z-popover)] outline-none",
135
+ floatingSurface,
136
+ "nx-motion nx-pop origin-[var(--radix-popover-content-transform-origin)]"
137
+ ),
138
+ children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col sm:flex-row", children: [
139
+ presets.length > 0 && /* @__PURE__ */ jsx("ul", { className: "flex flex-wrap gap-1 border-b border-[var(--color-outline-variant)] p-2 sm:flex-col sm:flex-nowrap sm:border-b-0 sm:border-e", children: presets.map((p) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
140
+ "button",
141
+ {
142
+ type: "button",
143
+ className: "nx-state-layer w-full rounded-shape-control px-3 py-1.5 text-start text-body-sm text-[var(--color-on-surface)]",
144
+ onClick: () => {
145
+ onChange?.(p.range(todayIn(timeZone)));
146
+ setOpen(false);
147
+ },
148
+ children: p.label
149
+ }
150
+ ) }, p.label)) }),
151
+ /* @__PURE__ */ jsxs("div", { children: [
152
+ /* @__PURE__ */ jsx(
153
+ DayPicker,
154
+ {
155
+ mode: "range",
156
+ selected: value,
157
+ defaultMonth: value?.from,
158
+ onSelect: onChange,
159
+ numberOfMonths: 2,
160
+ startMonth: minDate,
161
+ endMonth: maxDate,
162
+ autoFocus: true,
163
+ showOutsideDays: false,
164
+ className: "relative p-3",
165
+ classNames: calendarClassNames,
166
+ ...calendar.dayPickerProps
167
+ }
168
+ ),
169
+ zoneName && /* @__PURE__ */ jsx("p", { className: "px-4 pb-3 text-label-sm text-[var(--color-on-surface-muted)]", children: messages.timeZoneLabel(`${zoneName} (${timeZone})`) })
170
+ ] })
171
+ ] })
172
+ }
173
+ ) })
174
+ ] });
175
+ }
176
+ );
126
177
  DateRangePicker.displayName = "DateRangePicker";
127
178
  export {
128
- DateRangePicker
179
+ DateRangePicker,
180
+ todayIn
129
181
  };
@@ -1,2 +1,3 @@
1
1
  export { DateRangePicker } from './DateRangePicker.js';
2
- export type { DateRangePickerProps, DateRange } from './DateRangePicker.js';
2
+ export { todayIn } from './DateRangePicker.js';
3
+ export type { DateRangePickerProps, DateRange, DateRangePreset } from './DateRangePicker.js';
@@ -1,4 +1,6 @@
1
1
  import { DateRangePicker } from "./DateRangePicker.js";
2
+ import { todayIn } from "./DateRangePicker.js";
2
3
  export {
3
- DateRangePicker
4
+ DateRangePicker,
5
+ todayIn
4
6
  };
@@ -0,0 +1,14 @@
1
+ import * as React from 'react';
2
+ import type { NexubMessages } from '@nexub/core';
3
+ export type EmptyStateKind = keyof NexubMessages['emptyStates'];
4
+ export interface EmptyStatePresetProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
5
+ /** noData, noResults, noAccess (403), serverError (500) or offline. */
6
+ kind: EmptyStateKind;
7
+ /** Overrides the preset's title; the description too. Both default to localised text. */
8
+ title?: React.ReactNode;
9
+ description?: React.ReactNode;
10
+ /** Buttons under the text, e.g. Retry or Clear filters. */
11
+ actions?: React.ReactNode;
12
+ }
13
+ /** A ready-made empty state: icon, title and description for common cases. */
14
+ export declare const EmptyStatePreset: React.ForwardRefExoticComponent<EmptyStatePresetProps & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,23 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Inbox, Lock, Search, ServerCrash, WifiOff } from "@nexub/icons";
5
+ import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
6
+ import { EmptyState } from "./EmptyState.js";
7
+ const presetIcon = { noData: Inbox, noResults: Search, noAccess: Lock, serverError: ServerCrash, offline: WifiOff };
8
+ const EmptyStatePreset = React.forwardRef(
9
+ ({ kind, title, description, actions, ...props }, ref) => {
10
+ const text = useNexubMessages().emptyStates[kind];
11
+ const Icon = presetIcon[kind];
12
+ return /* @__PURE__ */ jsxs(EmptyState.Root, { ref, role: kind === "serverError" || kind === "offline" ? "alert" : void 0, ...props, children: [
13
+ /* @__PURE__ */ jsx(EmptyState.Illustration, { children: /* @__PURE__ */ jsx(Icon, { size: "xl", "aria-hidden": "true" }) }),
14
+ /* @__PURE__ */ jsx(EmptyState.Title, { children: title ?? text.title }),
15
+ /* @__PURE__ */ jsx(EmptyState.Description, { children: description ?? text.description }),
16
+ actions && /* @__PURE__ */ jsx(EmptyState.Actions, { children: actions })
17
+ ] });
18
+ }
19
+ );
20
+ EmptyStatePreset.displayName = "EmptyStatePreset";
21
+ export {
22
+ EmptyStatePreset
23
+ };
@@ -1 +1,3 @@
1
1
  export { EmptyState } from './EmptyState.js';
2
+ export { EmptyStatePreset } from './EmptyStatePreset.js';
3
+ export type { EmptyStatePresetProps, EmptyStateKind } from './EmptyStatePreset.js';
@@ -1,4 +1,6 @@
1
1
  import { EmptyState } from "./EmptyState.js";
2
+ import { EmptyStatePreset } from "./EmptyStatePreset.js";
2
3
  export {
3
- EmptyState
4
+ EmptyState,
5
+ EmptyStatePreset
4
6
  };
@@ -20,11 +20,18 @@ export interface FileUploadRootProps extends React.HTMLAttributes<HTMLDivElement
20
20
  /** Files turned away by `accept`, `maxSize` or `maxFiles`, from the picker or a drop. */
21
21
  onReject?: (rejections: FileRejection[]) => void;
22
22
  }
23
+ export interface FileUploadItemProps extends React.HTMLAttributes<HTMLLIElement> {
24
+ index: number;
25
+ /** Upload progress 0–100. Below 100 shows a bar and, with `onCancel`, a cancel button. Your app does the upload. */
26
+ progress?: number;
27
+ /** Stops this file's upload (e.g. AbortController.abort()); shown while `progress` is below 100. */
28
+ onCancel?: () => void;
29
+ /** Why this file failed; shown under its name. */
30
+ error?: string;
31
+ }
23
32
  export declare const FileUpload: {
24
33
  Root: React.ForwardRefExoticComponent<FileUploadRootProps & React.RefAttributes<HTMLDivElement>>;
25
34
  Dropzone: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
26
35
  List: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLUListElement> & React.RefAttributes<HTMLUListElement>>;
27
- Item: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLLIElement> & {
28
- index: number;
29
- } & React.RefAttributes<HTMLLIElement>>;
36
+ Item: React.ForwardRefExoticComponent<FileUploadItemProps & React.RefAttributes<HTMLLIElement>>;
30
37
  };
@@ -4,6 +4,7 @@ import * as React from "react";
4
4
  import { Upload, X } from "@nexub/icons";
5
5
  import { cn } from "../../utils/cn.js";
6
6
  import { useLocale, useNexubMessages } from "../NexubProvider/NexubProvider.js";
7
+ import { Progress } from "../Progress/Progress.js";
7
8
  function formatFileSize(bytes, locale) {
8
9
  const [value, unit] = bytes < 1024 ? [bytes, "byte"] : bytes < 1024 * 1024 ? [bytes / 1024, "kilobyte"] : [bytes / (1024 * 1024), "megabyte"];
9
10
  return new Intl.NumberFormat(locale, { style: "unit", unit, unitDisplay: "short", maximumFractionDigits: 1 }).format(value);
@@ -141,12 +142,14 @@ const List = React.forwardRef(
141
142
  }
142
143
  );
143
144
  List.displayName = "FileUpload.List";
144
- const Item = React.forwardRef(({ className, index, ...props }, ref) => {
145
+ const Item = React.forwardRef(({ className, index, progress, onCancel, error, ...props }, ref) => {
145
146
  const { files, removeFile } = useFileUpload();
146
147
  const messages = useNexubMessages();
147
148
  const { locale } = useLocale();
148
149
  const file = files[index];
149
150
  if (!file) return null;
151
+ const uploading = progress !== void 0 && progress < 100;
152
+ const percent = Math.round(Math.max(0, Math.min(100, progress ?? 0)));
150
153
  return /* @__PURE__ */ jsxs(
151
154
  "li",
152
155
  {
@@ -159,13 +162,19 @@ const Item = React.forwardRef(({ className, index, ...props }, ref) => {
159
162
  children: [
160
163
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
161
164
  /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-[var(--color-on-surface)]", children: file.name }),
162
- /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--color-on-surface-muted)]", children: formatFileSize(file.size, locale) })
165
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-[var(--color-on-surface-muted)]", children: [
166
+ formatFileSize(file.size, locale),
167
+ uploading && ` \xB7 ${percent}%`
168
+ ] }),
169
+ uploading && /* @__PURE__ */ jsx(Progress, { value: percent, size: "sm", className: "mt-1.5", "aria-label": messages.uploadProgress(file.name, percent) }),
170
+ error && /* @__PURE__ */ jsx("p", { role: "alert", className: "mt-1 text-xs text-[var(--color-error)]", children: error })
163
171
  ] }),
164
172
  /* @__PURE__ */ jsx(
165
173
  "button",
166
174
  {
167
- onClick: () => removeFile(index),
168
- "aria-label": messages.removeFile(file.name),
175
+ type: "button",
176
+ onClick: () => uploading && onCancel ? onCancel() : removeFile(index),
177
+ "aria-label": uploading && onCancel ? messages.cancelUpload(file.name) : messages.removeFile(file.name),
169
178
  className: "nx-state-layer shrink-0 rounded-full p-1 text-[var(--color-on-surface-muted)] hover:text-[var(--color-on-surface)] transition-colors",
170
179
  children: /* @__PURE__ */ jsx(X, { size: "sm", "aria-hidden": "true" })
171
180
  }
@@ -1,2 +1,2 @@
1
1
  export { FileUpload, fileAccepted } from './FileUpload.js';
2
- export type { FileUploadRootProps, FileRejection } from './FileUpload.js';
2
+ export type { FileUploadRootProps, FileUploadItemProps, FileRejection } from './FileUpload.js';
@@ -0,0 +1,50 @@
1
+ import * as React from 'react';
2
+ import type { NexubMessages } from '@nexub/core';
3
+ export type FilterFieldType = 'text' | 'number' | 'select' | 'date';
4
+ export type FilterOperator = keyof NexubMessages['filterOperators'];
5
+ export interface FilterField {
6
+ id: string;
7
+ label: string;
8
+ type: FilterFieldType;
9
+ /** Choices for `type: 'select'`. */
10
+ options?: {
11
+ value: string;
12
+ label: string;
13
+ }[];
14
+ }
15
+ export interface FilterRule {
16
+ id: string;
17
+ field: string;
18
+ operator: FilterOperator;
19
+ /** Always a string (dates as YYYY-MM-DD), so the query serialises as-is. */
20
+ value: string;
21
+ }
22
+ export interface FilterGroup {
23
+ id: string;
24
+ combinator: 'and' | 'or';
25
+ rules: Array<FilterRule | FilterGroup>;
26
+ }
27
+ /** Operators offered for each field type, in menu order. */
28
+ export declare const filterOperators: Record<FilterFieldType, FilterOperator[]>;
29
+ export declare const isFilterGroup: (r: FilterRule | FilterGroup) => r is FilterGroup;
30
+ /** An empty top-level group, a starting `value`. */
31
+ export declare const emptyFilter: () => FilterGroup;
32
+ /** Rules with a field, operator and (unless "is empty") a value. Counts nested groups too. */
33
+ export declare function countFilters(group: FilterGroup): number;
34
+ export interface FilterBuilderProps {
35
+ fields: FilterField[];
36
+ value?: FilterGroup;
37
+ defaultValue?: FilterGroup;
38
+ onValueChange?: (value: FilterGroup) => void;
39
+ /** How many levels of nested groups "Add group" may create. Default 1; 0 hides it. */
40
+ maxDepth?: number;
41
+ className?: string;
42
+ }
43
+ /**
44
+ * Builds an AND/OR filter query as plain JSON. Nexub doesn't run it: send it
45
+ * to your API or evaluate it yourself.
46
+ */
47
+ export declare function FilterBuilder({ fields, value: valueProp, defaultValue, onValueChange, maxDepth, className }: FilterBuilderProps): React.JSX.Element;
48
+ export declare namespace FilterBuilder {
49
+ var displayName: string;
50
+ }