@sincpro/web-ui 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -10
- package/dist/components/activity/Activity.d.ts +69 -0
- package/dist/components/activity/Activity.js +279 -0
- package/dist/components/{chatter → activity}/Messages.d.ts +2 -2
- package/dist/components/{chatter → activity}/Messages.js +9 -6
- package/dist/components/{chatter → activity}/index.d.ts +1 -1
- package/dist/components/activity/index.js +4 -0
- package/dist/components/assist/AssistMark.d.ts +3 -3
- package/dist/components/assist/AssistMark.js +1 -1
- package/dist/components/assist/assist.d.ts +1 -1
- package/dist/components/assist/assist.js +1 -1
- package/dist/components/form/Sheet.d.ts +4 -0
- package/dist/components/form/Sheet.js +11 -4
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/lines/LineItems.d.ts +42 -5
- package/dist/components/lines/LineItems.js +106 -18
- package/dist/components/listing/Listing.js +1 -1
- package/dist/components/listing/ListingBar.d.ts +41 -2
- package/dist/components/listing/ListingBar.js +43 -4
- package/dist/components/listing/Row.d.ts +5 -1
- package/dist/components/listing/Row.js +3 -2
- package/dist/components/record/RecordHeader.d.ts +21 -17
- package/dist/components/record/RecordHeader.js +46 -14
- package/dist/components/record/RecordNotices.d.ts +37 -0
- package/dist/components/record/RecordNotices.js +75 -0
- package/dist/components/record/ThumbBar.js +1 -1
- package/dist/foundations/copy.d.ts +96 -12
- package/dist/foundations/copy.es.js +63 -10
- package/dist/foundations/copy.js +55 -7
- package/dist/primitives/choice/ComboBox.d.ts +3 -0
- package/dist/primitives/choice/ComboBox.js +2 -2
- package/dist/primitives/choice/SearchField.d.ts +10 -1
- package/dist/primitives/choice/SearchField.js +21 -2
- package/dist/primitives/date/Calendar.js +30 -6
- package/dist/primitives/date/DatePicker.d.ts +11 -3
- package/dist/primitives/date/DatePicker.js +12 -8
- package/dist/primitives/date/DateTime.d.ts +35 -0
- package/dist/primitives/date/DateTime.js +100 -0
- package/dist/primitives/date/index.d.ts +1 -0
- package/dist/primitives/date/index.js +4 -2
- package/dist/primitives/dialog/Drill.d.ts +90 -0
- package/dist/primitives/dialog/Drill.js +172 -0
- package/dist/primitives/dialog/index.d.ts +1 -0
- package/dist/primitives/dialog/index.js +2 -0
- package/dist/primitives/field/DropZone.js +1 -1
- package/dist/primitives/field/Field.d.ts +14 -2
- package/dist/primitives/field/Field.js +25 -14
- package/dist/primitives/field/copy.d.ts +16 -0
- package/dist/primitives/field/copy.js +69 -0
- package/dist/primitives/field/index.d.ts +2 -0
- package/dist/primitives/field/index.js +4 -0
- package/dist/primitives/field/note.d.ts +55 -0
- package/dist/primitives/field/note.js +117 -0
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/menu/Menu.d.ts +28 -0
- package/dist/primitives/menu/Menu.js +47 -17
- package/dist/primitives/notice/Callout.js +1 -1
- package/dist/primitives/notice/Flag.d.ts +13 -0
- package/dist/primitives/notice/Flag.js +31 -0
- package/dist/primitives/notice/index.d.ts +1 -0
- package/dist/primitives/notice/index.js +1 -0
- package/dist/primitives/overlay/Preview.d.ts +19 -0
- package/dist/primitives/overlay/Preview.js +60 -0
- package/dist/primitives/overlay/index.d.ts +1 -0
- package/dist/primitives/overlay/index.js +3 -1
- package/dist/primitives/picker/Picker.d.ts +41 -0
- package/dist/primitives/picker/Picker.js +62 -8
- package/dist/primitives/table/CellCopy.d.ts +20 -0
- package/dist/primitives/table/CellCopy.js +23 -0
- package/dist/primitives/table/DataTable.d.ts +55 -1
- package/dist/primitives/table/DataTable.js +126 -40
- package/dist/primitives/table/EditableTable.d.ts +97 -2
- package/dist/primitives/table/EditableTable.js +223 -29
- package/dist/primitives/table/SheetTable.d.ts +4 -1
- package/dist/primitives/table/SheetTable.js +38 -5
- package/dist/primitives/table/index.d.ts +2 -2
- package/dist/primitives/table/index.js +1 -1
- package/dist/primitives/tabs/Tabs.d.ts +2 -2
- package/dist/primitives/tabs/Tabs.js +1 -1
- package/dist/primitives/tag/TagsField.d.ts +3 -0
- package/dist/primitives/tag/TagsField.js +6 -2
- package/dist/primitives/tree/Tree.d.ts +56 -0
- package/dist/primitives/tree/Tree.js +74 -0
- package/dist/primitives/tree/index.d.ts +1 -0
- package/dist/primitives/tree/index.js +3 -0
- package/dist/primitives/value/Copyable.d.ts +11 -0
- package/dist/primitives/value/Copyable.js +27 -6
- package/dist/primitives/value/Fecha.js +10 -1
- package/dist/primitives/value/ProgressBar.d.ts +31 -0
- package/dist/primitives/value/ProgressBar.js +53 -0
- package/dist/primitives/value/Relation.d.ts +6 -1
- package/dist/primitives/value/Relation.js +27 -10
- package/dist/primitives/value/Totals.d.ts +102 -7
- package/dist/primitives/value/Totals.js +52 -2
- package/dist/primitives/value/Value.d.ts +0 -37
- package/dist/primitives/value/Value.js +0 -39
- package/dist/primitives/value/index.d.ts +4 -3
- package/dist/primitives/value/index.js +4 -3
- package/dist/tokens.css +44 -7
- package/dist/views/list/List.d.ts +8 -2
- package/dist/views/list/List.js +6 -2
- package/dist/views/list/ListView.d.ts +1 -1
- package/dist/views/list/ListView.js +34 -8
- package/dist/views/list/derive.d.ts +12 -1
- package/dist/views/list/derive.js +29 -3
- package/dist/views/list/presentations.d.ts +2 -2
- package/dist/views/list/presentations.js +5 -3
- package/dist/views/list/types.d.ts +13 -1
- package/dist/views/record/Record.d.ts +27 -8
- package/dist/views/record/Record.js +97 -27
- package/dist/views/record/index.d.ts +1 -1
- package/package.json +5 -1
- package/dist/components/chatter/Chatter.d.ts +0 -161
- package/dist/components/chatter/Chatter.js +0 -232
- package/dist/components/chatter/index.js +0 -4
- /package/dist/components/{chatter → activity}/Timeline.d.ts +0 -0
- /package/dist/components/{chatter → activity}/Timeline.js +0 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { AriaLocale } from "../../foundations/aria.js";
|
|
4
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
5
|
+
import { byDensity, useDensity } from "../../foundations/density.js";
|
|
6
|
+
import { cn, focusRing, tv } from "../../foundations/variants.js";
|
|
7
|
+
import { Check, ChevronRight } from "lucide-react";
|
|
8
|
+
import { Button as AriaButton, Checkbox as AriaCheckbox, Tree as AriaTree, TreeItem as AriaTreeItem, TreeItemContent, } from "react-aria-components";
|
|
9
|
+
const row = tv({
|
|
10
|
+
extend: focusRing,
|
|
11
|
+
base: "group relative flex min-w-0 cursor-default items-center rounded-control text-meta text-ink -outline-offset-2 select-none forced-color-adjust-none",
|
|
12
|
+
variants: {
|
|
13
|
+
isSelected: {
|
|
14
|
+
true: "bg-primary/10 forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]",
|
|
15
|
+
},
|
|
16
|
+
isHovered: { true: "" },
|
|
17
|
+
},
|
|
18
|
+
compoundVariants: [{ isSelected: false, isHovered: true, class: "bg-ink/4" }],
|
|
19
|
+
});
|
|
20
|
+
/* The chevron column is exactly one indent step wide, so a child's chevron lands under its
|
|
21
|
+
parent's name and the levels read as columns, not as a ragged edge. */
|
|
22
|
+
const chevronButton = tv({
|
|
23
|
+
extend: focusRing,
|
|
24
|
+
base: "inline-flex size-(--spacing-indent) shrink-0 items-center justify-center rounded-mark text-muted transition-colors duration-(--duration-instant) hover:bg-ink/6 hover:text-ink pressed:bg-ink/10",
|
|
25
|
+
});
|
|
26
|
+
/** Every id of the hierarchy that can be chosen — what «select all» means here. */
|
|
27
|
+
function choosable(items) {
|
|
28
|
+
return items.flatMap((item) => [
|
|
29
|
+
...(item.disabled === true ? [] : [item.id]),
|
|
30
|
+
...choosable(item.children ?? []),
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A hierarchy of records — the chart of accounts, product categories, warehouse locations —
|
|
35
|
+
* with its groups that fold, a figure per row, and one or many rows to choose.
|
|
36
|
+
*
|
|
37
|
+
* Context: the keyboard is React Aria's `Tree`: arrows walk the visible rows, → opens a group
|
|
38
|
+
* and ← folds it (or goes up to the parent), Home and End jump, and typing a name moves to it.
|
|
39
|
+
* Escape does NOT clear the selection: the tree often lives in a dialog or a popover, and
|
|
40
|
+
* Escape there means «close» — clearing the choice on the way out would lose it without a word.
|
|
41
|
+
*/
|
|
42
|
+
export function Tree({ items, label, labelledBy, expanded, defaultExpanded, onExpandedChange, selectionMode = "none", selected, defaultSelected, onSelectedChange, onAction, empty, }) {
|
|
43
|
+
const copy = useUiCopy();
|
|
44
|
+
const density = useDensity();
|
|
45
|
+
const pad = byDensity(density, "py-1.5 pe-2", "py-0.5 pe-1.5");
|
|
46
|
+
const many = selectionMode === "multiple";
|
|
47
|
+
const onSelectionChange = (keys) => onSelectedChange?.(keys === "all" ? choosable(items) : [...keys].map(String));
|
|
48
|
+
const render = (node, level) => {
|
|
49
|
+
const children = node.children ?? [];
|
|
50
|
+
const group = children.length > 0;
|
|
51
|
+
const Icon = node.icon;
|
|
52
|
+
/* Each level is one indent step; the first one keeps a sliver of the row's own padding so
|
|
53
|
+
the chevron is not glued to the edge of its tint. */
|
|
54
|
+
const indent = {
|
|
55
|
+
paddingInlineStart: `calc(var(--spacing) * 1 + var(--spacing-indent) * ${level - 1})`,
|
|
56
|
+
};
|
|
57
|
+
return (_jsxs(AriaTreeItem, { id: node.id, textValue: node.label, isDisabled: node.disabled === true, className: ({ isSelected, isHovered, isFocusVisible }) => cn(row({ isSelected, isHovered, isFocusVisible }), pad), style: indent, children: [_jsx(TreeItemContent, { children: ({ isExpanded, isSelected }) => (_jsxs(_Fragment, { children: [group ? (_jsx(AriaButton, { slot: "chevron", className: ({ isFocusVisible }) => chevronButton({ isFocusVisible }), children: _jsx(ChevronRight, { className: `size-(--size-icon-sm) transition-transform duration-(--duration-quick) ease-(--ease-out) ${isExpanded ? "rotate-90" : ""}`, strokeWidth: 2, "aria-hidden": true }) })) : (_jsx("span", { "aria-hidden": true, className: "size-(--spacing-indent) shrink-0" })), _jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-1.5", children: [many ? _jsx(SelectionCheck, {}) : null, Icon === undefined ? null : (_jsx(Icon, { className: "size-(--size-icon-md) shrink-0 text-muted", strokeWidth: 2, "aria-hidden": true })), _jsx("span", { className: cn("min-w-0 flex-1 truncate", group ? "font-medium" : "", isSelected ? "font-medium" : "", node.disabled === true && selectionMode !== "none" ? "text-muted" : ""), children: node.label }), node.meta === undefined ? null : (_jsx("span", { className: "shrink-0 ps-3 text-right tabular-nums whitespace-nowrap text-muted", children: node.meta })), selectionMode === "single" && isSelected ? (_jsx(Check, { className: "size-(--size-icon-sm) shrink-0 text-primary forced-colors:text-[HighlightText]", strokeWidth: 2.5, "aria-hidden": true })) : null] })] })) }), children.map((child) => render(child, level + 1))] }, node.id));
|
|
58
|
+
};
|
|
59
|
+
return (_jsx(AriaLocale, { children: _jsx(AriaTree, { ...(labelledBy === undefined
|
|
60
|
+
? { "aria-label": label }
|
|
61
|
+
: { "aria-labelledby": labelledBy }), selectionMode: selectionMode,
|
|
62
|
+
/* One to choose: a click chooses and a double click opens. Many: the click opens and
|
|
63
|
+
the check chooses. See the header. */
|
|
64
|
+
selectionBehavior: selectionMode === "single" ? "replace" : "toggle", disabledBehavior: "selection", escapeKeyBehavior: "none", ...(selected === undefined ? {} : { selectedKeys: selected }), ...(defaultSelected === undefined ? {} : { defaultSelectedKeys: defaultSelected }), ...(expanded === undefined ? {} : { expandedKeys: expanded }), ...(defaultExpanded === undefined ? {} : { defaultExpandedKeys: defaultExpanded }), ...(onExpandedChange === undefined
|
|
65
|
+
? {}
|
|
66
|
+
: { onExpandedChange: (keys) => onExpandedChange([...keys].map(String)) }), ...(onSelectedChange === undefined ? {} : { onSelectionChange }), ...(onAction === undefined ? {} : { onAction: (key) => onAction(String(key)) }), renderEmptyState: () => (_jsx("p", { className: "px-2 py-3 text-meta text-muted", children: empty ?? copy.tree.empty })), className: "flex w-full min-w-0 flex-col gap-px outline-0", children: items.map((item) => render(item, 1)) }) }));
|
|
67
|
+
}
|
|
68
|
+
/* The check of a row when many can be chosen. It is React Aria's `selection` slot: it toggles
|
|
69
|
+
the row and never opens it. Drawn like `Checkbox`'s box so a check reads as one thing. */
|
|
70
|
+
function SelectionCheck() {
|
|
71
|
+
return (_jsx(AriaCheckbox, { slot: "selection", className: "group/check inline-flex shrink-0 outline-0", children: ({ isSelected, isFocusVisible }) => (_jsx("span", { "aria-hidden": true, className: cn("inline-flex size-4 items-center justify-center rounded-mark border transition-colors duration-(--duration-instant) forced-color-adjust-none", isSelected
|
|
72
|
+
? "border-primary bg-primary text-surface forced-colors:border-[Highlight] forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]"
|
|
73
|
+
: "border-line-strong bg-surface text-transparent forced-colors:border-[ButtonBorder]", focusRing({ isFocusVisible })), children: _jsx(Check, { className: "size-3", strokeWidth: 2.5 }) })) }));
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Tree, type TreeItem, type TreeProps } from "./Tree.js";
|
|
@@ -34,4 +34,15 @@ export type CopyableProps = {
|
|
|
34
34
|
* el valor ya está dibujado. */
|
|
35
35
|
bare?: boolean;
|
|
36
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* The button alone, for a value that is only known when it is pressed — what a field shows
|
|
39
|
+
* at that moment (`Field`'s copy button reads it from the screen). `Copyable` is this
|
|
40
|
+
* button next to its value.
|
|
41
|
+
*/
|
|
42
|
+
export declare function CopyButton({ value, label, }: {
|
|
43
|
+
/** The text, or how to get it at the moment of the press. */
|
|
44
|
+
value: string | (() => string);
|
|
45
|
+
/** Its name at rest. Defaults to the copy table's «Copiar». */
|
|
46
|
+
label?: string;
|
|
47
|
+
}): import("react").JSX.Element;
|
|
37
48
|
export declare function Copyable({ value, children, mono, bare }: CopyableProps): import("react").JSX.Element;
|
|
@@ -6,7 +6,12 @@ import { useEffect, useRef, useState } from "react";
|
|
|
6
6
|
/** Cuánto dura el tilde. Dos segundos: alcanza para verlo sin que el botón se
|
|
7
7
|
* quede en un estado que ya no es cierto. */
|
|
8
8
|
const CONFIRMED_FOR = 2000;
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The button alone, for a value that is only known when it is pressed — what a field shows
|
|
11
|
+
* at that moment (`Field`'s copy button reads it from the screen). `Copyable` is this
|
|
12
|
+
* button next to its value.
|
|
13
|
+
*/
|
|
14
|
+
export function CopyButton({ value, label, }) {
|
|
10
15
|
const copy = useUiCopy();
|
|
11
16
|
const [state, setState] = useState("idle");
|
|
12
17
|
const timer = useRef(null);
|
|
@@ -19,8 +24,15 @@ export function Copyable({ value, children, mono = false, bare = false }) {
|
|
|
19
24
|
async function run() {
|
|
20
25
|
if (timer.current !== null)
|
|
21
26
|
clearTimeout(timer.current);
|
|
27
|
+
const text = typeof value === "function" ? value() : value;
|
|
28
|
+
if (text.trim() === "") {
|
|
29
|
+
/* Nothing on screen to copy: said, not a check mark over an empty clipboard. */
|
|
30
|
+
setState("empty");
|
|
31
|
+
timer.current = setTimeout(() => setState("idle"), CONFIRMED_FOR);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
22
34
|
try {
|
|
23
|
-
await navigator.clipboard.writeText(
|
|
35
|
+
await navigator.clipboard.writeText(text);
|
|
24
36
|
setState("done");
|
|
25
37
|
}
|
|
26
38
|
catch {
|
|
@@ -30,16 +42,25 @@ export function Copyable({ value, children, mono = false, bare = false }) {
|
|
|
30
42
|
}
|
|
31
43
|
timer.current = setTimeout(() => setState("idle"), CONFIRMED_FOR);
|
|
32
44
|
}
|
|
33
|
-
const
|
|
45
|
+
const said = state === "done"
|
|
34
46
|
? copy.copyable.done
|
|
35
47
|
: state === "failed"
|
|
36
48
|
? copy.copyable.failed
|
|
37
|
-
:
|
|
38
|
-
|
|
49
|
+
: state === "empty"
|
|
50
|
+
? copy.copyable.empty
|
|
51
|
+
: (label ?? copy.copyable.label);
|
|
52
|
+
return (_jsx("button", { type: "button", onClick: (e) => {
|
|
53
|
+
/* Inside a row that opens or a link: copying is not opening. */
|
|
54
|
+
e.stopPropagation();
|
|
55
|
+
void run();
|
|
56
|
+
}, "aria-label": said, title: said, className: `inline-flex shrink-0 items-center justify-center rounded-control p-0.5 transition-colors duration-(--duration-instant) ${state === "done"
|
|
39
57
|
? "text-good"
|
|
40
|
-
: state === "failed"
|
|
58
|
+
: state === "failed" || state === "empty"
|
|
41
59
|
? "text-bad"
|
|
42
60
|
: "text-muted hover:bg-ink/6 hover:text-ink"}`, children: state === "done" ? (_jsx(Check, { className: "size-(--size-icon-sm)", strokeWidth: 2.5 })) : (_jsx(Copy, { className: "size-(--size-icon-sm)", strokeWidth: 2 })) }));
|
|
61
|
+
}
|
|
62
|
+
export function Copyable({ value, children, mono = false, bare = false }) {
|
|
63
|
+
const button = _jsx(CopyButton, { value: value });
|
|
43
64
|
if (bare)
|
|
44
65
|
return button;
|
|
45
66
|
return (
|
|
@@ -61,12 +61,21 @@ export function formatDateTime(when) {
|
|
|
61
61
|
const locale = currentCopyLocale();
|
|
62
62
|
return `${dateFormat(locale).format(when)} ${timeFormat(locale).format(when)}`;
|
|
63
63
|
}
|
|
64
|
+
/** A day written as `YYYY-MM-DD` (what a date field stores) is that day HERE. `new Date()`
|
|
65
|
+
* reads it as midnight in UTC, and west of Greenwich that is the evening before: a due date
|
|
66
|
+
* of the 15th was drawn as the 14th in La Paz. Anything else goes through `Date` as is. */
|
|
67
|
+
function toDate(when) {
|
|
68
|
+
const day = typeof when === "string" ? /^(\d{4})-(\d{2})-(\d{2})$/.exec(when) : null;
|
|
69
|
+
if (day === null)
|
|
70
|
+
return new Date(when);
|
|
71
|
+
return new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3]));
|
|
72
|
+
}
|
|
64
73
|
export function DateValue({ when, format = "relative",
|
|
65
74
|
/** So it can be tested: without this, a story with «hace 2 min» changes its
|
|
66
75
|
* text every minute and no snapshot works as a reference. */
|
|
67
76
|
now, }) {
|
|
68
77
|
const copy = useUiCopy();
|
|
69
|
-
const d = when instanceof Date ? when :
|
|
78
|
+
const d = when instanceof Date ? when : toDate(when);
|
|
70
79
|
if (Number.isNaN(d.getTime())) {
|
|
71
80
|
return (_jsx("span", { className: "text-meta text-muted italic", title: copy.time.invalid, children: copy.time.noDate }));
|
|
72
81
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Tone } from "../../foundations/types.js";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export type ProgressBarProps = {
|
|
4
|
+
/** Where the task is, from `minValue` to `maxValue`. Absent = under way and it cannot say
|
|
5
|
+
* how far: the bar sweeps and draws no figure. */
|
|
6
|
+
value?: number;
|
|
7
|
+
/** What is running: «Importación de clientes», «Cierre de octubre». Always its accessible
|
|
8
|
+
* name; drawn above the bar only with `showLabel`. */
|
|
9
|
+
label: string;
|
|
10
|
+
minValue?: number;
|
|
11
|
+
/** The size of the task — 4000 for an import of 4.000 rows. 100 by default, so a plain
|
|
12
|
+
* percentage needs nothing else. */
|
|
13
|
+
maxValue?: number;
|
|
14
|
+
/** How the figure is written. Absent: the percentage of the task. */
|
|
15
|
+
formatOptions?: Intl.NumberFormatOptions;
|
|
16
|
+
/** The figure written by hand, when a percentage does not say it: «1.240 de 4.000 filas».
|
|
17
|
+
* It is also what a screen reader reads. Indeterminate, it is the only figure drawn —
|
|
18
|
+
* «leyendo el archivo…». */
|
|
19
|
+
valueLabel?: ReactNode;
|
|
20
|
+
/** Draws the label above the bar: the bar a screen is about (an import, the closing of
|
|
21
|
+
* the month). Off in a cell or a row, where the column or the line already says it. */
|
|
22
|
+
showLabel?: boolean;
|
|
23
|
+
/** Off when the figure is already beside it — a cell with its column. */
|
|
24
|
+
showValue?: boolean;
|
|
25
|
+
/** The width of the track (inline) or of the whole piece (with `showLabel`). A bar in a
|
|
26
|
+
* list row is shorter than the bar of an import. */
|
|
27
|
+
width?: string;
|
|
28
|
+
/** The state of the task. `brand` — the gradient — is the long bar of a live process. */
|
|
29
|
+
tone?: Tone | "brand";
|
|
30
|
+
};
|
|
31
|
+
export declare function ProgressBar({ value, label, minValue, maxValue, formatOptions, valueLabel, showLabel, showValue, width, tone, }: ProgressBarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { AriaLocale } from "../../foundations/aria.js";
|
|
4
|
+
import { cn } from "../../foundations/variants.js";
|
|
5
|
+
import { ProgressBar as AriaProgressBar } from "react-aria-components";
|
|
6
|
+
/**
|
|
7
|
+
* A task under way: the import of 4.000 rows, the closing of the month, a file going up. A
|
|
8
|
+
* real bar, never a spinner — a spinner says «wait» and does not say how much; a bar says both.
|
|
9
|
+
*
|
|
10
|
+
* Context: it looks like `Meter` and it is the opposite thing. A meter is a QUANTITY in a
|
|
11
|
+
* range that just is — the stock against the warehouse, the credit used — and full is often
|
|
12
|
+
* the alarm. A progress bar is something HAPPENING, moving toward done, and full is the good
|
|
13
|
+
* news. That is why their color comes from different places: the meter's is the reading of
|
|
14
|
+
* its value; this one's is the state of the task, said by whoever runs it — `good` finished,
|
|
15
|
+
* `warn` is running slow, `bad` failed — and `brand` by default, the blue→gold gradient of
|
|
16
|
+
* the long bar of a live import. (At 6 px and 62 % in a list row a gradient reads as a
|
|
17
|
+
* broken bar, so a short bar there asks for a flat tone.)
|
|
18
|
+
*
|
|
19
|
+
* ── Determinate and indeterminate ─────────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* With a `value` it fills to it, and the figure next to it is in tabular figures so a column
|
|
22
|
+
* of bars keeps its digits aligned. Without one the task is under way and cannot say how far
|
|
23
|
+
* — the server has not counted the rows yet — and the bar SAYS so instead of inventing a
|
|
24
|
+
* number: a segment sweeps along the track and no figure is drawn. That sweep is the only
|
|
25
|
+
* loop of the system (`--animate-progress-sweep`), and it runs behind `motion-safe:`: with
|
|
26
|
+
* `prefers-reduced-motion` the segment stands still, the whole track faded — «busy» without
|
|
27
|
+
* movement, and without a fill that could be read as a percentage.
|
|
28
|
+
*
|
|
29
|
+
* React Aria writes `role="progressbar"` and the `aria-value*` — no `aria-valuenow` while it
|
|
30
|
+
* is indeterminate, which is how a screen reader tells the two apart — and formats the figure
|
|
31
|
+
* in the copy table's locale (`AriaLocale`).
|
|
32
|
+
*
|
|
33
|
+
* The track is the system's `--size-track`: at 3 px it was a line that could not be told
|
|
34
|
+
* from a separator; two bars of different thickness on one screen read as two things.
|
|
35
|
+
*/
|
|
36
|
+
const FILL = {
|
|
37
|
+
neutral: "bg-muted",
|
|
38
|
+
info: "bg-info",
|
|
39
|
+
good: "bg-good",
|
|
40
|
+
warn: "bg-warn",
|
|
41
|
+
bad: "bg-bad",
|
|
42
|
+
accent: "bg-accent",
|
|
43
|
+
};
|
|
44
|
+
export function ProgressBar({ value, label, minValue = 0, maxValue = 100, formatOptions, valueLabel, showLabel = false, showValue = true, width = "w-24", tone = "brand", }) {
|
|
45
|
+
const fill = tone === "brand" ? "bar-brand" : FILL[tone];
|
|
46
|
+
return (_jsx(AriaLocale, { children: _jsx(AriaProgressBar, { "aria-label": label, ...(value === undefined ? {} : { value }), isIndeterminate: value === undefined, minValue: minValue, maxValue: maxValue, formatOptions: formatOptions, valueLabel: valueLabel, className: showLabel
|
|
47
|
+
? `flex max-w-full flex-col gap-1.5 ${width}`
|
|
48
|
+
: "flex min-w-0 items-center gap-2", children: ({ percentage, valueText, isIndeterminate }) => {
|
|
49
|
+
const figure = showValue && valueText !== undefined ? (_jsx("span", { className: "shrink-0 text-meta tabular-nums text-muted", children: valueText })) : null;
|
|
50
|
+
const track = (_jsx("span", { className: cn("relative block h-(--size-track) overflow-hidden rounded-pill bg-ink/10 forced-colors:outline forced-colors:outline-[ButtonBorder]", showLabel ? "w-full" : width), children: isIndeterminate ? (_jsx("span", { "data-indeterminate": true, className: cn("absolute inset-y-0 left-0 w-2/5 rounded-pill forced-colors:bg-[Highlight]", "motion-safe:animate-progress-sweep motion-reduce:w-full motion-reduce:opacity-40", fill) })) : (_jsx("span", { className: cn("block h-full rounded-pill transition-[width] duration-(--duration-panel) ease-(--ease-out) forced-colors:bg-[Highlight]", fill), style: { width: `${percentage ?? 0}%` } })) }));
|
|
51
|
+
return showLabel ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "flex items-baseline justify-between gap-2", children: [_jsx("span", { className: "truncate text-meta text-ink", children: label }), figure] }), track] })) : (_jsxs(_Fragment, { children: [track, figure] }));
|
|
52
|
+
} }) }));
|
|
53
|
+
}
|
|
@@ -55,6 +55,11 @@ export type RelationValueProps = {
|
|
|
55
55
|
href?: string;
|
|
56
56
|
/** Present = an info button next to the name opens the `RelationCard`. */
|
|
57
57
|
summary?: RelationSummary;
|
|
58
|
+
/** Floats the summary from the NAME on hover, focus or long press, instead of from the
|
|
59
|
+
* info button. `true` = the `RelationCard` (with `summary` when given); a node = that
|
|
60
|
+
* content instead — the product of a line, a consumer's own card. Needs the name to be a
|
|
61
|
+
* link (`href` or `onOpen`); without one the content opens from the info button. */
|
|
62
|
+
preview?: boolean | ReactNode;
|
|
58
63
|
/** The control that EDITS this relation — a `Picker` — drawn instead in edit mode. */
|
|
59
64
|
editor?: ReactNode;
|
|
60
65
|
/** Forces the mode; by default the form's. Only matters with `editor`. */
|
|
@@ -65,5 +70,5 @@ export type RelationValueProps = {
|
|
|
65
70
|
* picture, and its summary one press away. Works in a table cell, a list row, a `Record.Facts`
|
|
66
71
|
* value and a form field (read mode; pass the `Picker` as `editor` for edit mode).
|
|
67
72
|
*/
|
|
68
|
-
export declare function RelationValue({ name, secondary, media, onOpen, href, summary, editor, mode, }: RelationValueProps): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
|
|
73
|
+
export declare function RelationValue({ name, secondary, media, onOpen, href, summary, preview, editor, mode, }: RelationValueProps): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
|
|
69
74
|
export {};
|
|
@@ -7,6 +7,7 @@ import { CellStack, Thumb } from "../cell/CellStack.js";
|
|
|
7
7
|
import { FieldIdsContext } from "../field/context.js";
|
|
8
8
|
import { useFormMode } from "../field/mode.js";
|
|
9
9
|
import { PopoverTrigger } from "../overlay/PopoverTrigger.js";
|
|
10
|
+
import { Preview } from "../overlay/Preview.js";
|
|
10
11
|
import { Tag } from "../tag/Tag.js";
|
|
11
12
|
import { ArrowRight, Info } from "lucide-react";
|
|
12
13
|
import { useContext } from "react";
|
|
@@ -32,7 +33,7 @@ export function RelationCard({ name, secondary, media, summary = {}, onOpen, hre
|
|
|
32
33
|
* picture, and its summary one press away. Works in a table cell, a list row, a `Record.Facts`
|
|
33
34
|
* value and a form field (read mode; pass the `Picker` as `editor` for edit mode).
|
|
34
35
|
*/
|
|
35
|
-
export function RelationValue({ name, secondary, media, onOpen, href, summary, editor, mode, }) {
|
|
36
|
+
export function RelationValue({ name, secondary, media, onOpen, href, summary, preview, editor, mode, }) {
|
|
36
37
|
const copy = useUiCopy();
|
|
37
38
|
const formMode = useFormMode(mode);
|
|
38
39
|
/* Inside a `Field` the value takes the control's height, so a form that goes from
|
|
@@ -45,13 +46,29 @@ export function RelationValue({ name, secondary, media, onOpen, href, summary, e
|
|
|
45
46
|
return frame(_jsx("span", { className: "text-meta text-muted italic", children: copy.relation.none }));
|
|
46
47
|
}
|
|
47
48
|
const goes = href !== undefined || onOpen !== undefined;
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
const summaryLabel = fillCopy(copy.relationCard.summary, { name });
|
|
50
|
+
const card = (open) => (_jsx(RelationCard, { name: name, ...(summary === undefined ? {} : { summary }), ...(secondary === undefined ? {} : { secondary }), ...(media === undefined ? {} : { media }), ...(href === undefined ? {} : { href }), ...(open === undefined ? {} : { onOpen: open }) }));
|
|
51
|
+
/* What floats from the name: the card, or the consumer's own content. */
|
|
52
|
+
const previewed = preview === undefined || preview === null || preview === false
|
|
53
|
+
? undefined
|
|
54
|
+
: preview === true
|
|
55
|
+
? card(onOpen)
|
|
56
|
+
: preview;
|
|
57
|
+
const link = goes ? (_jsx(Link, { ...(href === undefined ? {} : { href }), ...(onOpen === undefined ? {} : { onPress: onOpen }), className: ({ isFocusVisible }) => cn("cursor-pointer rounded-mark text-primary no-underline underline-offset-2 hover:underline", focusRing({ isFocusVisible })), children: name })) : undefined;
|
|
58
|
+
const primary = link === undefined ? (name) : previewed === undefined ? (link) : (_jsx(Preview, { trigger: link, label: summaryLabel, children: previewed }));
|
|
59
|
+
/* The `i`: the summary when there is no preview on the name; the preview's content when
|
|
60
|
+
there is no link for it to ride on. */
|
|
61
|
+
const info = previewed !== undefined
|
|
62
|
+
? goes
|
|
63
|
+
? undefined
|
|
64
|
+
: () => previewed
|
|
65
|
+
: summary === undefined
|
|
66
|
+
? undefined
|
|
67
|
+
: (close) => card(onOpen === undefined
|
|
68
|
+
? undefined
|
|
69
|
+
: () => {
|
|
70
|
+
close();
|
|
71
|
+
onOpen();
|
|
72
|
+
});
|
|
73
|
+
return frame(_jsxs("span", { className: "inline-flex max-w-full min-w-0 items-center gap-1", children: [_jsx(CellStack, { primary: primary, ...(secondary === undefined ? {} : { secondary }), ...(media === undefined ? {} : { media }) }), info === undefined ? null : (_jsx(PopoverTrigger, { label: summaryLabel, trigger: _jsx(IconButton, { icon: Info, label: summaryLabel, size: "sm" }), children: info }))] }));
|
|
57
74
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
2
|
/**
|
|
3
3
|
* The money summary of a document — subtotal, taxes, discounts and the TOTAL — under its
|
|
4
|
-
* lines, aligned to the right.
|
|
4
|
+
* lines, aligned to the right. And, once the document starts being settled, what was already
|
|
5
|
+
* taken off it, what is still owed, and what could still be taken off.
|
|
5
6
|
*
|
|
6
7
|
* Context: written as two more rows of the lines table, «IVA» and «Total» were the same size
|
|
7
8
|
* and weight as a product line, and the one number the document exists for had to be found.
|
|
@@ -10,17 +11,111 @@ import type { ReactNode } from "react";
|
|
|
10
11
|
* read (Jakob's law).
|
|
11
12
|
*
|
|
12
13
|
* Labels left and figures right of a narrow column, both in tabular figures, so the amounts
|
|
13
|
-
* line up digit by digit. The `total` row closes the
|
|
14
|
-
*
|
|
14
|
+
* line up digit by digit. The `total` row closes the breakdown: a rule above it.
|
|
15
|
+
*
|
|
16
|
+
* ── Deductions, balance and credits: one block, not three ────────────
|
|
17
|
+
*
|
|
18
|
+
* A posted invoice is read as one sum: the total, minus each payment, gives what is owed. Drawn
|
|
19
|
+
* as a second component under this one, the payments would be a second column of figures that
|
|
20
|
+
* the screen has to line up with the first by hand — which a template cannot do without
|
|
21
|
+
* styling — and the decision below, WHICH figure leads, needs to see the three at once. So it
|
|
22
|
+
* is this block, extended, and a block with only `rows` draws exactly what it drew before.
|
|
23
|
+
*
|
|
24
|
+
* It still does not know what an invoice is. It speaks of `deductions` (a payment, a credit
|
|
25
|
+
* note, a retention: anything already applied against the total), a `balance` (what remains
|
|
26
|
+
* after them), `settled` (the word for «nothing remains»: «Pagada», «Conciliado») and
|
|
27
|
+
* `credits` (what could still be applied). The domain formats the money and owns the actions;
|
|
28
|
+
* this only lays them out and names them.
|
|
29
|
+
*
|
|
30
|
+
* ── The figure that leads moves with the state ───────────────────────
|
|
31
|
+
*
|
|
32
|
+
* There is ONE figure at the title size, and which one it is depends on where the document is:
|
|
33
|
+
*
|
|
34
|
+
* nothing applied yet (a draft, a posted invoice) the total
|
|
35
|
+
* something applied, something owed the balance — the total steps down
|
|
36
|
+
* settled the total, with the `settled` mark
|
|
37
|
+
*
|
|
38
|
+
* Once a payment is in, the question the person brings is «how much is left?», and a total
|
|
39
|
+
* at the title size next to a balance at the body size answers the wrong question first.
|
|
40
|
+
* That is the default; `lead` says it explicitly when the screen knows better.
|
|
41
|
+
*
|
|
42
|
+
* ── A deduction is HEARD as one: «Pagado el 12/10/2026, menos Bs. 1.000,00» ──
|
|
43
|
+
*
|
|
44
|
+
* The amount arrives formatted and WITHOUT a sign; the block writes the minus. The visible
|
|
45
|
+
* «−» is hidden from assistive technology — screen readers skip it or say «dash» — and a
|
|
46
|
+
* visually hidden «menos» (`copy.totals.less`) is read in its place, so the label and the
|
|
47
|
+
* figure are one sentence and not two loose numbers.
|
|
48
|
+
*
|
|
49
|
+
* ── The detail of a deduction: an `i`, asked for ─────────────────────
|
|
50
|
+
*
|
|
51
|
+
* Where it came from (journal, reference, method), going to it, and unlinking it open from an
|
|
52
|
+
* `i` before the label — `PopoverTrigger`, the same `i` a `RelationValue` uses for its
|
|
53
|
+
* summary. Not a `Preview` on hover: the card holds an action that changes the document
|
|
54
|
+
* («Desvincular»), and that is asked for with a press, which the mouse, the keyboard and the
|
|
55
|
+
* finger all have. The `i` sits BEFORE the label so the label and its amount stay together
|
|
56
|
+
* in the reading order.
|
|
57
|
+
*
|
|
58
|
+
* ── None of these actions is the primary ─────────────────────────────
|
|
59
|
+
*
|
|
60
|
+
* The screen's one primary is somewhere else («Registrar pago», «Confirmar»). Unlinking is
|
|
61
|
+
* `secondary` and not `danger`: it does not destroy — the payment comes back as a credit and
|
|
62
|
+
* is applied again with one press, which is the undo. «Aplicar» and «Abrir» are `ghost`.
|
|
15
63
|
*/
|
|
16
64
|
export type TotalsRow = {
|
|
17
65
|
label: ReactNode;
|
|
18
66
|
value: ReactNode;
|
|
19
67
|
/** A note under the label — «incluido en el precio», «sobre Bs. 3.601,80». */
|
|
20
68
|
detail?: ReactNode;
|
|
21
|
-
/** The row
|
|
69
|
+
/** The row that closes the breakdown. Usually one, and last. */
|
|
22
70
|
total?: boolean;
|
|
23
71
|
};
|
|
24
|
-
|
|
72
|
+
/** A fact of a deduction's detail: «Diario · Banco BNB», «Referencia · TRF-88213». */
|
|
73
|
+
export type TotalsFact = {
|
|
74
|
+
label: string;
|
|
75
|
+
value: ReactNode;
|
|
76
|
+
};
|
|
77
|
+
/** Something already applied against the total: a payment, a credit note, a retention. */
|
|
78
|
+
export type TotalsDeduction = {
|
|
79
|
+
/** What it is, as one phrase — «Pagado el 12/10/2026». A string: it also names the `i`
|
|
80
|
+
* and the floating detail. */
|
|
81
|
+
label: string;
|
|
82
|
+
/** The amount taken off, formatted and WITHOUT a sign: the block writes the minus. */
|
|
83
|
+
value: ReactNode;
|
|
84
|
+
/** A quiet note under the label — «Transferencia · Banco BNB». */
|
|
85
|
+
detail?: ReactNode;
|
|
86
|
+
/** What the `i` shows: two to five facts — journal, reference, method. */
|
|
87
|
+
facts?: readonly TotalsFact[];
|
|
88
|
+
/** Go to the applied record. The detail shows «Abrir». */
|
|
89
|
+
onOpen?: () => void;
|
|
90
|
+
/** Undo the application — «Desvincular». Present = allowed; absent = not offered. */
|
|
91
|
+
onRemove?: () => void;
|
|
92
|
+
};
|
|
93
|
+
/** Something that COULD be applied and is not yet: an unused credit of the same party. */
|
|
94
|
+
export type TotalsCredit = {
|
|
95
|
+
/** What it is — «NC/2026/0012». A string: it also names its «Aplicar». */
|
|
96
|
+
label: string;
|
|
97
|
+
/** The amount available, formatted. */
|
|
98
|
+
value: ReactNode;
|
|
99
|
+
/** A quiet note — «del 02/10/2026», «anticipo». */
|
|
100
|
+
detail?: ReactNode;
|
|
101
|
+
onApply: () => void;
|
|
102
|
+
};
|
|
103
|
+
export type TotalsProps = {
|
|
104
|
+
/** The breakdown and its `total`: subtotal, each tax group, discounts, TOTAL. */
|
|
25
105
|
rows: readonly TotalsRow[];
|
|
26
|
-
|
|
106
|
+
/** What was already applied against the total, one row each, after it. */
|
|
107
|
+
deductions?: readonly TotalsDeduction[];
|
|
108
|
+
/** What remains once the deductions are taken off — «Importe adeudado». */
|
|
109
|
+
balance?: Omit<TotalsRow, "total">;
|
|
110
|
+
/** Nothing remains: the word for it, drawn as a mark next to the leading figure —
|
|
111
|
+
* «Pagada». */
|
|
112
|
+
settled?: string;
|
|
113
|
+
/** Which figure leads, at the title size. By default the balance when something was
|
|
114
|
+
* deducted and the document is not settled; otherwise the total. */
|
|
115
|
+
lead?: "total" | "balance";
|
|
116
|
+
/** What could still be applied, apart and under the sum: it is not part of it. */
|
|
117
|
+
credits?: readonly TotalsCredit[];
|
|
118
|
+
/** The title of the credits. `copy.totals.credits` by default. */
|
|
119
|
+
creditsLabel?: string;
|
|
120
|
+
};
|
|
121
|
+
export declare function Totals({ rows, deductions, balance, settled, lead, credits, creditsLabel, }: TotalsProps): import("react").JSX.Element;
|
|
@@ -1,4 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { Button, IconButton } from "../button/Button.js";
|
|
5
|
+
import { PopoverTrigger } from "../overlay/PopoverTrigger.js";
|
|
6
|
+
import { Tag } from "../tag/Tag.js";
|
|
7
|
+
import { ArrowRight, Info, Plus, Unlink } from "lucide-react";
|
|
8
|
+
import { useId } from "react";
|
|
9
|
+
/** The figure the block exists for: a rule above it and the figure at the title size. The
|
|
10
|
+
* `data-lead` marks it for the tests and for whoever reads the DOM. */
|
|
11
|
+
function LeadRow({ row, mark }) {
|
|
12
|
+
return (_jsxs("div", { "data-lead": "", className: "mt-1 flex flex-wrap items-baseline justify-between gap-x-4 border-t border-line-strong pt-2.5", children: [_jsxs("dt", { className: "flex min-w-0 flex-col", children: [_jsxs("span", { className: "flex flex-wrap items-center gap-2 text-body font-semibold text-ink", children: [row.label, mark === undefined ? null : _jsx(Tag, { tone: "good", children: mark })] }), row.detail === undefined ? null : (_jsx("span", { className: "text-micro text-muted", children: row.detail }))] }), _jsx("dd", { className: "ml-auto text-title font-semibold whitespace-nowrap tabular-nums text-ink", children: row.value })] }));
|
|
13
|
+
}
|
|
14
|
+
/** A figure that closes a part of the sum without leading it: the total once the balance
|
|
15
|
+
* leads, or the balance while the total does. Ruled, in the body size. */
|
|
16
|
+
function ClosingRow({ row }) {
|
|
17
|
+
return (_jsxs("div", { className: "mt-1 flex flex-wrap items-baseline justify-between gap-x-4 border-t border-line pt-2", children: [_jsxs("dt", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "text-body font-semibold text-ink", children: row.label }), row.detail === undefined ? null : (_jsx("span", { className: "text-micro text-muted", children: row.detail }))] }), _jsx("dd", { className: "ml-auto text-body font-semibold whitespace-nowrap tabular-nums text-ink", children: row.value })] }));
|
|
18
|
+
}
|
|
19
|
+
function PlainRow({ row }) {
|
|
20
|
+
return (_jsxs("div", { className: "flex items-baseline justify-between gap-4", children: [_jsxs("dt", { className: "flex min-w-0 flex-col text-meta text-muted", children: [row.label, row.detail === undefined ? null : (_jsx("span", { className: "text-micro text-muted", children: row.detail }))] }), _jsx("dd", { className: "text-meta whitespace-nowrap tabular-nums text-ink", children: row.value })] }));
|
|
21
|
+
}
|
|
22
|
+
/** What floats from a deduction's `i`: its facts and, when allowed, «Desvincular» and
|
|
23
|
+
* «Abrir». Both close the card first — the row may be gone once the action runs. */
|
|
24
|
+
function DeductionDetail({ item, close }) {
|
|
25
|
+
const copy = useUiCopy();
|
|
26
|
+
const facts = item.facts ?? [];
|
|
27
|
+
const run = (action) => () => {
|
|
28
|
+
close();
|
|
29
|
+
action();
|
|
30
|
+
};
|
|
31
|
+
return (_jsxs("div", { className: "flex w-64 max-w-full flex-col gap-3", children: [facts.length === 0 ? null : (_jsx("dl", { className: "grid grid-cols-2 gap-x-4 gap-y-2", children: facts.map((fact) => (_jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("dt", { className: "truncate text-micro text-muted", children: fact.label }), _jsx("dd", { className: "text-meta tabular-nums break-words text-ink", children: fact.value })] }, fact.label))) })), item.onRemove === undefined && item.onOpen === undefined ? null : (_jsxs("div", { className: "flex flex-wrap justify-end gap-2 border-t border-line pt-2", children: [item.onRemove === undefined ? null : (_jsx(Button, { role: "secondary", size: "sm", icon: Unlink, onClick: run(item.onRemove), children: copy.totals.unlink })), item.onOpen === undefined ? null : (_jsx(Button, { role: "ghost", size: "sm", icon: ArrowRight, onClick: run(item.onOpen), children: copy.totals.open }))] }))] }));
|
|
32
|
+
}
|
|
33
|
+
function DeductionRow({ item }) {
|
|
34
|
+
const copy = useUiCopy();
|
|
35
|
+
const asks = (item.facts !== undefined && item.facts.length > 0) ||
|
|
36
|
+
item.onRemove !== undefined ||
|
|
37
|
+
item.onOpen !== undefined;
|
|
38
|
+
return (_jsxs("div", { className: "flex items-baseline justify-between gap-4", children: [_jsxs("dt", { className: "flex min-w-0 items-baseline gap-1 text-meta text-muted", children: [asks ? (_jsx(PopoverTrigger, { title: item.label, placement: "bottom end", trigger: _jsx(IconButton, { icon: Info, label: fillCopy(copy.totals.details, { label: item.label }) }), children: (close) => _jsx(DeductionDetail, { item: item, close: close }) })) : null, _jsxs("span", { className: "flex min-w-0 flex-col", children: [item.label, item.detail === undefined ? null : (_jsx("span", { className: "text-micro text-muted", children: item.detail }))] })] }), _jsxs("dd", { className: "text-meta whitespace-nowrap tabular-nums text-ink", children: [_jsx("span", { "aria-hidden": true, children: "− " }), _jsxs("span", { className: "sr-only", children: [copy.totals.less, " "] }), item.value] })] }));
|
|
39
|
+
}
|
|
40
|
+
function Credits({ items, label }) {
|
|
41
|
+
const copy = useUiCopy();
|
|
42
|
+
const id = useId();
|
|
43
|
+
return (_jsxs("div", { className: "flex flex-col gap-1.5 border-t border-line pt-3", children: [_jsx("p", { id: id, className: "text-micro font-semibold text-muted", children: label }), _jsx("ul", { "aria-labelledby": id, className: "flex flex-col gap-1", children: items.map((credit, i) => (_jsxs("li", { className: "flex flex-wrap items-center justify-between gap-x-3 gap-y-1", children: [_jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "text-meta text-ink", children: credit.label }), credit.detail === undefined ? null : (_jsx("span", { className: "text-micro text-muted", children: credit.detail }))] }), _jsxs("span", { className: "ml-auto flex shrink-0 items-center gap-2", children: [_jsx("span", { className: "text-meta whitespace-nowrap tabular-nums text-ink", children: credit.value }), _jsx(Button, { role: "ghost", size: "sm", icon: Plus, "aria-label": fillCopy(copy.totals.applyCredit, { label: credit.label }), onClick: credit.onApply, children: copy.totals.apply })] })] }, i))) })] }));
|
|
44
|
+
}
|
|
45
|
+
export function Totals({ rows, deductions = [], balance, settled, lead, credits = [], creditsLabel, }) {
|
|
46
|
+
const copy = useUiCopy();
|
|
47
|
+
const leads = lead ??
|
|
48
|
+
(balance !== undefined && deductions.length > 0 && settled === undefined
|
|
49
|
+
? "balance"
|
|
50
|
+
: "total");
|
|
51
|
+
/* With nothing to lead it, the balance cannot lead: the total keeps the title size. */
|
|
52
|
+
const balanceLeads = leads === "balance" && balance !== undefined;
|
|
53
|
+
return (_jsxs("div", { className: "ml-auto flex w-full max-w-(--container-search) flex-col gap-3", children: [_jsxs("dl", { className: "flex flex-col gap-1.5", children: [rows.map((row, i) => row.total === true ? (balanceLeads ? (_jsx(ClosingRow, { row: row }, i)) : (_jsx(LeadRow, { row: row, ...(settled === undefined ? {} : { mark: settled }) }, i))) : (_jsx(PlainRow, { row: row }, i))), deductions.map((item, i) => (_jsx(DeductionRow, { item: item }, i))), balance === undefined ? null : balanceLeads ? (_jsx(LeadRow, { row: balance, ...(settled === undefined ? {} : { mark: settled }) })) : (_jsx(ClosingRow, { row: balance }))] }), credits.length === 0 ? null : (_jsx(Credits, { items: credits, label: creditsLabel ?? copy.totals.credits }))] }));
|
|
4
54
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Tone } from "../../foundations/types.js";
|
|
2
1
|
import type { ReactNode } from "react";
|
|
3
2
|
export declare const formatCount: (value: number, locale?: string) => string;
|
|
4
3
|
export declare const formatNumber: (value: number, locale?: string) => string;
|
|
@@ -21,42 +20,6 @@ export declare function Numeric({ value, unit, format, }: {
|
|
|
21
20
|
unit?: string;
|
|
22
21
|
format?: "integer" | "decimal" | "distance";
|
|
23
22
|
}): import("react").JSX.Element;
|
|
24
|
-
/**
|
|
25
|
-
* A real bar, never a spinner.
|
|
26
|
-
*
|
|
27
|
-
* Context: a spinner says «wait» and doesn't say how much; a bar says both
|
|
28
|
-
* things.
|
|
29
|
-
*
|
|
30
|
-
* ── Why the fill is FLAT and the gradient is asked for ──────────────
|
|
31
|
-
*
|
|
32
|
-
* The bar always lived with `bar-brand`, the blue→gold gradient. At 3 px and at
|
|
33
|
-
* 62 % that doesn't read as a brand: it reads as a blue bar with a broken tip.
|
|
34
|
-
* A gradient needs length to be a gradient, and a bar inside a list row doesn't
|
|
35
|
-
* have it.
|
|
36
|
-
*
|
|
37
|
-
* Now the fill is a flat tone —and the tone SAYS something: `good` finished,
|
|
38
|
-
* `warn` is running slow, `bad` failed— and the gradient is asked for with
|
|
39
|
-
* `tone="brand"`, which is the long bar of a live import: there it does have room
|
|
40
|
-
* to be read.
|
|
41
|
-
*
|
|
42
|
-
* And it got fatter, from 3 px to 6 px. At 3 px it isn't a bar, it's a line: it
|
|
43
|
-
* can't be told apart from a separator and the fill isn't visible unless it goes
|
|
44
|
-
* past 20 %.
|
|
45
|
-
*/
|
|
46
|
-
export declare function ProgressBar({ value, label, width, tone,
|
|
47
|
-
/** The percentage next to it. It is turned off when the bar lives inside
|
|
48
|
-
* something that already says the figure —a cell with its column— and it
|
|
49
|
-
* would only mean reading it twice. */
|
|
50
|
-
showValue, }: {
|
|
51
|
-
/** 0 to 100. */
|
|
52
|
-
value: number;
|
|
53
|
-
label: string;
|
|
54
|
-
/** The activity bar wants to be longer than a list row; there is no reason for
|
|
55
|
-
* it to be the same number on both sides. */
|
|
56
|
-
width?: string;
|
|
57
|
-
tone?: Tone | "brand";
|
|
58
|
-
showValue?: boolean;
|
|
59
|
-
}): import("react").JSX.Element;
|
|
60
23
|
/**
|
|
61
24
|
* The content of a data cell, with what the system decides and the view does not.
|
|
62
25
|
*
|