nexus-shared 2.0.0 → 3.1.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.
- package/CHANGELOG.md +446 -0
- package/LICENSE +21 -21
- package/README.md +1084 -903
- package/dist/Client.Index.d.ts +14 -0
- package/dist/Client.Index.js +14 -0
- package/dist/Components/Chats/Chat.d.ts +28 -0
- package/dist/Components/Chats/Chat.js +19 -0
- package/dist/Components/Chats/ChatButton.d.ts +26 -0
- package/dist/Components/Chats/ChatButton.js +41 -0
- package/dist/Components/Chats/ChatComposer.d.ts +41 -0
- package/dist/Components/Chats/ChatComposer.js +177 -0
- package/dist/Components/Chats/ChatConversations.d.ts +35 -0
- package/dist/Components/Chats/ChatConversations.js +38 -0
- package/dist/Components/Chats/ChatPanel.d.ts +66 -0
- package/dist/Components/Chats/ChatPanel.js +94 -0
- package/dist/Components/Chats/ChatParts.d.ts +87 -0
- package/dist/Components/Chats/ChatParts.js +96 -0
- package/dist/Components/Chats/ChatThread.d.ts +22 -0
- package/dist/Components/Chats/ChatThread.js +96 -0
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- package/dist/Components/Documents/Menu.js +44 -28
- package/dist/Components/Documents/SplitButton.d.ts +3 -1
- package/dist/Components/Documents/SplitButton.js +12 -8
- package/dist/Components/Documents/TabButtons.d.ts +29 -8
- package/dist/Components/Documents/TabButtons.js +24 -5
- package/dist/Components/Forms/ApiForm.d.ts +6 -4
- package/dist/Components/Forms/ApiForm.js +15 -14
- package/dist/Components/Forms/Crud.js +342 -71
- package/dist/Components/Forms/CrudParts.d.ts +5 -1
- package/dist/Components/Forms/CrudParts.js +37 -5
- package/dist/Components/Forms/ExcelImport.d.ts +58 -0
- package/dist/Components/Forms/ExcelImport.js +319 -0
- package/dist/Components/Forms/Form.js +9 -13
- package/dist/Components/Forms/SubmitForm.d.ts +11 -2
- package/dist/Components/Forms/SubmitForm.js +23 -12
- package/dist/Components/Inputs/DateTimePicker.js +2 -1
- package/dist/Components/Inputs/DropdownParts.d.ts +1 -1
- package/dist/Components/Inputs/DropdownParts.js +4 -6
- package/dist/Components/Inputs/GroupForm.js +4 -2
- package/dist/Components/Inputs/InputRenderer.d.ts +2 -0
- package/dist/Components/Inputs/InputRenderer.js +1 -1
- package/dist/Components/Inputs/NumberBox.js +24 -2
- package/dist/Components/Inputs/ReadOnlyNotice.js +2 -1
- package/dist/Components/Inputs/RowsInput.d.ts +12 -1
- package/dist/Components/Inputs/RowsInput.js +55 -6
- package/dist/Components/Inputs/Slider.js +4 -1
- package/dist/Components/Inputs/TabularForm.d.ts +7 -3
- package/dist/Components/Inputs/TabularForm.js +117 -20
- package/dist/Components/Inputs/TimePanel.js +4 -3
- package/dist/Components/Inputs/TimePicker.js +2 -1
- package/dist/Components/Layouts/Popup.js +1 -4
- package/dist/Components/Layouts/ThemePicker.d.ts +20 -3
- package/dist/Components/Layouts/ThemePicker.js +77 -6
- package/dist/Components/Layouts/ThemeScript.d.ts +4 -2
- package/dist/Components/Layouts/ThemeScript.js +4 -2
- package/dist/Components/Layouts/ThemeSwitcher.d.ts +6 -3
- package/dist/Components/Layouts/ThemeSwitcher.js +39 -18
- package/dist/Components/Viewers/Avatar.d.ts +21 -0
- package/dist/Components/Viewers/Avatar.js +21 -0
- package/dist/Components/Viewers/DataTable.js +262 -60
- package/dist/Components/Viewers/DataTableColumns.d.ts +32 -0
- package/dist/Components/Viewers/DataTableColumns.js +200 -0
- package/dist/Components/Viewers/DataTableFilters.d.ts +21 -0
- package/dist/Components/Viewers/DataTableFilters.js +68 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +8 -4
- package/dist/Components/Viewers/DataTableParts.js +24 -12
- package/dist/Helpers/AnimationHelpers.d.ts +11 -0
- package/dist/Helpers/AnimationHelpers.js +20 -2
- package/dist/Helpers/ApiClient.d.ts +22 -0
- package/dist/Helpers/ApiClient.js +4 -1
- package/dist/Helpers/ApiFormHelpers.d.ts +27 -4
- package/dist/Helpers/ApiFormHelpers.js +73 -9
- package/dist/Helpers/ApiModules.d.ts +0 -4
- package/dist/Helpers/ApiModules.js +1 -0
- package/dist/Helpers/ApiResponses.d.ts +14 -4
- package/dist/Helpers/ApiResponses.js +116 -25
- package/dist/Helpers/ApiRoutes.d.ts +0 -10
- package/dist/Helpers/ApiRoutes.js +2 -10
- package/dist/Helpers/ChatBackend.d.ts +134 -0
- package/dist/Helpers/ChatBackend.js +332 -0
- package/dist/Helpers/ChatHelpers.d.ts +189 -0
- package/dist/Helpers/ChatHelpers.js +486 -0
- package/dist/Helpers/ChatHooks.d.ts +81 -0
- package/dist/Helpers/ChatHooks.js +175 -0
- package/dist/Helpers/ChatStore.d.ts +132 -0
- package/dist/Helpers/ChatStore.js +394 -0
- package/dist/Helpers/ChatSummaries.d.ts +39 -0
- package/dist/Helpers/ChatSummaries.js +118 -0
- package/dist/Helpers/CrudBackend.d.ts +54 -9
- package/dist/Helpers/CrudBackend.js +103 -6
- package/dist/Helpers/CrudHelpers.d.ts +39 -4
- package/dist/Helpers/CrudHelpers.js +76 -11
- package/dist/Helpers/ExcelBackend.d.ts +106 -0
- package/dist/Helpers/ExcelBackend.js +296 -0
- package/dist/Helpers/ExcelHelpers.d.ts +126 -0
- package/dist/Helpers/ExcelHelpers.js +344 -0
- package/dist/Helpers/FormStore.d.ts +11 -5
- package/dist/Helpers/FormStore.js +44 -6
- package/dist/Helpers/MessageBuilder.js +1 -2
- package/dist/Helpers/NumberHelpers.d.ts +7 -1
- package/dist/Helpers/NumberHelpers.js +41 -3
- package/dist/Helpers/PopoverHelpers.d.ts +46 -9
- package/dist/Helpers/PopoverHelpers.js +163 -24
- package/dist/Helpers/PopoverHooks.d.ts +24 -0
- package/dist/Helpers/PopoverHooks.js +62 -0
- package/dist/Helpers/RowsStore.d.ts +14 -3
- package/dist/Helpers/RowsStore.js +37 -3
- package/dist/Helpers/TableColumns.d.ts +7 -0
- package/dist/Helpers/TableColumns.js +41 -13
- package/dist/Helpers/TableExport.js +12 -12
- package/dist/Helpers/TableFilters.d.ts +25 -0
- package/dist/Helpers/TableFilters.js +162 -0
- package/dist/Helpers/TableHelpers.d.ts +15 -4
- package/dist/Helpers/TableHelpers.js +28 -4
- package/dist/Helpers/TableStore.d.ts +11 -3
- package/dist/Helpers/TableStore.js +16 -5
- package/dist/Helpers/ThemeHelpers.d.ts +21 -4
- package/dist/Helpers/ThemeHelpers.js +46 -9
- package/dist/Helpers/UserPreferences.d.ts +14 -0
- package/dist/Helpers/UserPreferences.js +99 -0
- package/dist/Interfaces/ApiInterfaces.d.ts +29 -4
- package/dist/Interfaces/ApiInterfaces.js +16 -10
- package/dist/Interfaces/ChatInterfaces.d.ts +240 -0
- package/dist/Interfaces/ChatInterfaces.js +1 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +92 -12
- package/dist/Interfaces/ExcelInterfaces.d.ts +227 -0
- package/dist/Interfaces/ExcelInterfaces.js +45 -0
- package/dist/Interfaces/FormInterfaces.d.ts +85 -7
- package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
- package/dist/Interfaces/PreferenceInterfaces.d.ts +43 -0
- package/dist/Interfaces/PreferenceInterfaces.js +28 -0
- package/dist/Interfaces/TableInterfaces.d.ts +116 -9
- package/dist/Interfaces/ThemeInterfaces.d.ts +25 -0
- package/dist/Interfaces/ThemeInterfaces.js +15 -0
- package/dist/Services/BrowserApi.d.ts +18 -1
- package/dist/Services/BrowserApi.js +18 -0
- package/dist/Services/ChatApi.d.ts +37 -0
- package/dist/Services/ChatApi.js +98 -0
- package/dist/Services/ExcelApi.d.ts +48 -0
- package/dist/Services/ExcelApi.js +196 -0
- package/dist/Services/ServerApi.d.ts +6 -1
- package/dist/Services/ServerApi.js +3 -0
- package/dist/Services/ThemeService.d.ts +18 -2
- package/dist/Services/ThemeService.js +80 -16
- package/dist/Shared.Index.d.ts +10 -0
- package/dist/Shared.Index.js +10 -0
- package/package.json +85 -82
- package/src/Styles/Nexus.Avatar.css +79 -0
- package/src/Styles/Nexus.Base.css +117 -70
- package/src/Styles/Nexus.Button.css +180 -151
- package/src/Styles/Nexus.Calendar.css +273 -275
- package/src/Styles/Nexus.Chat.css +1443 -0
- package/src/Styles/Nexus.Choice.css +343 -345
- package/src/Styles/Nexus.Crud.css +168 -84
- package/src/Styles/Nexus.DateTime.css +81 -81
- package/src/Styles/Nexus.Dropdown.css +725 -774
- package/src/Styles/Nexus.Editor.css +295 -297
- package/src/Styles/Nexus.Excel.css +389 -0
- package/src/Styles/Nexus.Form.css +279 -298
- package/src/Styles/Nexus.Index.css +37 -34
- package/src/Styles/Nexus.Input.css +602 -637
- package/src/Styles/Nexus.Markdown.css +237 -265
- package/src/Styles/Nexus.Menu.css +237 -249
- package/src/Styles/Nexus.Popup.css +724 -731
- package/src/Styles/Nexus.RichText.css +2 -32
- package/src/Styles/Nexus.Rows.css +637 -566
- package/src/Styles/Nexus.Slider.css +276 -278
- package/src/Styles/Nexus.Tab.Buttons.css +432 -364
- package/src/Styles/Nexus.Table.css +1386 -1014
- package/src/Styles/Nexus.Theme.Picker.css +286 -159
- package/src/Styles/Nexus.Themes.css +194 -194
- package/src/Styles/Nexus.Time.css +144 -147
- package/src/Styles/Nexus.Toast.css +263 -265
- package/src/Styles/Nexus.Tokens.css +89 -72
- package/src/Styles/Nexus.Tree.css +431 -435
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { iconArrowBackUp, iconGripVertical, iconPin, InlineIcon } from "nexus-icons";
|
|
4
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
5
|
+
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
|
+
import { isPopoverOpen } from "../../Helpers/PopoverHelpers.js";
|
|
7
|
+
import { usePopoverPanel } from "../../Helpers/PopoverHooks.js";
|
|
8
|
+
import { Button } from "../Documents/Button.js";
|
|
9
|
+
/** The rows of the panel: a band for every run of neighbours that name the same group, as the header draws them. */
|
|
10
|
+
function panelRows(columns, groups) {
|
|
11
|
+
const rows = [];
|
|
12
|
+
for (const column of columns) {
|
|
13
|
+
const group = groups.get(column.key);
|
|
14
|
+
const last = rows[rows.length - 1];
|
|
15
|
+
if (group && last && last.band === group)
|
|
16
|
+
last.columns.push(column);
|
|
17
|
+
else if (group)
|
|
18
|
+
rows.push({ band: group, columns: [column] });
|
|
19
|
+
else
|
|
20
|
+
rows.push({ band: null, column });
|
|
21
|
+
}
|
|
22
|
+
return rows;
|
|
23
|
+
}
|
|
24
|
+
/** The keys of a row, in order: one column, or every column of a band. */
|
|
25
|
+
function rowKeys(row) {
|
|
26
|
+
return row.band === null ? [row.column.key] : row.columns.map(column => column.key);
|
|
27
|
+
}
|
|
28
|
+
/** Moves `from` to `to` in a list. */
|
|
29
|
+
function moveItem(items, from, to) {
|
|
30
|
+
const next = [...items];
|
|
31
|
+
const [item] = next.splice(from, 1);
|
|
32
|
+
if (item === undefined)
|
|
33
|
+
return next;
|
|
34
|
+
next.splice(to, 0, item);
|
|
35
|
+
return next;
|
|
36
|
+
}
|
|
37
|
+
/** Which place the pointer is over, by the middle of each row: the index the row held would take. */
|
|
38
|
+
function dropIndex(boxes, y, from) {
|
|
39
|
+
for (let index = 0; index < boxes.length; index++) {
|
|
40
|
+
const box = boxes[index];
|
|
41
|
+
if (y < box.top + box.height / 2)
|
|
42
|
+
return index > from ? index - 1 : index;
|
|
43
|
+
}
|
|
44
|
+
return boxes.length - 1;
|
|
45
|
+
}
|
|
46
|
+
/** The text of a label that may be a node, for an accessible name. */
|
|
47
|
+
function textOf(label) {
|
|
48
|
+
return typeof label === "string" || typeof label === "number" ? String(label) : "the column";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The Columns panel, opened through its `open()` handle under `anchor`: a checkbox per column, bands with their columns under
|
|
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,
|
|
53
|
+
* on a click outside, and on Tab.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* <TableColumnsPanel columns={columns} hidden={state.hidden} pinned={state.pinned}
|
|
57
|
+
* onToggle={(key, hide) => table.setHidden(key, hide)} onOrder={keys => table.setOrder(keys)} />
|
|
58
|
+
*/
|
|
59
|
+
export function TableColumnsPanel({ columns, hidden, pinned, reorderable = true, size = "md", changed = false, onToggle, onOrder, onReset, anchor, ref, }) {
|
|
60
|
+
const id = useId();
|
|
61
|
+
const panelId = `${id}-columns`;
|
|
62
|
+
const { panelRef, open, onToggle: onPanelToggle } = usePopoverPanel(anchor, ref);
|
|
63
|
+
// While a row is held: its key, the list it may move in, and where it would land.
|
|
64
|
+
const [drag, setDrag] = useState(null);
|
|
65
|
+
const [said, setSaid] = useState("");
|
|
66
|
+
// The columns of a band that were already hidden when its checkbox was cleared: shown again, the band keeps them off.
|
|
67
|
+
const bandMemory = useRef(new Map());
|
|
68
|
+
const all = columns.map(column => ({
|
|
69
|
+
key: column.key,
|
|
70
|
+
label: column.label,
|
|
71
|
+
shown: !hidden.has(column.key),
|
|
72
|
+
pin: pinned[column.key] ?? column.pinned ?? null,
|
|
73
|
+
movable: reorderable && column.movable !== false,
|
|
74
|
+
hideable: column.hideable !== false,
|
|
75
|
+
}));
|
|
76
|
+
const groups = new Map(columns.map(column => [column.key, column.group]));
|
|
77
|
+
const rows = panelRows(all, groups);
|
|
78
|
+
const shownCount = all.filter(column => column.shown).length;
|
|
79
|
+
const bands = rows.some(row => row.band !== null);
|
|
80
|
+
// A panel opened again starts without the last move's message, so a screen reader is not told about an old move.
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!open)
|
|
83
|
+
setSaid("");
|
|
84
|
+
}, [open]);
|
|
85
|
+
/** The whole order after moving a row of `list` ("" for the panel itself, a band's name for its own columns). */
|
|
86
|
+
function moved(list, from, to) {
|
|
87
|
+
if (list === "") {
|
|
88
|
+
const next = moveItem(rows, from, to);
|
|
89
|
+
return next.flatMap(rowKeys);
|
|
90
|
+
}
|
|
91
|
+
const band = rows.find(row => row.band === list);
|
|
92
|
+
if (!band || band.band === null)
|
|
93
|
+
return null;
|
|
94
|
+
const next = moveItem(band.columns, from, to);
|
|
95
|
+
return rows.flatMap(row => (row.band === list ? next.map(column => column.key) : rowKeys(row)));
|
|
96
|
+
}
|
|
97
|
+
function move(list, from, to, label) {
|
|
98
|
+
const length = list === "" ? rows.length : rows.find(row => row.band === list)?.columns.length;
|
|
99
|
+
if (!length)
|
|
100
|
+
return;
|
|
101
|
+
const target = Math.min(Math.max(to, 0), length - 1);
|
|
102
|
+
if (target === from)
|
|
103
|
+
return;
|
|
104
|
+
const order = moved(list, from, target);
|
|
105
|
+
if (!order)
|
|
106
|
+
return;
|
|
107
|
+
onOrder(order);
|
|
108
|
+
setSaid(list === "" ? `${label} is now ${target + 1} of ${length}.` : `${label} is now ${target + 1} of ${length} in ${list}.`);
|
|
109
|
+
}
|
|
110
|
+
function onHandleKeyDown(event, list, index, label) {
|
|
111
|
+
const length = list === "" ? rows.length : (rows.find(row => row.band === list)?.columns.length ?? 0);
|
|
112
|
+
const step = event.key === "ArrowUp" ? -1 : event.key === "ArrowDown" ? 1 : event.key === "Home" ? -index : event.key === "End" ? length - 1 - index : 0;
|
|
113
|
+
if (!step)
|
|
114
|
+
return;
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
move(list, index, index + step, label);
|
|
117
|
+
}
|
|
118
|
+
function onHandlePointerDown(event, list, index, key, label) {
|
|
119
|
+
if (event.button !== 0)
|
|
120
|
+
return;
|
|
121
|
+
// A row moves inside its own list only: a band among the bands, a column of a band among its siblings.
|
|
122
|
+
const owner = event.currentTarget.closest("[data-list]");
|
|
123
|
+
if (!owner)
|
|
124
|
+
return;
|
|
125
|
+
const handle = event.currentTarget;
|
|
126
|
+
const boxes = [...owner.children].map(child => child.getBoundingClientRect());
|
|
127
|
+
handle.setPointerCapture(event.pointerId);
|
|
128
|
+
setDrag({ key, list, to: index });
|
|
129
|
+
const end = (done, y) => {
|
|
130
|
+
handle.removeEventListener("pointermove", onMove);
|
|
131
|
+
handle.removeEventListener("pointerup", up);
|
|
132
|
+
handle.removeEventListener("pointercancel", cancel);
|
|
133
|
+
handle.removeEventListener("keydown", escape);
|
|
134
|
+
setDrag(null);
|
|
135
|
+
if (done && y !== undefined)
|
|
136
|
+
move(list, index, dropIndex(boxes, y, index), label);
|
|
137
|
+
};
|
|
138
|
+
const onMove = (moved) => setDrag({ key, list, to: dropIndex(boxes, moved.clientY, index) });
|
|
139
|
+
const up = (moved) => {
|
|
140
|
+
if (handle.hasPointerCapture(moved.pointerId))
|
|
141
|
+
handle.releasePointerCapture(moved.pointerId);
|
|
142
|
+
end(true, moved.clientY);
|
|
143
|
+
};
|
|
144
|
+
const cancel = () => end(false);
|
|
145
|
+
// Esc puts the row back where it was.
|
|
146
|
+
const escape = (key) => {
|
|
147
|
+
if (key.key !== "Escape")
|
|
148
|
+
return;
|
|
149
|
+
key.preventDefault();
|
|
150
|
+
key.stopPropagation();
|
|
151
|
+
end(false);
|
|
152
|
+
};
|
|
153
|
+
handle.addEventListener("pointermove", onMove);
|
|
154
|
+
handle.addEventListener("pointerup", up);
|
|
155
|
+
handle.addEventListener("pointercancel", cancel);
|
|
156
|
+
handle.addEventListener("keydown", escape);
|
|
157
|
+
}
|
|
158
|
+
/** The band's checkbox: off hides every column under it, on brings back the ones it was not already hiding. */
|
|
159
|
+
function toggleBand(band, columnsOfBand, show) {
|
|
160
|
+
if (show) {
|
|
161
|
+
const kept = new Set(bandMemory.current.get(band) ?? []);
|
|
162
|
+
bandMemory.current.delete(band);
|
|
163
|
+
for (const column of columnsOfBand)
|
|
164
|
+
if (!column.shown && !kept.has(column.key))
|
|
165
|
+
onToggle(column.key, false);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
bandMemory.current.set(band, columnsOfBand.filter(column => !column.shown).map(column => column.key));
|
|
169
|
+
for (const column of columnsOfBand)
|
|
170
|
+
if (column.shown)
|
|
171
|
+
onToggle(column.key, true);
|
|
172
|
+
}
|
|
173
|
+
function hidePopover(panel) {
|
|
174
|
+
if (isPopoverOpen(panel))
|
|
175
|
+
panel.hidePopover();
|
|
176
|
+
}
|
|
177
|
+
/** One column's row: the handle, the checkbox, the label, and a mark when it is pinned. */
|
|
178
|
+
function columnRow(column, list, index, inBand, bandOff) {
|
|
179
|
+
const lastShown = column.shown && shownCount <= 1;
|
|
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));
|
|
181
|
+
}
|
|
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 })] }) }));
|
|
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
|
+
}
|
|
@@ -38,8 +38,10 @@ export declare function TableColumnGroup<TRow extends TableRow>({ columns, width
|
|
|
38
38
|
colRefs: Map<string, HTMLTableColElement>;
|
|
39
39
|
}): import("react").JSX.Element;
|
|
40
40
|
/** The number of a row, its checkbox, or both: the number until the pointer is over the row or something is selected. */
|
|
41
|
-
export declare function TableIndexCell({ heading, own, number, numbered, selectable, checkbox, }: {
|
|
41
|
+
export declare function TableIndexCell({ heading, rowSpan, own, number, numbered, selectable, checkbox, }: {
|
|
42
42
|
heading?: boolean;
|
|
43
|
+
/** 2 in the heading while the header has a band of group headings above the columns' own. */
|
|
44
|
+
rowSpan?: number;
|
|
43
45
|
own: {
|
|
44
46
|
offset: number;
|
|
45
47
|
edge: boolean;
|
|
@@ -74,9 +76,11 @@ export interface TableRowViewProps<TRow extends TableRow> {
|
|
|
74
76
|
onToggle: (row: TRow, on: boolean, event: MouseEvent<HTMLInputElement>) => void;
|
|
75
77
|
onAction: (row: TRow, action: TableRowAction<TRow>) => void;
|
|
76
78
|
onOpen?: (row: TRow) => void;
|
|
79
|
+
/** A single click opens the row, as a double-click does. */
|
|
80
|
+
openOnClick?: boolean;
|
|
77
81
|
}
|
|
78
82
|
/** One row. It renders again only when its row, its state, or the columns change. */
|
|
79
|
-
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, }: TableRowViewProps<TRow>): import("react").JSX.Element;
|
|
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;
|
|
80
84
|
export declare const TableRowView: typeof TableRowViewInner;
|
|
81
85
|
/**
|
|
82
86
|
* The head of the pinned section: one row across the table with a button that folds the pinned records open and shut,
|
|
@@ -123,8 +127,8 @@ export declare function TablePageSize({ pageSize, pageSizes, compact, onChange }
|
|
|
123
127
|
onChange: (size: number) => void;
|
|
124
128
|
}): import("react").JSX.Element | null;
|
|
125
129
|
/**
|
|
126
|
-
* Under the table: which rows show and how many the search left ("Showing 41–50 of 234 · filtered
|
|
127
|
-
* of how many, and the pager.
|
|
130
|
+
* Under the table: which rows show and how many the search and the filters left ("Showing 41–50 of 234 · filtered
|
|
131
|
+
* from 1,000"), the page of how many, and the pager.
|
|
128
132
|
*/
|
|
129
133
|
export declare function TableFooter<TRow extends TableRow>({ state, disabled, onPage }: {
|
|
130
134
|
state: TableState<TRow>;
|
|
@@ -75,7 +75,7 @@ export function TableColumnGroup({ columns, widths, frame, colRefs, }) {
|
|
|
75
75
|
return (_jsxs("colgroup", { children: [own(TABLE_INDEX_COLUMN, Boolean(frame.index)), columns.map(view => (_jsx("col", { ref: hold(view.column.key), style: { width: widths[view.column.key] ?? cssLength(view.column.width) ?? FALLBACK_COLUMN_WIDTH } }, view.column.key))), _jsx("col", { className: "nx-table__col-filler" }), own(TABLE_ACTIONS_COLUMN, Boolean(frame.actions))] }));
|
|
76
76
|
}
|
|
77
77
|
/** The number of a row, its checkbox, or both: the number until the pointer is over the row or something is selected. */
|
|
78
|
-
export function TableIndexCell({ heading, own, number, numbered, selectable, checkbox, }) {
|
|
78
|
+
export function TableIndexCell({ heading, rowSpan, own, number, numbered, selectable, checkbox, }) {
|
|
79
79
|
const shared = {
|
|
80
80
|
className: cx(heading ? "nx-table__heading" : "nx-table__cell", "nx-table__cell--index"),
|
|
81
81
|
"data-col": TABLE_INDEX_COLUMN,
|
|
@@ -86,19 +86,31 @@ export function TableIndexCell({ heading, own, number, numbered, selectable, che
|
|
|
86
86
|
style: ownStyle(own, "left"),
|
|
87
87
|
};
|
|
88
88
|
const content = (_jsxs(_Fragment, { children: [numbered && (_jsx("span", { className: "nx-table__number", "aria-hidden": selectable || undefined, children: number })), checkbox] }));
|
|
89
|
-
return heading ? (_jsx("th", { scope: "col", ...shared, children: content })) : (_jsx("td", { ...shared, children: content }));
|
|
89
|
+
return heading ? (_jsx("th", { scope: "col", rowSpan: rowSpan, ...shared, children: content })) : (_jsx("td", { ...shared, children: content }));
|
|
90
|
+
}
|
|
91
|
+
/** What in a row is a control of its own: a click there does what the control does, and never opens the row. */
|
|
92
|
+
const ROW_CONTROLS = "button, a, input, select, textarea, label, [role=menu], [role=menuitem]";
|
|
93
|
+
/**
|
|
94
|
+
* Whether a click on a row should open it. Never on one of its controls. A single click also does not when it ends a
|
|
95
|
+
* text selection inside the row — somebody dragging across a cell to copy a code is not asking for a popup — but a
|
|
96
|
+
* double-click is not asked that, because a double-click selects the word under the pointer on its way in.
|
|
97
|
+
*/
|
|
98
|
+
function opensRow(event, single) {
|
|
99
|
+
if (event.target.closest(ROW_CONTROLS))
|
|
100
|
+
return false;
|
|
101
|
+
if (!single)
|
|
102
|
+
return true;
|
|
103
|
+
const selection = window.getSelection();
|
|
104
|
+
return !(selection && !selection.isCollapsed && event.currentTarget.contains(selection.anchorNode));
|
|
90
105
|
}
|
|
91
106
|
/** One row. It renders again only when its row, its state, or the columns change. */
|
|
92
|
-
function TableRowViewInner({ row, rowKey, number, label, columns, cellOptions, selectable, selected, selectionFull, busy, flashed, numbered, pinned, frame, sized, actions, onToggle, onAction, onOpen, }) {
|
|
107
|
+
function TableRowViewInner({ row, rowKey, number, label, columns, cellOptions, selectable, selected, selectionFull, busy, flashed, numbered, pinned, frame, sized, actions, onToggle, onAction, onOpen, openOnClick, }) {
|
|
93
108
|
const inline = actions?.filter(action => action.inline && action.icon) ?? [];
|
|
94
109
|
const inMenu = actions?.filter(action => !(action.inline && action.icon)) ?? [];
|
|
95
110
|
const items = inMenu.map(action => ({ value: action.id, label: action.label, icon: action.icon, description: action.description, tone: action.tone, disabled: action.disabled }));
|
|
96
|
-
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,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return;
|
|
100
|
-
onOpen(row);
|
|
101
|
-
}), 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}` })] }) }))] }));
|
|
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,
|
|
112
|
+
// 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}` })] }) }))] }));
|
|
102
114
|
}
|
|
103
115
|
export const TableRowView = memo(TableRowViewInner);
|
|
104
116
|
/**
|
|
@@ -131,8 +143,8 @@ export function TablePageSize({ pageSize, pageSizes, compact = false, onChange }
|
|
|
131
143
|
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` }) }));
|
|
132
144
|
}
|
|
133
145
|
/**
|
|
134
|
-
* Under the table: which rows show and how many the search left ("Showing 41–50 of 234 · filtered
|
|
135
|
-
* of how many, and the pager.
|
|
146
|
+
* Under the table: which rows show and how many the search and the filters left ("Showing 41–50 of 234 · filtered
|
|
147
|
+
* from 1,000"), the page of how many, and the pager.
|
|
136
148
|
*/
|
|
137
149
|
export function TableFooter({ state, disabled, onPage }) {
|
|
138
150
|
const { status, rows, offset, matches, total, query } = state;
|
|
@@ -141,5 +153,5 @@ export function TableFooter({ state, disabled, onPage }) {
|
|
|
141
153
|
const first = rows.length ? offset + 1 : 0;
|
|
142
154
|
const last = offset + rows.length;
|
|
143
155
|
const pages = pageCount(matches, query.pageSize);
|
|
144
|
-
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 })] })] }));
|
|
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 || 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 })] })] }));
|
|
145
157
|
}
|
|
@@ -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
|
|
4
|
-
* reports when its animations are turned off) or when
|
|
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.
|
|
@@ -12,6 +12,22 @@ export interface ApiCall<TOptions = object> {
|
|
|
12
12
|
/** What the call does and acts on, for its messages. */
|
|
13
13
|
context: MessageContext;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Reads a server's answer as an `ApiResult`. A client's `read` is the app's default for every call that does not bring
|
|
17
|
+
* its own (`ApiCallOptions.read`); `call` says what was asked for, and is left out for an answer that came from
|
|
18
|
+
* somewhere other than a call (an app's own send function).
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // A backend that answers { success, message, data, error: { code, message, fields } }
|
|
22
|
+
* configureApi({
|
|
23
|
+
* read: (body, status) => {
|
|
24
|
+
* const answer = body as { success?: boolean; message?: string; data?: unknown; error?: { code: string; fields?: Record<string, string[]> } };
|
|
25
|
+
* if (answer?.success) return { ok: true, status, result: answer.data, message: answer.message };
|
|
26
|
+
* return { ok: false, status, message: answer?.message, messageCode: answer?.error?.code, errors: … };
|
|
27
|
+
* },
|
|
28
|
+
* });
|
|
29
|
+
*/
|
|
30
|
+
export type ApiResponseReader<TOptions = object> = (body: unknown, status: number | undefined, call?: ApiCall<TOptions>) => ApiResult;
|
|
15
31
|
export interface ApiClientConfig<TOptions = object> {
|
|
16
32
|
/** The full URL of an endpoint, given its filled path: through the proxy in the browser, the module's root on a server. */
|
|
17
33
|
resolveUrl: (endpoint: ApiEndpoint, path: string, call: ApiCall<TOptions>) => string | Promise<string>;
|
|
@@ -24,6 +40,12 @@ export interface ApiClientConfig<TOptions = object> {
|
|
|
24
40
|
/** Milliseconds to wait for an answer. Default 60,000. */
|
|
25
41
|
timeout?: number;
|
|
26
42
|
credentials?: RequestCredentials;
|
|
43
|
+
/**
|
|
44
|
+
* Reads every answer the call itself did not bring a reader for. Default `readApiResponse`: Nexus API responses,
|
|
45
|
+
* ASP.NET Core problem details, and any JSON by its status. An app on another backend registers its own once, so
|
|
46
|
+
* components that call inside themselves (`Crud`, `ApiForm`) read that backend's envelope too.
|
|
47
|
+
*/
|
|
48
|
+
read?: ApiResponseReader<TOptions>;
|
|
27
49
|
/** Changes the request just before it is sent, such as Next.js caching on a server. */
|
|
28
50
|
prepare?: (init: RequestInit, call: ApiCall<TOptions>) => RequestInit;
|
|
29
51
|
/**
|
|
@@ -99,7 +99,10 @@ export function createApiClient(config) {
|
|
|
99
99
|
controller.abort();
|
|
100
100
|
}, timeout)
|
|
101
101
|
: undefined;
|
|
102
|
-
|
|
102
|
+
// The call's own reader, else the app's (`configureApi({ read })`), else the built-in one. The app's is looked up
|
|
103
|
+
// on every call, so registering it after a module was imported still takes effect.
|
|
104
|
+
const appRead = config.read;
|
|
105
|
+
const read = options.read ?? (appRead ? (answer, status) => appRead(answer, status, call) : (answer, status) => readApiResponse(answer, status, call.context));
|
|
103
106
|
const stopped = () => caller?.aborted ? aborted() : timedOut ? { ok: false, timeout: true, message: buildMessage("reason", { ...call.context, timeout: true }) } : undefined;
|
|
104
107
|
let init = { method, headers, body, credentials: options.credentials ?? config.credentials, signal: controller.signal };
|
|
105
108
|
if (config.prepare)
|
|
@@ -4,7 +4,10 @@ export interface SendApiOptions {
|
|
|
4
4
|
signal?: AbortSignal;
|
|
5
5
|
read?: (body: unknown, status: number | undefined) => ApiResult;
|
|
6
6
|
}
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* The call options of a form's `{ url, method, headers, bodyType, credentials }` request with its values. Without a
|
|
9
|
+
* `read` the client's own reader is used: the app's (`configureApi({ read })`), else the built-in one.
|
|
10
|
+
*/
|
|
8
11
|
export declare function apiRequestOptions(request: ApiRequest, body: unknown, { signal, read }?: SendApiOptions): ApiCallOptions;
|
|
9
12
|
/** An abort as the error `fetch` throws, for code that expects a stopped request to reject. */
|
|
10
13
|
export declare function abortError(): Error;
|
|
@@ -16,11 +19,31 @@ export declare function sendApiRequest(request: ApiRequest, body: unknown, optio
|
|
|
16
19
|
/** What an `ApiSend` function answered, read the same way: a `Response`, an `ApiResult`, or a body. */
|
|
17
20
|
export declare function toApiResult(answer: unknown, read?: (body: unknown, status: number | undefined) => ApiResult): Promise<ApiResult>;
|
|
18
21
|
/**
|
|
19
|
-
*
|
|
20
|
-
* an input ("Addresses[1].City" and "$.addresses" go to "addresses";
|
|
21
|
-
*
|
|
22
|
+
* The input a server's field name belongs to, or `undefined` when the form has no such input. Names match without case,
|
|
23
|
+
* and a path matches its first part that is an input ("Addresses[1].City" and "$.addresses" go to "addresses";
|
|
24
|
+
* "dto.Email" to "email"). The one rule every server message is matched by.
|
|
25
|
+
*/
|
|
26
|
+
export declare function matchInputName(name: string, inputNames: readonly string[]): string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Puts a server's messages on the inputs they name, by `matchInputName`. Messages that match no input are returned in
|
|
29
|
+
* `others`, for the form's message.
|
|
22
30
|
*/
|
|
23
31
|
export declare function matchFieldErrors(errors: Record<string, string> | undefined, inputNames: readonly string[]): {
|
|
24
32
|
fields: FormErrors;
|
|
25
33
|
others: string[];
|
|
26
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* A failed answer as what a form shows, through the one path a 400's field errors already take: every message the server
|
|
37
|
+
* put on an input (`inputName`, several named with "|") goes to that input, every other message - including one naming an
|
|
38
|
+
* input this form does not have - goes under the form, and nothing is shown twice. `message` is `null` when the inputs
|
|
39
|
+
* carry it all. An answer that explains nothing falls back to a message for its HTTP status; that is the only time our
|
|
40
|
+
* own wording is shown.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* const { errors, message } = formErrorsFromResult(answer, store.inputs.map(params => params.name));
|
|
44
|
+
* store.setErrors(errors, message);
|
|
45
|
+
*/
|
|
46
|
+
export declare function formErrorsFromResult(answer: ApiResult, inputNames: readonly string[]): {
|
|
47
|
+
errors: FormErrors;
|
|
48
|
+
message: string | null;
|
|
49
|
+
};
|