@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.
- package/accessibility.json +34 -1
- package/dist/components/AlertDialog/AlertDialog.d.ts +17 -0
- package/dist/components/AlertDialog/AlertDialog.js +40 -5
- package/dist/components/AlertDialog/index.d.ts +1 -0
- package/dist/components/DateRangePicker/DateRangePicker.d.ts +17 -0
- package/dist/components/DateRangePicker/DateRangePicker.js +123 -71
- package/dist/components/DateRangePicker/index.d.ts +2 -1
- package/dist/components/DateRangePicker/index.js +3 -1
- package/dist/components/EmptyState/EmptyStatePreset.d.ts +14 -0
- package/dist/components/EmptyState/EmptyStatePreset.js +23 -0
- package/dist/components/EmptyState/index.d.ts +2 -0
- package/dist/components/EmptyState/index.js +3 -1
- package/dist/components/FileUpload/FileUpload.d.ts +10 -3
- package/dist/components/FileUpload/FileUpload.js +13 -4
- package/dist/components/FileUpload/index.d.ts +1 -1
- package/dist/components/FilterBuilder/FilterBuilder.d.ts +50 -0
- package/dist/components/FilterBuilder/FilterBuilder.js +164 -0
- package/dist/components/FilterBuilder/index.d.ts +2 -0
- package/dist/components/FilterBuilder/index.js +8 -0
- package/dist/components/OTPInput/OTPInput.d.ts +28 -0
- package/dist/components/OTPInput/OTPInput.js +107 -0
- package/dist/components/OTPInput/index.d.ts +2 -0
- package/dist/components/OTPInput/index.js +4 -0
- package/dist/components/SavedViews/SavedViews.d.ts +26 -0
- package/dist/components/SavedViews/SavedViews.js +65 -0
- package/dist/components/SavedViews/index.d.ts +2 -0
- package/dist/components/SavedViews/index.js +4 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.js +14 -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.
|
|
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
|
-
|
|
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
|
|
@@ -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(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
2
|
+
export { todayIn } from './DateRangePicker.js';
|
|
3
|
+
export type { DateRangePickerProps, DateRange, DateRangePreset } from './DateRangePicker.js';
|
|
@@ -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
|
+
};
|
|
@@ -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.
|
|
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__ */
|
|
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
|
-
|
|
168
|
-
|
|
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
|
+
}
|