@tensor_1/react 2.47.0 → 2.49.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 (204) hide show
  1. package/README.md +540 -2
  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/Chip.d.ts +7 -3
  31. package/dist/components/Chip.js +2 -2
  32. package/dist/components/CodeDisplay.d.ts +65 -0
  33. package/dist/components/CodeDisplay.js +124 -0
  34. package/dist/components/Combobox.d.ts +27 -6
  35. package/dist/components/Combobox.js +195 -58
  36. package/dist/components/CommandPalette.d.ts +36 -1
  37. package/dist/components/CommandPalette.js +254 -2
  38. package/dist/components/ContactCard.d.ts +66 -0
  39. package/dist/components/ContactCard.js +74 -0
  40. package/dist/components/Count.d.ts +24 -0
  41. package/dist/components/Count.js +32 -0
  42. package/dist/components/DashboardCanvas.js +14 -0
  43. package/dist/components/DataWorkspace.d.ts +167 -0
  44. package/dist/components/DataWorkspace.js +380 -0
  45. package/dist/components/DateFieldParts.d.ts +2 -1
  46. package/dist/components/DateFieldParts.js +7 -0
  47. package/dist/components/DatePicker.d.ts +9 -2
  48. package/dist/components/DatePicker.js +67 -28
  49. package/dist/components/DateRangePicker.d.ts +12 -2
  50. package/dist/components/DateRangePicker.js +13 -12
  51. package/dist/components/DetailPage.d.ts +7 -1
  52. package/dist/components/DetailPage.js +2 -1
  53. package/dist/components/Dialogs.d.ts +71 -10
  54. package/dist/components/Dialogs.js +130 -30
  55. package/dist/components/DiffViewer.d.ts +23 -3
  56. package/dist/components/DiffViewer.js +26 -12
  57. package/dist/components/DiscardPrompt.js +16 -0
  58. package/dist/components/DisplayPreferences.d.ts +83 -0
  59. package/dist/components/DisplayPreferences.js +185 -0
  60. package/dist/components/EditorialHome.d.ts +23 -3
  61. package/dist/components/EditorialHome.js +16 -11
  62. package/dist/components/EntityPicker.d.ts +20 -3
  63. package/dist/components/EntityPicker.js +32 -13
  64. package/dist/components/FilterEditor.d.ts +18 -4
  65. package/dist/components/FilterEditor.js +141 -19
  66. package/dist/components/FocusOnMount.d.ts +282 -0
  67. package/dist/components/FocusOnMount.js +12 -0
  68. package/dist/components/InfoTip.js +11 -12
  69. package/dist/components/InlineEdit.d.ts +87 -20
  70. package/dist/components/InlineEdit.js +197 -54
  71. package/dist/components/Input.d.ts +12 -2
  72. package/dist/components/Input.js +5 -3
  73. package/dist/components/Kanban.d.ts +14 -3
  74. package/dist/components/Kanban.js +9 -4
  75. package/dist/components/Kbd.d.ts +6 -1
  76. package/dist/components/Kbd.js +6 -6
  77. package/dist/components/Lightbox.d.ts +41 -5
  78. package/dist/components/Lightbox.js +95 -9
  79. package/dist/components/MatchReview.d.ts +40 -1
  80. package/dist/components/MatchReview.js +130 -4
  81. package/dist/components/MetricInput.js +3 -2
  82. package/dist/components/Modal.d.ts +2 -1
  83. package/dist/components/Modal.js +13 -4
  84. package/dist/components/MotionList.d.ts +303 -0
  85. package/dist/components/MotionList.js +175 -0
  86. package/dist/components/NavFavourites.d.ts +5 -0
  87. package/dist/components/NavFavourites.js +7 -7
  88. package/dist/components/OtpInput.d.ts +33 -0
  89. package/dist/components/OtpInput.js +123 -0
  90. package/dist/components/PageHeader.d.ts +7 -1
  91. package/dist/components/PageHeader.js +2 -2
  92. package/dist/components/Pagination.js +6 -5
  93. package/dist/components/PasswordInput.d.ts +34 -0
  94. package/dist/components/PasswordInput.js +101 -0
  95. package/dist/components/Peek.js +3 -3
  96. package/dist/components/Popover.js +11 -0
  97. package/dist/components/PrintLayout.d.ts +63 -0
  98. package/dist/components/PrintLayout.js +114 -0
  99. package/dist/components/QrCode.d.ts +47 -0
  100. package/dist/components/QrCode.js +55 -0
  101. package/dist/components/QrScanner.d.ts +95 -0
  102. package/dist/components/QrScanner.js +266 -0
  103. package/dist/components/RelationshipGraph.d.ts +32 -1
  104. package/dist/components/RelationshipGraph.js +22 -6
  105. package/dist/components/RelationshipGraphLayered.d.ts +1 -1
  106. package/dist/components/RelationshipGraphLayered.js +268 -118
  107. package/dist/components/RelationshipGraphList.js +33 -6
  108. package/dist/components/RuleBuilder.js +39 -7
  109. package/dist/components/SchedulingAgenda.d.ts +3 -1
  110. package/dist/components/SchedulingCalendar.d.ts +3 -1
  111. package/dist/components/SchedulingCalendar.js +43 -4
  112. package/dist/components/SchedulingResourceDay.d.ts +57 -0
  113. package/dist/components/SchedulingResourceDay.js +287 -0
  114. package/dist/components/SchedulingTimeGrid.d.ts +3 -1
  115. package/dist/components/SchedulingTimeline.d.ts +3 -1
  116. package/dist/components/SearchSuggest.d.ts +23 -3
  117. package/dist/components/SearchSuggest.js +56 -18
  118. package/dist/components/Segmented.d.ts +39 -4
  119. package/dist/components/Segmented.js +39 -5
  120. package/dist/components/Select.d.ts +3 -2
  121. package/dist/components/Select.js +7 -1
  122. package/dist/components/ShortcutHint.d.ts +11 -10
  123. package/dist/components/ShortcutHint.js +44 -38
  124. package/dist/components/Sidebar.d.ts +36 -4
  125. package/dist/components/Sidebar.js +37 -12
  126. package/dist/components/SortableList.d.ts +45 -0
  127. package/dist/components/SortableList.js +147 -0
  128. package/dist/components/Stat.d.ts +28 -3
  129. package/dist/components/Stat.js +13 -3
  130. package/dist/components/States.d.ts +33 -1
  131. package/dist/components/States.js +43 -1
  132. package/dist/components/StatusPill.d.ts +14 -3
  133. package/dist/components/StatusPill.js +11 -3
  134. package/dist/components/Steps.js +2 -1
  135. package/dist/components/Table.d.ts +9 -2
  136. package/dist/components/Table.js +4 -2
  137. package/dist/components/TableControls.js +18 -58
  138. package/dist/components/TableOverflowProbe.d.ts +12 -0
  139. package/dist/components/TableOverflowProbe.js +27 -0
  140. package/dist/components/Tabs.d.ts +2 -1
  141. package/dist/components/Tabs.js +10 -2
  142. package/dist/components/TagInput.d.ts +67 -0
  143. package/dist/components/TagInput.js +342 -0
  144. package/dist/components/Textarea.d.ts +5 -0
  145. package/dist/components/Textarea.js +2 -2
  146. package/dist/components/TimePicker.d.ts +80 -0
  147. package/dist/components/TimePicker.js +347 -0
  148. package/dist/components/Toast.d.ts +1 -1
  149. package/dist/components/Toast.js +109 -4
  150. package/dist/components/Tooltip.d.ts +8 -4
  151. package/dist/components/Tooltip.js +86 -28
  152. package/dist/components/Tree.d.ts +8 -0
  153. package/dist/components/Tree.js +11 -5
  154. package/dist/display-preferences-model.d.ts +53 -0
  155. package/dist/display-preferences-model.js +64 -0
  156. package/dist/emoji.cjs +143 -49
  157. package/dist/filter-model.d.ts +59 -3
  158. package/dist/filter-model.js +67 -2
  159. package/dist/hooks/useAnchoredPosition.js +14 -2
  160. package/dist/hooks/useChangeHighlight.d.ts +33 -0
  161. package/dist/hooks/useChangeHighlight.js +44 -0
  162. package/dist/hooks/useDeferredReorder.d.ts +32 -0
  163. package/dist/hooks/useDeferredReorder.js +47 -0
  164. package/dist/hooks/useDialog.d.ts +5 -0
  165. package/dist/hooks/useDialog.js +17 -3
  166. package/dist/hooks/useKeyboardGrab.d.ts +58 -0
  167. package/dist/hooks/useKeyboardGrab.js +118 -0
  168. package/dist/hooks/useOptimisticValue.d.ts +51 -0
  169. package/dist/hooks/useOptimisticValue.js +61 -0
  170. package/dist/hooks/useReducedMotion.d.ts +4 -1
  171. package/dist/hooks/useReducedMotion.js +7 -1
  172. package/dist/hooks/useResolvedFilterLabels.d.ts +15 -0
  173. package/dist/hooks/useResolvedFilterLabels.js +34 -0
  174. package/dist/hooks/useSlidingIndicator.d.ts +18 -0
  175. package/dist/hooks/useSlidingIndicator.js +80 -0
  176. package/dist/hooks/useVirtualRows.d.ts +21 -2
  177. package/dist/hooks/useVirtualRows.js +30 -3
  178. package/dist/icons/Icon.d.ts +1 -1
  179. package/dist/index.cjs +19687 -13423
  180. package/dist/index.d.cts +3419 -651
  181. package/dist/index.d.ts +48 -18
  182. package/dist/index.js +153 -9
  183. package/dist/labels-context.d.ts +93 -3
  184. package/dist/motion.d.ts +11 -0
  185. package/dist/motion.js +35 -0
  186. package/dist/overlay-stack.d.ts +6 -0
  187. package/dist/overlay-stack.js +13 -0
  188. package/dist/qr-model.d.ts +65 -0
  189. package/dist/qr-model.js +323 -0
  190. package/dist/relationship-graph-model.d.ts +92 -3
  191. package/dist/relationship-graph-model.js +134 -15
  192. package/dist/reorder.d.ts +30 -0
  193. package/dist/reorder.js +74 -0
  194. package/dist/rich-text.cjs +46 -4
  195. package/dist/scheduling-resource-model.d.ts +76 -0
  196. package/dist/scheduling-resource-model.js +74 -0
  197. package/dist/search-suggest-model.js +3 -1
  198. package/dist/shell-context.d.ts +11 -0
  199. package/dist/shell-context.js +7 -0
  200. package/dist/time-field-model.d.ts +46 -0
  201. package/dist/time-field-model.js +152 -0
  202. package/dist/toast-store.d.ts +22 -3
  203. package/dist/toast-store.js +26 -9
  204. package/package.json +2 -1
@@ -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 };
@@ -1,9 +1,10 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useEffect, useId, useRef, useState } from "react";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
4
4
  import { cx } from "../cx.js";
5
5
  import { fillLabel } from "../fill-label.js";
6
6
  import { useUixLabels } from "../labels-context.js";
7
+ import { useDelayedPending } from "../hooks/useDelayedPending.js";
7
8
  import { PencilIcon } from "../icons/components.js";
8
9
  import { Button } from "./Button.js";
9
10
  import { Input } from "./Input.js";
@@ -15,8 +16,10 @@ const DEFAULT_INLINE_EDIT_LABELS = {
15
16
  saving: "Saving\u2026",
16
17
  saved: "Saved",
17
18
  failed: "Could not save.",
18
- empty: "Not set"
19
+ empty: "Not set",
20
+ readOnly: "Read-only:"
19
21
  };
22
+ const defaultEmpty = (value) => value == null || value === "";
20
23
  function InlineEdit({
21
24
  value,
22
25
  onSave,
@@ -25,22 +28,45 @@ function InlineEdit({
25
28
  renderEditor,
26
29
  multiline = false,
27
30
  validate,
31
+ equals = Object.is,
32
+ isEmpty = defaultEmpty,
33
+ commit = "explicit",
28
34
  placeholder,
29
35
  disabled = false,
36
+ readOnlyReason,
37
+ trigger = "value",
38
+ editing: editingProp,
39
+ onEditingChange,
40
+ draft: draftProp,
41
+ onDraftChange,
42
+ renderError,
43
+ onStateChange,
30
44
  labels: labelOverrides,
31
45
  className
32
46
  }) {
33
47
  const labels = { ...DEFAULT_INLINE_EDIT_LABELS, ...useUixLabels().inlineEdit, ...labelOverrides };
34
48
  const id = useId();
35
49
  const errorId = `${id}-error`;
36
- const [editing, setEditing] = useState(false);
37
- const [draft, setDraft] = useState(value);
50
+ const reasonId = `${id}-reason`;
51
+ const [editingState, setEditingState] = useState(false);
52
+ const isEditing = editingProp ?? editingState;
53
+ const [draftState, setDraftState] = useState(value);
54
+ const draft = draftProp !== void 0 ? draftProp : draftState;
38
55
  const [pending, setPending] = useState(false);
56
+ const pendingVisible = useDelayedPending(pending) && pending;
39
57
  const [error, setError] = useState();
40
58
  const [status, setStatus] = useState("");
59
+ const [saved, setSaved] = useState(false);
60
+ const [accepted, setAccepted] = useState(null);
41
61
  const viewRef = useRef(null);
42
62
  const editorRef = useRef(null);
43
- const focusNext = useRef(null);
63
+ const focusView = useRef(false);
64
+ const editingRef = useRef(isEditing);
65
+ editingRef.current = isEditing;
66
+ const inFlight = useRef(false);
67
+ const lastSent = useRef(null);
68
+ const session = useRef(0);
69
+ const pressInside = useRef(false);
44
70
  const mounted = useRef(true);
45
71
  useEffect(() => {
46
72
  mounted.current = true;
@@ -48,83 +74,143 @@ function InlineEdit({
48
74
  mounted.current = false;
49
75
  };
50
76
  }, []);
77
+ const lastValue = useRef(value);
78
+ useLayoutEffect(() => {
79
+ if (equals(lastValue.current, value)) return;
80
+ lastValue.current = value;
81
+ setAccepted(null);
82
+ });
83
+ const shown = accepted ? accepted.value : value;
51
84
  useEffect(() => {
52
- const target = focusNext.current;
53
- if (!target) return;
54
- focusNext.current = null;
55
- if (target === "view") {
56
- viewRef.current?.focus();
85
+ if (pendingVisible && pending) setStatus(labels.saving);
86
+ }, [pendingVisible, pending]);
87
+ const state = pending ? "saving" : error?.failure ? "error" : isEditing ? "editing" : saved ? "saved" : "idle";
88
+ const onStateChangeRef = useRef(onStateChange);
89
+ onStateChangeRef.current = onStateChange;
90
+ const reported = useRef("idle");
91
+ useEffect(() => {
92
+ if (reported.current === state) return;
93
+ reported.current = state;
94
+ onStateChangeRef.current?.(state);
95
+ }, [state]);
96
+ const setEditing = (next) => {
97
+ editingRef.current = next;
98
+ if (editingProp === void 0) setEditingState(next);
99
+ onEditingChange?.(next);
100
+ };
101
+ const setDraft = (next) => {
102
+ if (draftProp === void 0) setDraftState(next);
103
+ onDraftChange?.(next);
104
+ };
105
+ useEffect(() => {
106
+ if (isEditing) {
107
+ const control = editorRef.current?.querySelector("input, textarea, select, button, [tabindex]");
108
+ control?.focus();
109
+ if (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) control.select();
57
110
  return;
58
111
  }
59
- const control = editorRef.current?.querySelector("input, textarea, select, button, [tabindex]");
60
- control?.focus();
61
- if (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) control.select();
62
- }, [editing]);
63
- const start = () => {
64
- setDraft(value);
112
+ if (focusView.current) {
113
+ focusView.current = false;
114
+ viewRef.current?.focus();
115
+ }
116
+ }, [isEditing]);
117
+ const wasEditing = useRef(isEditing);
118
+ useLayoutEffect(() => {
119
+ if (isEditing && !wasEditing.current && editingProp !== void 0) resetForOpen();
120
+ wasEditing.current = isEditing;
121
+ });
122
+ function resetForOpen() {
123
+ setDraft(shown);
65
124
  setError(void 0);
66
125
  setStatus("");
67
- focusNext.current = "editor";
126
+ setSaved(false);
127
+ lastSent.current = null;
128
+ }
129
+ const start = () => {
130
+ session.current += 1;
131
+ resetForOpen();
68
132
  setEditing(true);
69
133
  };
70
- const close = () => {
71
- focusNext.current = "view";
134
+ const close = (returnFocus) => {
135
+ focusView.current = returnFocus;
136
+ inFlight.current = false;
72
137
  setEditing(false);
73
138
  };
74
139
  const cancel = () => {
75
- if (pending) return;
140
+ if (inFlight.current) return;
76
141
  setError(void 0);
77
- close();
142
+ setDraft(shown);
143
+ close(true);
78
144
  };
79
- const save = () => {
80
- if (pending) return;
81
- const issue = validate?.(draft);
145
+ const failureMessage = (reason) => typeof reason === "string" && reason ? reason : reason instanceof Error && reason.message ? reason.message : labels.failed;
146
+ const send = (next, explicit) => {
147
+ if (!editingRef.current || inFlight.current) return;
148
+ if (!explicit && error?.failure && lastSent.current && equals(lastSent.current.value, next)) return;
149
+ const issue = validate?.(next);
82
150
  if (issue != null) {
83
- setError(issue);
151
+ setError({ message: issue });
84
152
  return;
85
153
  }
86
- if (draft === value) {
87
- close();
154
+ if (equals(next, shown)) {
155
+ setError(void 0);
156
+ close(explicit);
88
157
  return;
89
158
  }
90
159
  setError(void 0);
160
+ lastSent.current = { value: next };
161
+ const mine = session.current;
91
162
  let result;
92
163
  try {
93
- result = onSave(draft);
164
+ result = onSave(next);
94
165
  } catch (thrown) {
95
- setError(thrown instanceof Error && thrown.message ? thrown.message : labels.failed);
166
+ setError({ message: failureMessage(thrown), failure: { reason: thrown } });
96
167
  return;
97
168
  }
98
- if (!result || typeof result.then !== "function") {
169
+ const done = () => {
170
+ setAccepted({ value: next });
99
171
  setStatus(labels.saved);
100
- close();
172
+ setSaved(true);
173
+ if (editingRef.current && session.current === mine) close(explicit);
174
+ };
175
+ if (!result || typeof result.then !== "function") {
176
+ done();
101
177
  return;
102
178
  }
179
+ inFlight.current = true;
103
180
  setPending(true);
104
- setStatus(labels.saving);
105
181
  result.then(
106
182
  () => {
107
183
  if (!mounted.current) return;
184
+ inFlight.current = false;
108
185
  setPending(false);
109
- setStatus(labels.saved);
110
- close();
186
+ done();
111
187
  },
112
188
  (reason) => {
113
189
  if (!mounted.current) return;
190
+ inFlight.current = false;
114
191
  setPending(false);
192
+ if (session.current !== mine) return;
115
193
  setStatus("");
116
- setError(typeof reason === "string" && reason ? reason : reason instanceof Error && reason.message ? reason.message : labels.failed);
117
- focusNext.current = null;
194
+ setError({ message: failureMessage(reason), failure: { reason } });
118
195
  editorRef.current?.querySelector("input, textarea, select")?.focus();
119
196
  }
120
197
  );
121
198
  };
199
+ const save = (next) => send(next === void 0 ? draft : next, true);
200
+ const retry = () => send(lastSent.current && equals(lastSent.current.value, draft) ? lastSent.current.value : draft, true);
201
+ const change = (next) => {
202
+ if (inFlight.current) return;
203
+ setDraft(next);
204
+ if (error && !error.failure) setError(void 0);
205
+ if (commit === "change") send(next, true);
206
+ };
122
207
  const onEditorKeyDown = (event) => {
123
208
  if (event.key === "Escape") {
209
+ if (event.defaultPrevented || event.target.closest?.('[aria-expanded="true"]')) return;
124
210
  event.preventDefault();
125
211
  event.stopPropagation();
126
212
  cancel();
127
- } else if (event.key === "Enter" && !event.defaultPrevented) {
213
+ } else if (event.key === "Enter" && !event.defaultPrevented && commit !== "change") {
128
214
  const inField = event.target.matches("input, textarea");
129
215
  if (!inField) return;
130
216
  const isTextarea = event.target.tagName === "TEXTAREA";
@@ -133,15 +219,56 @@ function InlineEdit({
133
219
  save();
134
220
  }
135
221
  };
222
+ const onEditorBlur = (event) => {
223
+ if (commit === "explicit") return;
224
+ const next = event.relatedTarget;
225
+ if (next && event.currentTarget.contains(next)) return;
226
+ if (pressInside.current) return;
227
+ if (next) {
228
+ send(draft, false);
229
+ return;
230
+ }
231
+ const draftNow = draft;
232
+ setTimeout(() => {
233
+ if (!editingRef.current || pressInside.current) return;
234
+ if (editorRef.current?.contains(document.activeElement)) return;
235
+ send(draftNow, false);
236
+ }, 0);
237
+ };
238
+ const onEditorPointerDown = () => {
239
+ pressInside.current = true;
240
+ const release = () => {
241
+ setTimeout(() => {
242
+ pressInside.current = false;
243
+ }, 0);
244
+ window.removeEventListener("pointerup", release, true);
245
+ window.removeEventListener("pointercancel", release, true);
246
+ };
247
+ window.addEventListener("pointerup", release, true);
248
+ window.addEventListener("pointercancel", release, true);
249
+ };
136
250
  const liveRegion = /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", role: "status", "aria-live": "polite", children: status });
137
- const shown = value === "" ? /* @__PURE__ */ jsx("span", { className: "uix-inline-edit__placeholder", children: placeholder ?? labels.empty }) : renderView?.(value) ?? value;
138
- if (disabled) {
139
- return /* @__PURE__ */ jsx("span", { className: cx("uix-inline-edit", "uix-inline-edit--readonly", className), children: /* @__PURE__ */ jsx("span", { className: "uix-inline-edit__value", children: shown }) });
251
+ const empty = isEmpty(shown);
252
+ const display = empty ? /* @__PURE__ */ jsx("span", { className: "uix-inline-edit__placeholder", children: placeholder ?? labels.empty }) : renderView?.(shown) ?? String(shown);
253
+ if (disabled || readOnlyReason != null) {
254
+ return /* @__PURE__ */ jsxs("span", { className: cx("uix-inline-edit", "uix-inline-edit--readonly", readOnlyReason != null && "uix-inline-edit--reason", className), children: [
255
+ /* @__PURE__ */ jsx("span", { className: "uix-inline-edit__value", "aria-describedby": readOnlyReason != null ? reasonId : void 0, children: display }),
256
+ readOnlyReason != null && /* @__PURE__ */ jsxs("span", { id: reasonId, className: "uix-inline-edit__reason", children: [
257
+ /* @__PURE__ */ jsxs("span", { className: "uix-visually-hidden", children: [
258
+ labels.readOnly,
259
+ " "
260
+ ] }),
261
+ readOnlyReason
262
+ ] })
263
+ ] });
140
264
  }
141
- if (!editing) {
142
- return /* @__PURE__ */ jsxs("span", { className: cx("uix-inline-edit", className), children: [
143
- /* @__PURE__ */ jsxs("button", { ref: viewRef, type: "button", className: "uix-inline-edit__view", onClick: start, children: [
144
- /* @__PURE__ */ jsx("span", { className: "uix-inline-edit__value", children: shown }),
265
+ if (!isEditing) {
266
+ return /* @__PURE__ */ jsxs("span", { className: cx("uix-inline-edit", trigger === "icon" && "uix-inline-edit--icon-trigger", className), children: [
267
+ trigger === "icon" ? /* @__PURE__ */ jsxs(Fragment, { children: [
268
+ /* @__PURE__ */ jsx("span", { className: "uix-inline-edit__value", children: display }),
269
+ /* @__PURE__ */ jsx("button", { ref: viewRef, type: "button", className: "uix-inline-edit__trigger", "aria-label": fillLabel(labels.edit, { label }), onClick: start, children: /* @__PURE__ */ jsx(PencilIcon, { size: "sm" }) })
270
+ ] }) : /* @__PURE__ */ jsxs("button", { ref: viewRef, type: "button", className: "uix-inline-edit__view", onClick: start, children: [
271
+ /* @__PURE__ */ jsx("span", { className: "uix-inline-edit__value", children: display }),
145
272
  /* @__PURE__ */ jsxs("span", { className: "uix-visually-hidden", children: [
146
273
  ", ",
147
274
  fillLabel(labels.edit, { label })
@@ -156,17 +283,33 @@ function InlineEdit({
156
283
  "aria-label": label,
157
284
  "aria-describedby": error != null ? errorId : void 0,
158
285
  "aria-invalid": error != null ? true : void 0,
159
- disabled: pending
286
+ disabled: pendingVisible,
287
+ readOnly: pendingVisible
160
288
  };
289
+ const textDraft = typeof draft === "string" ? draft : String(draft ?? "");
290
+ const errorBody = error && (error.failure && renderError ? renderError(error.failure.reason, { message: error.message, draft, retry, cancel }) : error.message);
161
291
  return /* @__PURE__ */ jsxs("span", { className: cx("uix-inline-edit", "uix-inline-edit--editing", className), children: [
162
- /* @__PURE__ */ jsxs("div", { ref: editorRef, className: "uix-inline-edit__editor", role: "group", "aria-label": fillLabel(labels.edit, { label }), "aria-busy": pending || void 0, onKeyDown: onEditorKeyDown, children: [
163
- /* @__PURE__ */ jsx("div", { className: "uix-inline-edit__control", children: renderEditor ? renderEditor({ value: draft, onChange: setDraft, save, cancel, field }) : multiline ? /* @__PURE__ */ jsx(Textarea, { ...field, rows: 3, value: draft, invalid: error != null, onChange: (event) => setDraft(event.currentTarget.value) }) : /* @__PURE__ */ jsx(Input, { ...field, size: "sm", value: draft, invalid: error != null, onChange: (event) => setDraft(event.currentTarget.value) }) }),
164
- /* @__PURE__ */ jsxs("span", { className: "uix-inline-edit__actions", children: [
165
- /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "primary", loading: pending, onClick: save, children: labels.save }),
166
- /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", disabled: pending, onClick: cancel, children: labels.cancel })
167
- ] }),
168
- error != null && /* @__PURE__ */ jsx("div", { id: errorId, className: "uix-field__error uix-inline-edit__error", role: "alert", children: error })
169
- ] }),
292
+ /* @__PURE__ */ jsxs(
293
+ "div",
294
+ {
295
+ ref: editorRef,
296
+ className: "uix-inline-edit__editor",
297
+ role: "group",
298
+ "aria-label": fillLabel(labels.edit, { label }),
299
+ "aria-busy": pending || void 0,
300
+ onKeyDown: onEditorKeyDown,
301
+ onBlur: onEditorBlur,
302
+ onPointerDownCapture: onEditorPointerDown,
303
+ children: [
304
+ /* @__PURE__ */ jsx("div", { className: "uix-inline-edit__control", children: renderEditor ? renderEditor({ value: draft, onChange: change, save, cancel, field }) : multiline ? /* @__PURE__ */ jsx(Textarea, { ...field, disabled: false, rows: 3, value: textDraft, invalid: error != null, onChange: (event) => change(event.currentTarget.value) }) : /* @__PURE__ */ jsx(Input, { ...field, disabled: false, size: "sm", value: textDraft, invalid: error != null, onChange: (event) => change(event.currentTarget.value) }) }),
305
+ /* @__PURE__ */ jsxs("span", { className: "uix-inline-edit__actions", children: [
306
+ commit !== "change" && /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "primary", loading: pending, onMouseDown: (event) => event.preventDefault(), onClick: () => save(), children: labels.save }),
307
+ /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", disabled: pendingVisible, onMouseDown: (event) => event.preventDefault(), onClick: cancel, children: labels.cancel })
308
+ ] }),
309
+ errorBody != null && /* @__PURE__ */ jsx("div", { id: errorId, className: "uix-field__error uix-inline-edit__error", role: "alert", children: errorBody })
310
+ ]
311
+ }
312
+ ),
170
313
  liveRegion
171
314
  ] });
172
315
  }
@@ -2,7 +2,11 @@ import * as react from 'react';
2
2
  import { InputHTMLAttributes, ReactNode, AriaAttributes } from 'react';
3
3
 
4
4
  interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
5
- size?: 'sm' | 'md';
5
+ /**
6
+ * `sm` 28 px, `md` 36 px (default), `lg` 44 px: the touch-target floor of a public page, the
7
+ * same height as `Button size="lg"` (HAR-1812).
8
+ */
9
+ size?: 'sm' | 'md' | 'lg';
6
10
  invalid?: boolean;
7
11
  valid?: boolean;
8
12
  /**
@@ -16,6 +20,12 @@ interface InputGroupProps {
16
20
  children: ReactNode;
17
21
  leadingIcon?: ReactNode;
18
22
  trailingIcon?: ReactNode;
23
+ /**
24
+ * A control at the end of the field, inside its border: a reveal toggle, a clear or a copy
25
+ * button (HAR-1376). Give it the class `uix-input-group__action` (an icon button sized to the
26
+ * field) and an accessible name. It takes the place of `trailingIcon`.
27
+ */
28
+ trailingAction?: ReactNode;
19
29
  className?: string;
20
30
  /** Injected by `<Field>` when it wraps the group; forwarded onto the child control. */
21
31
  id?: string;
@@ -29,6 +39,6 @@ interface InputGroupProps {
29
39
  * single child element (the child's own props win) so label/message association survives
30
40
  * the extra div (UIX-A11Y-3).
31
41
  */
32
- declare function InputGroup({ children, leadingIcon, trailingIcon, className, id, 'aria-describedby': describedBy, 'aria-invalid': invalid, 'aria-required': required, }: InputGroupProps): react.JSX.Element;
42
+ declare function InputGroup({ children, leadingIcon, trailingIcon, trailingAction, className, id, 'aria-describedby': describedBy, 'aria-invalid': invalid, 'aria-required': required, }: InputGroupProps): react.JSX.Element;
33
43
 
34
44
  export { Input, InputGroup, type InputGroupProps, type InputProps };
@@ -6,7 +6,7 @@ const Input = forwardRef(
6
6
  "input",
7
7
  {
8
8
  ref,
9
- className: cx("uix-input", size === "sm" && "uix-input--sm", className),
9
+ className: cx("uix-input", size !== "md" && `uix-input--${size}`, className),
10
10
  "aria-invalid": invalid || void 0,
11
11
  "data-valid": valid || void 0,
12
12
  "data-warning": warning || void 0,
@@ -19,6 +19,7 @@ function InputGroup({
19
19
  children,
20
20
  leadingIcon,
21
21
  trailingIcon,
22
+ trailingAction,
22
23
  className,
23
24
  id,
24
25
  "aria-describedby": describedBy,
@@ -37,13 +38,14 @@ function InputGroup({
37
38
  className: cx(
38
39
  "uix-input-group",
39
40
  leadingIcon && "uix-input-group--has-leading",
40
- trailingIcon && "uix-input-group--has-trailing",
41
+ (trailingIcon || trailingAction) && "uix-input-group--has-trailing",
42
+ trailingAction && "uix-input-group--has-action",
41
43
  className
42
44
  ),
43
45
  children: [
44
46
  leadingIcon && /* @__PURE__ */ jsx("span", { className: "uix-input-group__icon", "aria-hidden": "true", children: leadingIcon }),
45
47
  control,
46
- trailingIcon && /* @__PURE__ */ jsx("span", { className: "uix-input-group__icon uix-input-group__icon--trailing", "aria-hidden": "true", children: trailingIcon })
48
+ trailingAction ?? (trailingIcon && /* @__PURE__ */ jsx("span", { className: "uix-input-group__icon uix-input-group__icon--trailing", "aria-hidden": "true", children: trailingIcon }))
47
49
  ]
48
50
  }
49
51
  );
@@ -13,9 +13,11 @@ declare function Kanban({ announcement, children, className, ...props }: KanbanP
13
13
  interface KanbanColumnProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
14
14
  title?: ReactNode;
15
15
  count?: number;
16
+ /** A dragged card would land in this column: the column body is tinted and outlined (HAR-2590). */
17
+ dropTarget?: boolean;
16
18
  children?: ReactNode;
17
19
  }
18
- declare function KanbanColumn({ title, count, children, className, ...props }: KanbanColumnProps): react.JSX.Element;
20
+ declare function KanbanColumn({ title, count, dropTarget, children, className, ...props }: KanbanColumnProps): react.JSX.Element;
19
21
  type KanbanMoveDirection = 'up' | 'down' | 'left' | 'right';
20
22
  interface KanbanCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
21
23
  title?: ReactNode;
@@ -26,8 +28,17 @@ interface KanbanCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'>
26
28
  * the board-level `announcement` so SR users hear the result.
27
29
  */
28
30
  onMove?: (dir: KanbanMoveDirection) => void;
31
+ /** The card is being dragged with the pointer: it lifts (HAR-2590). Call `settleDrop(card)` when it lands. */
32
+ dragging?: boolean;
29
33
  children?: ReactNode;
30
34
  }
31
- declare function KanbanCard({ title, meta, children, className, onMove, onKeyDown, ...props }: KanbanCardProps): react.JSX.Element;
35
+ declare function KanbanCard({ title, meta, children, className, onMove, onKeyDown, dragging, ...props }: KanbanCardProps): react.JSX.Element;
36
+ type KanbanDropLineProps = HTMLAttributes<HTMLDivElement>;
37
+ /**
38
+ * Where a dragged card would land between two cards (HAR-2590): an accent line with an end mark.
39
+ * Render it in the column at the insertion point while a drag is over it (also after the last
40
+ * card); it is decorative (the keyboard path and the board's `announcement` say where it goes).
41
+ */
42
+ declare function KanbanDropLine({ className, ...props }: KanbanDropLineProps): react.JSX.Element;
32
43
 
33
- export { Kanban, KanbanCard, type KanbanCardProps, KanbanColumn, type KanbanColumnProps, type KanbanMoveDirection, type KanbanProps };
44
+ export { Kanban, KanbanCard, type KanbanCardProps, KanbanColumn, type KanbanColumnProps, KanbanDropLine, type KanbanDropLineProps, type KanbanMoveDirection, type KanbanProps };