@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.
Files changed (122) hide show
  1. package/README.md +430 -0
  2. package/dist/attachment-labels.d.ts +16 -0
  3. package/dist/attachment-labels.js +9 -0
  4. package/dist/calendar-model.d.ts +141 -1
  5. package/dist/calendar-model.js +200 -11
  6. package/dist/chart-preset.cjs +127 -7
  7. package/dist/chart-preset.d.cts +2 -2
  8. package/dist/chart-preset.d.ts +1 -0
  9. package/dist/chart-preset.js +6 -1
  10. package/dist/chart-theme-Cl0IrkKQ.d.cts +112 -0
  11. package/dist/chart-theme.d.ts +51 -0
  12. package/dist/chart-theme.js +99 -0
  13. package/dist/chart.cjs +127 -7
  14. package/dist/chart.d.cts +2 -2
  15. package/dist/chart.d.ts +1 -0
  16. package/dist/chart.js +6 -2
  17. package/dist/components/Alert.d.ts +17 -1
  18. package/dist/components/Alert.js +9 -4
  19. package/dist/components/AlertDismiss.d.ts +14 -0
  20. package/dist/components/AlertDismiss.js +11 -0
  21. package/dist/components/Attachment.d.ts +58 -0
  22. package/dist/components/Attachment.js +69 -0
  23. package/dist/components/AttachmentParts.d.ts +24 -0
  24. package/dist/components/AttachmentParts.js +20 -0
  25. package/dist/components/Chart.d.ts +1 -0
  26. package/dist/components/Chart.js +6 -1
  27. package/dist/components/ChartCore.d.ts +6 -1
  28. package/dist/components/ChartCore.js +23 -5
  29. package/dist/components/Chip.d.ts +17 -2
  30. package/dist/components/Chip.js +35 -11
  31. package/dist/components/ChipRemove.d.ts +17 -0
  32. package/dist/components/ChipRemove.js +13 -0
  33. package/dist/components/CollapsibleParts.d.ts +29 -0
  34. package/dist/components/CollapsibleParts.js +20 -0
  35. package/dist/components/CollapsibleSection.d.ts +30 -3
  36. package/dist/components/CollapsibleSection.js +7 -11
  37. package/dist/components/CollapsibleSectionState.d.ts +1 -1
  38. package/dist/components/CollapsibleSectionState.js +54 -27
  39. package/dist/components/DashboardGrid.d.ts +60 -0
  40. package/dist/components/DashboardGrid.js +40 -0
  41. package/dist/components/DateRangePicker.d.ts +7 -1
  42. package/dist/components/DateRangePicker.js +5 -1
  43. package/dist/components/DescriptionList.d.ts +12 -1
  44. package/dist/components/DescriptionList.js +5 -5
  45. package/dist/components/Dialogs.d.ts +24 -3
  46. package/dist/components/Dialogs.js +74 -16
  47. package/dist/components/EntityPicker.d.ts +56 -0
  48. package/dist/components/EntityPicker.js +168 -0
  49. package/dist/components/FileIcon.d.ts +9 -0
  50. package/dist/components/FileIcon.js +17 -0
  51. package/dist/components/FileUpload.d.ts +95 -0
  52. package/dist/components/FileUpload.js +145 -0
  53. package/dist/components/FilterEditor.d.ts +55 -0
  54. package/dist/components/FilterEditor.js +186 -0
  55. package/dist/components/Heartbeat.d.ts +31 -0
  56. package/dist/components/Heartbeat.js +28 -0
  57. package/dist/components/Lightbox.d.ts +45 -0
  58. package/dist/components/Lightbox.js +102 -0
  59. package/dist/components/Menu.d.ts +53 -5
  60. package/dist/components/Menu.js +104 -4
  61. package/dist/components/Meter.d.ts +7 -3
  62. package/dist/components/Meter.js +2 -1
  63. package/dist/components/Popover.d.ts +21 -2
  64. package/dist/components/Popover.js +94 -17
  65. package/dist/components/Prose.d.ts +6 -2
  66. package/dist/components/Prose.js +1 -1
  67. package/dist/components/RuleBuilder.d.ts +52 -4
  68. package/dist/components/RuleBuilder.js +109 -24
  69. package/dist/components/SchedulingCalendar.d.ts +142 -11
  70. package/dist/components/SchedulingCalendar.js +234 -57
  71. package/dist/components/SchedulingTimeline.d.ts +1 -0
  72. package/dist/components/SearchSuggest.d.ts +6 -2
  73. package/dist/components/SearchSuggest.js +4 -0
  74. package/dist/components/Segmented.d.ts +17 -3
  75. package/dist/components/Segmented.js +59 -5
  76. package/dist/components/Select.d.ts +66 -2
  77. package/dist/components/Select.js +800 -10
  78. package/dist/components/Spinner.d.ts +2 -1
  79. package/dist/components/Spinner.js +1 -1
  80. package/dist/components/StatusPill.d.ts +8 -2
  81. package/dist/components/StatusPill.js +2 -2
  82. package/dist/components/Steps.d.ts +28 -4
  83. package/dist/components/Steps.js +46 -16
  84. package/dist/components/TableControls.js +4 -3
  85. package/dist/components/Tabs.d.ts +7 -2
  86. package/dist/components/Tabs.js +14 -1
  87. package/dist/components/TextDiff.d.ts +49 -0
  88. package/dist/components/TextDiff.js +124 -0
  89. package/dist/{emoji-image-6mPZ15Zy.d.cts → emoji-image-DjCKpdwE.d.cts} +1 -1
  90. package/dist/emoji.cjs +169 -27
  91. package/dist/emoji.d.cts +3 -3
  92. package/dist/file-model.d.ts +56 -0
  93. package/dist/file-model.js +52 -0
  94. package/dist/filter-model.d.ts +112 -0
  95. package/dist/filter-model.js +103 -0
  96. package/dist/hooks/useAnchoredPosition.d.ts +24 -1
  97. package/dist/hooks/useAnchoredPosition.js +58 -8
  98. package/dist/hooks/useVirtualRows.d.ts +17 -2
  99. package/dist/hooks/useVirtualRows.js +6 -5
  100. package/dist/icons/Icon.d.ts +14 -14
  101. package/dist/index.cjs +4938 -1631
  102. package/dist/index.d.cts +1622 -178
  103. package/dist/index.d.ts +20 -6
  104. package/dist/index.js +88 -4
  105. package/dist/labels-context.d.ts +28 -0
  106. package/dist/{overlay-position-Cjv5E7mn.d.cts → overlay-position-2Afke51l.d.cts} +20 -0
  107. package/dist/overlay-position.d.ts +20 -0
  108. package/dist/overlay-position.js +17 -2
  109. package/dist/rich-text.cjs +169 -27
  110. package/dist/rich-text.d.cts +3 -3
  111. package/dist/rule-builder-model.d.ts +34 -2
  112. package/dist/rule-builder-model.js +48 -12
  113. package/dist/scheduling-calendar-model.d.ts +66 -0
  114. package/dist/scheduling-calendar-model.js +82 -0
  115. package/dist/select-model.d.ts +80 -0
  116. package/dist/select-model.js +109 -0
  117. package/dist/table-engine.d.ts +7 -0
  118. package/dist/table-engine.js +2 -1
  119. package/dist/text-diff-model.d.ts +71 -0
  120. package/dist/text-diff-model.js +261 -0
  121. package/package.json +1 -1
  122. 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
- closeLabel: string;
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
- closeLabel: string;
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({ open, title, description, inputLabel, defaultValue = "", placeholder, submitLabel, cancelLabel, closeLabel, onSubmit, onCancel, validate, pending }) {
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 [error, setError] = useState();
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
- setError(void 0);
218
+ setIssue(void 0);
193
219
  }
194
220
  }, [defaultValue, open]);
195
221
  const handleSubmit = (event) => {
196
222
  event.preventDefault();
197
- const issue = validate?.(value);
198
- setError(issue);
199
- if (issue == null) void onSubmit(value);
223
+ const found = validate?.(value);
224
+ setIssue(found);
225
+ if (found == null) void onSubmit(value);
200
226
  };
201
- return /* @__PURE__ */ jsx(Modal, { open, onClose: onCancel, title, closeLabel, children: /* @__PURE__ */ jsxs("form", { className: "uix-prompt", onSubmit: handleSubmit, children: [
202
- description != null && /* @__PURE__ */ jsx("div", { children: description }),
203
- /* @__PURE__ */ jsx("label", { className: "uix-field__label", htmlFor: inputId, children: inputLabel }),
204
- /* @__PURE__ */ jsx(Input, { id: inputId, value, placeholder, "aria-describedby": error ? errorId : void 0, invalid: error != null, onChange: (event) => setValue(event.currentTarget.value), autoFocus: true }),
205
- error != null && /* @__PURE__ */ jsx("div", { id: errorId, className: "uix-field__error", role: "alert", children: error }),
206
- /* @__PURE__ */ jsxs("div", { className: "uix-dialog__footer uix-prompt__actions", children: [
207
- /* @__PURE__ */ jsx(Button, { type: "button", onClick: onCancel, disabled: pending, children: cancelLabel }),
208
- /* @__PURE__ */ jsx(Button, { type: "submit", variant: "primary", loading: pending, children: submitLabel })
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 };