@nsail/ui 0.1.981 → 0.1.984
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/dist/dialog.js +12 -2
- package/dist/field.js +3 -0
- package/dist/format.d.ts +5 -1
- package/dist/format.js +8 -6
- package/dist/search.js +3 -0
- package/dist/select.js +1 -1
- package/dist/table.d.ts +3 -1
- package/dist/table.js +10 -3
- package/package.json +2 -2
package/dist/dialog.js
CHANGED
|
@@ -9,8 +9,17 @@ const DialogContext = createContext(null);
|
|
|
9
9
|
export function NsDialogHost({ children }) {
|
|
10
10
|
const [pending, setPending] = useState(null);
|
|
11
11
|
const dialog = useRef(null);
|
|
12
|
+
const open = useRef(null);
|
|
13
|
+
// A confirmation asked while another is open ends the first as declined: no caller waits forever.
|
|
12
14
|
const confirm = useCallback((text, danger = false) => {
|
|
13
|
-
return new Promise((resolve) =>
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
open.current?.resolve(false);
|
|
17
|
+
open.current = { text, danger, resolve };
|
|
18
|
+
setPending(open.current);
|
|
19
|
+
});
|
|
20
|
+
}, []);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
return () => open.current?.resolve(false);
|
|
14
23
|
}, []);
|
|
15
24
|
useEffect(() => {
|
|
16
25
|
if (!pending || !dialog.current) {
|
|
@@ -34,7 +43,8 @@ export function NsDialogHost({ children }) {
|
|
|
34
43
|
(pending.danger ? wayOut : act).focus();
|
|
35
44
|
}, [pending]);
|
|
36
45
|
function close(confirmed) {
|
|
37
|
-
|
|
46
|
+
open.current?.resolve(confirmed);
|
|
47
|
+
open.current = null;
|
|
38
48
|
dialog.current?.close();
|
|
39
49
|
setPending(null);
|
|
40
50
|
}
|
package/dist/field.js
CHANGED
|
@@ -25,6 +25,9 @@ export function NsField({ name, multiline = false, autoFocus, label }) {
|
|
|
25
25
|
}
|
|
26
26
|
function number(event) {
|
|
27
27
|
const raw = event.target.value;
|
|
28
|
+
if (raw !== "" && field.kind === "integer" && !Number.isInteger(Number(raw))) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
28
31
|
set(raw === "" ? (field.nullable ? null : undefined) : Number(raw));
|
|
29
32
|
}
|
|
30
33
|
// An emptied exact number is absent, as a number is: "" is no long the server reads.
|
package/dist/format.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { Field, Strings } from "@nsail/stack";
|
|
2
|
+
export interface DisplayOptions {
|
|
3
|
+
/** False renders a number without digit grouping. */
|
|
4
|
+
grouped?: boolean;
|
|
5
|
+
}
|
|
2
6
|
/** A wire value as a person reads it: the member's kind decides, the language formats. */
|
|
3
|
-
export declare function display(strings: Strings, field: Field, value: unknown): string;
|
|
7
|
+
export declare function display(strings: Strings, field: Field, value: unknown, options?: DisplayOptions): string;
|
package/dist/format.js
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
const whole = /^-?\d+$/;
|
|
4
4
|
const fixed = /^-?\d*(\.\d+)?$/;
|
|
5
5
|
/** A wire value as a person reads it: the member's kind decides, the language formats. */
|
|
6
|
-
export function display(strings, field, value) {
|
|
6
|
+
export function display(strings, field, value, options) {
|
|
7
|
+
// Omitted, not true: true forces grouping of four digits, which the language's own default does not.
|
|
8
|
+
const useGrouping = options?.grouped === false ? false : undefined;
|
|
7
9
|
if (value === null || value === undefined || value === "") {
|
|
8
10
|
return "";
|
|
9
11
|
}
|
|
@@ -21,24 +23,24 @@ export function display(strings, field, value) {
|
|
|
21
23
|
return new Date(String(value)).toLocaleString(strings.language, { dateStyle: "short", timeStyle: "short" });
|
|
22
24
|
// A value that is not the wire's text (a Number a product still writes) shows as a figure.
|
|
23
25
|
case "bigint":
|
|
24
|
-
return whole.test(String(value)) ? BigInt(String(value)).toLocaleString(strings.language) : Number(value).toLocaleString(strings.language);
|
|
26
|
+
return whole.test(String(value)) ? BigInt(String(value)).toLocaleString(strings.language, { useGrouping }) : Number(value).toLocaleString(strings.language, { useGrouping });
|
|
25
27
|
case "decimal":
|
|
26
|
-
return fixed.test(String(value)) ? decimal(strings.language, String(value)) : Number(value).toLocaleString(strings.language);
|
|
28
|
+
return fixed.test(String(value)) ? decimal(strings.language, String(value), useGrouping) : Number(value).toLocaleString(strings.language, { useGrouping });
|
|
27
29
|
case "integer":
|
|
28
30
|
case "number":
|
|
29
|
-
return Number(value).toLocaleString(strings.language);
|
|
31
|
+
return Number(value).toLocaleString(strings.language, { useGrouping });
|
|
30
32
|
default:
|
|
31
33
|
return String(value);
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
// A decimal carries up to 28 fraction digits and Intl stops at 20 (and reads a string as a
|
|
35
37
|
// Number before Intl V3): the whole part is grouped as a BigInt and the fraction kept as sent.
|
|
36
|
-
function decimal(language, text) {
|
|
38
|
+
function decimal(language, text, useGrouping) {
|
|
37
39
|
const negative = text.startsWith("-");
|
|
38
40
|
const [whole, fraction] = (negative ? text.slice(1) : text).split(".");
|
|
39
41
|
const parts = new Intl.NumberFormat(language).formatToParts(-0.5);
|
|
40
42
|
const point = parts.find((part) => part.type === "decimal")?.value ?? ".";
|
|
41
43
|
const minus = parts.find((part) => part.type === "minusSign")?.value ?? "-";
|
|
42
|
-
const grouped = BigInt(whole === "" ? "0" : whole).toLocaleString(language);
|
|
44
|
+
const grouped = BigInt(whole === "" ? "0" : whole).toLocaleString(language, { useGrouping });
|
|
43
45
|
return `${negative ? minus : ""}${grouped}${fraction ? `${point}${fraction}` : ""}`;
|
|
44
46
|
}
|
package/dist/search.js
CHANGED
|
@@ -7,6 +7,9 @@ import { useStrings } from "@nsail/stack";
|
|
|
7
7
|
export function NsSearch({ field, value, onSearch, delay = 300 }) {
|
|
8
8
|
const strings = useStrings();
|
|
9
9
|
const [term, setTerm] = useState(value ?? "");
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
setTerm((current) => (current.trim() === (value ?? "") ? current : (value ?? "")));
|
|
12
|
+
}, [value]);
|
|
10
13
|
useEffect(() => {
|
|
11
14
|
const trimmed = term.trim();
|
|
12
15
|
if (trimmed === (value ?? "")) {
|
package/dist/select.js
CHANGED
|
@@ -10,5 +10,5 @@ export function NsEnumSelect({ field, value, onChange, none = "Common.All" }) {
|
|
|
10
10
|
throw new Error(`${field.key} is not an enum member.`);
|
|
11
11
|
}
|
|
12
12
|
const type = field.enum;
|
|
13
|
-
return (_jsxs("select", { className: "ns-select", "aria-label": strings.translate(field.key), value: value ?? "", onChange: (event) => onChange((event.target.value || null)), children: [field.nullable && _jsx("option", { value: "", children: strings.translate(none) }), type.values.map((option) => (_jsx("option", { value: option, children: strings.enumValue(type, option) }, option)))] }));
|
|
13
|
+
return (_jsxs("select", { className: "ns-select", "aria-label": strings.translate(field.key), value: value ?? "", onChange: (event) => onChange((event.target.value || null)), children: [(field.nullable || value === null || value === undefined) && (_jsx("option", { value: "", children: field.nullable ? strings.translate(none) : "" })), type.values.map((option) => (_jsx("option", { value: option, children: strings.enumValue(type, option) }, option)))] }));
|
|
14
14
|
}
|
package/dist/table.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface NsTableProps<T> {
|
|
|
9
9
|
actions?: (row: T) => ReactNode;
|
|
10
10
|
/** A string key: what an empty answer says. */
|
|
11
11
|
empty?: string;
|
|
12
|
+
/** Columns shown without digit grouping: a number that names something (a voucher), not a quantity. */
|
|
13
|
+
ungrouped?: readonly (keyof T & string)[];
|
|
12
14
|
}
|
|
13
15
|
/** Rows of a projection: each header is the member's key, each cell its value formatted by kind. */
|
|
14
|
-
export declare function NsTable<T>({ model, rows, columns, rowKey, onRowClick, actions, empty }: NsTableProps<T>): import("react").JSX.Element;
|
|
16
|
+
export declare function NsTable<T>({ model, rows, columns, rowKey, onRowClick, actions, empty, ungrouped }: NsTableProps<T>): import("react").JSX.Element;
|
package/dist/table.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { memberOf, useStrings } from "@nsail/stack";
|
|
3
3
|
import { display } from "./format";
|
|
4
4
|
/** Rows of a projection: each header is the member's key, each cell its value formatted by kind. */
|
|
5
|
-
export function NsTable({ model, rows, columns, rowKey, onRowClick, actions, empty = "Common.NoRecords" }) {
|
|
5
|
+
export function NsTable({ model, rows, columns, rowKey, onRowClick, actions, empty = "Common.NoRecords", ungrouped }) {
|
|
6
6
|
const strings = useStrings();
|
|
7
7
|
if (rows.length === 0) {
|
|
8
8
|
return _jsx("div", { className: "ns-empty", children: strings.translate(empty) });
|
|
@@ -10,8 +10,15 @@ export function NsTable({ model, rows, columns, rowKey, onRowClick, actions, emp
|
|
|
10
10
|
return (_jsx("div", { className: "ns-table-scroll", children: _jsxs("table", { className: "ns-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [columns.map((column) => {
|
|
11
11
|
const field = model.fields[column];
|
|
12
12
|
return (_jsx("th", { className: `ns-col-${field.kind}`, children: strings.translate(field.key, memberOf(field)) }, column));
|
|
13
|
-
}), actions && _jsx("th", { className: "ns-col-actions" })] }) }), _jsx("tbody", { children: rows.map((row) => (_jsxs("tr", { className: onRowClick ? "ns-clickable" : undefined, onClick: onRowClick ? () => onRowClick(row) : undefined,
|
|
13
|
+
}), actions && _jsx("th", { className: "ns-col-actions" })] }) }), _jsx("tbody", { children: rows.map((row) => (_jsxs("tr", { className: onRowClick ? "ns-clickable" : undefined, tabIndex: onRowClick ? 0 : undefined, onClick: onRowClick ? () => onRowClick(row) : undefined, onKeyDown: onRowClick
|
|
14
|
+
? (event) => {
|
|
15
|
+
if (event.target === event.currentTarget && (event.key === "Enter" || event.key === " ")) {
|
|
16
|
+
event.preventDefault();
|
|
17
|
+
onRowClick(row);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
: undefined, children: [columns.map((column) => {
|
|
14
21
|
const field = model.fields[column];
|
|
15
|
-
return (_jsx("td", { className: `ns-col-${field.kind}`, children: display(strings, field, row[column]) }, column));
|
|
22
|
+
return (_jsx("td", { className: `ns-col-${field.kind}`, children: display(strings, field, row[column], { grouped: !ungrouped?.includes(column) }) }, column));
|
|
16
23
|
}), actions && (_jsx("td", { className: "ns-col-actions", onClick: (event) => event.stopPropagation(), children: actions(row) }))] }, String(row[rowKey])))) })] }) }));
|
|
17
24
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nsail/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.984",
|
|
4
4
|
"description": "The Ns* components for a React client, drawn from the descriptors an Sdk generates.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"react-dom": ">=19"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@nsail/stack": "0.1.
|
|
38
|
+
"@nsail/stack": "0.1.984"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "tsc -p tsconfig.build.json && node -e \"require('fs').copyFileSync('src/ns.css', 'dist/ns.css')\"",
|