@sincpro/web-ui 1.5.0 → 1.7.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/dist/components/attachments/Attachments.d.ts +56 -0
- package/dist/components/attachments/Attachments.js +93 -0
- package/dist/components/attachments/files.d.ts +28 -0
- package/dist/components/attachments/files.js +100 -0
- package/dist/components/attachments/index.d.ts +2 -0
- package/dist/components/attachments/index.js +3 -0
- package/dist/components/flow/StepFlow.d.ts +37 -0
- package/dist/components/flow/StepFlow.js +74 -0
- package/dist/components/flow/index.d.ts +1 -0
- package/dist/components/flow/index.js +2 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/components/lines/LineItems.d.ts +14 -3
- package/dist/components/lines/LineItems.js +6 -4
- package/dist/components/listing/SortMenu.d.ts +24 -20
- package/dist/components/listing/SortMenu.js +51 -91
- package/dist/components/listing/index.d.ts +2 -1
- package/dist/components/listing/index.js +2 -1
- package/dist/components/listing/modelCells.d.ts +7 -0
- package/dist/components/listing/modelCells.js +56 -0
- package/dist/components/listing/modelColumns.d.ts +9 -0
- package/dist/components/listing/modelColumns.js +59 -14
- package/dist/components/search/index.d.ts +1 -1
- package/dist/components/search/index.js +1 -1
- package/dist/components/search/model.d.ts +1 -0
- package/dist/components/search/model.js +7 -0
- package/dist/components/search/query.js +2 -3
- package/dist/foundations/copy.d.ts +131 -0
- package/dist/foundations/copy.es.js +86 -0
- package/dist/foundations/copy.js +86 -0
- package/dist/foundations/form.d.ts +18 -1
- package/dist/foundations/form.js +6 -3
- package/dist/primitives/button/Button.js +3 -1
- package/dist/primitives/field/CardNumberInput.d.ts +41 -0
- package/dist/primitives/field/CardNumberInput.js +117 -0
- package/dist/primitives/field/CodeInput.d.ts +18 -0
- package/dist/primitives/field/CodeInput.js +37 -0
- package/dist/primitives/field/ColorInput.d.ts +21 -0
- package/dist/primitives/field/ColorInput.js +42 -0
- package/dist/primitives/field/ContactInputs.d.ts +18 -0
- package/dist/primitives/field/ContactInputs.js +70 -0
- package/dist/primitives/field/DurationInput.d.ts +20 -0
- package/dist/primitives/field/DurationInput.js +54 -0
- package/dist/primitives/field/Field.d.ts +24 -4
- package/dist/primitives/field/Field.js +17 -4
- package/dist/primitives/field/ImageInput.d.ts +51 -0
- package/dist/primitives/field/ImageInput.js +267 -0
- package/dist/primitives/field/PasswordInput.d.ts +29 -0
- package/dist/primitives/field/PasswordInput.js +44 -0
- package/dist/primitives/field/PatternInput.d.ts +23 -0
- package/dist/primitives/field/PatternInput.js +48 -0
- package/dist/primitives/field/RichTextEditor.d.ts +18 -0
- package/dist/primitives/field/RichTextEditor.js +224 -0
- package/dist/primitives/field/RichTextInput.d.ts +30 -0
- package/dist/primitives/field/RichTextInput.js +38 -0
- package/dist/primitives/field/SignatureInput.d.ts +34 -0
- package/dist/primitives/field/SignatureInput.js +215 -0
- package/dist/primitives/field/card.d.ts +33 -0
- package/dist/primitives/field/card.js +173 -0
- package/dist/primitives/field/clipboard.js +3 -2
- package/dist/primitives/field/contact.d.ts +17 -0
- package/dist/primitives/field/contact.js +65 -0
- package/dist/primitives/field/duration.d.ts +5 -0
- package/dist/primitives/field/duration.js +48 -0
- package/dist/primitives/field/index.d.ts +14 -0
- package/dist/primitives/field/index.js +20 -0
- package/dist/primitives/field/mask.d.ts +33 -0
- package/dist/primitives/field/mask.js +56 -0
- package/dist/primitives/field/media.d.ts +99 -0
- package/dist/primitives/field/media.js +136 -0
- package/dist/primitives/field/pattern.d.ts +10 -0
- package/dist/primitives/field/pattern.js +68 -0
- package/dist/primitives/menu/Menu.d.ts +16 -0
- package/dist/primitives/menu/Menu.js +26 -8
- package/dist/primitives/menu/index.d.ts +1 -1
- package/dist/primitives/menu/index.js +1 -1
- package/dist/primitives/table/DataTable.d.ts +5 -3
- package/dist/primitives/table/DataTable.js +2 -4
- package/dist/primitives/table/EditableTable.d.ts +49 -7
- package/dist/primitives/table/EditableTable.js +321 -82
- package/dist/primitives/table/Table.d.ts +5 -8
- package/dist/primitives/table/Table.js +63 -15
- package/dist/primitives/table/index.d.ts +3 -2
- package/dist/primitives/table/index.js +2 -1
- package/dist/primitives/table/rowWindow.d.ts +3 -0
- package/dist/primitives/table/rowWindow.js +4 -0
- package/dist/primitives/table/sorting.d.ts +42 -0
- package/dist/primitives/table/sorting.js +110 -0
- package/dist/primitives/value/RichText.d.ts +10 -0
- package/dist/primitives/value/RichText.js +130 -0
- package/dist/primitives/value/Value.d.ts +20 -2
- package/dist/primitives/value/Value.js +26 -7
- package/dist/primitives/value/index.d.ts +3 -1
- package/dist/primitives/value/index.js +4 -1
- package/dist/primitives/value/rounding.d.ts +9 -0
- package/dist/primitives/value/rounding.js +48 -0
- package/dist/tokens.css +103 -0
- package/dist/views/list/List.d.ts +5 -4
- package/dist/views/list/ListView.js +2 -2
- package/dist/views/list/types.d.ts +6 -6
- package/package.json +9 -1
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
4
|
import { byDensity, useDensity } from "../../foundations/density.js";
|
|
5
|
+
import { ControlStateContext } from "../../foundations/form.js";
|
|
5
6
|
import { pathKey, pileIsOpen, pileItems, pilesOf, } from "../../foundations/grouping.js";
|
|
6
7
|
import { HANDHELD_WIDTH } from "../../foundations/layout.js";
|
|
7
8
|
import { stickyTop, useScrollBox } from "../../foundations/sticky.js";
|
|
8
9
|
import { IconButton } from "../button/Button.js";
|
|
9
|
-
import { NoteCard } from "../field/note.js";
|
|
10
|
+
import { NoteCard, NoteRegistryContext, } from "../field/note.js";
|
|
10
11
|
import { Flag } from "../notice/Flag.js";
|
|
12
|
+
import { useRowWindow } from "./rowWindow.js";
|
|
11
13
|
import { HEADER_TEXT } from "./Table.js";
|
|
12
|
-
import { GripVertical, Plus, Trash2 } from "lucide-react";
|
|
13
|
-
import { Fragment, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
14
|
+
import { AlertCircle, GripVertical, Plus, Trash2 } from "lucide-react";
|
|
15
|
+
import { Fragment, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
14
16
|
import { useKeyboard } from "react-aria";
|
|
15
17
|
/** The lines each section heads: the rows after it up to the next section. Notes do not
|
|
16
18
|
* cut a section, and are not lines. */
|
|
@@ -52,6 +54,30 @@ function says(node) {
|
|
|
52
54
|
function pileTag(pile, locale) {
|
|
53
55
|
return (_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx("span", { className: "shrink-0 text-micro text-muted", children: pile.field }), _jsx("span", { className: "truncate text-micro font-medium text-ink", children: pile.label }), _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: pile.count.toLocaleString(locale) })] }));
|
|
54
56
|
}
|
|
57
|
+
/** The note a flag stands for, as the record lists it. */
|
|
58
|
+
function noteOf(said) {
|
|
59
|
+
return {
|
|
60
|
+
tone: said.tone,
|
|
61
|
+
label: said.label,
|
|
62
|
+
...(said.detail === undefined ? {} : { detail: said.detail }),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** A cell's value as words, when it is words — what a note says it belongs to when the row
|
|
66
|
+
* is not drawn to read it from. */
|
|
67
|
+
function plainText(node) {
|
|
68
|
+
return typeof node === "string" || typeof node === "number"
|
|
69
|
+
? String(node).slice(0, 60)
|
|
70
|
+
: "";
|
|
71
|
+
}
|
|
72
|
+
/** The ids of the notes a row registers with its record — one for the row, one per cell. */
|
|
73
|
+
const rowNoteId = (rowId) => `flag-${rowId}`;
|
|
74
|
+
const cellNoteId = (rowId, columnId) => `flag-${rowId}-${columnId}`;
|
|
75
|
+
/** A cell's error, under its value: an icon and the sentence, in danger — the same shape as
|
|
76
|
+
* a field's. No `role="alert"`: errors that arrive with a save land on many rows at once,
|
|
77
|
+
* and each would interrupt the others; the control is described by it instead. */
|
|
78
|
+
function CellError({ id, numeric, children, }) {
|
|
79
|
+
return (_jsxs("span", { id: id, className: `mt-1 flex items-start gap-1 text-left text-micro font-normal whitespace-normal text-danger ${numeric === true ? "justify-end" : ""}`, children: [_jsx(AlertCircle, { "aria-hidden": true, className: "mt-px size-(--size-icon-sm) shrink-0", strokeWidth: 2 }), _jsx("span", { className: "min-w-0", children: children })] }));
|
|
80
|
+
}
|
|
55
81
|
/** The focus ring of an operable row: drawn on the row, inset, so it is not cut by the
|
|
56
82
|
* box and does not move the cells. */
|
|
57
83
|
const RING = "outline-0 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary forced-colors:focus-visible:outline-[Highlight]";
|
|
@@ -110,7 +136,7 @@ function ownsEnter(target) {
|
|
|
110
136
|
* of a new record in `DataTable`: one keyboard model, three shapes. Exported for that
|
|
111
137
|
* family sibling only; it is not in the barrel.
|
|
112
138
|
*/
|
|
113
|
-
export function EditRow({ as, rowId, rowRef, editable, editing, tabbable, className, onStart, onCommit, onCancel, onMove, onFocusRow, onTabOut, children, }) {
|
|
139
|
+
export function EditRow({ as, rowId, rowRef, editable, editing, tabbable, className, onStart, onCommit, onCancel, onMove, onFocusRow, onTabOut, windowed, children, }) {
|
|
114
140
|
const leaving = useRef(null);
|
|
115
141
|
const stay = () => {
|
|
116
142
|
if (leaving.current !== null)
|
|
@@ -218,7 +244,24 @@ export function EditRow({ as, rowId, rowRef, editable, editing, tabbable, classN
|
|
|
218
244
|
className,
|
|
219
245
|
"data-row-id": rowId,
|
|
220
246
|
};
|
|
221
|
-
|
|
247
|
+
if (windowed !== undefined) {
|
|
248
|
+
/* The row that holds the focus stays drawn wherever it scrolls: a control being written
|
|
249
|
+
in must not be unmounted under the person's hands. */
|
|
250
|
+
const own = props.onFocus;
|
|
251
|
+
props.onFocus = (event) => {
|
|
252
|
+
windowed.onFocus();
|
|
253
|
+
own?.(event);
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return as === "li" ? (_jsx("li", { ref: rowRef, ...props, children: children })) : (_jsx("tr", { ref: (node) => {
|
|
257
|
+
rowRef(node);
|
|
258
|
+
windowed?.ref(node);
|
|
259
|
+
}, ...(windowed === undefined
|
|
260
|
+
? {}
|
|
261
|
+
: {
|
|
262
|
+
"data-index": windowed["data-index"],
|
|
263
|
+
"aria-rowindex": windowed["aria-rowindex"],
|
|
264
|
+
}), ...props, children: children }));
|
|
222
265
|
}
|
|
223
266
|
/**
|
|
224
267
|
* A table whose rows are edited in place, one row at a time.
|
|
@@ -240,19 +283,60 @@ totals, empty, grouping,
|
|
|
240
283
|
/** El total de una columna DENTRO de un montón, ya formateado. Lo calcula quien
|
|
241
284
|
* compone, por el mismo motivo que `totals`: sumar significa cosas distintas
|
|
242
285
|
* según el dominio, y un porcentaje se promedia, no se suma. */
|
|
243
|
-
groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReorder, }) {
|
|
286
|
+
groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReorder, cellState, rowState, }) {
|
|
244
287
|
const copy = useUiCopy();
|
|
245
288
|
const bandOf = (row) => bands?.kind(row) ?? null;
|
|
289
|
+
const uid = useId();
|
|
290
|
+
/* A mark, and —in a record— its card, which registers the note with the record. */
|
|
291
|
+
const marked = (said, noteId, withCard) => (_jsxs(_Fragment, { children: [_jsx(Flag, { tone: said.tone, label: said.label, children: said.detail }), withCard ? (_jsx(NoteCard, { id: noteId, ...(said.owner === undefined ? {} : { owner: said.owner }), note: noteOf(said) })) : null] }));
|
|
246
292
|
/* The row's mark, before its first value — and, in a record's margin, its card. */
|
|
247
293
|
const flagOf = (row, withCard = false) => {
|
|
248
294
|
const said = flag?.(row) ?? null;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
295
|
+
return said === null ? null : marked(said, rowNoteId(getRowId(row)), withCard);
|
|
296
|
+
};
|
|
297
|
+
/* What the backend says of each cell on each row. A hidden cell says nothing else: no
|
|
298
|
+
error and no warning about a value that is not there. */
|
|
299
|
+
const stateOf = (row, column) => {
|
|
300
|
+
const said = cellState?.(row, column.id) ?? null;
|
|
301
|
+
return said?.hidden === true ? { hidden: true } : said;
|
|
302
|
+
};
|
|
303
|
+
const locked = (row) => rowState?.(row)?.readonly === true;
|
|
304
|
+
/* Whether a cell becomes a control while its row is edited. */
|
|
305
|
+
const writes = (row, column, state) => editable &&
|
|
306
|
+
column.edit !== undefined &&
|
|
307
|
+
state?.hidden !== true &&
|
|
308
|
+
state?.readonly !== true &&
|
|
309
|
+
!locked(row);
|
|
310
|
+
/* Ids built from a row id: `aria-describedby` is a list split on spaces, so an id that
|
|
311
|
+
carries one would describe the control with two ids that do not exist. */
|
|
312
|
+
const cellId = (rowId, columnId, what) => `${uid}-${rowId.replace(/[^\w-]/g, "_")}-${columnId.replace(/[^\w-]/g, "_")}-${what}`;
|
|
313
|
+
const cellFlagOf = (row, column, state, withCard = false) => {
|
|
314
|
+
const said = state?.flag ?? null;
|
|
315
|
+
return said === null
|
|
316
|
+
? null
|
|
317
|
+
: marked(said, cellNoteId(getRowId(row), column.id), withCard);
|
|
318
|
+
};
|
|
319
|
+
/* The control of a cell, wired through the form contract: its error and «required» reach
|
|
320
|
+
whatever control the column wrote — the package's or the application's own. */
|
|
321
|
+
const controlOf = (row, column, state, set) => {
|
|
322
|
+
const id = getRowId(row);
|
|
323
|
+
const error = state?.error ?? null;
|
|
324
|
+
const required = state?.required === true;
|
|
325
|
+
const describedBy = [
|
|
326
|
+
error === null ? null : cellId(id, column.id, "error"),
|
|
327
|
+
required ? cellId(id, column.id, "required") : null,
|
|
328
|
+
]
|
|
329
|
+
.filter((one) => one !== null)
|
|
330
|
+
.join(" ");
|
|
331
|
+
return (_jsx(ControlStateContext, { value: {
|
|
332
|
+
...(describedBy === "" ? {} : { describedBy }),
|
|
333
|
+
invalid: error !== null,
|
|
334
|
+
}, children: required ? (_jsxs("span", { className: "flex min-w-0 items-start gap-1", children: [_jsx("span", { className: "min-w-0 flex-1", children: column.edit?.(row, set) }), _jsx("span", { "aria-hidden": true, className: "pt-1 text-meta text-ink", children: "*" }), _jsx("span", { id: cellId(id, column.id, "required"), className: "sr-only", children: copy.field.required })] })) : (column.edit?.(row, set)) }));
|
|
335
|
+
};
|
|
336
|
+
/* The error of a cell, under its value — read with the cell, and describing its control. */
|
|
337
|
+
const errorOf = (row, column, state) => {
|
|
338
|
+
const error = state?.error ?? null;
|
|
339
|
+
return error === null ? null : (_jsx(CellError, { id: cellId(getRowId(row), column.id, "error"), numeric: column.numeric, children: error }));
|
|
256
340
|
};
|
|
257
341
|
const underSection = useMemo(() => (bands === undefined ? null : sectionLines(rows, getRowId, bands.kind)), [rows, bands, getRowId]);
|
|
258
342
|
/* Where a band's text stops: the first column with something of its own on that row. */
|
|
@@ -282,6 +366,7 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReo
|
|
|
282
366
|
/* The id `onAdd` returned, until that row shows up in `rows` and can be opened. */
|
|
283
367
|
const [arriving, setArriving] = useState(null);
|
|
284
368
|
const addButton = useRef(null);
|
|
369
|
+
const rowById = useMemo(() => new Map(rows.map((row) => [getRowId(row), row])), [rows, getRowId]);
|
|
285
370
|
/* La secuencia que se dibuja: montones y filas intercalados. Sin agrupado, las
|
|
286
371
|
filas tal cual — una tabla que no agrupa no paga nada por poder hacerlo. */
|
|
287
372
|
const drawn = useMemo(() => {
|
|
@@ -328,6 +413,20 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReo
|
|
|
328
413
|
return () => watch.disconnect();
|
|
329
414
|
}, [layout]);
|
|
330
415
|
const stacked = layout === "stacked" || (layout === "auto" && narrow);
|
|
416
|
+
/* Past a few hundred rows only the ones in view are drawn — see `rowWindow.tsx`. Measured
|
|
417
|
+
at 2,000 lines, drawing them all cost a quarter of a second per keystroke in the row
|
|
418
|
+
being written: every row re-rendered for one letter. The window counts what is DRAWN —
|
|
419
|
+
group bands included, folded rows left out. The cards of a phone are not windowed: a
|
|
420
|
+
phone does not hold a document of two thousand lines being written. */
|
|
421
|
+
const view = useRowWindow({
|
|
422
|
+
count: stacked ? 0 : drawn.length,
|
|
423
|
+
getKey: (index) => {
|
|
424
|
+
const entry = drawn[index];
|
|
425
|
+
if (entry?.pile !== undefined)
|
|
426
|
+
return `pile:${pathKey(entry.pile.path)}`;
|
|
427
|
+
return entry?.row === undefined ? String(index) : getRowId(entry.row);
|
|
428
|
+
},
|
|
429
|
+
});
|
|
331
430
|
const editable = onChange !== undefined;
|
|
332
431
|
/* A card that opens the consumer's form is operable even with nothing to edit in
|
|
333
432
|
place: it has a tab stop, takes Enter and answers a press. */
|
|
@@ -341,7 +440,68 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReo
|
|
|
341
440
|
const stop = active !== null && order.includes(active) ? active : (order[0] ?? null);
|
|
342
441
|
/* REORDER — a grip at the start of each row. Dragged, the row shows where it would land (a
|
|
343
442
|
line over the row it goes in front of); Alt + ↑ / ↓ on the grip does the same by keyboard. */
|
|
443
|
+
/* The notes of the rows the window does not draw. A drawn row's mark registers its note
|
|
444
|
+
with the record (`NoteCard`); one scrolled out of the window would vanish from the
|
|
445
|
+
record's activity — «2 avisos» for a document that has nine. The table registers those
|
|
446
|
+
itself, and «take me there» brings the row into the window. Every render, like a
|
|
447
|
+
`NoteCard`: a drawn row's card takes over its note, and gives it back when it leaves. */
|
|
448
|
+
const registry = useContext(NoteRegistryContext);
|
|
449
|
+
const ownNotes = useRef(new Set());
|
|
450
|
+
useLayoutEffect(() => {
|
|
451
|
+
if (registry === null)
|
|
452
|
+
return;
|
|
453
|
+
const now = new Set();
|
|
454
|
+
const theirs = new Set();
|
|
455
|
+
if (view.on) {
|
|
456
|
+
const inWindow = new Set(view.rows.flatMap((slot) => {
|
|
457
|
+
const row = drawn[slot.index]?.row;
|
|
458
|
+
return row === undefined ? [] : [getRowId(row)];
|
|
459
|
+
}));
|
|
460
|
+
for (const [index, entry] of drawn.entries()) {
|
|
461
|
+
const row = entry.row;
|
|
462
|
+
if (row === undefined || bandOf(row) !== null)
|
|
463
|
+
continue;
|
|
464
|
+
const id = getRowId(row);
|
|
465
|
+
const said = [];
|
|
466
|
+
const own = flag?.(row) ?? null;
|
|
467
|
+
if (own !== null)
|
|
468
|
+
said.push({ noteId: rowNoteId(id), flag: own });
|
|
469
|
+
for (const column of columns) {
|
|
470
|
+
const cellFlag = stateOf(row, column)?.flag ?? null;
|
|
471
|
+
if (cellFlag !== null)
|
|
472
|
+
said.push({ noteId: cellNoteId(id, column.id), flag: cellFlag });
|
|
473
|
+
}
|
|
474
|
+
for (const one of said) {
|
|
475
|
+
if (inWindow.has(id)) {
|
|
476
|
+
theirs.add(one.noteId);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
registry.put({
|
|
480
|
+
id: one.noteId,
|
|
481
|
+
note: noteOf(one.flag),
|
|
482
|
+
owner: one.flag.owner ?? plainText(columns[0]?.render(row)),
|
|
483
|
+
anchor: () => {
|
|
484
|
+
view.reveal(index);
|
|
485
|
+
return null;
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
now.add(one.noteId);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
for (const noteId of ownNotes.current) {
|
|
493
|
+
if (!now.has(noteId) && !theirs.has(noteId))
|
|
494
|
+
registry.drop(noteId);
|
|
495
|
+
}
|
|
496
|
+
ownNotes.current = now;
|
|
497
|
+
});
|
|
498
|
+
useLayoutEffect(() => () => {
|
|
499
|
+
for (const noteId of ownNotes.current)
|
|
500
|
+
registry?.drop(noteId);
|
|
501
|
+
}, [registry]);
|
|
344
502
|
const reorderable = onReorder !== undefined && grouping === undefined && !stacked;
|
|
503
|
+
/* Every cell of a row, the grip and the trash: what a spacer row spans. */
|
|
504
|
+
const width = columns.length + (onRemove === undefined ? 0 : 1) + (reorderable ? 1 : 0);
|
|
345
505
|
/* A ref and not state: `dragover` can arrive before React re-renders after `dragstart`. */
|
|
346
506
|
const dragged = useRef(null);
|
|
347
507
|
const [landing, setLanding] = useState(undefined);
|
|
@@ -384,17 +544,34 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReo
|
|
|
384
544
|
dragged.current = null;
|
|
385
545
|
setLanding(undefined);
|
|
386
546
|
}, className: "flex cursor-grab items-center rounded-control p-0.5 text-muted opacity-40 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 hover:text-ink focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-primary active:cursor-grabbing", children: _jsx(GripVertical, { "aria-hidden": true, className: "size-(--size-icon-sm)", strokeWidth: 2 }) }) }));
|
|
547
|
+
/* The focus a row is waiting for, once it is drawn: opened by the keyboard (its first or
|
|
548
|
+
last control), or reached by a jump (`Home`, `End`) past what the window draws. Every
|
|
549
|
+
render and not on `editing` alone: a row outside the window shows up a render or two
|
|
550
|
+
after it was asked for. */
|
|
551
|
+
const focusWhenDrawn = useRef(null);
|
|
552
|
+
const indexOf = (id) => drawn.findIndex((entry) => entry.row !== undefined && getRowId(entry.row) === id);
|
|
387
553
|
useEffect(() => {
|
|
388
554
|
const target = focusOnOpen.current;
|
|
389
|
-
if (target
|
|
555
|
+
if (target !== null && target.id === editing) {
|
|
556
|
+
const node = rowNodes.current.get(target.id);
|
|
557
|
+
if (node === undefined) {
|
|
558
|
+
view.reveal(indexOf(target.id));
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
focusOnOpen.current = null;
|
|
562
|
+
const control = target.at === "last" ? lastControl(node) : firstControl(node);
|
|
563
|
+
(control ?? node).focus();
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const waiting = focusWhenDrawn.current;
|
|
567
|
+
if (waiting === null)
|
|
390
568
|
return;
|
|
391
|
-
|
|
392
|
-
const node = rowNodes.current.get(target.id);
|
|
569
|
+
const node = rowNodes.current.get(waiting);
|
|
393
570
|
if (node === undefined)
|
|
394
571
|
return;
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
572
|
+
focusWhenDrawn.current = null;
|
|
573
|
+
node.focus();
|
|
574
|
+
});
|
|
398
575
|
const start = (id, row, viaKeyboard, { at = "first", added = false } = {}) => {
|
|
399
576
|
/* A row just added is written where it appeared, card or not: it was added in place
|
|
400
577
|
precisely so that it would not open somewhere else. */
|
|
@@ -403,6 +580,11 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReo
|
|
|
403
580
|
onOpen?.(id);
|
|
404
581
|
return;
|
|
405
582
|
}
|
|
583
|
+
/* Read, not written: the row keeps the focus and opens nothing. */
|
|
584
|
+
if (locked(row)) {
|
|
585
|
+
setActive(id);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
406
588
|
before.current = { id, row, touched: false, added };
|
|
407
589
|
focusOnOpen.current = viaKeyboard ? { id, at } : null;
|
|
408
590
|
setActive(id);
|
|
@@ -480,7 +662,18 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReo
|
|
|
480
662
|
const tabOut = (from, direction) => {
|
|
481
663
|
const was = before.current;
|
|
482
664
|
const blank = was !== null && was.id === from && was.added && !was.touched;
|
|
483
|
-
|
|
665
|
+
/* `Tab` walks the rows that can be written: a locked one is passed over, as a
|
|
666
|
+
read-only field is in a form. */
|
|
667
|
+
const writable = rowState === undefined
|
|
668
|
+
? order
|
|
669
|
+
: order.filter((id) => {
|
|
670
|
+
const row = rowById.get(id);
|
|
671
|
+
return id === from || row === undefined || !locked(row);
|
|
672
|
+
});
|
|
673
|
+
const target = tabTarget(writable, from, direction, {
|
|
674
|
+
canAdd: onAdd !== undefined,
|
|
675
|
+
blank,
|
|
676
|
+
});
|
|
484
677
|
if (target.kind === "leave")
|
|
485
678
|
return false;
|
|
486
679
|
commit(from, false);
|
|
@@ -488,7 +681,7 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReo
|
|
|
488
681
|
add();
|
|
489
682
|
return true;
|
|
490
683
|
}
|
|
491
|
-
const row =
|
|
684
|
+
const row = rowById.get(target.id);
|
|
492
685
|
if (row === undefined)
|
|
493
686
|
return false;
|
|
494
687
|
start(target.id, row, true, { at: direction === "next" ? "first" : "last" });
|
|
@@ -532,7 +725,13 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReo
|
|
|
532
725
|
if (id === undefined)
|
|
533
726
|
return;
|
|
534
727
|
setActive(id);
|
|
535
|
-
rowNodes.current.get(id)
|
|
728
|
+
const node = rowNodes.current.get(id);
|
|
729
|
+
if (node !== undefined) {
|
|
730
|
+
node.focus();
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
focusWhenDrawn.current = id;
|
|
734
|
+
view.reveal(indexOf(id));
|
|
536
735
|
};
|
|
537
736
|
/* ── the stacked list: one card per row, below `--container-handheld` ── */
|
|
538
737
|
const places = stackPlaces(columns);
|
|
@@ -546,7 +745,9 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReo
|
|
|
546
745
|
const id = getRowId(row);
|
|
547
746
|
const isEditing = editing === id;
|
|
548
747
|
const set = setter(id);
|
|
549
|
-
const
|
|
748
|
+
const states = new Map(columns.map((c) => [c.id, stateOf(row, c)]));
|
|
749
|
+
const shown = (c) => states.get(c.id)?.hidden !== true;
|
|
750
|
+
const parts = details.filter(shown).flatMap((c) => {
|
|
550
751
|
const said = c.brief === undefined ? null : c.brief(row);
|
|
551
752
|
if (c.brief !== undefined)
|
|
552
753
|
return says(said) ? [{ id: c.id, node: said }] : [];
|
|
@@ -557,15 +758,28 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReo
|
|
|
557
758
|
},
|
|
558
759
|
];
|
|
559
760
|
});
|
|
560
|
-
const controls =
|
|
561
|
-
|
|
761
|
+
const controls = isEditing
|
|
762
|
+
? columns.filter((c) => writes(row, c, states.get(c.id) ?? null))
|
|
763
|
+
: [];
|
|
764
|
+
/* The errors of the cells that are not a control right now: under the card. A control
|
|
765
|
+
carries its own, under it. */
|
|
766
|
+
const loose = columns.filter((c) => !controls.includes(c) && (states.get(c.id)?.error ?? null) !== null);
|
|
767
|
+
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
|
|
768
|
+
? opens
|
|
769
|
+
? "cursor-pointer"
|
|
770
|
+
: locked(row)
|
|
771
|
+
? ""
|
|
772
|
+
: "cursor-text"
|
|
773
|
+
: ""}`, children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [flagOf(row), columns.map((c) => (_jsx(Fragment, { children: cellFlagOf(row, c, states.get(c.id) ?? null) }, c.id))), _jsx("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5 text-meta text-ink", children: titles.filter(shown).map((c) => (_jsx("div", { className: "min-w-0", children: c.render(row) }, c.id))) }), trails.filter(shown).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.filter(shown).map((c) => (
|
|
562
774
|
/* The amount is read by its figure on screen and by its name aloud: a
|
|
563
775
|
lone «Bs. 1.296,00» at the end of a card does not say what it is. */
|
|
564
776
|
_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 : (
|
|
565
777
|
/* Each control under the column's name: a cell had the header above it, a
|
|
566
778
|
card has nothing, and a number box that does not say what it holds is a
|
|
567
779
|
guess. The name is drawn for the eye; the control carries its own label. */
|
|
568
|
-
_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.
|
|
780
|
+
_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 }), controlOf(row, c, states.get(c.id) ?? null, set), errorOf(row, c, states.get(c.id) ?? null)] }, c.id))) })), loose.length === 0 ? null : (
|
|
781
|
+
/* Under a card there is no column to stand under: each error says its column. */
|
|
782
|
+
_jsx("div", { className: "flex flex-col", children: loose.map((c) => (_jsxs(CellError, { id: cellId(id, c.id, "error"), children: [c.header, ": ", states.get(c.id)?.error] }, c.id))) }))] }, id));
|
|
569
783
|
};
|
|
570
784
|
/* A section or a note on a phone: the band and, for a section, what it adds up to in
|
|
571
785
|
the card's amount; editing, its control under it. */
|
|
@@ -602,11 +816,13 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReo
|
|
|
602
816
|
return null;
|
|
603
817
|
return (_jsxs("div", { className: "flex items-baseline justify-between gap-3 text-meta", children: [_jsx("dt", { className: "text-muted", children: c.header }), _jsx("dd", { className: "font-semibold text-ink tabular-nums", children: total })] }, c.id));
|
|
604
818
|
}) }))] }));
|
|
605
|
-
return (_jsxs("div", { ref: frame, className: "min-w-0", children: [_jsx("div", { ref: box.ref, tabIndex: box.tabIndex, className: `min-w-0 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary ${box.className}`, style: box.style, children: stacked ? (list) : (_jsxs("table", { className: "w-full border-separate border-spacing-0 px-1 text-left",
|
|
819
|
+
return (_jsxs("div", { ref: frame, className: "min-w-0", children: [_jsx("div", { ref: box.ref, tabIndex: box.tabIndex, className: `min-w-0 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary ${box.className}`, style: box.style, children: stacked ? (list) : (_jsxs("table", { className: "w-full border-separate border-spacing-0 px-1 text-left", ...(view.on
|
|
820
|
+
? { "aria-rowcount": 1 + drawn.length + (totals === undefined ? 0 : 1) }
|
|
821
|
+
: {}), children: [_jsx("thead", { className: "sticky z-10 bg-ground", style: stickyTop, children: _jsxs("tr", { ...(view.on ? { "aria-rowindex": 1 } : {}), children: [reorderable ? _jsx("td", { className: "w-0 border-b border-line-strong" }) : null, columns.map((c) => c.header === "" ? (_jsx("td", { className: "w-0 border-b border-line-strong" }, c.id)) : (_jsx("th", { scope: "col", style: c.width === undefined ? undefined : { width: c.width },
|
|
606
822
|
/* A column of figures takes its figures' width (`w-0` in an auto
|
|
607
823
|
table is «as narrow as its content») and the text takes the rest:
|
|
608
824
|
the product name was cut while «Cantidad» had room for ten digits. */
|
|
609
|
-
className: `border-b border-line-strong ${HEADER_TEXT} ${cell} ${c.numeric === true ? "text-right" : "text-left"} ${c.numeric === true && c.width === undefined ? "w-0" : ""}`, children: c.header }, c.id))), onRemove === undefined ? null : (_jsx("td", { className: "w-0 border-b border-line-strong" }))] }) }),
|
|
825
|
+
className: `border-b border-line-strong ${HEADER_TEXT} ${cell} ${c.numeric === true ? "text-right" : "text-left"} ${c.numeric === true && c.width === undefined ? "w-0" : ""}`, children: c.header }, c.id))), onRemove === undefined ? null : (_jsx("td", { className: "w-0 border-b border-line-strong" }))] }) }), _jsxs("tbody", { ref: view.body.ref, ...(reorderable
|
|
610
826
|
? {
|
|
611
827
|
onDragOver: overRow,
|
|
612
828
|
onDrop: (event) => {
|
|
@@ -617,60 +833,83 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReo
|
|
|
617
833
|
setLanding(undefined);
|
|
618
834
|
},
|
|
619
835
|
}
|
|
620
|
-
: {}), children: rows.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: columns.length +
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
/*
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
const
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
836
|
+
: {}), children: [rows.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: columns.length +
|
|
837
|
+
(onRemove === undefined ? 0 : 1) +
|
|
838
|
+
(reorderable ? 1 : 0), className: `text-meta text-muted italic ${cell}`, children: emptyText }) })) : (view.rows.flatMap((slot, at) => {
|
|
839
|
+
const entry = drawn[slot.index];
|
|
840
|
+
if (entry === undefined)
|
|
841
|
+
return [];
|
|
842
|
+
const windowed = view.windowed(slot.index);
|
|
843
|
+
/* What is not drawn is a spacer of its height: the rows above the window
|
|
844
|
+
before the first, and the gap before a row kept only because it holds
|
|
845
|
+
the focus. */
|
|
846
|
+
const gap = view.pad(at === 0 ? 0 : view.rows[at - 1].end, slot.start, width, `gap:${slot.index}`);
|
|
847
|
+
if (entry.pile !== undefined) {
|
|
848
|
+
const pile = entry.pile;
|
|
849
|
+
const inside = pile.piles.length === 0 ? pile.items : pileItems(pile.piles);
|
|
850
|
+
return [
|
|
851
|
+
gap,
|
|
852
|
+
/* La banda respeta la cuadrícula: el rótulo en la primera
|
|
853
|
+
columna y cada total bajo la suya. Un total en un rincón
|
|
854
|
+
obliga a buscar de qué columna es; alineado no hay nada que
|
|
855
|
+
buscar. */
|
|
856
|
+
_jsxs("tr", { ...windowed, children: [reorderable ? _jsx("td", { className: "w-0 bg-ground" }) : null, columns.map((c, ci) => (_jsx("td", { className: `bg-ground text-meta ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, style: ci === 0 && pile.depth > 0
|
|
857
|
+
? {
|
|
858
|
+
paddingLeft: `calc(var(--spacing-indent) * ${pile.depth} + 0.625rem)`,
|
|
859
|
+
}
|
|
860
|
+
: undefined, children: ci === 0
|
|
861
|
+
? pileTag(pile, copy.locale)
|
|
862
|
+
: (groupTotal?.(c, inside) ?? null) }, c.id))), onRemove === undefined ? null : _jsx("td", { className: "w-0 bg-ground" })] }, `pile:${pathKey(pile.path)}`),
|
|
863
|
+
];
|
|
864
|
+
}
|
|
865
|
+
const row = entry.row;
|
|
866
|
+
const id = getRowId(row);
|
|
867
|
+
const isEditing = editing === id;
|
|
868
|
+
const band = bandOf(row);
|
|
869
|
+
if (band !== null) {
|
|
870
|
+
const { span, figures } = bandFigures(row);
|
|
871
|
+
const bandWash = isEditing
|
|
872
|
+
? "bg-primary/5"
|
|
873
|
+
: band === "section"
|
|
874
|
+
? "bg-ground group-hover/row:bg-ink/6"
|
|
875
|
+
: "group-hover/row:bg-ink/4";
|
|
876
|
+
const bandMark = `border-b border-line first:rounded-l-control last:rounded-r-control ${bandWash}`;
|
|
877
|
+
const control = isEditing && editable && bands?.edit !== undefined;
|
|
878
|
+
return [
|
|
879
|
+
gap,
|
|
880
|
+
_jsxs(EditRow, { as: "tr", ...rowWiring(id, row), windowed: windowed, className: `group/row ${editable ? RING : ""} ${editable && !isEditing ? "cursor-text" : ""}`, children: [reorderable ? grip(id, bandMark) : null, _jsx("td", { colSpan: span, ...(control ? { "data-edit": "" } : {}), className: `text-meta ${bandMark} ${lineOver(id)} ${cell} ${band === "section"
|
|
881
|
+
? "font-semibold text-ink"
|
|
882
|
+
: "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
|
|
883
|
+
? "text-right font-semibold tabular-nums"
|
|
884
|
+
: ""}`, 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),
|
|
885
|
+
];
|
|
886
|
+
}
|
|
887
|
+
/* The wash lives on the cells. A background on the row paints
|
|
888
|
+
edge to edge, and `border-collapse` would ignore a radius. */
|
|
889
|
+
const wash = isEditing ? "bg-primary/5" : "group-hover/row:bg-ink/4";
|
|
890
|
+
const mark = `border-b border-line first:rounded-l-control last:rounded-r-control ${wash}`;
|
|
891
|
+
const set = setter(id);
|
|
892
|
+
return [
|
|
893
|
+
gap,
|
|
894
|
+
_jsxs(EditRow, { as: "tr", ...rowWiring(id, row), windowed: windowed,
|
|
895
|
+
/* The focus ring is drawn on the row, inset, so it is not cut by
|
|
896
|
+
the box and does not move the cells. */
|
|
897
|
+
className: `group/row ${editable ? RING : ""} ${editable && !isEditing && !locked(row) ? "cursor-text" : ""}`, children: [reorderable ? grip(id, mark) : null, columns.map((c, ci) => {
|
|
898
|
+
const state = stateOf(row, c);
|
|
899
|
+
const control = isEditing && writes(row, c, state);
|
|
900
|
+
/* Not `mark`: that name is the row's border and wash, above. Shadowed,
|
|
901
|
+
the cells were drawn with the class «null» — no hairline, no hover. */
|
|
902
|
+
const flagged = ci === 0 ? flagOf(row, true) : null;
|
|
903
|
+
const cellFlag = cellFlagOf(row, c, state, true);
|
|
904
|
+
const value = state?.hidden === true
|
|
905
|
+
? null
|
|
906
|
+
: control
|
|
907
|
+
? controlOf(row, c, state, set)
|
|
908
|
+
: c.render(row);
|
|
909
|
+
return (_jsxs("td", { ...(control ? { "data-edit": "" } : {}), className: `text-meta text-ink ${mark} ${lineOver(id)} ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: [flagged === null && cellFlag === null ? (value) : (_jsxs("span", { className: "flex min-w-0 items-start gap-1.5", children: [flagged, cellFlag, _jsx("span", { className: "min-w-0 flex-1", children: value })] })), errorOf(row, c, state)] }, c.id));
|
|
910
|
+
}), 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),
|
|
911
|
+
];
|
|
912
|
+
})), rows.length === 0 ? null : view.tail(width)] }), totals === undefined ? null : (_jsx("tfoot", { children: _jsxs("tr", { children: [reorderable ? _jsx("td", { className: "border-t border-line-strong" }) : null, 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 : (
|
|
674
913
|
/* The «add» goes under the grid, as text, not as a button away from it:
|
|
675
914
|
what is about to happen is that one more row shows up right here.
|
|
676
915
|
The hover is the same inset mark as a row: the shell lines the label
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import type { Tone } from "../../foundations/types.js";
|
|
2
2
|
import type { WindowedRow } from "./rowWindow.js";
|
|
3
|
+
import { type SortState } from "./sorting.js";
|
|
3
4
|
import { type ReactNode } from "react";
|
|
4
|
-
export type SortDirection = "asc" | "desc";
|
|
5
|
-
export interface SortState {
|
|
6
|
-
key: string;
|
|
7
|
-
direction: SortDirection;
|
|
8
|
-
}
|
|
9
5
|
export declare function Table({ children,
|
|
10
6
|
/** With a height, the table scrolls on its own and the header sticks.
|
|
11
7
|
* Without a height, the page scrolls and the header goes away. */
|
|
@@ -29,14 +25,15 @@ export declare function THead({ children, rowIndex, }: {
|
|
|
29
25
|
}): import("react").JSX.Element;
|
|
30
26
|
/** A column. If it gets a `sortKey`, it can be sorted.
|
|
31
27
|
*
|
|
32
|
-
* The icon is ALWAYS there, dimmed when the column is not
|
|
28
|
+
* The icon is ALWAYS there, dimmed when the column is not sorted: an
|
|
33
29
|
* indicator that only shows up once you sort does not say which columns are
|
|
34
30
|
* sortable, and then you have to try them one by one. */
|
|
35
31
|
export declare function Th({ children, sortKey, sort, onSort, numeric, align, width, }: {
|
|
36
32
|
children: ReactNode;
|
|
37
33
|
sortKey?: string;
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
/** The order of the set, outermost first. Empty is «as they came». */
|
|
35
|
+
sort?: readonly SortState[];
|
|
36
|
+
onSort?: (next: SortState[]) => void;
|
|
40
37
|
/** Numbers align to the right: that is what lets you compare orders of
|
|
41
38
|
* magnitude without reading the figures. */
|
|
42
39
|
numeric?: boolean;
|