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,9 +1,10 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { iconArrowBackUp, iconColumns3, iconGripVertical, iconPin, InlineIcon } from "nexus-icons";
4
- import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
3
+ import { iconArrowBackUp, iconGripVertical, iconPin, InlineIcon } from "nexus-icons";
4
+ import { useEffect, useId, useRef, useState } from "react";
5
5
  import { cx } from "../../Helpers/ClassHelpers.js";
6
- import { followAnchor, isPopoverOpen, placeUnderAnchor } from "../../Helpers/PopoverHelpers.js";
6
+ import { isPopoverOpen } from "../../Helpers/PopoverHelpers.js";
7
+ import { usePopoverPanel } from "../../Helpers/PopoverHooks.js";
7
8
  import { Button } from "../Documents/Button.js";
8
9
  /** The rows of the panel: a band for every run of neighbours that name the same group, as the header draws them. */
9
10
  function panelRows(columns, groups) {
@@ -42,15 +43,12 @@ function dropIndex(boxes, y, from) {
42
43
  }
43
44
  return boxes.length - 1;
44
45
  }
45
- /** The room kept between the panel and the window's edge, and the least height it is ever given. */
46
- const PANEL_MARGIN = 8;
47
- const PANEL_MIN_HEIGHT = 160;
48
46
  /** The text of a label that may be a node, for an accessible name. */
49
47
  function textOf(label) {
50
48
  return typeof label === "string" || typeof label === "number" ? String(label) : "the column";
51
49
  }
52
50
  /**
53
- * The button that opens the Columns panel, and the panel itself: a checkbox per column, bands with their columns under
51
+ * The Columns panel, opened through its `open()` handle under `anchor`: a checkbox per column, bands with their columns under
54
52
  * them, and a handle to drag a row into another place. The panel is a popover under the button, so it closes on Esc,
55
53
  * on a click outside, and on Tab.
56
54
  *
@@ -58,15 +56,10 @@ function textOf(label) {
58
56
  * <TableColumnsPanel columns={columns} hidden={state.hidden} pinned={state.pinned}
59
57
  * onToggle={(key, hide) => table.setHidden(key, hide)} onOrder={keys => table.setOrder(keys)} />
60
58
  */
61
- export function TableColumnsPanel({ columns, hidden, pinned, reorderable = true, size = "md", changed = false, onToggle, onOrder, onReset, "aria-label": ariaLabel = "Columns", }) {
59
+ export function TableColumnsPanel({ columns, hidden, pinned, reorderable = true, size = "md", changed = false, onToggle, onOrder, onReset, anchor, ref, }) {
62
60
  const id = useId();
63
61
  const panelId = `${id}-columns`;
64
- const triggerId = `${id}-columns-trigger`;
65
- const triggerRef = useRef(null);
66
- const panelRef = useRef(null);
67
- const [open, setOpen] = useState(false);
68
- // Set on pointer down while the panel is open: that press closes it, and its click must not open it again.
69
- const dismissing = useRef(false);
62
+ const { panelRef, open, onToggle: onPanelToggle } = usePopoverPanel(anchor, ref);
70
63
  // While a row is held: its key, the list it may move in, and where it would land.
71
64
  const [drag, setDrag] = useState(null);
72
65
  const [said, setSaid] = useState("");
@@ -84,25 +77,6 @@ export function TableColumnsPanel({ columns, hidden, pinned, reorderable = true,
84
77
  const rows = panelRows(all, groups);
85
78
  const shownCount = all.filter(column => column.shown).length;
86
79
  const bands = rows.some(row => row.band !== null);
87
- function place() {
88
- const panel = panelRef.current;
89
- const trigger = triggerRef.current;
90
- if (!panel || !trigger)
91
- return;
92
- // Measured at its own height, then capped to the room on the side it landed on: a table with many columns keeps
93
- // its heading, its note and Reset in the window, and the list of columns scrolls instead.
94
- panel.style.maxHeight = "";
95
- placeUnderAnchor(panel, trigger.getBoundingClientRect(), { align: "end" });
96
- const top = panel.getBoundingClientRect().top;
97
- panel.style.maxHeight = `${Math.max(PANEL_MIN_HEIGHT, window.innerHeight - top - PANEL_MARGIN)}px`;
98
- }
99
- // The panel is fixed, so it follows the button while the page or a scroller moves.
100
- useLayoutEffect(() => {
101
- if (!open)
102
- return;
103
- return followAnchor(place);
104
- // eslint-disable-next-line react-hooks/exhaustive-deps
105
- }, [open]);
106
80
  // A panel opened again starts without the last move's message, so a screen reader is not told about an old move.
107
81
  useEffect(() => {
108
82
  if (!open)
@@ -205,39 +179,22 @@ export function TableColumnsPanel({ columns, hidden, pinned, reorderable = true,
205
179
  const lastShown = column.shown && shownCount <= 1;
206
180
  return (_jsxs("li", { className: cx("nx-columns__row", inBand && "nx-columns__row--child"), "data-held": drag?.key === column.key ? "" : undefined, "data-drop": drag && drag.list === list && drag.key !== column.key && drag.to === index ? "" : undefined, children: [column.movable && !bandOff ? (_jsx("button", { type: "button", className: "nx-columns__handle", "aria-label": `Move ${textOf(column.label)}`, title: inBand ? `Drag to move it inside ${list}, or use the arrow keys` : "Drag to move, or use the arrow keys", onPointerDown: event => onHandlePointerDown(event, list, index, column.key, textOf(column.label)), onKeyDown: event => onHandleKeyDown(event, list, index, textOf(column.label)), children: _jsx(InlineIcon, { icon: iconGripVertical }) })) : (_jsx("span", { className: "nx-columns__handle nx-columns__handle--fixed", "aria-hidden": "true" })), _jsxs("label", { className: "nx-choice nx-choice--sm nx-columns__choice", children: [_jsx("input", { type: "checkbox", className: "nx-choice__control nx-choice__control--checkbox", checked: column.shown && !bandOff, disabled: bandOff || !column.hideable || lastShown, title: bandOff ? `${list} is hidden` : column.hideable ? undefined : "Always shown", onChange: event => onToggle(column.key, !event.currentTarget.checked) }), _jsx("span", { className: "nx-choice__text", children: _jsx("span", { className: "nx-choice__label nx-columns__label", children: column.label }) })] }), column.pin && (_jsxs("span", { className: "nx-columns__pin", title: `Pinned to the ${column.pin === "left" ? "left" : "right"}`, children: [_jsx(InlineIcon, { icon: iconPin }), _jsx("span", { className: "nx-visually-hidden", children: `Pinned to the ${column.pin === "left" ? "left" : "right"}` })] }))] }, column.key));
207
181
  }
208
- return (_jsxs(_Fragment, { children: [_jsx(Button, { ref: triggerRef, id: triggerId, size: size === "sm" ? "sm" : "md", icon: _jsx(InlineIcon, { icon: iconColumns3 }), "aria-label": ariaLabel, "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": panelId, title: "Columns: show, hide, and arrange", onPointerDown: () => {
209
- dismissing.current = isPopoverOpen(panelRef.current);
210
- }, onClick: () => {
211
- if (dismissing.current) {
212
- dismissing.current = false;
213
- return;
214
- }
215
- const panel = panelRef.current;
216
- if (!panel)
217
- return;
218
- if (isPopoverOpen(panel))
219
- hidePopover(panel);
220
- else {
221
- panel.showPopover();
222
- place();
223
- panel.focus();
224
- }
225
- } }), _jsxs("div", { ref: panelRef, id: panelId, popover: "auto", role: "dialog", tabIndex: -1, "aria-labelledby": `${panelId}-title`, className: cx("nx-popover", "nx-columns", size === "sm" && "nx-columns--sm"), "data-dragging": drag ? "" : undefined, onToggle: (event) => setOpen(event.newState === "open"), onKeyDown: event => {
226
- if (event.key === "Tab")
227
- hidePopover(event.currentTarget);
228
- }, children: [_jsxs("div", { className: "nx-columns__head", children: [_jsx("span", { className: "nx-columns__title", id: `${panelId}-title`, children: "Columns" }), _jsxs("span", { className: "nx-columns__count", children: [shownCount, " of ", all.length, " shown"] })] }), _jsx("ul", { className: "nx-columns__list", "data-list": "", children: rows.map((row, index) => {
229
- if (row.band === null)
230
- return columnRow(row.column, "", index, false, false);
231
- const band = row.band;
232
- // A band whose columns were split into two runs is drawn twice, so a row is known by its first column, never
233
- // by the band's name: two rows under one name would be one row to React.
234
- const bandId = `band:${row.columns[0].key}`;
235
- const shown = row.columns.filter(column => column.shown).length;
236
- const movable = row.columns.every(column => column.movable);
237
- const last = shown > 0 && shownCount - shown < 1;
238
- return (_jsxs("li", { className: "nx-columns__row nx-columns__row--band", "data-held": drag?.key === bandId ? "" : undefined, "data-drop": drag && drag.list === "" && drag.key !== bandId && drag.to === index ? "" : undefined, children: [_jsxs("div", { className: "nx-columns__band", children: [movable ? (_jsx("button", { type: "button", className: "nx-columns__handle", "aria-label": `Move ${band}`, title: "Drag to move the whole band, or use the arrow keys", onPointerDown: event => onHandlePointerDown(event, "", index, bandId, band), onKeyDown: event => onHandleKeyDown(event, "", index, band), children: _jsx(InlineIcon, { icon: iconGripVertical }) })) : (_jsx("span", { className: "nx-columns__handle nx-columns__handle--fixed", "aria-hidden": "true" })), _jsxs("label", { className: "nx-choice nx-choice--sm nx-columns__choice", children: [_jsx("input", { type: "checkbox", className: "nx-choice__control nx-choice__control--checkbox", checked: shown > 0, disabled: last, ref: input => {
239
- if (input)
240
- input.indeterminate = shown > 0 && shown < row.columns.length;
241
- }, "aria-label": `${band}: ${shown} of ${row.columns.length} shown`, onChange: event => toggleBand(band, row.columns, event.currentTarget.checked) }), _jsx("span", { className: "nx-choice__text", children: _jsx("span", { className: "nx-choice__label nx-columns__label", children: band }) })] }), _jsxs("span", { className: "nx-columns__band-count", children: [shown, " / ", row.columns.length] })] }), _jsx("ul", { className: "nx-columns__children", "data-list": band, children: row.columns.map((column, childIndex) => columnRow(column, band, childIndex, true, shown === 0)) })] }, bandId));
242
- }) }), _jsxs("p", { className: "nx-columns__note", children: [reorderable ? "Drag a handle, or move it with the arrow keys." : "This table keeps the order of its columns.", bands ? " A column under a band moves inside it; the band moves as one." : ""] }), onReset && (_jsx("div", { className: "nx-columns__foot", children: _jsx(Button, { size: "sm", variant: "ghost", icon: _jsx(InlineIcon, { icon: iconArrowBackUp }), disabled: !changed, onClick: () => onReset(), children: "Reset columns" }) })), _jsx("span", { className: "nx-visually-hidden", "aria-live": "polite", children: said })] })] }));
182
+ return (_jsx(_Fragment, { children: _jsxs("div", { ref: panelRef, id: panelId, popover: "auto", role: "dialog", tabIndex: -1, "aria-labelledby": `${panelId}-title`, className: cx("nx-popover", "nx-table-panel", "nx-columns", size === "sm" && "nx-columns--sm"), "data-dragging": drag ? "" : undefined, onToggle: onPanelToggle, onKeyDown: event => {
183
+ if (event.key === "Tab")
184
+ hidePopover(event.currentTarget);
185
+ }, children: [_jsxs("div", { className: "nx-table-panel__head", children: [_jsx("span", { className: "nx-table-panel__title", id: `${panelId}-title`, children: "Columns" }), _jsxs("span", { className: "nx-table-panel__count", children: [shownCount, " of ", all.length, " shown"] })] }), _jsx("ul", { className: "nx-columns__list nx-popover__scroll", "data-list": "", children: rows.map((row, index) => {
186
+ if (row.band === null)
187
+ return columnRow(row.column, "", index, false, false);
188
+ const band = row.band;
189
+ // A band whose columns were split into two runs is drawn twice, so a row is known by its first column, never
190
+ // by the band's name: two rows under one name would be one row to React.
191
+ const bandId = `band:${row.columns[0].key}`;
192
+ const shown = row.columns.filter(column => column.shown).length;
193
+ const movable = row.columns.every(column => column.movable);
194
+ const last = shown > 0 && shownCount - shown < 1;
195
+ return (_jsxs("li", { className: "nx-columns__row nx-columns__row--band", "data-held": drag?.key === bandId ? "" : undefined, "data-drop": drag && drag.list === "" && drag.key !== bandId && drag.to === index ? "" : undefined, children: [_jsxs("div", { className: "nx-columns__band", children: [movable ? (_jsx("button", { type: "button", className: "nx-columns__handle", "aria-label": `Move ${band}`, title: "Drag to move the whole band, or use the arrow keys", onPointerDown: event => onHandlePointerDown(event, "", index, bandId, band), onKeyDown: event => onHandleKeyDown(event, "", index, band), children: _jsx(InlineIcon, { icon: iconGripVertical }) })) : (_jsx("span", { className: "nx-columns__handle nx-columns__handle--fixed", "aria-hidden": "true" })), _jsxs("label", { className: "nx-choice nx-choice--sm nx-columns__choice", children: [_jsx("input", { type: "checkbox", className: "nx-choice__control nx-choice__control--checkbox", checked: shown > 0, disabled: last, ref: input => {
196
+ if (input)
197
+ input.indeterminate = shown > 0 && shown < row.columns.length;
198
+ }, "aria-label": `${band}: ${shown} of ${row.columns.length} shown`, onChange: event => toggleBand(band, row.columns, event.currentTarget.checked) }), _jsx("span", { className: "nx-choice__text", children: _jsx("span", { className: "nx-choice__label nx-columns__label", children: band }) })] }), _jsxs("span", { className: "nx-columns__band-count", children: [shown, " / ", row.columns.length] })] }), _jsx("ul", { className: "nx-columns__children", "data-list": band, children: row.columns.map((column, childIndex) => columnRow(column, band, childIndex, true, shown === 0)) })] }, bandId));
199
+ }) }), _jsxs("p", { className: "nx-columns__note", children: [reorderable ? "Drag a handle, or move it with the arrow keys." : "This table keeps the order of its columns.", bands ? " A column under a band moves inside it; the band moves as one." : ""] }), onReset && (_jsx("div", { className: "nx-table-panel__foot", children: _jsx(Button, { size: "sm", variant: "ghost", icon: _jsx(InlineIcon, { icon: iconArrowBackUp }), disabled: !changed, onClick: () => onReset(), children: "Reset columns" }) })), _jsx("span", { className: "nx-visually-hidden", "aria-live": "polite", children: said })] }) }));
243
200
  }
@@ -0,0 +1,21 @@
1
+ import { type Ref, type RefObject } from "react";
2
+ import { type PopoverPanelHandle } from "../../Helpers/PopoverHooks.ts";
3
+ import type { InputParams } from "../../Interfaces/FormInterfaces.ts";
4
+ import type { TableFilters } from "../../Interfaces/TableInterfaces.ts";
5
+ export interface TableFiltersPanelProps {
6
+ /** The filter inputs, as the table prepared them (`asFilterInput`). */
7
+ inputs: readonly InputParams[];
8
+ /** The table's filters: a change from outside, such as Clear filters, loads them into the form. */
9
+ value: TableFilters | undefined;
10
+ /** Milliseconds a typed value waits: the table's search delay. */
11
+ delay: number;
12
+ /** The rows the search and the filters leave, and every row, for the foot. */
13
+ matches: number;
14
+ total: number;
15
+ onChange: (values: Readonly<Record<string, unknown>>) => void;
16
+ /** The options button (⋮) the panel opens under; Esc gives the focus back to it. */
17
+ anchor: RefObject<HTMLElement | null>;
18
+ ref?: Ref<PopoverPanelHandle>;
19
+ }
20
+ /** The Filters panel, opened through its `open()` handle: a `Form` of the filter inputs, the count, and Clear all. */
21
+ export declare function TableFiltersPanel({ inputs, value, delay, matches, total, onChange, anchor, ref }: TableFiltersPanelProps): import("react").JSX.Element;
@@ -0,0 +1,68 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { iconFilterOff, InlineIcon } from "nexus-icons";
4
+ import { useEffect, useId, useRef } from "react";
5
+ import { usePopoverPanel } from "../../Helpers/PopoverHooks.js";
6
+ import { activeFilters } from "../../Helpers/TableFilters.js";
7
+ import { Button } from "../Documents/Button.js";
8
+ import { Form, useForm } from "../Forms/Form.js";
9
+ import { formatCount } from "./DataTableParts.js";
10
+ /*
11
+ * The Filters panel: the table's filter inputs as one small form under the options button (⋮), applied as they change.
12
+ * A choice applies at once; a value that can come a key or a step at a time (text, a number, a slider, a date) waits
13
+ * the table's search delay, or Enter, or the panel closing. Esc closes the panel, an open list or calendar first, and
14
+ * never clears a filter on its way out.
15
+ */
16
+ /** Inputs typed into or dragged: their value waits until the person stops. A date box reads a date from part of one. */
17
+ const TYPED = new Set(["text", "email", "search", "tel", "url", "textarea", "number", "slider", "range-slider", "date", "date-range", "time", "date-time"]);
18
+ /** The Filters panel, opened through its `open()` handle: a `Form` of the filter inputs, the count, and Clear all. */
19
+ export function TableFiltersPanel({ inputs, value, delay, matches, total, onChange, anchor, ref }) {
20
+ const titleId = useId();
21
+ const form = useForm();
22
+ // The form is drawn on the first open and kept: what was typed, and the labels a server list loaded, stay.
23
+ const { panelRef, shown, onToggle } = usePopoverPanel(anchor, ref, () => form.focus());
24
+ const pending = useRef(undefined);
25
+ const count = value ? Object.keys(value).length : 0;
26
+ const apply = () => {
27
+ clearTimeout(pending.current);
28
+ pending.current = undefined;
29
+ onChange(form.getValue());
30
+ };
31
+ const wait = () => {
32
+ clearTimeout(pending.current);
33
+ pending.current = setTimeout(apply, delay);
34
+ };
35
+ const flush = () => pending.current !== undefined && apply();
36
+ // Filters changed from outside reach the form, unless a typed value is still on its way.
37
+ useEffect(() => {
38
+ if (form.started && pending.current === undefined && JSON.stringify(activeFilters(inputs, form.getValue())) !== JSON.stringify(value))
39
+ form.load(value ?? {});
40
+ }, [form, inputs, value]);
41
+ useEffect(() => () => clearTimeout(pending.current), []);
42
+ return (_jsxs("div", { ref: panelRef, popover: "auto", role: "dialog", tabIndex: -1, "aria-labelledby": titleId, className: "nx-popover nx-table-panel nx-table-filters", onToggle: onToggle, onBeforeToggle: event => {
43
+ // Before the click that closes it lands: a clear that click makes then comes after what was typed.
44
+ if (event.target === event.currentTarget && event.newState === "closed")
45
+ flush();
46
+ }, onKeyDownCapture: event => {
47
+ // Kept from the inputs, whose Esc clears a value first: with no list or calendar open, Esc closes the panel.
48
+ if (event.key === "Escape" && !event.currentTarget.querySelector(":popover-open"))
49
+ event.stopPropagation();
50
+ }, onKeyDown: event => {
51
+ if (event.key === "Enter")
52
+ flush();
53
+ }, onInput: () => {
54
+ // A key that changes nothing the box reads ("2025-01-01 -") is typing too: a waiting value waits on.
55
+ if (pending.current !== undefined)
56
+ wait();
57
+ }, children: [_jsxs("div", { className: "nx-table-panel__head", children: [_jsx("span", { className: "nx-table-panel__title", id: titleId, children: "Filters" }), _jsx("span", { className: "nx-table-panel__count", children: count ? `${count} on` : "None" })] }), _jsx("div", { className: "nx-popover__scroll", children: shown && (_jsx(Form, { form: form, inputs: inputs, size: "sm", defaultValue: value, className: "nx-table-filters__form", onValueChange: (_, name) => {
58
+ if (name === null)
59
+ return;
60
+ if (delay > 0 && TYPED.has(inputs.find(params => params.name === name)?.type ?? ""))
61
+ wait();
62
+ else
63
+ apply();
64
+ } })) }), _jsxs("div", { className: "nx-table-panel__foot", children: [_jsxs("span", { className: "nx-table-filters__rows", "aria-live": "polite", children: [formatCount(matches), " of ", formatCount(total), " rows"] }), _jsx(Button, { size: "sm", variant: "ghost", icon: _jsx(InlineIcon, { icon: iconFilterOff }), disabled: !count, onClick: () => {
65
+ form.load({});
66
+ apply();
67
+ }, children: "Clear all" })] })] }));
68
+ }
@@ -1,10 +1,14 @@
1
1
  import { type IconNode } from "nexus-icons";
2
2
  import { type CSSProperties, type MouseEvent, type ReactNode } from "react";
3
3
  import { type CellTextOptions } from "../../Helpers/TableHelpers.ts";
4
- import type { TableColumn, TableRow, TableRowAction, TableState, TableViewColumn } from "../../Interfaces/TableInterfaces.ts";
4
+ import type { TableColumn, TableRow, TableRowAction, TableRowMark, TableState, TableViewColumn } from "../../Interfaces/TableInterfaces.ts";
5
5
  /** A number as the table writes counts: 1,234. */
6
6
  export declare const formatCount: (count: number) => string;
7
- /** A value in its column's format: text, a tag for an option with a tone, a check for `true`. Empty values show a dash. */
7
+ /**
8
+ * A value in its column's format: text, a tag for an option with a tone, a check for `true`. Empty values show a dash.
9
+ * A `join` column shows a placeholder until the record it looks up is in, and "Not found" for a key with no record; a
10
+ * column with a `tag` shows it after the value: "Asia" then "Trashed".
11
+ */
8
12
  export declare function TableCellValue<TRow extends TableRow>({ column, row, options }: {
9
13
  column: TableColumn<TRow>;
10
14
  row: TRow;
@@ -66,8 +70,8 @@ export interface TableRowViewProps<TRow extends TableRow> {
66
70
  busy: boolean;
67
71
  flashed: boolean;
68
72
  numbered: boolean;
69
- /** A row of the pinned section above the page: its number is a pin. */
70
- pinned?: boolean;
73
+ /** The row's marks (pinned, a favourite), worn one under the other where a row without any shows its number. */
74
+ marks?: readonly TableRowMark[];
71
75
  /** Where the table's own columns sit; `null` for the ones it does not have. */
72
76
  frame: TableFrameColumns;
73
77
  /** The user gave a column a width, so the table lays its columns out from the `<colgroup>`. */
@@ -80,13 +84,27 @@ export interface TableRowViewProps<TRow extends TableRow> {
80
84
  openOnClick?: boolean;
81
85
  }
82
86
  /** One row. It renders again only when its row, its state, or the columns change. */
83
- declare function TableRowViewInner<TRow extends TableRow>({ row, rowKey, number, label, columns, cellOptions, selectable, selected, selectionFull, busy, flashed, numbered, pinned, frame, sized, actions, onToggle, onAction, onOpen, openOnClick, }: TableRowViewProps<TRow>): import("react").JSX.Element;
87
+ declare function TableRowViewInner<TRow extends TableRow>({ row, rowKey, number, label, columns, cellOptions, selectable, selected, selectionFull, busy, flashed, numbered, marks, frame, sized, actions, onToggle, onAction, onOpen, openOnClick, }: TableRowViewProps<TRow>): import("react").JSX.Element;
84
88
  export declare const TableRowView: typeof TableRowViewInner;
85
89
  /**
86
- * The head of the pinned section: one row across the table with a button that folds the pinned records open and shut,
87
- * how many there are, and the most that can be pinned.
90
+ * The head of a section above the page: one row across the table with a button that folds the section's records open
91
+ * and shut, how many there are, and the most it can hold.
92
+ */
93
+ export declare function TableSectionHead({ span, label, icon, count, max, loading, open, onToggle }: {
94
+ span: number;
95
+ label: string;
96
+ icon: IconNode;
97
+ count: number;
98
+ max?: number;
99
+ loading: boolean;
100
+ open: boolean;
101
+ onToggle: () => void;
102
+ }): import("react").JSX.Element;
103
+ /**
104
+ * The pinned section's head, by its 3.1.0 name: a `TableSectionHead` with the pin icon.
105
+ * @deprecated since 3.2.0, when favourites became a second section: use `TableSectionHead`, which takes the icon.
88
106
  */
89
- export declare function TablePinnedHead({ span, label, count, max, loading, open, onToggle }: {
107
+ export declare function TablePinnedHead(props: {
90
108
  span: number;
91
109
  label: string;
92
110
  count: number;
@@ -127,8 +145,8 @@ export declare function TablePageSize({ pageSize, pageSizes, compact, onChange }
127
145
  onChange: (size: number) => void;
128
146
  }): import("react").JSX.Element | null;
129
147
  /**
130
- * Under the table: which rows show and how many the search left ("Showing 41–50 of 234 · filtered from 1,000"), the page
131
- * of how many, and the pager.
148
+ * Under the table: which rows show and how many the search and the filters left ("Showing 41–50 of 234 · filtered
149
+ * from 1,000"), the page of how many, and the pager.
132
150
  */
133
151
  export declare function TableFooter<TRow extends TableRow>({ state, disabled, onPage }: {
134
152
  state: TableState<TRow>;
@@ -1,21 +1,43 @@
1
1
  "use client";
2
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { iconCheck, iconChevronDown, iconChevronLeft, iconChevronRight, iconDotsVertical, iconLoader2, iconPin, InlineIcon } from "nexus-icons";
4
4
  import { memo } from "react";
5
5
  import { cx } from "../../Helpers/ClassHelpers.js";
6
6
  import { columnAlign, columnOption, columnValue, cssLength, formatCellText, pageButtons, pageCount, TABLE_ACTIONS_COLUMN, TABLE_INDEX_COLUMN, } from "../../Helpers/TableHelpers.js";
7
+ import { joinedState } from "../../Helpers/TableJoins.js";
7
8
  import { Button } from "../Documents/Button.js";
8
9
  import { Menu } from "../Documents/Menu.js";
9
10
  /*
10
11
  * The parts of a data table: a cell in its column's format, a row with its number and checkbox in one column and its
11
- * actions, the widths of the columns the user set, the head of the pinned section, the pager, and the footer. The
12
+ * actions, the widths of the columns the user set, the head of a section above the page, the pager, and the footer. The
12
13
  * details view of the CRUD draws values with the same `TableCellValue`.
13
14
  */
14
15
  const countFormat = new Intl.NumberFormat("en-US");
15
16
  /** A number as the table writes counts: 1,234. */
16
17
  export const formatCount = (count) => countFormat.format(count);
17
- /** A value in its column's format: text, a tag for an option with a tone, a check for `true`. Empty values show a dash. */
18
+ /**
19
+ * A value in its column's format: text, a tag for an option with a tone, a check for `true`. Empty values show a dash.
20
+ * A `join` column shows a placeholder until the record it looks up is in, and "Not found" for a key with no record; a
21
+ * column with a `tag` shows it after the value: "Asia" then "Trashed".
22
+ */
18
23
  export function TableCellValue({ column, row, options }) {
24
+ const joined = column.join ? joinedState(column.join, row) : null;
25
+ if (joined?.status === "loading")
26
+ return _jsx("span", { className: "nx-table__bone", "aria-label": "Loading" });
27
+ if (joined?.status === "missing" || joined?.status === "failed") {
28
+ return (_jsxs(_Fragment, { children: [_jsx("span", { className: "nx-table__dash", children: "\u2014" }), joined.status === "missing" && _jsx(TableTagView, { tag: { label: "Not found", tone: "danger" } })] }));
29
+ }
30
+ const tag = column.tag?.(row, joined?.status === "ready" ? joined.record : undefined);
31
+ const value = _jsx(TableCellContent, { column: column, row: row, options: options });
32
+ if (!tag)
33
+ return value;
34
+ return (_jsxs(_Fragment, { children: [value, _jsx(TableTagView, { tag: tag })] }));
35
+ }
36
+ /** A short tag after a cell's value, in the look of a column option's tag. */
37
+ function TableTagView({ tag }) {
38
+ return (_jsx("span", { className: "nx-table__tag nx-table__tag--after", "data-tone": tag.tone, children: tag.label }));
39
+ }
40
+ function TableCellContent({ column, row, options }) {
19
41
  const value = columnValue(column, row);
20
42
  if (column.render)
21
43
  return _jsx(_Fragment, { children: column.render(row, value) });
@@ -104,21 +126,28 @@ function opensRow(event, single) {
104
126
  return !(selection && !selection.isCollapsed && event.currentTarget.contains(selection.anchorNode));
105
127
  }
106
128
  /** One row. It renders again only when its row, its state, or the columns change. */
107
- function TableRowViewInner({ row, rowKey, number, label, columns, cellOptions, selectable, selected, selectionFull, busy, flashed, numbered, pinned, frame, sized, actions, onToggle, onAction, onOpen, openOnClick, }) {
129
+ function TableRowViewInner({ row, rowKey, number, label, columns, cellOptions, selectable, selected, selectionFull, busy, flashed, numbered, marks, frame, sized, actions, onToggle, onAction, onOpen, openOnClick, }) {
108
130
  const inline = actions?.filter(action => action.inline && action.icon) ?? [];
109
131
  const inMenu = actions?.filter(action => !(action.inline && action.icon)) ?? [];
110
132
  const items = inMenu.map(action => ({ value: action.id, label: action.label, icon: action.icon, description: action.description, tone: action.tone, disabled: action.disabled }));
111
- return (_jsxs("tr", { className: "nx-table__row", "data-key": rowKey, "data-pinned": pinned || undefined, "data-selected": selected || undefined, "data-busy": busy || undefined, "data-flash": flashed || undefined, "aria-busy": busy || undefined, "data-open": (onOpen && openOnClick) || undefined,
133
+ return (_jsxs("tr", { className: "nx-table__row", "data-key": rowKey, "data-selected": selected || undefined, "data-busy": busy || undefined, "data-flash": flashed || undefined, "aria-busy": busy || undefined, "data-open": (onOpen && openOnClick) || undefined,
112
134
  // One way in: with a single click the row is already open by the time a double-click would arrive.
113
- onClick: onOpen && openOnClick ? event => opensRow(event, true) && onOpen(row) : undefined, onDoubleClick: onOpen && !openOnClick ? event => opensRow(event, false) && onOpen(row) : undefined, children: [frame.index && (_jsx(TableIndexCell, { own: frame.index, number: pinned ? _jsx(InlineIcon, { icon: iconPin, className: "nx-table__pin-mark" }) : formatCount(number), numbered: numbered || Boolean(pinned), selectable: selectable, checkbox: selectable && (_jsx("label", { className: "nx-choice nx-choice--sm nx-table__check", title: selectionFull ? "No more rows can be selected." : undefined, children: _jsx("input", { type: "checkbox", className: "nx-choice__control nx-choice__control--checkbox", checked: selected, disabled: selectionFull && !selected, "aria-label": `Select ${label}`, onClick: event => onToggle(row, event.currentTarget.checked, event), onChange: () => undefined }) })) })), columns.map(view => (_jsx("td", { className: cx("nx-table__cell", `nx-table__cell--${columnAlign(view.column)}`, view.column.wrap && "nx-table__cell--wrap"), "data-col": view.column.key, "data-pin": view.pin ?? undefined, "data-pin-edge": (view.pin && view.edge) || undefined, style: columnStyle(view, sized), children: _jsx("span", { className: "nx-table__value", children: _jsx(TableCellValue, { column: view.column, row: row, options: cellOptions }) }) }, view.column.key))), sized && _jsx("td", { className: "nx-table__cell nx-table__cell--filler" }), frame.actions && (_jsx("td", { className: "nx-table__cell nx-table__cell--actions", "data-col": TABLE_ACTIONS_COLUMN, "data-pin": "right", "data-pin-edge": frame.actions.edge || undefined, style: ownStyle(frame.actions, "right"), children: _jsxs("div", { className: "nx-table__actions", children: [inline.map(action => (_jsx(Button, { variant: "ghost", size: "sm", icon: action.icon, hoverTone: action.tone === "danger" ? "danger" : undefined, "aria-label": `${action.label}: ${label}`, title: action.label, "data-action": action.id, disabled: busy || action.disabled, onClick: () => onAction(row, action) }, action.id))), items.length > 0 && (_jsx(Menu, { items: items, align: "end", "aria-label": `Actions: ${label}`, onSelect: value => onAction(row, inMenu.find(action => action.id === value)), children: _jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconDotsVertical }), loading: busy, "aria-label": `More actions: ${label}`, title: "More actions" }) })), items.length === 0 && busy && _jsx(Button, { variant: "ghost", size: "sm", loading: true, "aria-label": `Working: ${label}` })] }) }))] }));
135
+ onClick: onOpen && openOnClick ? event => opensRow(event, true) && onOpen(row) : undefined, onDoubleClick: onOpen && !openOnClick ? event => opensRow(event, false) && onOpen(row) : undefined, children: [frame.index && (_jsx(TableIndexCell, { own: frame.index, number: marks?.length ? (_jsx("span", { className: "nx-table__marks", children: marks.map(mark => (_jsx(InlineIcon, { icon: mark.icon, className: "nx-table__mark", label: mark.label }, mark.label))) })) : (formatCount(number)), numbered: numbered || Boolean(marks?.length), selectable: selectable, checkbox: selectable && (_jsx("label", { className: "nx-choice nx-choice--sm nx-table__check", title: selectionFull ? "No more rows can be selected." : undefined, children: _jsx("input", { type: "checkbox", className: "nx-choice__control nx-choice__control--checkbox", checked: selected, disabled: selectionFull && !selected, "aria-label": `Select ${label}`, onClick: event => onToggle(row, event.currentTarget.checked, event), onChange: () => undefined }) })) })), columns.map(view => (_jsx("td", { className: cx("nx-table__cell", `nx-table__cell--${columnAlign(view.column)}`, view.column.wrap && "nx-table__cell--wrap"), "data-col": view.column.key, "data-pin": view.pin ?? undefined, "data-pin-edge": (view.pin && view.edge) || undefined, style: columnStyle(view, sized), children: _jsx("span", { className: "nx-table__value", children: _jsx(TableCellValue, { column: view.column, row: row, options: cellOptions }) }) }, view.column.key))), sized && _jsx("td", { className: "nx-table__cell nx-table__cell--filler" }), frame.actions && (_jsx("td", { className: "nx-table__cell nx-table__cell--actions", "data-col": TABLE_ACTIONS_COLUMN, "data-pin": "right", "data-pin-edge": frame.actions.edge || undefined, style: ownStyle(frame.actions, "right"), children: _jsxs("div", { className: "nx-table__actions", children: [inline.map(action => (_jsx(Button, { variant: "ghost", size: "sm", icon: action.icon, hoverTone: action.tone === "danger" ? "danger" : undefined, "aria-label": `${action.label}: ${label}`, title: action.label, "data-action": action.id, disabled: busy || action.disabled, onClick: () => onAction(row, action) }, action.id))), items.length > 0 && (_jsx(Menu, { items: items, align: "end", "aria-label": `Actions: ${label}`, onSelect: value => onAction(row, inMenu.find(action => action.id === value)), children: _jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconDotsVertical }), loading: busy, "aria-label": `More actions: ${label}`, title: "More actions" }) })), items.length === 0 && busy && _jsx(Button, { variant: "ghost", size: "sm", loading: true, "aria-label": `Working: ${label}` })] }) }))] }));
114
136
  }
115
137
  export const TableRowView = memo(TableRowViewInner);
116
138
  /**
117
- * The head of the pinned section: one row across the table with a button that folds the pinned records open and shut,
118
- * how many there are, and the most that can be pinned.
139
+ * The head of a section above the page: one row across the table with a button that folds the section's records open
140
+ * and shut, how many there are, and the most it can hold.
141
+ */
142
+ export function TableSectionHead({ span, label, icon, count, max, loading, open, onToggle }) {
143
+ return (_jsx("tr", { className: "nx-table__row nx-table__section-row", children: _jsx("td", { colSpan: span, className: "nx-table__section-cell", children: _jsxs("button", { type: "button", className: "nx-table__section-toggle", "aria-expanded": open, onClick: onToggle, children: [_jsx(InlineIcon, { icon: iconChevronRight, className: "nx-table__section-caret" }), _jsx(InlineIcon, { icon: icon, className: "nx-table__section-icon" }), _jsx("span", { className: "nx-table__section-title", children: label }), (count > 0 || !loading) && (_jsx("span", { className: "nx-table__section-count", children: max ? `${formatCount(count)} of ${formatCount(max)}` : formatCount(count) })), loading && _jsx(InlineIcon, { icon: iconLoader2, className: "nx-spin nx-table__section-loading" })] }) }) }));
144
+ }
145
+ /**
146
+ * The pinned section's head, by its 3.1.0 name: a `TableSectionHead` with the pin icon.
147
+ * @deprecated since 3.2.0, when favourites became a second section: use `TableSectionHead`, which takes the icon.
119
148
  */
120
- export function TablePinnedHead({ span, label, count, max, loading, open, onToggle }) {
121
- return (_jsx("tr", { className: "nx-table__row nx-table__pin-row", children: _jsx("td", { colSpan: span, className: "nx-table__pin-cell", children: _jsxs("button", { type: "button", className: "nx-table__pin-toggle", "aria-expanded": open, onClick: onToggle, "data-table-pins": true, children: [_jsx(InlineIcon, { icon: iconChevronRight, className: "nx-table__pin-caret" }), _jsx(InlineIcon, { icon: iconPin, className: "nx-table__pin-icon" }), _jsx("span", { className: "nx-table__pin-title", children: label }), (count > 0 || !loading) && (_jsx("span", { className: "nx-table__pin-count", children: max ? `${formatCount(count)} of ${formatCount(max)}` : formatCount(count) })), loading && _jsx(InlineIcon, { icon: iconLoader2, className: "nx-spin nx-table__pin-loading" })] }) }) }));
149
+ export function TablePinnedHead(props) {
150
+ return _jsx(TableSectionHead, { ...props, icon: iconPin });
122
151
  }
123
152
  /** Placeholder rows while the first rows load, in the shape of the columns. */
124
153
  export function TableSkeletonRows({ count, cells, frame, sized }) {
@@ -143,8 +172,8 @@ export function TablePageSize({ pageSize, pageSizes, compact = false, onChange }
143
172
  return (_jsx(Menu, { items: items, kind: "radio", size: "sm", align: compact ? "start" : "end", "aria-label": "Rows per page", onSelect: value => onChange(Number(value)), children: _jsx(Button, { variant: "ghost", size: "sm", iconEnd: _jsx(InlineIcon, { icon: iconChevronDown }), className: cx("nx-table__page-size", compact && "nx-table__page-size--field"), "aria-label": `Rows per page: ${pageSize}`, title: "Rows per page", children: compact ? formatCount(pageSize) : `${formatCount(pageSize)} / page` }) }));
144
173
  }
145
174
  /**
146
- * Under the table: which rows show and how many the search left ("Showing 41–50 of 234 · filtered from 1,000"), the page
147
- * of how many, and the pager.
175
+ * Under the table: which rows show and how many the search and the filters left ("Showing 41–50 of 234 · filtered
176
+ * from 1,000"), the page of how many, and the pager.
148
177
  */
149
178
  export function TableFooter({ state, disabled, onPage }) {
150
179
  const { status, rows, offset, matches, total, query } = state;
@@ -153,5 +182,5 @@ export function TableFooter({ state, disabled, onPage }) {
153
182
  const first = rows.length ? offset + 1 : 0;
154
183
  const last = offset + rows.length;
155
184
  const pages = pageCount(matches, query.pageSize);
156
- return (_jsxs("div", { className: "nx-table__footer", children: [_jsxs("p", { className: "nx-table__range", "aria-live": "polite", children: [rows.length ? (_jsxs(_Fragment, { children: ["Showing ", _jsx("b", { children: `${formatCount(first)}–${formatCount(last)}` }), " of ", _jsx("b", { children: formatCount(matches) })] })) : (_jsx(_Fragment, { children: "No rows on this page" })), query.search && matches !== total && _jsxs("span", { className: "nx-table__range-all", children: [" \u00B7 filtered from ", formatCount(total)] })] }), _jsxs("div", { className: "nx-table__paging", children: [_jsxs("span", { className: "nx-table__page-info", children: ["Page ", formatCount(query.page), " of ", formatCount(pages)] }), _jsx(TablePager, { page: query.page, matches: matches, pageSize: query.pageSize, disabled: disabled, onPage: onPage })] })] }));
185
+ return (_jsxs("div", { className: "nx-table__footer", children: [_jsxs("p", { className: "nx-table__range", "aria-live": "polite", children: [rows.length ? (_jsxs(_Fragment, { children: ["Showing ", _jsx("b", { children: `${formatCount(first)}–${formatCount(last)}` }), " of ", _jsx("b", { children: formatCount(matches) })] })) : (_jsx(_Fragment, { children: "No rows on this page" })), (query.search || query.filters) && matches !== total && _jsxs("span", { className: "nx-table__range-all", children: [" \u00B7 filtered from ", formatCount(total)] })] }), _jsxs("div", { className: "nx-table__paging", children: [_jsxs("span", { className: "nx-table__page-info", children: ["Page ", formatCount(query.page), " of ", formatCount(pages)] }), _jsx(TablePager, { page: query.page, matches: matches, pageSize: query.pageSize, disabled: disabled, onPage: onPage })] })] }));
157
186
  }
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Whether things may move, as the stylesheets read it: `data-nx-motion` on `<html>`, which `ThemeScript` works out
3
+ * from the visitor's Animations choice and the system setting. Motion started from script (a flight, a smooth scroll)
4
+ * asks this, never `prefers-reduced-motion`, so "on" animates on a machine that reports reduced motion.
5
+ */
6
+ export declare function motionAllowed(): boolean;
7
+ /**
8
+ * Whether a scroll may be smooth: the visitor allows motion, and so does the system, because a browser whose system
9
+ * turned animations off scrolls at once whatever it is asked, and a smooth scroll that is skipped must not be waited on.
10
+ */
11
+ export declare function smoothScrollAllowed(): boolean;
1
12
  /**
2
13
  * Calls `done` once the element's CSS animation `name` has finished, or at once when the element runs no animation of
3
14
  * that name. Call it after the class that starts the animation is on the element, e.g. in a layout effect.
@@ -1,8 +1,26 @@
1
1
  /*
2
2
  * Exit animations. A closing element gets a class that starts a CSS animation, and the next step (closing a dialog,
3
- * removing a toast) waits for it. When no animation runs, the step must not wait: with reduced motion (which Windows
4
- * reports when its animations are turned off) or when a theme sets `animation: none`.
3
+ * removing a toast) waits for it. When no animation runs, the step must not wait: with animations off (the visitor's
4
+ * choice, or the system's under "system" - Windows reports reduced motion when its animations are turned off) or when
5
+ * a theme sets `animation: none`.
5
6
  */
7
+ import { MOTION_ATTRIBUTE } from "../Interfaces/ThemeInterfaces.js";
8
+ import { REDUCED_MOTION_QUERY } from "./ThemeHelpers.js";
9
+ /**
10
+ * Whether things may move, as the stylesheets read it: `data-nx-motion` on `<html>`, which `ThemeScript` works out
11
+ * from the visitor's Animations choice and the system setting. Motion started from script (a flight, a smooth scroll)
12
+ * asks this, never `prefers-reduced-motion`, so "on" animates on a machine that reports reduced motion.
13
+ */
14
+ export function motionAllowed() {
15
+ return typeof document === "undefined" || document.documentElement.getAttribute(MOTION_ATTRIBUTE) !== "reduce";
16
+ }
17
+ /**
18
+ * Whether a scroll may be smooth: the visitor allows motion, and so does the system, because a browser whose system
19
+ * turned animations off scrolls at once whatever it is asked, and a smooth scroll that is skipped must not be waited on.
20
+ */
21
+ export function smoothScrollAllowed() {
22
+ return motionAllowed() && !(typeof window !== "undefined" && window.matchMedia?.(REDUCED_MOTION_QUERY).matches);
23
+ }
6
24
  /**
7
25
  * Calls `done` once the element's CSS animation `name` has finished, or at once when the element runs no animation of
8
26
  * that name. Call it after the class that starts the animation is on the element, e.g. in a layout effect.
@@ -1,5 +1,5 @@
1
1
  import { NexusModule, type ApiEndpoint, type ApiModuleInfo, type ApiModuleValue } from "../Interfaces/ApiInterfaces.ts";
2
- import { apiEndpoint, type ApiEndpointExtras, type ApiUrlParts } from "./ApiRoutes.ts";
2
+ import { apiEndpoint, type ApiControllerOptions, type ApiEndpointExtras, type ApiUrlParts } from "./ApiRoutes.ts";
3
3
  /** Where the proxy route sits in an app: `app/api/app/[module]/[...path]/route.ts`. */
4
4
  export declare const DEFAULT_PROXY_PATH = "/api/app";
5
5
  /** An environment variable, read when it is needed (never inlined at build time), so one build serves every environment. */
@@ -67,39 +67,8 @@ export declare abstract class ApiModules<TModule extends ApiModuleValue = ApiMod
67
67
  protected readEnv(name: string | undefined): string | undefined;
68
68
  /** Endpoints typed to this app's modules: `MODULES.endpoint.get(HrModule.Staff, "Employee/Birthdays")`. */
69
69
  readonly endpoint: Record<keyof typeof apiEndpoint, (module: TModule, path: string, extras?: ApiEndpointExtras) => ApiEndpoint<TModule>>;
70
- /** A controller's standard endpoints, typed to this app's modules. See `apiController`. */
71
- controller(module: TModule, controller: string, subject?: string): {
72
- module: TModule;
73
- controller: string;
74
- listing: ApiEndpoint<TModule>;
75
- activeListing: ApiEndpoint<TModule>;
76
- findListing: ApiEndpoint<TModule>;
77
- pagination: ApiEndpoint<TModule>;
78
- lists: Record<import("../Shared.Index.ts").CrudMode, ApiEndpoint<TModule>>;
79
- pages: Record<import("../Shared.Index.ts").CrudMode, ApiEndpoint<TModule>>;
80
- pins: ApiEndpoint<TModule>;
81
- details: ApiEndpoint<TModule>;
82
- create: ApiEndpoint<TModule>;
83
- update: ApiEndpoint<TModule>;
84
- delete: ApiEndpoint<TModule>;
85
- trash: ApiEndpoint<TModule>;
86
- trashFromDelete: ApiEndpoint<TModule>;
87
- recover: ApiEndpoint<TModule>;
88
- flag: ApiEndpoint<TModule>;
89
- pin: ApiEndpoint<TModule>;
90
- audit: ApiEndpoint<TModule>;
91
- accessActions: ApiEndpoint<TModule>;
92
- mass: {
93
- description: ApiEndpoint<TModule>;
94
- trash: ApiEndpoint<TModule>;
95
- trashFromDelete: ApiEndpoint<TModule>;
96
- flag: ApiEndpoint<TModule>;
97
- pin: ApiEndpoint<TModule>;
98
- delete: ApiEndpoint<TModule>;
99
- recover: ApiEndpoint<TModule>;
100
- };
101
- action: (method: import("../Shared.Index.ts").HttpMethod, action: string, extras?: Partial<Omit<ApiEndpoint<number>, "path" | "method" | "module">> | undefined) => ApiEndpoint<TModule>;
102
- };
70
+ /** A controller's standard endpoints, typed to this app's modules: its generation and what it lacks in `options`. See `apiController`. */
71
+ controller(module: TModule, controller: string, subject?: string, options?: ApiControllerOptions): import("./ApiRoutes.ts").ApiControllerEndpoints<TModule>;
103
72
  }
104
73
  /** The Nexus modules, their names in proxy URLs, and the environment variables with their roots (as in the previous project). */
105
74
  export declare const NEXUS_MODULES: readonly ApiModuleInfo<NexusModule>[];
@@ -103,9 +103,9 @@ export class ApiModules {
103
103
  }
104
104
  /** Endpoints typed to this app's modules: `MODULES.endpoint.get(HrModule.Staff, "Employee/Birthdays")`. */
105
105
  endpoint = apiEndpoint;
106
- /** A controller's standard endpoints, typed to this app's modules. See `apiController`. */
107
- controller(module, controller, subject) {
108
- return apiController(module, controller, subject);
106
+ /** A controller's standard endpoints, typed to this app's modules: its generation and what it lacks in `options`. See `apiController`. */
107
+ controller(module, controller, subject, options) {
108
+ return apiController(module, controller, subject, options);
109
109
  }
110
110
  }
111
111
  /** The Nexus modules, their names in proxy URLs, and the environment variables with their roots (as in the previous project). */