@nexub/react 7.4.0 → 7.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 +1 -1
- package/dist/components/Badge/Badge.d.ts +9 -0
- package/dist/components/Badge/Badge.js +40 -2
- package/dist/components/Button/Button.d.ts +1 -1
- package/dist/components/Combobox/Combobox.d.ts +6 -9
- package/dist/components/Combobox/Combobox.js +30 -9
- package/dist/components/CommandPalette/CommandPalette.d.ts +34 -13
- package/dist/components/CommandPalette/CommandPalette.js +174 -40
- package/dist/components/CommandPalette/index.d.ts +1 -1
- package/dist/components/DatePicker/DatePicker.d.ts +16 -1
- package/dist/components/DatePicker/DatePicker.js +100 -26
- package/dist/components/Dialog/Dialog.d.ts +7 -0
- package/dist/components/Dialog/Dialog.js +35 -8
- package/dist/components/EmptyState/EmptyState.d.ts +7 -1
- package/dist/components/EmptyState/EmptyState.js +21 -5
- package/dist/components/EmptyState/EmptyStatePreset.d.ts +2 -1
- package/dist/components/FileUpload/FileUpload.js +86 -32
- package/dist/components/Heading/Heading.d.ts +1 -1
- package/dist/components/IconButton/IconButton.d.ts +5 -1
- package/dist/components/IconButton/IconButton.js +14 -3
- package/dist/components/Input/Input.d.ts +6 -0
- package/dist/components/Input/Input.js +47 -5
- package/dist/components/Select/Select.d.ts +18 -3
- package/dist/components/Select/Select.js +170 -71
- package/dist/components/Slider/Slider.d.ts +10 -0
- package/dist/components/Slider/Slider.js +48 -3
- package/dist/components/Slider/index.d.ts +1 -1
- package/dist/components/Switch/Switch.d.ts +6 -0
- package/dist/components/Switch/Switch.js +29 -2
- package/dist/components/Tabs/Tabs.d.ts +5 -1
- package/dist/components/Tabs/Tabs.js +98 -35
- package/dist/components/Text/Text.d.ts +1 -1
- package/dist/components/Tooltip/Tooltip.d.ts +10 -3
- package/dist/components/Tooltip/Tooltip.js +16 -4
- package/dist/index.d.ts +3 -1
- package/dist/styles.css +1 -1
- package/package.json +6 -6
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import * as RadixPopover from "@radix-ui/react-popover";
|
|
5
|
-
import { DayPicker } from "react-day-picker";
|
|
6
|
-
import { format } from "date-fns";
|
|
5
|
+
import { DayPicker, dateMatchModifiers } from "react-day-picker";
|
|
6
|
+
import { format, isValid, parse } from "date-fns";
|
|
7
7
|
import { CalendarDays, ChevronLeft, ChevronRight } from "@nexub/icons";
|
|
8
8
|
import { cn } from "../../utils/cn.js";
|
|
9
9
|
import { useIntlLocale, useLocale, useNexubMessages } from "../NexubProvider/NexubProvider.js";
|
|
10
|
-
import { field, fieldFocusVisible } from "../../utils/control.js";
|
|
10
|
+
import { field, fieldDisabled, fieldFocusVisible, fieldFocusWithin, fieldInvalid } from "../../utils/control.js";
|
|
11
11
|
import { floatingSurface } from "../../utils/surface.js";
|
|
12
|
+
import { Button } from "../Button/Button.js";
|
|
12
13
|
function formatDate(date, locale, dateLocale) {
|
|
13
14
|
if (dateLocale) return format(date, "PP", { locale: dateLocale });
|
|
14
15
|
return locale ? new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(date) : format(date, "PP");
|
|
@@ -71,7 +72,18 @@ const calendarClassNames = {
|
|
|
71
72
|
today: "[&>button]:border-[var(--color-primary)]",
|
|
72
73
|
outside: "[&:not([data-selected]):not([data-disabled])>button]:text-[var(--color-on-surface-muted)]",
|
|
73
74
|
disabled: "",
|
|
74
|
-
hidden: "invisible"
|
|
75
|
+
hidden: "invisible",
|
|
76
|
+
// Month change (animate): the months crossfade. react-day-picker adds each of
|
|
77
|
+
// these with classList.add, so one class apiece. Keyframes from @nexub/themes
|
|
78
|
+
// core.css; reduced motion collapses them and animationend still cleans up.
|
|
79
|
+
caption_after_enter: "animate-[nx-fade-in_var(--motion-spring-effects-default-duration)_var(--motion-spring-effects-default-easing)]",
|
|
80
|
+
caption_before_enter: "animate-[nx-fade-in_var(--motion-spring-effects-default-duration)_var(--motion-spring-effects-default-easing)]",
|
|
81
|
+
weeks_after_enter: "animate-[nx-fade-in_var(--motion-spring-effects-default-duration)_var(--motion-spring-effects-default-easing)]",
|
|
82
|
+
weeks_before_enter: "animate-[nx-fade-in_var(--motion-spring-effects-default-duration)_var(--motion-spring-effects-default-easing)]",
|
|
83
|
+
caption_after_exit: "animate-[nx-fade-out_var(--motion-spring-effects-fast-duration)_var(--motion-spring-effects-fast-easing)_forwards]",
|
|
84
|
+
caption_before_exit: "animate-[nx-fade-out_var(--motion-spring-effects-fast-duration)_var(--motion-spring-effects-fast-easing)_forwards]",
|
|
85
|
+
weeks_after_exit: "animate-[nx-fade-out_var(--motion-spring-effects-fast-duration)_var(--motion-spring-effects-fast-easing)_forwards]",
|
|
86
|
+
weeks_before_exit: "animate-[nx-fade-out_var(--motion-spring-effects-fast-duration)_var(--motion-spring-effects-fast-easing)_forwards]"
|
|
75
87
|
};
|
|
76
88
|
const DatePicker = React.forwardRef(
|
|
77
89
|
function DatePicker2({
|
|
@@ -84,15 +96,75 @@ const DatePicker = React.forwardRef(
|
|
|
84
96
|
className,
|
|
85
97
|
id,
|
|
86
98
|
"aria-label": ariaLabel,
|
|
87
|
-
locale: localeProp
|
|
99
|
+
locale: localeProp,
|
|
100
|
+
disabledDates,
|
|
101
|
+
presets,
|
|
102
|
+
typeable = false
|
|
88
103
|
}, ref) {
|
|
89
104
|
const [open, setOpen] = React.useState(false);
|
|
90
105
|
const messages = useNexubMessages();
|
|
91
106
|
const placeholder = placeholderProp ?? messages.pickDate;
|
|
92
107
|
const calendar = useCalendarLocale(localeProp);
|
|
93
108
|
const display = value ? calendar.format(value) : placeholder;
|
|
109
|
+
const [text, setText] = React.useState(null);
|
|
110
|
+
const dateLocale = calendar.dayPickerProps.locale;
|
|
111
|
+
const typed = text?.trim() ? parse(text.trim(), "P", /* @__PURE__ */ new Date(), { locale: dateLocale }) : void 0;
|
|
112
|
+
const invalid = typed !== void 0 && (!isValid(typed) || minDate !== void 0 && typed < minDate || maxDate !== void 0 && typed > maxDate || disabledDates !== void 0 && dateMatchModifiers(typed, disabledDates));
|
|
113
|
+
const commit = () => {
|
|
114
|
+
if (text === null || invalid) return;
|
|
115
|
+
onChange?.(typed);
|
|
116
|
+
setText(null);
|
|
117
|
+
};
|
|
118
|
+
const pick = (date) => {
|
|
119
|
+
onChange?.(date);
|
|
120
|
+
setText(null);
|
|
121
|
+
setOpen(false);
|
|
122
|
+
};
|
|
94
123
|
return /* @__PURE__ */ jsxs(RadixPopover.Root, { open, onOpenChange: setOpen, children: [
|
|
95
|
-
/* @__PURE__ */ jsx(RadixPopover.
|
|
124
|
+
typeable ? /* @__PURE__ */ jsx(RadixPopover.Anchor, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
125
|
+
"div",
|
|
126
|
+
{
|
|
127
|
+
className: cn(
|
|
128
|
+
"flex h-control-md w-full items-center gap-1 ps-3.5 pe-1",
|
|
129
|
+
field,
|
|
130
|
+
fieldFocusWithin,
|
|
131
|
+
invalid && fieldInvalid,
|
|
132
|
+
disabled && fieldDisabled,
|
|
133
|
+
className
|
|
134
|
+
),
|
|
135
|
+
children: [
|
|
136
|
+
/* @__PURE__ */ jsx(
|
|
137
|
+
"input",
|
|
138
|
+
{
|
|
139
|
+
id,
|
|
140
|
+
type: "text",
|
|
141
|
+
disabled,
|
|
142
|
+
"aria-label": ariaLabel,
|
|
143
|
+
"aria-invalid": invalid || void 0,
|
|
144
|
+
placeholder,
|
|
145
|
+
value: text ?? (value ? format(value, "P", { locale: dateLocale }) : ""),
|
|
146
|
+
onChange: (e) => setText(e.target.value),
|
|
147
|
+
onBlur: commit,
|
|
148
|
+
onKeyDown: (e) => {
|
|
149
|
+
if (e.key === "Enter") commit();
|
|
150
|
+
},
|
|
151
|
+
className: "min-w-0 flex-1 bg-transparent text-body-md outline-none placeholder:text-[var(--color-on-surface-muted)] disabled:cursor-not-allowed"
|
|
152
|
+
}
|
|
153
|
+
),
|
|
154
|
+
/* @__PURE__ */ jsx(RadixPopover.Trigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
155
|
+
"button",
|
|
156
|
+
{
|
|
157
|
+
ref,
|
|
158
|
+
type: "button",
|
|
159
|
+
disabled,
|
|
160
|
+
"aria-label": messages.pickDate,
|
|
161
|
+
className: "nx-state-layer inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full disabled:pointer-events-none",
|
|
162
|
+
children: /* @__PURE__ */ jsx(CalendarDays, { size: "sm", "aria-hidden": "true" })
|
|
163
|
+
}
|
|
164
|
+
) })
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
) }) : /* @__PURE__ */ jsx(RadixPopover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
96
168
|
"button",
|
|
97
169
|
{
|
|
98
170
|
ref,
|
|
@@ -115,7 +187,7 @@ const DatePicker = React.forwardRef(
|
|
|
115
187
|
]
|
|
116
188
|
}
|
|
117
189
|
) }),
|
|
118
|
-
/* @__PURE__ */ jsx(RadixPopover.Portal, { children: /* @__PURE__ */
|
|
190
|
+
/* @__PURE__ */ jsx(RadixPopover.Portal, { children: /* @__PURE__ */ jsxs(
|
|
119
191
|
RadixPopover.Content,
|
|
120
192
|
{
|
|
121
193
|
align: "start",
|
|
@@ -124,25 +196,27 @@ const DatePicker = React.forwardRef(
|
|
|
124
196
|
floatingSurface,
|
|
125
197
|
"nx-motion nx-pop origin-[var(--radix-popover-content-transform-origin)]"
|
|
126
198
|
),
|
|
127
|
-
children:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
199
|
+
children: [
|
|
200
|
+
/* @__PURE__ */ jsx(
|
|
201
|
+
DayPicker,
|
|
202
|
+
{
|
|
203
|
+
mode: "single",
|
|
204
|
+
selected: value,
|
|
205
|
+
defaultMonth: value,
|
|
206
|
+
onSelect: pick,
|
|
207
|
+
disabled: disabledDates,
|
|
208
|
+
startMonth: minDate,
|
|
209
|
+
endMonth: maxDate,
|
|
210
|
+
autoFocus: true,
|
|
211
|
+
animate: true,
|
|
212
|
+
showOutsideDays: true,
|
|
213
|
+
className: "relative p-3",
|
|
214
|
+
classNames: calendarClassNames,
|
|
215
|
+
...calendar.dayPickerProps
|
|
216
|
+
}
|
|
217
|
+
),
|
|
218
|
+
presets && presets.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1 border-t border-[var(--color-outline-variant)] p-2", children: presets.map((p) => /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "xs", onClick: () => pick(p.date), children: p.label }, p.label)) })
|
|
219
|
+
]
|
|
146
220
|
}
|
|
147
221
|
) })
|
|
148
222
|
] });
|
|
@@ -15,6 +15,12 @@ declare function Root({ scrollLock, modal, ...props }: DialogRootProps): React.J
|
|
|
15
15
|
interface ContentProps extends RadixDialog.DialogContentProps {
|
|
16
16
|
size?: Size | 'xl' | 'full';
|
|
17
17
|
surface?: SurfaceValue;
|
|
18
|
+
/** Adds a close button in the top-end corner. @default false */
|
|
19
|
+
showClose?: boolean;
|
|
20
|
+
/** Accessible name of the close button. @default messages.close */
|
|
21
|
+
closeLabel?: string;
|
|
22
|
+
/** `true`: bottom sheet. `'auto'`: bottom sheet below the `md` breakpoint, centred above. @default false */
|
|
23
|
+
sheet?: boolean | 'auto';
|
|
18
24
|
}
|
|
19
25
|
export declare const Dialog: {
|
|
20
26
|
Root: typeof Root;
|
|
@@ -26,6 +32,7 @@ export declare const Dialog: {
|
|
|
26
32
|
Title: React.ForwardRefExoticComponent<RadixDialog.DialogTitleProps & React.RefAttributes<HTMLHeadingElement>>;
|
|
27
33
|
Description: React.ForwardRefExoticComponent<RadixDialog.DialogDescriptionProps & React.RefAttributes<HTMLParagraphElement>>;
|
|
28
34
|
Header: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
|
|
35
|
+
Body: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
|
|
29
36
|
Footer: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
|
|
30
37
|
};
|
|
31
38
|
export {};
|
|
@@ -1,11 +1,15 @@
|
|
|
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 RadixDialog from "@radix-ui/react-dialog";
|
|
5
5
|
import { cn } from "../../utils/cn.js";
|
|
6
6
|
import { overlaySurface, scrim } from "../../utils/surface.js";
|
|
7
7
|
import { glassSurfaceClass } from "../../utils/glass.js";
|
|
8
8
|
import { ClassLockOverlay, ScrollLockContext } from "../../hooks/useClassScrollLock.js";
|
|
9
|
+
import { useMediaQuery } from "../../hooks/useMediaQuery.js";
|
|
10
|
+
import { X } from "@nexub/icons";
|
|
11
|
+
import { IconButton } from "../IconButton/IconButton.js";
|
|
12
|
+
import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
|
|
9
13
|
function Root({ scrollLock = "style", modal = true, ...props }) {
|
|
10
14
|
const lock = React.useMemo(() => ({ scrollLock, modal }), [scrollLock, modal]);
|
|
11
15
|
return /* @__PURE__ */ jsx(ScrollLockContext.Provider, { value: lock, children: /* @__PURE__ */ jsx(RadixDialog.Root, { modal, ...props }) });
|
|
@@ -33,8 +37,19 @@ const Overlay = React.forwardRef(
|
|
|
33
37
|
}
|
|
34
38
|
);
|
|
35
39
|
Overlay.displayName = "Dialog.Overlay";
|
|
40
|
+
const StackContext = React.createContext(null);
|
|
36
41
|
const Content = React.forwardRef(
|
|
37
|
-
function DialogContent({ className, size = "md", surface = "solid", children, ...props }, ref) {
|
|
42
|
+
function DialogContent({ className, size = "md", surface = "solid", showClose = false, closeLabel, sheet = false, children, ...props }, ref) {
|
|
43
|
+
const messages = useNexubMessages();
|
|
44
|
+
const narrow = useMediaQuery("(max-width: 47.99rem)");
|
|
45
|
+
const asSheet = sheet === true || sheet === "auto" && narrow;
|
|
46
|
+
const parent = React.useContext(StackContext);
|
|
47
|
+
const [openChildren, setOpenChildren] = React.useState(0);
|
|
48
|
+
React.useEffect(() => {
|
|
49
|
+
if (!parent) return;
|
|
50
|
+
parent((n) => n + 1);
|
|
51
|
+
return () => parent((n) => n - 1);
|
|
52
|
+
}, [parent]);
|
|
38
53
|
const sizeMap = {
|
|
39
54
|
sm: "max-w-sm",
|
|
40
55
|
md: "max-w-lg",
|
|
@@ -42,23 +57,28 @@ const Content = React.forwardRef(
|
|
|
42
57
|
xl: "max-w-4xl",
|
|
43
58
|
full: "max-w-[calc(100vw-2rem)]"
|
|
44
59
|
};
|
|
45
|
-
return /* @__PURE__ */
|
|
60
|
+
return /* @__PURE__ */ jsxs(
|
|
46
61
|
RadixDialog.Content,
|
|
47
62
|
{
|
|
48
63
|
ref,
|
|
49
64
|
className: cn(
|
|
50
|
-
"fixed left-1/2 top-1/2 z-[var(--z-modal)] w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2",
|
|
65
|
+
asSheet ? "fixed inset-x-0 bottom-0 z-[var(--z-modal)] w-full" : "fixed left-1/2 top-1/2 z-[var(--z-modal)] w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2",
|
|
51
66
|
"nx-motion nx-dialog",
|
|
52
67
|
overlaySurface,
|
|
53
68
|
"max-h-[90vh] overflow-y-auto p-6 focus:outline-none",
|
|
54
|
-
sizeMap[size],
|
|
69
|
+
asSheet ? "rounded-b-none" : sizeMap[size],
|
|
55
70
|
surface === "glass" && glassSurfaceClass,
|
|
56
71
|
className
|
|
57
72
|
),
|
|
58
73
|
"data-material": surface === "glass" ? "glass" : void 0,
|
|
59
74
|
"data-glass-thickness": surface === "glass" ? "thick" : void 0,
|
|
75
|
+
"data-sheet": asSheet ? "" : void 0,
|
|
76
|
+
"data-nested": openChildren > 0 ? "" : void 0,
|
|
60
77
|
...props,
|
|
61
|
-
children
|
|
78
|
+
children: [
|
|
79
|
+
/* @__PURE__ */ jsx(StackContext.Provider, { value: setOpenChildren, children }),
|
|
80
|
+
showClose && /* @__PURE__ */ jsx(RadixDialog.Close, { asChild: true, children: /* @__PURE__ */ jsx(IconButton, { label: closeLabel ?? messages.close, variant: "ghost", size: "sm", className: "absolute end-3 top-3", children: /* @__PURE__ */ jsx(X, { size: "sm", "aria-hidden": "true" }) }) })
|
|
81
|
+
]
|
|
62
82
|
}
|
|
63
83
|
);
|
|
64
84
|
}
|
|
@@ -92,16 +112,22 @@ const Description = React.forwardRef(
|
|
|
92
112
|
Description.displayName = "Dialog.Description";
|
|
93
113
|
const Header = React.forwardRef(
|
|
94
114
|
function DialogHeader({ className, ...props }, ref) {
|
|
95
|
-
return /* @__PURE__ */ jsx("div", { ref, className: cn("mb-4 flex flex-col gap-1", className), ...props });
|
|
115
|
+
return /* @__PURE__ */ jsx("div", { ref, className: cn("nx-dialog-header mb-4 flex flex-col gap-1", className), ...props });
|
|
96
116
|
}
|
|
97
117
|
);
|
|
98
118
|
Header.displayName = "Dialog.Header";
|
|
99
119
|
const Footer = React.forwardRef(
|
|
100
120
|
function DialogFooter({ className, ...props }, ref) {
|
|
101
|
-
return /* @__PURE__ */ jsx("div", { ref, className: cn("mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className), ...props });
|
|
121
|
+
return /* @__PURE__ */ jsx("div", { ref, className: cn("nx-dialog-footer mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className), ...props });
|
|
102
122
|
}
|
|
103
123
|
);
|
|
104
124
|
Footer.displayName = "Dialog.Footer";
|
|
125
|
+
const Body = React.forwardRef(
|
|
126
|
+
function DialogBody({ className, ...props }, ref) {
|
|
127
|
+
return /* @__PURE__ */ jsx("div", { ref, className: cn("nx-dialog-body", className), ...props });
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
Body.displayName = "Dialog.Body";
|
|
105
131
|
const Dialog = {
|
|
106
132
|
Root,
|
|
107
133
|
Trigger,
|
|
@@ -112,6 +138,7 @@ const Dialog = {
|
|
|
112
138
|
Title,
|
|
113
139
|
Description,
|
|
114
140
|
Header,
|
|
141
|
+
Body,
|
|
115
142
|
Footer
|
|
116
143
|
};
|
|
117
144
|
export {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
export interface EmptyStateRootProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
/** Scales spacing, icon and text. @default 'md' */
|
|
4
|
+
size?: 'sm' | 'md' | 'lg';
|
|
5
|
+
/** Fades in on mount (nx-reveal; reduced motion keeps the crossfade). */
|
|
6
|
+
animate?: boolean;
|
|
7
|
+
}
|
|
2
8
|
export declare const EmptyState: {
|
|
3
|
-
Root: React.ForwardRefExoticComponent<
|
|
9
|
+
Root: React.ForwardRefExoticComponent<EmptyStateRootProps & React.RefAttributes<HTMLDivElement>>;
|
|
4
10
|
Illustration: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
|
|
5
11
|
Title: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLHeadingElement> & React.RefAttributes<HTMLHeadingElement>>;
|
|
6
12
|
Description: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLParagraphElement> & React.RefAttributes<HTMLParagraphElement>>;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { cn } from "../../utils/cn.js";
|
|
4
|
+
const rootSizes = { sm: "gap-3 py-8", md: "gap-4 py-16", lg: "gap-6 py-24" };
|
|
4
5
|
const Root = React.forwardRef(
|
|
5
|
-
({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
6
|
+
({ className, size = "md", animate, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
6
7
|
"div",
|
|
7
8
|
{
|
|
8
9
|
ref,
|
|
10
|
+
"data-size": size,
|
|
9
11
|
className: cn(
|
|
10
|
-
"flex flex-col items-center justify-center
|
|
12
|
+
"group/empty-state flex flex-col items-center justify-center text-center",
|
|
13
|
+
rootSizes[size],
|
|
14
|
+
animate && "nx-reveal",
|
|
11
15
|
className
|
|
12
16
|
),
|
|
13
17
|
...props
|
|
@@ -20,7 +24,11 @@ const Illustration = React.forwardRef(
|
|
|
20
24
|
"div",
|
|
21
25
|
{
|
|
22
26
|
ref,
|
|
23
|
-
className: cn(
|
|
27
|
+
className: cn(
|
|
28
|
+
"text-[var(--color-on-surface-muted)]",
|
|
29
|
+
"group-data-[size=sm]/empty-state:[&_svg]:size-8 group-data-[size=lg]/empty-state:[&_svg]:size-16",
|
|
30
|
+
className
|
|
31
|
+
),
|
|
24
32
|
...props
|
|
25
33
|
}
|
|
26
34
|
)
|
|
@@ -31,7 +39,11 @@ const Title = React.forwardRef(
|
|
|
31
39
|
"h3",
|
|
32
40
|
{
|
|
33
41
|
ref,
|
|
34
|
-
className: cn(
|
|
42
|
+
className: cn(
|
|
43
|
+
"text-lg font-semibold text-[var(--color-on-surface)]",
|
|
44
|
+
"group-data-[size=sm]/empty-state:text-base group-data-[size=lg]/empty-state:text-xl",
|
|
45
|
+
className
|
|
46
|
+
),
|
|
35
47
|
...props
|
|
36
48
|
}
|
|
37
49
|
)
|
|
@@ -41,7 +53,11 @@ const Description = React.forwardRef(({ className, ...props }, ref) => /* @__PUR
|
|
|
41
53
|
"p",
|
|
42
54
|
{
|
|
43
55
|
ref,
|
|
44
|
-
className: cn(
|
|
56
|
+
className: cn(
|
|
57
|
+
"max-w-sm text-sm text-[var(--color-on-surface-muted)]",
|
|
58
|
+
"group-data-[size=sm]/empty-state:text-xs group-data-[size=lg]/empty-state:text-base",
|
|
59
|
+
className
|
|
60
|
+
),
|
|
45
61
|
...props
|
|
46
62
|
}
|
|
47
63
|
));
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import type { NexubMessages } from '@nexub/core';
|
|
3
|
+
import { type EmptyStateRootProps } from './EmptyState.js';
|
|
3
4
|
export type EmptyStateKind = keyof NexubMessages['emptyStates'];
|
|
4
|
-
export interface EmptyStatePresetProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
5
|
+
export interface EmptyStatePresetProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'>, Pick<EmptyStateRootProps, 'size' | 'animate'> {
|
|
5
6
|
/** noData, noResults, noAccess (403), serverError (500) or offline. */
|
|
6
7
|
kind: EmptyStateKind;
|
|
7
8
|
/** Overrides the preset's title; the description too. Both default to localised text. */
|
|
@@ -5,6 +5,7 @@ import { Upload, X } from "@nexub/icons";
|
|
|
5
5
|
import { cn } from "../../utils/cn.js";
|
|
6
6
|
import { useLocale, useNexubMessages } from "../NexubProvider/NexubProvider.js";
|
|
7
7
|
import { Progress } from "../Progress/Progress.js";
|
|
8
|
+
import { SPRING_MS } from "../Motion/useMotion.js";
|
|
8
9
|
function formatFileSize(bytes, locale) {
|
|
9
10
|
const [value, unit] = bytes < 1024 ? [bytes, "byte"] : bytes < 1024 * 1024 ? [bytes / 1024, "kilobyte"] : [bytes / (1024 * 1024), "megabyte"];
|
|
10
11
|
return new Intl.NumberFormat(locale, { style: "unit", unit, unitDisplay: "short", maximumFractionDigits: 1 }).format(value);
|
|
@@ -27,7 +28,7 @@ function useFileUpload() {
|
|
|
27
28
|
return ctx;
|
|
28
29
|
}
|
|
29
30
|
const Root = React.forwardRef(
|
|
30
|
-
({ className, accept, maxFiles, maxSize, onFilesChange, onReject, validate, children, ...props }, ref) => {
|
|
31
|
+
({ className, accept, maxFiles, maxSize, onFilesChange, onReject, validate, onPaste, children, ...props }, ref) => {
|
|
31
32
|
const [files, setFiles] = React.useState([]);
|
|
32
33
|
const [rejections, setRejections] = React.useState([]);
|
|
33
34
|
const [isDragging, setIsDragging] = React.useState(false);
|
|
@@ -65,7 +66,22 @@ const Root = React.forwardRef(
|
|
|
65
66
|
() => ({ accept, maxFiles, maxSize, onFilesChange, files, rejections, addFiles, removeFile, isDragging, setIsDragging }),
|
|
66
67
|
[accept, maxFiles, maxSize, onFilesChange, files, rejections, addFiles, removeFile, isDragging, setIsDragging]
|
|
67
68
|
);
|
|
68
|
-
return /* @__PURE__ */ jsx(FileUploadContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
|
|
69
|
+
return /* @__PURE__ */ jsx(FileUploadContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
|
|
70
|
+
"div",
|
|
71
|
+
{
|
|
72
|
+
ref,
|
|
73
|
+
className: cn("flex flex-col gap-4", className),
|
|
74
|
+
onPaste: (e) => {
|
|
75
|
+
onPaste?.(e);
|
|
76
|
+
const pasted = Array.from(e.clipboardData?.files ?? []);
|
|
77
|
+
if (e.defaultPrevented || !pasted.length) return;
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
addFiles(pasted);
|
|
80
|
+
},
|
|
81
|
+
...props,
|
|
82
|
+
children
|
|
83
|
+
}
|
|
84
|
+
) });
|
|
69
85
|
}
|
|
70
86
|
);
|
|
71
87
|
Root.displayName = "FileUpload.Root";
|
|
@@ -140,7 +156,7 @@ const List = React.forwardRef(
|
|
|
140
156
|
({ className, ...props }, ref) => {
|
|
141
157
|
const { files } = useFileUpload();
|
|
142
158
|
if (files.length === 0) return null;
|
|
143
|
-
return /* @__PURE__ */ jsx("ul", { ref, className: cn("flex flex-col
|
|
159
|
+
return /* @__PURE__ */ jsx("ul", { ref, className: cn("flex flex-col", className), ...props });
|
|
144
160
|
}
|
|
145
161
|
);
|
|
146
162
|
List.displayName = "FileUpload.List";
|
|
@@ -149,40 +165,78 @@ const Item = React.forwardRef(({ className, index, progress, onCancel, error, ..
|
|
|
149
165
|
const messages = useNexubMessages();
|
|
150
166
|
const { locale } = useLocale();
|
|
151
167
|
const file = files[index];
|
|
168
|
+
const [exiting, setExiting] = React.useState(null);
|
|
169
|
+
const [entered, setEntered] = React.useState(false);
|
|
170
|
+
const [thumb, setThumb] = React.useState();
|
|
171
|
+
React.useEffect(() => {
|
|
172
|
+
if (!file?.type.startsWith("image/") || typeof URL.createObjectURL !== "function") return;
|
|
173
|
+
const url = URL.createObjectURL(file);
|
|
174
|
+
setThumb(url);
|
|
175
|
+
return () => {
|
|
176
|
+
URL.revokeObjectURL(url);
|
|
177
|
+
setThumb(void 0);
|
|
178
|
+
};
|
|
179
|
+
}, [file]);
|
|
180
|
+
const isExiting = !!file && exiting === file;
|
|
181
|
+
React.useEffect(() => {
|
|
182
|
+
if (!isExiting) return;
|
|
183
|
+
const t = setTimeout(() => removeFile(index), SPRING_MS.effects.default + 50);
|
|
184
|
+
return () => clearTimeout(t);
|
|
185
|
+
}, [isExiting, index, removeFile]);
|
|
152
186
|
if (!file) return null;
|
|
153
187
|
const uploading = progress !== void 0 && progress < 100;
|
|
154
188
|
const percent = Math.round(Math.max(0, Math.min(100, progress ?? 0)));
|
|
155
|
-
return
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
className
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
uploading && /* @__PURE__ */ jsx(Progress, { value: percent, size: "sm", className: "mt-1.5", "aria-label": messages.uploadProgress(file.name, percent) }),
|
|
172
|
-
error && /* @__PURE__ */ jsx("p", { role: "alert", className: "mt-1 text-xs text-[var(--color-error)]", children: error })
|
|
173
|
-
] }),
|
|
174
|
-
/* @__PURE__ */ jsx(
|
|
175
|
-
"button",
|
|
189
|
+
return (
|
|
190
|
+
// nx-list-item: grows in on add, collapses out on remove (grid rows, spring tokens).
|
|
191
|
+
/* @__PURE__ */ jsx(
|
|
192
|
+
"li",
|
|
193
|
+
{
|
|
194
|
+
ref,
|
|
195
|
+
className: "nx-list-item",
|
|
196
|
+
"data-state": isExiting ? "exit" : entered ? void 0 : "enter",
|
|
197
|
+
onAnimationEnd: (e) => {
|
|
198
|
+
if (e.target !== e.currentTarget) return;
|
|
199
|
+
if (isExiting) removeFile(index);
|
|
200
|
+
else setEntered(true);
|
|
201
|
+
},
|
|
202
|
+
...props,
|
|
203
|
+
children: /* @__PURE__ */ jsx("div", { className: "nx-list-item-inner", children: /* @__PURE__ */ jsx("div", { className: "[li:not(:last-child)>.nx-list-item-inner>&]:pb-2", children: /* @__PURE__ */ jsxs(
|
|
204
|
+
"div",
|
|
176
205
|
{
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
children:
|
|
206
|
+
className: cn(
|
|
207
|
+
"flex items-center justify-between gap-3 rounded-shape-container border border-[var(--color-outline-variant)] bg-[var(--color-surface)] p-3",
|
|
208
|
+
className
|
|
209
|
+
),
|
|
210
|
+
children: [
|
|
211
|
+
thumb && /* @__PURE__ */ jsx("img", { src: thumb, alt: "", className: "h-10 w-10 shrink-0 rounded-shape-min object-cover" }),
|
|
212
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
213
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-[var(--color-on-surface)]", children: file.name }),
|
|
214
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs text-[var(--color-on-surface-muted)]", children: [
|
|
215
|
+
formatFileSize(file.size, locale),
|
|
216
|
+
uploading && ` \xB7 ${percent}%`
|
|
217
|
+
] }),
|
|
218
|
+
uploading && /* @__PURE__ */ jsx(Progress, { value: percent, size: "sm", className: "mt-1.5", "aria-label": messages.uploadProgress(file.name, percent) }),
|
|
219
|
+
error && /* @__PURE__ */ jsx("p", { role: "alert", className: "mt-1 text-xs text-[var(--color-error)]", children: error })
|
|
220
|
+
] }),
|
|
221
|
+
/* @__PURE__ */ jsx(
|
|
222
|
+
"button",
|
|
223
|
+
{
|
|
224
|
+
type: "button",
|
|
225
|
+
onClick: () => {
|
|
226
|
+
if (uploading && onCancel) return onCancel();
|
|
227
|
+
setEntered(true);
|
|
228
|
+
setExiting(file);
|
|
229
|
+
},
|
|
230
|
+
"aria-label": uploading && onCancel ? messages.cancelUpload(file.name) : messages.removeFile(file.name),
|
|
231
|
+
className: "nx-state-layer shrink-0 rounded-full p-1 text-[var(--color-on-surface-muted)] hover:text-[var(--color-on-surface)] transition-colors",
|
|
232
|
+
children: /* @__PURE__ */ jsx(X, { size: "sm", "aria-hidden": "true" })
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
]
|
|
182
236
|
}
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
|
|
237
|
+
) }) })
|
|
238
|
+
}
|
|
239
|
+
)
|
|
186
240
|
);
|
|
187
241
|
});
|
|
188
242
|
Item.displayName = "FileUpload.Item";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import type { ToneValue } from '../Button/Button.js';
|
|
3
3
|
export declare const headingVariants: (props?: ({
|
|
4
|
-
size?: "display" | "
|
|
4
|
+
size?: "display" | "h2" | "h3" | "h1" | "h4" | "h5" | "h6" | null | undefined;
|
|
5
5
|
weight?: "bold" | "regular" | "medium" | "semibold" | "extrabold" | null | undefined;
|
|
6
6
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
7
|
type HeadingSize = 'display' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { type VariantProps } from 'class-variance-authority';
|
|
3
3
|
export declare const iconButtonVariants: (props?: ({
|
|
4
|
-
variant?: "outline" | "solid" | "
|
|
4
|
+
variant?: "outline" | "solid" | "subtle" | "ghost" | null | undefined;
|
|
5
5
|
size?: "xs" | "sm" | "md" | "lg" | "xl" | null | undefined;
|
|
6
6
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
7
|
export interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof iconButtonVariants> {
|
|
8
8
|
label: string;
|
|
9
9
|
asChild?: boolean;
|
|
10
|
+
/** Spinner replaces the icon (size kept); clicks are blocked, focus stays. */
|
|
11
|
+
loading?: boolean;
|
|
12
|
+
/** Built-in tooltip: content, or `true` to show `label`. No Provider needed. */
|
|
13
|
+
tooltip?: React.ReactNode;
|
|
10
14
|
}
|
|
11
15
|
export declare const IconButton: React.ForwardRefExoticComponent<IconButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx } from "react/jsx-runtime";
|
|
2
3
|
import * as React from "react";
|
|
3
4
|
import { Slot } from "@radix-ui/react-slot";
|
|
4
5
|
import { cva } from "class-variance-authority";
|
|
5
6
|
import { cn } from "../../utils/cn.js";
|
|
6
7
|
import { disabledContainer, disabledText, pressable } from "../../utils/control.js";
|
|
8
|
+
import { spinnerVariants } from "../Spinner/Spinner.js";
|
|
9
|
+
const LazyTooltip = React.lazy(() => import("../Tooltip/Tooltip.js").then((m) => ({ default: m.Tooltip })));
|
|
7
10
|
const iconButtonVariants = cva(
|
|
8
11
|
[pressable, "relative inline-flex items-center justify-center flex-shrink-0"],
|
|
9
12
|
{
|
|
@@ -30,18 +33,26 @@ const iconButtonVariants = cva(
|
|
|
30
33
|
}
|
|
31
34
|
);
|
|
32
35
|
const IconButton = React.forwardRef(
|
|
33
|
-
({ className, variant, size, label, asChild = false, children, ...props }, ref) => {
|
|
36
|
+
({ className, variant, size, label, asChild = false, loading, tooltip, onClick, children, ...props }, ref) => {
|
|
34
37
|
const Comp = asChild ? Slot : "button";
|
|
35
|
-
|
|
38
|
+
const button = /* @__PURE__ */ jsx(
|
|
36
39
|
Comp,
|
|
37
40
|
{
|
|
38
41
|
ref,
|
|
39
42
|
"aria-label": label,
|
|
43
|
+
"aria-busy": loading || void 0,
|
|
44
|
+
"aria-disabled": loading || props["aria-disabled"] || void 0,
|
|
40
45
|
className: cn(iconButtonVariants({ variant, size }), "nx-hit", className),
|
|
46
|
+
onClick: (e) => {
|
|
47
|
+
if (loading) return e.preventDefault();
|
|
48
|
+
onClick?.(e);
|
|
49
|
+
},
|
|
41
50
|
...props,
|
|
42
|
-
children
|
|
51
|
+
children: loading && !asChild ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: cn(spinnerVariants({ size: size === "xs" ? "xs" : "sm" }), "border-current border-t-transparent") }) : children
|
|
43
52
|
}
|
|
44
53
|
);
|
|
54
|
+
if (tooltip == null || tooltip === false) return button;
|
|
55
|
+
return /* @__PURE__ */ jsx(React.Suspense, { fallback: button, children: /* @__PURE__ */ jsx(LazyTooltip, { label: tooltip === true ? label : tooltip, children: button }) });
|
|
45
56
|
}
|
|
46
57
|
);
|
|
47
58
|
IconButton.displayName = "IconButton";
|
|
@@ -17,5 +17,11 @@ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
|
|
|
17
17
|
leadingIcon?: React.ReactNode;
|
|
18
18
|
trailingIcon?: React.ReactNode;
|
|
19
19
|
wrapperClassName?: string;
|
|
20
|
+
/** Shows a clear button while there is a value. Clearing fires `onChange` with "". */
|
|
21
|
+
clearable?: boolean;
|
|
22
|
+
/** Shows a "12/80" counter; needs `maxLength`. Announced politely near the limit. */
|
|
23
|
+
showCount?: boolean;
|
|
24
|
+
/** Spinner in the trailing slot and `aria-busy` on the input. */
|
|
25
|
+
loading?: boolean;
|
|
20
26
|
}
|
|
21
27
|
export declare const Input: React.ForwardRefExoticComponent<InputProps & React.RefAttributes<HTMLInputElement>>;
|