@sincpro/web-ui 0.1.0 → 1.0.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 +40 -13
- package/dist/components/activity/Activity.d.ts +69 -0
- package/dist/components/activity/Activity.js +279 -0
- package/dist/components/activity/ActivityBadge.d.ts +16 -0
- package/dist/components/activity/ActivityBadge.js +29 -0
- package/dist/components/{chatter → activity}/Messages.d.ts +2 -2
- package/dist/components/{chatter → activity}/Messages.js +9 -6
- package/dist/components/activity/index.d.ts +4 -0
- package/dist/components/activity/index.js +5 -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/calendar/RecordCalendar.d.ts +48 -0
- package/dist/components/calendar/RecordCalendar.js +101 -0
- package/dist/components/calendar/index.d.ts +1 -0
- package/dist/components/calendar/index.js +1 -0
- package/dist/components/form/Sheet.d.ts +4 -0
- package/dist/components/form/Sheet.js +16 -5
- package/dist/components/gantt/RecordGantt.d.ts +44 -0
- package/dist/components/gantt/RecordGantt.js +110 -0
- package/dist/components/gantt/index.d.ts +1 -0
- package/dist/components/gantt/index.js +1 -0
- package/dist/components/index.d.ts +4 -1
- package/dist/components/index.js +4 -1
- package/dist/components/lines/LineItems.d.ts +45 -5
- package/dist/components/lines/LineItems.js +106 -18
- package/dist/components/listing/BoardColumns.d.ts +39 -0
- package/dist/components/listing/BoardColumns.js +138 -0
- package/dist/components/listing/Listing.js +1 -1
- package/dist/components/listing/ListingBar.d.ts +46 -2
- package/dist/components/listing/ListingBar.js +47 -5
- package/dist/components/listing/PivotExplorer.d.ts +34 -0
- package/dist/components/listing/PivotExplorer.js +86 -0
- package/dist/components/listing/RecordCard.d.ts +6 -8
- package/dist/components/listing/RecordCard.js +13 -7
- package/dist/components/listing/Row.d.ts +25 -2
- package/dist/components/listing/Row.js +30 -13
- package/dist/components/listing/index.d.ts +2 -0
- package/dist/components/listing/index.js +2 -0
- package/dist/components/listing/set.d.ts +1 -1
- package/dist/components/navigation/index.d.ts +0 -2
- package/dist/components/navigation/index.js +2 -3
- package/dist/components/record/RecordAudit.d.ts +17 -0
- package/dist/components/record/RecordAudit.js +27 -0
- package/dist/components/record/RecordHeader.d.ts +27 -17
- package/dist/components/record/RecordHeader.js +64 -20
- package/dist/components/record/RecordNotices.d.ts +37 -0
- package/dist/components/record/RecordNotices.js +75 -0
- package/dist/components/record/RecordRibbon.d.ts +13 -0
- package/dist/components/record/RecordRibbon.js +22 -0
- package/dist/components/record/ThumbBar.js +1 -1
- package/dist/components/record/index.d.ts +2 -0
- package/dist/components/record/index.js +2 -0
- package/dist/components/search/CriteriaChips.d.ts +6 -1
- package/dist/components/search/CriteriaChips.js +71 -20
- package/dist/components/search/SearchBar.d.ts +4 -1
- package/dist/components/search/SearchBar.js +15 -7
- package/dist/components/timeline/RecordTimeline.d.ts +60 -0
- package/dist/components/timeline/RecordTimeline.js +214 -0
- package/dist/components/timeline/TimelineToolbar.d.ts +30 -0
- package/dist/components/timeline/TimelineToolbar.js +31 -0
- package/dist/components/timeline/index.d.ts +3 -0
- package/dist/components/timeline/index.js +3 -0
- package/dist/components/timeline/time.d.ts +43 -0
- package/dist/components/timeline/time.js +135 -0
- package/dist/foundations/copy.d.ts +194 -18
- package/dist/foundations/copy.es.js +142 -15
- package/dist/foundations/copy.js +133 -11
- package/dist/foundations/index.d.ts +1 -1
- package/dist/foundations/match.d.ts +23 -0
- package/dist/foundations/match.js +63 -0
- package/dist/foundations/slots.d.ts +1 -1
- package/dist/foundations/slots.js +8 -3
- package/dist/foundations/types.d.ts +7 -0
- package/dist/foundations/types.js +0 -36
- package/dist/primitives/block/index.d.ts +1 -1
- package/dist/primitives/block/index.js +1 -1
- package/dist/primitives/cell/CellStack.d.ts +7 -1
- package/dist/primitives/cell/CellStack.js +5 -2
- package/dist/primitives/choice/ComboBox.d.ts +3 -0
- package/dist/primitives/choice/ComboBox.js +10 -6
- package/dist/primitives/choice/KanbanState.d.ts +14 -0
- package/dist/primitives/choice/KanbanState.js +37 -0
- package/dist/primitives/choice/Priority.d.ts +20 -0
- package/dist/primitives/choice/Priority.js +41 -0
- package/dist/primitives/choice/SearchField.d.ts +10 -1
- package/dist/primitives/choice/SearchField.js +21 -2
- package/dist/primitives/choice/Slider.d.ts +6 -1
- package/dist/primitives/choice/Slider.js +2 -2
- package/dist/primitives/choice/index.d.ts +2 -0
- package/dist/primitives/choice/index.js +2 -0
- 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/Dialog.d.ts +9 -3
- package/dist/primitives/dialog/Dialog.js +41 -5
- 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 +2 -1
- package/dist/primitives/notice/index.js +2 -1
- package/dist/primitives/overlay/Preview.d.ts +19 -0
- package/dist/primitives/overlay/Preview.js +69 -0
- package/dist/primitives/overlay/index.d.ts +1 -0
- package/dist/primitives/overlay/index.js +3 -1
- package/dist/primitives/picker/MatchedText.d.ts +10 -0
- package/dist/primitives/picker/MatchedText.js +12 -0
- package/dist/primitives/picker/Picker.d.ts +67 -4
- package/dist/primitives/picker/Picker.js +142 -23
- 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 +102 -2
- package/dist/primitives/table/EditableTable.js +287 -32
- package/dist/primitives/table/ReportTable.js +2 -1
- package/dist/primitives/table/RowActions.js +7 -2
- package/dist/primitives/table/SheetTable.d.ts +4 -1
- package/dist/primitives/table/SheetTable.js +38 -5
- package/dist/primitives/table/Table.d.ts +5 -0
- package/dist/primitives/table/Table.js +17 -8
- 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/Tags.d.ts +2 -0
- package/dist/primitives/tag/Tags.js +2 -1
- package/dist/primitives/tag/TagsField.d.ts +13 -0
- package/dist/primitives/tag/TagsField.js +39 -10
- package/dist/primitives/tag/choices.d.ts +2 -0
- 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/Avatar.d.ts +16 -0
- package/dist/primitives/value/Avatar.js +91 -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/Person.d.ts +6 -51
- package/dist/primitives/value/Person.js +9 -148
- package/dist/primitives/value/ProgressBar.d.ts +31 -0
- package/dist/primitives/value/ProgressBar.js +53 -0
- package/dist/primitives/value/Relation.d.ts +22 -5
- package/dist/primitives/value/Relation.js +42 -14
- 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/contact.d.ts +30 -0
- package/dist/primitives/value/contact.js +51 -0
- package/dist/primitives/value/index.d.ts +5 -4
- package/dist/primitives/value/index.js +5 -4
- package/dist/tokens.css +179 -7
- package/dist/views/list/List.d.ts +32 -3
- package/dist/views/list/List.js +51 -7
- package/dist/views/list/ListView.d.ts +1 -1
- package/dist/views/list/ListView.js +70 -13
- package/dist/views/list/columns.js +8 -1
- package/dist/views/list/derive.d.ts +12 -1
- package/dist/views/list/derive.js +29 -3
- package/dist/views/list/presentations.d.ts +5 -2
- package/dist/views/list/presentations.js +35 -3
- package/dist/views/list/types.d.ts +57 -1
- package/dist/views/list/useNarrowList.d.ts +10 -0
- package/dist/views/list/useNarrowList.js +20 -6
- package/dist/views/record/Record.d.ts +41 -8
- package/dist/views/record/Record.js +113 -28
- package/dist/views/record/index.d.ts +1 -1
- package/package.json +15 -4
- package/dist/components/chatter/Chatter.d.ts +0 -161
- package/dist/components/chatter/Chatter.js +0 -232
- package/dist/components/chatter/index.d.ts +0 -3
- package/dist/components/chatter/index.js +0 -4
- package/dist/components/navigation/CommandPalette.d.ts +0 -40
- package/dist/components/navigation/CommandPalette.js +0 -114
- package/dist/components/navigation/commands.d.ts +0 -30
- package/dist/components/navigation/commands.js +0 -30
- /package/dist/components/{chatter → activity}/Timeline.d.ts +0 -0
- /package/dist/components/{chatter → activity}/Timeline.js +0 -0
|
@@ -1,20 +1,69 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { AriaLocale } from "../../foundations/aria.js";
|
|
4
4
|
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
5
|
+
import { matchedWord, matches } from "../../foundations/match.js";
|
|
5
6
|
import { cn, tv } from "../../foundations/variants.js";
|
|
6
7
|
import { IconButton } from "../button/Button.js";
|
|
7
8
|
import { FieldButton, FieldGroup } from "../field/aria.js";
|
|
8
9
|
import { useFieldIds } from "../field/context.js";
|
|
9
10
|
import { ReadValue } from "../field/Field.js";
|
|
10
11
|
import { useFormMode } from "../field/mode.js";
|
|
12
|
+
import { Menu } from "../menu/Menu.js";
|
|
11
13
|
import { Popover } from "../overlay/Popover.js";
|
|
12
|
-
import {
|
|
14
|
+
import { MatchedText } from "./MatchedText.js";
|
|
15
|
+
import { Avatar } from "../value/Person.js";
|
|
16
|
+
import { AppWindow, Check, ChevronDown, Ellipsis, Eye, PanelRight, PictureInPicture2, SquareArrowOutUpRight, } from "lucide-react";
|
|
13
17
|
import { useContext, useMemo, useRef, useState } from "react";
|
|
14
|
-
import { ComboBox as AriaComboBox, ComboBoxStateContext, Input, ListBox, ListBoxItem, Text, useFilter, } from "react-aria-components";
|
|
15
|
-
/** The
|
|
18
|
+
import { Collection, ComboBox as AriaComboBox, ComboBoxStateContext, Header, Input, ListBox, ListBoxItem, ListBoxLoadMoreItem, ListBoxSection, Text, useFilter, } from "react-aria-components";
|
|
19
|
+
/** The icon of each place a record opens — the same in every field and list. */
|
|
20
|
+
const OPEN_ICON = {
|
|
21
|
+
modal: PictureInPicture2,
|
|
22
|
+
peek: Eye,
|
|
23
|
+
page: SquareArrowOutUpRight,
|
|
24
|
+
side: PanelRight,
|
|
25
|
+
window: AppWindow,
|
|
26
|
+
};
|
|
27
|
+
/** The id of the «Buscar más…» row. Not a record id: it cannot collide with one. */
|
|
16
28
|
const BROWSE_ALL = "\u0000browse-all";
|
|
29
|
+
/** The rows that create from what was typed — quick, or with the full form. */
|
|
30
|
+
const CREATE = "\u0000create";
|
|
31
|
+
const CREATE_EDIT = "\u0000create-edit";
|
|
32
|
+
/** The rows that act instead of choosing: after the records, in this order. */
|
|
33
|
+
const ACTIONS = new Set([CREATE, CREATE_EDIT, BROWSE_ALL]);
|
|
17
34
|
const CAP = 8;
|
|
35
|
+
/**
|
|
36
|
+
* What the list shows and how many are left out of it. Capped at 8 with the footer count,
|
|
37
|
+
* unless the list loads incrementally — then everything that arrived is listed and the
|
|
38
|
+
* scroll asks for more.
|
|
39
|
+
*/
|
|
40
|
+
export function shownRows(filtered, { loading, incremental }) {
|
|
41
|
+
if (loading)
|
|
42
|
+
return { visible: [], remaining: 0 };
|
|
43
|
+
if (incremental)
|
|
44
|
+
return { visible: filtered, remaining: 0 };
|
|
45
|
+
const visible = filtered.slice(0, CAP);
|
|
46
|
+
return { visible, remaining: filtered.length - visible.length };
|
|
47
|
+
}
|
|
48
|
+
/** Folds consecutive items of the same `section` into one run under a header. The ids of
|
|
49
|
+
* the runs cannot collide with a record id, like the «Buscar más…» one. */
|
|
50
|
+
function sectioned(items) {
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const item of items) {
|
|
53
|
+
if (item.section === undefined) {
|
|
54
|
+
out.push(item);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const last = out.at(-1);
|
|
58
|
+
if (last !== undefined && "items" in last && last.section === item.section) {
|
|
59
|
+
last.items.push(item);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
out.push({ id: `\u0000section-${out.length}`, section: item.section, items: [item] });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
18
67
|
/* No focus ring on an option: the focus stays in the input (the list is driven from it),
|
|
19
68
|
and the marked row is said by its tint — a ring inside the list as well as around the
|
|
20
69
|
field is two marks for one place. */
|
|
@@ -48,7 +97,7 @@ export function Picker({ mode, ...props }) {
|
|
|
48
97
|
to be ABOVE the component that asks for it. */
|
|
49
98
|
return (_jsx(AriaLocale, { children: _jsx(EditablePicker, { ...props }) }));
|
|
50
99
|
}
|
|
51
|
-
function EditablePicker({ id: ownId, items, value, onChange, placeholder, onBrowseAll, onOpen, emptyText, searchPlaceholder, loading = false, loadingText, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, }) {
|
|
100
|
+
function EditablePicker({ id: ownId, items, value, onChange, placeholder, onBrowseAll, onCreate, onCreateAndEdit, onOpen, openTargets = ["modal", "peek", "side", "page"], emptyText, searchPlaceholder, loading = false, loadingText, onLoadMore, loadingMore = false, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, "aria-label": ariaLabel, }) {
|
|
52
101
|
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
53
102
|
id: ownId,
|
|
54
103
|
describedBy: ownDescribedBy,
|
|
@@ -80,29 +129,55 @@ function EditablePicker({ id: ownId, items, value, onChange, placeholder, onBrow
|
|
|
80
129
|
return items;
|
|
81
130
|
return items.filter((i) => contains(`${i.name} ${i.meta ?? ""} ${i.haystack ?? ""}`, q));
|
|
82
131
|
}, [items, query, contains]);
|
|
83
|
-
const visible
|
|
84
|
-
|
|
132
|
+
const { visible, remaining } = shownRows(filtered, {
|
|
133
|
+
loading,
|
|
134
|
+
incremental: onLoadMore !== undefined,
|
|
135
|
+
});
|
|
85
136
|
const hasMarks = items.some((i) => i.icon !== undefined);
|
|
86
137
|
const hasHints = visible.some((i) => i.meta !== undefined);
|
|
87
138
|
/* «See all…» is an OPTION at the end of the list and not a button in a footer: the
|
|
88
139
|
arrows reach it like any other row. As a footer button it was out of the keyboard's
|
|
89
140
|
path — Tab leaves the field and closes the list before getting there. */
|
|
90
|
-
const browsable =
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
141
|
+
const browsable = onBrowseAll !== undefined;
|
|
142
|
+
const typed = query.trim();
|
|
143
|
+
/* Exactly the name of an option already: creating it again would make a twin. */
|
|
144
|
+
const named = typed !== "" &&
|
|
145
|
+
items.some((i) => i.name.localeCompare(typed, copy.locale, { sensitivity: "base" }) === 0);
|
|
146
|
+
const actions = loading
|
|
147
|
+
? []
|
|
148
|
+
: [
|
|
149
|
+
...(onCreate !== undefined && typed !== "" && !named
|
|
150
|
+
? [{ id: CREATE, name: fillCopy(copy.picker.create, { name: typed }) }]
|
|
151
|
+
: []),
|
|
152
|
+
...(onCreateAndEdit === undefined
|
|
153
|
+
? []
|
|
154
|
+
: [{ id: CREATE_EDIT, name: copy.picker.createAndEdit }]),
|
|
155
|
+
...(browsable ? [{ id: BROWSE_ALL, name: copy.picker.browseAll }] : []),
|
|
156
|
+
];
|
|
157
|
+
const listed = [...visible, ...actions];
|
|
94
158
|
const closeList = useRef(null);
|
|
95
159
|
const take = (key) => {
|
|
96
160
|
if (key === null)
|
|
97
161
|
return;
|
|
98
162
|
const next = String(key);
|
|
99
|
-
if (next
|
|
100
|
-
/* The full list opens somewhere else (a dialog, a panel):
|
|
101
|
-
field keeps the chosen name.
|
|
163
|
+
if (ACTIONS.has(next)) {
|
|
164
|
+
/* The full list, or the new record's form, opens somewhere else (a dialog, a panel):
|
|
165
|
+
this one closes and the field keeps the chosen name. A quick create chooses the new
|
|
166
|
+
record once it exists. */
|
|
167
|
+
const asked = query;
|
|
102
168
|
closeList.current?.();
|
|
103
169
|
setText(chosenName);
|
|
104
170
|
setQuery("");
|
|
105
|
-
|
|
171
|
+
if (next === BROWSE_ALL)
|
|
172
|
+
onBrowseAll?.(asked);
|
|
173
|
+
if (next === CREATE_EDIT)
|
|
174
|
+
onCreateAndEdit?.(asked.trim());
|
|
175
|
+
if (next === CREATE && onCreate !== undefined) {
|
|
176
|
+
/* The field keeps the new name from now on, not the old one until the record exists:
|
|
177
|
+
a controlled text that changes while the input has focus reopens the list. */
|
|
178
|
+
setText(asked.trim());
|
|
179
|
+
void Promise.resolve(onCreate(asked.trim())).then((created) => onChange?.(created));
|
|
180
|
+
}
|
|
106
181
|
return;
|
|
107
182
|
}
|
|
108
183
|
setText(items.find((i) => i.id === next)?.name ?? "");
|
|
@@ -110,7 +185,7 @@ function EditablePicker({ id: ownId, items, value, onChange, placeholder, onBrow
|
|
|
110
185
|
if (next !== value)
|
|
111
186
|
onChange?.(next);
|
|
112
187
|
};
|
|
113
|
-
return (_jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-describedby": describedBy, items: listed, value: value ?? null, onChange: take, inputValue: text, onInputChange: (next) => {
|
|
188
|
+
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
189
|
setText(next);
|
|
115
190
|
setQuery(next);
|
|
116
191
|
}, onOpenChange: (isOpen, trigger) => {
|
|
@@ -126,23 +201,67 @@ function EditablePicker({ id: ownId, items, value, onChange, placeholder, onBrow
|
|
|
126
201
|
the options are the answer before the first letter. */
|
|
127
202
|
menuTrigger: "focus",
|
|
128
203
|
/* An empty list stays open to SAY it is empty (or loading) instead of vanishing. */
|
|
129
|
-
allowsEmptyCollection: true, isInvalid: invalid, isDisabled: disabled, className: "w-full min-w-0", children: [_jsx(CloseBridge, { into: closeList }), _jsxs(FieldGroup, { children: [_jsx(Input, { placeholder: loading ? shownLoading : open ? shownSearch : shownPlaceholder, title: shownSearch,
|
|
204
|
+
allowsEmptyCollection: true, isInvalid: invalid, isDisabled: disabled, className: "w-full min-w-0", children: [_jsx(CloseBridge, { into: closeList }), _jsxs(FieldGroup, { children: [chosen?.avatar === undefined || open ? null : (_jsx("span", { className: "flex shrink-0 items-center pl-2", children: _jsx(Avatar, { name: chosen.name, size: "sm", ...(typeof chosen.avatar === "string" ? { src: chosen.avatar } : {}) }) })), _jsx(Input, { placeholder: loading ? shownLoading : open ? shownSearch : shownPlaceholder, title: shownSearch,
|
|
130
205
|
/* Arriving selects the name, so the first letter typed replaces it. Only when
|
|
131
206
|
ARRIVING: React Aria hands the focus back to the input while the list is open,
|
|
132
207
|
and selecting again then would swallow every second letter. */
|
|
133
208
|
onFocus: (event) => {
|
|
134
209
|
if (!open)
|
|
135
210
|
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 &&
|
|
137
|
-
|
|
138
|
-
|
|
211
|
+
}, 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 &&
|
|
212
|
+
value !== "" &&
|
|
213
|
+
onOpen !== undefined &&
|
|
214
|
+
openTargets[0] !== undefined ? (_jsxs(_Fragment, { children: [_jsx(IconButton, { icon: OPEN_ICON[openTargets[0]], label: copy.picker.openIn[openTargets[0]], size: "sm", disabled: disabled, onClick: () => onOpen(value, openTargets[0]) }), openTargets.length < 2 || disabled ? null : (_jsx(Menu, { label: copy.picker.openMore, icon: Ellipsis, iconOnly: true, chevron: false, variant: "text", align: "end", groups: [
|
|
215
|
+
{
|
|
216
|
+
options: openTargets.slice(1).map((to) => ({
|
|
217
|
+
id: to,
|
|
218
|
+
label: copy.picker.openIn[to],
|
|
219
|
+
icon: OPEN_ICON[to],
|
|
220
|
+
})),
|
|
221
|
+
},
|
|
222
|
+
], onSelect: (to) => onOpen(value, to) }))] })) : 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, query: query, emptyText: loading ? shownLoading : shownEmpty,
|
|
223
|
+
/* Not while the first page loads: the sentinel of an empty list is in view and
|
|
224
|
+
would ask for page two before page one arrived. */
|
|
225
|
+
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] })] }));
|
|
139
226
|
}
|
|
140
|
-
|
|
227
|
+
/**
|
|
228
|
+
* The list of a `Picker`: its rows, the section headers, the «Buscar más…» row and the loading
|
|
229
|
+
* row at the end. Internal — not exported from the family — and apart from the combobox so it
|
|
230
|
+
* can be rendered on the server without a popover, which is how the tests see the rows.
|
|
231
|
+
*/
|
|
232
|
+
export function PickerOptions({ items, marks, hints, remaining, query = "", emptyText, onLoadMore, loadingMore = false, "aria-label": ariaLabel, }) {
|
|
233
|
+
const copy = useUiCopy();
|
|
234
|
+
const entries = useMemo(() => sectioned(items), [items]);
|
|
235
|
+
/* The actions are one block under the records: one hairline above the first of them. */
|
|
236
|
+
const firstAction = items.find((i) => ACTIONS.has(i.id))?.id;
|
|
237
|
+
const renderItem = (item) => ACTIONS.has(item.id) ? (_jsx(ListBoxItem, { id: item.id, textValue: item.name, className: (state) => cn(option({ ...state, hints: false }), "text-primary", item.id === firstAction && items[0]?.id !== item.id
|
|
238
|
+
? "mt-1 border-t border-line"
|
|
239
|
+
: ""), children: _jsxs("span", { className: "flex w-full items-center justify-between gap-2", children: [_jsx("span", { className: "min-w-0 truncate font-medium", children: item.name }), item.id !== BROWSE_ALL || remaining === 0 ? null : (_jsx("span", { className: "text-micro tabular-nums text-muted", children: fillCopy(copy.picker.more, {
|
|
240
|
+
count: remaining.toLocaleString(copy.locale),
|
|
241
|
+
}) }))] }) })) : (_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, query: query, locale: copy.locale })) }));
|
|
242
|
+
return (_jsxs(ListBox, { "aria-label": ariaLabel,
|
|
243
|
+
/* Nothing matched YET while the next page is coming: «nothing matches» above «loading
|
|
244
|
+
more…» says two contradicting things, so the empty list says only the second. */
|
|
245
|
+
renderEmptyState: () => (_jsx("p", { className: "px-3 py-3 text-center text-meta text-muted", children: loadingMore ? copy.picker.loadingMore : emptyText })),
|
|
246
|
+
/* Incremental, the list is not capped: it scrolls inside its own height and the
|
|
247
|
+
sentinel at its end is what asks for more. */
|
|
248
|
+
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, dependencies: [query], 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, dependencies: [query], 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 }))] }));
|
|
249
|
+
}
|
|
250
|
+
function Row({ item, selected, marks, hints, query, locale, }) {
|
|
141
251
|
const Icon = item.icon;
|
|
142
|
-
|
|
252
|
+
const copy = useUiCopy();
|
|
253
|
+
/* Listed for a code that is not drawn — the barcode, the NIT —: say which, or the row reads
|
|
254
|
+
as a wrong match. */
|
|
255
|
+
const hidden = query.trim() === "" ||
|
|
256
|
+
matches(item.name, query, locale) ||
|
|
257
|
+
(item.meta !== undefined && matches(item.meta, query, locale)) ||
|
|
258
|
+
item.haystack === undefined
|
|
259
|
+
? null
|
|
260
|
+
: matchedWord(item.haystack, query, locale);
|
|
261
|
+
return (_jsxs(_Fragment, { children: [marks ? (_jsx("span", { className: cn(hints ? "mt-0.5" : "", "flex size-(--size-icon-sm) shrink-0 items-center justify-center"), children: selected ? (_jsx(Check, { "aria-hidden": true, className: "size-(--size-icon-sm) text-primary forced-colors:text-[HighlightText]", strokeWidth: 2.5 })) : Icon !== undefined ? (_jsx(Icon, { "aria-hidden": true, className: "size-(--size-icon-sm) text-muted", strokeWidth: 1.75 })) : null })) : null, item.avatar === undefined ? null : (_jsx(Avatar, { name: item.name, size: "sm", ...(typeof item.avatar === "string" ? { src: item.avatar } : {}) })), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx(Text, { slot: "label", className: cn("min-w-0 truncate text-meta text-ink", selected ? "font-medium" : ""), children: _jsx(MatchedText, { text: item.name, query: query, locale: locale }) }), item.tags] }), item.meta === undefined ? null : (_jsx(Text, { slot: "description", className: "truncate text-micro tabular-nums text-muted", children: _jsx(MatchedText, { text: item.meta, query: query, locale: locale }) })), hidden === null ? null : (_jsxs("span", { className: "truncate text-micro text-muted", children: [copy.picker.matched, " ", _jsx("span", { className: "font-mono", children: _jsx(MatchedText, { text: hidden, query: query, locale: locale }) })] }))] }), item.aside === undefined ? null : (_jsx("span", { className: "flex shrink-0 items-center gap-1.5 self-center text-meta text-ink tabular-nums", children: item.aside })), !marks && selected ? (_jsx(Check, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0 text-primary forced-colors:text-[HighlightText]", strokeWidth: 2.5 })) : null] }));
|
|
143
262
|
}
|
|
144
263
|
/** Hands the combobox's `close` to the code outside it: React Aria owns the open state and
|
|
145
|
-
* offers no controlled `isOpen`, and the «
|
|
264
|
+
* offers no controlled `isOpen`, and the «Buscar más…» row has to close the list it lives in. */
|
|
146
265
|
function CloseBridge({ into }) {
|
|
147
266
|
const state = useContext(ComboBoxStateContext);
|
|
148
267
|
into.current = state === null ? null : () => state.close();
|
|
@@ -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";
|