@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
|
@@ -11,10 +11,42 @@ import { useFormMode } from "../field/mode.js";
|
|
|
11
11
|
import { Popover } from "../overlay/Popover.js";
|
|
12
12
|
import { Check, ChevronDown, SquareArrowOutUpRight } from "lucide-react";
|
|
13
13
|
import { useContext, useMemo, useRef, useState } from "react";
|
|
14
|
-
import { ComboBox as AriaComboBox, ComboBoxStateContext, Input, ListBox, ListBoxItem, Text, useFilter, } from "react-aria-components";
|
|
14
|
+
import { Collection, ComboBox as AriaComboBox, ComboBoxStateContext, Header, Input, ListBox, ListBoxItem, ListBoxLoadMoreItem, ListBoxSection, Text, useFilter, } from "react-aria-components";
|
|
15
15
|
/** The id of the «see all…» row. Not a record id: it cannot collide with one. */
|
|
16
16
|
const BROWSE_ALL = "\u0000browse-all";
|
|
17
17
|
const CAP = 8;
|
|
18
|
+
/**
|
|
19
|
+
* What the list shows and how many are left out of it. Capped at 8 with the footer count,
|
|
20
|
+
* unless the list loads incrementally — then everything that arrived is listed and the
|
|
21
|
+
* scroll asks for more.
|
|
22
|
+
*/
|
|
23
|
+
export function shownRows(filtered, { loading, incremental }) {
|
|
24
|
+
if (loading)
|
|
25
|
+
return { visible: [], remaining: 0 };
|
|
26
|
+
if (incremental)
|
|
27
|
+
return { visible: filtered, remaining: 0 };
|
|
28
|
+
const visible = filtered.slice(0, CAP);
|
|
29
|
+
return { visible, remaining: filtered.length - visible.length };
|
|
30
|
+
}
|
|
31
|
+
/** Folds consecutive items of the same `section` into one run under a header. The ids of
|
|
32
|
+
* the runs cannot collide with a record id, like the «see all…» one. */
|
|
33
|
+
function sectioned(items) {
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const item of items) {
|
|
36
|
+
if (item.section === undefined) {
|
|
37
|
+
out.push(item);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const last = out.at(-1);
|
|
41
|
+
if (last !== undefined && "items" in last && last.section === item.section) {
|
|
42
|
+
last.items.push(item);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
out.push({ id: `\u0000section-${out.length}`, section: item.section, items: [item] });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
18
50
|
/* No focus ring on an option: the focus stays in the input (the list is driven from it),
|
|
19
51
|
and the marked row is said by its tint — a ring inside the list as well as around the
|
|
20
52
|
field is two marks for one place. */
|
|
@@ -48,7 +80,7 @@ export function Picker({ mode, ...props }) {
|
|
|
48
80
|
to be ABOVE the component that asks for it. */
|
|
49
81
|
return (_jsx(AriaLocale, { children: _jsx(EditablePicker, { ...props }) }));
|
|
50
82
|
}
|
|
51
|
-
function EditablePicker({ id: ownId, items, value, onChange, placeholder, onBrowseAll, onOpen, emptyText, searchPlaceholder, loading = false, loadingText, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, }) {
|
|
83
|
+
function EditablePicker({ id: ownId, items, value, onChange, placeholder, onBrowseAll, onOpen, emptyText, searchPlaceholder, loading = false, loadingText, onLoadMore, loadingMore = false, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, "aria-label": ariaLabel, }) {
|
|
52
84
|
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
53
85
|
id: ownId,
|
|
54
86
|
describedBy: ownDescribedBy,
|
|
@@ -80,8 +112,10 @@ function EditablePicker({ id: ownId, items, value, onChange, placeholder, onBrow
|
|
|
80
112
|
return items;
|
|
81
113
|
return items.filter((i) => contains(`${i.name} ${i.meta ?? ""} ${i.haystack ?? ""}`, q));
|
|
82
114
|
}, [items, query, contains]);
|
|
83
|
-
const visible
|
|
84
|
-
|
|
115
|
+
const { visible, remaining } = shownRows(filtered, {
|
|
116
|
+
loading,
|
|
117
|
+
incremental: onLoadMore !== undefined,
|
|
118
|
+
});
|
|
85
119
|
const hasMarks = items.some((i) => i.icon !== undefined);
|
|
86
120
|
const hasHints = visible.some((i) => i.meta !== undefined);
|
|
87
121
|
/* «See all…» is an OPTION at the end of the list and not a button in a footer: the
|
|
@@ -110,7 +144,7 @@ function EditablePicker({ id: ownId, items, value, onChange, placeholder, onBrow
|
|
|
110
144
|
if (next !== value)
|
|
111
145
|
onChange?.(next);
|
|
112
146
|
};
|
|
113
|
-
return (_jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-describedby": describedBy, items: listed, value: value ?? null, onChange: take, inputValue: text, onInputChange: (next) => {
|
|
147
|
+
return (_jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, items: listed, value: value ?? null, onChange: take, inputValue: text, onInputChange: (next) => {
|
|
114
148
|
setText(next);
|
|
115
149
|
setQuery(next);
|
|
116
150
|
}, onOpenChange: (isOpen, trigger) => {
|
|
@@ -133,9 +167,29 @@ function EditablePicker({ id: ownId, items, value, onChange, placeholder, onBrow
|
|
|
133
167
|
onFocus: (event) => {
|
|
134
168
|
if (!open)
|
|
135
169
|
event.currentTarget.select();
|
|
136
|
-
}, className: "h-full min-w-0 flex-1 bg-transparent px-2.5 text-meta text-ink outline-0 placeholder:text-muted disabled:cursor-not-allowed" }), value !== undefined && value !== "" && onOpen !== undefined ? (_jsx(IconButton, { icon: SquareArrowOutUpRight, label: copy.picker.open, size: "sm", disabled: disabled, onClick: () => onOpen(value) })) : null, _jsx(FieldButton, { className: "mr-1", children: _jsx(ChevronDown, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsxs(Popover, { placement: "bottom start", className: "flex w-(--trigger-width) flex-col overflow-hidden", children: [_jsx(
|
|
137
|
-
|
|
138
|
-
|
|
170
|
+
}, className: "h-full min-w-0 flex-1 bg-transparent px-2.5 text-meta text-ink outline-0 placeholder:text-muted disabled:cursor-not-allowed" }), value !== undefined && value !== "" && onOpen !== undefined ? (_jsx(IconButton, { icon: SquareArrowOutUpRight, label: copy.picker.open, size: "sm", disabled: disabled, onClick: () => onOpen(value) })) : null, _jsx(FieldButton, { className: "mr-1", children: _jsx(ChevronDown, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsxs(Popover, { placement: "bottom start", className: "flex w-(--trigger-width) flex-col overflow-hidden", children: [_jsx(PickerOptions, { items: listed, marks: hasMarks, hints: hasHints, remaining: remaining, emptyText: loading ? shownLoading : shownEmpty,
|
|
171
|
+
/* Not while the first page loads: the sentinel of an empty list is in view and
|
|
172
|
+
would ask for page two before page one arrived. */
|
|
173
|
+
onLoadMore: loading ? undefined : onLoadMore, loadingMore: loadingMore }), remaining > 0 && !browsable ? (_jsxs("div", { className: "flex shrink-0 items-center gap-2 border-t border-line bg-ground px-2.5 py-1.5", children: [_jsx("span", { className: "text-micro tabular-nums text-muted", children: fillCopy(copy.picker.more, { count: remaining.toLocaleString(copy.locale) }) }), _jsx("span", { className: "text-micro text-muted", children: copy.picker.refine })] })) : null] })] }));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The list of a `Picker`: its rows, the section headers, the «see all…» row and the loading
|
|
177
|
+
* row at the end. Internal — not exported from the family — and apart from the combobox so it
|
|
178
|
+
* can be rendered on the server without a popover, which is how the tests see the rows.
|
|
179
|
+
*/
|
|
180
|
+
export function PickerOptions({ items, marks, hints, remaining, emptyText, onLoadMore, loadingMore = false, "aria-label": ariaLabel, }) {
|
|
181
|
+
const copy = useUiCopy();
|
|
182
|
+
const entries = useMemo(() => sectioned(items), [items]);
|
|
183
|
+
const renderItem = (item) => item.id === BROWSE_ALL ? (_jsx(ListBoxItem, { id: BROWSE_ALL, textValue: item.name, className: (state) => `${option({ ...state, hints: false })} mt-1 border-t border-line text-primary`, children: _jsxs("span", { className: "flex w-full items-center justify-between gap-2", children: [_jsx("span", { className: "font-medium", children: item.name }), _jsx("span", { className: "text-micro tabular-nums text-muted", children: fillCopy(copy.picker.more, {
|
|
184
|
+
count: remaining.toLocaleString(copy.locale),
|
|
185
|
+
}) })] }) })) : (_jsx(ListBoxItem, { id: item.id, textValue: item.name, isDisabled: item.disabled, className: (state) => option({ ...state, hints }), children: ({ isSelected }) => (_jsx(Row, { item: item, selected: isSelected, marks: marks, hints: hints })) }));
|
|
186
|
+
return (_jsxs(ListBox, { "aria-label": ariaLabel,
|
|
187
|
+
/* Nothing matched YET while the next page is coming: «nothing matches» above «loading
|
|
188
|
+
more…» says two contradicting things, so the empty list says only the second. */
|
|
189
|
+
renderEmptyState: () => (_jsx("p", { className: "px-3 py-3 text-center text-meta text-muted", children: loadingMore ? copy.picker.loadingMore : emptyText })),
|
|
190
|
+
/* Incremental, the list is not capped: it scrolls inside its own height and the
|
|
191
|
+
sentinel at its end is what asks for more. */
|
|
192
|
+
className: cn("min-h-0 flex-1 overflow-y-auto p-1 outline-0", onLoadMore === undefined ? "" : "max-h-(--size-dropdown)"), children: [_jsx(Collection, { items: entries, children: (entry) => "items" in entry ? (_jsxs(ListBoxSection, { id: entry.id, className: "not-first:mt-1 not-first:border-t not-first:border-line not-first:pt-1", children: [_jsxs(Header, { className: "flex items-center gap-2 px-2 pt-1 pb-0.5 text-micro font-medium text-muted", children: [marks ? (_jsx("span", { className: "size-(--size-icon-sm) shrink-0", "aria-hidden": true })) : null, entry.section] }), _jsx(Collection, { items: entry.items, children: renderItem })] })) : (renderItem(entry)) }), onLoadMore === undefined ? null : (_jsx(ListBoxLoadMoreItem, { onLoadMore: onLoadMore, isLoading: loadingMore && items.length > 0, className: "px-3 py-1.5 text-center text-micro text-muted", children: copy.picker.loadingMore }))] }));
|
|
139
193
|
}
|
|
140
194
|
function Row({ item, selected, marks, hints, }) {
|
|
141
195
|
const Icon = item.icon;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A cell's value with its copy button — the table's version of a field's (see
|
|
4
|
+
* `primitives/field/copy.tsx`): the number of an invoice, a customer's name, an amount, copied
|
|
5
|
+
* without opening the row or selecting by hand.
|
|
6
|
+
*
|
|
7
|
+
* Context: the button appears where the pointer or the focus is (on a touch screen it is
|
|
8
|
+
* always there) and always takes its room, so nothing in the row moves when it shows. In a
|
|
9
|
+
* numeric cell it goes BEFORE the figure, so the figures stay aligned on the right edge —
|
|
10
|
+
* which is what makes a column of amounts comparable. It copies what the cell shows
|
|
11
|
+
* («Bs. 4.132,50», not `4132.5`) unless the column says what to copy.
|
|
12
|
+
*/
|
|
13
|
+
export declare function CellCopy({ children, header, numeric, value, }: {
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
/** The column's name, for the button's: «Copiar «Cliente»». */
|
|
16
|
+
header: string;
|
|
17
|
+
numeric?: boolean;
|
|
18
|
+
/** What to copy instead of what is seen. */
|
|
19
|
+
value?: string;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { shownText } from "../field/copy.js";
|
|
5
|
+
import { CopyButton } from "../value/Copyable.js";
|
|
6
|
+
import { useRef } from "react";
|
|
7
|
+
/**
|
|
8
|
+
* A cell's value with its copy button — the table's version of a field's (see
|
|
9
|
+
* `primitives/field/copy.tsx`): the number of an invoice, a customer's name, an amount, copied
|
|
10
|
+
* without opening the row or selecting by hand.
|
|
11
|
+
*
|
|
12
|
+
* Context: the button appears where the pointer or the focus is (on a touch screen it is
|
|
13
|
+
* always there) and always takes its room, so nothing in the row moves when it shows. In a
|
|
14
|
+
* numeric cell it goes BEFORE the figure, so the figures stay aligned on the right edge —
|
|
15
|
+
* which is what makes a column of amounts comparable. It copies what the cell shows
|
|
16
|
+
* («Bs. 4.132,50», not `4132.5`) unless the column says what to copy.
|
|
17
|
+
*/
|
|
18
|
+
export function CellCopy({ children, header, numeric = false, value, }) {
|
|
19
|
+
const ui = useUiCopy();
|
|
20
|
+
const shown = useRef(null);
|
|
21
|
+
const button = (_jsx("span", { "data-copy-skip": "", className: "inline-flex shrink-0 opacity-0 transition-opacity duration-(--duration-instant) group-hover/cell:opacity-100 focus-within:opacity-100 pointer-coarse:opacity-100", children: _jsx(CopyButton, { label: fillCopy(ui.copyable.field, { label: header }), value: () => value ?? (shown.current === null ? "" : shownText(shown.current)) }) }));
|
|
22
|
+
return (_jsxs("span", { className: `group/cell flex w-full min-w-0 items-center gap-1 ${numeric ? "justify-end" : ""}`, children: [numeric ? button : null, _jsx("span", { ref: shown, className: "min-w-0", children: children }), numeric ? null : button] }));
|
|
23
|
+
}
|
|
@@ -8,6 +8,17 @@ import { type ReactNode } from "react";
|
|
|
8
8
|
* identifiers or averaging file sizes produces numbers that look like data and
|
|
9
9
|
* mean nothing. An aggregate has to be chosen on purpose. */
|
|
10
10
|
export type Aggregate = "count" | "sum" | "mean" | "min" | "max" | "unique";
|
|
11
|
+
/**
|
|
12
|
+
* How the aggregate of a column is written: its `format` when the aggregate is in the
|
|
13
|
+
* column's units, `undefined` (the bare number) otherwise.
|
|
14
|
+
*
|
|
15
|
+
* Exported for the other places that draw `aggregateOf` — the bands of the sheet, the report
|
|
16
|
+
* built from the piles — so the footer and the band never write the same total two ways.
|
|
17
|
+
* Not public.
|
|
18
|
+
*/
|
|
19
|
+
export declare function aggregateFormat<T>(column: DataColumn<T>): ((value: number) => string) | undefined;
|
|
20
|
+
/** An aggregate of a column, written — or the bare number when nothing says how. */
|
|
21
|
+
export declare function aggregateText<T>(column: DataColumn<T>, value: number): string | number;
|
|
11
22
|
export interface DataColumn<T> {
|
|
12
23
|
id: string;
|
|
13
24
|
header: string;
|
|
@@ -44,6 +55,19 @@ export interface DataColumn<T> {
|
|
|
44
55
|
numeric?: boolean;
|
|
45
56
|
width?: number;
|
|
46
57
|
aggregate?: Aggregate;
|
|
58
|
+
/**
|
|
59
|
+
* How a number of this column is WRITTEN where the code draws it instead of a row: the
|
|
60
|
+
* footer's aggregate, the total of a band of the sheet, the lines of a report built
|
|
61
|
+
* from the piles. `(n) => money(n)` makes the footer read «Bs. 78.752,90» like the cells
|
|
62
|
+
* above it, and not «78.752,9».
|
|
63
|
+
*
|
|
64
|
+
* Context: the cells already had their way (`cell`), and that way takes a ROW — the
|
|
65
|
+
* footer has no row, only a number. A string and not a node because two of the three
|
|
66
|
+
* places are grids of text: the sheet and the report. Applies to the aggregates in the
|
|
67
|
+
* column's units (`sum`, `mean`, `min`, `max`); a `count` stays a count. Without it,
|
|
68
|
+
* the number in the copy table's locale, as before.
|
|
69
|
+
*/
|
|
70
|
+
format?: (value: number) => string;
|
|
47
71
|
sortable?: boolean;
|
|
48
72
|
/** Odoo: fields without `optional` cannot leave the view. When true, the
|
|
49
73
|
* column appears in the columns submenu and can be hidden. */
|
|
@@ -66,7 +90,32 @@ export interface DataColumn<T> {
|
|
|
66
90
|
/** Which rows can be changed — a paid invoice cannot. All of them by default. */
|
|
67
91
|
when?: (row: T) => boolean;
|
|
68
92
|
};
|
|
93
|
+
/** The control of this column in the row of a NEW record (`DataTable`'s `create`).
|
|
94
|
+
* The same contract as `EditableTable`'s `edit`: `set` replaces the draft. Give it an
|
|
95
|
+
* `aria-label` — a cell has no `Field` to name it. Without it the column stays
|
|
96
|
+
* empty in that row: a computed amount, a sequence the backend assigns. */
|
|
97
|
+
input?: (draft: T, set: (next: T) => void) => ReactNode;
|
|
98
|
+
/** The copy button of the cells (`CellCopy`): on by default, copying what the cell shows.
|
|
99
|
+
* A function copies what it returns for the row instead; `false` removes it from this
|
|
100
|
+
* column — a column of tags or of buttons has nothing worth pasting. */
|
|
101
|
+
copy?: false | ((row: T) => string);
|
|
69
102
|
}
|
|
103
|
+
/** Writing a new record in the list. See the file's header. */
|
|
104
|
+
export type DataTableCreate<T> = {
|
|
105
|
+
/** The record being written, or `null` when there is none. */
|
|
106
|
+
draft: T | null;
|
|
107
|
+
/** «Add» was asked for: the consumer sets a blank `draft`. */
|
|
108
|
+
onStart: () => void;
|
|
109
|
+
onChange: (next: T) => void;
|
|
110
|
+
/** `Enter`, leaving the row after writing in it, or `Tab` past its last control. The
|
|
111
|
+
* consumer validates and saves, and sets `draft` back to `null` — or keeps it, to
|
|
112
|
+
* say what is missing. */
|
|
113
|
+
onCommit: (draft: T) => void;
|
|
114
|
+
/** `Escape`, or leaving the row without having written anything. */
|
|
115
|
+
onCancel: () => void;
|
|
116
|
+
/** The «add» under the table. Defaults to the copy table's. */
|
|
117
|
+
label?: string;
|
|
118
|
+
};
|
|
70
119
|
/**
|
|
71
120
|
* El agregado de una columna sobre un conjunto de filas.
|
|
72
121
|
*
|
|
@@ -99,7 +148,7 @@ export type GroupColumn<T> = {
|
|
|
99
148
|
/** Cómo se lee esa clave. */
|
|
100
149
|
labelOf: (key: string) => string;
|
|
101
150
|
};
|
|
102
|
-
export declare function DataTable<T extends object>({ data, columns, getRowId, sort, onSortChange, levels, selected, onSelectedChange, height, onRowClick, rowActions, totals, collapsed, onGroupToggle, }: {
|
|
151
|
+
export declare function DataTable<T extends object>({ data, columns, getRowId, sort, onSortChange, levels, selected, onSelectedChange, height, onRowClick, rowActions, totals, collapsed, onGroupToggle, create, copyable, }: {
|
|
103
152
|
data: readonly T[];
|
|
104
153
|
columns: readonly DataColumn<T>[];
|
|
105
154
|
getRowId: (row: T) => string;
|
|
@@ -132,4 +181,9 @@ export declare function DataTable<T extends object>({ data, columns, getRowId, s
|
|
|
132
181
|
* table does not reopen what rows just folded. */
|
|
133
182
|
collapsed?: readonly string[];
|
|
134
183
|
onGroupToggle?: (key: string) => void;
|
|
184
|
+
/** Write a new record as a row of the list. See the file's header. */
|
|
185
|
+
create?: DataTableCreate<T>;
|
|
186
|
+
/** The cells' copy buttons, on by default. `false` takes them off the whole table; a
|
|
187
|
+
* column turns its own off with `copy: false`. */
|
|
188
|
+
copyable?: boolean;
|
|
135
189
|
}): import("react").JSX.Element;
|
|
@@ -2,27 +2,54 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
4
|
import { pathKey } from "../../foundations/grouping.js";
|
|
5
|
+
import { CellCopy } from "./CellCopy.js";
|
|
5
6
|
import { CellEditor } from "./CellEditor.js";
|
|
7
|
+
import { EditRow, firstControl } from "./EditableTable.js";
|
|
6
8
|
import { RowActions } from "./RowActions.js";
|
|
7
9
|
import { SelectBox, Table, Td, Th, THead, Tr, } from "./Table.js";
|
|
8
10
|
import { aggregationFns, columnGroupingFeature, createColumnHelper, createExpandedRowModel, createGroupedRowModel, createSortedRowModel, rowAggregationFeature, rowExpandingFeature, rowSortingFeature, sortFns, tableFeatures, useTable, } from "@tanstack/react-table";
|
|
9
|
-
import { ChevronRight } from "lucide-react";
|
|
10
|
-
import { useMemo } from "react";
|
|
11
|
-
|
|
11
|
+
import { ChevronRight, Plus } from "lucide-react";
|
|
12
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
13
|
+
/** The aggregates that are an amount OF the column — a sum of bolivianos is bolivianos — and
|
|
14
|
+
* so are written the way the column writes its numbers. `count` and `unique` count rows
|
|
15
|
+
* and values: «Bs. 12,00 rows» would be nonsense. */
|
|
16
|
+
const IN_COLUMN_UNITS = new Set(["sum", "mean", "min", "max"]);
|
|
17
|
+
/**
|
|
18
|
+
* How the aggregate of a column is written: its `format` when the aggregate is in the
|
|
19
|
+
* column's units, `undefined` (the bare number) otherwise.
|
|
20
|
+
*
|
|
21
|
+
* Exported for the other places that draw `aggregateOf` — the bands of the sheet, the report
|
|
22
|
+
* built from the piles — so the footer and the band never write the same total two ways.
|
|
23
|
+
* Not public.
|
|
24
|
+
*/
|
|
25
|
+
export function aggregateFormat(column) {
|
|
26
|
+
return column.aggregate !== undefined && IN_COLUMN_UNITS.has(column.aggregate)
|
|
27
|
+
? column.format
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
30
|
+
/** An aggregate of a column, written — or the bare number when nothing says how. */
|
|
31
|
+
export function aggregateText(column, value) {
|
|
32
|
+
const format = aggregateFormat(column);
|
|
33
|
+
return format === undefined ? value : format(value);
|
|
34
|
+
}
|
|
35
|
+
function aggregateLabel(copy, column, v) {
|
|
12
36
|
const n = v.toLocaleString(copy.locale);
|
|
13
|
-
|
|
37
|
+
/* Raw, a sum of amounts read «78.752,9»: one decimal lost and no currency, under a
|
|
38
|
+
column whose every cell says «Bs. 12.300,00». */
|
|
39
|
+
const amount = (fallback) => column.format === undefined ? fallback : column.format(v);
|
|
40
|
+
switch (column.aggregate ?? "count") {
|
|
14
41
|
case "sum":
|
|
15
|
-
return n;
|
|
42
|
+
return amount(n);
|
|
16
43
|
case "count":
|
|
17
44
|
return `${n} ${v === 1 ? copy.table.row : copy.table.rows}`;
|
|
18
45
|
case "mean":
|
|
19
46
|
return fillCopy(copy.table.mean, {
|
|
20
|
-
value: v.toLocaleString(copy.locale, { maximumFractionDigits: 2 }),
|
|
47
|
+
value: amount(v.toLocaleString(copy.locale, { maximumFractionDigits: 2 })),
|
|
21
48
|
});
|
|
22
49
|
case "min":
|
|
23
|
-
return fillCopy(copy.table.min, { value: n });
|
|
50
|
+
return fillCopy(copy.table.min, { value: amount(n) });
|
|
24
51
|
case "max":
|
|
25
|
-
return fillCopy(copy.table.max, { value: n });
|
|
52
|
+
return fillCopy(copy.table.max, { value: amount(n) });
|
|
26
53
|
case "unique":
|
|
27
54
|
return fillCopy(copy.table.distinct, { value: n });
|
|
28
55
|
}
|
|
@@ -90,8 +117,46 @@ export function aggregateOf(column, rows) {
|
|
|
90
117
|
return numbers.length;
|
|
91
118
|
}
|
|
92
119
|
}
|
|
93
|
-
export function DataTable({ data, columns, getRowId, sort, onSortChange, levels, selected, onSelectedChange, height, onRowClick, rowActions, totals = false, collapsed, onGroupToggle, }) {
|
|
120
|
+
export function DataTable({ data, columns, getRowId, sort, onSortChange, levels, selected, onSelectedChange, height, onRowClick, rowActions, totals = false, collapsed, onGroupToggle, create, copyable = true, }) {
|
|
94
121
|
const copy = useUiCopy();
|
|
122
|
+
const draft = create?.draft ?? null;
|
|
123
|
+
const draftRow = useRef(null);
|
|
124
|
+
const addButton = useRef(null);
|
|
125
|
+
/* Whether anything was written in the draft: leaving a blank one drops it. */
|
|
126
|
+
const touched = useRef(false);
|
|
127
|
+
const writing = draft !== null;
|
|
128
|
+
/* Each «add» is a session. With `Tab` the next draft replaces the saved one without
|
|
129
|
+
ever being `null`, so `writing` alone would not say a new record began. */
|
|
130
|
+
const [session, setSession] = useState(0);
|
|
131
|
+
/* «add» is hidden while writing: the focus goes back to it once it is there again. */
|
|
132
|
+
const backToAdd = useRef(false);
|
|
133
|
+
const start = () => {
|
|
134
|
+
setSession((n) => n + 1);
|
|
135
|
+
create?.onStart();
|
|
136
|
+
};
|
|
137
|
+
/* A draft that appears takes the focus on its first control — «add» is a request to
|
|
138
|
+
write, not to look for where to. */
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!writing) {
|
|
141
|
+
if (backToAdd.current)
|
|
142
|
+
addButton.current?.focus();
|
|
143
|
+
backToAdd.current = false;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
touched.current = false;
|
|
147
|
+
const node = draftRow.current;
|
|
148
|
+
if (node !== null)
|
|
149
|
+
(firstControl(node) ?? node).focus();
|
|
150
|
+
}, [writing, session]);
|
|
151
|
+
const saveDraft = (refocus) => {
|
|
152
|
+
if (create === undefined || draft === null)
|
|
153
|
+
return;
|
|
154
|
+
backToAdd.current = refocus;
|
|
155
|
+
if (touched.current)
|
|
156
|
+
create.onCommit(draft);
|
|
157
|
+
else
|
|
158
|
+
create.onCancel();
|
|
159
|
+
};
|
|
95
160
|
// In v9 the helper is bound to the features AND to the data type: that is what
|
|
96
161
|
// makes the compiler know about `enableGrouping` only if we ask for the
|
|
97
162
|
// feature.
|
|
@@ -208,37 +273,58 @@ export function DataTable({ data, columns, getRowId, sort, onSortChange, levels,
|
|
|
208
273
|
n.delete(id);
|
|
209
274
|
onSelectedChange?.(n);
|
|
210
275
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
276
|
+
const grid = (_jsxs(Table, { height: height, children: [_jsxs(THead, { children: [selecting ? (_jsx(Th, { width: 36, children: _jsx(SelectBox, { label: copy.selection.all, checked: all, indeterminate: some, onChange: (v) => onSelectedChange?.(v ? new Set(leaves.map((r) => r.id)) : new Set()) }) })) : null, columns.map((c) => (_jsx(Th, { sortKey: c.sortable === false ? undefined : c.id, sort: sort, onSort: onSortChange, numeric: c.numeric, width: c.width, children: c.header }, c.id))), rowActions === undefined ? null : _jsx(Th, { width: 1, children: "" })] }), _jsxs("tbody", { children: [drawn.map((row) => {
|
|
277
|
+
const isGroup = row.subRows.length > 0;
|
|
278
|
+
if (isGroup) {
|
|
279
|
+
const path = pathOf(row);
|
|
280
|
+
const key = pathKey(path);
|
|
281
|
+
const depth = path.length - 1;
|
|
282
|
+
const id = row.groupingColumnId ?? undefined;
|
|
283
|
+
const level = levels?.find((one) => one.id === id);
|
|
284
|
+
/* La CLAVE del montón —la del motor— y el rótulo, que lo pone el
|
|
285
|
+
nivel: son dos textos y sólo el segundo cambia con el idioma. */
|
|
286
|
+
const value = path[depth] ?? key;
|
|
287
|
+
const open = collapsed === undefined
|
|
288
|
+
? row.getIsExpanded()
|
|
289
|
+
: collapsed.includes(key) !== true;
|
|
290
|
+
return (
|
|
291
|
+
/* The group row: it spans the whole width, says which value it
|
|
292
|
+
groups by and how many are inside. The counter is what makes it
|
|
293
|
+
useful without opening it. */
|
|
294
|
+
_jsx("tr", { className: "border-b border-line bg-ground", children: _jsx("td", { colSpan: columns.length + (selecting ? 1 : 0) + (rowActions === undefined ? 0 : 1), className: "px-3 py-1.5", style: depth === 0
|
|
295
|
+
? undefined
|
|
296
|
+
: { paddingLeft: `calc(var(--spacing-indent) * ${depth} + 0.75rem)` }, children: _jsxs("button", { type: "button", onClick: () => onGroupToggle === undefined ? row.toggleExpanded() : onGroupToggle(key), "aria-expanded": open, className: "inline-flex items-center gap-1.5 rounded-control text-meta font-medium text-ink transition-colors duration-(--duration-instant) hover:text-primary focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary", children: [_jsx(ChevronRight, { className: `size-(--size-icon-sm) text-muted transition-transform duration-(--duration-instant) ${open ? "rotate-90" : ""}`, strokeWidth: 2, "aria-hidden": true }), level === undefined ? null : (_jsx("span", { className: "text-micro font-normal text-muted/70", children: level.label })), _jsx("span", { children: level === undefined ? value : level.labelOf(value) }), _jsx("span", { className: "inline-flex shrink-0 items-center rounded-pill border border-line-strong bg-ink/5 px-2 py-0.5 text-micro tabular-nums text-muted", children: row.getLeafRows().length.toLocaleString(copy.locale) })] }) }) }, row.id));
|
|
297
|
+
}
|
|
298
|
+
const original = row.original;
|
|
299
|
+
return (_jsxs(Tr, { selected: selected?.has(row.id) === true, onClick: onRowClick === undefined ? undefined : () => onRowClick(original), children: [selecting ? (_jsx(Td, { children: _jsx(SelectBox, { label: copy.selection.row, checked: selected?.has(row.id) === true, onChange: (v) => toggle(row.id, v) }) })) : null, columns.map((c) => {
|
|
300
|
+
const t = c.tone?.(original);
|
|
301
|
+
const n = c.note?.(original);
|
|
302
|
+
const edit = c.edit?.when?.(original) === false ? undefined : c.edit;
|
|
303
|
+
return (_jsx(Td, { numeric: c.numeric, mono: c.mono === true, ...(t === undefined ? {} : { tone: t }), ...(n === undefined ? {} : { title: n }), children: edit !== undefined ? (_jsx(CellEditor, { label: fillCopy(copy.inlineEdit.editCell, { column: c.header }), numeric: c.numeric === true, editor: (done) => edit.render(original, done), children: drawCell(copy, c, original) })) : copyable && c.copy !== false ? (_jsx(CellCopy, { header: c.header, numeric: c.numeric === true, ...(c.copy === undefined ? {} : { value: c.copy(original) }), children: drawCell(copy, c, original) })) : (drawCell(copy, c, original)) }, c.id));
|
|
304
|
+
}), rowActions === undefined ? null : (_jsx(RowActions, { actions: rowActions(original) }))] }, row.id));
|
|
305
|
+
}), create === undefined || draft === null ? null : (_jsxs(EditRow, { as: "tr", rowId: "\\u0000draft", rowRef: (node) => {
|
|
306
|
+
draftRow.current = node;
|
|
307
|
+
}, editable: true, editing: true, tabbable: false, className: "group/row", onStart: () => { }, onCommit: saveDraft, onCancel: () => {
|
|
308
|
+
backToAdd.current = true;
|
|
309
|
+
create.onCancel();
|
|
310
|
+
}, onMove: () => { }, onFocusRow: () => { }, onTabOut: (direction) => {
|
|
311
|
+
/* Past the last control: saved, and the next one started — unless nothing
|
|
312
|
+
was written, which is «I am done». Backwards, the browser's way. */
|
|
313
|
+
if (direction === "prev" || !touched.current)
|
|
314
|
+
return false;
|
|
315
|
+
create.onCommit(draft);
|
|
316
|
+
start();
|
|
317
|
+
return true;
|
|
318
|
+
}, children: [selecting ? _jsx("td", { className: DRAFT_CELL }) : null, columns.map((c) => (_jsx("td", { ...(c.input === undefined ? {} : { "data-edit": "" }), className: `${DRAFT_CELL} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: c.input?.(draft, (next) => {
|
|
319
|
+
touched.current = true;
|
|
320
|
+
create.onChange(next);
|
|
321
|
+
}) ?? null }, c.id))), rowActions === undefined ? null : _jsx("td", { className: DRAFT_CELL })] }))] }), footer === null ? null : (_jsx("tfoot", { className: "sticky bottom-0 z-10 bg-ground", children: _jsxs("tr", { className: "border-t border-line-strong", children: [selecting ? _jsx("td", {}) : null, columns.map((c, i) => {
|
|
241
322
|
const value = footer[i] ?? null;
|
|
242
|
-
return (_jsx("td", { className: `px-3 py-1.5 text-meta font-semibold text-ink ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: i === 0 && value === null ? (_jsxs("span", { className: "text-muted", children: [data.length.toLocaleString(copy.locale), " ", data.length === 1 ? copy.table.row : copy.table.rows] })) : value === null ? null : (aggregateLabel(copy, c
|
|
323
|
+
return (_jsx("td", { className: `px-3 py-1.5 text-meta font-semibold text-ink ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: i === 0 && value === null ? (_jsxs("span", { className: "text-muted", children: [data.length.toLocaleString(copy.locale), " ", data.length === 1 ? copy.table.row : copy.table.rows] })) : value === null ? null : (aggregateLabel(copy, c, value)) }, c.id));
|
|
243
324
|
}), rowActions === undefined ? null : _jsx("td", {})] }) }))] }));
|
|
325
|
+
if (create === undefined)
|
|
326
|
+
return grid;
|
|
327
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col", children: [grid, writing ? null : (_jsx("div", { className: "px-1 py-0.5", children: _jsxs("button", { ref: addButton, type: "button", onClick: start, className: "flex w-full items-center gap-1.5 rounded-control px-3 py-1 text-left text-meta text-primary transition-colors duration-(--duration-instant) hover:bg-primary/5 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary", children: [_jsx(Plus, { className: "size-(--size-icon-sm)", strokeWidth: 2.5, "aria-hidden": true }), create.label ?? copy.table.add] }) }))] }));
|
|
244
328
|
}
|
|
329
|
+
/* A cell of the row being written: the table's cell, washed like an edited row. */
|
|
330
|
+
const DRAFT_CELL = "border-b border-line bg-primary/5 px-3 py-1 align-middle text-meta text-ink first:rounded-l-control last:rounded-r-control";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Grouping } from "../../foundations/grouping.js";
|
|
2
|
+
import { type FlagProps } from "../notice/Flag.js";
|
|
2
3
|
import { type ReactNode } from "react";
|
|
3
4
|
/** Where a column goes when the row is drawn as a stacked card (a narrow box).
|
|
4
5
|
* `trail` sits at the top right, beside the title — a state, the row's buttons. */
|
|
@@ -28,7 +29,32 @@ export interface EditColumn<T> {
|
|
|
28
29
|
* discount. `null` or `""` leaves it out of that row's line — a zero discount is
|
|
29
30
|
* not news. Without it the line reads «<header> <cell>». */
|
|
30
31
|
brief?: (row: T) => ReactNode;
|
|
32
|
+
/** Drawn on section and note rows too, instead of being covered by their text — the
|
|
33
|
+
* row's buttons. `render` receives the band's row. */
|
|
34
|
+
onBands?: boolean;
|
|
31
35
|
}
|
|
36
|
+
/** What a row that is not a line is. See «sections and notes» in the file's header. */
|
|
37
|
+
export type EditBands<T> = {
|
|
38
|
+
/** `section` heads the lines under it; `note` is a sentence between lines; `null` is a
|
|
39
|
+
* line. */
|
|
40
|
+
kind: (row: T) => "section" | "note" | null;
|
|
41
|
+
/** The band's text as it reads. */
|
|
42
|
+
render: (row: T) => ReactNode;
|
|
43
|
+
/** Its control while the row is edited — a text field, named. */
|
|
44
|
+
edit?: (row: T, set: (next: T) => void) => ReactNode;
|
|
45
|
+
/** What a section's lines add up to in `column`, already formatted; `null` where the
|
|
46
|
+
* column does not add up. The lines are the ones under it until the next section. */
|
|
47
|
+
total?: (column: EditColumn<T>, lines: readonly T[]) => ReactNode | null;
|
|
48
|
+
};
|
|
49
|
+
/** One more way to add, beside «add a line»: a section, a note. */
|
|
50
|
+
export type EditAdd = {
|
|
51
|
+
label: string;
|
|
52
|
+
/** As `onAdd`: return the new row's id and it opens. */
|
|
53
|
+
onAdd: () => string | void | Promise<string | void>;
|
|
54
|
+
};
|
|
55
|
+
/** The lines each section heads: the rows after it up to the next section. Notes do not
|
|
56
|
+
* cut a section, and are not lines. */
|
|
57
|
+
export declare function sectionLines<T>(rows: readonly T[], getRowId: (row: T) => string, kind: (row: T) => "section" | "note" | null): Map<string, T[]>;
|
|
32
58
|
/**
|
|
33
59
|
* Where each column goes in the stacked card, defaults resolved: a declared `stack`
|
|
34
60
|
* wins; otherwise the first undeclared column is the title (unless one was declared),
|
|
@@ -39,6 +65,56 @@ export declare function stackPlaces(columns: readonly {
|
|
|
39
65
|
stack?: StackPlace;
|
|
40
66
|
numeric?: boolean;
|
|
41
67
|
}[]): StackPlace[];
|
|
68
|
+
/** The first control of a row being edited: the first one in an editable cell, else
|
|
69
|
+
* anything focusable in the row (a column without `edit` may still hold a button). */
|
|
70
|
+
export declare function firstControl(row: HTMLElement): HTMLElement | null;
|
|
71
|
+
/** Where `Tab` goes from the edge of an edited row: the next or previous row opened, a new
|
|
72
|
+
* row added, or out of the table the browser's way. */
|
|
73
|
+
export type TabTarget = {
|
|
74
|
+
kind: "row";
|
|
75
|
+
id: string;
|
|
76
|
+
} | {
|
|
77
|
+
kind: "add";
|
|
78
|
+
} | {
|
|
79
|
+
kind: "leave";
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Where `Tab` past the last control (`next`) or `Shift+Tab` before the first (`prev`) of
|
|
83
|
+
* the edited row `from` goes, over the rows in the order they are drawn.
|
|
84
|
+
*
|
|
85
|
+
* Past the last row it adds one when the table can — unless `from` is itself a row just
|
|
86
|
+
* added and still blank: one more `Tab` there means «I am done», not «another blank line».
|
|
87
|
+
*/
|
|
88
|
+
export declare function tabTarget(order: readonly string[], from: string, direction: "next" | "prev", { canAdd, blank }: {
|
|
89
|
+
canAdd: boolean;
|
|
90
|
+
blank: boolean;
|
|
91
|
+
}): TabTarget;
|
|
92
|
+
export type RowKey = "ArrowDown" | "ArrowUp" | "Home" | "End";
|
|
93
|
+
/**
|
|
94
|
+
* One body row. Its own component so that it can hold React Aria's `useKeyboard`
|
|
95
|
+
* (a hook per row) and the timer that decides whether the focus really left it.
|
|
96
|
+
* The same row is a `<tr>` in the table and an `<li>` in the stacked list — and the row
|
|
97
|
+
* of a new record in `DataTable`: one keyboard model, three shapes. Exported for that
|
|
98
|
+
* family sibling only; it is not in the barrel.
|
|
99
|
+
*/
|
|
100
|
+
export declare function EditRow({ as, rowId, rowRef, editable, editing, tabbable, className, onStart, onCommit, onCancel, onMove, onFocusRow, onTabOut, children, }: {
|
|
101
|
+
as: "tr" | "li";
|
|
102
|
+
rowId: string;
|
|
103
|
+
rowRef: (node: HTMLElement | null) => void;
|
|
104
|
+
editable: boolean;
|
|
105
|
+
editing: boolean;
|
|
106
|
+
tabbable: boolean;
|
|
107
|
+
className: string;
|
|
108
|
+
onStart: (viaKeyboard: boolean) => void;
|
|
109
|
+
onCommit: (refocus: boolean) => void;
|
|
110
|
+
onCancel: () => void;
|
|
111
|
+
onMove: (key: RowKey) => void;
|
|
112
|
+
onFocusRow: () => void;
|
|
113
|
+
/** `Tab` left the row by one of its edges. `true` = the table took it (opened another
|
|
114
|
+
* row, added one); `false` = the browser moves the focus as usual. */
|
|
115
|
+
onTabOut: (direction: "next" | "prev") => boolean;
|
|
116
|
+
children: ReactNode;
|
|
117
|
+
}): import("react").JSX.Element;
|
|
42
118
|
/**
|
|
43
119
|
* A table whose rows are edited in place, one row at a time.
|
|
44
120
|
*
|
|
@@ -59,13 +135,16 @@ totals, empty, grouping,
|
|
|
59
135
|
/** El total de una columna DENTRO de un montón, ya formateado. Lo calcula quien
|
|
60
136
|
* compone, por el mismo motivo que `totals`: sumar significa cosas distintas
|
|
61
137
|
* según el dominio, y un porcentaje se promedia, no se suma. */
|
|
62
|
-
groupTotal, height, layout, onOpen, }: {
|
|
138
|
+
groupTotal, height, layout, onOpen, onDiscard, bands, adds, flag, }: {
|
|
63
139
|
rows: readonly T[];
|
|
64
140
|
columns: readonly EditColumn<T>[];
|
|
65
141
|
getRowId: (row: T) => string;
|
|
66
142
|
onChange?: (id: string, next: T) => void;
|
|
67
143
|
onRemove?: (id: string) => void;
|
|
68
|
-
|
|
144
|
+
/** «Add a line». Return the new row's id —or a promise of it, when the backend makes
|
|
145
|
+
* the record— and the table opens that row with the focus on its first control.
|
|
146
|
+
* Return nothing and the row just appears, closed. */
|
|
147
|
+
onAdd?: () => string | void | Promise<string | void>;
|
|
69
148
|
addLabel?: string;
|
|
70
149
|
totals?: readonly (ReactNode | null)[];
|
|
71
150
|
empty?: string;
|
|
@@ -93,4 +172,20 @@ groupTotal, height, layout, onOpen, }: {
|
|
|
93
172
|
* form of its own, a dialog that is a bottom sheet on a phone. The table ignores it:
|
|
94
173
|
* there the row is edited where it is read. */
|
|
95
174
|
onOpen?: (id: string) => void;
|
|
175
|
+
/** Take back a row `onAdd` opened when it is abandoned: `Escape` on it, or leaving it
|
|
176
|
+
* without a single change. Not `onRemove`: nobody removed a line, so there is nothing
|
|
177
|
+
* to undo or to announce. Without it the blank row stays. */
|
|
178
|
+
onDiscard?: (id: string) => void;
|
|
179
|
+
/** Rows that are not lines: sections and notes. See the file's header. */
|
|
180
|
+
bands?: EditBands<T>;
|
|
181
|
+
/** More ways to add, drawn beside «add» under the grid: «add a section», «add a note». */
|
|
182
|
+
adds?: readonly EditAdd[];
|
|
183
|
+
/** A warning about one row — stock, a price under cost — drawn as a `Flag` at its start.
|
|
184
|
+
* `null` for the rows with nothing to say, which should be most of them. `owner` is what
|
|
185
|
+
* the record's activity says it belongs to («Línea 3 · Impresora…»); without it, the
|
|
186
|
+
* row's first value. */
|
|
187
|
+
flag?: (row: T) => (Omit<FlagProps, "children"> & {
|
|
188
|
+
detail?: ReactNode;
|
|
189
|
+
owner?: string;
|
|
190
|
+
}) | null;
|
|
96
191
|
}): import("react").JSX.Element;
|