nexus-shared 3.0.0 → 3.2.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 (162) hide show
  1. package/CHANGELOG.md +442 -0
  2. package/README.md +1123 -1000
  3. package/dist/Client.Index.d.ts +6 -0
  4. package/dist/Client.Index.js +6 -0
  5. package/dist/Components/Chats/ChatParts.d.ts +2 -2
  6. package/dist/Components/Chats/ChatParts.js +3 -7
  7. package/dist/Components/Chats/ChatThread.js +3 -3
  8. package/dist/Components/Documents/Button.d.ts +6 -1
  9. package/dist/Components/Documents/Button.js +2 -2
  10. package/dist/Components/Documents/Menu.js +21 -9
  11. package/dist/Components/Documents/SplitButton.d.ts +3 -1
  12. package/dist/Components/Documents/SplitButton.js +7 -5
  13. package/dist/Components/Documents/TabButtons.d.ts +5 -2
  14. package/dist/Components/Documents/TabButtons.js +2 -2
  15. package/dist/Components/Forms/ApiForm.d.ts +1 -1
  16. package/dist/Components/Forms/ApiForm.js +1 -1
  17. package/dist/Components/Forms/Crud.js +486 -206
  18. package/dist/Components/Forms/CrudHistory.d.ts +66 -0
  19. package/dist/Components/Forms/CrudHistory.js +140 -0
  20. package/dist/Components/Forms/CrudParts.d.ts +5 -1
  21. package/dist/Components/Forms/CrudParts.js +38 -5
  22. package/dist/Components/Forms/ExcelImport.d.ts +23 -7
  23. package/dist/Components/Forms/ExcelImport.js +189 -60
  24. package/dist/Components/Forms/Form.js +9 -14
  25. package/dist/Components/Forms/SubmitForm.d.ts +11 -2
  26. package/dist/Components/Forms/SubmitForm.js +23 -12
  27. package/dist/Components/Inputs/CodeBox.d.ts +69 -0
  28. package/dist/Components/Inputs/CodeBox.js +316 -0
  29. package/dist/Components/Inputs/DatePicker.js +4 -1
  30. package/dist/Components/Inputs/DateRangePicker.js +4 -1
  31. package/dist/Components/Inputs/DateTimePicker.js +4 -1
  32. package/dist/Components/Inputs/Dropdown.js +4 -1
  33. package/dist/Components/Inputs/DropdownParts.d.ts +1 -1
  34. package/dist/Components/Inputs/DropdownParts.js +4 -6
  35. package/dist/Components/Inputs/InputRenderer.js +3 -0
  36. package/dist/Components/Inputs/NumberBox.js +26 -2
  37. package/dist/Components/Inputs/RowsInput.js +2 -2
  38. package/dist/Components/Inputs/Slider.js +4 -1
  39. package/dist/Components/Inputs/TabularForm.js +42 -10
  40. package/dist/Components/Inputs/TextArea.js +4 -1
  41. package/dist/Components/Inputs/TextBox.js +4 -1
  42. package/dist/Components/Inputs/TimePanel.js +4 -3
  43. package/dist/Components/Inputs/TimePicker.js +4 -1
  44. package/dist/Components/Inputs/TreeView.d.ts +55 -5
  45. package/dist/Components/Inputs/TreeView.js +372 -23
  46. package/dist/Components/Layouts/Popup.js +1 -4
  47. package/dist/Components/Layouts/ThemePicker.d.ts +21 -3
  48. package/dist/Components/Layouts/ThemePicker.js +89 -6
  49. package/dist/Components/Layouts/ThemeScript.d.ts +4 -2
  50. package/dist/Components/Layouts/ThemeScript.js +4 -2
  51. package/dist/Components/Layouts/ThemeSwitcher.d.ts +6 -3
  52. package/dist/Components/Layouts/ThemeSwitcher.js +9 -8
  53. package/dist/Components/Viewers/Avatar.d.ts +21 -0
  54. package/dist/Components/Viewers/Avatar.js +21 -0
  55. package/dist/Components/Viewers/DataTable.js +252 -62
  56. package/dist/Components/Viewers/DataTableColumns.d.ts +7 -3
  57. package/dist/Components/Viewers/DataTableColumns.js +25 -68
  58. package/dist/Components/Viewers/DataTableFilters.d.ts +21 -0
  59. package/dist/Components/Viewers/DataTableFilters.js +68 -0
  60. package/dist/Components/Viewers/DataTableParts.d.ts +28 -10
  61. package/dist/Components/Viewers/DataTableParts.js +42 -13
  62. package/dist/Helpers/AnimationHelpers.d.ts +11 -0
  63. package/dist/Helpers/AnimationHelpers.js +20 -2
  64. package/dist/Helpers/ApiModules.d.ts +3 -34
  65. package/dist/Helpers/ApiModules.js +3 -3
  66. package/dist/Helpers/ApiRoutes.d.ts +68 -26
  67. package/dist/Helpers/ApiRoutes.js +31 -27
  68. package/dist/Helpers/ArrayHelpers.d.ts +0 -0
  69. package/dist/Helpers/ArrayHelpers.js +2 -0
  70. package/dist/Helpers/CodeHelpers.d.ts +97 -0
  71. package/dist/Helpers/CodeHelpers.js +332 -0
  72. package/dist/Helpers/CrudBackend.d.ts +101 -8
  73. package/dist/Helpers/CrudBackend.js +172 -11
  74. package/dist/Helpers/CrudColumns.d.ts +51 -0
  75. package/dist/Helpers/CrudColumns.js +69 -0
  76. package/dist/Helpers/CrudHelpers.d.ts +50 -5
  77. package/dist/Helpers/CrudHelpers.js +109 -12
  78. package/dist/Helpers/ExcelBackend.d.ts +3 -3
  79. package/dist/Helpers/ExcelBackend.js +8 -2
  80. package/dist/Helpers/ExcelHelpers.d.ts +36 -8
  81. package/dist/Helpers/ExcelHelpers.js +55 -13
  82. package/dist/Helpers/FormStore.d.ts +9 -5
  83. package/dist/Helpers/FormStore.js +25 -5
  84. package/dist/Helpers/InputParamsHelpers.js +8 -0
  85. package/dist/Helpers/MessageBuilder.js +3 -0
  86. package/dist/Helpers/NexusHelpers.d.ts +0 -0
  87. package/dist/Helpers/NexusHelpers.js +1 -0
  88. package/dist/Helpers/NumberHelpers.d.ts +7 -1
  89. package/dist/Helpers/NumberHelpers.js +41 -3
  90. package/dist/Helpers/PopoverHelpers.d.ts +17 -5
  91. package/dist/Helpers/PopoverHelpers.js +52 -17
  92. package/dist/Helpers/PopoverHooks.d.ts +24 -0
  93. package/dist/Helpers/PopoverHooks.js +62 -0
  94. package/dist/Helpers/RequestCache.d.ts +35 -0
  95. package/dist/Helpers/RequestCache.js +113 -0
  96. package/dist/Helpers/RowsStore.d.ts +7 -2
  97. package/dist/Helpers/RowsStore.js +15 -3
  98. package/dist/Helpers/TableFilters.d.ts +25 -0
  99. package/dist/Helpers/TableFilters.js +162 -0
  100. package/dist/Helpers/TableHelpers.d.ts +7 -4
  101. package/dist/Helpers/TableHelpers.js +15 -6
  102. package/dist/Helpers/TableJoins.d.ts +70 -0
  103. package/dist/Helpers/TableJoins.js +250 -0
  104. package/dist/Helpers/TableStore.d.ts +16 -3
  105. package/dist/Helpers/TableStore.js +25 -5
  106. package/dist/Helpers/ThemeHelpers.d.ts +21 -4
  107. package/dist/Helpers/ThemeHelpers.js +46 -9
  108. package/dist/Helpers/TreeStore.d.ts +225 -11
  109. package/dist/Helpers/TreeStore.js +683 -90
  110. package/dist/Helpers/UserPreferences.d.ts +22 -0
  111. package/dist/Helpers/UserPreferences.js +116 -0
  112. package/dist/Interfaces/CrudInterfaces.d.ts +67 -10
  113. package/dist/Interfaces/ExcelInterfaces.d.ts +12 -0
  114. package/dist/Interfaces/FormInterfaces.d.ts +37 -6
  115. package/dist/Interfaces/InputInterfaces.d.ts +24 -0
  116. package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
  117. package/dist/Interfaces/PreferenceInterfaces.d.ts +51 -0
  118. package/dist/Interfaces/PreferenceInterfaces.js +25 -0
  119. package/dist/Interfaces/TableInterfaces.d.ts +145 -16
  120. package/dist/Interfaces/ThemeInterfaces.d.ts +25 -0
  121. package/dist/Interfaces/ThemeInterfaces.js +15 -0
  122. package/dist/Services/ApiProxy.d.ts +36 -1
  123. package/dist/Services/ApiProxy.js +182 -52
  124. package/dist/Services/ApiProxyRules.d.ts +203 -0
  125. package/dist/Services/ApiProxyRules.js +426 -0
  126. package/dist/Services/ExcelApi.d.ts +3 -1
  127. package/dist/Services/ExcelApi.js +1 -1
  128. package/dist/Services/JoinSources.d.ts +48 -0
  129. package/dist/Services/JoinSources.js +74 -0
  130. package/dist/Services/ServerApi.d.ts +13 -2
  131. package/dist/Services/ServerApi.js +8 -3
  132. package/dist/Services/ThemeService.d.ts +18 -2
  133. package/dist/Services/ThemeService.js +80 -16
  134. package/dist/Shared.Index.d.ts +6 -0
  135. package/dist/Shared.Index.js +6 -0
  136. package/package.json +4 -2
  137. package/src/Styles/Nexus.Avatar.css +79 -0
  138. package/src/Styles/Nexus.Base.css +51 -4
  139. package/src/Styles/Nexus.Button.css +27 -0
  140. package/src/Styles/Nexus.Calendar.css +4 -6
  141. package/src/Styles/Nexus.Chat.css +5 -51
  142. package/src/Styles/Nexus.Choice.css +4 -6
  143. package/src/Styles/Nexus.Crud.css +16 -9
  144. package/src/Styles/Nexus.Dropdown.css +12 -61
  145. package/src/Styles/Nexus.Editor.css +4 -6
  146. package/src/Styles/Nexus.Excel.css +34 -15
  147. package/src/Styles/Nexus.Form.css +25 -67
  148. package/src/Styles/Nexus.Index.css +1 -0
  149. package/src/Styles/Nexus.Input.css +794 -637
  150. package/src/Styles/Nexus.Markdown.css +0 -28
  151. package/src/Styles/Nexus.Menu.css +8 -30
  152. package/src/Styles/Nexus.Popup.css +22 -29
  153. package/src/Styles/Nexus.RichText.css +2 -32
  154. package/src/Styles/Nexus.Rows.css +51 -10
  155. package/src/Styles/Nexus.Slider.css +3 -5
  156. package/src/Styles/Nexus.Tab.Buttons.css +20 -7
  157. package/src/Styles/Nexus.Table.css +1387 -1296
  158. package/src/Styles/Nexus.Theme.Picker.css +342 -232
  159. package/src/Styles/Nexus.Time.css +9 -12
  160. package/src/Styles/Nexus.Toast.css +6 -8
  161. package/src/Styles/Nexus.Tokens.css +17 -0
  162. package/src/Styles/Nexus.Tree.css +76 -11
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { iconArrowDown, iconArrowUp, iconCopy, iconDotsVertical, iconLock, iconRowInsertBottom, iconRowInsertTop, iconTrash, InlineIcon } from "nexus-icons";
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
6
  import { Menu } from "../Documents/Menu.js";
@@ -35,6 +35,8 @@ const COLUMN_WIDTH = {
35
35
  };
36
36
  /** Columns that keep their width instead of sharing the spare room. */
37
37
  const FIXED_TYPES = new Set(["checkbox", "switch", "radio"]);
38
+ /** What a locked row's cells say when one is used. */
39
+ const LOCKED_ROW = "Locked: unlock the row to change it.";
38
40
  const ENTER_MOVES = new Set(["text", "email", "tel", "url", "search", "password", "number"]);
39
41
  const cssLength = (value) => (typeof value === "number" ? `${value}px` : value);
40
42
  /** How a cell lines up: numbers at the end, checkboxes and radios in the middle, the rest at the start. */
@@ -64,11 +66,17 @@ const TabularRow = memo(function TabularRow({ store, rowKey, index, count, colum
64
66
  items.push({ separator: true });
65
67
  items.push({ value: "move-up", label: "Move up", icon: _jsx(InlineIcon, { icon: iconArrowUp }), disabled: index === 0 || Boolean(context.rowLock(store.keyAt(index - 1), index - 1)) }, { value: "move-down", label: "Move down", icon: _jsx(InlineIcon, { icon: iconArrowDown }), disabled: index === count - 1 });
66
68
  }
67
- if (context.removable && !fixed) {
69
+ // At the end of the row, Remove and Lock are buttons of their own, as a data table's are; the menu holds the rest.
70
+ const atEnd = context.actionsAt === "end";
71
+ const locked = context.isLocked(rowKey);
72
+ const removable = context.removable && !fixed;
73
+ const lockable = context.lockable && !fixed;
74
+ if (removable && !atEnd) {
68
75
  if (items.length)
69
76
  items.push({ separator: true });
70
77
  items.push({ value: "remove", label: "Remove", icon: _jsx(InlineIcon, { icon: iconTrash }), tone: "danger" });
71
78
  }
79
+ const menu = items.length > 0 && (_jsx(Menu, { items: items, align: atEnd ? "end" : undefined, onSelect: value => onAction(value, rowKey), "aria-label": `${title} actions`, children: _jsx("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `${title} actions`, title: `${title} actions`, "data-nx-row-actions": "", children: _jsx(InlineIcon, { icon: iconDotsVertical }) }) }));
72
80
  // Leaving an input counts as using it: a repeated value shows its message from then on.
73
81
  function onBlur(event) {
74
82
  const cell = event.target.closest("[data-nx-cell]");
@@ -76,7 +84,7 @@ const TabularRow = memo(function TabularRow({ store, rowKey, index, count, colum
76
84
  return;
77
85
  store.touch(rowKey, cell.dataset.nxCell ?? "");
78
86
  }
79
- return (_jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": index + 2, "data-nx-row": rowKey, "data-problem": invalid || undefined, onBlur: onBlur, children: [context.numbered && (_jsxs("div", { role: "cell", className: "nx-tabular__num", children: [_jsx("span", { className: "nx-tabular__num-text", children: index + 1 }), items.length > 0 && (_jsx(Menu, { items: items, onSelect: value => onAction(value, rowKey), "aria-label": `${title} actions`, children: _jsx("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `${title} actions`, title: `${title} actions`, "data-nx-row-actions": "", children: _jsx(InlineIcon, { icon: iconDotsVertical }) }) }))] })), columns.map(params => (_jsx("div", { role: "cell", className: cx("nx-tabular__cell", `nx-tabular__cell--${cellKind(params)}`), "data-nx-cell": params.name, children: _jsx(RowCell, { store: store, rowKey: rowKey, params: context.cellParams(rowKey, params, index), index: index, error: store.shownError(rowKey, params.name), layout: "tabular", idPrefix: context.idPrefix, size: context.size, readOnly: context.readOnly, disabled: context.disabled, lock: context.cellLock(rowKey, params.name, index) }, store.revision(rowKey, params.name)) }, params.name)))] }));
87
+ return (_jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": index + 2, "data-nx-row": rowKey, "data-problem": invalid || undefined, onBlur: onBlur, children: [context.numbered && (_jsxs("div", { role: "cell", className: "nx-tabular__num", children: [_jsx("span", { className: "nx-tabular__num-text", children: index + 1 }), !atEnd && menu] })), columns.map(params => (_jsx("div", { role: "cell", className: cx("nx-tabular__cell", `nx-tabular__cell--${cellKind(params)}`), "data-nx-cell": params.name, children: _jsx(RowCell, { store: store, rowKey: rowKey, params: context.cellParams(rowKey, params, index), index: index, error: store.shownError(rowKey, params.name), layout: "tabular", idPrefix: context.idPrefix, size: context.size, readOnly: context.readOnly, disabled: context.disabled, lock: context.cellLock(rowKey, params.name, index) }, store.revision(rowKey, params.name)) }, params.name))), atEnd && (_jsxs("div", { role: "cell", className: "nx-tabular__actions", children: [removable && (_jsx("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `Remove ${title}`, title: locked ? "Unlock the row to remove it" : "Remove", "data-tone": "danger", "data-nx-row-actions": "", disabled: locked || context.disabled, onClick: () => onAction("remove", rowKey), children: _jsx(InlineIcon, { icon: iconTrash }) })), lockable && (_jsx("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `${locked ? "Unlock" : "Lock"} ${title}`, "aria-pressed": locked, title: locked ? "Locked: unlock to change it" : "Lock, so it cannot be changed", "data-nx-row-actions": "", disabled: context.disabled, onClick: () => onAction(locked ? "unlock" : "lock", rowKey), children: _jsx(InlineIcon, { icon: locked ? iconLock : iconLockOpen }) })), menu] }))] }));
80
88
  });
81
89
  /**
82
90
  * Rows of inputs in a table, one column per input: a person's emails or addresses, invoice lines, a salary sheet. The
@@ -108,6 +116,10 @@ export function TabularForm(props) {
108
116
  const addable = props.addable !== false && !readOnly;
109
117
  const removable = props.removable !== false && !readOnly && !disabled;
110
118
  const sortable = Boolean(props.sortable) && !readOnly && !disabled;
119
+ const actionsAt = props.actionsAt ?? "number";
120
+ const lockable = Boolean(props.lockable) && !readOnly;
121
+ /** The rows the person locked, by key: the screen's, never the value's. */
122
+ const [lockedKeys, setLockedKeys] = useState(() => new Set());
111
123
  const layoutRef = useRef(null);
112
124
  const rows = useRowsInput(props, "Row", layoutRef);
113
125
  const store = rows.store;
@@ -238,9 +250,22 @@ export function TabularForm(props) {
238
250
  run.move(key, -1);
239
251
  else if (action === "move-down")
240
252
  run.move(key, 1);
253
+ else if (action === "lock" || action === "unlock") {
254
+ setLockedKeys(current => {
255
+ const next = new Set(current);
256
+ if (action === "lock")
257
+ next.add(key);
258
+ else
259
+ next.delete(key);
260
+ return next;
261
+ });
262
+ }
241
263
  else
242
264
  run.remove(key);
243
265
  }, [store]);
266
+ const isLocked = useCallback((key) => lockedKeys.has(key), [lockedKeys]);
267
+ // A locked row's cells are read-only with the lock's reason, as a row nobody may change has its own.
268
+ const cellLock = useCallback((key, name, index) => (lockedKeys.has(key) ? LOCKED_ROW : rows.cellLock(key, name, index)), [lockedKeys, rows.cellLock]);
244
269
  /** The message or hint of the input with focus, shown under the table: cells have no room for their own. */
245
270
  function readStatus() {
246
271
  const active = document.activeElement;
@@ -286,7 +311,7 @@ export function TabularForm(props) {
286
311
  layoutRef.current?.focusRow(key, name);
287
312
  }
288
313
  }
289
- const context = useMemo(() => ({ idPrefix: frame.id, size, readOnly, disabled, numbered, removable, sortable, canAdd: rows.canAdd, showAll: snapshot.showAll, cellLock: rows.cellLock, cellParams: rows.cellParams, rowLock: rows.rowLock }), [frame.id, size, readOnly, disabled, numbered, removable, sortable, rows.canAdd, snapshot.showAll, rows.cellLock, rows.cellParams, rows.rowLock]);
314
+ const context = useMemo(() => ({ idPrefix: frame.id, size, readOnly, disabled, numbered, removable, sortable, canAdd: rows.canAdd, showAll: snapshot.showAll, cellLock, cellParams: rows.cellParams, rowLock: rows.rowLock, actionsAt, lockable, isLocked }), [frame.id, size, readOnly, disabled, numbered, removable, sortable, rows.canAdd, snapshot.showAll, cellLock, rows.cellParams, rows.rowLock, actionsAt, lockable, isLocked]);
290
315
  /**
291
316
  * Which columns are generated for **every** row on the page. The lock is then said once, on the column header,
292
317
  * instead of on each of its cells: eighteen locks down one column is eighteen times the ink for one fact. The cells
@@ -307,7 +332,7 @@ export function TabularForm(props) {
307
332
  let reason;
308
333
  for (const [offset, key] of shownKeys.entries()) {
309
334
  // A locked row says nothing about its columns: the rows around it decide whether a column is generated.
310
- if (rows.rowLock(key, start + offset))
335
+ if (lockedKeys.has(key) || rows.rowLock(key, start + offset))
311
336
  continue;
312
337
  const lock = rows.cellLock(key, params.name, start + offset);
313
338
  if (!lock) {
@@ -323,9 +348,13 @@ export function TabularForm(props) {
323
348
  return locked;
324
349
  // `shownKeys` is a fresh array on every render; the work is a call per visible cell, and nothing memoised is
325
350
  // handed to the rows, so recomputing it costs a map and never a re-render of a row.
326
- }, [columns, readOnly, rows.cellLock, rows.rowLock, shownKeys, start]);
351
+ }, [columns, readOnly, rows.cellLock, rows.rowLock, shownKeys, start, lockedKeys]);
327
352
  // The number and the row's menu, side by side, plus the frame around them. Both are always there, so nothing moves.
328
- const numberWidth = `calc(${Math.max(2, String(count).length)}ch + 18px + var(--nx-tabular-menu))`;
353
+ const atEnd = actionsAt === "end";
354
+ const numberWidth = atEnd ? `calc(${Math.max(2, String(count).length)}ch + 16px)` : `calc(${Math.max(2, String(count).length)}ch + 18px + var(--nx-tabular-menu))`;
355
+ // At the end: a button for Remove, one for Lock, and one for the menu when there is anything else in it.
356
+ const buttons = atEnd ? Number(removable) + Number(lockable) + Number(addable || sortable) : 0;
357
+ const actionsWidth = buttons ? `calc(${buttons} * var(--nx-tabular-menu) + ${buttons - 1} * 2px + 9px)` : null;
329
358
  const widths = columns.map(columnWidth);
330
359
  const template = [
331
360
  numbered ? numberWidth : null,
@@ -333,16 +362,19 @@ export function TabularForm(props) {
333
362
  const grow = params.grow ?? (FIXED_TYPES.has(params.type) ? 0 : 1);
334
363
  return grow > 0 ? `minmax(${widths[index]}, ${grow}fr)` : widths[index];
335
364
  }),
365
+ actionsWidth,
336
366
  ]
337
367
  .filter(Boolean)
338
368
  .join(" ");
339
- const minWidth = `calc(${[numbered ? numberWidth : "0px", ...widths].join(" + ")})`;
369
+ const minWidth = `calc(${[numbered ? numberWidth : "0px", ...widths, actionsWidth ?? "0px"].join(" + ")})`;
340
370
  const tableStyle = { "--nx-tabular-columns": template, "--nx-tabular-num": numbered ? numberWidth : "0px", minWidth };
341
371
  const invalid = Boolean(error || snapshot.message || snapshot.invalidRows);
342
- return (_jsxs("fieldset", { id: frame.id, className: cx("nx-rows", "nx-tabular", `nx-tabular--${size}`, className), style: style, disabled: disabled, "data-problem": invalid || undefined, "data-readonly": readOnly || undefined, "aria-label": props["aria-label"], "aria-describedby": frame.messageId, children: [_jsx(RowsLegend, { id: frame.legendId, label: label, required: required || (props.min ?? 0) > 0, lock: frame.lock }), _jsx("input", { ref: rows.proxyRef, className: "nx-visually-hidden", tabIndex: -1, "aria-hidden": "true", form: props.form, onInvalid: rows.onInvalid }), _jsx("div", { ref: viewportRef, className: "nx-tabular__viewport", tabIndex: -1, style: { maxHeight: viewportMax ?? cssLength(maxHeight) }, onScroll: updateRange, children: _jsxs("div", { role: "table", className: "nx-tabular__table", style: tableStyle, "aria-labelledby": label ? frame.legendId : undefined, "aria-label": label ? undefined : props["aria-label"], "aria-rowcount": count + 1, children: [_jsx("div", { ref: headRef, role: "rowgroup", className: "nx-tabular__head", children: _jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": 1, children: [numbered && (_jsxs("div", { role: "columnheader", className: "nx-tabular__num", children: [_jsx("span", { "aria-hidden": "true", children: "#" }), _jsx("span", { className: "nx-visually-hidden", children: store.names.title })] })), columns.map(params => {
372
+ return (_jsxs("fieldset", { id: frame.id, className: cx("nx-rows", "nx-tabular", `nx-tabular--${size}`, className), style: style, disabled: disabled, "data-problem": invalid || undefined, "data-readonly": readOnly || undefined, "data-actions-at": atEnd ? "end" : undefined, "aria-label": props["aria-label"], "aria-describedby": frame.messageId, children: [_jsx(RowsLegend, { id: frame.legendId, label: label, required: required || (props.min ?? 0) > 0, lock: frame.lock }), _jsx("input", { ref: rows.proxyRef, className: "nx-visually-hidden", tabIndex: -1, "aria-hidden": "true", form: props.form, onInvalid: rows.onInvalid }), _jsx("div", { ref: viewportRef, className: "nx-tabular__viewport", tabIndex: -1,
373
+ // A focused cell scrolls clear of the actions fixed at the end, as it does of the number at the start.
374
+ style: { maxHeight: viewportMax ?? cssLength(maxHeight), scrollPaddingRight: actionsWidth ?? undefined }, onScroll: updateRange, children: _jsxs("div", { role: "table", className: "nx-tabular__table", style: tableStyle, "aria-labelledby": label ? frame.legendId : undefined, "aria-label": label ? undefined : props["aria-label"], "aria-rowcount": count + 1, children: [_jsx("div", { ref: headRef, role: "rowgroup", className: "nx-tabular__head", children: _jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": 1, children: [numbered && (_jsxs("div", { role: "columnheader", className: "nx-tabular__num", children: [_jsx("span", { "aria-hidden": "true", children: "#" }), _jsx("span", { className: "nx-visually-hidden", children: store.names.title })] })), columns.map(params => {
343
375
  const columnHint = params.hint;
344
376
  const locked = lockedColumns.get(params.name);
345
377
  const reason = typeof locked === "string" ? locked : locked ? "Read-only" : undefined;
346
378
  return (_jsxs("div", { role: "columnheader", className: cx("nx-tabular__head-cell", `nx-tabular__cell--${cellKind(params)}`), title: reason ?? (typeof columnHint === "string" ? columnHint : undefined), "data-nx-locked": locked ? "" : undefined, children: [_jsx("span", { className: "nx-tabular__head-label", children: params.label ?? params.name }), params.required && !locked && (_jsx("span", { className: "nx-field__required", "aria-hidden": "true", children: "*" })), locked && _jsx(InlineIcon, { icon: iconLock, className: "nx-tabular__head-lock", "aria-hidden": "true" })] }, params.name));
347
- })] }) }), _jsx("div", { ref: bodyRef, role: "rowgroup", className: "nx-tabular__body", style: { paddingTop: start * rowHeight, paddingBottom: Math.max(0, count - end) * rowHeight }, onKeyDown: onBodyKeyDown, onFocus: readStatus, onBlur: readStatusSoon, onInput: readStatusSoon, children: count === 0 ? (_jsx("div", { role: "row", className: "nx-tabular__row nx-tabular__row--empty", children: _jsx("div", { role: "cell", className: "nx-tabular__empty", children: frame.emptyMessage }) })) : (shownKeys.map((key, offset) => (_jsx(TabularRow, { store: store, rowKey: key, index: start + offset, count: count, columns: columns, context: context, onAction: onAction }, `${snapshot.generation}:${key}`)))) })] }) }), _jsx(RowsFooter, { messageId: frame.messageId, store: store, snapshot: snapshot, error: error, hint: hint, status: status, addLabel: frame.addLabel, addable: addable, canAdd: rows.canAdd, max: max, maxMessage: frame.maxMessage, size: size, onAdd: () => rows.actions.add(), onUndo: () => rows.actions.undo() })] }));
379
+ }), actionsWidth && (_jsx("div", { role: "columnheader", className: "nx-tabular__actions", children: _jsx("span", { className: "nx-visually-hidden", children: "Actions" }) }))] }) }), _jsx("div", { ref: bodyRef, role: "rowgroup", className: "nx-tabular__body", style: { paddingTop: start * rowHeight, paddingBottom: Math.max(0, count - end) * rowHeight }, onKeyDown: onBodyKeyDown, onFocus: readStatus, onBlur: readStatusSoon, onInput: readStatusSoon, children: count === 0 ? (_jsx("div", { role: "row", className: "nx-tabular__row nx-tabular__row--empty", children: _jsx("div", { role: "cell", className: "nx-tabular__empty", children: frame.emptyMessage }) })) : (shownKeys.map((key, offset) => (_jsx(TabularRow, { store: store, rowKey: key, index: start + offset, count: count, columns: columns, context: context, onAction: onAction }, `${snapshot.generation}:${key}`)))) })] }) }), _jsx(RowsFooter, { messageId: frame.messageId, store: store, snapshot: snapshot, error: error, hint: hint, status: status, addLabel: frame.addLabel, addable: addable, canAdd: rows.canAdd, max: max, maxMessage: frame.maxMessage, size: size, onAdd: () => rows.actions.add(), onUndo: () => rows.actions.undo() })] }));
348
380
  }
@@ -4,13 +4,16 @@ import { cx } from "../../Helpers/ClassHelpers.js";
4
4
  import { Field } from "./Field.js";
5
5
  import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
6
6
  import { useTextField } from "./TextField.js";
7
+ import { useInputDesign } from "../../Helpers/UserPreferences.js";
7
8
  /**
8
9
  * A multi-line text input with the same label, hint, validation, and designs as `TextBox`.
9
10
  *
10
11
  * @example <TextArea label="Description" maxLength={500} showCount autoResize />
11
12
  */
12
13
  export function TextArea(props) {
13
- const { label, hint, error, design = "standard", 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;
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;
15
+ // The person's input design (the quick preferences) when this page and its form name none, for a labelled input.
16
+ const design = useInputDesign(ownDesign, Boolean(label));
14
17
  const field = useTextField(props);
15
18
  const rowsStyle = { "--nx-field-rows": rows, "--nx-field-max-rows": Math.max(rows, maxRows) };
16
19
  // A lock at the top end of the box, and a note under it when someone tries to change the text, say why nothing happens.
@@ -7,6 +7,7 @@ import { Field, hasContent } from "./Field.js";
7
7
  import { setInputValue } from "./InputField.js";
8
8
  import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
9
9
  import { useTextField } from "./TextField.js";
10
+ import { useInputDesign } from "../../Helpers/UserPreferences.js";
10
11
  // Values that are not prose: no red spell-check underline, no capital first letter on phones.
11
12
  const PLAIN_TYPES = new Set(["email", "password", "tel", "url"]);
12
13
  /**
@@ -17,7 +18,9 @@ const PLAIN_TYPES = new Set(["email", "password", "tel", "url"]);
17
18
  * @example <TextBox label="Email" type="email" required pattern={TEXT_PATTERNS.email} icon={<InlineIcon icon={iconMail} />} />
18
19
  */
19
20
  export function TextBox(props) {
20
- const { type = "text", label, hint, error, design = "standard", size = "md", icon, iconEnd, prefix, suffix, actionsStart, clearable = false, showCount = false, readOnlyMessage = "Read-only: this field cannot be changed.", fieldName, validateOn, onValueChange, className, inputClassName, style, required, minLength, maxLength, pattern, patternMessage, validate, placeholder, disabled, readOnly, ref, onChange, onBlur, onInvalid, onKeyDown, onClick, ...inputProps } = props;
21
+ const { type = "text", label, hint, error, design: ownDesign, size = "md", icon, iconEnd, prefix, suffix, actionsStart, clearable = false, showCount = false, readOnlyMessage = "Read-only: this field cannot be changed.", fieldName, validateOn, onValueChange, className, inputClassName, style, required, minLength, maxLength, pattern, patternMessage, validate, placeholder, disabled, readOnly, ref, onChange, onBlur, onInvalid, onKeyDown, onClick, ...inputProps } = props;
22
+ // The person's input design (the quick preferences) when this page and its form name none, for a labelled input.
23
+ const design = useInputDesign(ownDesign, Boolean(label));
21
24
  const field = useTextField(props);
22
25
  const [revealed, setRevealed] = useState(false);
23
26
  const editable = !disabled && !readOnly;
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useLayoutEffect, useMemo, useRef } from "react";
4
+ import { smoothScrollAllowed } from "../../Helpers/AnimationHelpers.js";
4
5
  import { cx } from "../../Helpers/ClassHelpers.js";
5
6
  import { toDevanagariDigits } from "../../Helpers/DateHelpers.js";
6
7
  import { formatTime, getMeridiemNames, isSecondsInRange, isTwelveHourFormat, listTimeSlots, normalizeTimeStep, nowIsoTime, SECONDS_PER_DAY, secondsToIsoTime, timeToSeconds, toIsoTime, } from "../../Helpers/TimeHelpers.js";
@@ -157,15 +158,15 @@ export function TimePanel(props) {
157
158
  return (slots.find(slot => !slot.disabled) ?? slots[0]).value;
158
159
  }, [view, slots, selected]);
159
160
  // Chosen items scroll to the middle row of their column, so they line up across columns with their neighbours above
160
- // and below; the list centers its slot the same way. At once when the panel opens, smoothly after that, unless the
161
- // user asked for reduced motion. (Columns are positioned, so an item's offsetTop is measured from the column.)
161
+ // and below; the list centers its slot the same way. At once when the panel opens, smoothly after that, unless motion
162
+ // is off. (Columns are positioned, so an item's offsetTop is measured from the column.)
162
163
  // With `autoFocus`, focus then moves in once.
163
164
  useLayoutEffect(() => {
164
165
  const root = rootRef.current;
165
166
  if (!root)
166
167
  return;
167
168
  const align = () => {
168
- const smooth = scrolledRef.current && !window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
169
+ const smooth = scrolledRef.current && smoothScrollAllowed();
169
170
  for (const column of root.querySelectorAll("[data-column]")) {
170
171
  const target = column.querySelector(view === "list" ? "[data-anchor]" : '[aria-selected="true"]');
171
172
  if (!target)
@@ -12,6 +12,7 @@ import { Field, hasContent } from "./Field.js";
12
12
  import { setInputValue, useInputField } from "./InputField.js";
13
13
  import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
14
14
  import { TimePanel } from "./TimePanel.js";
15
+ import { useInputDesign } from "../../Helpers/UserPreferences.js";
15
16
  /**
16
17
  * A time input: type a time on either clock ("2:30 pm", "1430"), or pick one from the panel that opens under the box.
17
18
  * Written in the user's time format, 12- or 24-hour, and always held as an ISO time with no date and no zone, which a
@@ -20,7 +21,9 @@ import { TimePanel } from "./TimePanel.js";
20
21
  * @example <TimePicker name="arrival" label="Arrival" min="12:00" max="22:00" step={1800} view="list" />
21
22
  */
22
23
  export function TimePicker(props) {
23
- const { label, hint, error, design = "standard", size = "md", icon, prefix, view = "columns", clearable = true, fieldName, readOnlyMessage = "Read-only: this time cannot be changed.", validateOn, value, defaultValue, onValueChange, isTimeDisabled, className, inputClassName, style, required, min, max, step, validate, name, form, placeholder, disabled, readOnly, ref, onChange, onBlur, onFocus, onInvalid, onKeyDown, onClick, ...inputProps } = props;
24
+ const { label, hint, error, design: ownDesign, size = "md", icon, prefix, view = "columns", clearable = true, fieldName, readOnlyMessage = "Read-only: this time cannot be changed.", validateOn, value, defaultValue, onValueChange, isTimeDisabled, className, inputClassName, style, required, min, max, step, validate, name, form, placeholder, disabled, readOnly, ref, onChange, onBlur, onFocus, onInvalid, onKeyDown, onClick, ...inputProps } = props;
25
+ // The person's input design (the quick preferences) when this page and its form name none, for a labelled input.
26
+ const design = useInputDesign(ownDesign, Boolean(label));
24
27
  const preferences = useDatePreferences();
25
28
  const script = props.script ?? (usesDevanagari(preferences.calendar, preferences.script) ? "devanagari" : "latin");
26
29
  const seconds = normalizeTimeStep(step) % 60 !== 0;
@@ -1,5 +1,5 @@
1
1
  import { type CSSProperties, type ReactNode, type Ref } from "react";
2
- import { type TreeItem, type TreeValue, type TreeValueMode } from "../../Helpers/TreeStore.ts";
2
+ import { type TreeChildrenPage, type TreeChildrenRequest, type TreeItem, type TreeValue, type TreeValueMode } from "../../Helpers/TreeStore.ts";
3
3
  import type { Size } from "../../Interfaces/TypeInterfaces.ts";
4
4
  export type TreeAction = "create" | "update" | "delete";
5
5
  export interface TreeCreateRequest<TItem extends TreeItem = TreeItem> {
@@ -47,13 +47,46 @@ export interface TreeViewHandle<TItem extends TreeItem = TreeItem> {
47
47
  remove: (value: TreeValue) => Promise<boolean>;
48
48
  /** Focuses the focused row, or the first. */
49
49
  focus: () => void;
50
+ /**
51
+ * With `loadChildren`: drops the pages kept for the whole tree, or for one item and everything under it, and loads
52
+ * again only what is on screen - the top level and the open items with rows in view. An open item out of view loads
53
+ * when it scrolls in, a closed one when it is next expanded. What is drawn stays until the fresh pages replace it.
54
+ */
55
+ refresh: (value?: TreeValue) => void;
50
56
  }
51
57
  export interface TreeViewProps<TItem extends TreeItem = TreeItem> {
52
58
  /**
53
59
  * The items, flat: each names its parent through `parentKey`, and siblings keep this order. Keep the array stable
54
- * (state, a constant, or `useMemo`): a different array, or one with different items, builds the tree again.
60
+ * (state, a constant, or `useMemo`): a different array, or one with different items, builds the tree again. With
61
+ * `loadChildren`, leave it out to load the top level too, or give the top level and let the children load.
62
+ */
63
+ items?: readonly TItem[];
64
+ /**
65
+ * Loads children from a server, a page at a time: when an item is expanded the first time, when Load more is
66
+ * pressed, and for the top level when no `items` are given. It answers `{ items, total?, hasMore? }`, at once or in
67
+ * a promise; the children go under the item asked for, whatever their own parent field says. Values must be unique
68
+ * across the whole tree: map items whose ids could repeat between levels (`"c" + countryId`, `"r" + regionId`).
69
+ * A thrown error or rejected promise shows "Could not load the items." with Retry.
70
+ */
71
+ loadChildren?: (request: TreeChildrenRequest<TItem>) => TreeChildrenPage<TItem> | PromiseLike<TreeChildrenPage<TItem>>;
72
+ /** With `loadChildren`: children asked for at a time (the request's `take`). Default 50. */
73
+ childPageSize?: number;
74
+ /**
75
+ * With `loadChildren`: the item field saying whether it has children, before they are loaded - a boolean or a count,
76
+ * such as `childCount`. An item without it shows a chevron until its first page answers, and loses it if the page
77
+ * is empty. With `checkable` and `cascade`, an item known to have none can be checked before it is opened.
78
+ */
79
+ hasChildrenKey?: string;
80
+ /**
81
+ * With `loadChildren`: keeps the pages loaded in a cache shared by every tree on the page with the same key, for
82
+ * `cacheLifetime`: leaving the page and coming back, or a second tree, makes no call, and a request on its way is
83
+ * shared. Name the source, e.g. `"administrative-regions"`. Without it, pages are kept only in this tree.
55
84
  */
56
- items: readonly TItem[];
85
+ cacheKey?: string;
86
+ /** How long a kept page is used, in milliseconds. Default 5 minutes. */
87
+ cacheLifetime?: number;
88
+ /** What a click on an item whose items are not all loaded says, with `checkable` and `cascade`. */
89
+ partialMessage?: string;
57
90
  /** The item field that holds its value. Default `id`. */
58
91
  valueKey?: string;
59
92
  /** The item field that holds its parent's value; empty for a top-level item. Default `parentId`. */
@@ -96,7 +129,10 @@ export interface TreeViewProps<TItem extends TreeItem = TreeItem> {
96
129
  form?: string;
97
130
  /** With `checkable` and `cascade`: a parent shows how many of its items are checked, as "3/5". Default true. */
98
131
  counts?: boolean;
99
- /** What is expanded at the start: `"all"`, a number of levels, or the values of items to open. */
132
+ /**
133
+ * What is expanded at the start: `"all"`, a number of levels, or the values of items to open. With `loadChildren`,
134
+ * only items whose children are given in `items`: nothing is asked for at the start but the top level.
135
+ */
100
136
  defaultExpanded?: "all" | number | readonly TreeValue[];
101
137
  /** Items can be added: a + on each row adds under it, and Add in the toolbar adds at the top level. */
102
138
  creatable?: boolean;
@@ -115,7 +151,11 @@ export interface TreeViewProps<TItem extends TreeItem = TreeItem> {
115
151
  onUpdate?: (request: TreeUpdateRequest<TItem>) => TItem | void | Promise<TItem | void>;
116
152
  /** Deletes an item and everything under it. A thrown error keeps the items and shows the message. */
117
153
  onDelete?: (request: TreeDeleteRequest<TItem>) => void | Promise<void>;
118
- /** After an item is added, renamed, or deleted: every item in tree order. Passing this array back as `items` does not rebuild the tree. */
154
+ /**
155
+ * After an item is added, renamed, or deleted: every item in tree order. Passing this array back as `items` does not
156
+ * rebuild the tree. With `loadChildren` it holds the loaded children too, whose own parent fields may not name the
157
+ * item they were loaded under: read it, but keep passing the `items` you gave.
158
+ */
119
159
  onItemsChange?: (items: TItem[]) => void;
120
160
  /** Asks before deleting. Default true; needs `<PopupHost />` on the page. */
121
161
  confirmDelete?: boolean;
@@ -154,7 +194,17 @@ export interface TreeViewProps<TItem extends TreeItem = TreeItem> {
154
194
  * Keys: arrows move and expand, Home and End jump, letters find a row, Space checks, Enter opens, `*` expands the
155
195
  * siblings, F2 renames, Delete deletes, Insert adds under the focused row, and Shift+F10 opens the actions menu.
156
196
  *
197
+ * With `loadChildren` the children come from a server, a page at a time, when an item is first expanded: a loading
198
+ * row while they come, a row that says the page failed with Retry, and Load more with how many are left. Those rows
199
+ * are rows like the others, reached with the arrows and pressed with Enter or Space. What is loaded stays (collapse
200
+ * and expand again asks nothing), `cacheKey` shares it with the next tree, and the handle's `refresh` loads again what
201
+ * is on screen. With `cascade`, an item whose items are not all loaded cannot be checked as a whole: the value can only
202
+ * hold items that were loaded, so its check would silently leave the rest out.
203
+ *
157
204
  * @example
158
205
  * <TreeView label="Permissions" items={permissions} checkable name="actionIds" defaultExpanded={1} isItemDisabled={item => !item.canAssign} />
206
+ *
207
+ * <TreeView label="Regions" items={countries} hasChildrenKey="childCount" cacheKey="regions"
208
+ * loadChildren={({ parent, skip, take, signal }) => loadRegions(parent, skip, take, signal)} />
159
209
  */
160
210
  export declare function TreeView<TItem extends TreeItem = TreeItem>(props: TreeViewProps<TItem>): import("react").JSX.Element;