nexus-shared 3.1.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2268 -446
- package/LICENSE +21 -21
- package/README.md +1705 -1084
- package/dist/Client.Index.d.ts +17 -0
- package/dist/Client.Index.js +17 -0
- package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
- package/dist/Components/Documents/DocumentFrame.js +83 -0
- package/dist/Components/Documents/Menu.d.ts +4 -0
- package/dist/Components/Documents/Menu.js +40 -24
- package/dist/Components/Documents/PrintDocument.d.ts +95 -0
- package/dist/Components/Documents/PrintDocument.js +62 -0
- package/dist/Components/Documents/Tabs.d.ts +59 -0
- package/dist/Components/Documents/Tabs.js +70 -0
- package/dist/Components/Forms/ApiForm.js +19 -2
- package/dist/Components/Forms/Crud.d.ts +2 -2
- package/dist/Components/Forms/Crud.js +871 -377
- package/dist/Components/Forms/CrudActions.d.ts +136 -0
- package/dist/Components/Forms/CrudActions.js +184 -0
- package/dist/Components/Forms/CrudHistory.d.ts +66 -0
- package/dist/Components/Forms/CrudHistory.js +140 -0
- package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
- package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
- package/dist/Components/Forms/CrudParts.d.ts +30 -2
- package/dist/Components/Forms/CrudParts.js +56 -5
- package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
- package/dist/Components/Forms/CrudRowForms.js +114 -0
- package/dist/Components/Forms/CrudScopes.d.ts +28 -0
- package/dist/Components/Forms/CrudScopes.js +76 -0
- package/dist/Components/Forms/CrudTree.d.ts +90 -0
- package/dist/Components/Forms/CrudTree.js +363 -0
- package/dist/Components/Forms/CrudWorkspace.d.ts +63 -0
- package/dist/Components/Forms/CrudWorkspace.js +169 -0
- package/dist/Components/Forms/ExcelImport.js +3 -3
- package/dist/Components/Forms/Form.d.ts +4 -4
- package/dist/Components/Forms/Form.js +125 -16
- package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
- package/dist/Components/Forms/FormLeaveGuard.js +65 -0
- package/dist/Components/Forms/RecordFlows.d.ts +50 -0
- package/dist/Components/Forms/RecordFlows.js +282 -0
- package/dist/Components/Forms/SubmitForm.d.ts +37 -2
- package/dist/Components/Forms/SubmitForm.js +195 -10
- package/dist/Components/Inputs/CheckBox.js +1 -1
- package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
- package/dist/Components/Inputs/ChoiceField.js +28 -2
- 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.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +8 -5
- package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
- package/dist/Components/Inputs/DropdownParts.js +16 -4
- package/dist/Components/Inputs/Field.d.ts +7 -1
- package/dist/Components/Inputs/Field.js +2 -2
- package/dist/Components/Inputs/GroupForm.d.ts +6 -6
- package/dist/Components/Inputs/GroupForm.js +10 -11
- package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
- package/dist/Components/Inputs/InputRenderer.js +39 -31
- package/dist/Components/Inputs/NumberBox.js +4 -2
- package/dist/Components/Inputs/PictureInput.d.ts +41 -0
- package/dist/Components/Inputs/PictureInput.js +70 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
- package/dist/Components/Inputs/RadioGroup.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +1 -1
- package/dist/Components/Inputs/RowsInput.js +18 -3
- package/dist/Components/Inputs/TabularForm.d.ts +5 -5
- package/dist/Components/Inputs/TabularForm.js +29 -29
- package/dist/Components/Inputs/TextArea.d.ts +6 -0
- package/dist/Components/Inputs/TextArea.js +14 -2
- package/dist/Components/Inputs/TextBox.js +4 -1
- package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
- package/dist/Components/Inputs/TextBoxTypes.js +17 -0
- package/dist/Components/Inputs/TimePicker.js +4 -1
- package/dist/Components/Inputs/TreeView.d.ts +107 -6
- package/dist/Components/Inputs/TreeView.js +513 -30
- package/dist/Components/Layouts/MessageBox.d.ts +15 -2
- package/dist/Components/Layouts/MessageBox.js +56 -7
- package/dist/Components/Layouts/Popup.d.ts +13 -6
- package/dist/Components/Layouts/Popup.js +37 -9
- package/dist/Components/Layouts/PopupDock.d.ts +3 -1
- package/dist/Components/Layouts/PopupDock.js +2 -1
- package/dist/Components/Layouts/PopupHost.js +5 -4
- package/dist/Components/Layouts/ThemePicker.d.ts +3 -2
- package/dist/Components/Layouts/ThemePicker.js +16 -4
- package/dist/Components/Viewers/DataTable.js +205 -61
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +46 -12
- package/dist/Components/Viewers/DataTableParts.js +62 -21
- package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
- package/dist/Components/Viewers/OwnerFiles.js +589 -0
- package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
- package/dist/Components/Viewers/QrCodeImage.js +15 -0
- package/dist/Components/Viewers/RecordCards.d.ts +16 -0
- package/dist/Components/Viewers/RecordCards.js +509 -0
- 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/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -0
- package/dist/Helpers/CodeHelpers.d.ts +97 -0
- package/dist/Helpers/CodeHelpers.js +332 -0
- package/dist/Helpers/CrudBackend.d.ts +114 -6
- package/dist/Helpers/CrudBackend.js +214 -11
- package/dist/Helpers/CrudColumns.d.ts +98 -0
- package/dist/Helpers/CrudColumns.js +160 -0
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +130 -6
- package/dist/Helpers/CrudHelpers.js +304 -12
- package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
- package/dist/Helpers/CrudNarrowPick.js +157 -0
- package/dist/Helpers/CrudRowForms.d.ts +15 -0
- package/dist/Helpers/CrudRowForms.js +43 -0
- package/dist/Helpers/CrudScopes.d.ts +32 -0
- package/dist/Helpers/CrudScopes.js +79 -0
- package/dist/Helpers/DropdownHelpers.d.ts +3 -1
- package/dist/Helpers/DropdownHelpers.js +6 -2
- package/dist/Helpers/ExcelHelpers.d.ts +1 -1
- package/dist/Helpers/ExcelHelpers.js +12 -11
- package/dist/Helpers/FileAdapter.d.ts +78 -0
- package/dist/Helpers/FileAdapter.js +102 -0
- package/dist/Helpers/FilePrimaries.d.ts +48 -0
- package/dist/Helpers/FilePrimaries.js +138 -0
- package/dist/Helpers/FormConfirm.d.ts +32 -0
- package/dist/Helpers/FormConfirm.js +40 -0
- package/dist/Helpers/FormStore.d.ts +90 -5
- package/dist/Helpers/FormStore.js +318 -22
- package/dist/Helpers/InputConditions.d.ts +50 -0
- package/dist/Helpers/InputConditions.js +168 -0
- package/dist/Helpers/InputMemory.d.ts +106 -0
- package/dist/Helpers/InputMemory.js +385 -0
- package/dist/Helpers/InputNarrowing.d.ts +18 -0
- package/dist/Helpers/InputNarrowing.js +67 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
- package/dist/Helpers/InputParamsHelpers.js +162 -71
- package/dist/Helpers/MessageBuilder.js +2 -0
- package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
- package/dist/Helpers/NexusDriveAdapter.js +207 -0
- package/dist/Helpers/NexusHelpers.d.ts +0 -0
- package/dist/Helpers/NexusHelpers.js +1 -0
- package/dist/Helpers/NumberHelpers.d.ts +95 -0
- package/dist/Helpers/NumberHelpers.js +126 -0
- package/dist/Helpers/Permissions.js +2 -1
- package/dist/Helpers/QrCode.d.ts +26 -0
- package/dist/Helpers/QrCode.js +463 -0
- package/dist/Helpers/RecordCards.d.ts +205 -0
- package/dist/Helpers/RecordCards.js +378 -0
- package/dist/Helpers/RequestCache.d.ts +47 -0
- package/dist/Helpers/RequestCache.js +132 -0
- package/dist/Helpers/RowsStore.d.ts +37 -2
- package/dist/Helpers/RowsStore.js +128 -10
- package/dist/Helpers/ServerPreview.d.ts +69 -0
- package/dist/Helpers/ServerPreview.js +99 -0
- package/dist/Helpers/TableColumns.js +4 -3
- package/dist/Helpers/TableExport.d.ts +7 -1
- package/dist/Helpers/TableExport.js +21 -15
- package/dist/Helpers/TableFilters.d.ts +1 -1
- package/dist/Helpers/TableFilters.js +24 -23
- package/dist/Helpers/TableHelpers.d.ts +24 -3
- package/dist/Helpers/TableHelpers.js +58 -12
- package/dist/Helpers/TableJoins.d.ts +83 -0
- package/dist/Helpers/TableJoins.js +284 -0
- package/dist/Helpers/TableStore.d.ts +26 -1
- package/dist/Helpers/TableStore.js +73 -7
- package/dist/Helpers/TreeStore.d.ts +306 -11
- package/dist/Helpers/TreeStore.js +1048 -90
- package/dist/Helpers/UserPreferences.d.ts +8 -0
- package/dist/Helpers/UserPreferences.js +17 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +512 -14
- package/dist/Interfaces/FileInterfaces.d.ts +162 -0
- package/dist/Interfaces/FileInterfaces.js +4 -0
- package/dist/Interfaces/FormInterfaces.d.ts +393 -29
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +39 -3
- package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
- package/dist/Interfaces/PreferenceInterfaces.d.ts +8 -0
- package/dist/Interfaces/PreferenceInterfaces.js +2 -5
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +160 -5
- 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.js +5 -0
- package/dist/Services/FileApi.d.ts +13 -0
- package/dist/Services/FileApi.js +29 -0
- package/dist/Services/ImageEditorLoader.d.ts +14 -0
- package/dist/Services/ImageEditorLoader.js +51 -0
- package/dist/Services/JoinSources.d.ts +80 -0
- package/dist/Services/JoinSources.js +150 -0
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Services/ServerApi.d.ts +13 -2
- package/dist/Services/ServerApi.js +8 -3
- package/dist/Shared.Index.d.ts +20 -0
- package/dist/Shared.Index.js +20 -0
- package/package.json +92 -85
- package/src/Styles/Nexus.Avatar.css +80 -79
- package/src/Styles/Nexus.Base.css +117 -117
- package/src/Styles/Nexus.Button.css +186 -180
- package/src/Styles/Nexus.Calendar.css +273 -273
- package/src/Styles/Nexus.Chat.css +1443 -1443
- package/src/Styles/Nexus.Choice.css +359 -343
- package/src/Styles/Nexus.Crud.css +542 -168
- package/src/Styles/Nexus.DateTime.css +81 -81
- package/src/Styles/Nexus.Dropdown.css +733 -725
- package/src/Styles/Nexus.Editor.css +295 -295
- package/src/Styles/Nexus.Excel.css +389 -389
- package/src/Styles/Nexus.Files.css +410 -0
- package/src/Styles/Nexus.Form.css +282 -279
- package/src/Styles/Nexus.Index.css +41 -37
- package/src/Styles/Nexus.Input.css +238 -4
- package/src/Styles/Nexus.Markdown.css +237 -237
- package/src/Styles/Nexus.Menu.css +246 -237
- package/src/Styles/Nexus.Popup.css +800 -724
- package/src/Styles/Nexus.Print.css +288 -0
- package/src/Styles/Nexus.QrCode.css +12 -0
- package/src/Styles/Nexus.Record.css +430 -0
- package/src/Styles/Nexus.Rows.css +637 -637
- package/src/Styles/Nexus.Slider.css +276 -276
- package/src/Styles/Nexus.Tab.Buttons.css +497 -432
- package/src/Styles/Nexus.Table.css +98 -35
- package/src/Styles/Nexus.Theme.Picker.css +59 -3
- package/src/Styles/Nexus.Themes.css +200 -194
- package/src/Styles/Nexus.Time.css +144 -144
- package/src/Styles/Nexus.Toast.css +263 -263
- package/src/Styles/Nexus.Tokens.css +94 -89
- package/src/Styles/Nexus.Tree.css +530 -431
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ReactNode } from "react";
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
2
|
import type { DropdownColumn, DropdownItem } from "../../Interfaces/InputInterfaces.ts";
|
|
3
3
|
/**
|
|
4
4
|
* Why a current value cannot be picked again:
|
|
@@ -24,6 +24,8 @@ export declare function DropdownImage({ src }: {
|
|
|
24
24
|
export declare function iconNode(icon: unknown): ReactNode;
|
|
25
25
|
/** Whether an icon field has something to draw. */
|
|
26
26
|
export declare function hasIcon(icon: unknown): boolean;
|
|
27
|
+
/** A column's cells and heading where they sit (`dropdownColumnAlign`): nothing to say for `start`. */
|
|
28
|
+
export declare const columnAlignStyle: (column: Pick<AnyColumn, "type" | "align">) => CSSProperties | undefined;
|
|
27
29
|
/** One cell of a row with columns. `label` names the avatar's initials; `tag` follows the text. */
|
|
28
30
|
export declare function DropdownCell({ column, item, label, tag }: {
|
|
29
31
|
column: AnyColumn;
|
|
@@ -3,7 +3,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import { Icon, iconCheck, iconX, InlineIcon } from "nexus-icons";
|
|
4
4
|
import { isValidElement, memo, useState } from "react";
|
|
5
5
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
|
-
import { isMediaColumn, readField } from "../../Helpers/DropdownHelpers.js";
|
|
6
|
+
import { dropdownColumnAlign, isMediaColumn, readField } from "../../Helpers/DropdownHelpers.js";
|
|
7
|
+
import { formatMoney } from "../../Helpers/NumberHelpers.js";
|
|
8
|
+
import { useNumberLocale } from "../../Helpers/UserPreferences.js";
|
|
7
9
|
import { Avatar } from "../Viewers/Avatar.js";
|
|
8
10
|
export const DROPDOWN_STATE_LABELS = {
|
|
9
11
|
inactive: "Inactive",
|
|
@@ -38,11 +40,21 @@ export function iconNode(icon) {
|
|
|
38
40
|
export function hasIcon(icon) {
|
|
39
41
|
return (typeof icon === "string" && icon !== "") || isValidElement(icon);
|
|
40
42
|
}
|
|
41
|
-
|
|
43
|
+
/** A column's cells and heading where they sit (`dropdownColumnAlign`): nothing to say for `start`. */
|
|
44
|
+
export const columnAlignStyle = (column) => {
|
|
45
|
+
const align = dropdownColumnAlign(column);
|
|
46
|
+
return align !== "start" ? { justifySelf: align, textAlign: align } : undefined;
|
|
47
|
+
};
|
|
48
|
+
/** A money column's figure: 2 places in the person's number format; nothing for a missing one or one that is no figure. */
|
|
49
|
+
function DropdownMoney({ value }) {
|
|
50
|
+
const locale = useNumberLocale();
|
|
51
|
+
const figure = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : Number.NaN;
|
|
52
|
+
return _jsx("span", { className: "nx-dropdown-cell__text", children: formatMoney(Number.isFinite(figure) ? figure : null, { locale }) });
|
|
53
|
+
}
|
|
42
54
|
/** One cell of a row with columns. `label` names the avatar's initials; `tag` follows the text. */
|
|
43
55
|
export function DropdownCell({ column, item, label, tag }) {
|
|
44
56
|
const type = column.type ?? "text";
|
|
45
|
-
const style =
|
|
57
|
+
const style = columnAlignStyle(column);
|
|
46
58
|
if (column.render) {
|
|
47
59
|
return (_jsxs("span", { className: cx("nx-dropdown-cell", `nx-dropdown-cell--${type}`), style: style, children: [column.render(item), tag] }));
|
|
48
60
|
}
|
|
@@ -51,7 +63,7 @@ export function DropdownCell({ column, item, label, tag }) {
|
|
|
51
63
|
return (_jsx("span", { className: "nx-dropdown-cell nx-dropdown-cell--media", style: style, "aria-hidden": "true", children: type === "avatar" ? _jsx(DropdownAvatar, { src: value, name: label }) : type === "image" ? _jsx(DropdownImage, { src: value }) : _jsx("span", { className: "nx-dropdown-icon", children: iconNode(value) }) }));
|
|
52
64
|
}
|
|
53
65
|
const text = value === null || value === undefined ? "" : String(value);
|
|
54
|
-
return (_jsxs("span", { className: cx("nx-dropdown-cell", `nx-dropdown-cell--${type}`), style: style, children: [_jsx("span", { className: "nx-dropdown-cell__text", children: text }), tag] }));
|
|
66
|
+
return (_jsxs("span", { className: cx("nx-dropdown-cell", `nx-dropdown-cell--${type}`), style: style, children: [type === "money" ? _jsx(DropdownMoney, { value: value }) : _jsx("span", { className: "nx-dropdown-cell__text", children: text }), tag] }));
|
|
55
67
|
}
|
|
56
68
|
/** The picture that leads a value in the box or a chip: the first avatar, image, or icon column, or the icon field. */
|
|
57
69
|
export function DropdownMedia({ item, label, columns, iconKey }) {
|
|
@@ -17,6 +17,12 @@ export interface FieldProps {
|
|
|
17
17
|
hint?: ReactNode;
|
|
18
18
|
/** Error message. Marks the field invalid and replaces the hint. */
|
|
19
19
|
error?: ReactNode;
|
|
20
|
+
/**
|
|
21
|
+
* Draws the field as invalid, the look an error gives it, with no message line of its own: for a field the page says
|
|
22
|
+
* why about elsewhere, such as a CRUD's narrowing box while its list asks for a pick. Nothing is said to a screen
|
|
23
|
+
* reader by it: point the control's `aria-describedby` at the words that say why.
|
|
24
|
+
*/
|
|
25
|
+
invalid?: boolean;
|
|
20
26
|
/** Id of the hint or error line, for the control's `aria-describedby`. */
|
|
21
27
|
messageId?: string;
|
|
22
28
|
/** Decorative icon at the start of the box. */
|
|
@@ -48,6 +54,6 @@ export interface FieldProps {
|
|
|
48
54
|
* The frame around every input: label, box, icons, and the hint or error line. It has no state, so it renders
|
|
49
55
|
* anywhere; inputs such as `TextBox` build on it, and so can custom controls.
|
|
50
56
|
*/
|
|
51
|
-
export declare function Field({ controlId, label, labelId, required, design, size, hint, error, messageId, icon, iconEnd, prefix, suffix, actionsStart, actions, aside, disabled, readOnly, raiseLabel, multiline, className, style, children, }: FieldProps): import("react").JSX.Element;
|
|
57
|
+
export declare function Field({ controlId, label, labelId, required, design, size, hint, error, invalid, messageId, icon, iconEnd, prefix, suffix, actionsStart, actions, aside, disabled, readOnly, raiseLabel, multiline, className, style, children, }: FieldProps): import("react").JSX.Element;
|
|
52
58
|
/** Whether a node renders something: not `undefined`, `null`, `false`, or "". */
|
|
53
59
|
export declare const hasContent: (node: ReactNode) => boolean;
|
|
@@ -5,12 +5,12 @@ import { cx } from "../../Helpers/ClassHelpers.js";
|
|
|
5
5
|
* The frame around every input: label, box, icons, and the hint or error line. It has no state, so it renders
|
|
6
6
|
* anywhere; inputs such as `TextBox` build on it, and so can custom controls.
|
|
7
7
|
*/
|
|
8
|
-
export function Field({ controlId, label, labelId, required = false, design = "standard", size = "md", hint, error, messageId, icon, iconEnd, prefix, suffix, actionsStart, actions, aside, disabled = false, readOnly = false, raiseLabel = false, multiline = false, className, style, children, }) {
|
|
8
|
+
export function Field({ controlId, label, labelId, required = false, design = "standard", size = "md", hint, error, invalid = false, messageId, icon, iconEnd, prefix, suffix, actionsStart, actions, aside, disabled = false, readOnly = false, raiseLabel = false, multiline = false, className, style, children, }) {
|
|
9
9
|
const hasLabel = hasContent(label);
|
|
10
10
|
const hasError = hasContent(error);
|
|
11
11
|
const message = hasError ? error : hint;
|
|
12
12
|
const labelElement = hasLabel && (_jsxs("label", { id: labelId, className: "nx-field__label", htmlFor: controlId, children: [label, required && (_jsx("span", { className: "nx-field__required", "aria-hidden": "true", children: "*" }))] }));
|
|
13
|
-
return (_jsxs("div", { className: cx("nx-field", `nx-field--${design}`, `nx-field--${size}`, multiline && "nx-field--multiline", className), "data-invalid": hasError || undefined, "data-disabled": disabled || undefined, "data-readonly": readOnly || undefined, "data-raised": raiseLabel || undefined, style: style, children: [design === "standard" && labelElement, _jsxs("div", { className: "nx-field__box", children: [actionsStart && _jsx("span", { className: "nx-field__actions nx-field__actions--start", children: actionsStart }), icon && (_jsx("span", { className: "nx-field__icon", "aria-hidden": "true", children: icon })), hasContent(prefix) && (_jsx("span", { id: `${controlId}-prefix`, className: "nx-field__affix", children: prefix })), children, design !== "standard" && labelElement, hasContent(suffix) && (_jsx("span", { id: `${controlId}-suffix`, className: "nx-field__affix nx-field__affix--end", children: suffix })), actions && _jsx("span", { className: "nx-field__actions", children: actions }), iconEnd && (_jsx("span", { className: "nx-field__icon nx-field__icon--end", "aria-hidden": "true", children: iconEnd }))] }), (message || aside) && (_jsxs("div", { className: "nx-field__footer", children: [message && (_jsxs("p", { id: messageId, className: cx("nx-field__message", hasError && "nx-field__message--error"), children: [hasError && _jsx(InlineIcon, { icon: iconAlertCircle }), _jsx("span", { children: message })] })), aside && _jsx("span", { className: "nx-field__aside", children: aside })] }))] }));
|
|
13
|
+
return (_jsxs("div", { className: cx("nx-field", `nx-field--${design}`, `nx-field--${size}`, multiline && "nx-field--multiline", className), "data-invalid": hasError || invalid || undefined, "data-disabled": disabled || undefined, "data-readonly": readOnly || undefined, "data-raised": raiseLabel || undefined, style: style, children: [design === "standard" && labelElement, _jsxs("div", { className: "nx-field__box", children: [actionsStart && _jsx("span", { className: "nx-field__actions nx-field__actions--start", children: actionsStart }), icon && (_jsx("span", { className: "nx-field__icon", "aria-hidden": "true", children: icon })), hasContent(prefix) && (_jsx("span", { id: `${controlId}-prefix`, className: "nx-field__affix", children: prefix })), children, design !== "standard" && labelElement, hasContent(suffix) && (_jsx("span", { id: `${controlId}-suffix`, className: "nx-field__affix nx-field__affix--end", children: suffix })), actions && _jsx("span", { className: "nx-field__actions", children: actions }), iconEnd && (_jsx("span", { className: "nx-field__icon nx-field__icon--end", "aria-hidden": "true", children: iconEnd }))] }), (message || aside) && (_jsxs("div", { className: "nx-field__footer", children: [message && (_jsxs("p", { id: messageId, className: cx("nx-field__message", hasError && "nx-field__message--error"), children: [hasError && _jsx(InlineIcon, { icon: iconAlertCircle }), _jsx("span", { children: message })] })), aside && _jsx("span", { className: "nx-field__aside", children: aside })] }))] }));
|
|
14
14
|
}
|
|
15
15
|
/** Whether a node renders something: not `undefined`, `null`, `false`, or "". */
|
|
16
16
|
export const hasContent = (node) => node !== undefined && node !== null && node !== false && node !== "";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type GroupFormProps, type RowValue } from "../../Interfaces/FormInterfaces.ts";
|
|
2
2
|
/**
|
|
3
3
|
* The rows of a `TabularForm` as sections: each row's inputs with their own labels, on a 12-column grid (`span` in
|
|
4
4
|
* each input's params). The same params, value, rules, and form behavior as `TabularForm`; only the layout differs. It
|
|
@@ -10,11 +10,11 @@ import type { GroupFormProps, RowValue } from "../../Interfaces/FormInterfaces.t
|
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* <GroupForm name="addresses" label="Addresses" itemLabel="Address" max={5} inputs={[
|
|
13
|
-
* { type:
|
|
14
|
-
* { type:
|
|
15
|
-
* { type:
|
|
16
|
-
* { type:
|
|
17
|
-
* { type:
|
|
13
|
+
* { type: EInputTypes.Dropdown, name: "country", label: "Country", required: true, options: countries, span: 6 },
|
|
14
|
+
* { type: EInputTypes.Text, name: "city", label: "City", required: true, span: 6 },
|
|
15
|
+
* { type: EInputTypes.Text, name: "zipCode", label: "Zip code", span: 4 },
|
|
16
|
+
* { type: EInputTypes.Dropdown, name: "type", label: "Type", options: ADDRESS_TYPES, span: 4 },
|
|
17
|
+
* { type: EInputTypes.Radio, name: "isPrimary", label: "Primary address", span: 4, required: true },
|
|
18
18
|
* ]} />
|
|
19
19
|
*/
|
|
20
20
|
export declare function GroupForm<TRow extends RowValue = RowValue>(props: GroupFormProps<TRow>): import("react").JSX.Element;
|
|
@@ -4,13 +4,12 @@ import { iconAlertCircle, iconArrowDown, iconArrowUp, iconChevronDown, iconCopy,
|
|
|
4
4
|
import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
5
5
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
6
|
import { useDatePreferences } from "../../Helpers/DatePreferences.js";
|
|
7
|
-
import { formatInputValue } from "../../Helpers/InputParamsHelpers.js";
|
|
7
|
+
import { formatInputValue, isBooleanInput } from "../../Helpers/InputParamsHelpers.js";
|
|
8
|
+
import { EInputTypes } from "../../Interfaces/FormInterfaces.js";
|
|
8
9
|
import { Button } from "../Documents/Button.js";
|
|
9
10
|
import { firstFocusable, labelText, RowCell, RowsFooter, RowsLegend, useRowsFrame, useRowsInput, useRowVersion } from "./RowsInput.js";
|
|
10
11
|
/** Inputs that read as a line of text in a collapsed section's summary. */
|
|
11
|
-
const SUMMARY_SKIP = new Set([
|
|
12
|
-
/** Inputs without a box: they line up with the boxes beside them. */
|
|
13
|
-
const CHOICE_TYPES = new Set(["checkbox", "switch", "radio"]);
|
|
12
|
+
const SUMMARY_SKIP = new Set([EInputTypes.CheckBox, EInputTypes.Switch, EInputTypes.Radio, EInputTypes.Tree, EInputTypes.Tabular, EInputTypes.Group, EInputTypes.Markdown, EInputTypes.Picture]);
|
|
14
13
|
function defaultSummary(row, inputs, preferences) {
|
|
15
14
|
const parts = [];
|
|
16
15
|
for (const params of inputs) {
|
|
@@ -46,7 +45,7 @@ const GroupSection = memo(function GroupSection({ store, rowKey, index, count, i
|
|
|
46
45
|
setCollapsed(false);
|
|
47
46
|
}, [problems, collapsed]);
|
|
48
47
|
// Radio columns this row has on show as tags: "Primary".
|
|
49
|
-
const tags = inputs.filter(params => params.type ===
|
|
48
|
+
const tags = inputs.filter(params => params.type === EInputTypes.Radio && !params.hidden && row[params.name] === true).map(labelText);
|
|
50
49
|
let summary = null;
|
|
51
50
|
if (!open) {
|
|
52
51
|
const { summary: pick } = context;
|
|
@@ -64,7 +63,7 @@ const GroupSection = memo(function GroupSection({ store, rowKey, index, count, i
|
|
|
64
63
|
store.touch(rowKey, cell.dataset.nxCell ?? "");
|
|
65
64
|
}
|
|
66
65
|
const heading = (_jsxs(_Fragment, { children: [_jsx("span", { id: titleId, className: "nx-group-form__title", children: title }), tags.map(tag => (_jsx("span", { className: "nx-group-form__tag", children: tag }, tag))), summary && _jsx("span", { className: "nx-group-form__summary", children: summary }), problems > 0 && (_jsxs("span", { className: "nx-group-form__problems", children: [_jsx(InlineIcon, { icon: iconAlertCircle }), problems, " to fix"] }))] }));
|
|
67
|
-
return (_jsxs("li", { className: "nx-group-form__section", "data-nx-row": rowKey, "data-open": open || undefined, "data-problem": problems > 0 || undefined, children: [_jsxs("div", { className: "nx-group-form__head", children: [context.collapsible ? (_jsxs("button", { type: "button", className: "nx-group-form__toggle", "aria-expanded": open, "aria-controls": open ? bodyId : undefined, "data-nx-row-actions": "", onClick: () => setCollapsed(open), children: [_jsx(InlineIcon, { icon: iconChevronDown, className: "nx-group-form__chevron" }), heading] })) : (_jsx("div", { className: "nx-group-form__heading", children: heading })), (context.sortable || context.canAdd || context.removable) && (_jsxs("div", { className: "nx-group-form__actions", children: [context.sortable && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconArrowUp }), "aria-label": `Move ${name} up`, title: "Move up", disabled: index === 0, onClick: () => onAction("move-up", rowKey) }), _jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconArrowDown }), "aria-label": `Move ${name} down`, title: "Move down", disabled: index === count - 1, onClick: () => onAction("move-down", rowKey) })] })), context.canAdd && (_jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconCopy }), "aria-label": `Duplicate ${name}`, title: "Duplicate", onClick: () => onAction("duplicate", rowKey) })), context.removable && (_jsx(Button, { variant: "ghost", size: "sm", hoverTone: "danger", icon: _jsx(InlineIcon, { icon: iconTrash }), "aria-label": `Remove ${name}`, title: "Remove", onClick: () => onAction("remove", rowKey) }))] }))] }), open && (_jsx("div", { id: bodyId, role: "group", "aria-labelledby": titleId, className: "nx-group-form__body", onBlur: onBlur, children: inputs.map(params => params.hidden ? null : (_jsx("div", { className: cx("nx-group-form__cell",
|
|
66
|
+
return (_jsxs("li", { className: "nx-group-form__section", "data-nx-row": rowKey, "data-open": open || undefined, "data-problem": problems > 0 || undefined, children: [_jsxs("div", { className: "nx-group-form__head", children: [context.collapsible ? (_jsxs("button", { type: "button", className: "nx-group-form__toggle", "aria-expanded": open, "aria-controls": open ? bodyId : undefined, "data-nx-row-actions": "", onClick: () => setCollapsed(open), children: [_jsx(InlineIcon, { icon: iconChevronDown, className: "nx-group-form__chevron" }), heading] })) : (_jsx("div", { className: "nx-group-form__heading", children: heading })), (context.sortable || context.canAdd || context.removable) && (_jsxs("div", { className: "nx-group-form__actions", children: [context.sortable && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconArrowUp }), "aria-label": `Move ${name} up`, title: "Move up", disabled: index === 0, onClick: () => onAction("move-up", rowKey) }), _jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconArrowDown }), "aria-label": `Move ${name} down`, title: "Move down", disabled: index === count - 1, onClick: () => onAction("move-down", rowKey) })] })), context.canAdd && (_jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconCopy }), "aria-label": `Duplicate ${name}`, title: "Duplicate", onClick: () => onAction("duplicate", rowKey) })), context.removable && (_jsx(Button, { variant: "ghost", size: "sm", hoverTone: "danger", icon: _jsx(InlineIcon, { icon: iconTrash }), "aria-label": `Remove ${name}`, title: "Remove", onClick: () => onAction("remove", rowKey) }))] }))] }), open && (_jsx("div", { id: bodyId, role: "group", "aria-labelledby": titleId, className: "nx-group-form__body", onBlur: onBlur, children: inputs.map(params => params.hidden ? null : (_jsx("div", { className: cx("nx-group-form__cell", isBooleanInput(params) && "nx-group-form__cell--choice"), style: { "--nx-span": Math.max(1, Math.min(12, params.span ?? 12)) }, "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: "group", idPrefix: context.idPrefix, size: context.size, design: context.design, readOnly: context.readOnly, disabled: context.disabled, lock: context.cellLock(rowKey, params.name, index) }, store.revision(rowKey, params.name)) }, params.name))) }))] }));
|
|
68
67
|
});
|
|
69
68
|
/**
|
|
70
69
|
* The rows of a `TabularForm` as sections: each row's inputs with their own labels, on a 12-column grid (`span` in
|
|
@@ -77,11 +76,11 @@ const GroupSection = memo(function GroupSection({ store, rowKey, index, count, i
|
|
|
77
76
|
*
|
|
78
77
|
* @example
|
|
79
78
|
* <GroupForm name="addresses" label="Addresses" itemLabel="Address" max={5} inputs={[
|
|
80
|
-
* { type:
|
|
81
|
-
* { type:
|
|
82
|
-
* { type:
|
|
83
|
-
* { type:
|
|
84
|
-
* { type:
|
|
79
|
+
* { type: EInputTypes.Dropdown, name: "country", label: "Country", required: true, options: countries, span: 6 },
|
|
80
|
+
* { type: EInputTypes.Text, name: "city", label: "City", required: true, span: 6 },
|
|
81
|
+
* { type: EInputTypes.Text, name: "zipCode", label: "Zip code", span: 4 },
|
|
82
|
+
* { type: EInputTypes.Dropdown, name: "type", label: "Type", options: ADDRESS_TYPES, span: 4 },
|
|
83
|
+
* { type: EInputTypes.Radio, name: "isPrimary", label: "Primary address", span: 4, required: true },
|
|
85
84
|
* ]} />
|
|
86
85
|
*/
|
|
87
86
|
export function GroupForm(props) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import type
|
|
2
|
+
import { type InputParams } from "../../Interfaces/FormInterfaces.ts";
|
|
3
3
|
import type { InputDesign } from "../../Interfaces/InputInterfaces.ts";
|
|
4
4
|
import type { Size } from "../../Interfaces/TypeInterfaces.ts";
|
|
5
5
|
/** What a form sets on an input it renders from params. Keys given here replace the params' own, even when `undefined`. */
|
|
@@ -10,8 +10,8 @@ export interface InputOverrides {
|
|
|
10
10
|
form?: string;
|
|
11
11
|
/** The value the input starts with: `defaultChecked` for a checkbox, switch, or radio, `defaultValue` for the rest. */
|
|
12
12
|
defaultValue?: unknown;
|
|
13
|
-
/** The new value after each change, before the params' own `onValueChange
|
|
14
|
-
onValueChange?: (value: unknown) => void;
|
|
13
|
+
/** The new value after each change, before the params' own `onValueChange`; a dropdown's item (items) with it. */
|
|
14
|
+
onValueChange?: (value: unknown, item?: unknown) => void;
|
|
15
15
|
error?: string | null;
|
|
16
16
|
/** Replaces the params' own, e.g. `false` for a radio in rows, where "required" means one row has it. */
|
|
17
17
|
required?: boolean;
|
|
@@ -25,13 +25,16 @@ export interface InputOverrides {
|
|
|
25
25
|
readOnlyMessage?: string;
|
|
26
26
|
/** On top of the params, like `readOnly`. */
|
|
27
27
|
disabled?: boolean;
|
|
28
|
+
/** A dropdown's value is on its way: its spinner shows, as a form's does while a picker is asked about a remembered value. */
|
|
29
|
+
loading?: boolean;
|
|
28
30
|
fieldName?: string;
|
|
29
31
|
className?: string;
|
|
30
32
|
"aria-label"?: string;
|
|
31
33
|
}
|
|
32
34
|
/**
|
|
33
|
-
* Renders an input from its params: `{ type:
|
|
34
|
-
* `TextBox` of type email. Forms render their `inputs` with it, and tabular and group forms render each row's inputs.
|
|
35
|
-
* Layout keys (`span`, `width`, `grow
|
|
35
|
+
* Renders an input from its params: `{ type: EInputTypes.Email, name: "email", label: "Email", required: true }` gives a
|
|
36
|
+
* `TextBox` of HTML type email. Forms render their `inputs` with it, and tabular and group forms render each row's inputs.
|
|
37
|
+
* Layout keys (`span`, `width`, `grow`, and the Form-only ones such as `showWhen` and `compute`) stay out of the
|
|
38
|
+
* component's props; `hidden` renders nothing.
|
|
36
39
|
*/
|
|
37
40
|
export declare function renderInput(params: InputParams, overrides?: InputOverrides): ReactNode;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { isBooleanInput } from "../../Helpers/InputParamsHelpers.js";
|
|
3
|
+
import { EInputTypes } from "../../Interfaces/FormInterfaces.js";
|
|
3
4
|
import { CheckBox, RadioButton, Switch } from "./CheckBox.js";
|
|
4
5
|
import { CheckBoxGroup } from "./CheckBoxGroup.js";
|
|
6
|
+
import { CodeBox } from "./CodeBox.js";
|
|
5
7
|
import { DatePicker } from "./DatePicker.js";
|
|
6
8
|
import { DateRangePicker } from "./DateRangePicker.js";
|
|
7
9
|
import { DateTimePicker } from "./DateTimePicker.js";
|
|
@@ -9,23 +11,26 @@ import { Dropdown } from "./Dropdown.js";
|
|
|
9
11
|
import { GroupForm } from "./GroupForm.js";
|
|
10
12
|
import { MarkdownEditor } from "./MarkdownEditor.js";
|
|
11
13
|
import { NumberBox } from "./NumberBox.js";
|
|
14
|
+
import { PictureInput } from "./PictureInput.js";
|
|
12
15
|
import { RadioGroup } from "./RadioGroup.js";
|
|
13
16
|
import { RichTextEditor } from "./RichTextEditor.js";
|
|
14
17
|
import { RangeSlider, Slider } from "./Slider.js";
|
|
15
18
|
import { TabularForm } from "./TabularForm.js";
|
|
16
19
|
import { TextArea } from "./TextArea.js";
|
|
17
20
|
import { TextBox } from "./TextBox.js";
|
|
21
|
+
import { isTextInputType, TEXT_BOX_TYPES } from "./TextBoxTypes.js";
|
|
18
22
|
import { TimePicker } from "./TimePicker.js";
|
|
19
23
|
import { TreeView } from "./TreeView.js";
|
|
20
24
|
/**
|
|
21
|
-
* Renders an input from its params: `{ type:
|
|
22
|
-
* `TextBox` of type email. Forms render their `inputs` with it, and tabular and group forms render each row's inputs.
|
|
23
|
-
* Layout keys (`span`, `width`, `grow
|
|
25
|
+
* Renders an input from its params: `{ type: EInputTypes.Email, name: "email", label: "Email", required: true }` gives a
|
|
26
|
+
* `TextBox` of HTML type email. Forms render their `inputs` with it, and tabular and group forms render each row's inputs.
|
|
27
|
+
* Layout keys (`span`, `width`, `grow`, and the Form-only ones such as `showWhen` and `compute`) stay out of the
|
|
28
|
+
* component's props; `hidden` renders nothing.
|
|
24
29
|
*/
|
|
25
30
|
export function renderInput(params, overrides = {}) {
|
|
26
31
|
if (params.hidden)
|
|
27
32
|
return null;
|
|
28
|
-
const { type, section: _section, span: _span, width: _width, grow: _grow, hidden: _hidden, dependsOn: _dependsOn, onValueChange: ownChange, ...rest } = params;
|
|
33
|
+
const { type, section: _section, span: _span, width: _width, grow: _grow, hidden: _hidden, dependsOn: _dependsOn, narrowedBy: _narrowedBy, showWhen: _showWhen, readOnlyWhen: _readOnlyWhen, remember: _remember, labelWhen: _labelWhen, spanWhen: _spanWhen, compute: _compute, onValueChange: ownChange, ...rest } = params;
|
|
29
34
|
const { defaultValue, onValueChange, readOnly, disabled, ...set } = overrides;
|
|
30
35
|
const props = { ...rest, ...set };
|
|
31
36
|
if (readOnly)
|
|
@@ -42,57 +47,60 @@ export function renderInput(params, overrides = {}) {
|
|
|
42
47
|
}
|
|
43
48
|
if (onValueChange || ownChange) {
|
|
44
49
|
props.onValueChange = (value, ...more) => {
|
|
45
|
-
|
|
50
|
+
// A dropdown says which item it picked; the other inputs' second argument is their event.
|
|
51
|
+
onValueChange?.(value, type === EInputTypes.Dropdown ? more[0] : undefined);
|
|
46
52
|
ownChange?.(value, ...more);
|
|
47
53
|
};
|
|
48
54
|
}
|
|
55
|
+
if (isTextInputType(type))
|
|
56
|
+
return _jsx(TextBox, { ...props, type: TEXT_BOX_TYPES[type] });
|
|
49
57
|
switch (type) {
|
|
50
|
-
case
|
|
51
|
-
case "email":
|
|
52
|
-
case "password":
|
|
53
|
-
case "search":
|
|
54
|
-
case "tel":
|
|
55
|
-
case "url":
|
|
56
|
-
return _jsx(TextBox, { ...props, type: type });
|
|
57
|
-
case "textarea":
|
|
58
|
+
case EInputTypes.TextArea:
|
|
58
59
|
return _jsx(TextArea, { ...props });
|
|
59
|
-
case
|
|
60
|
+
case EInputTypes.Number:
|
|
60
61
|
return _jsx(NumberBox, { ...props });
|
|
61
|
-
case
|
|
62
|
+
case EInputTypes.Code:
|
|
63
|
+
return _jsx(CodeBox, { ...props });
|
|
64
|
+
case EInputTypes.CheckBox:
|
|
62
65
|
return _jsx(CheckBox, { ...props });
|
|
63
|
-
case
|
|
66
|
+
case EInputTypes.Switch:
|
|
64
67
|
return _jsx(Switch, { ...props });
|
|
65
|
-
case
|
|
68
|
+
case EInputTypes.Radio:
|
|
66
69
|
return _jsx(RadioButton, { ...props });
|
|
67
|
-
case
|
|
70
|
+
case EInputTypes.CheckBoxGroup:
|
|
68
71
|
return _jsx(CheckBoxGroup, { ...props });
|
|
69
|
-
case
|
|
72
|
+
case EInputTypes.RadioGroup:
|
|
70
73
|
return _jsx(RadioGroup, { ...props });
|
|
71
|
-
case
|
|
74
|
+
case EInputTypes.Dropdown:
|
|
72
75
|
// Either overload: `multiple` travels in the props.
|
|
73
76
|
return _jsx(Dropdown, { ...props });
|
|
74
|
-
case
|
|
77
|
+
case EInputTypes.Date:
|
|
75
78
|
return _jsx(DatePicker, { ...props });
|
|
76
|
-
case
|
|
79
|
+
case EInputTypes.DateRange:
|
|
77
80
|
return _jsx(DateRangePicker, { ...props });
|
|
78
|
-
case
|
|
81
|
+
case EInputTypes.Time:
|
|
79
82
|
return _jsx(TimePicker, { ...props });
|
|
80
|
-
case
|
|
83
|
+
case EInputTypes.DateTime:
|
|
81
84
|
return _jsx(DateTimePicker, { ...props });
|
|
82
|
-
case
|
|
85
|
+
case EInputTypes.Slider:
|
|
83
86
|
return _jsx(Slider, { ...props });
|
|
84
|
-
case
|
|
87
|
+
case EInputTypes.RangeSlider:
|
|
85
88
|
return _jsx(RangeSlider, { ...props });
|
|
86
|
-
case
|
|
89
|
+
case EInputTypes.Markdown:
|
|
87
90
|
return _jsx(MarkdownEditor, { ...props });
|
|
88
|
-
case
|
|
91
|
+
case EInputTypes.RichText:
|
|
89
92
|
return _jsx(RichTextEditor, { ...props });
|
|
90
|
-
case
|
|
93
|
+
case EInputTypes.Tree:
|
|
91
94
|
return _jsx(TreeView, { ...props });
|
|
92
|
-
case
|
|
95
|
+
case EInputTypes.Tabular:
|
|
93
96
|
return _jsx(TabularForm, { ...props });
|
|
94
|
-
case
|
|
97
|
+
case EInputTypes.Group:
|
|
95
98
|
return _jsx(GroupForm, { ...props });
|
|
99
|
+
case EInputTypes.Picture: {
|
|
100
|
+
// A picture is the owner's file, not a value: the form's value and field name stay out of it.
|
|
101
|
+
const { defaultValue: _value, name: _name, form: _form, ...picture } = props;
|
|
102
|
+
return _jsx(PictureInput, { ...picture });
|
|
103
|
+
}
|
|
96
104
|
default:
|
|
97
105
|
return null;
|
|
98
106
|
}
|
|
@@ -4,7 +4,7 @@ import { InlineIcon, iconMinus, iconPlus } from "nexus-icons";
|
|
|
4
4
|
import { useEffect, useLayoutEffect, useRef, } from "react";
|
|
5
5
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
6
|
import { clampNumber, cleanNumberText, formatNumber, getNumberSymbols, isPartialNumber, parseNumber, readPastedNumber, roundDecimal, stepNumber, } from "../../Helpers/NumberHelpers.js";
|
|
7
|
-
import { useNumberLocale } from "../../Helpers/UserPreferences.js";
|
|
7
|
+
import { useInputDesign, useNumberLocale } from "../../Helpers/UserPreferences.js";
|
|
8
8
|
import { validateNumber } from "../../Helpers/ValidationHelpers.js";
|
|
9
9
|
import { Field, hasContent } from "./Field.js";
|
|
10
10
|
import { setInputValue, useInputField } from "./InputField.js";
|
|
@@ -24,7 +24,9 @@ const isNumber = (value) => value !== null && !Number.isNaN(value);
|
|
|
24
24
|
* @example <NumberBox label="Amount" prefix="Rs" min={0} decimals={2} fixedDecimals grouping required />
|
|
25
25
|
*/
|
|
26
26
|
export function NumberBox(props) {
|
|
27
|
-
const { label, hint, error, design
|
|
27
|
+
const { label, hint, error, design: ownDesign, size = "md", icon, iconEnd, prefix, suffix, step = 1, stepper = false, clamp = false, readOnlyMessage = "Read-only: this number cannot be changed.", fieldName, validateOn, value, defaultValue, onValueChange, decimals, fixedDecimals = false, grouping = false, locale: ownLocale, className, inputClassName, style, required, min, max, validate, name, form, placeholder, disabled, readOnly, ref, onChange, onBlur, onFocus, onInvalid, onKeyDown, onPaste, onClick, ...inputProps } = props;
|
|
28
|
+
// The person's input design (the quick preferences) when this page and its form name none, for a labelled input.
|
|
29
|
+
const design = useInputDesign(ownDesign, Boolean(label));
|
|
28
30
|
// Without a locale of its own, the box writes and reads numbers the way the person chose (Number scale, Thousands).
|
|
29
31
|
const preferred = useNumberLocale();
|
|
30
32
|
const locale = ownLocale ?? preferred;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import type { ApiModuleValue } from "../../Interfaces/ApiInterfaces.ts";
|
|
3
|
+
import type { Size } from "../../Interfaces/TypeInterfaces.ts";
|
|
4
|
+
import { type OwnerFilesParams } from "../Viewers/OwnerFiles.tsx";
|
|
5
|
+
export interface PictureInputProps extends Omit<OwnerFilesParams<ApiModuleValue>, "ownerId"> {
|
|
6
|
+
/**
|
|
7
|
+
* The owner's id: the record the form edits (`row?.id` in a CRUD's inputs). Null or missing for a record not saved
|
|
8
|
+
* yet, which has no pictures until it is: the input says so.
|
|
9
|
+
*/
|
|
10
|
+
ownerId?: string | null;
|
|
11
|
+
label?: ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* Help text under the picture. Default: what the category takes, once it is known. There is no `required`: the
|
|
14
|
+
* picture is saved at once, apart from the form, so a form could not refuse a save without one.
|
|
15
|
+
*/
|
|
16
|
+
hint?: ReactNode;
|
|
17
|
+
/** The preview's height: `sm` 72px, `md` 112px, `lg` 152px. Default `md`. */
|
|
18
|
+
size?: Size;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
/** What the record is, for the note a new one shows: "branch". Default "record". */
|
|
21
|
+
subject?: string;
|
|
22
|
+
id?: string;
|
|
23
|
+
className?: string;
|
|
24
|
+
style?: CSSProperties;
|
|
25
|
+
"aria-label"?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A record's picture in a form: the owner's main picture, with Upload (a new one, edited first, made the main picture),
|
|
29
|
+
* Edit (the main one, saved as a new picture), Remove (no main picture; the pictures stay) and, when the category keeps
|
|
30
|
+
* more than one, All pictures (the gallery). In a category that keeps one file, Upload is Replace and Remove deletes it.
|
|
31
|
+
* Clicking or pressing the picture opens the pictures window too, read only where the input is. The change is saved at
|
|
32
|
+
* once, not with the form: a picture is the owner's file, not one of the record's values, so the form keeps no field for
|
|
33
|
+
* it and sends nothing under its name.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* inputs: (action, row) => [
|
|
37
|
+
* { type: EInputTypes.Picture, name: "logo", label: "Logo", module: NexusModule.Enterprise, category: "company-logo", ownerId: row?.id, aspect: 1 },
|
|
38
|
+
* ...
|
|
39
|
+
* ]
|
|
40
|
+
*/
|
|
41
|
+
export declare function PictureInput({ label, hint, size, disabled, readOnly, subject, id, className, style, "aria-label": ariaLabel, ...params }: PictureInputProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { iconPencil, iconPhoto, iconTrash, iconUpload, InlineIcon } from "nexus-icons";
|
|
4
|
+
import { useId, useRef, useState } from "react";
|
|
5
|
+
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
|
+
import { getFileAdapter } from "../../Services/FileApi.js";
|
|
7
|
+
import { preloadImageEditor } from "../../Services/ImageEditorLoader.js";
|
|
8
|
+
import { Button } from "../Documents/Button.js";
|
|
9
|
+
import { busyProps, openPictureGallery, useFocusThroughChange, useOwnerFiles } from "../Viewers/OwnerFiles.js";
|
|
10
|
+
import { ChoiceLegend, GroupFooter } from "./ChoiceField.js";
|
|
11
|
+
const HEIGHTS = { sm: 72, md: 112, lg: 152 };
|
|
12
|
+
/**
|
|
13
|
+
* A record's picture in a form: the owner's main picture, with Upload (a new one, edited first, made the main picture),
|
|
14
|
+
* Edit (the main one, saved as a new picture), Remove (no main picture; the pictures stay) and, when the category keeps
|
|
15
|
+
* more than one, All pictures (the gallery). In a category that keeps one file, Upload is Replace and Remove deletes it.
|
|
16
|
+
* Clicking or pressing the picture opens the pictures window too, read only where the input is. The change is saved at
|
|
17
|
+
* once, not with the form: a picture is the owner's file, not one of the record's values, so the form keeps no field for
|
|
18
|
+
* it and sends nothing under its name.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* inputs: (action, row) => [
|
|
22
|
+
* { type: EInputTypes.Picture, name: "logo", label: "Logo", module: NexusModule.Enterprise, category: "company-logo", ownerId: row?.id, aspect: 1 },
|
|
23
|
+
* ...
|
|
24
|
+
* ]
|
|
25
|
+
*/
|
|
26
|
+
export function PictureInput({ label, hint, size = "md", disabled = false, readOnly = false, subject = "record", id, className, style, "aria-label": ariaLabel, ...params }) {
|
|
27
|
+
const generated = useId();
|
|
28
|
+
const fieldId = id ?? generated;
|
|
29
|
+
const [failed, setFailed] = useState(null);
|
|
30
|
+
const files = useOwnerFiles({ ...params, readOnly: readOnly || disabled });
|
|
31
|
+
const { status, data, busy, canWrite, readOnlyReason, editorProblem, actions } = files;
|
|
32
|
+
// A keyboard's focus stays in the input through a change: on the button pressed, or the first left when it went.
|
|
33
|
+
const rootRef = useRef(null);
|
|
34
|
+
useFocusThroughChange(rootRef, busy);
|
|
35
|
+
const primary = data?.files.find(file => file.isPrimary) ?? null;
|
|
36
|
+
const url = primary ? getFileAdapter().fileUrl(primary, "thumbnail") : null;
|
|
37
|
+
const shown = url && failed !== url;
|
|
38
|
+
const ratio = typeof params.aspect === "number" && params.aspect > 0 ? params.aspect : 1;
|
|
39
|
+
const preview = { "--nx-picture-h": `${HEIGHTS[size]}px`, "--nx-picture-ratio": ratio };
|
|
40
|
+
const unsaved = !params.ownerId;
|
|
41
|
+
// A category that keeps one file: a new picture takes its place, and Remove deletes it (there is nothing else to show).
|
|
42
|
+
const single = data?.category.maxCount === 1;
|
|
43
|
+
const kept = data?.files ?? [];
|
|
44
|
+
const note = unsaved
|
|
45
|
+
? `Save the ${subject} first, then add its picture.`
|
|
46
|
+
: status.kind === "failed"
|
|
47
|
+
? status.message
|
|
48
|
+
: busy
|
|
49
|
+
? `${busy}…`
|
|
50
|
+
: editorProblem
|
|
51
|
+
? // Why Upload or Edit did not open the editor, until a try opens it: never a button that does nothing.
|
|
52
|
+
editorProblem
|
|
53
|
+
: // Why it cannot be changed, when the page or the backend says (`lockReason`, a refused key): never a silent input.
|
|
54
|
+
(readOnlyReason ?? hint ?? (data && canWrite ? `${data.files.length} of ${data.category.maxCount} ${single ? "picture" : "pictures"} kept.` : undefined));
|
|
55
|
+
const problem = status.kind === "failed" || (!unsaved && !busy && Boolean(editorProblem));
|
|
56
|
+
const named = typeof label === "string" && label ? label : "Picture";
|
|
57
|
+
/** The pictures window, as All pictures opens it: what changes there is read again here. */
|
|
58
|
+
const openAll = () => openPictureGallery({
|
|
59
|
+
...params,
|
|
60
|
+
ownerId: params.ownerId,
|
|
61
|
+
readOnly: readOnly || disabled,
|
|
62
|
+
onChange: primary => {
|
|
63
|
+
void actions.reload();
|
|
64
|
+
params.onChange?.(primary);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const pictureClass = cx("nx-picture", params.shape === "circle" && "nx-picture--round", !shown && "nx-picture--empty", status.kind === "loading" && "nx-picture--loading");
|
|
68
|
+
const face = shown ? _jsx("img", { src: url, alt: "", decoding: "async", onError: () => setFailed(url) }) : _jsx(InlineIcon, { icon: iconPhoto });
|
|
69
|
+
return (_jsxs("fieldset", { ref: rootRef, id: fieldId, className: cx("nx-choices", size !== "md" && `nx-choices--${size}`, "nx-picture-input", className), style: style, disabled: disabled, "aria-label": ariaLabel, "aria-busy": status.kind === "loading" || Boolean(busy) || undefined, "aria-describedby": note ? `${fieldId}-message` : undefined, "data-readonly": readOnly || undefined, children: [_jsx(ChoiceLegend, { label: label }), _jsxs("div", { className: "nx-picture-input__body", children: [!unsaved && data ? (_jsx("button", { type: "button", className: pictureClass, style: preview, "aria-label": primary ? `${named}: open the pictures` : `${named}: none yet, open the pictures`, "aria-haspopup": "dialog", title: "All pictures", ...busyProps(busy, openAll), onPointerEnter: preloadImageEditor, children: face })) : (_jsx("span", { className: pictureClass, style: preview, role: "img", "aria-label": primary ? `${named}: ${primary.name}` : "No picture", children: face })), !unsaved && data && (_jsxs("div", { className: "nx-picture-input__actions", role: "group", children: [canWrite && (_jsx(Button, { size: "sm", icon: _jsx(InlineIcon, { icon: iconUpload }), ...busyProps(busy, () => void actions.replace()), onPointerEnter: preloadImageEditor, onFocus: preloadImageEditor, children: single && kept.length > 0 ? "Replace" : "Upload" })), canWrite && primary && (_jsx(Button, { size: "sm", variant: "ghost", icon: _jsx(InlineIcon, { icon: iconPencil }), ...busyProps(busy, () => void actions.edit(primary)), onPointerEnter: preloadImageEditor, onFocus: preloadImageEditor, children: "Edit" })), canWrite && primary && (_jsx(Button, { size: "sm", variant: "ghost", hoverTone: "danger", icon: _jsx(InlineIcon, { icon: iconTrash }), ...busyProps(busy, () => void (single ? actions.remove(primary) : actions.makePrimary(null))), children: single ? "Delete" : "Remove" })), (data.category.maxCount > 1 || kept.length > 1 || (kept.length > 0 && !primary)) && (_jsxs(Button, { size: "sm", variant: "ghost", ...busyProps(busy, openAll), children: ["All pictures (", data.files.length, ")"] }))] }))] }), _jsx(GroupFooter, { id: `${fieldId}-message`, hint: problem ? undefined : note, error: problem ? note : undefined })] }));
|
|
70
|
+
}
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import type { ChangeEvent } from "react";
|
|
2
|
-
import type { OptionValue } from "../../Interfaces/InputInterfaces.ts";
|
|
2
|
+
import type { ChoiceDesign, OptionValue } from "../../Interfaces/InputInterfaces.ts";
|
|
3
3
|
import { type ChoiceGroupProps } from "./ChoiceField.tsx";
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* How a `RadioGroup` draws its options: `default` (a circle beside each label), `cards` (a card each, with its
|
|
6
|
+
* description), or `tabs` (nav-style tabs on one track, as `TabButtons` draws them).
|
|
7
|
+
*/
|
|
8
|
+
export type RadioGroupDesign = ChoiceDesign | "tabs";
|
|
9
|
+
export interface RadioGroupProps<T extends OptionValue = string> extends Omit<ChoiceGroupProps<T>, "design"> {
|
|
10
|
+
/**
|
|
11
|
+
* `default`: a circle beside each label. `cards`: each option is a card. `tabs`: the options as nav-style tabs on one
|
|
12
|
+
* track (`TabButtons`' look), wrapping onto more rows where they do not fit; each option's `description` is its
|
|
13
|
+
* tooltip and what a screen reader hears with it, and the chosen option's shows under the track. `orientation` and
|
|
14
|
+
* `columns` do not apply to tabs.
|
|
15
|
+
*/
|
|
16
|
+
design?: RadioGroupDesign;
|
|
5
17
|
/** The chosen value, or `null` for none. With `value`, the group is controlled; pair it with `onValueChange`. */
|
|
6
18
|
value?: T | null;
|
|
7
19
|
defaultValue?: T | null;
|
|
@@ -12,7 +24,8 @@ export interface RadioGroupProps<T extends OptionValue = string> extends ChoiceG
|
|
|
12
24
|
}
|
|
13
25
|
/**
|
|
14
26
|
* One choice from a list of options, such as a room type or a payment method. Native radio buttons, so the arrow keys
|
|
15
|
-
* move the choice. `design="cards"` shows each option as a card with its description
|
|
27
|
+
* move the choice. `design="cards"` shows each option as a card with its description, `design="tabs"` as nav-style tabs
|
|
28
|
+
* whose descriptions are tooltips (the chosen one's under the track). Read-only (default note
|
|
16
29
|
* "Read-only: this choice cannot be changed.") shows a lock after the label and a note under a clicked option.
|
|
17
30
|
*
|
|
18
31
|
* @example <RadioGroup name="roomType" label="Room type" required design="cards" options={[{ value: "deluxe", label: "Deluxe" }]} />
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { validateSelection } from "../../Helpers/ValidationHelpers.js";
|
|
4
|
-
import {
|
|
4
|
+
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
5
|
+
import { ChoiceItem, ChoiceLegend, ChoiceTab, ChoiceTabNotes, GroupFooter, groupLayout, leavesField, OPTION_ATTRIBUTE, optionAnchor, showChosenNote, useChoiceField } from "./ChoiceField.js";
|
|
5
6
|
import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
|
|
6
7
|
/**
|
|
7
8
|
* One choice from a list of options, such as a room type or a payment method. Native radio buttons, so the arrow keys
|
|
8
|
-
* move the choice. `design="cards"` shows each option as a card with its description
|
|
9
|
+
* move the choice. `design="cards"` shows each option as a card with its description, `design="tabs"` as nav-style tabs
|
|
10
|
+
* whose descriptions are tooltips (the chosen one's under the track). Read-only (default note
|
|
9
11
|
* "Read-only: this choice cannot be changed.") shows a lock after the label and a note under a clicked option.
|
|
10
12
|
*
|
|
11
13
|
* @example <RadioGroup name="roomType" label="Room type" required design="cards" options={[{ value: "deluxe", label: "Deluxe" }]} />
|
|
@@ -14,6 +16,7 @@ export function RadioGroup(props) {
|
|
|
14
16
|
const { options, name, label, hint, required, validate, value, defaultValue, readOnly = false, disabled, onValueChange } = props;
|
|
15
17
|
const { readOnlyMessage = "Read-only: this choice cannot be changed." } = props;
|
|
16
18
|
const { size = "md" } = props;
|
|
19
|
+
const tabs = props.design === "tabs";
|
|
17
20
|
const chosen = (inputs) => {
|
|
18
21
|
const input = inputs.find(item => item.checked);
|
|
19
22
|
return input ? [options[Number(input.getAttribute(OPTION_ATTRIBUTE))].value] : [];
|
|
@@ -21,6 +24,8 @@ export function RadioGroup(props) {
|
|
|
21
24
|
const field = useChoiceField(props, {
|
|
22
25
|
check: (inputs, fieldName) => validateSelection(chosen(inputs), { required, validate: validate && (values => validate(values[0] ?? null)) }, fieldName),
|
|
23
26
|
checkKeys: [value, required, validate, options],
|
|
27
|
+
// Tabs show the chosen option's description under the track: it follows every change and a form's reset.
|
|
28
|
+
sync: tabs ? showChosenNote : undefined,
|
|
24
29
|
});
|
|
25
30
|
function handleChange(event) {
|
|
26
31
|
if (readOnly)
|
|
@@ -39,5 +44,5 @@ export function RadioGroup(props) {
|
|
|
39
44
|
}
|
|
40
45
|
const invalid = Boolean(field.error);
|
|
41
46
|
const layout = groupLayout(props);
|
|
42
|
-
return (_jsxs("fieldset", { ref: field.containerRef, id: field.fieldId, role: "radiogroup", className: layout.className, style: props.style, disabled: disabled, "data-invalid": invalid || undefined, "data-readonly": readOnly || undefined, "aria-label": props["aria-label"], "aria-labelledby": props["aria-labelledby"], "aria-describedby": field.error || hint ? field.messageId : undefined, "aria-required": required || undefined, "aria-invalid": invalid || undefined, "aria-readonly": readOnly || undefined, onChange: handleChange, onClick: readOnlyNotice.onClick, onKeyDown: readOnlyNotice.onKeyDown, onBlur: handleBlur, children: [_jsx(ChoiceLegend, { label: label, required: required, lock: readOnlyNotice.lock }), _jsx("div", { className: "nx-choices__options", style: layout.optionsStyle, children: options.map((option, index) => (_jsx(ChoiceItem, { type: "radio", groupId: field.fieldId, name: name ?? field.fieldId, option: option, index: index, size: size, checked: value !== undefined ? value === option.value : undefined, defaultChecked: defaultValue === option.value, invalid: invalid, readOnly: readOnly, onInvalid: field.onInvalid }, String(option.value)))) }), _jsx(GroupFooter, { id: field.messageId, hint: hint, error: field.error }), readOnlyNotice.notice] }));
|
|
47
|
+
return (_jsxs("fieldset", { ref: field.containerRef, id: field.fieldId, role: "radiogroup", className: layout.className, style: props.style, disabled: disabled, "data-invalid": invalid || undefined, "data-readonly": readOnly || undefined, "aria-label": props["aria-label"], "aria-labelledby": props["aria-labelledby"], "aria-describedby": field.error || hint ? field.messageId : undefined, "aria-required": required || undefined, "aria-invalid": invalid || undefined, "aria-readonly": readOnly || undefined, onChange: handleChange, onClick: readOnlyNotice.onClick, onKeyDown: readOnlyNotice.onKeyDown, onBlur: handleBlur, children: [_jsx(ChoiceLegend, { label: label, required: required, lock: readOnlyNotice.lock }), tabs ? (_jsx("div", { className: cx("nx-tabs", `nx-tabs--${size}`, "nx-tabs--wrap"), children: options.map((option, index) => (_jsx(ChoiceTab, { type: "radio", groupId: field.fieldId, name: name ?? field.fieldId, option: option, index: index, checked: value !== undefined ? value === option.value : undefined, defaultChecked: defaultValue === option.value, invalid: invalid, readOnly: readOnly, onInvalid: field.onInvalid }, String(option.value)))) })) : (_jsx("div", { className: "nx-choices__options", style: layout.optionsStyle, children: options.map((option, index) => (_jsx(ChoiceItem, { type: "radio", groupId: field.fieldId, name: name ?? field.fieldId, option: option, index: index, size: size, checked: value !== undefined ? value === option.value : undefined, defaultChecked: defaultValue === option.value, invalid: invalid, readOnly: readOnly, onInvalid: field.onInvalid }, String(option.value)))) })), tabs && _jsx(ChoiceTabNotes, { groupId: field.fieldId, options: options, chosen: options.findIndex(option => option.value === (value !== undefined ? value : defaultValue)) }), _jsx(GroupFooter, { id: field.messageId, hint: hint, error: field.error }), readOnlyNotice.notice] }));
|
|
43
48
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type FormEvent, type ReactNode } from "react";
|
|
2
2
|
import { RowsStore, type RowsSnapshot } from "../../Helpers/RowsStore.ts";
|
|
3
|
-
import type
|
|
3
|
+
import { type InputParams, type RowsInputProps, type RowValue } from "../../Interfaces/FormInterfaces.ts";
|
|
4
4
|
import type { InputDesign } from "../../Interfaces/InputInterfaces.ts";
|
|
5
5
|
import type { Size } from "../../Interfaces/TypeInterfaces.ts";
|
|
6
6
|
/** Form names of inputs inside rows that need one (radio and checkbox groups). They are taken out of the form's data. */
|