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