@nexub/react 7.4.0 → 7.5.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/Combobox/Combobox.d.ts +6 -9
- package/dist/components/Combobox/Combobox.js +30 -9
- package/dist/components/DatePicker/DatePicker.d.ts +16 -1
- package/dist/components/DatePicker/DatePicker.js +100 -26
- package/dist/components/FileUpload/FileUpload.js +86 -32
- 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/index.d.ts +1 -1
- package/dist/styles.css +1 -1
- package/package.json +6 -6
package/accessibility.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "7.
|
|
2
|
+
"version": "7.5.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, and on open popups (menus, listboxes, dialogs), on every pull request; a new violation fails the build.",
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import { Command } from 'cmdk';
|
|
3
|
+
export interface ComboboxListProps extends React.ComponentProps<typeof Command.List> {
|
|
4
|
+
/** Options are loading: shows a loading row, sets aria-busy and hides Empty. */
|
|
5
|
+
loading?: boolean;
|
|
6
|
+
}
|
|
2
7
|
export declare const Combobox: {
|
|
3
8
|
Root: React.ForwardRefExoticComponent<Omit<{
|
|
4
9
|
children?: React.ReactNode;
|
|
@@ -25,15 +30,7 @@ export declare const Combobox: {
|
|
|
25
30
|
value?: string;
|
|
26
31
|
onValueChange?: (search: string) => void;
|
|
27
32
|
} & React.RefAttributes<HTMLInputElement>, "ref"> & React.RefAttributes<HTMLInputElement>>;
|
|
28
|
-
List: React.ForwardRefExoticComponent<Omit<
|
|
29
|
-
children?: React.ReactNode;
|
|
30
|
-
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
31
|
-
ref?: React.Ref<HTMLDivElement>;
|
|
32
|
-
} & {
|
|
33
|
-
asChild?: boolean;
|
|
34
|
-
}, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
35
|
-
label?: string;
|
|
36
|
-
} & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
33
|
+
List: React.ForwardRefExoticComponent<Omit<ComboboxListProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
37
34
|
Empty: React.ForwardRefExoticComponent<Omit<{
|
|
38
35
|
children?: React.ReactNode;
|
|
39
36
|
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
@@ -1,8 +1,10 @@
|
|
|
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 { Command } from "cmdk";
|
|
5
5
|
import { cn } from "../../utils/cn.js";
|
|
6
|
+
import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
|
|
7
|
+
import { Spinner } from "../Spinner/Spinner.js";
|
|
6
8
|
function scrollWithinList(el) {
|
|
7
9
|
if (!el) return;
|
|
8
10
|
el.scrollIntoView = () => {
|
|
@@ -47,14 +49,33 @@ const Input = React.forwardRef(
|
|
|
47
49
|
);
|
|
48
50
|
Input.displayName = "Combobox.Input";
|
|
49
51
|
const List = React.forwardRef(
|
|
50
|
-
({ className, ...props }, ref) =>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
({ className, loading, children, ...props }, ref) => {
|
|
53
|
+
const messages = useNexubMessages();
|
|
54
|
+
return /* @__PURE__ */ jsxs(
|
|
55
|
+
Command.List,
|
|
56
|
+
{
|
|
57
|
+
ref,
|
|
58
|
+
"aria-busy": loading || void 0,
|
|
59
|
+
className: cn(
|
|
60
|
+
// cmdk measures its content into --cmdk-list-height; box-content keeps the padding outside it.
|
|
61
|
+
"box-content h-[var(--cmdk-list-height)] max-h-64 overflow-y-auto overflow-x-hidden p-1",
|
|
62
|
+
"transition-[height] duration-spatial-default ease-spatial-default motion-reduce:transition-none",
|
|
63
|
+
loading && "[&_[cmdk-empty]]:hidden",
|
|
64
|
+
className
|
|
65
|
+
),
|
|
66
|
+
...props,
|
|
67
|
+
children: [
|
|
68
|
+
loading && // Not Command.Loading: its role="progressbar" isn't allowed inside a listbox
|
|
69
|
+
// (axe aria-required-children). The listbox's aria-busy carries the state.
|
|
70
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-2.5 py-2 text-body-sm text-[var(--color-on-surface-muted)]", children: [
|
|
71
|
+
/* @__PURE__ */ jsx(Spinner, { size: "sm", "aria-hidden": "true" }),
|
|
72
|
+
messages.loading
|
|
73
|
+
] }),
|
|
74
|
+
children
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
}
|
|
58
79
|
);
|
|
59
80
|
List.displayName = "Combobox.List";
|
|
60
81
|
const Empty = React.forwardRef(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { type ChevronProps, type Labels } from 'react-day-picker';
|
|
2
|
+
import { type ChevronProps, type Labels, type Matcher } from 'react-day-picker';
|
|
3
3
|
import { type Locale } from 'date-fns';
|
|
4
4
|
/**
|
|
5
5
|
* A date-fns Locale wins (date-fns 'PP'); else with a NexubProvider locale,
|
|
@@ -34,6 +34,21 @@ export interface DatePickerProps {
|
|
|
34
34
|
'aria-label'?: string;
|
|
35
35
|
/** date-fns Locale for the calendar and trigger text; wins over NexubProvider's `dateLocale`. */
|
|
36
36
|
locale?: Locale;
|
|
37
|
+
/** Days that can't be picked (react-day-picker Matcher), e.g. `{ dayOfWeek: [0, 6] }`. */
|
|
38
|
+
disabledDates?: Matcher | Matcher[];
|
|
39
|
+
/** Quick picks under the calendar (Today, In a week…); choosing one sets the value and closes. */
|
|
40
|
+
presets?: {
|
|
41
|
+
label: string;
|
|
42
|
+
date: Date;
|
|
43
|
+
}[];
|
|
44
|
+
/**
|
|
45
|
+
* The trigger is a text input that also accepts a typed date in the locale's
|
|
46
|
+
* short format (date-fns `P`, e.g. 01/15/2024). Enter or blur commits; an
|
|
47
|
+
* invalid or unavailable date stays as typed, marked aria-invalid, and the
|
|
48
|
+
* value is unchanged. `id` and `aria-label` go to the input, `ref` to the
|
|
49
|
+
* calendar button.
|
|
50
|
+
*/
|
|
51
|
+
typeable?: boolean;
|
|
37
52
|
}
|
|
38
53
|
export declare const DatePicker: React.ForwardRefExoticComponent<DatePickerProps & React.RefAttributes<HTMLButtonElement>>;
|
|
39
54
|
export {};
|
|
@@ -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
|
] });
|
|
@@ -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";
|
|
@@ -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>>;
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
3
|
import * as React from "react";
|
|
4
|
+
import { X } from "@nexub/icons";
|
|
3
5
|
import { cva } from "class-variance-authority";
|
|
4
6
|
import { cn } from "../../utils/cn.js";
|
|
5
7
|
import { field, fieldDisabledWithin, fieldFocusWithin, fieldInvalid } from "../../utils/control.js";
|
|
8
|
+
import { useMergedRefs } from "../../hooks/useMergedRefs.js";
|
|
9
|
+
import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
|
|
10
|
+
import { IconButton } from "../IconButton/IconButton.js";
|
|
11
|
+
import { Spinner } from "../Spinner/Spinner.js";
|
|
6
12
|
const inputVariants = cva(
|
|
7
13
|
[
|
|
8
14
|
// The wrapper owns ALL focus styling (border + ring). Strip every possible
|
|
@@ -67,6 +73,7 @@ const addonVariants = cva(
|
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
75
|
);
|
|
76
|
+
const nearLimit = (length, max) => length >= max - Math.max(1, Math.ceil(max * 0.1));
|
|
70
77
|
const Input = React.forwardRef(
|
|
71
78
|
({
|
|
72
79
|
className,
|
|
@@ -78,26 +85,61 @@ const Input = React.forwardRef(
|
|
|
78
85
|
trailingIcon,
|
|
79
86
|
wrapperClassName,
|
|
80
87
|
disabled,
|
|
88
|
+
clearable,
|
|
89
|
+
showCount,
|
|
90
|
+
loading,
|
|
81
91
|
...props
|
|
82
92
|
}, ref) => {
|
|
83
93
|
const hasAddon = Boolean(leadingAddon ?? trailingAddon);
|
|
94
|
+
const messages = useNexubMessages();
|
|
95
|
+
const innerRef = React.useRef(null);
|
|
96
|
+
const mergedRef = useMergedRefs(ref, innerRef);
|
|
97
|
+
const countId = React.useId();
|
|
98
|
+
const [uncontrolledLength, setLength] = React.useState(String(props.defaultValue ?? "").length);
|
|
99
|
+
const length = props.value !== void 0 && props.value !== null ? String(props.value).length : uncontrolledLength;
|
|
100
|
+
const max = props.maxLength;
|
|
101
|
+
const counting = Boolean(showCount && max !== void 0 && max >= 0);
|
|
102
|
+
const handleChange = (e) => {
|
|
103
|
+
setLength(e.target.value.length);
|
|
104
|
+
props.onChange?.(e);
|
|
105
|
+
};
|
|
106
|
+
const clear = () => {
|
|
107
|
+
const el = innerRef.current;
|
|
108
|
+
if (!el) return;
|
|
109
|
+
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set?.call(el, "");
|
|
110
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
111
|
+
el.focus();
|
|
112
|
+
};
|
|
84
113
|
const inputEl = /* @__PURE__ */ jsx(
|
|
85
114
|
"input",
|
|
86
115
|
{
|
|
87
|
-
ref,
|
|
88
116
|
className: cn(inputVariants({ size }), className),
|
|
89
117
|
"aria-invalid": invalid ?? void 0,
|
|
118
|
+
"aria-busy": loading || void 0,
|
|
90
119
|
disabled,
|
|
91
|
-
...props
|
|
120
|
+
...props,
|
|
121
|
+
"aria-describedby": [props["aria-describedby"], counting && countId].filter(Boolean).join(" ") || void 0,
|
|
122
|
+
onChange: handleChange,
|
|
123
|
+
ref: mergedRef
|
|
92
124
|
}
|
|
93
125
|
);
|
|
126
|
+
const showClear = clearable && length > 0 && !disabled && !props.readOnly;
|
|
127
|
+
const trailing = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
128
|
+
counting && /* @__PURE__ */ jsxs("span", { className: "shrink-0 text-label-sm tabular-nums text-[var(--color-on-surface-muted)]", children: [
|
|
129
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: `${length}/${max}` }),
|
|
130
|
+
/* @__PURE__ */ jsx("span", { id: countId, className: "sr-only", "aria-live": nearLimit(length, max) ? "polite" : "off", children: messages.characterCount(length, max) })
|
|
131
|
+
] }),
|
|
132
|
+
loading && /* @__PURE__ */ jsx(Spinner, { size: "sm", className: "shrink-0" }),
|
|
133
|
+
showClear && /* @__PURE__ */ jsx(IconButton, { size: "xs", label: messages.clear, onClick: clear, className: "-me-1.5", children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4", "aria-hidden": "true" }) }),
|
|
134
|
+
trailingIcon && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[var(--color-on-surface-variant)]", children: trailingIcon })
|
|
135
|
+
] });
|
|
94
136
|
if (hasAddon) {
|
|
95
137
|
return /* @__PURE__ */ jsxs("div", { className: cn("flex w-full overflow-hidden", field, fieldFocusWithin, invalid && fieldInvalid), children: [
|
|
96
138
|
leadingAddon && /* @__PURE__ */ jsx("span", { className: cn(addonVariants({ side: "leading" })), children: leadingAddon }),
|
|
97
139
|
/* @__PURE__ */ jsxs("div", { className: cn(inputWrapperVariants({ size, invalid }), "rounded-none border-0 shadow-none focus-within:shadow-none", wrapperClassName), children: [
|
|
98
140
|
leadingIcon && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[var(--color-on-surface-variant)]", children: leadingIcon }),
|
|
99
141
|
inputEl,
|
|
100
|
-
|
|
142
|
+
trailing
|
|
101
143
|
] }),
|
|
102
144
|
trailingAddon && /* @__PURE__ */ jsx("span", { className: cn(addonVariants({ side: "trailing" })), children: trailingAddon })
|
|
103
145
|
] });
|
|
@@ -105,7 +147,7 @@ const Input = React.forwardRef(
|
|
|
105
147
|
return /* @__PURE__ */ jsxs("div", { className: cn(inputWrapperVariants({ size, invalid }), wrapperClassName), children: [
|
|
106
148
|
leadingIcon && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[var(--color-on-surface-variant)]", children: leadingIcon }),
|
|
107
149
|
inputEl,
|
|
108
|
-
|
|
150
|
+
trailing
|
|
109
151
|
] });
|
|
110
152
|
}
|
|
111
153
|
);
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import * as RadixSelect from '@radix-ui/react-select';
|
|
3
|
+
export interface SelectRootProps extends RadixSelect.SelectProps {
|
|
4
|
+
/** Shows a clear button on the Trigger while there is a value; clearing calls `onValueChange('')` and shows the placeholder. */
|
|
5
|
+
clearable?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface SelectContentProps extends RadixSelect.SelectContentProps {
|
|
8
|
+
/** A filter field at the top; items whose text (or `textValue`) doesn't match are hidden. Typing on an item moves to the field. */
|
|
9
|
+
searchable?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface SelectItemProps extends RadixSelect.SelectItemProps {
|
|
12
|
+
/** A secondary line under the label: the option's accessible description, not part of its name. */
|
|
13
|
+
description?: React.ReactNode;
|
|
14
|
+
}
|
|
3
15
|
export declare const Select: {
|
|
4
|
-
Root:
|
|
16
|
+
Root: {
|
|
17
|
+
({ clearable, value, defaultValue, onValueChange, onOpenChange, ...props }: SelectRootProps): React.JSX.Element;
|
|
18
|
+
displayName: string;
|
|
19
|
+
};
|
|
5
20
|
Group: React.ForwardRefExoticComponent<RadixSelect.SelectGroupProps & React.RefAttributes<HTMLDivElement>>;
|
|
6
21
|
Value: React.ForwardRefExoticComponent<RadixSelect.SelectValueProps & React.RefAttributes<HTMLSpanElement>>;
|
|
7
22
|
Trigger: React.ForwardRefExoticComponent<RadixSelect.SelectTriggerProps & {
|
|
@@ -9,8 +24,8 @@ export declare const Select: {
|
|
|
9
24
|
} & React.RefAttributes<HTMLButtonElement>>;
|
|
10
25
|
ScrollUpButton: React.ForwardRefExoticComponent<RadixSelect.SelectScrollUpButtonProps & React.RefAttributes<HTMLDivElement>>;
|
|
11
26
|
ScrollDownButton: React.ForwardRefExoticComponent<RadixSelect.SelectScrollDownButtonProps & React.RefAttributes<HTMLDivElement>>;
|
|
12
|
-
Content: React.ForwardRefExoticComponent<
|
|
27
|
+
Content: React.ForwardRefExoticComponent<SelectContentProps & React.RefAttributes<HTMLDivElement>>;
|
|
13
28
|
Label: React.ForwardRefExoticComponent<RadixSelect.SelectLabelProps & React.RefAttributes<HTMLDivElement>>;
|
|
14
|
-
Item: React.ForwardRefExoticComponent<
|
|
29
|
+
Item: React.ForwardRefExoticComponent<SelectItemProps & React.RefAttributes<HTMLDivElement>>;
|
|
15
30
|
Separator: React.ForwardRefExoticComponent<RadixSelect.SelectSeparatorProps & React.RefAttributes<HTMLDivElement>>;
|
|
16
31
|
};
|