@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
|
@@ -6,9 +6,27 @@ import { pathKey, pileIsOpen, pileItems, pilesOf, } from "../../foundations/grou
|
|
|
6
6
|
import { HANDHELD_WIDTH } from "../../foundations/layout.js";
|
|
7
7
|
import { stickyTop, useScrollBox } from "../../foundations/sticky.js";
|
|
8
8
|
import { IconButton } from "../button/Button.js";
|
|
9
|
+
import { NoteCard } from "../field/note.js";
|
|
10
|
+
import { Flag } from "../notice/Flag.js";
|
|
9
11
|
import { Plus, Trash2 } from "lucide-react";
|
|
10
12
|
import { Fragment, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
11
13
|
import { useKeyboard } from "react-aria";
|
|
14
|
+
/** The lines each section heads: the rows after it up to the next section. Notes do not
|
|
15
|
+
* cut a section, and are not lines. */
|
|
16
|
+
export function sectionLines(rows, getRowId, kind) {
|
|
17
|
+
const out = new Map();
|
|
18
|
+
let current = null;
|
|
19
|
+
for (const row of rows) {
|
|
20
|
+
const k = kind(row);
|
|
21
|
+
if (k === "section") {
|
|
22
|
+
current = [];
|
|
23
|
+
out.set(getRowId(row), current);
|
|
24
|
+
}
|
|
25
|
+
else if (k === null)
|
|
26
|
+
current?.push(row);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
12
30
|
/**
|
|
13
31
|
* Where each column goes in the stacked card, defaults resolved: a declared `stack`
|
|
14
32
|
* wins; otherwise the first undeclared column is the title (unless one was declared),
|
|
@@ -36,14 +54,47 @@ function pileTag(pile, locale) {
|
|
|
36
54
|
/** The focus ring of an operable row: drawn on the row, inset, so it is not cut by the
|
|
37
55
|
* box and does not move the cells. */
|
|
38
56
|
const RING = "outline-0 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary forced-colors:focus-visible:outline-[Highlight]";
|
|
39
|
-
/** What can take the focus in a row. Filtered again in `
|
|
40
|
-
*
|
|
57
|
+
/** What can take the focus in a row. Filtered again in `stopsIn`: React Aria's hidden form
|
|
58
|
+
* mirrors (a `<select>` behind a `Select`, an `<input hidden>` in a date picker) match this
|
|
59
|
+
* and are not stops. */
|
|
41
60
|
const FOCUSABLE = 'input:not([type="hidden"]):not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), a[href], [tabindex]';
|
|
61
|
+
/** What `Tab` stops on inside `scope` of a row: focusable, and not a hidden form mirror.
|
|
62
|
+
*
|
|
63
|
+
* Context: NOT filtered by `aria-hidden` / `inert`. While a list is open (a tags field, a
|
|
64
|
+
* picker) React Aria hides everything outside it that way — the rest of the row included —
|
|
65
|
+
* and the row then looked as if its open control were its last: `Tab` added a new row from
|
|
66
|
+
* the middle of this one. The mirrors carry their own marks instead. */
|
|
67
|
+
function stopsIn(row, scope) {
|
|
68
|
+
return [...row.querySelectorAll(scope)].filter((el) => el.tabIndex >= 0 && el.closest("[hidden], [data-react-aria-prevent-focus]") === null);
|
|
69
|
+
}
|
|
70
|
+
/** The controls of the editable cells of a row, in tab order. */
|
|
71
|
+
function editStops(row) {
|
|
72
|
+
return stopsIn(row, `[data-edit] :is(${FOCUSABLE})`);
|
|
73
|
+
}
|
|
42
74
|
/** The first control of a row being edited: the first one in an editable cell, else
|
|
43
75
|
* anything focusable in the row (a column without `edit` may still hold a button). */
|
|
44
|
-
function firstControl(row) {
|
|
45
|
-
|
|
46
|
-
|
|
76
|
+
export function firstControl(row) {
|
|
77
|
+
return editStops(row)[0] ?? stopsIn(row, FOCUSABLE)[0] ?? null;
|
|
78
|
+
}
|
|
79
|
+
/** The last control of a row being edited — where `Shift+Tab` from the next row lands. */
|
|
80
|
+
function lastControl(row) {
|
|
81
|
+
return editStops(row).at(-1) ?? stopsIn(row, FOCUSABLE).at(-1) ?? null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Where `Tab` past the last control (`next`) or `Shift+Tab` before the first (`prev`) of
|
|
85
|
+
* the edited row `from` goes, over the rows in the order they are drawn.
|
|
86
|
+
*
|
|
87
|
+
* Past the last row it adds one when the table can — unless `from` is itself a row just
|
|
88
|
+
* added and still blank: one more `Tab` there means «I am done», not «another blank line».
|
|
89
|
+
*/
|
|
90
|
+
export function tabTarget(order, from, direction, { canAdd, blank }) {
|
|
91
|
+
const at = order.indexOf(from);
|
|
92
|
+
if (at === -1)
|
|
93
|
+
return { kind: "leave" };
|
|
94
|
+
const to = order[direction === "next" ? at + 1 : at - 1];
|
|
95
|
+
if (to !== undefined)
|
|
96
|
+
return { kind: "row", id: to };
|
|
97
|
+
return direction === "next" && canAdd && !blank ? { kind: "add" } : { kind: "leave" };
|
|
47
98
|
}
|
|
48
99
|
/** Keys a control inside the row already answers with `Enter`: a button presses, a
|
|
49
100
|
* link follows, a text area breaks the line, an open list picks its option. */
|
|
@@ -54,10 +105,11 @@ function ownsEnter(target) {
|
|
|
54
105
|
/**
|
|
55
106
|
* One body row. Its own component so that it can hold React Aria's `useKeyboard`
|
|
56
107
|
* (a hook per row) and the timer that decides whether the focus really left it.
|
|
57
|
-
* The same row is a `<tr>` in the table and an `<li>` in the stacked list
|
|
58
|
-
* keyboard model,
|
|
108
|
+
* The same row is a `<tr>` in the table and an `<li>` in the stacked list — and the row
|
|
109
|
+
* of a new record in `DataTable`: one keyboard model, three shapes. Exported for that
|
|
110
|
+
* family sibling only; it is not in the barrel.
|
|
59
111
|
*/
|
|
60
|
-
function EditRow({ as, rowId, rowRef, editable, editing, tabbable, className, onStart, onCommit, onCancel, onMove, onFocusRow, children, }) {
|
|
112
|
+
export function EditRow({ as, rowId, rowRef, editable, editing, tabbable, className, onStart, onCommit, onCancel, onMove, onFocusRow, onTabOut, children, }) {
|
|
61
113
|
const leaving = useRef(null);
|
|
62
114
|
const stay = () => {
|
|
63
115
|
if (leaving.current !== null)
|
|
@@ -108,10 +160,23 @@ function EditRow({ as, rowId, rowRef, editable, editing, tabbable, className, on
|
|
|
108
160
|
that field write the value it was holding (it commits on blur). */
|
|
109
161
|
onKeyDownCapture: editing
|
|
110
162
|
? (e) => {
|
|
111
|
-
if (e.
|
|
163
|
+
if (e.nativeEvent.isComposing)
|
|
112
164
|
return;
|
|
113
165
|
if (!(e.target instanceof Node) || !e.currentTarget.contains(e.target))
|
|
114
166
|
return;
|
|
167
|
+
if (e.key === "Tab") {
|
|
168
|
+
/* Only the default is prevented, never the propagation: a combobox still
|
|
169
|
+
hears its `Tab` and takes the option it has marked before the row moves. */
|
|
170
|
+
const stops = editStops(e.currentTarget);
|
|
171
|
+
const edge = e.shiftKey ? stops[0] : stops.at(-1);
|
|
172
|
+
if (edge === undefined || !edge.contains(e.target))
|
|
173
|
+
return;
|
|
174
|
+
if (onTabOut(e.shiftKey ? "prev" : "next"))
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (e.key !== "Enter")
|
|
179
|
+
return;
|
|
115
180
|
if (ownsEnter(e.target))
|
|
116
181
|
return;
|
|
117
182
|
e.preventDefault();
|
|
@@ -174,8 +239,32 @@ totals, empty, grouping,
|
|
|
174
239
|
/** El total de una columna DENTRO de un montón, ya formateado. Lo calcula quien
|
|
175
240
|
* compone, por el mismo motivo que `totals`: sumar significa cosas distintas
|
|
176
241
|
* según el dominio, y un porcentaje se promedia, no se suma. */
|
|
177
|
-
groupTotal, height, layout = "auto", onOpen, }) {
|
|
242
|
+
groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, }) {
|
|
178
243
|
const copy = useUiCopy();
|
|
244
|
+
const bandOf = (row) => bands?.kind(row) ?? null;
|
|
245
|
+
/* The row's mark, before its first value — and, in a record's margin, its card. */
|
|
246
|
+
const flagOf = (row, withCard = false) => {
|
|
247
|
+
const said = flag?.(row) ?? null;
|
|
248
|
+
if (said === null)
|
|
249
|
+
return null;
|
|
250
|
+
return (_jsxs(_Fragment, { children: [_jsx(Flag, { tone: said.tone, label: said.label, children: said.detail }), withCard ? (_jsx(NoteCard, { id: `flag-${getRowId(row)}`, ...(said.owner === undefined ? {} : { owner: said.owner }), note: {
|
|
251
|
+
tone: said.tone,
|
|
252
|
+
label: said.label,
|
|
253
|
+
...(said.detail === undefined ? {} : { detail: said.detail }),
|
|
254
|
+
} })) : null] }));
|
|
255
|
+
};
|
|
256
|
+
const underSection = useMemo(() => (bands === undefined ? null : sectionLines(rows, getRowId, bands.kind)), [rows, bands, getRowId]);
|
|
257
|
+
/* Where a band's text stops: the first column with something of its own on that row. */
|
|
258
|
+
const bandFigures = (row) => {
|
|
259
|
+
const lines = underSection?.get(getRowId(row)) ?? null;
|
|
260
|
+
const figures = columns.map((c) => c.onBands === true
|
|
261
|
+
? c.render(row)
|
|
262
|
+
: lines === null
|
|
263
|
+
? null
|
|
264
|
+
: (bands?.total?.(c, lines) ?? null));
|
|
265
|
+
const first = figures.findIndex((f, i) => columns[i]?.onBands === true || (f !== null && f !== undefined));
|
|
266
|
+
return { span: first <= 0 ? columns.length : first, figures };
|
|
267
|
+
};
|
|
179
268
|
const d = useDensity();
|
|
180
269
|
const box = useScrollBox(height === undefined ? {} : { height });
|
|
181
270
|
const [editing, setEditing] = useState(null);
|
|
@@ -185,9 +274,13 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
185
274
|
/* The row as it was when editing began, to put it back on `Escape`; `touched` says
|
|
186
275
|
whether any control wrote to it since, so a cancel without changes changes nothing. */
|
|
187
276
|
const before = useRef(null);
|
|
188
|
-
/* Set when the keyboard opened the row: its first control
|
|
189
|
-
the controls exist. A click opens it without
|
|
277
|
+
/* Set when the keyboard opened the row: its first control —or its last, coming back
|
|
278
|
+
with `Shift+Tab`— takes the focus once the controls exist. A click opens it without
|
|
279
|
+
moving the focus. */
|
|
190
280
|
const focusOnOpen = useRef(null);
|
|
281
|
+
/* The id `onAdd` returned, until that row shows up in `rows` and can be opened. */
|
|
282
|
+
const [arriving, setArriving] = useState(null);
|
|
283
|
+
const addButton = useRef(null);
|
|
191
284
|
/* La secuencia que se dibuja: montones y filas intercalados. Sin agrupado, las
|
|
192
285
|
filas tal cual — una tabla que no agrupa no paga nada por poder hacerlo. */
|
|
193
286
|
const drawn = useMemo(() => {
|
|
@@ -240,35 +333,82 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
240
333
|
const opens = stacked && onOpen !== undefined;
|
|
241
334
|
const operable = editable || opens;
|
|
242
335
|
const cell = byDensity(d, "px-3 py-1.5", "px-2 py-1");
|
|
243
|
-
const
|
|
336
|
+
const addText = addLabel ?? copy.editable.add;
|
|
244
337
|
const emptyText = empty ?? copy.editable.empty;
|
|
245
338
|
/* The rows in the order they are drawn: what the arrows walk. */
|
|
246
339
|
const order = drawn.flatMap((entry) => entry.row === undefined ? [] : [getRowId(entry.row)]);
|
|
247
340
|
const stop = active !== null && order.includes(active) ? active : (order[0] ?? null);
|
|
248
341
|
useEffect(() => {
|
|
249
|
-
const
|
|
250
|
-
if (
|
|
342
|
+
const target = focusOnOpen.current;
|
|
343
|
+
if (target === null || target.id !== editing)
|
|
251
344
|
return;
|
|
252
345
|
focusOnOpen.current = null;
|
|
253
|
-
const node = rowNodes.current.get(id);
|
|
254
|
-
if (node
|
|
255
|
-
|
|
346
|
+
const node = rowNodes.current.get(target.id);
|
|
347
|
+
if (node === undefined)
|
|
348
|
+
return;
|
|
349
|
+
const control = target.at === "last" ? lastControl(node) : firstControl(node);
|
|
350
|
+
(control ?? node).focus();
|
|
256
351
|
}, [editing]);
|
|
257
|
-
const start = (id, row, viaKeyboard) => {
|
|
258
|
-
|
|
352
|
+
const start = (id, row, viaKeyboard, { at = "first", added = false } = {}) => {
|
|
353
|
+
/* A row just added is written where it appeared, card or not: it was added in place
|
|
354
|
+
precisely so that it would not open somewhere else. */
|
|
355
|
+
if (opens && !added && bandOf(row) === null) {
|
|
259
356
|
setActive(id);
|
|
260
357
|
onOpen?.(id);
|
|
261
358
|
return;
|
|
262
359
|
}
|
|
263
|
-
before.current = { id, row, touched: false };
|
|
264
|
-
focusOnOpen.current = viaKeyboard ? id : null;
|
|
360
|
+
before.current = { id, row, touched: false, added };
|
|
361
|
+
focusOnOpen.current = viaKeyboard ? { id, at } : null;
|
|
265
362
|
setActive(id);
|
|
266
363
|
setEditing(id);
|
|
267
364
|
};
|
|
365
|
+
/* The added row is opened the render it appears in — which may be this one, or the one
|
|
366
|
+
after the backend answered. */
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
if (arriving === null)
|
|
369
|
+
return;
|
|
370
|
+
const row = rows.find((r) => getRowId(r) === arriving);
|
|
371
|
+
if (row === undefined)
|
|
372
|
+
return;
|
|
373
|
+
setArriving(null);
|
|
374
|
+
start(arriving, row, true, { added: true });
|
|
375
|
+
/* `start` and `getRowId` are new every render; what decides is the id and the rows. */
|
|
376
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
377
|
+
}, [arriving, rows]);
|
|
378
|
+
const add = (maker = onAdd) => {
|
|
379
|
+
if (maker === undefined)
|
|
380
|
+
return;
|
|
381
|
+
const made = maker();
|
|
382
|
+
if (typeof made === "string")
|
|
383
|
+
setArriving(made);
|
|
384
|
+
else if (made instanceof Promise) {
|
|
385
|
+
void made.then((id) => {
|
|
386
|
+
if (typeof id === "string")
|
|
387
|
+
setArriving(id);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
};
|
|
268
391
|
/* Keyed by the row: a commit that arrives late (the blur of the row you clicked
|
|
269
392
|
away from) must not close the row the click just opened. */
|
|
270
393
|
const close = (id) => setEditing((current) => (current === id ? null : current));
|
|
394
|
+
/* A row added and abandoned goes away. `true` when it did. */
|
|
395
|
+
const discard = (id, untouchedOnly) => {
|
|
396
|
+
const was = before.current;
|
|
397
|
+
if (was === null || was.id !== id || !was.added || onDiscard === undefined)
|
|
398
|
+
return false;
|
|
399
|
+
if (untouchedOnly && was.touched)
|
|
400
|
+
return false;
|
|
401
|
+
before.current = null;
|
|
402
|
+
close(id);
|
|
403
|
+
onDiscard(id);
|
|
404
|
+
return true;
|
|
405
|
+
};
|
|
271
406
|
const commit = (id, refocus) => {
|
|
407
|
+
if (discard(id, true)) {
|
|
408
|
+
if (refocus)
|
|
409
|
+
addButton.current?.focus();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
272
412
|
if (refocus)
|
|
273
413
|
rowNodes.current.get(id)?.focus();
|
|
274
414
|
if (before.current?.id === id)
|
|
@@ -280,12 +420,34 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
280
420
|
(a number field) writes it now, and the old row is put back after it, not
|
|
281
421
|
before. */
|
|
282
422
|
rowNodes.current.get(id)?.focus();
|
|
423
|
+
if (discard(id, false)) {
|
|
424
|
+
/* The row is gone with the focus in it: «add» is where it came from. */
|
|
425
|
+
addButton.current?.focus();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
283
428
|
const was = before.current;
|
|
284
429
|
before.current = null;
|
|
285
430
|
if (was !== null && was.id === id && was.touched)
|
|
286
431
|
onChange?.(id, was.row);
|
|
287
432
|
close(id);
|
|
288
433
|
};
|
|
434
|
+
const tabOut = (from, direction) => {
|
|
435
|
+
const was = before.current;
|
|
436
|
+
const blank = was !== null && was.id === from && was.added && !was.touched;
|
|
437
|
+
const target = tabTarget(order, from, direction, { canAdd: onAdd !== undefined, blank });
|
|
438
|
+
if (target.kind === "leave")
|
|
439
|
+
return false;
|
|
440
|
+
commit(from, false);
|
|
441
|
+
if (target.kind === "add") {
|
|
442
|
+
add();
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
const row = rows.find((r) => getRowId(r) === target.id);
|
|
446
|
+
if (row === undefined)
|
|
447
|
+
return false;
|
|
448
|
+
start(target.id, row, true, { at: direction === "next" ? "first" : "last" });
|
|
449
|
+
return true;
|
|
450
|
+
};
|
|
289
451
|
/* What a control writes, marking the row as touched for `Escape`. */
|
|
290
452
|
const setter = (id) => (next) => {
|
|
291
453
|
if (before.current?.id === id)
|
|
@@ -309,6 +471,7 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
309
471
|
onCancel: () => cancel(id),
|
|
310
472
|
onMove: (key) => move(id, key),
|
|
311
473
|
onFocusRow: () => setActive(id),
|
|
474
|
+
onTabOut: (direction) => tabOut(id, direction),
|
|
312
475
|
});
|
|
313
476
|
const move = (from, key) => {
|
|
314
477
|
const at = order.indexOf(from);
|
|
@@ -349,7 +512,7 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
349
512
|
];
|
|
350
513
|
});
|
|
351
514
|
const controls = editable && isEditing ? columns.filter((c) => c.edit !== undefined) : [];
|
|
352
|
-
return (_jsxs(EditRow, { as: "li", ...rowWiring(id, row), className: `group/row flex min-w-0 flex-col gap-1 rounded-control border-b border-line ${pad} ${isEditing ? "bg-primary/5" : "hover:bg-ink/4"} ${operable ? RING : ""} ${operable && !isEditing ? (opens ? "cursor-pointer" : "cursor-text") : ""}`, children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [_jsx("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5 text-meta text-ink", children: titles.map((c) => (_jsx("div", { className: "min-w-0", children: c.render(row) }, c.id))) }), trails.map((c) => (_jsx("div", { className: "shrink-0", children: c.render(row) }, c.id))), isEditing && onRemove !== undefined ? (_jsx(IconButton, { icon: Trash2, label: copy.editable.remove, size: "sm", tone: "danger", onClick: () => onRemove(id) })) : null] }), parts.length === 0 && amounts.length === 0 ? null : (_jsxs("div", { className: "flex min-w-0 items-baseline gap-3 text-meta", children: [_jsx("span", { className: "min-w-0 flex-1 text-muted tabular-nums", children: parts.map((p, i) => (_jsxs(Fragment, { children: [i > 0 ? " · " : null, p.node] }, p.id))) }), amounts.map((c) => (
|
|
515
|
+
return (_jsxs(EditRow, { as: "li", ...rowWiring(id, row), className: `group/row flex min-w-0 flex-col gap-1 rounded-control border-b border-line ${pad} ${isEditing ? "bg-primary/5" : "hover:bg-ink/4"} ${operable ? RING : ""} ${operable && !isEditing ? (opens ? "cursor-pointer" : "cursor-text") : ""}`, children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [flagOf(row), _jsx("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5 text-meta text-ink", children: titles.map((c) => (_jsx("div", { className: "min-w-0", children: c.render(row) }, c.id))) }), trails.map((c) => (_jsx("div", { className: "shrink-0", children: c.render(row) }, c.id))), isEditing && onRemove !== undefined ? (_jsx(IconButton, { icon: Trash2, label: copy.editable.remove, size: "sm", tone: "danger", onClick: () => onRemove(id) })) : null] }), parts.length === 0 && amounts.length === 0 ? null : (_jsxs("div", { className: "flex min-w-0 items-baseline gap-3 text-meta", children: [_jsx("span", { className: "min-w-0 flex-1 text-muted tabular-nums", children: parts.map((p, i) => (_jsxs(Fragment, { children: [i > 0 ? " · " : null, p.node] }, p.id))) }), amounts.map((c) => (
|
|
353
516
|
/* The amount is read by its figure on screen and by its name aloud: a
|
|
354
517
|
lone «Bs. 1.296,00» at the end of a card does not say what it is. */
|
|
355
518
|
_jsxs("span", { className: "shrink-0 text-right font-medium whitespace-nowrap text-ink tabular-nums", children: [_jsxs("span", { className: "sr-only", children: [c.header, " "] }), c.render(row)] }, c.id)))] })), controls.length === 0 ? null : (
|
|
@@ -358,6 +521,17 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
358
521
|
guess. The name is drawn for the eye; the control carries its own label. */
|
|
359
522
|
_jsx("div", { className: "grid grid-cols-2 gap-x-3 gap-y-2 pt-1.5", children: controls.map((c) => (_jsxs("div", { "data-edit": "", className: `flex min-w-0 flex-col gap-1 ${c.numeric === true ? "" : "col-span-2"}`, children: [_jsx("span", { "aria-hidden": "true", className: "text-micro text-muted", children: c.header }), c.edit?.(row, set)] }, c.id))) }))] }, id));
|
|
360
523
|
};
|
|
524
|
+
/* A section or a note on a phone: the band and, for a section, what it adds up to in
|
|
525
|
+
the card's amount; editing, its control under it. */
|
|
526
|
+
const bandCard = (row) => {
|
|
527
|
+
const id = getRowId(row);
|
|
528
|
+
const band = bandOf(row);
|
|
529
|
+
const isEditing = editing === id;
|
|
530
|
+
const lines = underSection?.get(id) ?? null;
|
|
531
|
+
return (_jsxs(EditRow, { as: "li", ...rowWiring(id, row), className: `flex min-w-0 flex-col gap-1.5 rounded-control border-b border-line ${pad} ${isEditing ? "bg-primary/5" : band === "section" ? "bg-ground" : ""} ${operable ? RING : ""}`, children: [_jsxs("div", { className: "flex min-w-0 items-baseline gap-3 text-meta", children: [_jsx("span", { className: `min-w-0 flex-1 ${band === "section" ? "font-semibold text-ink" : "text-muted italic"}`, children: bands?.render(row) }), lines === null
|
|
532
|
+
? null
|
|
533
|
+
: amounts.map((c) => (_jsx("span", { className: "shrink-0 font-semibold text-ink tabular-nums", children: bands?.total?.(c, lines) ?? null }, c.id))), isEditing && onRemove !== undefined ? (_jsx(IconButton, { icon: Trash2, label: copy.editable.remove, size: "sm", tone: "danger", onClick: () => onRemove(id) })) : null] }), isEditing && editable && bands?.edit !== undefined ? (_jsx("div", { "data-edit": "", children: bands.edit(row, setter(id)) })) : null] }, id));
|
|
534
|
+
};
|
|
361
535
|
const list = (_jsxs(_Fragment, { children: [_jsx("ul", { "data-stacked": "", className: "flex min-w-0 flex-col px-1", children: rows.length === 0 ? (_jsx("li", { className: `text-meta text-muted italic ${pad}`, children: emptyText })) : (drawn.map((entry) => {
|
|
362
536
|
if (entry.pile !== undefined) {
|
|
363
537
|
const pile = entry.pile;
|
|
@@ -371,7 +545,8 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
371
545
|
}
|
|
372
546
|
: undefined, children: [_jsx("span", { className: "min-w-0 flex-1", children: pileTag(pile, copy.locale) }), amounts.map((c) => (_jsx("span", { className: "shrink-0 text-ink tabular-nums", children: groupTotal?.(c, inside) ?? null }, c.id)))] }, `pile:${pathKey(pile.path)}`));
|
|
373
547
|
}
|
|
374
|
-
|
|
548
|
+
const row = entry.row;
|
|
549
|
+
return bandOf(row) === null ? card(row) : bandCard(row);
|
|
375
550
|
})) }), totals === undefined ? null : (
|
|
376
551
|
/* The footer's aggregates, one per line with the column's name: under a card
|
|
377
552
|
there is no column to stand under. */
|
|
@@ -401,6 +576,22 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
401
576
|
const row = entry.row;
|
|
402
577
|
const id = getRowId(row);
|
|
403
578
|
const isEditing = editing === id;
|
|
579
|
+
const band = bandOf(row);
|
|
580
|
+
if (band !== null) {
|
|
581
|
+
const { span, figures } = bandFigures(row);
|
|
582
|
+
const bandWash = isEditing
|
|
583
|
+
? "bg-primary/5"
|
|
584
|
+
: band === "section"
|
|
585
|
+
? "bg-ground group-hover/row:bg-ink/6"
|
|
586
|
+
: "group-hover/row:bg-ink/4";
|
|
587
|
+
const bandMark = `border-b border-line first:rounded-l-control last:rounded-r-control ${bandWash}`;
|
|
588
|
+
const control = isEditing && editable && bands?.edit !== undefined;
|
|
589
|
+
return (_jsxs(EditRow, { as: "tr", ...rowWiring(id, row), className: `group/row ${editable ? RING : ""} ${editable && !isEditing ? "cursor-text" : ""}`, children: [_jsx("td", { colSpan: span, ...(control ? { "data-edit": "" } : {}), className: `text-meta ${bandMark} ${cell} ${band === "section"
|
|
590
|
+
? "font-semibold text-ink"
|
|
591
|
+
: "text-muted italic"}`, children: control ? bands?.edit?.(row, setter(id)) : bands?.render(row) }), columns.slice(span).map((c, i) => (_jsx("td", { className: `text-meta text-ink ${bandMark} ${cell} ${c.numeric === true
|
|
592
|
+
? "text-right font-semibold tabular-nums"
|
|
593
|
+
: ""}`, children: figures[span + i] ?? null }, c.id))), onRemove === undefined ? null : (_jsx("td", { className: `w-0 py-1 pr-2 pl-1 align-middle ${bandMark}`, children: _jsx("span", { className: "flex opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 group-focus-visible/row:opacity-100 focus-within:opacity-100", onClick: (e) => e.stopPropagation(), children: _jsx(IconButton, { icon: Trash2, label: copy.editable.remove, size: "sm", tone: "danger", ...(editable && stop !== id ? { tabIndex: -1 } : {}), onClick: () => onRemove(id) }) }) }))] }, id));
|
|
594
|
+
}
|
|
404
595
|
/* The wash lives on the cells. A background on the row paints
|
|
405
596
|
edge to edge, and `border-collapse` would ignore a radius. */
|
|
406
597
|
const wash = isEditing ? "bg-primary/5" : "group-hover/row:bg-ink/4";
|
|
@@ -409,15 +600,18 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
409
600
|
return (_jsxs(EditRow, { as: "tr", ...rowWiring(id, row),
|
|
410
601
|
/* The focus ring is drawn on the row, inset, so it is not cut by
|
|
411
602
|
the box and does not move the cells. */
|
|
412
|
-
className: `group/row ${editable ? RING : ""} ${editable && !isEditing ? "cursor-text" : ""}`, children: [columns.map((c) => {
|
|
603
|
+
className: `group/row ${editable ? RING : ""} ${editable && !isEditing ? "cursor-text" : ""}`, children: [columns.map((c, ci) => {
|
|
413
604
|
const control = isEditing && c.edit !== undefined && editable;
|
|
414
|
-
|
|
605
|
+
const mark = ci === 0 ? flagOf(row, true) : null;
|
|
606
|
+
return (_jsx("td", { ...(control ? { "data-edit": "" } : {}), className: `text-meta text-ink ${mark} ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: mark === null ? (control ? (c.edit?.(row, set)) : (c.render(row))) : (_jsxs("span", { className: "flex min-w-0 items-start gap-1.5", children: [mark, _jsx("span", { className: "min-w-0 flex-1", children: control ? c.edit?.(row, set) : c.render(row) })] })) }, c.id));
|
|
415
607
|
}), onRemove === undefined ? null : (_jsx("td", { className: `w-0 py-1 pr-2 pl-1 align-middle ${mark}`, children: _jsx("span", { className: "flex opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 group-focus-visible/row:opacity-100 focus-within:opacity-100", onClick: (e) => e.stopPropagation(), children: _jsx(IconButton, { icon: Trash2, label: copy.editable.remove, size: "sm", tone: "danger", ...(editable && stop !== id ? { tabIndex: -1 } : {}), onClick: () => onRemove(id) }) }) }))] }, id));
|
|
416
|
-
})) }), totals === undefined ? null : (_jsx("tfoot", { children: _jsxs("tr", { children: [columns.map((c, i) => (_jsx("td", { className: `border-t border-line-strong text-meta font-semibold text-ink ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: totals[i] ?? null }, c.id))), onRemove === undefined ? null : (_jsx("td", { className: "border-t border-line-strong" }))] }) }))] })) }), onAdd === undefined ? null : (
|
|
608
|
+
})) }), totals === undefined ? null : (_jsx("tfoot", { children: _jsxs("tr", { children: [columns.map((c, i) => (_jsx("td", { className: `border-t border-line-strong text-meta font-semibold text-ink ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: totals[i] ?? null }, c.id))), onRemove === undefined ? null : (_jsx("td", { className: "border-t border-line-strong" }))] }) }))] })) }), onAdd === undefined && (adds === undefined || adds.length === 0) ? null : (
|
|
417
609
|
/* The «add» goes under the grid, as text, not as a button away from it:
|
|
418
610
|
what is about to happen is that one more row shows up right here.
|
|
419
611
|
The hover is the same inset mark as a row: the shell lines the label
|
|
420
612
|
up with the cells, and the button itself is the rounded wash. It sits
|
|
421
|
-
outside the scroll box, so a table with its own height keeps it in view.
|
|
422
|
-
|
|
613
|
+
outside the scroll box, so a table with its own height keeps it in view.
|
|
614
|
+
A section and a note sit beside it, quieter: the line is what is added
|
|
615
|
+
most, so it keeps the lead and the whole free width. */
|
|
616
|
+
_jsxs("div", { className: "flex flex-wrap items-center gap-x-1 px-1 py-0.5", children: [onAdd === undefined ? null : (_jsxs("button", { ref: addButton, type: "button", onClick: () => add(), className: `flex items-center gap-1.5 rounded-control 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 ${adds === undefined || adds.length === 0 ? "w-full" : ""} ${byDensity(d, "px-3 py-1", "px-2 py-0.5")}`, children: [_jsx(Plus, { className: "size-(--size-icon-sm)", strokeWidth: 2.5, "aria-hidden": true }), addText] })), (adds ?? []).map((extra) => (_jsx("button", { type: "button", onClick: () => add(extra.onAdd), className: `rounded-control text-left text-meta text-muted transition-colors duration-(--duration-instant) hover:bg-primary/5 hover:text-primary focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary ${byDensity(d, "px-3 py-1", "px-2 py-0.5")}`, children: extra.label }, extra.label)))] }))] }));
|
|
423
617
|
}
|
|
@@ -20,7 +20,7 @@ value,
|
|
|
20
20
|
* nothing. */
|
|
21
21
|
tone,
|
|
22
22
|
/** What the cell says on hover: «imputado por la mediana». */
|
|
23
|
-
note, height, rowLabel, band, onBandToggle, caption, onCommit, editable, }: {
|
|
23
|
+
note, height, rowLabel, band, onBandToggle, caption, onCommit, editable, onAddRow, }: {
|
|
24
24
|
columns: readonly SheetColumn[];
|
|
25
25
|
rows: number;
|
|
26
26
|
value: (row: number, column: SheetColumn, columnIndex: number) => string | number | null;
|
|
@@ -67,4 +67,7 @@ note, height, rowLabel, band, onBandToggle, caption, onCommit, editable, }: {
|
|
|
67
67
|
/** Which cells are editable. Without this, all of them — but a computed column
|
|
68
68
|
* or an identifier is not touched. */
|
|
69
69
|
editable?: (row: number, column: SheetColumn, columnIndex: number) => boolean;
|
|
70
|
+
/** Present = rows can be added (see the file's header). Only with `onCommit`: a row
|
|
71
|
+
* that cannot be written in is not worth adding. */
|
|
72
|
+
onAddRow?: () => void;
|
|
70
73
|
}): import("react").JSX.Element;
|
|
@@ -4,8 +4,8 @@ import { useUiCopy } from "../../foundations/copy.js";
|
|
|
4
4
|
import { stickyTop, useScrollBox } from "../../foundations/sticky.js";
|
|
5
5
|
import { Cell } from "../value/Value.js";
|
|
6
6
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
7
|
-
import { ChevronRight } from "lucide-react";
|
|
8
|
-
import { useState } from "react";
|
|
7
|
+
import { ChevronRight, Plus } from "lucide-react";
|
|
8
|
+
import { useEffect, useState } from "react";
|
|
9
9
|
const TONE = {
|
|
10
10
|
neutral: "bg-ink/6",
|
|
11
11
|
info: "bg-info/12",
|
|
@@ -26,7 +26,7 @@ value,
|
|
|
26
26
|
* nothing. */
|
|
27
27
|
tone,
|
|
28
28
|
/** What the cell says on hover: «imputado por la mediana». */
|
|
29
|
-
note, height = 420, rowLabel, band, onBandToggle, caption, onCommit, editable, }) {
|
|
29
|
+
note, height = 420, rowLabel, band, onBandToggle, caption, onCommit, editable, onAddRow, }) {
|
|
30
30
|
const copy = useUiCopy();
|
|
31
31
|
const labelOf = rowLabel ?? ((i) => (i + 1).toLocaleString(copy.locale));
|
|
32
32
|
/* `"auto"` es para un conjunto que ya vino CORTADO —una página de ochenta— y
|
|
@@ -78,6 +78,27 @@ note, height = 420, rowLabel, band, onBandToggle, caption, onCommit, editable, }
|
|
|
78
78
|
if (dr !== 0 || dc !== 0)
|
|
79
79
|
goTo(active.r + dr, active.c + dc);
|
|
80
80
|
}
|
|
81
|
+
/* The column the active cell waits in until the added row arrives, and how many rows
|
|
82
|
+
there were when it was asked for. */
|
|
83
|
+
const canAdd = isEditable && onAddRow !== undefined;
|
|
84
|
+
const [arriving, setArriving] = useState(null);
|
|
85
|
+
function addRow(c) {
|
|
86
|
+
if (!canAdd)
|
|
87
|
+
return;
|
|
88
|
+
setArriving({ c, from: rows });
|
|
89
|
+
onAddRow?.();
|
|
90
|
+
}
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (arriving === null || rows <= arriving.from)
|
|
93
|
+
return;
|
|
94
|
+
setArriving(null);
|
|
95
|
+
setActive({ r: rows - 1, c: arriving.c });
|
|
96
|
+
setDraft(null);
|
|
97
|
+
v.scrollToIndex(rows - 1);
|
|
98
|
+
caja.ref.current?.focus();
|
|
99
|
+
/* `v` and `caja` are new objects every render; the row count is what decides. */
|
|
100
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
|
+
}, [rows, arriving]);
|
|
81
102
|
/* All the keyboard handling in one place. Splitting it between the box and
|
|
82
103
|
each cell is how you end up with spreadsheets whose arrows sometimes move
|
|
83
104
|
and sometimes scroll. */
|
|
@@ -92,7 +113,12 @@ note, height = 420, rowLabel, band, onBandToggle, caption, onCommit, editable, }
|
|
|
92
113
|
}
|
|
93
114
|
else if (e.key === "Enter") {
|
|
94
115
|
e.preventDefault();
|
|
95
|
-
|
|
116
|
+
if (!e.shiftKey && canAdd && r === rows - 1) {
|
|
117
|
+
commit(0, 0);
|
|
118
|
+
addRow(c);
|
|
119
|
+
}
|
|
120
|
+
else
|
|
121
|
+
commit(e.shiftKey ? -1 : 1, 0);
|
|
96
122
|
}
|
|
97
123
|
else if (e.key === "Tab") {
|
|
98
124
|
e.preventDefault();
|
|
@@ -192,6 +218,13 @@ note, height = 420, rowLabel, band, onBandToggle, caption, onCommit, editable, }
|
|
|
192
218
|
? "cursor-cell"
|
|
193
219
|
: ""}`, children: isEditingHere ? (_jsx("input", { autoFocus: true, value: draft, onChange: (ev) => setDraft(ev.target.value), onBlur: () => commit(0, 0), className: `absolute inset-0 w-full bg-surface px-2.5 text-meta tabular-nums text-ink outline-none ${c.numeric === true ? "text-right" : "text-left"}` })) : (_jsx(Cell, { value: value(item.index, c, ci), max: 24 })) }, c.id));
|
|
194
220
|
}), _jsx("div", { className: "flex-1" })] }, item.key));
|
|
195
|
-
}) })] }) })
|
|
221
|
+
}) })] }) }), canAdd ? (
|
|
222
|
+
/* Under the grid and outside its scroll box, like the editable table's «add»: in
|
|
223
|
+
view however far down a long sheet is scrolled. The new row's first cell that
|
|
224
|
+
can be written takes the place. */
|
|
225
|
+
_jsx("div", { className: "px-1 py-0.5", children: _jsxs("button", { type: "button", onClick: () => {
|
|
226
|
+
const first = columns.findIndex((_, ci) => canEdit(rows, ci));
|
|
227
|
+
addRow(first === -1 ? 0 : first);
|
|
228
|
+
}, className: "flex w-full items-center gap-1.5 rounded-control px-2 py-0.5 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 }), copy.editable.addRow] }) })) : null] }));
|
|
196
229
|
}
|
|
197
230
|
const ROW_LABEL_WIDTH = 64;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { type Aggregate, type DataColumn, DataTable, type GroupColumn, } from "./DataTable.js";
|
|
2
|
-
export { EditableTable, type EditColumn, type EditLayout, type StackPlace, stackPlaces, } from "./EditableTable.js";
|
|
1
|
+
export { type Aggregate, type DataColumn, DataTable, type DataTableCreate, type GroupColumn, } from "./DataTable.js";
|
|
2
|
+
export { EditableTable, type EditAdd, type EditBands, type EditColumn, type EditLayout, sectionLines, type StackPlace, stackPlaces, type TabTarget, tabTarget, } from "./EditableTable.js";
|
|
3
3
|
export { PivotTable, type PivotTableProps, } from "./PivotTable.js";
|
|
4
4
|
export { type ReportColumn, type ReportRow, type ReportRowKind, ReportTable, } from "./ReportTable.js";
|
|
5
5
|
export { type RowAction, RowActions } from "./RowActions.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* The presentation (`Table`) is ours and is used on its own; `DataTable` is the
|
|
2
2
|
wiring to TanStack for grouping and sorting. See the note in `DataTable.tsx`. */
|
|
3
3
|
export { DataTable, } from "./DataTable.js";
|
|
4
|
-
export { EditableTable, stackPlaces, } from "./EditableTable.js";
|
|
4
|
+
export { EditableTable, sectionLines, stackPlaces, tabTarget, } from "./EditableTable.js";
|
|
5
5
|
export { PivotTable, } from "./PivotTable.js";
|
|
6
6
|
export { ReportTable, } from "./ReportTable.js";
|
|
7
7
|
export { RowActions } from "./RowActions.js";
|
|
@@ -4,7 +4,7 @@ import type { LucideIcon } from "lucide-react";
|
|
|
4
4
|
* UNA TIRA DE PESTAÑAS. La misma en todo el sistema.
|
|
5
5
|
*
|
|
6
6
|
* Context: esto vivía adentro del encabezado del registro y se llamaba
|
|
7
|
-
* `Aspects`, así que
|
|
7
|
+
* `Aspects`, así que la actividad —que necesita exactamente lo mismo— no podía
|
|
8
8
|
* usarlo: un contexto no importa de otro contexto. El resultado era una tira de
|
|
9
9
|
* pestañas de segunda mano, con el activo marcado de otra manera, en la misma
|
|
10
10
|
* pantalla que la buena.
|
|
@@ -76,7 +76,7 @@ export type TabItem = {
|
|
|
76
76
|
notice?: TabNotice;
|
|
77
77
|
};
|
|
78
78
|
/**
|
|
79
|
-
* The strip only: what each tab opens is drawn by whoever uses it (`Record`, `
|
|
79
|
+
* The strip only: what each tab opens is drawn by whoever uses it (`Record`, `Activity`).
|
|
80
80
|
*
|
|
81
81
|
* Built on React Aria's `Tabs`, so the strip is a real tab list for the keyboard: the
|
|
82
82
|
* arrows move between tabs and select them, Home/End jump to the ends, and only the open
|
|
@@ -22,7 +22,7 @@ const NOTICE_DOT = {
|
|
|
22
22
|
accent: "bg-accent",
|
|
23
23
|
};
|
|
24
24
|
/**
|
|
25
|
-
* The strip only: what each tab opens is drawn by whoever uses it (`Record`, `
|
|
25
|
+
* The strip only: what each tab opens is drawn by whoever uses it (`Record`, `Activity`).
|
|
26
26
|
*
|
|
27
27
|
* Built on React Aria's `Tabs`, so the strip is a real tab list for the keyboard: the
|
|
28
28
|
* arrows move between tabs and select them, Home/End jump to the ends, and only the open
|
|
@@ -20,6 +20,9 @@ export type TagsFieldProps = {
|
|
|
20
20
|
id?: string;
|
|
21
21
|
describedBy?: string;
|
|
22
22
|
invalid?: boolean;
|
|
23
|
+
/** The name of the control when it is NOT inside a `Field` — a cell of an editable table.
|
|
24
|
+
* Inside a field, the field's label names it. */
|
|
25
|
+
"aria-label"?: string;
|
|
23
26
|
/** Forces the mode. By default it inherits the form's: in read mode it draws `Tags`. */
|
|
24
27
|
mode?: FormMode;
|
|
25
28
|
};
|
|
@@ -70,7 +70,7 @@ export function TagsField({ mode, ...props }) {
|
|
|
70
70
|
to be ABOVE the component that asks for it. */
|
|
71
71
|
return (_jsx(AriaLocale, { children: _jsx(EditableTagsField, { ...props }) }));
|
|
72
72
|
}
|
|
73
|
-
function EditableTagsField({ value, onChange, options, onCreate, placeholder, disabled = false, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, }) {
|
|
73
|
+
function EditableTagsField({ value, onChange, options, onCreate, placeholder, disabled = false, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, "aria-label": ariaLabel, }) {
|
|
74
74
|
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
75
75
|
id: ownId,
|
|
76
76
|
describedBy: ownDescribedBy,
|
|
@@ -132,7 +132,11 @@ function EditableTagsField({ value, onChange, options, onCreate, placeholder, di
|
|
|
132
132
|
/* The box grows with the chips: five categories wrap to a second line instead of
|
|
133
133
|
scrolling out of sight. */
|
|
134
134
|
className: "h-auto min-h-(--size-control-md) flex-wrap gap-1 py-1 pl-1.5",
|
|
135
|
-
}), children: [chips.length === 0 ? null : (_jsx(Tags, { items: chips, ...(labelledBy === undefined
|
|
135
|
+
}), children: [chips.length === 0 ? null : (_jsx(Tags, { items: chips, ...(labelledBy === undefined
|
|
136
|
+
? ariaLabel === undefined
|
|
137
|
+
? {}
|
|
138
|
+
: { label: ariaLabel }
|
|
139
|
+
: { labelledBy }) })), _jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, items: listed, selectedKey: null, onSelectionChange: commit, inputValue: text, onInputChange: setText,
|
|
136
140
|
/* Arriving at the field shows what can still be added, the way a many2one opens its
|
|
137
141
|
list on focus. With every tag chosen and no text there is nothing to show, and the
|
|
138
142
|
list stays closed. */
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type LucideIcon } from "lucide-react";
|
|
2
|
+
/** One record of the hierarchy. With `children` it is a group that folds. */
|
|
3
|
+
export interface TreeItem {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
/** What reads beside the name, at the right, in tabular figures: a balance, a stock, a
|
|
7
|
+
* count. Already formatted — the tree does not know a currency from a unit. */
|
|
8
|
+
meta?: string;
|
|
9
|
+
icon?: LucideIcon;
|
|
10
|
+
children?: readonly TreeItem[];
|
|
11
|
+
/** It cannot be chosen — a view account that groups others and takes no entries. It still
|
|
12
|
+
* folds and opens. */
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** One name or the other: a tree with no name is announced as a lone «tree». */
|
|
16
|
+
type TreeName = {
|
|
17
|
+
/** The accessible name: «Plan de cuentas». */
|
|
18
|
+
label: string;
|
|
19
|
+
labelledBy?: never;
|
|
20
|
+
} | {
|
|
21
|
+
/** The id of a visible heading that names the tree — the label of the `Field` it lives
|
|
22
|
+
* in. */
|
|
23
|
+
labelledBy: string;
|
|
24
|
+
label?: never;
|
|
25
|
+
};
|
|
26
|
+
export type TreeProps = TreeName & {
|
|
27
|
+
items: readonly TreeItem[];
|
|
28
|
+
/** The groups open right now. Pass it with `onExpandedChange` to own the expansion. */
|
|
29
|
+
expanded?: readonly string[];
|
|
30
|
+
/** The groups that start open, when the tree owns its expansion. */
|
|
31
|
+
defaultExpanded?: readonly string[];
|
|
32
|
+
onExpandedChange?: (ids: string[]) => void;
|
|
33
|
+
/** `none` reads; `single` chooses one record (a category, a location); `multiple` a set. */
|
|
34
|
+
selectionMode?: "none" | "single" | "multiple";
|
|
35
|
+
/** The chosen ids — a list in both modes, like every other control, so a screen does not
|
|
36
|
+
* switch shapes when one becomes many. */
|
|
37
|
+
selected?: readonly string[];
|
|
38
|
+
defaultSelected?: readonly string[];
|
|
39
|
+
onSelectedChange?: (ids: string[]) => void;
|
|
40
|
+
/** Open the record: Enter, or a click (a double click when the tree chooses one). Without
|
|
41
|
+
* it, a click on a group folds or opens it. */
|
|
42
|
+
onAction?: (id: string) => void;
|
|
43
|
+
/** What an empty tree says. By default, the copy table's `tree.empty`. */
|
|
44
|
+
empty?: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* A hierarchy of records — the chart of accounts, product categories, warehouse locations —
|
|
48
|
+
* with its groups that fold, a figure per row, and one or many rows to choose.
|
|
49
|
+
*
|
|
50
|
+
* Context: the keyboard is React Aria's `Tree`: arrows walk the visible rows, → opens a group
|
|
51
|
+
* and ← folds it (or goes up to the parent), Home and End jump, and typing a name moves to it.
|
|
52
|
+
* Escape does NOT clear the selection: the tree often lives in a dialog or a popover, and
|
|
53
|
+
* Escape there means «close» — clearing the choice on the way out would lose it without a word.
|
|
54
|
+
*/
|
|
55
|
+
export declare function Tree({ items, label, labelledBy, expanded, defaultExpanded, onExpandedChange, selectionMode, selected, defaultSelected, onSelectedChange, onAction, empty, }: TreeProps): import("react").JSX.Element;
|
|
56
|
+
export {};
|