@sincpro/web-ui 1.5.0 → 1.7.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.
Files changed (101) hide show
  1. package/dist/components/attachments/Attachments.d.ts +56 -0
  2. package/dist/components/attachments/Attachments.js +93 -0
  3. package/dist/components/attachments/files.d.ts +28 -0
  4. package/dist/components/attachments/files.js +100 -0
  5. package/dist/components/attachments/index.d.ts +2 -0
  6. package/dist/components/attachments/index.js +3 -0
  7. package/dist/components/flow/StepFlow.d.ts +37 -0
  8. package/dist/components/flow/StepFlow.js +74 -0
  9. package/dist/components/flow/index.d.ts +1 -0
  10. package/dist/components/flow/index.js +2 -0
  11. package/dist/components/index.d.ts +2 -0
  12. package/dist/components/index.js +2 -0
  13. package/dist/components/lines/LineItems.d.ts +14 -3
  14. package/dist/components/lines/LineItems.js +6 -4
  15. package/dist/components/listing/SortMenu.d.ts +24 -20
  16. package/dist/components/listing/SortMenu.js +51 -91
  17. package/dist/components/listing/index.d.ts +2 -1
  18. package/dist/components/listing/index.js +2 -1
  19. package/dist/components/listing/modelCells.d.ts +7 -0
  20. package/dist/components/listing/modelCells.js +56 -0
  21. package/dist/components/listing/modelColumns.d.ts +9 -0
  22. package/dist/components/listing/modelColumns.js +59 -14
  23. package/dist/components/search/index.d.ts +1 -1
  24. package/dist/components/search/index.js +1 -1
  25. package/dist/components/search/model.d.ts +1 -0
  26. package/dist/components/search/model.js +7 -0
  27. package/dist/components/search/query.js +2 -3
  28. package/dist/foundations/copy.d.ts +131 -0
  29. package/dist/foundations/copy.es.js +86 -0
  30. package/dist/foundations/copy.js +86 -0
  31. package/dist/foundations/form.d.ts +18 -1
  32. package/dist/foundations/form.js +6 -3
  33. package/dist/primitives/button/Button.js +3 -1
  34. package/dist/primitives/field/CardNumberInput.d.ts +41 -0
  35. package/dist/primitives/field/CardNumberInput.js +117 -0
  36. package/dist/primitives/field/CodeInput.d.ts +18 -0
  37. package/dist/primitives/field/CodeInput.js +37 -0
  38. package/dist/primitives/field/ColorInput.d.ts +21 -0
  39. package/dist/primitives/field/ColorInput.js +42 -0
  40. package/dist/primitives/field/ContactInputs.d.ts +18 -0
  41. package/dist/primitives/field/ContactInputs.js +70 -0
  42. package/dist/primitives/field/DurationInput.d.ts +20 -0
  43. package/dist/primitives/field/DurationInput.js +54 -0
  44. package/dist/primitives/field/Field.d.ts +24 -4
  45. package/dist/primitives/field/Field.js +17 -4
  46. package/dist/primitives/field/ImageInput.d.ts +51 -0
  47. package/dist/primitives/field/ImageInput.js +267 -0
  48. package/dist/primitives/field/PasswordInput.d.ts +29 -0
  49. package/dist/primitives/field/PasswordInput.js +44 -0
  50. package/dist/primitives/field/PatternInput.d.ts +23 -0
  51. package/dist/primitives/field/PatternInput.js +48 -0
  52. package/dist/primitives/field/RichTextEditor.d.ts +18 -0
  53. package/dist/primitives/field/RichTextEditor.js +224 -0
  54. package/dist/primitives/field/RichTextInput.d.ts +30 -0
  55. package/dist/primitives/field/RichTextInput.js +38 -0
  56. package/dist/primitives/field/SignatureInput.d.ts +34 -0
  57. package/dist/primitives/field/SignatureInput.js +215 -0
  58. package/dist/primitives/field/card.d.ts +33 -0
  59. package/dist/primitives/field/card.js +173 -0
  60. package/dist/primitives/field/clipboard.js +3 -2
  61. package/dist/primitives/field/contact.d.ts +17 -0
  62. package/dist/primitives/field/contact.js +65 -0
  63. package/dist/primitives/field/duration.d.ts +5 -0
  64. package/dist/primitives/field/duration.js +48 -0
  65. package/dist/primitives/field/index.d.ts +14 -0
  66. package/dist/primitives/field/index.js +20 -0
  67. package/dist/primitives/field/mask.d.ts +33 -0
  68. package/dist/primitives/field/mask.js +56 -0
  69. package/dist/primitives/field/media.d.ts +99 -0
  70. package/dist/primitives/field/media.js +136 -0
  71. package/dist/primitives/field/pattern.d.ts +10 -0
  72. package/dist/primitives/field/pattern.js +68 -0
  73. package/dist/primitives/menu/Menu.d.ts +16 -0
  74. package/dist/primitives/menu/Menu.js +26 -8
  75. package/dist/primitives/menu/index.d.ts +1 -1
  76. package/dist/primitives/menu/index.js +1 -1
  77. package/dist/primitives/table/DataTable.d.ts +5 -3
  78. package/dist/primitives/table/DataTable.js +2 -4
  79. package/dist/primitives/table/EditableTable.d.ts +49 -7
  80. package/dist/primitives/table/EditableTable.js +321 -82
  81. package/dist/primitives/table/Table.d.ts +5 -8
  82. package/dist/primitives/table/Table.js +63 -15
  83. package/dist/primitives/table/index.d.ts +3 -2
  84. package/dist/primitives/table/index.js +2 -1
  85. package/dist/primitives/table/rowWindow.d.ts +3 -0
  86. package/dist/primitives/table/rowWindow.js +4 -0
  87. package/dist/primitives/table/sorting.d.ts +42 -0
  88. package/dist/primitives/table/sorting.js +110 -0
  89. package/dist/primitives/value/RichText.d.ts +10 -0
  90. package/dist/primitives/value/RichText.js +130 -0
  91. package/dist/primitives/value/Value.d.ts +20 -2
  92. package/dist/primitives/value/Value.js +26 -7
  93. package/dist/primitives/value/index.d.ts +3 -1
  94. package/dist/primitives/value/index.js +4 -1
  95. package/dist/primitives/value/rounding.d.ts +9 -0
  96. package/dist/primitives/value/rounding.js +48 -0
  97. package/dist/tokens.css +103 -0
  98. package/dist/views/list/List.d.ts +5 -4
  99. package/dist/views/list/ListView.js +2 -2
  100. package/dist/views/list/types.d.ts +6 -6
  101. package/package.json +9 -1
@@ -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 { ArrowDownUp, ArrowDownWideNarrow, ArrowUpNarrowWide } from "lucide-react";
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 cuando el campo activo declara `value`; si no, se devuelven tal cual —
10
- * ordenar una página de un conjunto que se pagina afuera produce un orden que es
11
- * mentira, porque ordena las doce que llegaron y no las ciento ochenta y siete.
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. Es lo que hace
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 === null || sort === undefined)
20
+ if (sort === undefined || sort.length === 0)
20
21
  return items;
21
- const field = fields.find((one) => one.field === sort.key);
22
- if (field?.value === undefined)
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
- const read = field.value;
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 current = fields.find((one) => one.field === sort?.key);
47
- const groups = [
48
- {
49
- label: copy.sorting.by,
50
- options: fields.map((one) => ({
51
- id: one.field,
52
- label: one.label,
53
- checked: one.field === sort?.key,
54
- })),
55
- },
56
- /* La dirección sólo cuando hay algo ordenado: «descendente» sin campo no
57
- ordena nada, y una opción que no hace nada es una opción que se prueba. */
58
- ...(current === undefined
59
- ? []
60
- : [
61
- {
62
- options: [
63
- {
64
- id: "__asc__",
65
- label: current.ascLabel ?? copy.sorting.asc,
66
- icon: ArrowUpNarrowWide,
67
- checked: sort?.direction === "asc",
68
- },
69
- {
70
- id: "__desc__",
71
- label: current.descLabel ?? copy.sorting.desc,
72
- icon: ArrowDownWideNarrow,
73
- checked: sort?.direction === "desc",
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 ClaimedField, claimedFields, columnsFromModel, readModelValue, } from "./modelColumns.js";
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 { claimedFields, columnsFromModel, readModelValue, } from "./modelColumns.js";
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 { fieldLabel } from "../search/model.js";
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(type) {
44
- if (type === "integer" || type === "number")
76
+ function widthFor(kind) {
77
+ if (kind === "number" || kind === "duration")
45
78
  return 120;
46
- if (type === "datetime" || type === "date")
79
+ if (kind === "datetime" || kind === "date")
47
80
  return 190;
48
- if (type === "boolean")
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 numeric = definition.type === "integer" || definition.type === "number";
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: (row) => readModelValue(row[field]),
85
- /* El valor entero en el `title`: una huella de 64 caracteres no entra en
86
- ninguna celda, y cortada no sirve para nada — salvo para pasar el mouse
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 = readModelValue(row[field]);
124
+ const value = read(row);
90
125
  if (value === null)
91
126
  return _jsx("span", { className: "text-subtle", children: "\u2014" });
92
- const text = String(value);
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(definition.type),
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 (NUMERIC.has(type))
109
+ if (isNumericType(type))
111
110
  return "number";
112
111
  if (type === "date" || type === "datetime")
113
112
  return "date";
@@ -122,6 +122,22 @@ export type UiCopy = {
122
122
  none: string;
123
123
  /** Next to the label of a field whose value changed and is not saved yet. */
124
124
  changed: string;
125
+ /** The eye button of a password: show it… */
126
+ showSecret: string;
127
+ /** …and hide it again. */
128
+ hideSecret: string;
129
+ /** Under a password while Caps Lock is on: the most common reason a right password fails. */
130
+ capsLock: string;
131
+ /** What a stored password says in read mode, for a screen reader: there is one, not which. */
132
+ secretSet: string;
133
+ /** Said after a link that leaves for another tab — a website, WhatsApp. */
134
+ newTab: string;
135
+ /** The unit after a duration: «8:30 h». */
136
+ hours: string;
137
+ /** The name of the unit (or currency) chooser inside a number field. */
138
+ unit: string;
139
+ /** The row of colours a colour field offers. */
140
+ palette: string;
125
141
  };
126
142
  /** The choice controls: combo boxes, selects, suggestion lists. */
127
143
  choice: {
@@ -343,6 +359,32 @@ export type UiCopy = {
343
359
  /** A field with nothing in it: said, not a silent success. */
344
360
  empty: string;
345
361
  };
362
+ /** The rich-text editor: its toolbar, its link form, its list of people to mention. */
363
+ richText: {
364
+ toolbar: string;
365
+ bold: string;
366
+ italic: string;
367
+ underline: string;
368
+ strike: string;
369
+ heading: string;
370
+ subheading: string;
371
+ bulletList: string;
372
+ orderedList: string;
373
+ quote: string;
374
+ link: string;
375
+ linkUrl: string;
376
+ applyLink: string;
377
+ removeLink: string;
378
+ /** Said when the address is not a website, an email or a phone: what to write instead. */
379
+ badLink: string;
380
+ table: string;
381
+ undo: string;
382
+ redo: string;
383
+ clear: string;
384
+ /** The list that opens on «@»: people to mention. */
385
+ mentions: string;
386
+ noMatches: string;
387
+ };
346
388
  /** Ordenar, cuando la presentación no tiene encabezados donde hacer clic. */
347
389
  sorting: {
348
390
  label: string;
@@ -350,6 +392,24 @@ export type UiCopy = {
350
392
  asc: string;
351
393
  desc: string;
352
394
  clear: string;
395
+ /** On a sortable header: how to add it as a tie-breaker instead of replacing the order. */
396
+ addHint: string;
397
+ /** What a tie-breaker header says of itself: «sort 2, descending». */
398
+ place: string;
399
+ /** Adds one more criterion at the end of the order. */
400
+ add: string;
401
+ /** Where the criteria are listed when there are none: the rows go as they came. */
402
+ none: string;
403
+ /** The direction button of a criterion, named by its field. */
404
+ flip: string;
405
+ /** Takes one criterion out, named by its field. */
406
+ remove: string;
407
+ /** The handle a criterion is dragged by, named by its field. */
408
+ move: string;
409
+ /** The trigger with more than one criterion: «Fecha +2». */
410
+ more: string;
411
+ /** The second and later criteria: they only decide between rows the first leaves equal. */
412
+ then: string;
353
413
  };
354
414
  /** Los recortes de fecha RELATIVOS. Las unidades van en singular y plural
355
415
  * porque «1 mes» y «3 meses» no se resuelven con una `s` en todos los
@@ -470,6 +530,26 @@ export type UiCopy = {
470
530
  finish: string;
471
531
  busy: string;
472
532
  };
533
+ /** A multi-step operation done in one sitting (`StepFlow`): where you are, what is left,
534
+ * and its own buttons. */
535
+ stepFlow: {
536
+ /** The accessible name of the strip of steps. */
537
+ steps: string;
538
+ /** Over the step's title: «Step 2 of 3». */
539
+ position: string;
540
+ /** Read by the live region on every move: `{position}` is the line above, `{title}`
541
+ * the step's. */
542
+ announce: string;
543
+ /** The state of a step on the strip, said to a screen reader — never only a colour. */
544
+ done: string;
545
+ pending: string;
546
+ cancel: string;
547
+ back: string;
548
+ next: string;
549
+ finish: string;
550
+ /** Read by the live region while the operation runs. */
551
+ working: string;
552
+ };
473
553
  report: {
474
554
  open: string;
475
555
  close: string;
@@ -701,6 +781,28 @@ export type UiCopy = {
701
781
  /** The accessible name of an editable cell. `{column}` is the column's header. */
702
782
  editCell: string;
703
783
  };
784
+ /** The files of a record (`Attachments`): listing, adding, removing and their states. */
785
+ attachments: {
786
+ /** The accessible name of the list. */
787
+ label: string;
788
+ /** An empty list in read mode. */
789
+ empty: string;
790
+ /** The drop zone's sentence when there is no file yet… */
791
+ emptyAdd: string;
792
+ /** …and when there already are some. */
793
+ add: string;
794
+ download: string;
795
+ remove: string;
796
+ /** The notice after removing. `{name}` is the file. */
797
+ removed: string;
798
+ undo: string;
799
+ /** The name of an upload's progress bar. `{name}` is the file. */
800
+ uploading: string;
801
+ cancelUpload: string;
802
+ /** A failed upload, when the server did not say why. */
803
+ failed: string;
804
+ retry: string;
805
+ };
704
806
  /** The theme switch: the three choices an app offers for the palette. */
705
807
  theme: {
706
808
  /** The name of the control — what a screen reader says for the group. */
@@ -763,6 +865,35 @@ export type UiCopy = {
763
865
  /** A value that was empty before the change. */
764
866
  empty: string;
765
867
  };
868
+ /** The media controls: an image chosen and cropped (`ImageInput`), a signature drawn or
869
+ * typed (`SignatureInput`). */
870
+ media: {
871
+ /** What the empty image zone says. */
872
+ dropImage: string;
873
+ chooseImage: string;
874
+ changeImage: string;
875
+ removeImage: string;
876
+ /** A file that is not an image: what to choose instead. */
877
+ notImage: string;
878
+ /** The name of the crop area, and how it is moved from the keyboard. */
879
+ crop: string;
880
+ cropHelp: string;
881
+ zoomIn: string;
882
+ zoomOut: string;
883
+ applyCrop: string;
884
+ cancel: string;
885
+ /** The name of the drawing pad, and what to do there. */
886
+ signaturePad: string;
887
+ signatureHelp: string;
888
+ clear: string;
889
+ /** The way in for whoever cannot draw: type the name, and it is written as the signature. */
890
+ typeName: string;
891
+ drawInstead: string;
892
+ /** The label of the box the name is typed in. */
893
+ typedName: string;
894
+ /** The image of a signature, when the form does not say whose it is. */
895
+ signature: string;
896
+ };
766
897
  };
767
898
  export declare const ENGLISH_COPY: UiCopy;
768
899
  /** What changes of web-ui's words, at any depth: `{ search: { joins: { any: "o" } } }`. */