@tensor_1/react 2.30.0 → 2.32.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/README.md +430 -0
- package/dist/attachment-labels.d.ts +16 -0
- package/dist/attachment-labels.js +9 -0
- package/dist/calendar-model.d.ts +141 -1
- package/dist/calendar-model.js +200 -11
- package/dist/chart-preset.cjs +127 -7
- package/dist/chart-preset.d.cts +2 -2
- package/dist/chart-preset.d.ts +1 -0
- package/dist/chart-preset.js +6 -1
- package/dist/chart-theme-Cl0IrkKQ.d.cts +112 -0
- package/dist/chart-theme.d.ts +51 -0
- package/dist/chart-theme.js +99 -0
- package/dist/chart.cjs +127 -7
- package/dist/chart.d.cts +2 -2
- package/dist/chart.d.ts +1 -0
- package/dist/chart.js +6 -2
- package/dist/components/Alert.d.ts +17 -1
- package/dist/components/Alert.js +9 -4
- package/dist/components/AlertDismiss.d.ts +14 -0
- package/dist/components/AlertDismiss.js +11 -0
- package/dist/components/Attachment.d.ts +58 -0
- package/dist/components/Attachment.js +69 -0
- package/dist/components/AttachmentParts.d.ts +24 -0
- package/dist/components/AttachmentParts.js +20 -0
- package/dist/components/Chart.d.ts +1 -0
- package/dist/components/Chart.js +6 -1
- package/dist/components/ChartCore.d.ts +6 -1
- package/dist/components/ChartCore.js +23 -5
- package/dist/components/Chip.d.ts +17 -2
- package/dist/components/Chip.js +35 -11
- package/dist/components/ChipRemove.d.ts +17 -0
- package/dist/components/ChipRemove.js +13 -0
- package/dist/components/CollapsibleParts.d.ts +29 -0
- package/dist/components/CollapsibleParts.js +20 -0
- package/dist/components/CollapsibleSection.d.ts +30 -3
- package/dist/components/CollapsibleSection.js +7 -11
- package/dist/components/CollapsibleSectionState.d.ts +1 -1
- package/dist/components/CollapsibleSectionState.js +54 -27
- package/dist/components/DashboardGrid.d.ts +60 -0
- package/dist/components/DashboardGrid.js +40 -0
- package/dist/components/DateRangePicker.d.ts +7 -1
- package/dist/components/DateRangePicker.js +5 -1
- package/dist/components/DescriptionList.d.ts +12 -1
- package/dist/components/DescriptionList.js +5 -5
- package/dist/components/Dialogs.d.ts +24 -3
- package/dist/components/Dialogs.js +74 -16
- package/dist/components/EntityPicker.d.ts +56 -0
- package/dist/components/EntityPicker.js +168 -0
- package/dist/components/FileIcon.d.ts +9 -0
- package/dist/components/FileIcon.js +17 -0
- package/dist/components/FileUpload.d.ts +95 -0
- package/dist/components/FileUpload.js +145 -0
- package/dist/components/FilterEditor.d.ts +55 -0
- package/dist/components/FilterEditor.js +186 -0
- package/dist/components/Heartbeat.d.ts +31 -0
- package/dist/components/Heartbeat.js +28 -0
- package/dist/components/Lightbox.d.ts +45 -0
- package/dist/components/Lightbox.js +102 -0
- package/dist/components/Menu.d.ts +53 -5
- package/dist/components/Menu.js +104 -4
- package/dist/components/Meter.d.ts +7 -3
- package/dist/components/Meter.js +2 -1
- package/dist/components/Popover.d.ts +21 -2
- package/dist/components/Popover.js +94 -17
- package/dist/components/Prose.d.ts +6 -2
- package/dist/components/Prose.js +1 -1
- package/dist/components/RuleBuilder.d.ts +52 -4
- package/dist/components/RuleBuilder.js +109 -24
- package/dist/components/SchedulingCalendar.d.ts +142 -11
- package/dist/components/SchedulingCalendar.js +234 -57
- package/dist/components/SchedulingTimeline.d.ts +1 -0
- package/dist/components/SearchSuggest.d.ts +6 -2
- package/dist/components/SearchSuggest.js +4 -0
- package/dist/components/Segmented.d.ts +17 -3
- package/dist/components/Segmented.js +59 -5
- package/dist/components/Select.d.ts +66 -2
- package/dist/components/Select.js +800 -10
- package/dist/components/Spinner.d.ts +2 -1
- package/dist/components/Spinner.js +1 -1
- package/dist/components/StatusPill.d.ts +8 -2
- package/dist/components/StatusPill.js +2 -2
- package/dist/components/Steps.d.ts +28 -4
- package/dist/components/Steps.js +46 -16
- package/dist/components/TableControls.js +4 -3
- package/dist/components/Tabs.d.ts +7 -2
- package/dist/components/Tabs.js +14 -1
- package/dist/components/TextDiff.d.ts +49 -0
- package/dist/components/TextDiff.js +124 -0
- package/dist/{emoji-image-6mPZ15Zy.d.cts → emoji-image-DjCKpdwE.d.cts} +1 -1
- package/dist/emoji.cjs +169 -27
- package/dist/emoji.d.cts +3 -3
- package/dist/file-model.d.ts +56 -0
- package/dist/file-model.js +52 -0
- package/dist/filter-model.d.ts +112 -0
- package/dist/filter-model.js +103 -0
- package/dist/hooks/useAnchoredPosition.d.ts +24 -1
- package/dist/hooks/useAnchoredPosition.js +58 -8
- package/dist/hooks/useVirtualRows.d.ts +17 -2
- package/dist/hooks/useVirtualRows.js +6 -5
- package/dist/icons/Icon.d.ts +14 -14
- package/dist/index.cjs +4938 -1631
- package/dist/index.d.cts +1622 -178
- package/dist/index.d.ts +20 -6
- package/dist/index.js +88 -4
- package/dist/labels-context.d.ts +28 -0
- package/dist/{overlay-position-Cjv5E7mn.d.cts → overlay-position-2Afke51l.d.cts} +20 -0
- package/dist/overlay-position.d.ts +20 -0
- package/dist/overlay-position.js +17 -2
- package/dist/rich-text.cjs +169 -27
- package/dist/rich-text.d.cts +3 -3
- package/dist/rule-builder-model.d.ts +34 -2
- package/dist/rule-builder-model.js +48 -12
- package/dist/scheduling-calendar-model.d.ts +66 -0
- package/dist/scheduling-calendar-model.js +82 -0
- package/dist/select-model.d.ts +80 -0
- package/dist/select-model.js +109 -0
- package/dist/table-engine.d.ts +7 -0
- package/dist/table-engine.js +2 -1
- package/dist/text-diff-model.d.ts +71 -0
- package/dist/text-diff-model.js +261 -0
- package/package.json +1 -1
- package/dist/ChartCore-DzWe10bN.d.cts +0 -59
|
@@ -16,6 +16,7 @@ const DEFAULT_DATE_RANGE_PICKER_LABELS = {
|
|
|
16
16
|
rangeEnd: "end of selected range",
|
|
17
17
|
inRange: "in selected range"
|
|
18
18
|
};
|
|
19
|
+
const localDateKey = (now = /* @__PURE__ */ new Date()) => `${now.getFullYear().toString().padStart(4, "0")}-${(now.getMonth() + 1).toString().padStart(2, "0")}-${now.getDate().toString().padStart(2, "0")}`;
|
|
19
20
|
const monthLabel = (date, locale) => new Intl.DateTimeFormat(locale, { month: "long", year: "numeric", timeZone: "UTC" }).format(/* @__PURE__ */ new Date(`${date}T00:00:00Z`));
|
|
20
21
|
const dateLabel = (date, locale) => new Intl.DateTimeFormat(locale, { dateStyle: "full", timeZone: "UTC" }).format(/* @__PURE__ */ new Date(`${date}T00:00:00Z`));
|
|
21
22
|
const weekdayNames = (locale) => {
|
|
@@ -37,7 +38,8 @@ function DateRangePicker({
|
|
|
37
38
|
invalidMessage = "The selected date range is invalid.",
|
|
38
39
|
disabled,
|
|
39
40
|
className,
|
|
40
|
-
labels: labelOverrides
|
|
41
|
+
labels: labelOverrides,
|
|
42
|
+
today
|
|
41
43
|
}) {
|
|
42
44
|
const id = useId();
|
|
43
45
|
const uixLabels = useUixLabels();
|
|
@@ -53,6 +55,7 @@ function DateRangePicker({
|
|
|
53
55
|
const month = startOfMonth(addCalendarMonths(visibleMonth, index));
|
|
54
56
|
return { month, days: buildMonthGrid(month).filter((day) => day.inMonth) };
|
|
55
57
|
}), [visibleMonth, count]);
|
|
58
|
+
const todayDate = today === void 0 ? localDateKey() : today;
|
|
56
59
|
const rangeInvalid = invalid || !!value.start && !!value.end && value.start > value.end;
|
|
57
60
|
useEffect(() => {
|
|
58
61
|
const first = calendars[0]?.days[0]?.date;
|
|
@@ -123,6 +126,7 @@ function DateRangePicker({
|
|
|
123
126
|
"data-in-range": isDateInRange(day.date, value) || void 0,
|
|
124
127
|
"aria-pressed": edge != null || isDateInRange(day.date, value),
|
|
125
128
|
"aria-label": `${dateLabel(day.date, locale)}${edge ? `, ${edge === "start" ? labels.rangeStart : labels.rangeEnd}` : isDateInRange(day.date, value) ? `, ${labels.inRange}` : ""}`,
|
|
129
|
+
"aria-current": day.date === todayDate ? "date" : void 0,
|
|
126
130
|
tabIndex: activeDate === day.date ? 0 : -1,
|
|
127
131
|
disabled: unavailable,
|
|
128
132
|
onFocus: () => setActiveDate(day.date),
|
|
@@ -26,6 +26,10 @@ interface DescriptionListItem {
|
|
|
26
26
|
id?: string;
|
|
27
27
|
term: ReactNode;
|
|
28
28
|
description: ReactNode;
|
|
29
|
+
/** Attributes for this row's `<dt>` (an `id` to anchor to, `data-*`). */
|
|
30
|
+
termProps?: HTMLAttributes<HTMLElement>;
|
|
31
|
+
/** Attributes for this row's `<dd>`. */
|
|
32
|
+
descriptionProps?: HTMLAttributes<HTMLElement>;
|
|
29
33
|
}
|
|
30
34
|
interface DescriptionListProps extends HTMLAttributes<HTMLDListElement> {
|
|
31
35
|
items?: DescriptionListItem[];
|
|
@@ -35,12 +39,19 @@ declare function DescriptionList({ items, children, className, ...props }: Descr
|
|
|
35
39
|
interface DescriptionItemProps {
|
|
36
40
|
term: ReactNode;
|
|
37
41
|
children?: ReactNode;
|
|
42
|
+
/**
|
|
43
|
+
* Attributes for the `<dt>`: an `id` a deep link or `aria-describedby` points at, `data-*`,
|
|
44
|
+
* a `className` (HAR-1628). The pair has no wrapper element, so this is where they go.
|
|
45
|
+
*/
|
|
46
|
+
termProps?: HTMLAttributes<HTMLElement>;
|
|
47
|
+
/** Attributes for the `<dd>`. */
|
|
48
|
+
descriptionProps?: HTMLAttributes<HTMLElement>;
|
|
38
49
|
}
|
|
39
50
|
/**
|
|
40
51
|
* One hand-authored row. Renders a `<dt>`/`<dd>` fragment so the pair lands as
|
|
41
52
|
* direct grid children of the enclosing `<DescriptionList>` (never wrap it in
|
|
42
53
|
* an element — that breaks the two-column alignment).
|
|
43
54
|
*/
|
|
44
|
-
declare function DescriptionItem({ term, children }: DescriptionItemProps): react.JSX.Element;
|
|
55
|
+
declare function DescriptionItem({ term, children, termProps, descriptionProps }: DescriptionItemProps): react.JSX.Element;
|
|
45
56
|
|
|
46
57
|
export { DescriptionItem, type DescriptionItemProps, DescriptionList, type DescriptionListItem, type DescriptionListProps };
|
|
@@ -4,16 +4,16 @@ import { cx } from "../cx.js";
|
|
|
4
4
|
function DescriptionList({ items, children, className, ...props }) {
|
|
5
5
|
return /* @__PURE__ */ jsxs("dl", { className: cx("uix-dl", className), ...props, children: [
|
|
6
6
|
items?.map((item, i) => /* @__PURE__ */ jsxs(Fragment2, { children: [
|
|
7
|
-
/* @__PURE__ */ jsx("dt", { children: item.term }),
|
|
8
|
-
/* @__PURE__ */ jsx("dd", { children: item.description })
|
|
7
|
+
/* @__PURE__ */ jsx("dt", { ...item.termProps, children: item.term }),
|
|
8
|
+
/* @__PURE__ */ jsx("dd", { ...item.descriptionProps, children: item.description })
|
|
9
9
|
] }, item.id ?? i)),
|
|
10
10
|
children
|
|
11
11
|
] });
|
|
12
12
|
}
|
|
13
|
-
function DescriptionItem({ term, children }) {
|
|
13
|
+
function DescriptionItem({ term, children, termProps, descriptionProps }) {
|
|
14
14
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
15
|
-
/* @__PURE__ */ jsx("dt", { children: term }),
|
|
16
|
-
/* @__PURE__ */ jsx("dd", { children })
|
|
15
|
+
/* @__PURE__ */ jsx("dt", { ...termProps, children: term }),
|
|
16
|
+
/* @__PURE__ */ jsx("dd", { ...descriptionProps, children })
|
|
17
17
|
] });
|
|
18
18
|
}
|
|
19
19
|
export {
|
|
@@ -8,7 +8,8 @@ interface ConfirmDialogProps {
|
|
|
8
8
|
description?: ReactNode;
|
|
9
9
|
confirmLabel: ReactNode;
|
|
10
10
|
cancelLabel: ReactNode;
|
|
11
|
-
|
|
11
|
+
/** Accessible name of the close button. Default: `UixLabelsProvider` `modal.close`, then "Close dialog". */
|
|
12
|
+
closeLabel?: string;
|
|
12
13
|
onConfirm: () => void | Promise<void>;
|
|
13
14
|
onCancel: () => void;
|
|
14
15
|
pending?: boolean;
|
|
@@ -75,12 +76,32 @@ interface PromptDialogProps {
|
|
|
75
76
|
placeholder?: string;
|
|
76
77
|
submitLabel: ReactNode;
|
|
77
78
|
cancelLabel: ReactNode;
|
|
78
|
-
|
|
79
|
+
/** Accessible name of the close button. Default: `UixLabelsProvider` `modal.close`, then "Close dialog". */
|
|
80
|
+
closeLabel?: string;
|
|
79
81
|
onSubmit: (value: string) => void | Promise<void>;
|
|
80
82
|
onCancel: () => void;
|
|
81
83
|
validate?: (value: string) => ReactNode | undefined;
|
|
82
84
|
pending?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Ask for a longer answer in a textarea (a reason, a note); a number sets its rows, `true`
|
|
87
|
+
* is 4. Enter adds a line there, so the form is sent with the submit button or Ctrl/⌘+Enter
|
|
88
|
+
* (HAR-1614).
|
|
89
|
+
*/
|
|
90
|
+
multiline?: boolean | number;
|
|
91
|
+
/**
|
|
92
|
+
* The answer confirms something that cannot be taken back (a forced import): the submit
|
|
93
|
+
* button is the danger button and the dialog is an `alertdialog`.
|
|
94
|
+
*/
|
|
95
|
+
destructive?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Why the last submit failed, from the server. Shown under the field (`role="alert"`) and
|
|
98
|
+
* tied to it; the dialog stays open. A message from `validate` takes its place while the
|
|
99
|
+
* value is invalid.
|
|
100
|
+
*/
|
|
101
|
+
error?: ReactNode;
|
|
102
|
+
/** Default: `'alertdialog'` when `destructive`, else `'dialog'`. */
|
|
103
|
+
role?: 'dialog' | 'alertdialog';
|
|
83
104
|
}
|
|
84
|
-
declare function PromptDialog({ open, title, description, inputLabel, defaultValue, placeholder, submitLabel, cancelLabel, closeLabel, onSubmit, onCancel, validate, pending }: PromptDialogProps): react.JSX.Element;
|
|
105
|
+
declare function PromptDialog({ open, title, description, inputLabel, defaultValue, placeholder, submitLabel, cancelLabel, closeLabel, onSubmit, onCancel, validate, pending, multiline, destructive, error: submitError, role, }: PromptDialogProps): react.JSX.Element;
|
|
85
106
|
|
|
86
107
|
export { ConfirmDialog, type ConfirmDialogProps, Popconfirm, type PopconfirmProps, PromptDialog, type PromptDialogProps };
|
|
@@ -4,6 +4,7 @@ import { useEffect, useId, useRef, useState } from "react";
|
|
|
4
4
|
import { Button } from "./Button.js";
|
|
5
5
|
import { Input } from "./Input.js";
|
|
6
6
|
import { Modal } from "./Modal.js";
|
|
7
|
+
import { Textarea } from "./Textarea.js";
|
|
7
8
|
import { Popover } from "./Popover.js";
|
|
8
9
|
import { fillLabel } from "../fill-label.js";
|
|
9
10
|
import { resolveAnchor } from "../hooks/useAnchoredPosition.js";
|
|
@@ -181,33 +182,90 @@ function Popconfirm({
|
|
|
181
182
|
}
|
|
182
183
|
);
|
|
183
184
|
}
|
|
184
|
-
function PromptDialog({
|
|
185
|
+
function PromptDialog({
|
|
186
|
+
open,
|
|
187
|
+
title,
|
|
188
|
+
description,
|
|
189
|
+
inputLabel,
|
|
190
|
+
defaultValue = "",
|
|
191
|
+
placeholder,
|
|
192
|
+
submitLabel,
|
|
193
|
+
cancelLabel,
|
|
194
|
+
closeLabel,
|
|
195
|
+
onSubmit,
|
|
196
|
+
onCancel,
|
|
197
|
+
validate,
|
|
198
|
+
pending,
|
|
199
|
+
multiline = false,
|
|
200
|
+
destructive = false,
|
|
201
|
+
error: submitError,
|
|
202
|
+
role
|
|
203
|
+
}) {
|
|
185
204
|
const inputId = useId();
|
|
186
205
|
const errorId = `${inputId}-error`;
|
|
206
|
+
const descriptionId = `${inputId}-description`;
|
|
207
|
+
const formRef = useRef(null);
|
|
208
|
+
const fieldRef = useRef(null);
|
|
187
209
|
const [value, setValue] = useState(defaultValue);
|
|
188
|
-
const [
|
|
210
|
+
const [issue, setIssue] = useState();
|
|
211
|
+
const error = issue ?? submitError;
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
if (open) fieldRef.current?.focus();
|
|
214
|
+
}, [open]);
|
|
189
215
|
useEffect(() => {
|
|
190
216
|
if (open) {
|
|
191
217
|
setValue(defaultValue);
|
|
192
|
-
|
|
218
|
+
setIssue(void 0);
|
|
193
219
|
}
|
|
194
220
|
}, [defaultValue, open]);
|
|
195
221
|
const handleSubmit = (event) => {
|
|
196
222
|
event.preventDefault();
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
if (
|
|
223
|
+
const found = validate?.(value);
|
|
224
|
+
setIssue(found);
|
|
225
|
+
if (found == null) void onSubmit(value);
|
|
200
226
|
};
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
error != null
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
227
|
+
const field = {
|
|
228
|
+
id: inputId,
|
|
229
|
+
value,
|
|
230
|
+
placeholder,
|
|
231
|
+
"aria-describedby": error != null ? errorId : void 0,
|
|
232
|
+
invalid: error != null,
|
|
233
|
+
ref: fieldRef
|
|
234
|
+
};
|
|
235
|
+
return /* @__PURE__ */ jsx(
|
|
236
|
+
Modal,
|
|
237
|
+
{
|
|
238
|
+
open,
|
|
239
|
+
onClose: onCancel,
|
|
240
|
+
title,
|
|
241
|
+
closeLabel,
|
|
242
|
+
role: role ?? (destructive ? "alertdialog" : "dialog"),
|
|
243
|
+
"aria-describedby": description != null ? descriptionId : void 0,
|
|
244
|
+
children: /* @__PURE__ */ jsxs("form", { ref: formRef, className: "uix-prompt", onSubmit: handleSubmit, children: [
|
|
245
|
+
description != null && /* @__PURE__ */ jsx("div", { id: descriptionId, children: description }),
|
|
246
|
+
/* @__PURE__ */ jsx("label", { className: "uix-field__label", htmlFor: inputId, children: inputLabel }),
|
|
247
|
+
multiline ? /* @__PURE__ */ jsx(
|
|
248
|
+
Textarea,
|
|
249
|
+
{
|
|
250
|
+
...field,
|
|
251
|
+
rows: typeof multiline === "number" ? multiline : 4,
|
|
252
|
+
onChange: (event) => setValue(event.currentTarget.value),
|
|
253
|
+
onKeyDown: (event) => {
|
|
254
|
+
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
|
255
|
+
event.preventDefault();
|
|
256
|
+
formRef.current?.requestSubmit();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
) : /* @__PURE__ */ jsx(Input, { ...field, onChange: (event) => setValue(event.currentTarget.value) }),
|
|
261
|
+
error != null && /* @__PURE__ */ jsx("div", { id: errorId, className: "uix-field__error", role: "alert", children: error }),
|
|
262
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-dialog__footer uix-prompt__actions", children: [
|
|
263
|
+
/* @__PURE__ */ jsx(Button, { type: "button", onClick: onCancel, disabled: pending, children: cancelLabel }),
|
|
264
|
+
/* @__PURE__ */ jsx(Button, { type: "submit", variant: destructive ? "danger" : "primary", loading: pending, children: submitLabel })
|
|
265
|
+
] })
|
|
266
|
+
] })
|
|
267
|
+
}
|
|
268
|
+
);
|
|
211
269
|
}
|
|
212
270
|
export {
|
|
213
271
|
ConfirmDialog,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { SearchSuggestOption } from './SearchSuggest.js';
|
|
4
|
+
|
|
5
|
+
interface EntityPickerLabels {
|
|
6
|
+
/** Accessible name of the search field while picking. */
|
|
7
|
+
search: string;
|
|
8
|
+
loading: string;
|
|
9
|
+
empty: string;
|
|
10
|
+
error: string;
|
|
11
|
+
retry: string;
|
|
12
|
+
/** Clears the chosen record. `{title}`. */
|
|
13
|
+
clear: string;
|
|
14
|
+
/** Names the chosen-record button that reopens the search. `{label}`, `{title}`. */
|
|
15
|
+
change: string;
|
|
16
|
+
/** Shown in the field when nothing is chosen. */
|
|
17
|
+
none: string;
|
|
18
|
+
/** Polite count after a search. `{count}`. */
|
|
19
|
+
results: string;
|
|
20
|
+
}
|
|
21
|
+
declare const DEFAULT_ENTITY_PICKER_LABELS: EntityPickerLabels;
|
|
22
|
+
interface EntityPickerProps {
|
|
23
|
+
/** The chosen record, or null. */
|
|
24
|
+
value: SearchSuggestOption | null;
|
|
25
|
+
onValueChange: (next: SearchSuggestOption | null) => void;
|
|
26
|
+
/** Finds records for the query; a rejection shows the error state with a Retry row. */
|
|
27
|
+
onSearch: (query: string) => Promise<readonly SearchSuggestOption[]>;
|
|
28
|
+
/** The field's name (the visible label usually comes from a surrounding `Field`). */
|
|
29
|
+
label: string;
|
|
30
|
+
/** Id of the control, for `Field htmlFor`/`label for`. */
|
|
31
|
+
id?: string;
|
|
32
|
+
/** Hidden form value: the chosen record's id. */
|
|
33
|
+
name?: string;
|
|
34
|
+
placeholder?: string;
|
|
35
|
+
/** Search only from this many characters. Default 0 (an empty query lists suggestions). */
|
|
36
|
+
minQueryLength?: number;
|
|
37
|
+
/** Debounce before searching, in ms. Default 250. */
|
|
38
|
+
delay?: number;
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
invalid?: boolean;
|
|
41
|
+
/** Ids of hint/error text (from `Field`). */
|
|
42
|
+
'aria-describedby'?: string;
|
|
43
|
+
/** How the chosen record shows in the field. Default: title, then the last breadcrumb. */
|
|
44
|
+
renderValue?: (option: SearchSuggestOption) => ReactNode;
|
|
45
|
+
labels?: Partial<EntityPickerLabels>;
|
|
46
|
+
className?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A form field that holds one record found by an async search (HAR-1366; TENSOR B28/C5
|
|
50
|
+
* `entity-picker.tsx`, MOTUS B-P10): the chosen record shows as the field's value with a
|
|
51
|
+
* clear ×; choosing again or typing opens a `SearchSuggest` list with loading, empty and error
|
|
52
|
+
* states, and Retry is a list row, so the keyboard reaches it. Stale results are ignored.
|
|
53
|
+
*/
|
|
54
|
+
declare function EntityPicker({ value, onValueChange, onSearch, label, id, name, placeholder, minQueryLength, delay, disabled, invalid, 'aria-describedby': describedBy, renderValue, labels: labelOverrides, className, }: EntityPickerProps): react.JSX.Element;
|
|
55
|
+
|
|
56
|
+
export { DEFAULT_ENTITY_PICKER_LABELS, EntityPicker, type EntityPickerLabels, type EntityPickerProps };
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { cx } from "../cx.js";
|
|
5
|
+
import { fillLabel } from "../fill-label.js";
|
|
6
|
+
import { SearchSuggest } from "./SearchSuggest.js";
|
|
7
|
+
const DEFAULT_ENTITY_PICKER_LABELS = {
|
|
8
|
+
search: "Search",
|
|
9
|
+
loading: "Searching\u2026",
|
|
10
|
+
empty: "No matches.",
|
|
11
|
+
error: "Search failed.",
|
|
12
|
+
retry: "Try again",
|
|
13
|
+
clear: "Clear {title}",
|
|
14
|
+
change: "{label}: {title}. Change",
|
|
15
|
+
none: "Choose\u2026",
|
|
16
|
+
results: "{count} results"
|
|
17
|
+
};
|
|
18
|
+
function EntityPicker({
|
|
19
|
+
value,
|
|
20
|
+
onValueChange,
|
|
21
|
+
onSearch,
|
|
22
|
+
label,
|
|
23
|
+
id,
|
|
24
|
+
name,
|
|
25
|
+
placeholder,
|
|
26
|
+
minQueryLength = 0,
|
|
27
|
+
delay = 250,
|
|
28
|
+
disabled,
|
|
29
|
+
invalid,
|
|
30
|
+
"aria-describedby": describedBy,
|
|
31
|
+
renderValue,
|
|
32
|
+
labels: labelOverrides,
|
|
33
|
+
className
|
|
34
|
+
}) {
|
|
35
|
+
const labels = { ...DEFAULT_ENTITY_PICKER_LABELS, ...labelOverrides };
|
|
36
|
+
const [editing, setEditing] = useState(value == null);
|
|
37
|
+
const [query, setQuery] = useState("");
|
|
38
|
+
const [results, setResults] = useState([]);
|
|
39
|
+
const [status, setStatus] = useState("idle");
|
|
40
|
+
const [attempt, setAttempt] = useState(0);
|
|
41
|
+
const latest = useRef(0);
|
|
42
|
+
const inputRef = useRef(null);
|
|
43
|
+
const valueButtonRef = useRef(null);
|
|
44
|
+
const returnFocus = useRef(false);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (value) setEditing(false);
|
|
47
|
+
else setEditing(true);
|
|
48
|
+
}, [value?.id]);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!editing || disabled) return;
|
|
51
|
+
const q = query.trim();
|
|
52
|
+
if (q.length < minQueryLength) {
|
|
53
|
+
setStatus("idle");
|
|
54
|
+
setResults([]);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const run = ++latest.current;
|
|
58
|
+
const timer = window.setTimeout(() => {
|
|
59
|
+
setStatus("loading");
|
|
60
|
+
onSearch(q).then(
|
|
61
|
+
(found) => {
|
|
62
|
+
if (run === latest.current) {
|
|
63
|
+
setResults(found);
|
|
64
|
+
setStatus("done");
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
() => {
|
|
68
|
+
if (run === latest.current) {
|
|
69
|
+
setResults([]);
|
|
70
|
+
setStatus("error");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
}, delay);
|
|
75
|
+
return () => window.clearTimeout(timer);
|
|
76
|
+
}, [editing, query, attempt, minQueryLength, delay, disabled]);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!returnFocus.current) return;
|
|
79
|
+
returnFocus.current = false;
|
|
80
|
+
(editing ? inputRef.current : valueButtonRef.current)?.focus();
|
|
81
|
+
}, [editing]);
|
|
82
|
+
const startEditing = () => {
|
|
83
|
+
setQuery("");
|
|
84
|
+
returnFocus.current = true;
|
|
85
|
+
setEditing(true);
|
|
86
|
+
};
|
|
87
|
+
const choose = (option) => {
|
|
88
|
+
onValueChange(option);
|
|
89
|
+
returnFocus.current = true;
|
|
90
|
+
setEditing(false);
|
|
91
|
+
};
|
|
92
|
+
const clear = () => {
|
|
93
|
+
onValueChange(null);
|
|
94
|
+
returnFocus.current = true;
|
|
95
|
+
setEditing(true);
|
|
96
|
+
};
|
|
97
|
+
const hidden = name ? /* @__PURE__ */ jsx("input", { type: "hidden", name, value: value?.id ?? "" }) : null;
|
|
98
|
+
if (!editing && value) {
|
|
99
|
+
const shown = renderValue?.(value) ?? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
100
|
+
/* @__PURE__ */ jsx("span", { className: "uix-entity-picker__title", children: value.title }),
|
|
101
|
+
value.meta?.length ? /* @__PURE__ */ jsx("span", { className: "uix-entity-picker__meta", children: value.meta[value.meta.length - 1] }) : null
|
|
102
|
+
] });
|
|
103
|
+
return /* @__PURE__ */ jsxs("div", { className: cx("uix-entity-picker", className), "data-invalid": invalid || void 0, children: [
|
|
104
|
+
/* @__PURE__ */ jsxs(
|
|
105
|
+
"button",
|
|
106
|
+
{
|
|
107
|
+
ref: valueButtonRef,
|
|
108
|
+
id,
|
|
109
|
+
type: "button",
|
|
110
|
+
className: "uix-input uix-entity-picker__value",
|
|
111
|
+
"aria-label": fillLabel(labels.change, { label, title: value.title }),
|
|
112
|
+
"aria-describedby": describedBy,
|
|
113
|
+
"aria-invalid": invalid || void 0,
|
|
114
|
+
disabled,
|
|
115
|
+
onClick: startEditing,
|
|
116
|
+
children: [
|
|
117
|
+
value.icon != null && /* @__PURE__ */ jsx("span", { className: "uix-entity-picker__icon", "aria-hidden": "true", children: value.icon }),
|
|
118
|
+
/* @__PURE__ */ jsx("span", { className: "uix-entity-picker__text", children: shown })
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
),
|
|
122
|
+
!disabled && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-entity-picker__clear", "aria-label": fillLabel(labels.clear, { title: value.title }), onClick: clear, children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" }) }) }),
|
|
123
|
+
hidden
|
|
124
|
+
] });
|
|
125
|
+
}
|
|
126
|
+
return /* @__PURE__ */ jsxs(
|
|
127
|
+
"div",
|
|
128
|
+
{
|
|
129
|
+
className: cx("uix-entity-picker", "uix-entity-picker--editing", className),
|
|
130
|
+
"data-invalid": invalid || void 0,
|
|
131
|
+
onKeyDown: (event) => {
|
|
132
|
+
if (event.key === "Escape" && value && !query) {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
returnFocus.current = true;
|
|
135
|
+
setEditing(false);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
children: [
|
|
139
|
+
/* @__PURE__ */ jsx(
|
|
140
|
+
SearchSuggest,
|
|
141
|
+
{
|
|
142
|
+
inputId: id,
|
|
143
|
+
inputDescribedBy: describedBy,
|
|
144
|
+
inputRef,
|
|
145
|
+
value: query,
|
|
146
|
+
onValueChange: setQuery,
|
|
147
|
+
options: status === "done" ? results : [],
|
|
148
|
+
onSelect: (_, option) => choose(option),
|
|
149
|
+
label: `${label}. ${labels.search}`,
|
|
150
|
+
placeholder: placeholder ?? labels.none,
|
|
151
|
+
loading: status === "loading",
|
|
152
|
+
loadingLabel: labels.loading,
|
|
153
|
+
empty: labels.empty,
|
|
154
|
+
error: status === "error" ? labels.error : void 0,
|
|
155
|
+
footer: status === "error" ? { label: labels.retry, onSelect: () => setAttempt((n) => n + 1) } : void 0,
|
|
156
|
+
status: status === "done" ? fillLabel(labels.results, { count: results.length }) : void 0,
|
|
157
|
+
strategy: "fixed"
|
|
158
|
+
}
|
|
159
|
+
),
|
|
160
|
+
hidden
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
export {
|
|
166
|
+
DEFAULT_ENTITY_PICKER_LABELS,
|
|
167
|
+
EntityPicker
|
|
168
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
/** A decorative glyph for a file's kind (Lucide-style strokes). Internal to FileUpload and Attachment. */
|
|
4
|
+
declare function FileIcon({ name, type }: {
|
|
5
|
+
name: string;
|
|
6
|
+
type?: string;
|
|
7
|
+
}): react.JSX.Element;
|
|
8
|
+
|
|
9
|
+
export { FileIcon };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { fileKind } from "../file-model.js";
|
|
3
|
+
const PATHS = {
|
|
4
|
+
image: "M4 4h16v16H4zM8.5 10a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM20 15l-5-5L4 20",
|
|
5
|
+
video: "M4 6h12v12H4zM16 10l4-2v8l-4-2",
|
|
6
|
+
pdf: "M14 3H6a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8zM14 3v5h5M8 13h8M8 17h5",
|
|
7
|
+
sheet: "M14 3H6a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8zM14 3v5h5M8 12h8M8 16h8M12 12v8",
|
|
8
|
+
archive: "M14 3H6a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8zM14 3v5h5M10 6h1M10 9h1M10 12h1M10 15h1v3h-1z",
|
|
9
|
+
text: "M14 3H6a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8zM14 3v5h5M8 13h8M8 17h8",
|
|
10
|
+
other: "M14 3H6a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8zM14 3v5h5"
|
|
11
|
+
};
|
|
12
|
+
function FileIcon({ name, type }) {
|
|
13
|
+
return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.75, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: PATHS[fileKind({ name, type })] }) });
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
FileIcon
|
|
17
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { FileRejection, FileSizeFormatter } from '../file-model.js';
|
|
4
|
+
|
|
5
|
+
type FileUploadStatus = 'queued' | 'uploading' | 'done' | 'error';
|
|
6
|
+
interface FileUploadItem {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
/** Bytes. */
|
|
10
|
+
size: number;
|
|
11
|
+
type?: string;
|
|
12
|
+
status: FileUploadStatus;
|
|
13
|
+
/** 0–100 while uploading; without it the bar is indeterminate. */
|
|
14
|
+
progress?: number;
|
|
15
|
+
/** Why it failed (shown under the name with a Retry button). */
|
|
16
|
+
error?: ReactNode;
|
|
17
|
+
/** An object URL or remote URL for an image thumbnail. */
|
|
18
|
+
previewUrl?: string;
|
|
19
|
+
/** Alternative text, when the product asks for it (`onAltChange`). */
|
|
20
|
+
alt?: string;
|
|
21
|
+
}
|
|
22
|
+
interface FileUploadLabels {
|
|
23
|
+
/** Main line of the drop zone. */
|
|
24
|
+
title: string;
|
|
25
|
+
choose: string;
|
|
26
|
+
/** Shown while a file is dragged over. */
|
|
27
|
+
drop: string;
|
|
28
|
+
list: string;
|
|
29
|
+
remove: string;
|
|
30
|
+
/** Accessible name of Retry. `{name}`. */
|
|
31
|
+
retry: string;
|
|
32
|
+
/** Visible text of Retry. */
|
|
33
|
+
retryShort: string;
|
|
34
|
+
queued: string;
|
|
35
|
+
/** `{progress}`. */
|
|
36
|
+
uploading: string;
|
|
37
|
+
done: string;
|
|
38
|
+
failed: string;
|
|
39
|
+
/** `{name}` — set `alt` for images. */
|
|
40
|
+
alt: string;
|
|
41
|
+
/** `{count}` files added. */
|
|
42
|
+
added: string;
|
|
43
|
+
/** `{name}`, `{accept}`. */
|
|
44
|
+
wrongType: string;
|
|
45
|
+
/** `{name}`, `{max}`. */
|
|
46
|
+
tooLarge: string;
|
|
47
|
+
/** `{name}`, `{max}`. */
|
|
48
|
+
tooMany: string;
|
|
49
|
+
}
|
|
50
|
+
declare const DEFAULT_FILE_UPLOAD_LABELS: FileUploadLabels;
|
|
51
|
+
interface FileUploadProps {
|
|
52
|
+
/** The files so far, with their upload state (controlled). */
|
|
53
|
+
items: readonly FileUploadItem[];
|
|
54
|
+
/**
|
|
55
|
+
* Called with the files that passed `accept`, `maxSize` and `maxFiles`, and the ones that did
|
|
56
|
+
* not. Add accepted files to `items` and upload them; the component never uploads.
|
|
57
|
+
*/
|
|
58
|
+
onFilesAdded: (accepted: File[], rejected: FileRejection[]) => void;
|
|
59
|
+
onRemove?: (id: string) => void;
|
|
60
|
+
onRetry?: (id: string) => void;
|
|
61
|
+
/** Ask for alt text on image items (MOTUS photo intake). */
|
|
62
|
+
onAltChange?: (id: string, alt: string) => void;
|
|
63
|
+
/** Accepted types, as for `<input accept>`. */
|
|
64
|
+
accept?: string;
|
|
65
|
+
/** Largest file in bytes. */
|
|
66
|
+
maxSize?: number;
|
|
67
|
+
/** Most files in `items`. */
|
|
68
|
+
maxFiles?: number;
|
|
69
|
+
/** Default true. */
|
|
70
|
+
multiple?: boolean;
|
|
71
|
+
/** Open the camera on phones (`user` or `environment`). */
|
|
72
|
+
capture?: 'user' | 'environment';
|
|
73
|
+
disabled?: boolean;
|
|
74
|
+
/** Second line of the drop zone, e.g. "PDF or images, up to 20 MB". */
|
|
75
|
+
hint?: ReactNode;
|
|
76
|
+
locale?: string;
|
|
77
|
+
/** Count sizes in 1024s (1 MB = 1,048,576 bytes) instead of 1000s. Default `1000`. */
|
|
78
|
+
sizeBase?: 1000 | 1024;
|
|
79
|
+
/** The product's own size formatter; it replaces `formatFileSize` (and `sizeBase`). */
|
|
80
|
+
formatSize?: FileSizeFormatter;
|
|
81
|
+
/**
|
|
82
|
+
* Words for this instance. Each one defaults to `UixLabelsProvider` `fileUpload`, then to
|
|
83
|
+
* English, so one provider translates every upload below it (HAR-1630).
|
|
84
|
+
*/
|
|
85
|
+
labels?: Partial<FileUploadLabels>;
|
|
86
|
+
className?: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Drop zone, file picker and upload list (HAR-984; TENSOR C15, MOTUS C-7, MEDx S18/S19). The
|
|
90
|
+
* product owns the upload: it receives validated `File`s and reports progress, success and
|
|
91
|
+
* failure through `items`. Keyboard users get a real button; drop is an extra, not the only way.
|
|
92
|
+
*/
|
|
93
|
+
declare function FileUpload({ items, onFilesAdded, onRemove, onRetry, onAltChange, accept, maxSize, maxFiles, multiple, capture, disabled, hint, locale, sizeBase, formatSize, labels: labelOverrides, className, }: FileUploadProps): react.JSX.Element;
|
|
94
|
+
|
|
95
|
+
export { DEFAULT_FILE_UPLOAD_LABELS, FileUpload, type FileUploadItem, type FileUploadLabels, type FileUploadProps, type FileUploadStatus };
|