nexus-shared 3.2.0 → 4.0.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 (187) hide show
  1. package/CHANGELOG.md +1564 -0
  2. package/README.md +1705 -1123
  3. package/dist/Client.Index.d.ts +14 -0
  4. package/dist/Client.Index.js +14 -0
  5. package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
  6. package/dist/Components/Documents/DocumentFrame.js +83 -0
  7. package/dist/Components/Documents/Menu.d.ts +4 -0
  8. package/dist/Components/Documents/Menu.js +40 -24
  9. package/dist/Components/Documents/PrintDocument.d.ts +95 -0
  10. package/dist/Components/Documents/PrintDocument.js +62 -0
  11. package/dist/Components/Documents/Tabs.d.ts +59 -0
  12. package/dist/Components/Documents/Tabs.js +70 -0
  13. package/dist/Components/Forms/ApiForm.js +19 -2
  14. package/dist/Components/Forms/Crud.d.ts +2 -2
  15. package/dist/Components/Forms/Crud.js +576 -243
  16. package/dist/Components/Forms/CrudActions.d.ts +136 -0
  17. package/dist/Components/Forms/CrudActions.js +184 -0
  18. package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
  19. package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
  20. package/dist/Components/Forms/CrudParts.d.ts +30 -2
  21. package/dist/Components/Forms/CrudParts.js +55 -5
  22. package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
  23. package/dist/Components/Forms/CrudRowForms.js +114 -0
  24. package/dist/Components/Forms/CrudScopes.d.ts +28 -0
  25. package/dist/Components/Forms/CrudScopes.js +76 -0
  26. package/dist/Components/Forms/CrudTree.d.ts +90 -0
  27. package/dist/Components/Forms/CrudTree.js +363 -0
  28. package/dist/Components/Forms/CrudWorkspace.d.ts +63 -0
  29. package/dist/Components/Forms/CrudWorkspace.js +169 -0
  30. package/dist/Components/Forms/ExcelImport.js +3 -3
  31. package/dist/Components/Forms/Form.d.ts +4 -4
  32. package/dist/Components/Forms/Form.js +123 -17
  33. package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
  34. package/dist/Components/Forms/FormLeaveGuard.js +65 -0
  35. package/dist/Components/Forms/RecordFlows.d.ts +50 -0
  36. package/dist/Components/Forms/RecordFlows.js +282 -0
  37. package/dist/Components/Forms/SubmitForm.d.ts +37 -2
  38. package/dist/Components/Forms/SubmitForm.js +195 -10
  39. package/dist/Components/Inputs/CheckBox.js +1 -1
  40. package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
  41. package/dist/Components/Inputs/ChoiceField.js +28 -2
  42. package/dist/Components/Inputs/Dropdown.d.ts +13 -0
  43. package/dist/Components/Inputs/Dropdown.js +5 -5
  44. package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
  45. package/dist/Components/Inputs/DropdownParts.js +16 -4
  46. package/dist/Components/Inputs/Field.d.ts +7 -1
  47. package/dist/Components/Inputs/Field.js +2 -2
  48. package/dist/Components/Inputs/GroupForm.d.ts +6 -6
  49. package/dist/Components/Inputs/GroupForm.js +10 -11
  50. package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
  51. package/dist/Components/Inputs/InputRenderer.js +37 -32
  52. package/dist/Components/Inputs/PictureInput.d.ts +41 -0
  53. package/dist/Components/Inputs/PictureInput.js +70 -0
  54. package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
  55. package/dist/Components/Inputs/RadioGroup.js +8 -3
  56. package/dist/Components/Inputs/RowsInput.d.ts +1 -1
  57. package/dist/Components/Inputs/RowsInput.js +18 -3
  58. package/dist/Components/Inputs/TabularForm.d.ts +5 -5
  59. package/dist/Components/Inputs/TabularForm.js +29 -29
  60. package/dist/Components/Inputs/TextArea.d.ts +6 -0
  61. package/dist/Components/Inputs/TextArea.js +11 -2
  62. package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
  63. package/dist/Components/Inputs/TextBoxTypes.js +17 -0
  64. package/dist/Components/Inputs/TreeView.d.ts +53 -2
  65. package/dist/Components/Inputs/TreeView.js +147 -13
  66. package/dist/Components/Layouts/MessageBox.d.ts +15 -2
  67. package/dist/Components/Layouts/MessageBox.js +56 -7
  68. package/dist/Components/Layouts/Popup.d.ts +13 -6
  69. package/dist/Components/Layouts/Popup.js +37 -9
  70. package/dist/Components/Layouts/PopupDock.d.ts +3 -1
  71. package/dist/Components/Layouts/PopupDock.js +2 -1
  72. package/dist/Components/Layouts/PopupHost.js +5 -4
  73. package/dist/Components/Viewers/DataTable.js +150 -48
  74. package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
  75. package/dist/Components/Viewers/DataTableFilters.js +17 -2
  76. package/dist/Components/Viewers/DataTableParts.d.ts +21 -5
  77. package/dist/Components/Viewers/DataTableParts.js +26 -14
  78. package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
  79. package/dist/Components/Viewers/OwnerFiles.js +589 -0
  80. package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
  81. package/dist/Components/Viewers/QrCodeImage.js +15 -0
  82. package/dist/Components/Viewers/RecordCards.d.ts +16 -0
  83. package/dist/Components/Viewers/RecordCards.js +509 -0
  84. package/dist/Helpers/BusyHooks.d.ts +6 -0
  85. package/dist/Helpers/BusyHooks.js +39 -0
  86. package/dist/Helpers/CrudBackend.d.ts +20 -2
  87. package/dist/Helpers/CrudBackend.js +46 -2
  88. package/dist/Helpers/CrudColumns.d.ts +53 -6
  89. package/dist/Helpers/CrudColumns.js +98 -7
  90. package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
  91. package/dist/Helpers/CrudDetailsTabs.js +126 -0
  92. package/dist/Helpers/CrudHelpers.d.ts +81 -2
  93. package/dist/Helpers/CrudHelpers.js +195 -0
  94. package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
  95. package/dist/Helpers/CrudNarrowPick.js +157 -0
  96. package/dist/Helpers/CrudRowForms.d.ts +15 -0
  97. package/dist/Helpers/CrudRowForms.js +43 -0
  98. package/dist/Helpers/CrudScopes.d.ts +32 -0
  99. package/dist/Helpers/CrudScopes.js +79 -0
  100. package/dist/Helpers/DropdownHelpers.d.ts +3 -1
  101. package/dist/Helpers/DropdownHelpers.js +6 -2
  102. package/dist/Helpers/ExcelHelpers.d.ts +1 -1
  103. package/dist/Helpers/ExcelHelpers.js +12 -11
  104. package/dist/Helpers/FileAdapter.d.ts +78 -0
  105. package/dist/Helpers/FileAdapter.js +102 -0
  106. package/dist/Helpers/FilePrimaries.d.ts +48 -0
  107. package/dist/Helpers/FilePrimaries.js +138 -0
  108. package/dist/Helpers/FormConfirm.d.ts +32 -0
  109. package/dist/Helpers/FormConfirm.js +40 -0
  110. package/dist/Helpers/FormStore.d.ts +90 -5
  111. package/dist/Helpers/FormStore.js +318 -22
  112. package/dist/Helpers/InputConditions.d.ts +50 -0
  113. package/dist/Helpers/InputConditions.js +168 -0
  114. package/dist/Helpers/InputMemory.d.ts +106 -0
  115. package/dist/Helpers/InputMemory.js +385 -0
  116. package/dist/Helpers/InputNarrowing.d.ts +18 -0
  117. package/dist/Helpers/InputNarrowing.js +67 -0
  118. package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
  119. package/dist/Helpers/InputParamsHelpers.js +157 -74
  120. package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
  121. package/dist/Helpers/NexusDriveAdapter.js +207 -0
  122. package/dist/Helpers/NumberHelpers.d.ts +95 -0
  123. package/dist/Helpers/NumberHelpers.js +126 -0
  124. package/dist/Helpers/Permissions.js +2 -1
  125. package/dist/Helpers/QrCode.d.ts +26 -0
  126. package/dist/Helpers/QrCode.js +463 -0
  127. package/dist/Helpers/RecordCards.d.ts +205 -0
  128. package/dist/Helpers/RecordCards.js +378 -0
  129. package/dist/Helpers/RequestCache.d.ts +14 -2
  130. package/dist/Helpers/RequestCache.js +22 -3
  131. package/dist/Helpers/RowsStore.d.ts +37 -2
  132. package/dist/Helpers/RowsStore.js +128 -10
  133. package/dist/Helpers/ServerPreview.d.ts +69 -0
  134. package/dist/Helpers/ServerPreview.js +99 -0
  135. package/dist/Helpers/TableColumns.js +4 -3
  136. package/dist/Helpers/TableExport.d.ts +7 -1
  137. package/dist/Helpers/TableExport.js +9 -3
  138. package/dist/Helpers/TableFilters.d.ts +1 -1
  139. package/dist/Helpers/TableFilters.js +24 -23
  140. package/dist/Helpers/TableHelpers.d.ts +23 -2
  141. package/dist/Helpers/TableHelpers.js +51 -10
  142. package/dist/Helpers/TableJoins.d.ts +13 -0
  143. package/dist/Helpers/TableJoins.js +34 -0
  144. package/dist/Helpers/TableStore.d.ts +21 -1
  145. package/dist/Helpers/TableStore.js +64 -7
  146. package/dist/Helpers/TreeStore.d.ts +81 -0
  147. package/dist/Helpers/TreeStore.js +365 -0
  148. package/dist/Interfaces/CrudInterfaces.d.ts +465 -5
  149. package/dist/Interfaces/FileInterfaces.d.ts +162 -0
  150. package/dist/Interfaces/FileInterfaces.js +4 -0
  151. package/dist/Interfaces/FormInterfaces.d.ts +391 -30
  152. package/dist/Interfaces/FormInterfaces.js +79 -1
  153. package/dist/Interfaces/InputInterfaces.d.ts +15 -3
  154. package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
  155. package/dist/Interfaces/RecordInterfaces.js +1 -0
  156. package/dist/Interfaces/TableInterfaces.d.ts +74 -1
  157. package/dist/Services/ExcelApi.js +5 -0
  158. package/dist/Services/FileApi.d.ts +13 -0
  159. package/dist/Services/FileApi.js +29 -0
  160. package/dist/Services/ImageEditorLoader.d.ts +14 -0
  161. package/dist/Services/ImageEditorLoader.js +51 -0
  162. package/dist/Services/JoinSources.d.ts +37 -5
  163. package/dist/Services/JoinSources.js +90 -14
  164. package/dist/Services/ModuleImport.d.ts +7 -0
  165. package/dist/Services/ModuleImport.js +22 -0
  166. package/dist/Shared.Index.d.ts +16 -0
  167. package/dist/Shared.Index.js +16 -0
  168. package/package.json +7 -2
  169. package/src/Styles/Nexus.Avatar.css +6 -5
  170. package/src/Styles/Nexus.Button.css +10 -4
  171. package/src/Styles/Nexus.Choice.css +16 -0
  172. package/src/Styles/Nexus.Crud.css +383 -9
  173. package/src/Styles/Nexus.Dropdown.css +8 -0
  174. package/src/Styles/Nexus.Files.css +410 -0
  175. package/src/Styles/Nexus.Form.css +5 -2
  176. package/src/Styles/Nexus.Index.css +4 -0
  177. package/src/Styles/Nexus.Input.css +42 -0
  178. package/src/Styles/Nexus.Menu.css +10 -1
  179. package/src/Styles/Nexus.Popup.css +77 -1
  180. package/src/Styles/Nexus.Print.css +288 -0
  181. package/src/Styles/Nexus.QrCode.css +12 -0
  182. package/src/Styles/Nexus.Record.css +430 -0
  183. package/src/Styles/Nexus.Tab.Buttons.css +87 -22
  184. package/src/Styles/Nexus.Table.css +67 -5
  185. package/src/Styles/Nexus.Themes.css +12 -6
  186. package/src/Styles/Nexus.Tokens.css +8 -3
  187. package/src/Styles/Nexus.Tree.css +31 -1
@@ -0,0 +1,70 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { iconPencil, iconPhoto, iconTrash, iconUpload, InlineIcon } from "nexus-icons";
4
+ import { useId, useRef, useState } from "react";
5
+ import { cx } from "../../Helpers/ClassHelpers.js";
6
+ import { getFileAdapter } from "../../Services/FileApi.js";
7
+ import { preloadImageEditor } from "../../Services/ImageEditorLoader.js";
8
+ import { Button } from "../Documents/Button.js";
9
+ import { busyProps, openPictureGallery, useFocusThroughChange, useOwnerFiles } from "../Viewers/OwnerFiles.js";
10
+ import { ChoiceLegend, GroupFooter } from "./ChoiceField.js";
11
+ const HEIGHTS = { sm: 72, md: 112, lg: 152 };
12
+ /**
13
+ * A record's picture in a form: the owner's main picture, with Upload (a new one, edited first, made the main picture),
14
+ * Edit (the main one, saved as a new picture), Remove (no main picture; the pictures stay) and, when the category keeps
15
+ * more than one, All pictures (the gallery). In a category that keeps one file, Upload is Replace and Remove deletes it.
16
+ * Clicking or pressing the picture opens the pictures window too, read only where the input is. The change is saved at
17
+ * once, not with the form: a picture is the owner's file, not one of the record's values, so the form keeps no field for
18
+ * it and sends nothing under its name.
19
+ *
20
+ * @example
21
+ * inputs: (action, row) => [
22
+ * { type: EInputTypes.Picture, name: "logo", label: "Logo", module: NexusModule.Enterprise, category: "company-logo", ownerId: row?.id, aspect: 1 },
23
+ * ...
24
+ * ]
25
+ */
26
+ export function PictureInput({ label, hint, size = "md", disabled = false, readOnly = false, subject = "record", id, className, style, "aria-label": ariaLabel, ...params }) {
27
+ const generated = useId();
28
+ const fieldId = id ?? generated;
29
+ const [failed, setFailed] = useState(null);
30
+ const files = useOwnerFiles({ ...params, readOnly: readOnly || disabled });
31
+ const { status, data, busy, canWrite, readOnlyReason, editorProblem, actions } = files;
32
+ // A keyboard's focus stays in the input through a change: on the button pressed, or the first left when it went.
33
+ const rootRef = useRef(null);
34
+ useFocusThroughChange(rootRef, busy);
35
+ const primary = data?.files.find(file => file.isPrimary) ?? null;
36
+ const url = primary ? getFileAdapter().fileUrl(primary, "thumbnail") : null;
37
+ const shown = url && failed !== url;
38
+ const ratio = typeof params.aspect === "number" && params.aspect > 0 ? params.aspect : 1;
39
+ const preview = { "--nx-picture-h": `${HEIGHTS[size]}px`, "--nx-picture-ratio": ratio };
40
+ const unsaved = !params.ownerId;
41
+ // A category that keeps one file: a new picture takes its place, and Remove deletes it (there is nothing else to show).
42
+ const single = data?.category.maxCount === 1;
43
+ const kept = data?.files ?? [];
44
+ const note = unsaved
45
+ ? `Save the ${subject} first, then add its picture.`
46
+ : status.kind === "failed"
47
+ ? status.message
48
+ : busy
49
+ ? `${busy}…`
50
+ : editorProblem
51
+ ? // Why Upload or Edit did not open the editor, until a try opens it: never a button that does nothing.
52
+ editorProblem
53
+ : // Why it cannot be changed, when the page or the backend says (`lockReason`, a refused key): never a silent input.
54
+ (readOnlyReason ?? hint ?? (data && canWrite ? `${data.files.length} of ${data.category.maxCount} ${single ? "picture" : "pictures"} kept.` : undefined));
55
+ const problem = status.kind === "failed" || (!unsaved && !busy && Boolean(editorProblem));
56
+ const named = typeof label === "string" && label ? label : "Picture";
57
+ /** The pictures window, as All pictures opens it: what changes there is read again here. */
58
+ const openAll = () => openPictureGallery({
59
+ ...params,
60
+ ownerId: params.ownerId,
61
+ readOnly: readOnly || disabled,
62
+ onChange: primary => {
63
+ void actions.reload();
64
+ params.onChange?.(primary);
65
+ },
66
+ });
67
+ const pictureClass = cx("nx-picture", params.shape === "circle" && "nx-picture--round", !shown && "nx-picture--empty", status.kind === "loading" && "nx-picture--loading");
68
+ const face = shown ? _jsx("img", { src: url, alt: "", decoding: "async", onError: () => setFailed(url) }) : _jsx(InlineIcon, { icon: iconPhoto });
69
+ return (_jsxs("fieldset", { ref: rootRef, id: fieldId, className: cx("nx-choices", size !== "md" && `nx-choices--${size}`, "nx-picture-input", className), style: style, disabled: disabled, "aria-label": ariaLabel, "aria-busy": status.kind === "loading" || Boolean(busy) || undefined, "aria-describedby": note ? `${fieldId}-message` : undefined, "data-readonly": readOnly || undefined, children: [_jsx(ChoiceLegend, { label: label }), _jsxs("div", { className: "nx-picture-input__body", children: [!unsaved && data ? (_jsx("button", { type: "button", className: pictureClass, style: preview, "aria-label": primary ? `${named}: open the pictures` : `${named}: none yet, open the pictures`, "aria-haspopup": "dialog", title: "All pictures", ...busyProps(busy, openAll), onPointerEnter: preloadImageEditor, children: face })) : (_jsx("span", { className: pictureClass, style: preview, role: "img", "aria-label": primary ? `${named}: ${primary.name}` : "No picture", children: face })), !unsaved && data && (_jsxs("div", { className: "nx-picture-input__actions", role: "group", children: [canWrite && (_jsx(Button, { size: "sm", icon: _jsx(InlineIcon, { icon: iconUpload }), ...busyProps(busy, () => void actions.replace()), onPointerEnter: preloadImageEditor, onFocus: preloadImageEditor, children: single && kept.length > 0 ? "Replace" : "Upload" })), canWrite && primary && (_jsx(Button, { size: "sm", variant: "ghost", icon: _jsx(InlineIcon, { icon: iconPencil }), ...busyProps(busy, () => void actions.edit(primary)), onPointerEnter: preloadImageEditor, onFocus: preloadImageEditor, children: "Edit" })), canWrite && primary && (_jsx(Button, { size: "sm", variant: "ghost", hoverTone: "danger", icon: _jsx(InlineIcon, { icon: iconTrash }), ...busyProps(busy, () => void (single ? actions.remove(primary) : actions.makePrimary(null))), children: single ? "Delete" : "Remove" })), (data.category.maxCount > 1 || kept.length > 1 || (kept.length > 0 && !primary)) && (_jsxs(Button, { size: "sm", variant: "ghost", ...busyProps(busy, openAll), children: ["All pictures (", data.files.length, ")"] }))] }))] }), _jsx(GroupFooter, { id: `${fieldId}-message`, hint: problem ? undefined : note, error: problem ? note : undefined })] }));
70
+ }
@@ -1,7 +1,19 @@
1
1
  import type { ChangeEvent } from "react";
2
- import type { OptionValue } from "../../Interfaces/InputInterfaces.ts";
2
+ import type { ChoiceDesign, OptionValue } from "../../Interfaces/InputInterfaces.ts";
3
3
  import { type ChoiceGroupProps } from "./ChoiceField.tsx";
4
- export interface RadioGroupProps<T extends OptionValue = string> extends ChoiceGroupProps<T> {
4
+ /**
5
+ * How a `RadioGroup` draws its options: `default` (a circle beside each label), `cards` (a card each, with its
6
+ * description), or `tabs` (nav-style tabs on one track, as `TabButtons` draws them).
7
+ */
8
+ export type RadioGroupDesign = ChoiceDesign | "tabs";
9
+ export interface RadioGroupProps<T extends OptionValue = string> extends Omit<ChoiceGroupProps<T>, "design"> {
10
+ /**
11
+ * `default`: a circle beside each label. `cards`: each option is a card. `tabs`: the options as nav-style tabs on one
12
+ * track (`TabButtons`' look), wrapping onto more rows where they do not fit; each option's `description` is its
13
+ * tooltip and what a screen reader hears with it, and the chosen option's shows under the track. `orientation` and
14
+ * `columns` do not apply to tabs.
15
+ */
16
+ design?: RadioGroupDesign;
5
17
  /** The chosen value, or `null` for none. With `value`, the group is controlled; pair it with `onValueChange`. */
6
18
  value?: T | null;
7
19
  defaultValue?: T | null;
@@ -12,7 +24,8 @@ export interface RadioGroupProps<T extends OptionValue = string> extends ChoiceG
12
24
  }
13
25
  /**
14
26
  * One choice from a list of options, such as a room type or a payment method. Native radio buttons, so the arrow keys
15
- * move the choice. `design="cards"` shows each option as a card with its description. Read-only (default note
27
+ * move the choice. `design="cards"` shows each option as a card with its description, `design="tabs"` as nav-style tabs
28
+ * whose descriptions are tooltips (the chosen one's under the track). Read-only (default note
16
29
  * "Read-only: this choice cannot be changed.") shows a lock after the label and a note under a clicked option.
17
30
  *
18
31
  * @example <RadioGroup name="roomType" label="Room type" required design="cards" options={[{ value: "deluxe", label: "Deluxe" }]} />
@@ -1,11 +1,13 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { validateSelection } from "../../Helpers/ValidationHelpers.js";
4
- import { ChoiceItem, ChoiceLegend, GroupFooter, groupLayout, leavesField, OPTION_ATTRIBUTE, optionAnchor, useChoiceField } from "./ChoiceField.js";
4
+ import { cx } from "../../Helpers/ClassHelpers.js";
5
+ import { ChoiceItem, ChoiceLegend, ChoiceTab, ChoiceTabNotes, GroupFooter, groupLayout, leavesField, OPTION_ATTRIBUTE, optionAnchor, showChosenNote, useChoiceField } from "./ChoiceField.js";
5
6
  import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
6
7
  /**
7
8
  * One choice from a list of options, such as a room type or a payment method. Native radio buttons, so the arrow keys
8
- * move the choice. `design="cards"` shows each option as a card with its description. Read-only (default note
9
+ * move the choice. `design="cards"` shows each option as a card with its description, `design="tabs"` as nav-style tabs
10
+ * whose descriptions are tooltips (the chosen one's under the track). Read-only (default note
9
11
  * "Read-only: this choice cannot be changed.") shows a lock after the label and a note under a clicked option.
10
12
  *
11
13
  * @example <RadioGroup name="roomType" label="Room type" required design="cards" options={[{ value: "deluxe", label: "Deluxe" }]} />
@@ -14,6 +16,7 @@ export function RadioGroup(props) {
14
16
  const { options, name, label, hint, required, validate, value, defaultValue, readOnly = false, disabled, onValueChange } = props;
15
17
  const { readOnlyMessage = "Read-only: this choice cannot be changed." } = props;
16
18
  const { size = "md" } = props;
19
+ const tabs = props.design === "tabs";
17
20
  const chosen = (inputs) => {
18
21
  const input = inputs.find(item => item.checked);
19
22
  return input ? [options[Number(input.getAttribute(OPTION_ATTRIBUTE))].value] : [];
@@ -21,6 +24,8 @@ export function RadioGroup(props) {
21
24
  const field = useChoiceField(props, {
22
25
  check: (inputs, fieldName) => validateSelection(chosen(inputs), { required, validate: validate && (values => validate(values[0] ?? null)) }, fieldName),
23
26
  checkKeys: [value, required, validate, options],
27
+ // Tabs show the chosen option's description under the track: it follows every change and a form's reset.
28
+ sync: tabs ? showChosenNote : undefined,
24
29
  });
25
30
  function handleChange(event) {
26
31
  if (readOnly)
@@ -39,5 +44,5 @@ export function RadioGroup(props) {
39
44
  }
40
45
  const invalid = Boolean(field.error);
41
46
  const layout = groupLayout(props);
42
- return (_jsxs("fieldset", { ref: field.containerRef, id: field.fieldId, role: "radiogroup", className: layout.className, style: props.style, disabled: disabled, "data-invalid": invalid || undefined, "data-readonly": readOnly || undefined, "aria-label": props["aria-label"], "aria-labelledby": props["aria-labelledby"], "aria-describedby": field.error || hint ? field.messageId : undefined, "aria-required": required || undefined, "aria-invalid": invalid || undefined, "aria-readonly": readOnly || undefined, onChange: handleChange, onClick: readOnlyNotice.onClick, onKeyDown: readOnlyNotice.onKeyDown, onBlur: handleBlur, children: [_jsx(ChoiceLegend, { label: label, required: required, lock: readOnlyNotice.lock }), _jsx("div", { className: "nx-choices__options", style: layout.optionsStyle, children: options.map((option, index) => (_jsx(ChoiceItem, { type: "radio", groupId: field.fieldId, name: name ?? field.fieldId, option: option, index: index, size: size, checked: value !== undefined ? value === option.value : undefined, defaultChecked: defaultValue === option.value, invalid: invalid, readOnly: readOnly, onInvalid: field.onInvalid }, String(option.value)))) }), _jsx(GroupFooter, { id: field.messageId, hint: hint, error: field.error }), readOnlyNotice.notice] }));
47
+ return (_jsxs("fieldset", { ref: field.containerRef, id: field.fieldId, role: "radiogroup", className: layout.className, style: props.style, disabled: disabled, "data-invalid": invalid || undefined, "data-readonly": readOnly || undefined, "aria-label": props["aria-label"], "aria-labelledby": props["aria-labelledby"], "aria-describedby": field.error || hint ? field.messageId : undefined, "aria-required": required || undefined, "aria-invalid": invalid || undefined, "aria-readonly": readOnly || undefined, onChange: handleChange, onClick: readOnlyNotice.onClick, onKeyDown: readOnlyNotice.onKeyDown, onBlur: handleBlur, children: [_jsx(ChoiceLegend, { label: label, required: required, lock: readOnlyNotice.lock }), tabs ? (_jsx("div", { className: cx("nx-tabs", `nx-tabs--${size}`, "nx-tabs--wrap"), children: options.map((option, index) => (_jsx(ChoiceTab, { type: "radio", groupId: field.fieldId, name: name ?? field.fieldId, option: option, index: index, checked: value !== undefined ? value === option.value : undefined, defaultChecked: defaultValue === option.value, invalid: invalid, readOnly: readOnly, onInvalid: field.onInvalid }, String(option.value)))) })) : (_jsx("div", { className: "nx-choices__options", style: layout.optionsStyle, children: options.map((option, index) => (_jsx(ChoiceItem, { type: "radio", groupId: field.fieldId, name: name ?? field.fieldId, option: option, index: index, size: size, checked: value !== undefined ? value === option.value : undefined, defaultChecked: defaultValue === option.value, invalid: invalid, readOnly: readOnly, onInvalid: field.onInvalid }, String(option.value)))) })), tabs && _jsx(ChoiceTabNotes, { groupId: field.fieldId, options: options, chosen: options.findIndex(option => option.value === (value !== undefined ? value : defaultValue)) }), _jsx(GroupFooter, { id: field.messageId, hint: hint, error: field.error }), readOnlyNotice.notice] }));
43
48
  }
@@ -1,6 +1,6 @@
1
1
  import { type FormEvent, type ReactNode } from "react";
2
2
  import { RowsStore, type RowsSnapshot } from "../../Helpers/RowsStore.ts";
3
- import type { InputParams, RowsInputProps, RowValue } from "../../Interfaces/FormInterfaces.ts";
3
+ import { type InputParams, type RowsInputProps, type RowValue } from "../../Interfaces/FormInterfaces.ts";
4
4
  import type { InputDesign } from "../../Interfaces/InputInterfaces.ts";
5
5
  import type { Size } from "../../Interfaces/TypeInterfaces.ts";
6
6
  /** Form names of inputs inside rows that need one (radio and checkbox groups). They are taken out of the form's data. */
@@ -4,8 +4,10 @@ import { iconAlertCircle, iconArrowBackUp, iconPlus, InlineIcon } from "nexus-ic
4
4
  import { memo, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState, useSyncExternalStore, } from "react";
5
5
  import { cx } from "../../Helpers/ClassHelpers.js";
6
6
  import { useDatePreferences } from "../../Helpers/DatePreferences.js";
7
+ import { browserInputMemory, FORM_SAVED_EVENT } from "../../Helpers/InputMemory.js";
7
8
  import { inputFieldName } from "../../Helpers/InputParamsHelpers.js";
8
9
  import { RowsStore } from "../../Helpers/RowsStore.js";
10
+ import { EInputTypes } from "../../Interfaces/FormInterfaces.js";
9
11
  import { Button } from "../Documents/Button.js";
10
12
  import { hasContent } from "./Field.js";
11
13
  import { isFirstInvalid } from "./InputField.js";
@@ -64,7 +66,14 @@ export function useRowsInput(props, fallbackItem, layoutRef) {
64
66
  });
65
67
  }, [store, syncValidity]);
66
68
  useEffect(syncValidity);
67
- // Inside a form: the rows go into its data as JSON under `name`, and a reset brings back the rows loaded last.
69
+ // Remembered row inputs (`remember`) read and keep their values in this browser: rows added from now on open on them.
70
+ // Given once drawn, so the server's draw and the first one in the browser are the same.
71
+ useLayoutEffect(() => {
72
+ if (inputs.some(params => params.remember))
73
+ store.attachMemory(browserInputMemory());
74
+ }, [store, inputs]);
75
+ // Inside a form: the rows go into its data as JSON under `name`, a reset brings back the rows loaded last, and a save
76
+ // (a SubmitForm's) keeps the remembered values of the row added last.
68
77
  useEffect(() => {
69
78
  const form = proxyRef.current?.form;
70
79
  if (!form)
@@ -79,11 +88,17 @@ export function useRowsInput(props, fallbackItem, layoutRef) {
79
88
  };
80
89
  // The reset event comes before the browser restores the fields.
81
90
  const onReset = () => setTimeout(() => store.reset());
91
+ const onSaved = () => {
92
+ if (!latest.current.disabled)
93
+ store.rememberAdded();
94
+ };
82
95
  form.addEventListener("formdata", onFormData);
83
96
  form.addEventListener("reset", onReset);
97
+ form.addEventListener(FORM_SAVED_EVENT, onSaved);
84
98
  return () => {
85
99
  form.removeEventListener("formdata", onFormData);
86
100
  form.removeEventListener("reset", onReset);
101
+ form.removeEventListener(FORM_SAVED_EVENT, onSaved);
87
102
  };
88
103
  }, [store, props.form, name]);
89
104
  function focusProblem(place) {
@@ -236,7 +251,7 @@ export function labelText(params) {
236
251
  export const RowCell = memo(function RowCell({ store, rowKey, params, index, error, layout, idPrefix, size, design, readOnly, disabled, lock }) {
237
252
  const [initial] = useState(() => store.get(rowKey, params.name));
238
253
  const onValueChange = useCallback((next) => store.set(rowKey, params.name, next, "input"), [store, rowKey, params.name]);
239
- const needsName = params.type === "radio-group" || params.type === "checkbox-group";
254
+ const needsName = params.type === EInputTypes.RadioGroup || params.type === EInputTypes.CheckBoxGroup;
240
255
  const overrides = {
241
256
  id: `${idPrefix}-${rowKey.replace(/[^A-Za-z0-9_-]/g, "_")}-${params.name}`,
242
257
  // Nothing goes into a parent form by itself: the rows go in whole, under the rows input's name.
@@ -257,7 +272,7 @@ export const RowCell = memo(function RowCell({ store, rowKey, params, index, err
257
272
  if (error !== undefined)
258
273
  overrides.error = error;
259
274
  // A required radio means one row has it on, which the rows check; each radio alone is not required.
260
- if (params.type === "radio")
275
+ if (params.type === EInputTypes.Radio)
261
276
  overrides.required = false;
262
277
  if (layout === "tabular") {
263
278
  overrides.label = undefined;
@@ -1,4 +1,4 @@
1
- import type { RowValue, TabularFormProps } from "../../Interfaces/FormInterfaces.ts";
1
+ import { type RowValue, type TabularFormProps } from "../../Interfaces/FormInterfaces.ts";
2
2
  /**
3
3
  * Rows of inputs in a table, one column per input: a person's emails or addresses, invoice lines, a salary sheet. The
4
4
  * value is an array of row objects, one value per input, with any other fields the rows came with (such as ids). Inputs
@@ -18,10 +18,10 @@ import type { RowValue, TabularFormProps } from "../../Interfaces/FormInterfaces
18
18
  *
19
19
  * @example
20
20
  * <TabularForm name="emails" label="Emails" itemLabel="Email" max={5} unique={["email"]} inputs={[
21
- * { type: "email", name: "email", label: "Email", required: true, pattern: TEXT_PATTERNS.email },
22
- * { type: "dropdown", name: "type", label: "Type", options: EMAIL_TYPES },
23
- * { type: "checkbox", name: "isVerified", label: "Verified" },
24
- * { type: "radio", name: "isPrimary", label: "Primary", required: true },
21
+ * { type: EInputTypes.Email, name: "email", label: "Email", required: true, pattern: TEXT_PATTERNS.email },
22
+ * { type: EInputTypes.Dropdown, name: "type", label: "Type", options: EMAIL_TYPES },
23
+ * { type: EInputTypes.CheckBox, name: "isVerified", label: "Verified" },
24
+ * { type: EInputTypes.Radio, name: "isPrimary", label: "Primary", required: true },
25
25
  * ]} />
26
26
  */
27
27
  export declare function TabularForm<TRow extends RowValue = RowValue>(props: TabularFormProps<TRow>): import("react").JSX.Element;
@@ -3,6 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { iconArrowDown, iconArrowUp, iconCopy, iconDotsVertical, iconLock, iconLockOpen, iconRowInsertBottom, iconRowInsertTop, iconTrash, InlineIcon } from "nexus-icons";
4
4
  import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState, } from "react";
5
5
  import { cx } from "../../Helpers/ClassHelpers.js";
6
+ import { isBooleanInput } from "../../Helpers/InputParamsHelpers.js";
7
+ import { EInputTypes } from "../../Interfaces/FormInterfaces.js";
6
8
  import { Menu } from "../Documents/Menu.js";
7
9
  import { firstFocusable, labelText, RowCell, RowsFooter, RowsLegend, useRowsFrame, useRowsInput, useRowVersion } from "./RowsInput.js";
8
10
  /** Height of a row: the input's box and the line under it. Measured from the first row once it renders. */
@@ -12,38 +14,36 @@ const OVERSCAN = 8;
12
14
  const DEFAULT_MAX_HEIGHT = 440;
13
15
  /** A column's width by input type, when its params give none. */
14
16
  const COLUMN_WIDTH = {
15
- text: "10rem",
16
- email: "13rem",
17
- password: "10rem",
18
- search: "10rem",
19
- tel: "9rem",
20
- url: "13rem",
21
- textarea: "14rem",
22
- number: "7.5rem",
23
- checkbox: "6rem",
24
- switch: "6rem",
25
- radio: "6rem",
26
- "checkbox-group": "16rem",
27
- "radio-group": "16rem",
28
- dropdown: "10rem",
29
- date: "10rem",
30
- "date-range": "15rem",
31
- time: "8rem",
32
- "date-time": "14rem",
33
- slider: "10rem",
34
- "range-slider": "12rem",
17
+ [EInputTypes.Text]: "10rem",
18
+ [EInputTypes.Email]: "13rem",
19
+ [EInputTypes.Password]: "10rem",
20
+ [EInputTypes.Search]: "10rem",
21
+ [EInputTypes.Tel]: "9rem",
22
+ [EInputTypes.Url]: "13rem",
23
+ [EInputTypes.TextArea]: "14rem",
24
+ [EInputTypes.Number]: "7.5rem",
25
+ [EInputTypes.CheckBox]: "6rem",
26
+ [EInputTypes.Switch]: "6rem",
27
+ [EInputTypes.Radio]: "6rem",
28
+ [EInputTypes.CheckBoxGroup]: "16rem",
29
+ [EInputTypes.RadioGroup]: "16rem",
30
+ [EInputTypes.Dropdown]: "10rem",
31
+ [EInputTypes.Date]: "10rem",
32
+ [EInputTypes.DateRange]: "15rem",
33
+ [EInputTypes.Time]: "8rem",
34
+ [EInputTypes.DateTime]: "14rem",
35
+ [EInputTypes.Slider]: "10rem",
36
+ [EInputTypes.RangeSlider]: "12rem",
35
37
  };
36
- /** Columns that keep their width instead of sharing the spare room. */
37
- const FIXED_TYPES = new Set(["checkbox", "switch", "radio"]);
38
38
  /** What a locked row's cells say when one is used. */
39
39
  const LOCKED_ROW = "Locked: unlock the row to change it.";
40
40
  const ENTER_MOVES = new Set(["text", "email", "tel", "url", "search", "password", "number"]);
41
41
  const cssLength = (value) => (typeof value === "number" ? `${value}px` : value);
42
42
  /** How a cell lines up: numbers at the end, checkboxes and radios in the middle, the rest at the start. */
43
43
  function cellKind(params) {
44
- if (params.type === "number")
44
+ if (params.type === EInputTypes.Number)
45
45
  return "number";
46
- if (FIXED_TYPES.has(params.type))
46
+ if (isBooleanInput(params))
47
47
  return "check";
48
48
  return "text";
49
49
  }
@@ -105,10 +105,10 @@ const TabularRow = memo(function TabularRow({ store, rowKey, index, count, colum
105
105
  *
106
106
  * @example
107
107
  * <TabularForm name="emails" label="Emails" itemLabel="Email" max={5} unique={["email"]} inputs={[
108
- * { type: "email", name: "email", label: "Email", required: true, pattern: TEXT_PATTERNS.email },
109
- * { type: "dropdown", name: "type", label: "Type", options: EMAIL_TYPES },
110
- * { type: "checkbox", name: "isVerified", label: "Verified" },
111
- * { type: "radio", name: "isPrimary", label: "Primary", required: true },
108
+ * { type: EInputTypes.Email, name: "email", label: "Email", required: true, pattern: TEXT_PATTERNS.email },
109
+ * { type: EInputTypes.Dropdown, name: "type", label: "Type", options: EMAIL_TYPES },
110
+ * { type: EInputTypes.CheckBox, name: "isVerified", label: "Verified" },
111
+ * { type: EInputTypes.Radio, name: "isPrimary", label: "Primary", required: true },
112
112
  * ]} />
113
113
  */
114
114
  export function TabularForm(props) {
@@ -359,7 +359,7 @@ export function TabularForm(props) {
359
359
  const template = [
360
360
  numbered ? numberWidth : null,
361
361
  ...columns.map((params, index) => {
362
- const grow = params.grow ?? (FIXED_TYPES.has(params.type) ? 0 : 1);
362
+ const grow = params.grow ?? (isBooleanInput(params) ? 0 : 1);
363
363
  return grow > 0 ? `minmax(${widths[index]}, ${grow}fr)` : widths[index];
364
364
  }),
365
365
  actionsWidth,
@@ -22,6 +22,12 @@ export interface TextAreaProps extends NativeTextAreaProps, TextRules {
22
22
  maxRows?: number;
23
23
  /** Shows the character count, and the limit when `maxLength` is set. */
24
24
  showCount?: boolean;
25
+ /**
26
+ * Text that is code (HTML, JSON): the monospace font, lines kept as typed (no wrapping, a long line scrolls sideways
27
+ * inside the box), a tab two spaces wide, and no spelling, capitals, corrections, suggestions or translation. Tab still
28
+ * leaves the box, so a keyboard is never trapped in it.
29
+ */
30
+ code?: boolean;
25
31
  /** What a read-only field says when someone tries to change it. Default "Read-only: this field cannot be changed." */
26
32
  readOnlyMessage?: string;
27
33
  /** Name used in messages. Default: the label, when it is text. */
@@ -5,13 +5,22 @@ import { Field } from "./Field.js";
5
5
  import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
6
6
  import { useTextField } from "./TextField.js";
7
7
  import { useInputDesign } from "../../Helpers/UserPreferences.js";
8
+ /** A `code` text area's attributes: lines kept as typed, and nothing that would change what was typed. A page's own props win. */
9
+ const CODE_PROPS = {
10
+ wrap: "off",
11
+ spellCheck: false,
12
+ autoCapitalize: "off",
13
+ autoCorrect: "off",
14
+ autoComplete: "off",
15
+ translate: "no",
16
+ };
8
17
  /**
9
18
  * A multi-line text input with the same label, hint, validation, and designs as `TextBox`.
10
19
  *
11
20
  * @example <TextArea label="Description" maxLength={500} showCount autoResize />
12
21
  */
13
22
  export function TextArea(props) {
14
- const { label, hint, error, design: ownDesign, size = "md", icon, rows = 3, autoResize = false, maxRows = 12, showCount = false, readOnlyMessage = "Read-only: this field cannot be changed.", fieldName, validateOn, onValueChange, className, textAreaClassName, style, required, minLength, maxLength, pattern, patternMessage, validate, placeholder, disabled, readOnly, ref, onChange, onBlur, onInvalid, onKeyDown, onClick, ...textAreaProps } = props;
23
+ const { label, hint, error, design: ownDesign, size = "md", icon, rows = 3, autoResize = false, maxRows = 12, showCount = false, code = false, readOnlyMessage = "Read-only: this field cannot be changed.", fieldName, validateOn, onValueChange, className, textAreaClassName, style, required, minLength, maxLength, pattern, patternMessage, validate, placeholder, disabled, readOnly, ref, onChange, onBlur, onInvalid, onKeyDown, onClick, ...textAreaProps } = props;
15
24
  // The person's input design (the quick preferences) when this page and its form name none, for a labelled input.
16
25
  const design = useInputDesign(ownDesign, Boolean(label));
17
26
  const field = useTextField(props);
@@ -30,5 +39,5 @@ export function TextArea(props) {
30
39
  readOnlyNotice.hide();
31
40
  field.controlProps.onBlur(event);
32
41
  }
33
- return (_jsxs(_Fragment, { children: [_jsx(Field, { controlId: field.controlId, messageId: field.messageId, label: label, required: required, design: design, size: size, hint: hint, error: field.error, icon: icon, actions: readOnlyNotice.lock, aside: field.count, disabled: disabled, readOnly: readOnly, raiseLabel: Boolean(placeholder), multiline: true, className: className, style: { ...rowsStyle, ...style }, children: _jsx("textarea", { ...textAreaProps, ...field.controlProps, className: cx("nx-field__control", autoResize && "nx-field__control--auto", textAreaClassName), rows: rows, placeholder: placeholder || " ", disabled: disabled, readOnly: readOnly, onClick: handleClick, onBlur: handleBlur, onKeyDown: handleKeyDown }) }), readOnlyNotice.notice] }));
42
+ return (_jsxs(_Fragment, { children: [_jsx(Field, { controlId: field.controlId, messageId: field.messageId, label: label, required: required, design: design, size: size, hint: hint, error: field.error, icon: icon, actions: readOnlyNotice.lock, aside: field.count, disabled: disabled, readOnly: readOnly, raiseLabel: Boolean(placeholder), multiline: true, className: className, style: { ...rowsStyle, ...style }, children: _jsx("textarea", { ...(code ? CODE_PROPS : null), ...textAreaProps, ...field.controlProps, className: cx("nx-field__control", autoResize && "nx-field__control--auto", code && "nx-field__control--code", textAreaClassName), rows: rows, placeholder: placeholder || " ", disabled: disabled, readOnly: readOnly, onClick: handleClick, onBlur: handleBlur, onKeyDown: handleKeyDown }) }), readOnlyNotice.notice] }));
34
43
  }
@@ -0,0 +1,6 @@
1
+ import { EInputTypes, type TextInputType } from "../../Interfaces/FormInterfaces.ts";
2
+ import type { TextBoxType } from "./TextBox.tsx";
3
+ /** The HTML type of each text box kind. */
4
+ export declare const TEXT_BOX_TYPES: Readonly<Record<TextInputType, TextBoxType>>;
5
+ /** Whether a `TextBox` renders this input type, e.g. `true` for `EInputTypes.Email`, `false` for `EInputTypes.TextArea`. */
6
+ export declare const isTextInputType: (type: EInputTypes) => type is TextInputType;
@@ -0,0 +1,17 @@
1
+ import { EInputTypes } from "../../Interfaces/FormInterfaces.js";
2
+ /*
3
+ * Which input types a `TextBox` renders, and the HTML type each one gets: the one place an input type becomes a string.
4
+ * `renderInput` reads it; it is its own file, not exported from the package, so the unit suites can check it without
5
+ * a browser (scripts/test-form-store.mts).
6
+ */
7
+ /** The HTML type of each text box kind. */
8
+ export const TEXT_BOX_TYPES = {
9
+ [EInputTypes.Text]: "text",
10
+ [EInputTypes.Email]: "email",
11
+ [EInputTypes.Password]: "password",
12
+ [EInputTypes.Search]: "search",
13
+ [EInputTypes.Tel]: "tel",
14
+ [EInputTypes.Url]: "url",
15
+ };
16
+ /** Whether a `TextBox` renders this input type, e.g. `true` for `EInputTypes.Email`, `false` for `EInputTypes.TextArea`. */
17
+ export const isTextInputType = (type) => Object.hasOwn(TEXT_BOX_TYPES, type);
@@ -1,7 +1,17 @@
1
1
  import { type CSSProperties, type ReactNode, type Ref } from "react";
2
- import { type TreeChildrenPage, type TreeChildrenRequest, type TreeItem, type TreeValue, type TreeValueMode } from "../../Helpers/TreeStore.ts";
2
+ import { type TreeChangeRules, type TreeChildrenPage, type TreeChildrenRequest, type TreeItem, type TreeItemChange, type TreeValue, type TreeValueMode } from "../../Helpers/TreeStore.ts";
3
+ import type { TableRowAction } from "../../Interfaces/TableInterfaces.ts";
3
4
  import type { Size } from "../../Interfaces/TypeInterfaces.ts";
4
5
  export type TreeAction = "create" | "update" | "delete";
6
+ /**
7
+ * An action of the page's own on one item: an icon button on its row when `inline` (shown under the pointer or with
8
+ * focus, always on a touch screen), and an entry of the row's context menu either way. The row is busy while a
9
+ * promise `onSelect` returns runs.
10
+ */
11
+ export interface TreeRowAction<TItem extends TreeItem = TreeItem> extends TableRowAction<TItem> {
12
+ /** The key that runs it on the focused row, as Insert adds, F2 renames and Delete deletes in a tree that edits its own items. */
13
+ shortcut?: "Insert" | "F2" | "Delete";
14
+ }
5
15
  export interface TreeCreateRequest<TItem extends TreeItem = TreeItem> {
6
16
  /** The item the new one goes under, or `null` at the top level. */
7
17
  parent: TItem | null;
@@ -31,7 +41,10 @@ export interface TreeViewHandle<TItem extends TreeItem = TreeItem> {
31
41
  /** Expands an item and its parents. */
32
42
  expand: (value: TreeValue) => void;
33
43
  collapse: (value: TreeValue) => void;
34
- /** Expands the parents of an item, scrolls to it, and focuses it unless `focus` is false. */
44
+ /**
45
+ * Expands the parents of an item, scrolls to it, and focuses it unless `focus` is false. An item not loaded yet is
46
+ * revealed when the page that brings it comes (opened by `expand`, or Load more), unless the user moves on first.
47
+ */
35
48
  reveal: (value: TreeValue, focus?: boolean) => void;
36
49
  getValue: () => TreeValue[];
37
50
  /** Checks exactly these values, locked items included. For a tree without `value`. */
@@ -40,6 +53,8 @@ export interface TreeViewHandle<TItem extends TreeItem = TreeItem> {
40
53
  checkAll: (checked: boolean) => void;
41
54
  /** Every item in tree order, with the changes made in the tree. */
42
55
  getItems: () => TItem[];
56
+ /** The item of a value, as the tree holds it now; `undefined` when it is not loaded. */
57
+ getItem: (value: TreeValue) => TItem | undefined;
43
58
  /** Opens the editor for a new item under `parentValue`, or at the top level for `null`. */
44
59
  startCreate: (parentValue: TreeValue | null) => void;
45
60
  startRename: (value: TreeValue) => void;
@@ -53,6 +68,12 @@ export interface TreeViewHandle<TItem extends TreeItem = TreeItem> {
53
68
  * when it scrolls in, a closed one when it is next expanded. What is drawn stays until the fresh pages replace it.
54
69
  */
55
70
  refresh: (value?: TreeValue) => void;
71
+ /**
72
+ * Writes changes made elsewhere into this tree, as `writeTreeChanges` does into every tree with a `cacheKey` (which
73
+ * calls this): items placed, moved and taken out, the parents' counts and chevrons, and focus kept on its row, or on the
74
+ * row in its place when the item left. `rules` gives `compare`, the order of siblings.
75
+ */
76
+ apply: (changes: readonly TreeItemChange<TItem>[], rules?: TreeChangeRules<TItem>) => void;
56
77
  }
57
78
  export interface TreeViewProps<TItem extends TreeItem = TreeItem> {
58
79
  /**
@@ -159,6 +180,30 @@ export interface TreeViewProps<TItem extends TreeItem = TreeItem> {
159
180
  onItemsChange?: (items: TItem[]) => void;
160
181
  /** Asks before deleting. Default true; needs `<PopupHost />` on the page. */
161
182
  confirmDelete?: boolean;
183
+ /**
184
+ * Actions of the page's own on each item - Add under it, Edit, Move to trash - for a tree whose items are records
185
+ * changed through forms rather than renamed in the row: icon buttons on the row for the `inline` ones, all of them in
186
+ * the row's context menu, and the key each names. Not offered on locked items, nor in a read-only or disabled tree.
187
+ */
188
+ rowActions?: (item: TItem) => readonly TreeRowAction<TItem>[];
189
+ /**
190
+ * A button of the page's own at the end of the toolbar, such as Add New for a form. Its `description` is its tooltip
191
+ * and what a screen reader hears after its name. `disabled` with a `description` says why it cannot be pressed yet: it
192
+ * stays in the Tab order, looks unavailable, and a press - a click, a tap, Enter - shows the description in a note under
193
+ * it rather than doing nothing.
194
+ */
195
+ add?: {
196
+ label: ReactNode;
197
+ onClick: () => void;
198
+ disabled?: boolean;
199
+ description?: string;
200
+ };
201
+ /**
202
+ * The row shown as selected: the item the page picked - through `onItemClick`, say - marked `aria-selected` with a tint
203
+ * that stays when focus moves on. The page decides it: moving focus, a chevron, and opening or closing a row leave it
204
+ * where it is. Not on a `checkable` tree, whose rows are checked instead.
205
+ */
206
+ selectedValue?: TreeValue | null;
162
207
  /** The longest name the editor takes. Default 100. */
163
208
  maxLabelLength?: number;
164
209
  /** A click on a row without a checkbox, or Enter on it. Without it, a click expands or collapses a parent. */
@@ -173,6 +218,12 @@ export interface TreeViewProps<TItem extends TreeItem = TreeItem> {
173
218
  height?: number | string;
174
219
  /** The tallest the rows' area grows before it scrolls. Default 420. */
175
220
  maxHeight?: number | string;
221
+ /**
222
+ * Takes the height it is given, as a data table's `fill` does: the toolbar stays on top and the rows scroll inside
223
+ * under it, so the page around it never has to. `height` and `maxHeight` are ignored while it is on. Give it a cell
224
+ * that has a height: a side of an `AdminSplit` on an `AdminPage` with `fill` (nexus-admin), or a parent of your own.
225
+ */
226
+ fill?: boolean;
176
227
  /** Can be browsed, expanded, and focused, but not changed. A lock follows the label, and a try shows a note. */
177
228
  readOnly?: boolean;
178
229
  /** What a read-only tree says when someone tries to change it. Default "Read-only: this tree cannot be changed." */