@tensor_1/react 2.47.0 → 2.48.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 (183) hide show
  1. package/README.md +470 -1
  2. package/dist/{SchedulingCalendar-B6TakO2h.d.ts → SchedulingCalendar-WWyFL3Eo.d.ts} +51 -2
  3. package/dist/announce.d.ts +10 -0
  4. package/dist/announce.js +30 -0
  5. package/dist/chart-analytic.cjs +33 -25
  6. package/dist/chart-preset.cjs +23 -15
  7. package/dist/chart.cjs +23 -15
  8. package/dist/combobox-model.d.ts +10 -1
  9. package/dist/combobox-model.js +15 -0
  10. package/dist/command-palette-model.d.ts +44 -0
  11. package/dist/command-palette-model.js +36 -0
  12. package/dist/components/AccountMenu.d.ts +44 -0
  13. package/dist/components/AccountMenu.js +43 -0
  14. package/dist/components/AppShell.d.ts +45 -2
  15. package/dist/components/AppShell.js +116 -5
  16. package/dist/components/AuditLog.d.ts +128 -0
  17. package/dist/components/AuditLog.js +196 -0
  18. package/dist/components/BrandProfiles.js +4 -3
  19. package/dist/components/Breadcrumbs.d.ts +9 -2
  20. package/dist/components/Breadcrumbs.js +3 -2
  21. package/dist/components/Button.d.ts +12 -5
  22. package/dist/components/Button.js +7 -3
  23. package/dist/components/Card.d.ts +18 -1
  24. package/dist/components/Card.js +5 -1
  25. package/dist/components/ChangeHighlight.d.ts +30 -0
  26. package/dist/components/ChangeHighlight.js +21 -0
  27. package/dist/components/ChartCore.js +2 -1
  28. package/dist/components/Checkbox.d.ts +27 -1
  29. package/dist/components/Checkbox.js +31 -2
  30. package/dist/components/CodeDisplay.d.ts +65 -0
  31. package/dist/components/CodeDisplay.js +124 -0
  32. package/dist/components/Combobox.d.ts +27 -6
  33. package/dist/components/Combobox.js +195 -58
  34. package/dist/components/CommandPalette.d.ts +36 -1
  35. package/dist/components/CommandPalette.js +254 -2
  36. package/dist/components/ContactCard.d.ts +66 -0
  37. package/dist/components/ContactCard.js +74 -0
  38. package/dist/components/Count.d.ts +24 -0
  39. package/dist/components/Count.js +32 -0
  40. package/dist/components/DataWorkspace.d.ts +167 -0
  41. package/dist/components/DataWorkspace.js +383 -0
  42. package/dist/components/DateFieldParts.d.ts +2 -1
  43. package/dist/components/DateFieldParts.js +7 -0
  44. package/dist/components/DatePicker.d.ts +9 -2
  45. package/dist/components/DatePicker.js +67 -28
  46. package/dist/components/DateRangePicker.d.ts +12 -2
  47. package/dist/components/DateRangePicker.js +13 -12
  48. package/dist/components/DetailPage.d.ts +7 -1
  49. package/dist/components/DetailPage.js +2 -1
  50. package/dist/components/Dialogs.d.ts +71 -10
  51. package/dist/components/Dialogs.js +130 -30
  52. package/dist/components/DiffViewer.d.ts +23 -3
  53. package/dist/components/DiffViewer.js +26 -12
  54. package/dist/components/DiscardPrompt.js +16 -0
  55. package/dist/components/DisplayPreferences.d.ts +83 -0
  56. package/dist/components/DisplayPreferences.js +185 -0
  57. package/dist/components/EditorialHome.d.ts +23 -3
  58. package/dist/components/EditorialHome.js +16 -11
  59. package/dist/components/EntityPicker.d.ts +20 -3
  60. package/dist/components/EntityPicker.js +32 -13
  61. package/dist/components/FilterEditor.d.ts +18 -4
  62. package/dist/components/FilterEditor.js +141 -19
  63. package/dist/components/InfoTip.js +11 -12
  64. package/dist/components/InlineEdit.d.ts +87 -20
  65. package/dist/components/InlineEdit.js +197 -54
  66. package/dist/components/Input.d.ts +12 -2
  67. package/dist/components/Input.js +5 -3
  68. package/dist/components/Kbd.d.ts +6 -1
  69. package/dist/components/Kbd.js +6 -6
  70. package/dist/components/Lightbox.d.ts +41 -5
  71. package/dist/components/Lightbox.js +95 -9
  72. package/dist/components/MatchReview.d.ts +40 -1
  73. package/dist/components/MatchReview.js +130 -4
  74. package/dist/components/MetricInput.js +3 -2
  75. package/dist/components/Modal.d.ts +2 -1
  76. package/dist/components/Modal.js +13 -4
  77. package/dist/components/NavFavourites.d.ts +5 -0
  78. package/dist/components/NavFavourites.js +7 -7
  79. package/dist/components/OtpInput.d.ts +33 -0
  80. package/dist/components/OtpInput.js +123 -0
  81. package/dist/components/PageHeader.d.ts +7 -1
  82. package/dist/components/PageHeader.js +2 -2
  83. package/dist/components/Pagination.js +6 -5
  84. package/dist/components/PasswordInput.d.ts +34 -0
  85. package/dist/components/PasswordInput.js +101 -0
  86. package/dist/components/Peek.js +3 -3
  87. package/dist/components/Popover.js +2 -0
  88. package/dist/components/PrintLayout.d.ts +63 -0
  89. package/dist/components/PrintLayout.js +114 -0
  90. package/dist/components/QrCode.d.ts +47 -0
  91. package/dist/components/QrCode.js +55 -0
  92. package/dist/components/QrScanner.d.ts +95 -0
  93. package/dist/components/QrScanner.js +266 -0
  94. package/dist/components/RelationshipGraph.d.ts +32 -1
  95. package/dist/components/RelationshipGraph.js +18 -5
  96. package/dist/components/RelationshipGraphLayered.d.ts +1 -1
  97. package/dist/components/RelationshipGraphLayered.js +190 -115
  98. package/dist/components/RelationshipGraphList.js +33 -6
  99. package/dist/components/RuleBuilder.js +39 -7
  100. package/dist/components/SchedulingAgenda.d.ts +3 -1
  101. package/dist/components/SchedulingCalendar.d.ts +3 -1
  102. package/dist/components/SchedulingCalendar.js +43 -4
  103. package/dist/components/SchedulingResourceDay.d.ts +57 -0
  104. package/dist/components/SchedulingResourceDay.js +287 -0
  105. package/dist/components/SchedulingTimeGrid.d.ts +3 -1
  106. package/dist/components/SchedulingTimeline.d.ts +3 -1
  107. package/dist/components/SearchSuggest.d.ts +23 -3
  108. package/dist/components/SearchSuggest.js +48 -17
  109. package/dist/components/Segmented.d.ts +39 -4
  110. package/dist/components/Segmented.js +31 -3
  111. package/dist/components/Select.d.ts +3 -2
  112. package/dist/components/Select.js +1 -1
  113. package/dist/components/ShortcutHint.d.ts +11 -10
  114. package/dist/components/ShortcutHint.js +44 -38
  115. package/dist/components/Sidebar.d.ts +36 -4
  116. package/dist/components/Sidebar.js +37 -12
  117. package/dist/components/SortableList.d.ts +45 -0
  118. package/dist/components/SortableList.js +134 -0
  119. package/dist/components/Stat.d.ts +28 -3
  120. package/dist/components/Stat.js +13 -3
  121. package/dist/components/StatusPill.d.ts +14 -3
  122. package/dist/components/StatusPill.js +11 -3
  123. package/dist/components/Steps.js +2 -1
  124. package/dist/components/Table.d.ts +9 -2
  125. package/dist/components/Table.js +4 -2
  126. package/dist/components/TableControls.js +18 -58
  127. package/dist/components/TableOverflowProbe.d.ts +12 -0
  128. package/dist/components/TableOverflowProbe.js +27 -0
  129. package/dist/components/Tabs.d.ts +2 -1
  130. package/dist/components/Tabs.js +1 -0
  131. package/dist/components/TagInput.d.ts +67 -0
  132. package/dist/components/TagInput.js +342 -0
  133. package/dist/components/Textarea.d.ts +5 -0
  134. package/dist/components/Textarea.js +2 -2
  135. package/dist/components/TimePicker.d.ts +80 -0
  136. package/dist/components/TimePicker.js +347 -0
  137. package/dist/components/Toast.js +3 -2
  138. package/dist/components/Tooltip.d.ts +8 -4
  139. package/dist/components/Tooltip.js +24 -21
  140. package/dist/components/Tree.d.ts +8 -0
  141. package/dist/components/Tree.js +11 -5
  142. package/dist/display-preferences-model.d.ts +53 -0
  143. package/dist/display-preferences-model.js +64 -0
  144. package/dist/emoji.cjs +28 -35
  145. package/dist/filter-model.d.ts +59 -3
  146. package/dist/filter-model.js +67 -2
  147. package/dist/hooks/useChangeHighlight.d.ts +33 -0
  148. package/dist/hooks/useChangeHighlight.js +44 -0
  149. package/dist/hooks/useDialog.d.ts +5 -0
  150. package/dist/hooks/useDialog.js +3 -2
  151. package/dist/hooks/useOptimisticValue.d.ts +51 -0
  152. package/dist/hooks/useOptimisticValue.js +61 -0
  153. package/dist/hooks/useReducedMotion.d.ts +4 -1
  154. package/dist/hooks/useReducedMotion.js +7 -1
  155. package/dist/hooks/useResolvedFilterLabels.d.ts +15 -0
  156. package/dist/hooks/useResolvedFilterLabels.js +34 -0
  157. package/dist/hooks/useVirtualRows.d.ts +21 -2
  158. package/dist/hooks/useVirtualRows.js +30 -3
  159. package/dist/icons/Icon.d.ts +1 -1
  160. package/dist/index.cjs +18644 -13210
  161. package/dist/index.d.cts +3003 -699
  162. package/dist/index.d.ts +42 -16
  163. package/dist/index.js +139 -7
  164. package/dist/labels-context.d.ts +90 -3
  165. package/dist/overlay-stack.d.ts +6 -0
  166. package/dist/overlay-stack.js +13 -0
  167. package/dist/qr-model.d.ts +65 -0
  168. package/dist/qr-model.js +323 -0
  169. package/dist/relationship-graph-model.d.ts +78 -3
  170. package/dist/relationship-graph-model.js +112 -5
  171. package/dist/reorder.d.ts +30 -0
  172. package/dist/reorder.js +74 -0
  173. package/dist/rich-text.cjs +4 -2
  174. package/dist/scheduling-resource-model.d.ts +76 -0
  175. package/dist/scheduling-resource-model.js +74 -0
  176. package/dist/search-suggest-model.js +3 -1
  177. package/dist/shell-context.d.ts +11 -0
  178. package/dist/shell-context.js +7 -0
  179. package/dist/time-field-model.d.ts +46 -0
  180. package/dist/time-field-model.js +152 -0
  181. package/dist/toast-store.d.ts +17 -1
  182. package/dist/toast-store.js +23 -7
  183. package/package.json +2 -1
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { TextFilterOperator, NumberFilterOperator, FilterField, FilterValue } from '../filter-model.js';
2
+ import { TextFilterOperator, NumberFilterOperator, DateFilterOperator, FilterField, FilterValue } from '../filter-model.js';
3
3
 
4
4
  interface FilterEditorLabels {
5
5
  apply: string;
@@ -26,7 +26,15 @@ interface FilterEditorLabels {
26
26
  remove: string;
27
27
  /** Names the row of `field.presets` chips. Optional so existing label objects still type-check; default "Presets". */
28
28
  presets?: string;
29
+ /** `tags` (HAR-1832): the field that takes a new value, and its hint. Optional, like `presets`. */
30
+ addTag?: string;
31
+ /** `tags`: how to add one, read with the field. */
32
+ addTagHint?: string;
33
+ /** `date` (HAR-1832): the date of on / before / after. Optional, like `presets`. */
34
+ date?: string;
29
35
  operators: Record<TextFilterOperator | NumberFilterOperator, string>;
36
+ /** `date` conditions (HAR-1832). Optional, like `presets`. */
37
+ dateOperators?: Partial<Record<DateFilterOperator, string>>;
30
38
  }
31
39
  declare const DEFAULT_FILTER_EDITOR_LABELS: FilterEditorLabels;
32
40
  interface FilterEditorProps {
@@ -34,7 +42,12 @@ interface FilterEditorProps {
34
42
  /** Undefined means "not filtered yet". */
35
43
  value: FilterValue | undefined;
36
44
  onValueChange: (value: FilterValue) => void;
37
- onApply: () => void;
45
+ /**
46
+ * Apply the filter: the Apply button, or Enter in any field (HAR-1832). Gets the value to
47
+ * apply, which already holds a tag still being typed: read it rather than your state, which
48
+ * has not caught up when Enter applies in the same event.
49
+ */
50
+ onApply: (value: FilterValue) => void;
38
51
  onClear: () => void;
39
52
  labels?: Partial<FilterEditorLabels>;
40
53
  /** Show an option filter above an enum list once it has more options than this. Default 8. */
@@ -44,8 +57,9 @@ interface FilterEditorProps {
44
57
  }
45
58
  /**
46
59
  * Editor content for one typed column filter (HAR-1365; TENSOR C6, MOTUS C-1): an enum
47
- * multi-select, text or number with a condition, a date range, yes/no, or records found by
48
- * an async search. The product owns the popover trigger and the query; `summarizeFilter`
60
+ * multi-select, text or number with a condition, a date range, a date with a condition
61
+ * (on / before / after / between), yes/no, free-text tags, or records found by an async search
62
+ * (restored ids get their labels from `field.resolveLabels`). Enter applies in every kind. The product owns the popover trigger and the query; `summarizeFilter`
49
63
  * turns the applied value into the chip text. `FilterPopover` stays for the one-string case.
50
64
  * An enum field's `presets` (HAR-1505) render as toggle chips above its options. Words come
51
65
  * from `labels`, or from `UixLabelsProvider`'s `filterEditor` entry.
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useEffect, useId, useMemo, useRef, useState } from "react";
3
+ import { Fragment as Fragment2, useEffect, useId, useMemo, useRef, useState } from "react";
4
4
  import { Button } from "./Button.js";
5
5
  import { Checkbox } from "./Checkbox.js";
6
6
  import { Input } from "./Input.js";
@@ -8,9 +8,11 @@ import { Radio, RadioGroup } from "./Radio.js";
8
8
  import { Select } from "./Select.js";
9
9
  import { Chip } from "./Chip.js";
10
10
  import { fillLabel } from "../fill-label.js";
11
+ import { cx } from "../cx.js";
11
12
  import { useUixLabels } from "../labels-context.js";
13
+ import { useDelayedPending } from "../hooks/useDelayedPending.js";
12
14
  import { foldForSearch } from "../search-suggest-model.js";
13
- import { NUMBER_FILTER_OPERATORS, TEXT_FILTER_OPERATORS, emptyFilterValue, matchFilterPreset } from "../filter-model.js";
15
+ import { DATE_FILTER_OPERATORS, NUMBER_FILTER_OPERATORS, TEXT_FILTER_OPERATORS, emptyFilterValue, isUnresolvedChoice, matchFilterPreset, orderFilterRange } from "../filter-model.js";
14
16
  const DEFAULT_FILTER_EDITOR_LABELS = {
15
17
  apply: "Apply",
16
18
  clear: "Clear",
@@ -32,6 +34,10 @@ const DEFAULT_FILTER_EDITOR_LABELS = {
32
34
  selected: "{count} selected",
33
35
  remove: "Remove {label}",
34
36
  presets: "Presets",
37
+ addTag: "Add a value",
38
+ addTagHint: "Enter or a comma adds it.",
39
+ date: "Date",
40
+ dateOperators: { on: "on", before: "before", after: "after", between: "between" },
35
41
  operators: {
36
42
  contains: "contains",
37
43
  "not-contains": "does not contain",
@@ -53,10 +59,21 @@ function FilterEditor({ field, value: valueProp, onValueChange, onApply, onClear
53
59
  ...DEFAULT_FILTER_EDITOR_LABELS,
54
60
  ...provided,
55
61
  ...labelOverrides,
56
- operators: { ...DEFAULT_FILTER_EDITOR_LABELS.operators, ...provided?.operators, ...labelOverrides?.operators }
62
+ operators: { ...DEFAULT_FILTER_EDITOR_LABELS.operators, ...provided?.operators, ...labelOverrides?.operators },
63
+ dateOperators: { ...DEFAULT_FILTER_EDITOR_LABELS.dateOperators, ...provided?.dateOperators, ...labelOverrides?.dateOperators }
57
64
  };
58
65
  const id = useId();
59
- const value = valueProp?.kind === field.kind ? valueProp : emptyFilterValue(field.kind);
66
+ let value = valueProp?.kind === field.kind ? valueProp : emptyFilterValue(field.kind);
67
+ if (value.kind === "date" && field.dateOperators?.length && !field.dateOperators.includes(value.operator)) value = { ...value, operator: field.dateOperators[0] };
68
+ const pendingTag = useRef(() => void 0);
69
+ const apply = () => onApply(orderFilterRange((value.kind === "tags" ? pendingTag.current() : void 0) ?? value));
70
+ const onKeyDown = (event) => {
71
+ if (event.key !== "Enter" || event.defaultPrevented || event.nativeEvent.isComposing || event.keyCode === 229) return;
72
+ const target = event.target;
73
+ if (target.closest('button, a, textarea, [role="option"], [role="listbox"], [aria-expanded="true"]')) return;
74
+ event.preventDefault();
75
+ apply();
76
+ };
60
77
  let body = null;
61
78
  switch (value.kind) {
62
79
  case "enum":
@@ -65,13 +82,31 @@ function FilterEditor({ field, value: valueProp, onValueChange, onApply, onClear
65
82
  case "reference":
66
83
  body = /* @__PURE__ */ jsx(ReferenceBody, { field, value, onChange: onValueChange, labels, delay: searchDelay });
67
84
  break;
85
+ case "tags":
86
+ body = /* @__PURE__ */ jsx(TagsBody, { value, onChange: onValueChange, labels, pending: pendingTag, id });
87
+ break;
88
+ case "date": {
89
+ const ops = field.dateOperators ?? DATE_FILTER_OPERATORS;
90
+ const words = labels.dateOperators ?? {};
91
+ const between = value.operator === "between";
92
+ body = /* @__PURE__ */ jsxs("div", { className: "uix-filter-editor__row", children: [
93
+ ops.length > 1 && /* @__PURE__ */ jsx(Select, { size: "sm", "aria-label": labels.operator, value: value.operator, onChange: (e) => onValueChange({ ...value, operator: e.currentTarget.value, ...e.currentTarget.value === "between" ? {} : { to: void 0 } }), children: ops.map((op) => /* @__PURE__ */ jsx("option", { value: op, children: words[op] ?? DEFAULT_FILTER_EDITOR_LABELS.dateOperators[op] }, op)) }),
94
+ /* @__PURE__ */ jsxs("label", { className: "uix-filter-editor__date", children: [
95
+ /* @__PURE__ */ jsx("span", { children: between ? labels.from : labels.date ?? DEFAULT_FILTER_EDITOR_LABELS.date }),
96
+ /* @__PURE__ */ jsx(Input, { type: "date", value: value.from ?? "", max: between ? value.to : void 0, onChange: (e) => onValueChange({ ...value, from: e.currentTarget.value || void 0 }), autoFocus: true })
97
+ ] }),
98
+ between && /* @__PURE__ */ jsxs("label", { className: "uix-filter-editor__date", children: [
99
+ /* @__PURE__ */ jsx("span", { children: labels.to }),
100
+ /* @__PURE__ */ jsx(Input, { type: "date", value: value.to ?? "", min: value.from, onChange: (e) => onValueChange({ ...value, to: e.currentTarget.value || void 0 }) })
101
+ ] })
102
+ ] });
103
+ break;
104
+ }
68
105
  case "text": {
69
106
  const ops = field.operators ?? TEXT_FILTER_OPERATORS;
70
107
  body = /* @__PURE__ */ jsxs("div", { className: "uix-filter-editor__row", children: [
71
108
  ops.length > 1 && /* @__PURE__ */ jsx(Select, { size: "sm", "aria-label": labels.operator, value: value.operator, onChange: (e) => onValueChange({ ...value, operator: e.currentTarget.value }), children: ops.map((op) => /* @__PURE__ */ jsx("option", { value: op, children: labels.operators[op] }, op)) }),
72
- /* @__PURE__ */ jsx(Input, { "aria-label": labels.value, value: value.text, onChange: (e) => onValueChange({ ...value, text: e.currentTarget.value }), onKeyDown: (e) => {
73
- if (e.key === "Enter") onApply();
74
- }, autoFocus: true })
109
+ /* @__PURE__ */ jsx(Input, { "aria-label": labels.value, value: value.text, onChange: (e) => onValueChange({ ...value, text: e.currentTarget.value }), autoFocus: true })
75
110
  ] });
76
111
  break;
77
112
  }
@@ -111,12 +146,12 @@ function FilterEditor({ field, value: valueProp, onValueChange, onApply, onClear
111
146
  break;
112
147
  }
113
148
  }
114
- return /* @__PURE__ */ jsxs("div", { className: "uix-filter-popover uix-filter-editor", role: "group", "aria-labelledby": `${id}-label`, "data-kind": field.kind, children: [
149
+ return /* @__PURE__ */ jsxs("div", { className: "uix-filter-popover uix-filter-editor", role: "group", "aria-labelledby": `${id}-label`, "data-kind": field.kind, onKeyDown, children: [
115
150
  /* @__PURE__ */ jsx("div", { id: `${id}-label`, className: "uix-filter-editor__label", children: field.label }),
116
- body,
151
+ /* @__PURE__ */ jsx(Fragment2, { children: body }, `${field.kind}:${field.id}`),
117
152
  /* @__PURE__ */ jsxs("div", { className: "uix-filter-popover__actions", children: [
118
153
  /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", onClick: onClear, children: labels.clear }),
119
- /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "primary", onClick: onApply, children: labels.apply })
154
+ /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "primary", onClick: apply, children: labels.apply })
120
155
  ] })
121
156
  ] });
122
157
  }
@@ -146,13 +181,18 @@ function ReferenceBody({ field, value, onChange, labels, delay }) {
146
181
  const [query, setQuery] = useState("");
147
182
  const [state, setState] = useState({ status: "idle", results: [] });
148
183
  const [attempt, setAttempt] = useState(0);
184
+ const slow = useDelayedPending(state.status === "loading");
149
185
  const latest = useRef(0);
186
+ const search = useRef(field.onSearch);
187
+ search.current = field.onSearch;
188
+ const resolve = useRef(field.resolveLabels);
189
+ resolve.current = field.resolveLabels;
150
190
  useEffect(() => {
151
- if (!field.onSearch) return;
191
+ if (!search.current) return;
152
192
  const run = ++latest.current;
153
193
  const timer = window.setTimeout(() => {
154
194
  setState((s) => ({ ...s, status: "loading" }));
155
- field.onSearch(query.trim()).then(
195
+ search.current(query.trim()).then(
156
196
  (results) => {
157
197
  if (run === latest.current) setState({ status: "done", results });
158
198
  },
@@ -162,22 +202,104 @@ function ReferenceBody({ field, value, onChange, labels, delay }) {
162
202
  );
163
203
  }, delay);
164
204
  return () => window.clearTimeout(timer);
165
- }, [query, attempt, field, delay]);
166
- const chosen = new Map(value.values.map((v) => [v.value, v]));
167
- const toggle = (choice, on) => onChange({ kind: "reference", values: on ? [...value.values, choice] : value.values.filter((v) => v.value !== choice.value) });
205
+ }, [query, attempt, field.id, delay]);
206
+ const [known, setKnown] = useState(/* @__PURE__ */ new Map());
207
+ const answered = useRef(/* @__PURE__ */ new Set());
208
+ const requests = useRef(/* @__PURE__ */ new Map());
209
+ const unresolved = value.values.filter((v) => isUnresolvedChoice(v) && !known.has(v.value) && !answered.current.has(v.value)).map((v) => v.value);
210
+ const unresolvedKey = unresolved.join("\0");
211
+ useEffect(() => {
212
+ if (!unresolvedKey || !resolve.current) return;
213
+ let live = true;
214
+ const ids = unresolvedKey.split("\0");
215
+ let request = requests.current.get(unresolvedKey);
216
+ if (!request) {
217
+ request = resolve.current(ids);
218
+ requests.current.set(unresolvedKey, request);
219
+ }
220
+ const settle = (choices) => {
221
+ for (const id of ids) answered.current.add(id);
222
+ if (live) setKnown((prev) => new Map([...prev, ...choices.map((c) => [c.value, c.label])]));
223
+ };
224
+ request.then(settle, () => settle([]));
225
+ return () => {
226
+ live = false;
227
+ };
228
+ }, [unresolvedKey, field.id]);
229
+ const labelled = (v) => isUnresolvedChoice(v) && known.has(v.value) ? { value: v.value, label: known.get(v.value) } : v;
230
+ const values = value.values.map(labelled);
231
+ const chosen = new Map(values.map((v) => [v.value, v]));
232
+ const toggle = (choice, on) => onChange({ kind: "reference", values: on ? [...values, choice] : values.filter((v) => v.value !== choice.value) });
233
+ const showResults = state.status === "done" || state.status === "loading" && state.results.length > 0;
168
234
  return /* @__PURE__ */ jsxs(Fragment, { children: [
169
- value.values.length > 0 && /* @__PURE__ */ jsx("div", { className: "uix-chip-group", role: "group", "aria-label": fillLabel(labels.selected, { count: value.values.length }), children: value.values.map((v) => /* @__PURE__ */ jsx(Chip, { removeLabel: fillLabel(labels.remove, { label: v.label }), onRemove: () => toggle(v, false), children: v.label }, v.value)) }),
235
+ values.length > 0 && /* @__PURE__ */ jsx("div", { className: "uix-chip-group", role: "group", "aria-label": fillLabel(labels.selected, { count: values.length }), children: values.map((v) => /* @__PURE__ */ jsx(Chip, { removeLabel: fillLabel(labels.remove, { label: v.label }), onRemove: () => toggle(v, false), children: v.label }, v.value)) }),
170
236
  /* @__PURE__ */ jsx(Input, { type: "search", size: "sm", "aria-label": labels.search, placeholder: labels.search, value: query, onChange: (e) => setQuery(e.currentTarget.value), autoFocus: true }),
171
237
  /* @__PURE__ */ jsxs("div", { className: "uix-filter-editor__list", "aria-busy": state.status === "loading" || void 0, children: [
172
- state.status === "loading" && /* @__PURE__ */ jsx("p", { className: "uix-filter-editor__note", role: "status", children: labels.searching }),
173
238
  state.status === "error" && /* @__PURE__ */ jsxs("p", { className: "uix-filter-editor__note", role: "alert", children: [
174
239
  labels.searchFailed,
175
240
  " ",
176
241
  /* @__PURE__ */ jsx(Button, { type: "button", variant: "link", size: "sm", onClick: () => setAttempt((n) => n + 1), children: labels.retry })
177
242
  ] }),
178
243
  state.status === "done" && state.results.length === 0 && /* @__PURE__ */ jsx("p", { className: "uix-filter-editor__note", children: labels.noMatches }),
179
- state.status === "done" && state.results.map((r) => /* @__PURE__ */ jsx(Checkbox, { label: r.label, checked: chosen.has(r.value), onChange: (e) => toggle(r, e.currentTarget.checked) }, r.value))
180
- ] })
244
+ showResults && state.results.map((r) => /* @__PURE__ */ jsx(Checkbox, { label: r.label, checked: chosen.has(r.value), onChange: (e) => toggle(r, e.currentTarget.checked) }, r.value))
245
+ ] }),
246
+ /* @__PURE__ */ jsx("p", { className: cx("uix-filter-editor__note", "uix-filter-editor__searching", !slow && "uix-visually-hidden"), role: "status", "data-shown": slow || void 0, children: slow ? labels.searching : "" })
247
+ ] });
248
+ }
249
+ const splitTags = (text) => text.split(/[,\n]/).map((t) => t.trim()).filter(Boolean);
250
+ function TagsBody({ value, onChange, labels, pending, id }) {
251
+ const [draft, setDraft] = useState("");
252
+ const field = useRef(null);
253
+ const withTags = (tags) => ({ kind: "tags", values: [...value.values, ...tags.filter((t, i) => !value.values.includes(t) && tags.indexOf(t) === i)] });
254
+ pending.current = () => splitTags(draft).length ? withTags(splitTags(draft)) : void 0;
255
+ const commit = (text) => {
256
+ const tags = splitTags(text);
257
+ if (tags.length) onChange(withTags(tags));
258
+ setDraft("");
259
+ };
260
+ const remove = (tag) => {
261
+ onChange({ kind: "tags", values: value.values.filter((t) => t !== tag) });
262
+ field.current?.focus();
263
+ };
264
+ const onKeyDown = (event) => {
265
+ if (event.nativeEvent.isComposing || event.keyCode === 229) return;
266
+ if ((event.key === "Enter" || event.key === ",") && draft.trim()) {
267
+ event.preventDefault();
268
+ commit(draft);
269
+ } else if (event.key === ",") {
270
+ event.preventDefault();
271
+ } else if (event.key === "Backspace" && draft === "" && value.values.length > 0) {
272
+ onChange({ kind: "tags", values: value.values.slice(0, -1) });
273
+ }
274
+ };
275
+ const onPaste = (event) => {
276
+ const text = event.clipboardData.getData("text");
277
+ if (!/[,\n]/.test(text)) return;
278
+ event.preventDefault();
279
+ commit(`${draft}${text}`);
280
+ };
281
+ return /* @__PURE__ */ jsxs("div", { className: "uix-taginput uix-filter-editor__tags", children: [
282
+ value.values.map((tag) => /* @__PURE__ */ jsxs("span", { className: "uix-tag", children: [
283
+ tag,
284
+ /* @__PURE__ */ jsx("button", { type: "button", className: "uix-tag__remove", "aria-label": fillLabel(labels.remove, { label: tag }), onClick: () => remove(tag), children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" }) }) })
285
+ ] }, tag)),
286
+ /* @__PURE__ */ jsx(
287
+ "input",
288
+ {
289
+ ref: field,
290
+ className: "uix-taginput__field",
291
+ "aria-label": labels.addTag ?? DEFAULT_FILTER_EDITOR_LABELS.addTag,
292
+ "aria-describedby": `${id}-tags-hint`,
293
+ placeholder: labels.addTag ?? DEFAULT_FILTER_EDITOR_LABELS.addTag,
294
+ value: draft,
295
+ enterKeyHint: "enter",
296
+ onChange: (e) => setDraft(e.currentTarget.value),
297
+ onKeyDown,
298
+ onPaste,
299
+ autoFocus: true
300
+ }
301
+ ),
302
+ /* @__PURE__ */ jsx("span", { id: `${id}-tags-hint`, className: "uix-visually-hidden", children: labels.addTagHint ?? DEFAULT_FILTER_EDITOR_LABELS.addTagHint })
181
303
  ] });
182
304
  }
183
305
  export {
@@ -1,11 +1,12 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useEffect, useId, useRef, useState } from "react";
3
+ import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
4
4
  import { cx } from "../cx.js";
5
5
  import { helpParagraphs } from "../info-tip-model.js";
6
6
  import { Popover } from "./Popover.js";
7
7
  const OPEN_DELAY = 300;
8
8
  const CLOSE_DELAY = 100;
9
+ const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
9
10
  const QuestionMark = () => /* @__PURE__ */ jsx("svg", { viewBox: "262 -840 436 760", fill: "currentColor", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx("path", { d: "M424-320q0-81 14.5-116.5T500-514q41-36 62.5-62.5T584-637q0-41-27.5-68T480-732q-51 0-77.5 31T365-638l-103-44q21-64 77-111t141-47q105 0 161.5 58.5T698-641q0 50-21.5 85.5T609-475q-49 47-59.5 71.5T539-320H424Zm56 240q-33 0-56.5-23.5T400-160q0-33 23.5-56.5T480-240q33 0 56.5 23.5T560-160q0 33-23.5 56.5T480-80Z" }) });
10
11
  const isShown = (el) => {
11
12
  try {
@@ -93,18 +94,21 @@ function InfoTipControl({
93
94
  refocusing.current = false;
94
95
  }
95
96
  };
96
- useEffect(() => {
97
- if (!open) return;
97
+ useIsomorphicLayoutEffect(() => {
98
+ if (!open) return void 0;
98
99
  const onKey = (e) => {
99
- if (e.key === "Escape") escape();
100
+ if (e.key !== "Escape" || !openRef.current) return;
101
+ e.preventDefault();
102
+ e.stopPropagation();
103
+ escape();
100
104
  };
101
105
  const onPointer = (e) => {
102
106
  if (!wrapRef.current?.contains(e.target)) hide();
103
107
  };
104
- document.addEventListener("keydown", onKey);
108
+ document.addEventListener("keydown", onKey, true);
105
109
  document.addEventListener("pointerdown", onPointer, true);
106
110
  return () => {
107
- document.removeEventListener("keydown", onKey);
111
+ document.removeEventListener("keydown", onKey, true);
108
112
  document.removeEventListener("pointerdown", onPointer, true);
109
113
  };
110
114
  }, [open]);
@@ -148,13 +152,8 @@ function InfoTipControl({
148
152
  pinned.current = true;
149
153
  }
150
154
  },
151
- onKeyDown: (e) => {
155
+ onKeyDown: () => {
152
156
  pressing.current = false;
153
- if (e.key === "Escape" && openRef.current) {
154
- e.preventDefault();
155
- e.stopPropagation();
156
- hide();
157
- }
158
157
  },
159
158
  children: /* @__PURE__ */ jsx(QuestionMark, {})
160
159
  }
@@ -3,11 +3,11 @@ import { ReactNode } from 'react';
3
3
 
4
4
  /** Every word `InlineEdit` renders or announces. `{label}` is the field's name. */
5
5
  interface InlineEditLabels {
6
- /** Added to the view button's name after the value: "…, Edit {label}". */
6
+ /** Added to the view button's name after the value: "…, Edit {label}"; the icon trigger's whole name. */
7
7
  edit: string;
8
8
  save: string;
9
9
  cancel: string;
10
- /** Polite status while `onSave` runs. */
10
+ /** Polite status while `onSave` runs (shown after 300 ms, so a fast save does not flash it). */
11
11
  saving: string;
12
12
  /** Polite status after a save. */
13
13
  saved: string;
@@ -15,50 +15,114 @@ interface InlineEditLabels {
15
15
  failed: string;
16
16
  /** What the view shows when the value is empty and no `placeholder` was given. */
17
17
  empty: string;
18
+ /** Visually hidden before a `readOnlyReason`, so it is read as one: "Read-only: …". */
19
+ readOnly: string;
18
20
  }
19
21
  declare const DEFAULT_INLINE_EDIT_LABELS: InlineEditLabels;
22
+ /** Where an `InlineEdit` is (HAR-1833). `saved` holds until the field is opened again. */
23
+ type InlineEditState = 'idle' | 'editing' | 'saving' | 'saved' | 'error';
20
24
  /** What a custom editor receives. Spread the field props onto your control. */
21
- interface InlineEditEditorProps {
25
+ interface InlineEditEditorProps<T = string> {
22
26
  /** The draft. */
23
- value: string;
24
- onChange: (next: string) => void;
25
- /** Save the draft (what Enter does in the default editor). */
26
- save: () => void;
27
+ value: T;
28
+ /** Change the draft. With `commit="change"` this also saves it. Ignored while a save runs. */
29
+ onChange: (next: T) => void;
30
+ /**
31
+ * Save the draft (what Enter does in the default editor). Pass the value to save one you
32
+ * just chose: `save(next)` does not wait for the draft to update, so it never saves a stale one.
33
+ */
34
+ save: (next?: T) => void;
27
35
  cancel: () => void;
28
- /** `id`, the accessible name, the error wiring and `disabled` while saving. */
36
+ /** `id`, the accessible name, the error wiring, and the busy state while saving. */
29
37
  field: {
30
38
  id: string;
31
39
  'aria-label': string;
32
40
  'aria-describedby': string | undefined;
33
41
  'aria-invalid': true | undefined;
42
+ /** True once a save has run for 300 ms (changes are ignored from its start, so a fast save never greys the control). */
34
43
  disabled: boolean;
44
+ /** The same moment as `disabled`, for a text control that should keep focus and its text selectable. */
45
+ readOnly: boolean;
35
46
  };
36
47
  }
37
- interface InlineEditProps {
48
+ /** What `renderError` receives besides the rejection itself. */
49
+ interface InlineEditErrorContext<T = string> {
50
+ /** The rejection's message, else `labels.failed`: what the default rendering shows. */
51
+ message: ReactNode;
52
+ /** The draft that failed. */
53
+ draft: T;
54
+ /** Send the draft again. Show it when a retry can help (a timeout, not a refusal). */
55
+ retry: () => void;
56
+ /** Close the editor and drop the draft, e.g. after "Reload latest" on a version conflict. */
57
+ cancel: () => void;
58
+ }
59
+ interface InlineEditProps<T = string> {
38
60
  /** The saved value. */
39
- value: string;
61
+ value: T;
40
62
  /**
41
63
  * Save the new value. Return a promise to show the pending state; a rejection keeps the
42
64
  * editor open with the error under it (the rejection's message, else `labels.failed`).
65
+ * The editor waits for it: the value shows as saved only once it resolves.
43
66
  */
44
- onSave: (next: string) => void | Promise<void>;
67
+ onSave: (next: T) => void | Promise<unknown>;
45
68
  /** The field's name: "Title", "Due date". It names the edit button and the editor. */
46
69
  label: string;
47
- /** How the saved value shows. Default: the text itself. */
48
- renderView?: (value: string) => ReactNode;
70
+ /**
71
+ * How the saved value shows. Default: the text itself (`String(value)`); pass it for any
72
+ * value that is not a string (an option, a person, a time window).
73
+ */
74
+ renderView?: (value: T) => ReactNode;
49
75
  /**
50
76
  * A custom editor: a `Select`, a `Combobox`, a date field. Spread `field` onto the control.
51
- * Default: an `Input`, or a `Textarea` with `multiline`.
77
+ * Default: an `Input`, or a `Textarea` with `multiline` (string values only).
52
78
  */
53
- renderEditor?: (props: InlineEditEditorProps) => ReactNode;
79
+ renderEditor?: (props: InlineEditEditorProps<T>) => ReactNode;
54
80
  /** Edit in a textarea: Enter adds a line, Ctrl/⌘+Enter saves. */
55
81
  multiline?: boolean;
56
82
  /** Return a message to refuse the draft; nothing is saved while it returns one. */
57
- validate?: (next: string) => ReactNode | undefined;
83
+ validate?: (next: T) => ReactNode | undefined;
84
+ /** Whether two values are the same (an unchanged draft saves nothing). Default `Object.is`. */
85
+ equals?: (a: T, b: T) => boolean;
86
+ /** Whether a value counts as empty (the view then shows `placeholder`). Default: `null`, `undefined` or `''`. */
87
+ isEmpty?: (value: T) => boolean;
88
+ /**
89
+ * When the draft is saved (HAR-1833).
90
+ * `'explicit'` (default): Enter or Save only; leaving the field keeps the draft open.
91
+ * `'blur'`: also when focus leaves the editor (text fields of a record).
92
+ * `'change'`: as soon as the editor reports a change (a select, a toggle, a date), with no
93
+ * Save button; leaving it unchanged closes it.
94
+ */
95
+ commit?: 'explicit' | 'blur' | 'change';
58
96
  /** Shown in the view while the value is empty, e.g. "Add a description". */
59
97
  placeholder?: ReactNode;
60
- /** Read-only: the value shows as plain text, with no edit button. */
98
+ /** Read-only: the value shows as plain text, with no edit control. */
61
99
  disabled?: boolean;
100
+ /**
101
+ * Why the field cannot be edited here ("Set by the integration"). Makes it read-only and
102
+ * shows the reason under the value, which is described by it.
103
+ */
104
+ readOnlyReason?: ReactNode;
105
+ /**
106
+ * `'value'` (default): the whole value is the button, named "{value}, Edit {label}".
107
+ * `'icon'`: the value is plain text and a pencil button after it is named only "Edit {label}"
108
+ * (for a value that is itself a link, a chip or long text).
109
+ */
110
+ trigger?: 'value' | 'icon';
111
+ /** Controlled open state, e.g. one field of a record open at a time. */
112
+ editing?: boolean;
113
+ /** Called when the field asks to open (`true`) or close (`false`). */
114
+ onEditingChange?: (editing: boolean) => void;
115
+ /** Controlled draft, e.g. to report unsaved input. Reset to `value` on open. */
116
+ draft?: T;
117
+ /** Called with every change of the draft, and with `value` when the field opens. */
118
+ onDraftChange?: (draft: T) => void;
119
+ /**
120
+ * Renders a failed save under the editor, in place of the message: a Try again button that
121
+ * calls `retry`, or a Reload latest after a version conflict.
122
+ */
123
+ renderError?: (error: unknown, context: InlineEditErrorContext<T>) => ReactNode;
124
+ /** Called on every change of `InlineEditState`. */
125
+ onStateChange?: (state: InlineEditState) => void;
62
126
  /** Each word defaults to `UixLabelsProvider` `inlineEdit`, then to English. */
63
127
  labels?: Partial<InlineEditLabels>;
64
128
  className?: string;
@@ -67,8 +131,11 @@ interface InlineEditProps {
67
131
  * A value that is edited in place (HAR-1381; TENSOR C16 `editable-record.tsx`): the value is
68
132
  * a button that says what it edits; activating it swaps in an editor with Save and Cancel.
69
133
  * Enter saves and Escape cancels; while `onSave` runs the editor is busy; a failed save keeps
70
- * the draft and shows why; afterwards focus is back on the value. Nothing is saved on blur.
134
+ * the draft and shows why; afterwards focus is back on the value, which shows what was saved
135
+ * until `value` catches up. HAR-1833: any value type (`equals`, `renderView`, `renderEditor`),
136
+ * saving on blur or on change (`commit`), controllable `editing` and `draft`, `renderError`
137
+ * with retry, `onStateChange`, an icon trigger, and a `readOnlyReason`.
71
138
  */
72
- declare function InlineEdit({ value, onSave, label, renderView, renderEditor, multiline, validate, placeholder, disabled, labels: labelOverrides, className, }: InlineEditProps): react.JSX.Element;
139
+ declare function InlineEdit<T = string>({ value, onSave, label, renderView, renderEditor, multiline, validate, equals, isEmpty, commit, placeholder, disabled, readOnlyReason, trigger, editing: editingProp, onEditingChange, draft: draftProp, onDraftChange, renderError, onStateChange, labels: labelOverrides, className, }: InlineEditProps<T>): react.JSX.Element;
73
140
 
74
- export { DEFAULT_INLINE_EDIT_LABELS, InlineEdit, type InlineEditEditorProps, type InlineEditLabels, type InlineEditProps };
141
+ export { DEFAULT_INLINE_EDIT_LABELS, InlineEdit, type InlineEditEditorProps, type InlineEditErrorContext, type InlineEditLabels, type InlineEditProps, type InlineEditState };