@sincpro/web-ui 1.6.0 → 1.7.1
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/components/attachments/Attachments.d.ts +56 -0
- package/dist/components/attachments/Attachments.js +93 -0
- package/dist/components/attachments/files.d.ts +28 -0
- package/dist/components/attachments/files.js +100 -0
- package/dist/components/attachments/index.d.ts +2 -0
- package/dist/components/attachments/index.js +3 -0
- package/dist/components/flow/StepFlow.d.ts +37 -0
- package/dist/components/flow/StepFlow.js +74 -0
- package/dist/components/flow/index.d.ts +1 -0
- package/dist/components/flow/index.js +2 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/components/lines/LineItems.js +55 -45
- package/dist/components/listing/SortMenu.d.ts +24 -20
- package/dist/components/listing/SortMenu.js +51 -91
- package/dist/components/listing/index.d.ts +2 -1
- package/dist/components/listing/index.js +2 -1
- package/dist/components/listing/modelCells.d.ts +7 -0
- package/dist/components/listing/modelCells.js +56 -0
- package/dist/components/listing/modelColumns.d.ts +9 -0
- package/dist/components/listing/modelColumns.js +59 -14
- package/dist/components/search/index.d.ts +1 -1
- package/dist/components/search/index.js +1 -1
- package/dist/components/search/model.d.ts +1 -0
- package/dist/components/search/model.js +7 -0
- package/dist/components/search/query.js +2 -3
- package/dist/foundations/copy.d.ts +131 -0
- package/dist/foundations/copy.es.js +86 -0
- package/dist/foundations/copy.js +86 -0
- package/dist/foundations/form.d.ts +25 -0
- package/dist/foundations/form.js +27 -0
- package/dist/foundations/index.d.ts +1 -1
- package/dist/foundations/index.js +1 -1
- package/dist/primitives/button/Button.js +3 -1
- package/dist/primitives/choice/ComboBox.js +2 -2
- package/dist/primitives/choice/SearchField.js +2 -5
- package/dist/primitives/choice/SegmentedControl.js +2 -2
- package/dist/primitives/choice/Slider.js +2 -2
- package/dist/primitives/date/DatePicker.js +4 -4
- package/dist/primitives/date/DateTime.js +3 -3
- package/dist/primitives/field/CardNumberInput.d.ts +41 -0
- package/dist/primitives/field/CardNumberInput.js +117 -0
- package/dist/primitives/field/CodeInput.d.ts +18 -0
- package/dist/primitives/field/CodeInput.js +37 -0
- package/dist/primitives/field/ColorInput.d.ts +21 -0
- package/dist/primitives/field/ColorInput.js +42 -0
- package/dist/primitives/field/ContactInputs.d.ts +18 -0
- package/dist/primitives/field/ContactInputs.js +70 -0
- package/dist/primitives/field/DurationInput.d.ts +20 -0
- package/dist/primitives/field/DurationInput.js +54 -0
- package/dist/primitives/field/Field.d.ts +26 -5
- package/dist/primitives/field/Field.js +19 -6
- package/dist/primitives/field/ImageInput.d.ts +51 -0
- package/dist/primitives/field/ImageInput.js +267 -0
- package/dist/primitives/field/PasswordInput.d.ts +29 -0
- package/dist/primitives/field/PasswordInput.js +44 -0
- package/dist/primitives/field/PatternInput.d.ts +23 -0
- package/dist/primitives/field/PatternInput.js +48 -0
- package/dist/primitives/field/RichTextEditor.d.ts +18 -0
- package/dist/primitives/field/RichTextEditor.js +224 -0
- package/dist/primitives/field/RichTextInput.d.ts +30 -0
- package/dist/primitives/field/RichTextInput.js +38 -0
- package/dist/primitives/field/SignatureInput.d.ts +34 -0
- package/dist/primitives/field/SignatureInput.js +215 -0
- package/dist/primitives/field/Suggest.js +2 -2
- package/dist/primitives/field/card.d.ts +33 -0
- package/dist/primitives/field/card.js +173 -0
- package/dist/primitives/field/clipboard.js +3 -2
- package/dist/primitives/field/contact.d.ts +17 -0
- package/dist/primitives/field/contact.js +65 -0
- package/dist/primitives/field/duration.d.ts +5 -0
- package/dist/primitives/field/duration.js +48 -0
- package/dist/primitives/field/index.d.ts +14 -0
- package/dist/primitives/field/index.js +20 -0
- package/dist/primitives/field/mask.d.ts +33 -0
- package/dist/primitives/field/mask.js +56 -0
- package/dist/primitives/field/media.d.ts +99 -0
- package/dist/primitives/field/media.js +136 -0
- package/dist/primitives/field/pattern.d.ts +10 -0
- package/dist/primitives/field/pattern.js +68 -0
- package/dist/primitives/menu/Menu.d.ts +16 -0
- package/dist/primitives/menu/Menu.js +26 -8
- package/dist/primitives/menu/index.d.ts +1 -1
- package/dist/primitives/menu/index.js +1 -1
- package/dist/primitives/picker/Picker.js +2 -2
- package/dist/primitives/picker/TagsField.js +2 -2
- package/dist/primitives/table/DataTable.d.ts +6 -4
- package/dist/primitives/table/DataTable.js +12 -8
- package/dist/primitives/table/EditableTable.js +102 -99
- package/dist/primitives/table/Table.d.ts +5 -8
- package/dist/primitives/table/Table.js +63 -15
- package/dist/primitives/table/index.d.ts +2 -1
- package/dist/primitives/table/index.js +2 -1
- package/dist/primitives/table/sorting.d.ts +42 -0
- package/dist/primitives/table/sorting.js +110 -0
- package/dist/primitives/value/RichText.d.ts +10 -0
- package/dist/primitives/value/RichText.js +130 -0
- package/dist/primitives/value/Value.d.ts +20 -2
- package/dist/primitives/value/Value.js +26 -7
- package/dist/primitives/value/index.d.ts +3 -1
- package/dist/primitives/value/index.js +4 -1
- package/dist/primitives/value/rounding.d.ts +9 -0
- package/dist/primitives/value/rounding.js +48 -0
- package/dist/tokens.css +103 -0
- package/dist/views/list/List.d.ts +5 -4
- package/dist/views/list/ListView.js +2 -2
- package/dist/views/list/types.d.ts +6 -6
- package/package.json +9 -1
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
import type { Surface } from "../../foundations/types.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type SortState } from "../../primitives/table/sorting.js";
|
|
3
3
|
/**
|
|
4
|
-
* ORDENAR
|
|
4
|
+
* ORDENAR — por uno o por varios campos, desde la barra.
|
|
5
5
|
*
|
|
6
6
|
* Context: ordenar era una capacidad de `DataTable` y de nadie más. Se hacía
|
|
7
7
|
* clic en el encabezado de la columna, que es la forma correcta **cuando hay
|
|
8
8
|
* encabezados**. En filas, en tablero y en la planilla no los hay, así que
|
|
9
9
|
* cambiar de presentación no perdía sólo el orden puesto: perdía la posibilidad
|
|
10
|
-
* de ordenar.
|
|
10
|
+
* de ordenar.
|
|
11
11
|
*
|
|
12
12
|
* El orden es del CONJUNTO y no de la presentación, igual que los niveles y que
|
|
13
13
|
* lo plegado. Por eso el control vive en la barra y no adentro de la tabla, y
|
|
14
14
|
* por eso la tabla sigue teniendo además sus encabezados: son dos puertas a lo
|
|
15
15
|
* mismo, y la de los encabezados es más rápida cuando existe.
|
|
16
16
|
*
|
|
17
|
-
* ──
|
|
17
|
+
* ── Varios criterios, y por eso un panel y no un menú ───────────────
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* Se decía que ordenar por dos campos era «un desempate que hace el motor» y
|
|
20
|
+
* que alcanzaba con elegir uno. La gente pidió lo contrario en la pantalla: por
|
|
21
|
+
* fecha, y en la misma fecha por cliente, y después por producto. El motor ya
|
|
22
|
+
* lo aceptaba (`order: [...]`); faltaba poder decirlo. Un orden de N campos
|
|
23
|
+
* tiene ORDEN entre ellos —el segundo sólo decide entre las filas que el
|
|
24
|
+
* primero dejó iguales—, y eso un menú de tildes no lo expresa: hace falta una
|
|
25
|
+
* lista que se reordena. Es la de React Aria (`GridList` + `useDragAndDrop`):
|
|
26
|
+
* se arrastra con el mouse, y con el teclado desde el asa (Enter, flechas,
|
|
27
|
+
* Enter), anunciado.
|
|
23
28
|
*/
|
|
24
29
|
export type SortField<T = unknown> = {
|
|
25
30
|
field: string;
|
|
@@ -33,7 +38,7 @@ export type SortField<T = unknown> = {
|
|
|
33
38
|
* falso— así que ahí el orden viaja en la lectura y esto sólo lo refleja.
|
|
34
39
|
*/
|
|
35
40
|
value?: (item: T) => string | number | null | undefined;
|
|
36
|
-
/** Qué quiere decir «ascendente» para este campo. Sin esto, «
|
|
41
|
+
/** Qué quiere decir «ascendente» para este campo. Sin esto, «ascendente».
|
|
37
42
|
* Una fecha asciende de vieja a nueva y un importe de chico a grande, y
|
|
38
43
|
* decirlo evita el clic de prueba. */
|
|
39
44
|
ascLabel?: string;
|
|
@@ -42,21 +47,20 @@ export type SortField<T = unknown> = {
|
|
|
42
47
|
export type SortMenuProps = {
|
|
43
48
|
/** Por qué campos se puede ordenar. Sin ninguno, el control no se dibuja. */
|
|
44
49
|
fields: readonly SortField<never>[];
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
/** El orden, de afuera hacia adentro. Vacío es «como llegaron». */
|
|
51
|
+
sort: readonly SortState[];
|
|
52
|
+
onSortChange: (next: SortState[]) => void;
|
|
47
53
|
surface?: Surface;
|
|
48
54
|
};
|
|
49
55
|
/**
|
|
50
|
-
* Las filas, ordenadas ac
|
|
56
|
+
* Las filas, ordenadas acá, por cada criterio en su orden.
|
|
51
57
|
*
|
|
52
|
-
* Sólo
|
|
53
|
-
*
|
|
54
|
-
*
|
|
58
|
+
* Sólo por los campos que declaran `value`; los demás se saltean —ordenar una
|
|
59
|
+
* página de un conjunto que se pagina afuera produce un orden que es mentira,
|
|
60
|
+
* porque ordena las doce que llegaron y no las ciento ochenta y siete. Sin
|
|
61
|
+
* ningún criterio legible, se devuelven tal cual.
|
|
55
62
|
*
|
|
56
|
-
* Los faltantes van SIEMPRE al final, suba o baje el orden
|
|
57
|
-
* cualquier planilla, y la razón es que un vacío no es «el más chico»: es la
|
|
58
|
-
* ausencia de respuesta, y ponerlo primero al invertir el orden llena la primera
|
|
59
|
-
* pantalla con las filas que menos dicen.
|
|
63
|
+
* Los faltantes van SIEMPRE al final, suba o baje el orden (ver `compareBy`).
|
|
60
64
|
*/
|
|
61
|
-
export declare function sortItems<T>(items: readonly T[], fields: readonly SortField<T>[], sort: SortState |
|
|
65
|
+
export declare function sortItems<T>(items: readonly T[], fields: readonly SortField<T>[], sort: readonly SortState[] | undefined): readonly T[];
|
|
62
66
|
export declare function SortMenu({ fields, sort, onSortChange, surface }: SortMenuProps): import("react").JSX.Element | null;
|
|
@@ -1,105 +1,65 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
|
-
import { Menu } from "../../primitives/menu/Menu.js";
|
|
5
|
-
import {
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { Menu, menuTriggerClasses } from "../../primitives/menu/Menu.js";
|
|
5
|
+
import { Popover } from "../../primitives/overlay/Popover.js";
|
|
6
|
+
import { compareBy, flipSort, moveSort, } from "../../primitives/table/sorting.js";
|
|
7
|
+
import { ArrowDownUp, ArrowDownWideNarrow, ArrowUpNarrowWide, ChevronDown, GripVertical, Plus, X, } from "lucide-react";
|
|
8
|
+
import { Button as AriaButton, Dialog, DialogTrigger, GridList, GridListItem, useDragAndDrop, } from "react-aria-components";
|
|
6
9
|
/**
|
|
7
|
-
* Las filas, ordenadas ac
|
|
10
|
+
* Las filas, ordenadas acá, por cada criterio en su orden.
|
|
8
11
|
*
|
|
9
|
-
* Sólo
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
+
* Sólo por los campos que declaran `value`; los demás se saltean —ordenar una
|
|
13
|
+
* página de un conjunto que se pagina afuera produce un orden que es mentira,
|
|
14
|
+
* porque ordena las doce que llegaron y no las ciento ochenta y siete. Sin
|
|
15
|
+
* ningún criterio legible, se devuelven tal cual.
|
|
12
16
|
*
|
|
13
|
-
* Los faltantes van SIEMPRE al final, suba o baje el orden
|
|
14
|
-
* cualquier planilla, y la razón es que un vacío no es «el más chico»: es la
|
|
15
|
-
* ausencia de respuesta, y ponerlo primero al invertir el orden llena la primera
|
|
16
|
-
* pantalla con las filas que menos dicen.
|
|
17
|
+
* Los faltantes van SIEMPRE al final, suba o baje el orden (ver `compareBy`).
|
|
17
18
|
*/
|
|
18
19
|
export function sortItems(items, fields, sort) {
|
|
19
|
-
if (sort ===
|
|
20
|
+
if (sort === undefined || sort.length === 0)
|
|
20
21
|
return items;
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
22
|
+
const read = (key) => fields.find((one) => one.field === key)?.value;
|
|
23
|
+
if (!sort.some((one) => read(one.key) !== undefined))
|
|
23
24
|
return items;
|
|
24
|
-
|
|
25
|
-
const sign = sort.direction === "asc" ? 1 : -1;
|
|
26
|
-
return [...items].sort((a, b) => {
|
|
27
|
-
const left = read(a);
|
|
28
|
-
const right = read(b);
|
|
29
|
-
const leftMissing = left === null || left === undefined || left === "";
|
|
30
|
-
const rightMissing = right === null || right === undefined || right === "";
|
|
31
|
-
if (leftMissing && rightMissing)
|
|
32
|
-
return 0;
|
|
33
|
-
if (leftMissing)
|
|
34
|
-
return 1;
|
|
35
|
-
if (rightMissing)
|
|
36
|
-
return -1;
|
|
37
|
-
if (typeof left === "number" && typeof right === "number")
|
|
38
|
-
return (left - right) * sign;
|
|
39
|
-
return String(left).localeCompare(String(right)) * sign;
|
|
40
|
-
});
|
|
25
|
+
return [...items].sort(compareBy(sort, read));
|
|
41
26
|
}
|
|
42
27
|
export function SortMenu({ fields, sort, onSortChange, surface }) {
|
|
43
28
|
const copy = useUiCopy();
|
|
29
|
+
const fieldOf = (key) => fields.find((one) => one.field === key);
|
|
30
|
+
const labelOf = (key) => fieldOf(key)?.label ?? key;
|
|
31
|
+
const { dragAndDropHooks } = useDragAndDrop({
|
|
32
|
+
getItems: (keys) => [...keys].map((key) => ({ "text/plain": labelOf(String(key)) })),
|
|
33
|
+
onReorder: (event) => onSortChange(moveSort(sort, new Set([...event.keys].map(String)), String(event.target.key), event.target.dropPosition === "before" ? "before" : "after")),
|
|
34
|
+
});
|
|
44
35
|
if (fields.length === 0)
|
|
45
36
|
return null;
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
},
|
|
77
|
-
{ options: [{ id: "__none__", label: copy.sorting.clear }] },
|
|
78
|
-
]),
|
|
79
|
-
];
|
|
80
|
-
return (_jsx(Menu, { label: current === undefined ? copy.sorting.label : current.label, icon: sort === null || sort === undefined
|
|
81
|
-
? ArrowDownUp
|
|
82
|
-
: sort.direction === "asc"
|
|
83
|
-
? ArrowUpNarrowWide
|
|
84
|
-
: ArrowDownWideNarrow, groups: groups, active: sort !== null && sort !== undefined, size: "sm", width: "sm", closeOnSelect: false, ...(surface === undefined ? {} : { surface }), onSelect: (id) => {
|
|
85
|
-
if (id === "__none__")
|
|
86
|
-
return onSortChange(null);
|
|
87
|
-
if (id === "__asc__") {
|
|
88
|
-
return onSortChange(sort === null ? null : { ...sort, direction: "asc" });
|
|
89
|
-
}
|
|
90
|
-
if (id === "__desc__") {
|
|
91
|
-
return onSortChange(sort === null ? null : { ...sort, direction: "desc" });
|
|
92
|
-
}
|
|
93
|
-
/* Volver a elegir el campo que ya está puesto DA VUELTA la dirección, que
|
|
94
|
-
es lo que hace el clic en un encabezado. Sin eso, invertir el orden son
|
|
95
|
-
dos gestos en un menú y uno en la tabla — la misma capacidad costando
|
|
96
|
-
distinto según la presentación. */
|
|
97
|
-
if (sort?.key === id) {
|
|
98
|
-
return onSortChange({
|
|
99
|
-
key: id,
|
|
100
|
-
direction: sort.direction === "asc" ? "desc" : "asc",
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
onSortChange({ key: id, direction: "asc" });
|
|
104
|
-
} }));
|
|
37
|
+
const first = sort[0];
|
|
38
|
+
const label = first === undefined
|
|
39
|
+
? copy.sorting.label
|
|
40
|
+
: sort.length === 1
|
|
41
|
+
? labelOf(first.key)
|
|
42
|
+
: fillCopy(copy.sorting.more, { label: labelOf(first.key), count: sort.length - 1 });
|
|
43
|
+
const TriggerIcon = first === undefined
|
|
44
|
+
? ArrowDownUp
|
|
45
|
+
: first.direction === "asc"
|
|
46
|
+
? ArrowUpNarrowWide
|
|
47
|
+
: ArrowDownWideNarrow;
|
|
48
|
+
const left = fields.filter((one) => !sort.some((s) => s.key === one.field));
|
|
49
|
+
return (_jsxs(DialogTrigger, { children: [_jsxs(AriaButton, { className: ({ isPressed, isFocusVisible }) => menuTriggerClasses({
|
|
50
|
+
...(surface === undefined ? {} : { surface }),
|
|
51
|
+
active: sort.length > 0,
|
|
52
|
+
open: isPressed,
|
|
53
|
+
focusVisible: isFocusVisible,
|
|
54
|
+
}), children: [_jsx(TriggerIcon, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true }), label, _jsx(ChevronDown, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true })] }), _jsx(Popover, { placement: "bottom start", className: "w-80", children: _jsxs(Dialog, { "aria-label": copy.sorting.label, className: "flex flex-col gap-1 p-2 outline-0", children: [sort.length === 0 ? (_jsx("p", { className: "px-1 py-1 text-meta text-muted", children: copy.sorting.none })) : (_jsx(GridList, { "aria-label": copy.sorting.label, items: sort.map((one, index) => ({ ...one, id: one.key, index })), dragAndDropHooks: dragAndDropHooks, className: "flex flex-col outline-0", children: (item) => {
|
|
55
|
+
const field = fieldOf(item.key);
|
|
56
|
+
const name = labelOf(item.key);
|
|
57
|
+
const direction = item.direction === "asc"
|
|
58
|
+
? (field?.ascLabel ?? copy.sorting.asc)
|
|
59
|
+
: (field?.descLabel ?? copy.sorting.desc);
|
|
60
|
+
const DirectionIcon = item.direction === "asc" ? ArrowUpNarrowWide : ArrowDownWideNarrow;
|
|
61
|
+
return (_jsxs(GridListItem, { id: item.id, textValue: `${item.index + 1}. ${name}, ${direction}`, className: "flex min-w-0 items-center gap-1.5 rounded-control px-1 py-1 outline-0 transition-colors duration-(--duration-instant) data-dragging:opacity-50 data-drop-target:bg-primary/8 data-focus-visible:outline-2 data-focus-visible:-outline-offset-2 data-focus-visible:outline-primary data-hovered:bg-ink/4", children: [_jsx(AriaButton, { slot: "drag", "aria-label": fillCopy(copy.sorting.move, { field: name }), className: "inline-flex shrink-0 cursor-grab items-center rounded-mark p-0.5 text-muted outline-0 hover:text-ink data-focus-visible:outline-2 data-focus-visible:outline-primary", children: _jsx(GripVertical, { className: "size-(--size-icon-sm)", strokeWidth: 2 }) }), _jsx("span", { className: "w-16 shrink-0 text-micro text-muted", children: item.index === 0 ? copy.sorting.by : copy.sorting.then }), _jsx("span", { className: "min-w-0 flex-1 truncate text-meta text-ink", children: name }), _jsxs(AriaButton, { "aria-label": `${fillCopy(copy.sorting.flip, { field: name })}: ${direction}`, onPress: () => onSortChange(flipSort(sort, item.index)), className: "inline-flex shrink-0 items-center gap-1 rounded-mark px-1 py-0.5 text-micro text-primary outline-0 hover:bg-primary/8 data-focus-visible:outline-2 data-focus-visible:outline-primary", children: [_jsx(DirectionIcon, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true }), direction] }), _jsx(AriaButton, { "aria-label": fillCopy(copy.sorting.remove, { field: name }), onPress: () => onSortChange(sort.filter((_, i) => i !== item.index)), className: "inline-flex shrink-0 items-center rounded-mark p-0.5 text-muted outline-0 hover:bg-ink/6 hover:text-ink data-focus-visible:outline-2 data-focus-visible:outline-primary", children: _jsx(X, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true }) })] }));
|
|
62
|
+
} })), _jsxs("div", { className: "flex items-center justify-between gap-2 border-t border-line pt-2", children: [left.length === 0 ? (_jsx("span", {})) : (_jsx(Menu, { label: copy.sorting.add, icon: Plus, variant: "text", chevron: false, width: "sm", groups: [
|
|
63
|
+
{ options: left.map((one) => ({ id: one.field, label: one.label })) },
|
|
64
|
+
], onSelect: (id) => onSortChange([...sort, { key: id, direction: "asc" }]) })), sort.length === 0 ? null : (_jsx(AriaButton, { onPress: () => onSortChange([]), className: "rounded-mark px-1.5 py-0.5 text-micro text-muted outline-0 hover:bg-ink/6 hover:text-ink data-focus-visible:outline-2 data-focus-visible:outline-primary", children: copy.sorting.clear }))] })] }) })] }));
|
|
105
65
|
}
|
|
@@ -2,7 +2,8 @@ export { type ColumnControl, type ColumnSpec, defaultVisibleIds, reconcileVisibl
|
|
|
2
2
|
export { ColumnsMenu, specsFromColumns, } from "./ColumnsMenu.js";
|
|
3
3
|
export { Listing } from "./Listing.js";
|
|
4
4
|
export { type ListIdentity, ListingBar } from "./ListingBar.js";
|
|
5
|
-
export { type
|
|
5
|
+
export { type ModelCellKind } from "./modelCells.js";
|
|
6
|
+
export { type ClaimedField, claimedFields, columnsFromModel, modelCellKind, readModelValue, } from "./modelColumns.js";
|
|
6
7
|
export { type CursorSource, DEFAULT_PAGE_SIZE, PAGE_SIZES, type Pages, type PaginationBridge, type ServerPage, useCursorPagination, usePagination, } from "./pages.js";
|
|
7
8
|
export { Pagination } from "./Pagination.js";
|
|
8
9
|
export { RecordLink } from "./RecordLink.js";
|
|
@@ -4,7 +4,8 @@ export { defaultVisibleIds, reconcileVisible, toggleColumn, useColumnControl, vi
|
|
|
4
4
|
export { ColumnsMenu, specsFromColumns, } from "./ColumnsMenu.js";
|
|
5
5
|
export { Listing } from "./Listing.js";
|
|
6
6
|
export { ListingBar } from "./ListingBar.js";
|
|
7
|
-
export {
|
|
7
|
+
export {} from "./modelCells.js";
|
|
8
|
+
export { claimedFields, columnsFromModel, modelCellKind, readModelValue, } from "./modelColumns.js";
|
|
8
9
|
export { DEFAULT_PAGE_SIZE, PAGE_SIZES, useCursorPagination, usePagination, } from "./pages.js";
|
|
9
10
|
export { Pagination } from "./Pagination.js";
|
|
10
11
|
export { RecordLink } from "./RecordLink.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** How a model field is drawn. `text` is today's behaviour, for any type not known here. */
|
|
2
|
+
export type ModelCellKind = "text" | "number" | "email" | "url" | "phone" | "duration" | "boolean" | "date" | "datetime";
|
|
3
|
+
/** One cell of a model column. `value` is what `readModelValue` read — never `null` here. */
|
|
4
|
+
export declare function ModelCell({ kind, value, }: {
|
|
5
|
+
kind: ModelCellKind;
|
|
6
|
+
value: string | number | boolean;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { emailHref, telHref, urlHref, urlLabel, } from "../../primitives/field/contact.js";
|
|
5
|
+
import { formatDuration } from "../../primitives/field/duration.js";
|
|
6
|
+
import { DateValue } from "../../primitives/value/DateValue.js";
|
|
7
|
+
import { Numeric } from "../../primitives/value/Value.js";
|
|
8
|
+
import { Check } from "lucide-react";
|
|
9
|
+
const LINK = "block truncate rounded-mark text-primary underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary";
|
|
10
|
+
function Plain({ text }) {
|
|
11
|
+
return (_jsx("span", { className: "block truncate text-muted", title: text, children: text }));
|
|
12
|
+
}
|
|
13
|
+
/** A link in a cell, or the plain text when the value does not make one. */
|
|
14
|
+
function Link({ text, href, label, external = false, }) {
|
|
15
|
+
const copy = useUiCopy();
|
|
16
|
+
if (href === null)
|
|
17
|
+
return _jsx(Plain, { text: text });
|
|
18
|
+
return (_jsxs("a", { href: href, title: text, className: LINK,
|
|
19
|
+
/* The row opens its record on a click; writing to its email must not ALSO open it. */
|
|
20
|
+
onClick: (event) => event.stopPropagation(), ...(external ? { target: "_blank", rel: "noopener noreferrer" } : {}), children: [label ?? text, external ? _jsxs("span", { className: "sr-only", children: [" (", copy.field.newTab, ")"] }) : null] }));
|
|
21
|
+
}
|
|
22
|
+
/** A yes/no in words: a check alone is not read by everyone the same way, and «no» needs
|
|
23
|
+
* no icon — an empty mark next to every false row would only add noise to the column. */
|
|
24
|
+
function YesNo({ value }) {
|
|
25
|
+
const copy = useUiCopy();
|
|
26
|
+
if (!value)
|
|
27
|
+
return _jsx("span", { className: "text-muted", children: copy.table.no });
|
|
28
|
+
return (_jsxs("span", { className: "inline-flex items-center gap-1 text-ink", children: [_jsx(Check, { "aria-hidden": true, className: "size-(--size-icon-sm) text-good", strokeWidth: 2.5 }), copy.table.yes] }));
|
|
29
|
+
}
|
|
30
|
+
/** One cell of a model column. `value` is what `readModelValue` read — never `null` here. */
|
|
31
|
+
export function ModelCell({ kind, value, }) {
|
|
32
|
+
const text = String(value);
|
|
33
|
+
switch (kind) {
|
|
34
|
+
case "number":
|
|
35
|
+
/* A figure in the locale's digits and right aligned, so a column of amounts lines up
|
|
36
|
+
— never «1234.5» as text. */
|
|
37
|
+
return typeof value === "number" ? (_jsx(Numeric, { value: value, format: "decimal" })) : (_jsx(Plain, { text: text }));
|
|
38
|
+
case "email":
|
|
39
|
+
return _jsx(Link, { text: text, href: emailHref(text) });
|
|
40
|
+
case "phone":
|
|
41
|
+
return _jsx(Link, { text: text, href: telHref(text) });
|
|
42
|
+
case "url":
|
|
43
|
+
return _jsx(Link, { text: text, href: urlHref(text), label: urlLabel(text), external: true });
|
|
44
|
+
case "duration":
|
|
45
|
+
return typeof value === "number" ? (
|
|
46
|
+
/* Right aligned like `Numeric`: a column of hours lines up by the colon. */
|
|
47
|
+
_jsx("span", { className: "block text-right text-body tabular-nums whitespace-nowrap text-ink", children: formatDuration(value) })) : (_jsx(Plain, { text: text }));
|
|
48
|
+
case "boolean":
|
|
49
|
+
return typeof value === "boolean" ? _jsx(YesNo, { value: value }) : _jsx(Plain, { text: text });
|
|
50
|
+
case "date":
|
|
51
|
+
case "datetime":
|
|
52
|
+
return typeof value === "boolean" ? (_jsx(Plain, { text: text })) : (_jsx(DateValue, { when: value, format: kind === "date" ? "date" : "relative" }));
|
|
53
|
+
default:
|
|
54
|
+
return _jsx(Plain, { text: text });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ModelCellKind } from "./modelCells.js";
|
|
1
2
|
import { type ModelDefinition } from "../search/model.js";
|
|
2
3
|
import type { DataColumn } from "../../primitives/table/DataTable.js";
|
|
3
4
|
/** Qué campo del modelo muestra una columna declarada a mano.
|
|
@@ -28,6 +29,14 @@ export declare function claimedFields(columns: readonly ClaimedField[]): Set<str
|
|
|
28
29
|
* Las fechas se dejan como vienen —ISO— porque en ISO el orden alfabético ES
|
|
29
30
|
* el cronológico, y así la columna ordena bien sin parsear nada. */
|
|
30
31
|
export declare function readModelValue(raw: unknown): string | number | boolean | null;
|
|
32
|
+
/** How a field of the model is drawn, by its `type` and in any case — «Email» and `email` are
|
|
33
|
+
* the same field. A type not known here stays `text`, exactly as before.
|
|
34
|
+
*
|
|
35
|
+
* in "monetary" out "number"
|
|
36
|
+
* in "float_time" out "duration" «8:30», not «8.5»
|
|
37
|
+
* in "jsonb" out "text"
|
|
38
|
+
*/
|
|
39
|
+
export declare function modelCellKind(type: string): ModelCellKind;
|
|
31
40
|
/**
|
|
32
41
|
* Las columnas declaradas, más una por cada campo del modelo que ninguna
|
|
33
42
|
* contestaba.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { ModelCell } from "./modelCells.js";
|
|
3
|
+
import { fieldLabel, isNumericType, } from "../search/model.js";
|
|
4
|
+
import { formatDuration } from "../../primitives/field/duration.js";
|
|
3
5
|
/** Los campos que las columnas declaradas ya contestan.
|
|
4
6
|
*
|
|
5
7
|
* in [{id:"rows", field:"row_count"}, {id:"name"}]
|
|
@@ -38,14 +40,45 @@ export function readModelValue(raw) {
|
|
|
38
40
|
return raw === "" ? null : raw;
|
|
39
41
|
return JSON.stringify(raw);
|
|
40
42
|
}
|
|
43
|
+
/** The names an engine publishes for each kind it knows how to draw — Odoo's (`char` with a
|
|
44
|
+
* widget aside, `float_time`, `boolean`, `date`, `datetime`) and the plain ones a schema
|
|
45
|
+
* uses (`email`, `url`, `phone`, `duration`, `bool`, `timestamp`). */
|
|
46
|
+
const KIND_OF_TYPE = {
|
|
47
|
+
email: "email",
|
|
48
|
+
mail: "email",
|
|
49
|
+
url: "url",
|
|
50
|
+
uri: "url",
|
|
51
|
+
website: "url",
|
|
52
|
+
phone: "phone",
|
|
53
|
+
tel: "phone",
|
|
54
|
+
float_time: "duration",
|
|
55
|
+
duration: "duration",
|
|
56
|
+
boolean: "boolean",
|
|
57
|
+
bool: "boolean",
|
|
58
|
+
date: "date",
|
|
59
|
+
datetime: "datetime",
|
|
60
|
+
timestamp: "datetime",
|
|
61
|
+
};
|
|
62
|
+
/** How a field of the model is drawn, by its `type` and in any case — «Email» and `email` are
|
|
63
|
+
* the same field. A type not known here stays `text`, exactly as before.
|
|
64
|
+
*
|
|
65
|
+
* in "monetary" out "number"
|
|
66
|
+
* in "float_time" out "duration" «8:30», not «8.5»
|
|
67
|
+
* in "jsonb" out "text"
|
|
68
|
+
*/
|
|
69
|
+
export function modelCellKind(type) {
|
|
70
|
+
if (isNumericType(type))
|
|
71
|
+
return "number";
|
|
72
|
+
return KIND_OF_TYPE[type.toLowerCase()] ?? "text";
|
|
73
|
+
}
|
|
41
74
|
/** Cuánto ancho merece un campo, por su tipo. Una huella son 64 caracteres y
|
|
42
75
|
* una cuenta son seis: darles el mismo ancho arruina las dos. */
|
|
43
|
-
function widthFor(
|
|
44
|
-
if (
|
|
76
|
+
function widthFor(kind) {
|
|
77
|
+
if (kind === "number" || kind === "duration")
|
|
45
78
|
return 120;
|
|
46
|
-
if (
|
|
79
|
+
if (kind === "datetime" || kind === "date")
|
|
47
80
|
return 190;
|
|
48
|
-
if (
|
|
81
|
+
if (kind === "boolean")
|
|
49
82
|
return 90;
|
|
50
83
|
return 260;
|
|
51
84
|
}
|
|
@@ -76,27 +109,39 @@ export function columnsFromModel(declared, model, locale = "default") {
|
|
|
76
109
|
for (const [field, definition] of Object.entries(model.fields)) {
|
|
77
110
|
if (claimed.has(field))
|
|
78
111
|
continue;
|
|
79
|
-
const
|
|
112
|
+
const kind = modelCellKind(definition.type);
|
|
113
|
+
/* A duration is a number of hours: it sorts, sums and aligns as one. */
|
|
114
|
+
const numeric = kind === "number" || kind === "duration";
|
|
115
|
+
const read = (row) => readModelValue(row[field]);
|
|
80
116
|
extra.push({
|
|
81
117
|
id: field,
|
|
82
118
|
field,
|
|
83
119
|
header: fieldLabel(model, field, locale),
|
|
84
|
-
value:
|
|
85
|
-
/*
|
|
86
|
-
|
|
87
|
-
por encima y leerla. */
|
|
120
|
+
value: read,
|
|
121
|
+
/* Drawn by what the model says the field IS (`modelCells.tsx`); a plain text keeps
|
|
122
|
+
its whole value in the `title` — a 64 character fingerprint fits no cell. */
|
|
88
123
|
cell: (row) => {
|
|
89
|
-
const value =
|
|
124
|
+
const value = read(row);
|
|
90
125
|
if (value === null)
|
|
91
126
|
return _jsx("span", { className: "text-subtle", children: "\u2014" });
|
|
92
|
-
|
|
93
|
-
return (_jsx("span", { className: "block truncate text-muted", title: text, children: text }));
|
|
127
|
+
return _jsx(ModelCell, { kind: kind, value: value });
|
|
94
128
|
},
|
|
129
|
+
/* Where a node cannot be drawn — the sheet, the footer's total — a duration still
|
|
130
|
+
reads «42:30» and never «42.5». */
|
|
131
|
+
...(kind === "duration"
|
|
132
|
+
? {
|
|
133
|
+
format: formatDuration,
|
|
134
|
+
text: (row) => {
|
|
135
|
+
const value = read(row);
|
|
136
|
+
return typeof value === "number" ? formatDuration(value) : String(value ?? "");
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
: {}),
|
|
95
140
|
...(numeric ? { numeric: true } : {}),
|
|
96
141
|
sortable: definition.sortable,
|
|
97
142
|
optional: true,
|
|
98
143
|
defaultHidden: true,
|
|
99
|
-
width: widthFor(
|
|
144
|
+
width: widthFor(kind),
|
|
100
145
|
});
|
|
101
146
|
}
|
|
102
147
|
return [...declared, ...extra];
|
|
@@ -5,6 +5,6 @@ export { formatJson, JsonEditor } from "./JsonEditor.js";
|
|
|
5
5
|
export { SearchBar, type SearchBarProps, type SearchFacet, } from "./SearchBar.js";
|
|
6
6
|
export { at, criteriaId, type CriteriaJoin, type CriteriaPath, filterCriteria, filtersOf, type GroupCriteria, groupCriteria, groupsOf, hasNamed, levelsOf, nextJoin, removeAt, replaceAt, type SearchCriteria, toggleNamed, whereOf, withLevels, } from "./criteria.js";
|
|
7
7
|
export { CriteriaChips, type CriteriaChipsProps, } from "./CriteriaChips.js";
|
|
8
|
-
export { type FieldDefinition, fieldLabel, filterableNames, isFilterable, type ModelDefinition, type ModelTranslations, } from "./model.js";
|
|
8
|
+
export { type FieldDefinition, fieldLabel, filterableNames, isFilterable, isNumericType, type ModelDefinition, type ModelTranslations, } from "./model.js";
|
|
9
9
|
export { answers, defaultOp, describeCondition, globToLike, opLabel, type QueryDraft, type QueryField, queryFields, type QueryKind, type QuerySuggestion, readTyped, suggest, type TypedValue, valueFor, type ValueShape, withoutValue, } from "./query.js";
|
|
10
10
|
export { type DatePeriod, datePeriodGroup, datePeriodOptions, type DateRange, periodId, readPeriodId, recentMonths, recentYears, resolvePeriods, } from "./datePeriods.js";
|
|
@@ -12,7 +12,7 @@ export { CriteriaChips, } from "./CriteriaChips.js";
|
|
|
12
12
|
/* EL VOCABULARIO DEL MODELO — lo que el servidor publica sobre sus campos. Lo
|
|
13
13
|
hablan el constructor de filtros, el de criterias y la barra de búsqueda, así
|
|
14
14
|
que vive aparte de los tres. */
|
|
15
|
-
export { fieldLabel, filterableNames, isFilterable, } from "./model.js";
|
|
15
|
+
export { fieldLabel, filterableNames, isFilterable, isNumericType, } from "./model.js";
|
|
16
16
|
/* LA GRAMÁTICA DE LA BARRA: qué se puede preguntar sobre un modelo, y con qué.
|
|
17
17
|
Tres pasos —campo, operador, valor— que se pueden escribir en uno. */
|
|
18
18
|
export { answers, defaultOp, describeCondition, globToLike, opLabel, queryFields, readTyped, suggest, valueFor, withoutValue, } from "./query.js";
|
|
@@ -23,6 +23,7 @@ export interface FieldDefinition {
|
|
|
23
23
|
/** Los valores que un campo de enum puede tomar — de ahí sale un `<select>`. */
|
|
24
24
|
choices?: readonly unknown[];
|
|
25
25
|
}
|
|
26
|
+
export declare function isNumericType(type: string): boolean;
|
|
26
27
|
/** Si este campo se puede usar en un `where`, un `order` o un `grouping.by` directamente.
|
|
27
28
|
* `ops.length === 0` alcanza como señal: la publican tanto `embedded` como los tres
|
|
28
29
|
* relacionales, así que no hace falta mirar `kind` aparte. */
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
* para saber cómo se llama un operador. Un vocabulario compartido no puede
|
|
14
14
|
* colgar de uno de los que lo hablan.
|
|
15
15
|
* ==================================================================== */
|
|
16
|
+
/** The types an engine publishes for an AMOUNT — integer, float, decimal, money. A search
|
|
17
|
+
* compares them as numbers and a listing aligns and formats them as numbers: «monetary»
|
|
18
|
+
* read as text made «total <= 1000» impossible and drew «1234.5» flush left. */
|
|
19
|
+
const NUMERIC_TYPES = new Set(["integer", "number", "float", "decimal", "monetary", "money"]);
|
|
20
|
+
export function isNumericType(type) {
|
|
21
|
+
return NUMERIC_TYPES.has(type.toLowerCase());
|
|
22
|
+
}
|
|
16
23
|
/** Si este campo se puede usar en un `where`, un `order` o un `grouping.by` directamente.
|
|
17
24
|
* `ops.length === 0` alcanza como señal: la publican tanto `embedded` como los tres
|
|
18
25
|
* relacionales, así que no hace falta mirar `kind` aparte. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fieldLabel, isFilterable, } from "./model.js";
|
|
1
|
+
import { fieldLabel, isFilterable, isNumericType, } from "./model.js";
|
|
2
2
|
import { currentCopy, fillCopy } from "../../foundations/copy.js";
|
|
3
3
|
/**
|
|
4
4
|
* Las palabras que, después del numeral, abren un GRUPO en vez de nombrar un
|
|
@@ -97,7 +97,6 @@ export function readTyped(text) {
|
|
|
97
97
|
export function globToLike(value) {
|
|
98
98
|
return value.replace(/\*/g, "%");
|
|
99
99
|
}
|
|
100
|
-
const NUMERIC = new Set(["integer", "number", "float", "decimal", "monetary", "money"]);
|
|
101
100
|
/** Qué clase de valor es un campo, leída del tipo que publica el modelo. */
|
|
102
101
|
function kindOf(definition) {
|
|
103
102
|
if (definition.kind === "relation")
|
|
@@ -107,7 +106,7 @@ function kindOf(definition) {
|
|
|
107
106
|
const type = definition.type.toLowerCase();
|
|
108
107
|
/* An amount is a number: «decimal» and «monetary» read as text made «total <= 1000»
|
|
109
108
|
impossible to ask of an invoice. */
|
|
110
|
-
if (
|
|
109
|
+
if (isNumericType(type))
|
|
111
110
|
return "number";
|
|
112
111
|
if (type === "date" || type === "datetime")
|
|
113
112
|
return "date";
|