@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.
Files changed (108) 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.js +55 -45
  14. package/dist/components/listing/SortMenu.d.ts +24 -20
  15. package/dist/components/listing/SortMenu.js +51 -91
  16. package/dist/components/listing/index.d.ts +2 -1
  17. package/dist/components/listing/index.js +2 -1
  18. package/dist/components/listing/modelCells.d.ts +7 -0
  19. package/dist/components/listing/modelCells.js +56 -0
  20. package/dist/components/listing/modelColumns.d.ts +9 -0
  21. package/dist/components/listing/modelColumns.js +59 -14
  22. package/dist/components/search/index.d.ts +1 -1
  23. package/dist/components/search/index.js +1 -1
  24. package/dist/components/search/model.d.ts +1 -0
  25. package/dist/components/search/model.js +7 -0
  26. package/dist/components/search/query.js +2 -3
  27. package/dist/foundations/copy.d.ts +131 -0
  28. package/dist/foundations/copy.es.js +86 -0
  29. package/dist/foundations/copy.js +86 -0
  30. package/dist/foundations/form.d.ts +25 -0
  31. package/dist/foundations/form.js +27 -0
  32. package/dist/foundations/index.d.ts +1 -1
  33. package/dist/foundations/index.js +1 -1
  34. package/dist/primitives/button/Button.js +3 -1
  35. package/dist/primitives/choice/ComboBox.js +2 -2
  36. package/dist/primitives/choice/SearchField.js +2 -5
  37. package/dist/primitives/choice/SegmentedControl.js +2 -2
  38. package/dist/primitives/choice/Slider.js +2 -2
  39. package/dist/primitives/date/DatePicker.js +4 -4
  40. package/dist/primitives/date/DateTime.js +3 -3
  41. package/dist/primitives/field/CardNumberInput.d.ts +41 -0
  42. package/dist/primitives/field/CardNumberInput.js +117 -0
  43. package/dist/primitives/field/CodeInput.d.ts +18 -0
  44. package/dist/primitives/field/CodeInput.js +37 -0
  45. package/dist/primitives/field/ColorInput.d.ts +21 -0
  46. package/dist/primitives/field/ColorInput.js +42 -0
  47. package/dist/primitives/field/ContactInputs.d.ts +18 -0
  48. package/dist/primitives/field/ContactInputs.js +70 -0
  49. package/dist/primitives/field/DurationInput.d.ts +20 -0
  50. package/dist/primitives/field/DurationInput.js +54 -0
  51. package/dist/primitives/field/Field.d.ts +26 -5
  52. package/dist/primitives/field/Field.js +19 -6
  53. package/dist/primitives/field/ImageInput.d.ts +51 -0
  54. package/dist/primitives/field/ImageInput.js +267 -0
  55. package/dist/primitives/field/PasswordInput.d.ts +29 -0
  56. package/dist/primitives/field/PasswordInput.js +44 -0
  57. package/dist/primitives/field/PatternInput.d.ts +23 -0
  58. package/dist/primitives/field/PatternInput.js +48 -0
  59. package/dist/primitives/field/RichTextEditor.d.ts +18 -0
  60. package/dist/primitives/field/RichTextEditor.js +224 -0
  61. package/dist/primitives/field/RichTextInput.d.ts +30 -0
  62. package/dist/primitives/field/RichTextInput.js +38 -0
  63. package/dist/primitives/field/SignatureInput.d.ts +34 -0
  64. package/dist/primitives/field/SignatureInput.js +215 -0
  65. package/dist/primitives/field/Suggest.js +2 -2
  66. package/dist/primitives/field/card.d.ts +33 -0
  67. package/dist/primitives/field/card.js +173 -0
  68. package/dist/primitives/field/clipboard.js +3 -2
  69. package/dist/primitives/field/contact.d.ts +17 -0
  70. package/dist/primitives/field/contact.js +65 -0
  71. package/dist/primitives/field/duration.d.ts +5 -0
  72. package/dist/primitives/field/duration.js +48 -0
  73. package/dist/primitives/field/index.d.ts +14 -0
  74. package/dist/primitives/field/index.js +20 -0
  75. package/dist/primitives/field/mask.d.ts +33 -0
  76. package/dist/primitives/field/mask.js +56 -0
  77. package/dist/primitives/field/media.d.ts +99 -0
  78. package/dist/primitives/field/media.js +136 -0
  79. package/dist/primitives/field/pattern.d.ts +10 -0
  80. package/dist/primitives/field/pattern.js +68 -0
  81. package/dist/primitives/menu/Menu.d.ts +16 -0
  82. package/dist/primitives/menu/Menu.js +26 -8
  83. package/dist/primitives/menu/index.d.ts +1 -1
  84. package/dist/primitives/menu/index.js +1 -1
  85. package/dist/primitives/picker/Picker.js +2 -2
  86. package/dist/primitives/picker/TagsField.js +2 -2
  87. package/dist/primitives/table/DataTable.d.ts +6 -4
  88. package/dist/primitives/table/DataTable.js +12 -8
  89. package/dist/primitives/table/EditableTable.js +102 -99
  90. package/dist/primitives/table/Table.d.ts +5 -8
  91. package/dist/primitives/table/Table.js +63 -15
  92. package/dist/primitives/table/index.d.ts +2 -1
  93. package/dist/primitives/table/index.js +2 -1
  94. package/dist/primitives/table/sorting.d.ts +42 -0
  95. package/dist/primitives/table/sorting.js +110 -0
  96. package/dist/primitives/value/RichText.d.ts +10 -0
  97. package/dist/primitives/value/RichText.js +130 -0
  98. package/dist/primitives/value/Value.d.ts +20 -2
  99. package/dist/primitives/value/Value.js +26 -7
  100. package/dist/primitives/value/index.d.ts +3 -1
  101. package/dist/primitives/value/index.js +4 -1
  102. package/dist/primitives/value/rounding.d.ts +9 -0
  103. package/dist/primitives/value/rounding.js +48 -0
  104. package/dist/tokens.css +103 -0
  105. package/dist/views/list/List.d.ts +5 -4
  106. package/dist/views/list/ListView.js +2 -2
  107. package/dist/views/list/types.d.ts +6 -6
  108. package/package.json +9 -1
@@ -0,0 +1,130 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useUiCopy } from "../../foundations/copy.js";
4
+ import DOMPurify from "dompurify";
5
+ import { useSyncExternalStore } from "react";
6
+ /* ==================================================================== *
7
+ * RICH TEXT, READ — the HTML of a note shown as what it says.
8
+ *
9
+ * Context: this HTML comes from people (a description pasted from an email)
10
+ * and from engines (an Odoo `html` field), and either can carry a script, an
11
+ * `onerror`, a `javascript:` link. So it is never put on the page as it came:
12
+ * DOMPurify keeps the text and its structure (paragraphs, headings, lists,
13
+ * links, tables, a mention) and drops everything that runs. A link always
14
+ * opens in another tab, without handing that tab our window.
15
+ *
16
+ * ── On the server there is no DOM to clean with ─────────────────────
17
+ *
18
+ * DOMPurify needs a document. Without one (server rendering, a Node test), the
19
+ * safe answer is the TEXT, tags removed and escaped by React — never the HTML
20
+ * unchecked. The browser then upgrades it to the cleaned HTML; the switch goes
21
+ * through `useSyncExternalStore`, so hydration does not see two different trees.
22
+ * ==================================================================== */
23
+ const ALLOWED_TAGS = [
24
+ "p",
25
+ "br",
26
+ "strong",
27
+ "b",
28
+ "em",
29
+ "i",
30
+ "u",
31
+ "s",
32
+ "strike",
33
+ "del",
34
+ "code",
35
+ "pre",
36
+ "h1",
37
+ "h2",
38
+ "h3",
39
+ "h4",
40
+ "ul",
41
+ "ol",
42
+ "li",
43
+ "blockquote",
44
+ "hr",
45
+ "a",
46
+ "span",
47
+ "table",
48
+ "thead",
49
+ "tbody",
50
+ "tr",
51
+ "th",
52
+ "td",
53
+ "colgroup",
54
+ "col",
55
+ "img",
56
+ ];
57
+ const ALLOWED_ATTR = [
58
+ "href",
59
+ "title",
60
+ "colspan",
61
+ "rowspan",
62
+ "src",
63
+ "alt",
64
+ "data-type",
65
+ "data-id",
66
+ "data-label",
67
+ ];
68
+ let hooked = false;
69
+ /** The HTML with everything that runs taken out, or `null` where there is no DOM to do it. */
70
+ export function sanitizeRichText(html) {
71
+ if (typeof window === "undefined" || !DOMPurify.isSupported)
72
+ return null;
73
+ if (!hooked) {
74
+ hooked = true;
75
+ DOMPurify.addHook("afterSanitizeAttributes", (node) => {
76
+ if (node.tagName === "A") {
77
+ node.setAttribute("target", "_blank");
78
+ node.setAttribute("rel", "noopener noreferrer");
79
+ }
80
+ if (node.tagName === "IMG") {
81
+ /* An image whose address was not allowed is a broken box: gone. One without a
82
+ description is read as decoration (`alt=""`) instead of as its file name. */
83
+ if (!node.hasAttribute("src"))
84
+ node.remove();
85
+ else if (!node.hasAttribute("alt"))
86
+ node.setAttribute("alt", "");
87
+ }
88
+ });
89
+ }
90
+ return DOMPurify.sanitize(html, {
91
+ ALLOWED_TAGS,
92
+ ALLOWED_ATTR,
93
+ ALLOWED_URI_REGEXP: /^(?:https?:|mailto:|tel:|data:image\/(?:png|jpeg|gif|webp);)/i,
94
+ });
95
+ }
96
+ /** The words of an HTML, without its tags: what is shown where it cannot be cleaned. */
97
+ export function richTextPlain(html) {
98
+ return html
99
+ .replace(/<(?:br|\/p|\/li|\/h[1-6]|\/tr)\s*\/?>/gi, " ")
100
+ .replace(/<[^>]*>/g, "")
101
+ .replace(/&nbsp;/g, " ")
102
+ .replace(/&amp;/g, "&")
103
+ .replace(/&lt;/g, "<")
104
+ .replace(/&gt;/g, ">")
105
+ .replace(/&quot;/g, '"')
106
+ .replace(/&#39;/g, "'")
107
+ .replace(/\s+/g, " ")
108
+ .trim();
109
+ }
110
+ /** Nothing written: no text and no image — «<p></p>» from an emptied editor counts as nothing. */
111
+ export function isRichTextEmpty(html) {
112
+ if (html === null || html === undefined)
113
+ return true;
114
+ return richTextPlain(html) === "" && !/<(?:img|hr|table)\b/i.test(html);
115
+ }
116
+ const subscribe = () => () => { };
117
+ /** The HTML of a note, read: cleaned, in the system's text style (`rich-text` in tokens.css). */
118
+ export function RichTextValue({ value }) {
119
+ const unset = useUiCopy().field.unset;
120
+ const inBrowser = useSyncExternalStore(subscribe, () => true, () => false);
121
+ if (isRichTextEmpty(value)) {
122
+ return _jsx("span", { className: "text-meta text-muted italic", children: unset });
123
+ }
124
+ const html = value ?? "";
125
+ const clean = inBrowser ? sanitizeRichText(html) : null;
126
+ if (clean === null)
127
+ return _jsx("p", { className: "rich-text", children: richTextPlain(html) });
128
+ /* Cleaned by DOMPurify just above, with the tags and URLs listed there. */
129
+ return _jsx("div", { className: "rich-text min-w-0", dangerouslySetInnerHTML: { __html: clean } });
130
+ }
@@ -1,3 +1,4 @@
1
+ import { type Rounding } from "./rounding.js";
1
2
  import type { ReactNode } from "react";
2
3
  export declare const formatCount: (value: number, locale?: string) => string;
3
4
  export declare const formatNumber: (value: number, locale?: string) => string;
@@ -7,6 +8,10 @@ export declare function KeyValue({ label, value }: {
7
8
  label: string;
8
9
  value: ReactNode;
9
10
  }): import("react").JSX.Element;
11
+ /** A number written with any `Intl.NumberFormatOptions` — a currency, a percentage, fixed
12
+ * decimals — in the session locale. The same options `NumberInput` takes, so a value reads
13
+ * the same in its form and in the cell or the figure that shows it elsewhere. */
14
+ export declare const formatWith: (value: number, options: Intl.NumberFormatOptions, locale?: string) => string;
10
15
  /**
11
16
  * A number with its unit: right aligned, with tabular figures.
12
17
  *
@@ -14,11 +19,24 @@ export declare function KeyValue({ label, value }: {
14
19
  * the same width, so `32.561` lines up across rows exactly as it did in monospace — without
15
20
  * making a business report look like a terminal. The unit stays in the same face so the
16
21
  * digits keep their column.
22
+ *
23
+ * `format` is a preset or any `Intl.NumberFormatOptions`: `{ style: "currency", currency:
24
+ * "BOB" }` writes «Bs 1.234,50», `{ style: "percent" }` takes 0–1, `{ minimumFractionDigits:
25
+ * 2, maximumFractionDigits: 2 }` rounds to two. `align="left"` for a figure that is not in a
26
+ * column.
17
27
  */
18
- export declare function Numeric({ value, unit, format, }: {
28
+ export declare function Numeric({ value, unit, format, align, rounding, signed, }: {
19
29
  value: number;
20
30
  unit?: string;
21
- format?: "integer" | "decimal" | "distance";
31
+ format?: "integer" | "decimal" | "distance" | Intl.NumberFormatOptions;
32
+ align?: "right" | "left";
33
+ /** Round the VALUE before writing it — to 0,05 for cash, to the unit's precision. See
34
+ * `rounding.ts`: the format alone only rounds what is drawn. */
35
+ rounding?: Rounding;
36
+ /** A figure whose sign is the point — a balance, a difference, a variation: it carries
37
+ * «+» or «−» always, and a negative is drawn in the danger tone (the sign says it too, so
38
+ * it never rests on the color alone). */
39
+ signed?: boolean;
22
40
  }): import("react").JSX.Element;
23
41
  /**
24
42
  * The content of a data cell, with what the system decides and the view does not.
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { currentCopyLocale, useUiCopy } from "../../foundations/copy.js";
4
+ import { roundTo } from "./rounding.js";
4
5
  /**
5
6
  * Context: the mono/sans rule decides everything in here. The label goes in
6
7
  * PROPORTIONAL —«orígenes», «mediana» are words, not data— and the value in
@@ -31,6 +32,17 @@ export const formatDistance = (value, locale = currentCopyLocale()) => distanceF
31
32
  export function KeyValue({ label, value }) {
32
33
  return (_jsxs("div", { className: "min-w-0", children: [_jsx("dt", { className: "text-micro text-muted", children: label }), _jsx("dd", { className: "mt-0.5 truncate text-lead tabular-nums text-ink", children: value })] }));
33
34
  }
35
+ /** The three presets, as the options they are — so `signed` and a custom format share one
36
+ * path. Kept in step with `integerFormat`, `decimalFormat` and `distanceFormat` above. */
37
+ const PRESET = {
38
+ integer: { maximumFractionDigits: 0 },
39
+ decimal: { maximumFractionDigits: 2 },
40
+ distance: { minimumFractionDigits: 4, maximumFractionDigits: 4 },
41
+ };
42
+ /** A number written with any `Intl.NumberFormatOptions` — a currency, a percentage, fixed
43
+ * decimals — in the session locale. The same options `NumberInput` takes, so a value reads
44
+ * the same in its form and in the cell or the figure that shows it elsewhere. */
45
+ export const formatWith = (value, options, locale = currentCopyLocale()) => new Intl.NumberFormat(locale, options).format(value);
34
46
  /**
35
47
  * A number with its unit: right aligned, with tabular figures.
36
48
  *
@@ -38,14 +50,21 @@ export function KeyValue({ label, value }) {
38
50
  * the same width, so `32.561` lines up across rows exactly as it did in monospace — without
39
51
  * making a business report look like a terminal. The unit stays in the same face so the
40
52
  * digits keep their column.
53
+ *
54
+ * `format` is a preset or any `Intl.NumberFormatOptions`: `{ style: "currency", currency:
55
+ * "BOB" }` writes «Bs 1.234,50», `{ style: "percent" }` takes 0–1, `{ minimumFractionDigits:
56
+ * 2, maximumFractionDigits: 2 }` rounds to two. `align="left"` for a figure that is not in a
57
+ * column.
41
58
  */
42
- export function Numeric({ value, unit, format = "integer", }) {
43
- const text = format === "distance"
44
- ? formatDistance(value)
45
- : format === "decimal"
46
- ? formatNumber(value)
47
- : formatCount(value);
48
- return (_jsxs("span", { className: "block text-right text-body tabular-nums whitespace-nowrap text-ink", children: [_jsx("span", { className: "tabular-nums", children: text }), unit === undefined ? null : _jsx("span", { className: "ml-1 text-muted", children: unit })] }));
59
+ export function Numeric({ value, unit, format = "integer", align = "right", rounding, signed = false, }) {
60
+ /* The locale of the copy in context, as `NumberInput` reads it — not only the installed
61
+ one: inside a `UiCopyProvider` the cell and the form next to it write the same digits. */
62
+ const { locale } = useUiCopy();
63
+ const shown = rounding === undefined ? value : roundTo(value, rounding.increment, rounding.method);
64
+ const options = typeof format === "object" ? format : PRESET[format];
65
+ const text = formatWith(shown, signed ? { ...options, signDisplay: "exceptZero" } : options, locale);
66
+ const negative = signed && shown < 0;
67
+ return (_jsxs("span", { className: `${align === "left" ? "inline-block text-left" : "block text-right"} text-body tabular-nums whitespace-nowrap ${negative ? "text-danger" : "text-ink"}`, children: [_jsx("span", { className: "tabular-nums", children: text }), unit === undefined ? null : _jsx("span", { className: "ml-1 text-muted", children: unit })] }));
49
68
  }
50
69
  /**
51
70
  * The content of a data cell, with what the system decides and the view does not.
@@ -5,5 +5,7 @@ export { Metric, type MetricProps, Metrics } from "./Metric.js";
5
5
  export { Avatar, type AvatarSize, type PersonContact, UserChip, type UserChipProps, UserStack, } from "./Person.js";
6
6
  export { ProgressBar, type ProgressBarProps, } from "./ProgressBar.js";
7
7
  export { RelationCard, type RelationCardProps, type RelationSummary, RelationValue, type RelationValueProps, } from "./Relation.js";
8
+ export { isRichTextEmpty, richTextPlain, RichTextValue, sanitizeRichText, } from "./RichText.js";
9
+ export { type Rounding, type RoundingMethod, roundTo, } from "./rounding.js";
8
10
  export { Totals, type TotalsCredit, type TotalsDeduction, type TotalsFact, type TotalsProps, type TotalsRow, } from "./Totals.js";
9
- export { Cell, formatCount, formatDistance, formatNumber, KeyValue, Numeric, } from "./Value.js";
11
+ export { Cell, formatCount, formatDistance, formatNumber, formatWith, KeyValue, Numeric, } from "./Value.js";
@@ -6,5 +6,8 @@ export { Metric, Metrics } from "./Metric.js";
6
6
  export { Avatar, UserChip, UserStack, } from "./Person.js";
7
7
  export { ProgressBar, } from "./ProgressBar.js";
8
8
  export { RelationCard, RelationValue, } from "./Relation.js";
9
+ /* The HTML of a note, read — cleaned of anything that runs. */
10
+ export { isRichTextEmpty, richTextPlain, RichTextValue, sanitizeRichText, } from "./RichText.js";
11
+ export { roundTo, } from "./rounding.js";
9
12
  export { Totals, } from "./Totals.js";
10
- export { Cell, formatCount, formatDistance, formatNumber, KeyValue, Numeric, } from "./Value.js";
13
+ export { Cell, formatCount, formatDistance, formatNumber, formatWith, KeyValue, Numeric, } from "./Value.js";
@@ -0,0 +1,9 @@
1
+ export type RoundingMethod = "half-up" | "half-even" | "up" | "down";
2
+ export type Rounding = {
3
+ /** The multiple: 0.05 rounds to five cents, 1 to a whole unit, 0.001 to grams. */
4
+ increment: number;
5
+ /** `half-up` by default — what a price expects. `up`/`down` are away from/towards zero. */
6
+ method?: RoundingMethod;
7
+ };
8
+ /** `value` rounded to a multiple of `increment`, by `method`. */
9
+ export declare function roundTo(value: number, increment: number, method?: RoundingMethod): number;
@@ -0,0 +1,48 @@
1
+ /* ==================================================================== *
2
+ * ROUNDING — to a multiple, and saying how.
3
+ *
4
+ * Context: `maximumFractionDigits` rounds what is DRAWN; the value underneath
5
+ * keeps its tail, and a total of drawn values then differs from the drawn
6
+ * total by a cent. An ERP rounds the VALUE, and not always to a decimal: cash
7
+ * to 0,05 (Bolivia has no 1-centavo coin in circulation), a unit of measure to
8
+ * its precision (a box is 1, a kilo 0,001), a price to 0,10. And the method
9
+ * is part of the rule: half up for a price, half even for statistics, up for
10
+ * what is charged per started hour, down for what is paid.
11
+ *
12
+ * Pure, in the display tier so a value that shows (`Numeric`) and a control
13
+ * that asks (`NumberInput`) round with the same function.
14
+ * ==================================================================== */
15
+ /** How many decimals a number is written with: 0.05 → 2. Used to cut the binary tail that
16
+ * `0.1 + 0.2` leaves, so 1.15 rounded to 0.05 is 1.15 and not 1.1500000000000001. */
17
+ function decimalsOf(value) {
18
+ const text = String(value);
19
+ if (text.includes("e-"))
20
+ return Number(text.split("e-")[1]);
21
+ return text.includes(".") ? (text.split(".")[1]?.length ?? 0) : 0;
22
+ }
23
+ /** `value` rounded to a multiple of `increment`, by `method`. */
24
+ export function roundTo(value, increment, method = "half-up") {
25
+ if (!Number.isFinite(value) || !(increment > 0))
26
+ return value;
27
+ const decimals = decimalsOf(increment);
28
+ /* The quotient, cleaned of the float tail before deciding: 1.15 / 0.05 is 22.999999… */
29
+ const steps = Number((Math.abs(value) / increment).toFixed(9));
30
+ const whole = Math.floor(steps);
31
+ const fraction = Number((steps - whole).toFixed(9));
32
+ let rounded;
33
+ switch (method) {
34
+ case "up":
35
+ rounded = fraction > 0 ? whole + 1 : whole;
36
+ break;
37
+ case "down":
38
+ rounded = whole;
39
+ break;
40
+ case "half-even":
41
+ rounded = fraction > 0.5 || (fraction === 0.5 && whole % 2 === 1) ? whole + 1 : whole;
42
+ break;
43
+ default:
44
+ rounded = fraction >= 0.5 ? whole + 1 : whole;
45
+ }
46
+ const result = Number((rounded * increment).toFixed(decimals));
47
+ return value < 0 ? -result : result;
48
+ }
package/dist/tokens.css CHANGED
@@ -1237,3 +1237,106 @@
1237
1237
  writing-mode: vertical-rl;
1238
1238
  transform: rotate(180deg);
1239
1239
  }
1240
+
1241
+ /* ==================================================================== *
1242
+ * RICH TEXT — the HTML of a note, a description, an email body.
1243
+ *
1244
+ * The SAME rules for the editor (`RichTextInput`, Tiptap) and for reading it
1245
+ * (`RichTextValue`): what is written is what is read, to the line. HTML that
1246
+ * arrives from an engine (Odoo `html` fields) has no classes, so the look is
1247
+ * given by the element — and only with tokens, so it follows the theme.
1248
+ * ==================================================================== */
1249
+ @utility rich-text {
1250
+ font-size: var(--text-meta);
1251
+ line-height: var(--text-meta--line-height);
1252
+ color: var(--color-ink);
1253
+ overflow-wrap: anywhere;
1254
+ & > * + * {
1255
+ margin-top: calc(var(--spacing) * 2);
1256
+ }
1257
+ & h2 {
1258
+ font-size: var(--text-lead);
1259
+ line-height: var(--text-lead--line-height);
1260
+ font-weight: 600;
1261
+ }
1262
+ & h3 {
1263
+ font-size: var(--text-body);
1264
+ line-height: var(--text-body--line-height);
1265
+ font-weight: 600;
1266
+ }
1267
+ & ul {
1268
+ list-style: disc;
1269
+ padding-left: calc(var(--spacing) * 5);
1270
+ }
1271
+ & ol {
1272
+ list-style: decimal;
1273
+ padding-left: calc(var(--spacing) * 5);
1274
+ }
1275
+ & li > p {
1276
+ margin: 0;
1277
+ }
1278
+ & blockquote {
1279
+ border-left: calc(var(--spacing) * 0.5) solid var(--color-line-strong);
1280
+ padding-left: calc(var(--spacing) * 3);
1281
+ color: var(--color-muted);
1282
+ }
1283
+ & a {
1284
+ color: var(--color-primary);
1285
+ text-decoration: underline;
1286
+ text-underline-offset: calc(var(--spacing) * 0.5);
1287
+ }
1288
+ & code {
1289
+ font-family: var(--font-mono);
1290
+ background: color-mix(in oklab, var(--color-ink) 6%, transparent);
1291
+ border-radius: var(--radius-mark);
1292
+ padding: 0 calc(var(--spacing) * 1);
1293
+ }
1294
+ & pre {
1295
+ font-family: var(--font-mono);
1296
+ background: color-mix(in oklab, var(--color-ink) 6%, transparent);
1297
+ border-radius: var(--radius-control);
1298
+ padding: calc(var(--spacing) * 2) calc(var(--spacing) * 3);
1299
+ overflow-x: auto;
1300
+ }
1301
+ & pre code {
1302
+ background: none;
1303
+ padding: 0;
1304
+ }
1305
+ & hr {
1306
+ border: none;
1307
+ border-top: calc(var(--spacing) * 0.25) solid var(--color-line);
1308
+ }
1309
+ & table {
1310
+ border-collapse: collapse;
1311
+ width: 100%;
1312
+ }
1313
+ & th,
1314
+ & td {
1315
+ border: calc(var(--spacing) * 0.25) solid var(--color-line-strong);
1316
+ padding: calc(var(--spacing) * 1) calc(var(--spacing) * 2);
1317
+ text-align: left;
1318
+ vertical-align: top;
1319
+ }
1320
+ & th {
1321
+ font-weight: 600;
1322
+ background: var(--color-ground);
1323
+ }
1324
+ /* A person named in the text: the same tint as a link, so it reads as «someone», and a
1325
+ box so it is one thing that is deleted at once. */
1326
+ & [data-type="mention"] {
1327
+ color: var(--color-primary);
1328
+ background: color-mix(in oklab, var(--color-primary) 10%, transparent);
1329
+ border-radius: var(--radius-mark);
1330
+ padding: 0 calc(var(--spacing) * 0.5);
1331
+ }
1332
+ & p.is-editor-empty:first-child::before {
1333
+ content: attr(data-placeholder);
1334
+ color: var(--color-muted);
1335
+ float: left;
1336
+ height: 0;
1337
+ pointer-events: none;
1338
+ }
1339
+ & .selectedCell {
1340
+ background: color-mix(in oklab, var(--color-primary) 10%, transparent);
1341
+ }
1342
+ }
@@ -5,7 +5,7 @@ import type { ViewIcon } from "../../components/listing/ViewSwitcher.js";
5
5
  import type { Surface } from "../../foundations/types.js";
6
6
  import type { DataColumn, DataTableCreate } from "../../primitives/table/DataTable.js";
7
7
  import type { RowAction } from "../../primitives/table/RowActions.js";
8
- import type { SortState } from "../../primitives/table/Table.js";
8
+ import type { SortState } from "../../primitives/table/sorting.js";
9
9
  import { type CardWays, type ListCalendar, type ListCrumb, type ListEmpty, type ListGantt, type ListGroup, type ListPivot, type ListPresent, type ListReport, type ListSet, type ListSheet, type ListTimeline, type RowOf } from "./types.js";
10
10
  import type { ReactNode } from "react";
11
11
  type BarProps = {
@@ -97,9 +97,10 @@ export type ListProps<T extends object> = {
97
97
  onSelect: () => void;
98
98
  };
99
99
  };
100
- /** El orden, que también es del conjunto. */
101
- sort?: SortState | null;
102
- onSortChange?: (next: SortState | null) => void;
100
+ /** El orden, que también es del conjunto: una lista de criterios, el primero manda y los
101
+ * demás desempatan. Vacío es «como llegaron». */
102
+ sort?: readonly SortState[];
103
+ onSortChange?: (next: SortState[]) => void;
103
104
  /** Por qué campos se puede ordenar en las caras que NO tienen encabezados
104
105
  * donde hacer clic. */
105
106
  sortable?: readonly SortField<T>[];
@@ -147,7 +147,7 @@ export function ListView({ items, keyOf, identity, crumbs, embedded = false, sur
147
147
  const sortMenu = sortable === undefined ||
148
148
  sortable.length === 0 ||
149
149
  declaredReport ||
150
- present === "pivot" ? null : (_jsx(SortMenu, { fields: sortable, sort: sort ?? null, onSortChange: (next) => onSortChange?.(next), ...(surface === undefined ? {} : { surface }) }));
150
+ present === "pivot" ? null : (_jsx(SortMenu, { fields: sortable, sort: sort ?? [], onSortChange: (next) => onSortChange?.(next), ...(surface === undefined ? {} : { surface }) }));
151
151
  const rightViews = views ?? switcher;
152
152
  const viewsSlot = columnsMenu === null && sortMenu === null && rightViews === undefined ? undefined : (_jsxs(_Fragment, { children: [sortMenu, columnsMenu, rightViews] }));
153
153
  /* Embedded, the line is drawn only if it has something to OFFER: a count alone repeats
@@ -202,7 +202,7 @@ export function ListView({ items, keyOf, identity, crumbs, embedded = false, sur
202
202
  veces no rompe nada, pero es trabajo al pedo sobre cada render. */
203
203
  const ordered = useMemo(() => present === "table" || sortable === undefined
204
204
  ? items
205
- : sortItems(items, sortable, sort ?? null), [items, present, sortable, sort]);
205
+ : sortItems(items, sortable, sort), [items, present, sortable, sort]);
206
206
  /**
207
207
  * La grilla que hay que dibujar: la que le pasaron plegada, o la que se arma
208
208
  * con las filas en la mano.
@@ -14,7 +14,7 @@ import type { DataColumn, DataTableCreate } from "../../primitives/table/DataTab
14
14
  import type { ReportColumn, ReportRow } from "../../primitives/table/ReportTable.js";
15
15
  import type { RowAction } from "../../primitives/table/RowActions.js";
16
16
  import type { SheetColumn } from "../../primitives/table/SheetTable.js";
17
- import type { SortState } from "../../primitives/table/Table.js";
17
+ import type { SortState } from "../../primitives/table/sorting.js";
18
18
  import type { LucideIcon } from "lucide-react";
19
19
  import type { ReactNode } from "react";
20
20
  /**
@@ -276,11 +276,11 @@ export type ListViewProps<T extends object> = {
276
276
  * Owned by the view when omitted — catalog filters must not reset them. */
277
277
  visibleColumnIds?: readonly string[];
278
278
  onVisibleColumnIdsChange?: (ids: readonly string[]) => void;
279
- /** The sorting of the table. It lives outside because it belongs to the SET
280
- * and not to the presentation: switching from table to rows has no reason to
281
- * lose it. */
282
- sort?: SortState | null;
283
- onSortChange?: (next: SortState | null) => void;
279
+ /** The order of the set, outermost first: «by date, then by customer». It lives outside
280
+ * because it belongs to the SET and not to the presentation: switching from table to
281
+ * rows has no reason to lose it. Empty or absent is «as they came». */
282
+ sort?: readonly SortState[];
283
+ onSortChange?: (next: SortState[]) => void;
284
284
  /**
285
285
  * Por qué campos se puede ordenar, para las presentaciones que NO tienen
286
286
  * encabezados donde hacer clic.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sincpro/web-ui",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "description": "An enterprise web design system for business software — ERPs, back offices, management tools — in React and Tailwind: tokens, primitives, components and the two views every management screen is made of.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -56,7 +56,15 @@
56
56
  "@fullcalendar/react": "7.1.3",
57
57
  "@internationalized/date": "^3.12.4",
58
58
  "@svar-ui/react-gantt": "2.8.2",
59
+ "@tiptap/extension-mention": "^3.32.2",
60
+ "@tiptap/extension-table": "^3.32.2",
61
+ "@tiptap/extensions": "^3.32.2",
62
+ "@tiptap/pm": "^3.32.2",
63
+ "@tiptap/react": "^3.32.2",
64
+ "@tiptap/starter-kit": "^3.32.2",
65
+ "@tiptap/suggestion": "^3.32.2",
59
66
  "@uiw/react-codemirror": "^4.25.11",
67
+ "dompurify": "^3.4.17",
60
68
  "lucide-react": "^1.45.0",
61
69
  "react-aria": "^3.53.1",
62
70
  "react-aria-components": "^1.22.1",