@sincpro/web-ui 1.4.0 → 1.6.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/lines/LineItems.d.ts +14 -3
- package/dist/components/lines/LineItems.js +6 -4
- package/dist/components/listing/Row.d.ts +5 -4
- package/dist/components/listing/Row.js +5 -4
- package/dist/foundations/copy.d.ts +4 -0
- package/dist/foundations/copy.es.js +2 -0
- package/dist/foundations/copy.js +2 -0
- package/dist/foundations/form.d.ts +18 -1
- package/dist/foundations/form.js +6 -3
- package/dist/foundations/grouping.d.ts +28 -0
- package/dist/foundations/grouping.js +0 -0
- package/dist/foundations/index.d.ts +1 -1
- package/dist/foundations/index.js +1 -1
- package/dist/primitives/picker/MatchedText.d.ts +3 -2
- package/dist/primitives/picker/MatchedText.js +3 -2
- package/dist/primitives/picker/Picker.d.ts +18 -1
- package/dist/primitives/picker/Picker.js +5 -5
- package/dist/primitives/picker/TagsField.d.ts +10 -0
- package/dist/primitives/picker/TagsField.js +2 -2
- package/dist/primitives/picker/index.d.ts +2 -1
- package/dist/primitives/picker/index.js +2 -0
- package/dist/primitives/picker/tagChoices.d.ts +7 -0
- package/dist/primitives/picker/tagChoices.js +9 -4
- package/dist/primitives/table/DataTable.js +10 -11
- package/dist/primitives/table/EditableTable.d.ts +49 -7
- package/dist/primitives/table/EditableTable.js +321 -82
- package/dist/primitives/table/PileFootLine.d.ts +14 -0
- package/dist/primitives/table/PileFootLine.js +22 -0
- package/dist/primitives/table/index.d.ts +2 -1
- package/dist/primitives/table/index.js +1 -0
- package/dist/primitives/table/rowWindow.d.ts +3 -0
- package/dist/primitives/table/rowWindow.js +4 -0
- package/dist/views/list/List.d.ts +3 -2
- package/dist/views/list/ListView.js +13 -6
- package/dist/views/list/index.d.ts +1 -1
- package/dist/views/list/types.d.ts +16 -2
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { type FormMode } from "../../foundations/form.js";
|
|
|
2
2
|
import { type DialogContext } from "../../primitives/dialog/Dialog.js";
|
|
3
3
|
import type { FlagTone } from "../../primitives/notice/Flag.js";
|
|
4
4
|
import type { DataColumn } from "../../primitives/table/DataTable.js";
|
|
5
|
-
import { type EditColumn, type EditLayout } from "../../primitives/table/EditableTable.js";
|
|
5
|
+
import { type EditCellState, type EditColumn, type EditLayout, type EditRowState } from "../../primitives/table/EditableTable.js";
|
|
6
6
|
import { type TotalsProps, type TotalsRow } from "../../primitives/value/Totals.js";
|
|
7
7
|
import { type ReactNode } from "react";
|
|
8
8
|
/**
|
|
@@ -41,6 +41,8 @@ export type LineEditorState = {
|
|
|
41
41
|
export type LineItemsProps<T> = {
|
|
42
42
|
lines: readonly T[];
|
|
43
43
|
columns: readonly LineItemColumn<T>[];
|
|
44
|
+
/** The line's key on this screen — LOCAL, not its id on the server: a new line gets one
|
|
45
|
+
* when it is made and keeps it after saving. See «A line's key is local» above. */
|
|
44
46
|
getLineId: (line: T) => string;
|
|
45
47
|
/** How a line is named — in the removal notice, the editor's header and for the
|
|
46
48
|
* screen reader next to each row's buttons. Defaults to the first column's text. */
|
|
@@ -52,7 +54,8 @@ export type LineItemsProps<T> = {
|
|
|
52
54
|
onChange?: (id: string, next: T) => void;
|
|
53
55
|
/** A new line, already filled in the editor (or blank, without an editor). */
|
|
54
56
|
onAdd?: (line: T) => void;
|
|
55
|
-
/** The blank line «Agregar una línea» starts from. Required for adding.
|
|
57
|
+
/** The blank line «Agregar una línea» starts from. Required for adding. It carries its
|
|
58
|
+
* local key from here — `{ key: crypto.randomUUID(), … }` — since it has no server id. */
|
|
56
59
|
newLine?: () => T;
|
|
57
60
|
/** A line was removed. It is gone at once; the notice offers to undo it. */
|
|
58
61
|
onRemove?: (id: string) => void;
|
|
@@ -93,6 +96,14 @@ export type LineItemsProps<T> = {
|
|
|
93
96
|
detail?: ReactNode;
|
|
94
97
|
owner?: string;
|
|
95
98
|
} | null;
|
|
99
|
+
/** What each cell is on each line — readonly, required, hidden, its error, its warning
|
|
100
|
+
* («precio bajo el costo» on the price) — as the backend says it per line and field.
|
|
101
|
+
* `EditableTable`'s `cellState`, passed through. The error reaches the cell's control as
|
|
102
|
+
* `aria-invalid` and `aria-describedby`. */
|
|
103
|
+
cellState?: (line: T, columnId: string) => EditCellState | null;
|
|
104
|
+
/** What a whole line is: `readonly` is read in place and offers no pencil (a delivered
|
|
105
|
+
* line). A line that should not be seen is not passed in `lines`. */
|
|
106
|
+
rowState?: (line: T) => EditRowState | null;
|
|
96
107
|
/** Sections and notes among the lines. See the file's header. */
|
|
97
108
|
structure?: LineStructure<T>;
|
|
98
109
|
/** The lines' order is the document's (Odoo's `sequence`): a grip at the start of each line
|
|
@@ -111,4 +122,4 @@ export type LineItemsProps<T> = {
|
|
|
111
122
|
* removing asked first, what an empty document said. One component, built on
|
|
112
123
|
* `EditableTable` and `Dialog`, makes the one2many the same block everywhere.
|
|
113
124
|
*/
|
|
114
|
-
export declare function LineItems<T>({ lines, columns, getLineId, lineLabel, mode, onChange, onAdd, newLine, onRemove, onRestore, editor, isValid, context, searchText, searchFrom, totals, layout, adding, onOpenLine, flag, structure, onReorder, visibleColumnIds, onVisibleColumnIdsChange, }: LineItemsProps<T>): import("react").JSX.Element;
|
|
125
|
+
export declare function LineItems<T>({ lines, columns, getLineId, lineLabel, mode, onChange, onAdd, newLine, onRemove, onRestore, editor, isValid, context, searchText, searchFrom, totals, layout, adding, onOpenLine, flag, cellState, rowState, structure, onReorder, visibleColumnIds, onVisibleColumnIdsChange, }: LineItemsProps<T>): import("react").JSX.Element;
|
|
@@ -38,7 +38,7 @@ function plain(v) {
|
|
|
38
38
|
* removing asked first, what an empty document said. One component, built on
|
|
39
39
|
* `EditableTable` and `Dialog`, makes the one2many the same block everywhere.
|
|
40
40
|
*/
|
|
41
|
-
export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange, onAdd, newLine, onRemove, onRestore, editor, isValid, context, searchText, searchFrom = 8, totals, layout = "auto", adding, onOpenLine, flag, structure, onReorder, visibleColumnIds, onVisibleColumnIdsChange, }) {
|
|
41
|
+
export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange, onAdd, newLine, onRemove, onRestore, editor, isValid, context, searchText, searchFrom = 8, totals, layout = "auto", adding, onOpenLine, flag, cellState, rowState, structure, onReorder, visibleColumnIds, onVisibleColumnIdsChange, }) {
|
|
42
42
|
const copy = useUiCopy();
|
|
43
43
|
const toast = useOptionalToast();
|
|
44
44
|
const read = useFormMode(mode) === "read";
|
|
@@ -259,7 +259,7 @@ export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange
|
|
|
259
259
|
and a trash can lit on each of twenty lines are forty invitations. They
|
|
260
260
|
fade instead of unmounting, so Tab still reaches them. Pressing them does
|
|
261
261
|
not put the row into in-place editing. */
|
|
262
|
-
_jsxs("span", { "data-line": id, className: "flex items-center justify-end gap-0.5 whitespace-nowrap opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 focus-within:opacity-100 in-data-stacked:opacity-100", onClick: (e) => e.stopPropagation(), children: [_jsx("span", { id: nameId, className: "sr-only", children: labelOf(line) }), canEdit && kindOf(line) === "line" ? (_jsx(IconButton, { icon: Pencil, label: copy.lines.edit, "aria-describedby": nameId, haspopup: "dialog", onClick: () => openLine(id, line) })) : null, canRemove ? (_jsx(IconButton, { icon: Trash2, label: copy.lines.remove, tone: "danger", "aria-describedby": nameId, onClick: () => remove(line) })) : null] }));
|
|
262
|
+
_jsxs("span", { "data-line": id, className: "flex items-center justify-end gap-0.5 whitespace-nowrap opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 focus-within:opacity-100 in-data-stacked:opacity-100", onClick: (e) => e.stopPropagation(), children: [_jsx("span", { id: nameId, className: "sr-only", children: labelOf(line) }), canEdit && kindOf(line) === "line" && rowState?.(line)?.readonly !== true ? (_jsx(IconButton, { icon: Pencil, label: copy.lines.edit, "aria-describedby": nameId, haspopup: "dialog", onClick: () => openLine(id, line) })) : null, canRemove ? (_jsx(IconButton, { icon: Trash2, label: copy.lines.remove, tone: "danger", "aria-describedby": nameId, onClick: () => remove(line) })) : null] }));
|
|
263
263
|
},
|
|
264
264
|
});
|
|
265
265
|
}
|
|
@@ -283,7 +283,7 @@ export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange
|
|
|
283
283
|
invalid: false,
|
|
284
284
|
}, children: _jsx(SearchField, { value: query, onChange: setQuery, placeholder: copy.lines.search }) })] })) : null, _jsx("span", { className: "text-meta text-muted tabular-nums", "aria-live": "polite", children: count }), columnControl.hasOptional || addButton !== null ? (_jsxs("span", { className: "ml-auto flex items-center gap-2", children: [_jsx(ColumnsMenu, { control: columnControl }), addButton] })) : null] }), _jsx(EditableTable, { rows: shown, columns: tableColumns, getRowId: getLineId, empty: lines.length === 0
|
|
285
285
|
? copy.lines.empty
|
|
286
|
-
: fillCopy(copy.lineItems.noMatch, { query: query.trim() }), layout: layout, ...(inline ? { onAdd: () => addInline(), addLabel: copy.lines.add } : {}), ...(bandAdds.length === 0 ? {} : { adds: bandAdds }), ...(bands === undefined ? {} : { bands }), ...(flag === undefined ? {} : { flag }), ...(onReorder === undefined || read ? {} : { onReorder }), ...(inPlace && onRemove !== undefined
|
|
286
|
+
: fillCopy(copy.lineItems.noMatch, { query: query.trim() }), layout: layout, ...(inline ? { onAdd: () => addInline(), addLabel: copy.lines.add } : {}), ...(bandAdds.length === 0 ? {} : { adds: bandAdds }), ...(bands === undefined ? {} : { bands }), ...(flag === undefined ? {} : { flag }), ...(cellState === undefined ? {} : { cellState }), ...(rowState === undefined ? {} : { rowState }), ...(onReorder === undefined || read ? {} : { onReorder }), ...(inPlace && onRemove !== undefined
|
|
287
287
|
? /* A blank row nobody wrote in is taken back without a notice: nobody
|
|
288
288
|
removed anything, so there is nothing to undo. */
|
|
289
289
|
{ onDiscard: onRemove }
|
|
@@ -291,8 +291,10 @@ export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange
|
|
|
291
291
|
? {
|
|
292
292
|
onOpen: (id) => {
|
|
293
293
|
const line = lines.find((l) => getLineId(l) === id);
|
|
294
|
-
|
|
294
|
+
/* A read-only line opens no form to change it. */
|
|
295
|
+
if (line !== undefined && rowState?.(line)?.readonly !== true) {
|
|
295
296
|
openLine(id, line);
|
|
297
|
+
}
|
|
296
298
|
},
|
|
297
299
|
}
|
|
298
300
|
: {}), ...(canChange && columns.some((c) => c.edit !== undefined)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PileFoot } from "../../foundations/grouping.js";
|
|
1
2
|
import { type MenuGroup } from "../../primitives/menu/Menu.js";
|
|
2
3
|
import type { LucideIcon } from "lucide-react";
|
|
3
4
|
import type { ReactNode } from "react";
|
|
@@ -127,10 +128,10 @@ export declare function GroupHeader({ field, label, count, detail, depth, open,
|
|
|
127
128
|
open?: boolean;
|
|
128
129
|
onToggle?: () => void;
|
|
129
130
|
}): import("react").JSX.Element;
|
|
130
|
-
/** Under a pile
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
/** Under a short pile: «Ver 50 más de 49.950», or «Se muestran 500 de 49.950» when there is
|
|
132
|
+
* no next page. Indented like the pile's rows. */
|
|
133
|
+
export declare function GroupMore({ foot, depth, onMore, }: {
|
|
134
|
+
foot: PileFoot;
|
|
134
135
|
depth?: number;
|
|
135
136
|
onMore: () => void;
|
|
136
137
|
}): import("react").JSX.Element;
|
|
@@ -5,6 +5,7 @@ import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
|
5
5
|
import { byDensity, useDensity } from "../../foundations/density.js";
|
|
6
6
|
import { stickyTop } from "../../foundations/sticky.js";
|
|
7
7
|
import { Menu } from "../../primitives/menu/Menu.js";
|
|
8
|
+
import { PileFootLine } from "../../primitives/table/PileFootLine.js";
|
|
8
9
|
import { HEADER_TEXT } from "../../primitives/table/Table.js";
|
|
9
10
|
import { CopyButton } from "../../primitives/value/Copyable.js";
|
|
10
11
|
import { Avatar } from "../../primitives/value/Person.js";
|
|
@@ -137,9 +138,9 @@ export function GroupHeader({ field, label, count, detail, depth = 0, open = tru
|
|
|
137
138
|
const mark = (_jsx("span", { "aria-hidden": true, className: "pointer-events-none absolute inset-x-1 inset-y-0.5 rounded-control group-hover/head:bg-ink/4" }));
|
|
138
139
|
return onToggle === undefined ? (_jsx("div", { className: box, style: indent, children: content })) : (_jsxs("button", { type: "button", onClick: onToggle, "aria-expanded": open, style: indent, className: `group/head relative ${box} w-full text-left focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary`, children: [mark, content] }));
|
|
139
140
|
}
|
|
140
|
-
/** Under a pile
|
|
141
|
-
|
|
141
|
+
/** Under a short pile: «Ver 50 más de 49.950», or «Se muestran 500 de 49.950» when there is
|
|
142
|
+
* no next page. Indented like the pile's rows. */
|
|
143
|
+
export function GroupMore({ foot, depth = 0, onMore, }) {
|
|
142
144
|
const d = useDensity();
|
|
143
|
-
|
|
144
|
-
return (_jsx("div", { className: `border-b border-line ${byDensity(d, "px-3 py-1", "px-2.5 py-0.5")}`, style: depth === 0 ? undefined : { paddingLeft: `calc(var(--spacing-indent) * ${depth})` }, children: _jsx("button", { type: "button", onClick: onMore, className: "rounded-control text-meta text-primary transition-colors duration-(--duration-instant) hover:underline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary", children: fillCopy(copy.table.pileMore, { count: left.toLocaleString(copy.locale) }) }) }));
|
|
145
|
+
return (_jsx("div", { className: `border-b border-line ${byDensity(d, "px-3 py-1", "px-2.5 py-0.5")}`, style: depth === 0 ? undefined : { paddingLeft: `calc(var(--spacing-indent) * ${depth})` }, children: _jsx(PileFootLine, { foot: foot, onMore: onMore }) }));
|
|
145
146
|
}
|
|
@@ -239,6 +239,10 @@ export type UiCopy = {
|
|
|
239
239
|
distinct: string;
|
|
240
240
|
/** Under a pile that holds its first page and counts them all: ask for the rest. */
|
|
241
241
|
pileMore: string;
|
|
242
|
+
/** The same, when whoever pages says how many one press brings: «Show 50 more of 49,950». */
|
|
243
|
+
pileMoreOf: string;
|
|
244
|
+
/** Under a short pile with no next page: what it shows of what it counts. */
|
|
245
|
+
pileShown: string;
|
|
242
246
|
};
|
|
243
247
|
menu: {
|
|
244
248
|
empty: string;
|
|
@@ -205,6 +205,8 @@ export const SPANISH_COPY = {
|
|
|
205
205
|
max: "máx {value}",
|
|
206
206
|
distinct: "{value} distintos",
|
|
207
207
|
pileMore: "Ver {count} más",
|
|
208
|
+
pileMoreOf: "Ver {count} más de {left}",
|
|
209
|
+
pileShown: "Se muestran {shown} de {count}",
|
|
208
210
|
},
|
|
209
211
|
menu: { empty: "Nada coincide." },
|
|
210
212
|
list: {
|
package/dist/foundations/copy.js
CHANGED
|
@@ -151,6 +151,8 @@ export const ENGLISH_COPY = {
|
|
|
151
151
|
max: "max {value}",
|
|
152
152
|
distinct: "{value} distinct",
|
|
153
153
|
pileMore: "Show {count} more",
|
|
154
|
+
pileMoreOf: "Show {count} more of {left}",
|
|
155
|
+
pileShown: "Showing {shown} of {count}",
|
|
154
156
|
},
|
|
155
157
|
menu: {
|
|
156
158
|
empty: "Nothing matches.",
|
|
@@ -18,7 +18,24 @@ export type FieldIds = {
|
|
|
18
18
|
invalid: boolean;
|
|
19
19
|
};
|
|
20
20
|
export declare const FieldIdsContext: import("react").Context<FieldIds | null>;
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* What a place WITHOUT a `Field` says to the control in it: what describes it and whether it
|
|
23
|
+
* is in error — no id, no label.
|
|
24
|
+
*
|
|
25
|
+
* Context: a cell of an editable table has no label of its own (its control carries an
|
|
26
|
+
* `aria-label`, the column's name) but it can be wrong — «la cantidad supera el stock» — and
|
|
27
|
+
* that has to reach the control the same way a field's error does, or the error is a red
|
|
28
|
+
* cell nobody hears. The table provides this around each control it edits; every control
|
|
29
|
+
* that reads `useFieldIds` (the package's and an application's own) gets `aria-invalid` and
|
|
30
|
+
* `aria-describedby` without the table knowing what control it is.
|
|
31
|
+
*/
|
|
32
|
+
export type ControlState = {
|
|
33
|
+
describedBy?: string;
|
|
34
|
+
invalid?: boolean;
|
|
35
|
+
};
|
|
36
|
+
export declare const ControlStateContext: import("react").Context<ControlState | null>;
|
|
37
|
+
/** The ids of the enclosing `Field` — or, outside one, the state of the place the control
|
|
38
|
+
* sits in (a cell) — resolved against the control's own props. */
|
|
22
39
|
export declare function useFieldIds(own: {
|
|
23
40
|
id?: string;
|
|
24
41
|
describedBy?: string;
|
package/dist/foundations/form.js
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { createContext, useContext } from "react";
|
|
4
4
|
export const FieldIdsContext = createContext(null);
|
|
5
|
-
|
|
5
|
+
export const ControlStateContext = createContext(null);
|
|
6
|
+
/** The ids of the enclosing `Field` — or, outside one, the state of the place the control
|
|
7
|
+
* sits in (a cell) — resolved against the control's own props. */
|
|
6
8
|
export function useFieldIds(own) {
|
|
7
9
|
const field = useContext(FieldIdsContext);
|
|
10
|
+
const place = useContext(ControlStateContext);
|
|
8
11
|
return {
|
|
9
12
|
id: own.id ?? field?.id,
|
|
10
13
|
labelledBy: field?.labelId,
|
|
11
|
-
describedBy: own.describedBy ?? field?.describedBy,
|
|
12
|
-
invalid: own.invalid ?? field?.invalid ?? false,
|
|
14
|
+
describedBy: own.describedBy ?? field?.describedBy ?? place?.describedBy,
|
|
15
|
+
invalid: own.invalid ?? field?.invalid ?? place?.invalid ?? false,
|
|
13
16
|
};
|
|
14
17
|
}
|
|
15
18
|
const Ctx = createContext("edit");
|
|
@@ -104,6 +104,15 @@ export type Pile<T> = {
|
|
|
104
104
|
items: readonly T[];
|
|
105
105
|
/** El nivel de abajo. Vacío en el más profundo. */
|
|
106
106
|
piles: readonly Pile<T>[];
|
|
107
|
+
/**
|
|
108
|
+
* What the next «show more» of a short pile brings — said by whoever pages it.
|
|
109
|
+
*
|
|
110
|
+
* `50` reads «Show 50 more of 49,950»: the button says what one press adds, not what is
|
|
111
|
+
* missing. `false` says there is no next page although the count says more (a cap on
|
|
112
|
+
* the rows held, a server that stops paging): the button goes and the pile says how many
|
|
113
|
+
* it shows. Absent, the button counts what is left, as it did before this existed.
|
|
114
|
+
*/
|
|
115
|
+
more?: number | false;
|
|
107
116
|
};
|
|
108
117
|
/**
|
|
109
118
|
* La identidad de un NIVEL: el campo y, si es fecha, su grano.
|
|
@@ -280,6 +289,25 @@ export declare function itemPath<T>(item: T, levels: readonly GroupLevel[], of:
|
|
|
280
289
|
* presentaciones que necesitan la secuencia y no el árbol —la planilla
|
|
281
290
|
* virtualizada, que cuenta índices— y el que quiera saber qué quedó visible. */
|
|
282
291
|
export declare function pileItems<T>(piles: readonly Pile<T>[]): readonly T[];
|
|
292
|
+
/**
|
|
293
|
+
* What the foot of a pile offers: the next page, or the word that there is none.
|
|
294
|
+
*
|
|
295
|
+
* Context: every presentation that draws piles —rows, cards, the table, one of the
|
|
296
|
+
* application's own— asks this, so «Show 50 more of 49,950» and «Showing 500 of 49,950»
|
|
297
|
+
* are decided once. `null` when the pile holds every row it counts, or holds piles and not
|
|
298
|
+
* rows. A short pile with no way to page (`more: false`, or no `onMore`) still says so: a
|
|
299
|
+
* header that reads 49,950 over 500 rows is a count the screen silently contradicts.
|
|
300
|
+
*/
|
|
301
|
+
export type PileFoot = {
|
|
302
|
+
kind: "more";
|
|
303
|
+
left: number;
|
|
304
|
+
next?: number;
|
|
305
|
+
} | {
|
|
306
|
+
kind: "end";
|
|
307
|
+
shown: number;
|
|
308
|
+
count: number;
|
|
309
|
+
};
|
|
310
|
+
export declare function pileFoot<T>(pile: Pile<T>, canAsk: boolean): PileFoot | null;
|
|
283
311
|
/** Cuántas filas hay en total. Suma los conteos del primer nivel, que ya
|
|
284
312
|
* incluyen los de abajo — no recorre las filas, porque puede no tenerlas. */
|
|
285
313
|
export declare function pileCount<T>(piles: readonly Pile<T>[]): number;
|
|
Binary file
|
|
@@ -9,7 +9,7 @@ export type { OpenTarget, Size, Surface, Tone } from "./types.js";
|
|
|
9
9
|
export { SPANISH_COPY } from "./copy.es.js";
|
|
10
10
|
export { flatten, loose, pick, pickAll, propsOf, slot, slotOf, type Slotted, } from "./slots.js";
|
|
11
11
|
export { dateKey, dateLabel, grainName, monthName, } from "./calendar.js";
|
|
12
|
-
export { GRAIN_ORDER, type GroupableField, type GroupGrain, type Grouping, type GroupLevel, itemPath, levelKey, moveLevel, normalizeLevels, pathKey, pathLabels, type Pile, pileCount, pileIsOpen, pileItems, pileLabel, type PileLabelOf, type PileOf, pilesFrom, pilesOf, } from "./grouping.js";
|
|
12
|
+
export { GRAIN_ORDER, type GroupableField, type GroupGrain, type Grouping, type GroupLevel, itemPath, levelKey, moveLevel, normalizeLevels, pathKey, pathLabels, type Pile, pileCount, type PileFoot, pileFoot, pileIsOpen, pileItems, pileLabel, type PileLabelOf, type PileOf, pilesFrom, pilesOf, } from "./grouping.js";
|
|
13
13
|
export { axisLabels, axisSpans, keyStarts, type Pivot, type PivotAxis, type PivotCell, type PivotCrossing, type PivotFold, pivotFrom, pivotIsEmpty, type PivotKey, type PivotLevel, type PivotMargin, type PivotMeasure, pivotOf, type PivotSource, type PivotSpec, } from "./pivot.js";
|
|
14
14
|
export { downloadPivot, pivotSheet, type PivotSheetOptions, pivotXlsx, } from "./pivotSheet.js";
|
|
15
15
|
export { STICKY_TOP, stickyReset, stickyTop, useScrollBox, useStickyLayer, } from "./sticky.js";
|
|
@@ -23,7 +23,7 @@ export { dateKey, dateLabel, grainName, monthName, } from "./calendar.js";
|
|
|
23
23
|
/* LA AGRUPACIÓN — el vocabulario, que hablan las dos capas: la lista arma el
|
|
24
24
|
árbol de montones y la tabla lo dibuja. Vive acá y no en `contexts/list`
|
|
25
25
|
porque una primitiva no puede importar de un contexto. */
|
|
26
|
-
export { GRAIN_ORDER, itemPath, levelKey, moveLevel, normalizeLevels, pathKey, pathLabels, pileCount, pileIsOpen, pileItems, pileLabel, pilesFrom, pilesOf, } from "./grouping.js";
|
|
26
|
+
export { GRAIN_ORDER, itemPath, levelKey, moveLevel, normalizeLevels, pathKey, pathLabels, pileCount, pileFoot, pileIsOpen, pileItems, pileLabel, pilesFrom, pilesOf, } from "./grouping.js";
|
|
27
27
|
/* EL PIVOTE — el mismo reparto: el vocabulario acá, la grilla en `primitives`.
|
|
28
28
|
Que los márgenes vengan de la base y que un agujero no sea un cero son reglas
|
|
29
29
|
del CONTRATO, no del dibujo, y por eso viven de este lado. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A text with the part that matched the search marked —
|
|
3
|
-
*
|
|
2
|
+
* A text with the part that matched the search marked — what the lists that filter as you
|
|
3
|
+
* type (`Picker`, `TagsField`) draw in every row, and what an option of the application's own
|
|
4
|
+
* (`renderOption`) uses to say the same. A `<mark>`, so it is also said as marked text, and
|
|
4
5
|
* bold besides the tint: in forced colors the tint is gone and the weight is what remains.
|
|
5
6
|
*/
|
|
6
7
|
export declare function MatchedText({ text, query, locale, }: {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { matchSpans } from "../../foundations/match.js";
|
|
3
3
|
/**
|
|
4
|
-
* A text with the part that matched the search marked —
|
|
5
|
-
*
|
|
4
|
+
* A text with the part that matched the search marked — what the lists that filter as you
|
|
5
|
+
* type (`Picker`, `TagsField`) draw in every row, and what an option of the application's own
|
|
6
|
+
* (`renderOption`) uses to say the same. A `<mark>`, so it is also said as marked text, and
|
|
6
7
|
* bold besides the tint: in forced colors the tint is gone and the weight is what remains.
|
|
7
8
|
*/
|
|
8
9
|
export function MatchedText({ text, query, locale, }) {
|
|
@@ -30,6 +30,14 @@ export interface PickerItem {
|
|
|
30
30
|
* lined up in one column, eight customers' debts are read top to bottom. */
|
|
31
31
|
aside?: ReactNode;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* What a `renderOption` is handed beside the item: whether it is the chosen one, and what was
|
|
35
|
+
* typed — mark it with `MatchedText`, so the row still says why it is listed.
|
|
36
|
+
*/
|
|
37
|
+
export type PickerOptionState = {
|
|
38
|
+
selected: boolean;
|
|
39
|
+
query: string;
|
|
40
|
+
};
|
|
33
41
|
/**
|
|
34
42
|
* What the list shows and how many are left out of it. Capped at 8 with the footer count,
|
|
35
43
|
* unless the list loads incrementally — then everything that arrived is listed and the
|
|
@@ -94,6 +102,14 @@ export type PickerProps = {
|
|
|
94
102
|
/** A page asked for by `onLoadMore` is on its way: a quiet row at the end says so. Unlike
|
|
95
103
|
* `loading`, the rows already there stay. */
|
|
96
104
|
loadingMore?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* The content of an option, for one that does not fit «name + meta + aside» — a product
|
|
107
|
+
* with its photo, a slot of an agenda. The row around it stays the picker's: keys, focus,
|
|
108
|
+
* the search by `name` (and `haystack`), the ✓ of the chosen one. What it draws is also
|
|
109
|
+
* what the option is called, so it should show the name. Data beyond `PickerItem` is looked
|
|
110
|
+
* up by `item.id` in the consumer's own records.
|
|
111
|
+
*/
|
|
112
|
+
renderOption?: (item: PickerItem, state: PickerOptionState) => ReactNode;
|
|
97
113
|
describedBy?: string;
|
|
98
114
|
invalid?: boolean;
|
|
99
115
|
disabled?: boolean;
|
|
@@ -109,7 +125,7 @@ export declare function Picker({ mode, ...props }: PickerProps): import("react")
|
|
|
109
125
|
* row at the end. Internal — not exported from the family — and apart from the combobox so it
|
|
110
126
|
* can be rendered on the server without a popover, which is how the tests see the rows.
|
|
111
127
|
*/
|
|
112
|
-
export declare function PickerOptions({ items, marks, hints, remaining, query, emptyText, onLoadMore, loadingMore, "aria-label": ariaLabel, }: {
|
|
128
|
+
export declare function PickerOptions({ items, marks, hints, remaining, query, emptyText, onLoadMore, loadingMore, renderOption, "aria-label": ariaLabel, }: {
|
|
113
129
|
items: readonly PickerItem[];
|
|
114
130
|
marks: boolean;
|
|
115
131
|
hints: boolean;
|
|
@@ -119,6 +135,7 @@ export declare function PickerOptions({ items, marks, hints, remaining, query, e
|
|
|
119
135
|
emptyText: string;
|
|
120
136
|
onLoadMore?: () => void;
|
|
121
137
|
loadingMore?: boolean;
|
|
138
|
+
renderOption?: (item: PickerItem, state: PickerOptionState) => ReactNode;
|
|
122
139
|
/** Only when rendered outside the combobox, which otherwise names the list. */
|
|
123
140
|
"aria-label"?: string;
|
|
124
141
|
}): import("react").JSX.Element;
|
|
@@ -98,7 +98,7 @@ export function Picker({ mode, ...props }) {
|
|
|
98
98
|
to be ABOVE the component that asks for it. */
|
|
99
99
|
return (_jsx(AriaLocale, { children: _jsx(EditablePicker, { ...props }) }));
|
|
100
100
|
}
|
|
101
|
-
function EditablePicker({ id: ownId, items, value, onChange, onSearch, valueLabel, placeholder, onBrowseAll, onCreate, onCreateAndEdit, onOpen, openTargets = ["modal", "peek", "side", "page"], emptyText, searchPlaceholder, loading = false, loadingText, onLoadMore, loadingMore = false, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, "aria-label": ariaLabel, }) {
|
|
101
|
+
function EditablePicker({ id: ownId, items, value, onChange, onSearch, valueLabel, placeholder, onBrowseAll, onCreate, onCreateAndEdit, onOpen, openTargets = ["modal", "peek", "side", "page"], emptyText, searchPlaceholder, loading = false, loadingText, onLoadMore, loadingMore = false, renderOption, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, "aria-label": ariaLabel, }) {
|
|
102
102
|
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
103
103
|
id: ownId,
|
|
104
104
|
describedBy: ownDescribedBy,
|
|
@@ -235,14 +235,14 @@ function EditablePicker({ id: ownId, items, value, onChange, onSearch, valueLabe
|
|
|
235
235
|
], onSelect: (to) => onOpen(value, to) }))] })) : null, _jsx(FieldButton, { className: "mr-1", children: _jsx(ChevronDown, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsxs(Popover, { placement: "bottom start", className: "flex w-(--trigger-width) flex-col overflow-hidden", children: [_jsx(PickerOptions, { items: listed, marks: hasMarks, hints: hasHints, remaining: remaining, query: query, emptyText: loading ? shownLoading : shownEmpty,
|
|
236
236
|
/* Not while the first page loads: the sentinel of an empty list is in view and
|
|
237
237
|
would ask for page two before page one arrived. */
|
|
238
|
-
onLoadMore: loading ? undefined : onLoadMore, loadingMore: loadingMore }), remaining > 0 && !browsable ? (_jsxs("div", { className: "flex shrink-0 items-center gap-2 border-t border-line bg-ground px-2.5 py-1.5", children: [_jsx("span", { className: "text-micro tabular-nums text-muted", children: fillCopy(copy.picker.more, { count: remaining.toLocaleString(copy.locale) }) }), _jsx("span", { className: "text-micro text-muted", children: copy.picker.refine })] })) : null] })] }));
|
|
238
|
+
onLoadMore: loading ? undefined : onLoadMore, loadingMore: loadingMore, ...(renderOption === undefined ? {} : { renderOption }) }), remaining > 0 && !browsable ? (_jsxs("div", { className: "flex shrink-0 items-center gap-2 border-t border-line bg-ground px-2.5 py-1.5", children: [_jsx("span", { className: "text-micro tabular-nums text-muted", children: fillCopy(copy.picker.more, { count: remaining.toLocaleString(copy.locale) }) }), _jsx("span", { className: "text-micro text-muted", children: copy.picker.refine })] })) : null] })] }));
|
|
239
239
|
}
|
|
240
240
|
/**
|
|
241
241
|
* The list of a `Picker`: its rows, the section headers, the «Buscar más…» row and the loading
|
|
242
242
|
* row at the end. Internal — not exported from the family — and apart from the combobox so it
|
|
243
243
|
* can be rendered on the server without a popover, which is how the tests see the rows.
|
|
244
244
|
*/
|
|
245
|
-
export function PickerOptions({ items, marks, hints, remaining, query = "", emptyText, onLoadMore, loadingMore = false, "aria-label": ariaLabel, }) {
|
|
245
|
+
export function PickerOptions({ items, marks, hints, remaining, query = "", emptyText, onLoadMore, loadingMore = false, renderOption, "aria-label": ariaLabel, }) {
|
|
246
246
|
const copy = useUiCopy();
|
|
247
247
|
const entries = useMemo(() => sectioned(items), [items]);
|
|
248
248
|
/* The actions are one block under the records: one hairline above the first of them. */
|
|
@@ -251,14 +251,14 @@ export function PickerOptions({ items, marks, hints, remaining, query = "", empt
|
|
|
251
251
|
? "mt-1 border-t border-line"
|
|
252
252
|
: ""), children: _jsxs("span", { className: "flex w-full items-center justify-between gap-2", children: [_jsx("span", { className: "min-w-0 truncate font-medium", children: item.name }), item.id !== BROWSE_ALL || remaining === 0 ? null : (_jsx("span", { className: "text-micro tabular-nums text-muted", children: fillCopy(copy.picker.more, {
|
|
253
253
|
count: remaining.toLocaleString(copy.locale),
|
|
254
|
-
}) }))] }) })) : (_jsx(ListBoxItem, { id: item.id, textValue: item.name, isDisabled: item.disabled, className: (state) => option({ ...state, hints }), children: ({ isSelected }) => (_jsx(Row, { item: item, selected: isSelected, marks: marks, hints: hints, query: query, locale: copy.locale })) }));
|
|
254
|
+
}) }))] }) })) : (_jsx(ListBoxItem, { id: item.id, textValue: item.name, isDisabled: item.disabled, className: (state) => option({ ...state, hints }), children: ({ isSelected }) => renderOption === undefined ? (_jsx(Row, { item: item, selected: isSelected, marks: marks, hints: hints, query: query, locale: copy.locale })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "min-w-0 flex-1", children: renderOption(item, { selected: isSelected, query }) }), isSelected ? (_jsx(Check, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0 self-center text-primary forced-colors:text-[HighlightText]", strokeWidth: 2.5 })) : null] })) }));
|
|
255
255
|
return (_jsxs(ListBox, { "aria-label": ariaLabel,
|
|
256
256
|
/* Nothing matched YET while the next page is coming: «nothing matches» above «loading
|
|
257
257
|
more…» says two contradicting things, so the empty list says only the second. */
|
|
258
258
|
renderEmptyState: () => (_jsx("p", { className: "px-3 py-3 text-center text-meta text-muted", children: loadingMore ? copy.picker.loadingMore : emptyText })),
|
|
259
259
|
/* Incremental, the list is not capped: it scrolls inside its own height and the
|
|
260
260
|
sentinel at its end is what asks for more. */
|
|
261
|
-
className: cn("min-h-0 flex-1 overflow-y-auto p-1 outline-0", onLoadMore === undefined ? "" : "max-h-(--size-dropdown)"), children: [_jsx(Collection, { items: entries, dependencies: [query], children: (entry) => "items" in entry ? (_jsxs(ListBoxSection, { id: entry.id, className: "not-first:mt-1 not-first:border-t not-first:border-line not-first:pt-1", children: [_jsxs(Header, { className: "flex items-center gap-2 px-2 pt-1 pb-0.5 text-micro font-medium text-muted", children: [marks ? (_jsx("span", { className: "size-(--size-icon-sm) shrink-0", "aria-hidden": true })) : null, entry.section] }), _jsx(Collection, { items: entry.items, dependencies: [query], children: renderItem })] })) : (renderItem(entry)) }), onLoadMore === undefined ? null : (_jsx(ListBoxLoadMoreItem, { onLoadMore: onLoadMore, isLoading: loadingMore && items.length > 0, className: "px-3 py-1.5 text-center text-micro text-muted", children: copy.picker.loadingMore }))] }));
|
|
261
|
+
className: cn("min-h-0 flex-1 overflow-y-auto p-1 outline-0", onLoadMore === undefined ? "" : "max-h-(--size-dropdown)"), children: [_jsx(Collection, { items: entries, dependencies: [query, renderOption], children: (entry) => "items" in entry ? (_jsxs(ListBoxSection, { id: entry.id, className: "not-first:mt-1 not-first:border-t not-first:border-line not-first:pt-1", children: [_jsxs(Header, { className: "flex items-center gap-2 px-2 pt-1 pb-0.5 text-micro font-medium text-muted", children: [marks ? (_jsx("span", { className: "size-(--size-icon-sm) shrink-0", "aria-hidden": true })) : null, entry.section] }), _jsx(Collection, { items: entry.items, dependencies: [query, renderOption], children: renderItem })] })) : (renderItem(entry)) }), onLoadMore === undefined ? null : (_jsx(ListBoxLoadMoreItem, { onLoadMore: onLoadMore, isLoading: loadingMore && items.length > 0, className: "px-3 py-1.5 text-center text-micro text-muted", children: copy.picker.loadingMore }))] }));
|
|
262
262
|
}
|
|
263
263
|
function Row({ item, selected, marks, hints, query, locale, }) {
|
|
264
264
|
const Icon = item.icon;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type FormMode } from "../../foundations/form.js";
|
|
2
2
|
import type { OpenTarget } from "../../foundations/types.js";
|
|
3
3
|
import { type TagOption } from "./tagChoices.js";
|
|
4
|
+
import { type ReactNode } from "react";
|
|
4
5
|
export type TagsFieldProps = {
|
|
5
6
|
/** The ids of the chosen tags, in the order they were added. */
|
|
6
7
|
value: readonly string[];
|
|
@@ -31,6 +32,15 @@ export type TagsFieldProps = {
|
|
|
31
32
|
/** Where a pressed chip opens. A peek by default: a chip is a glance at one of many — «who is
|
|
32
33
|
* this follower?» — not a place to go and work. */
|
|
33
34
|
openTarget?: OpenTarget;
|
|
35
|
+
/**
|
|
36
|
+
* The content of an option in the list, for one that does not fit «face + name + meta». The
|
|
37
|
+
* row stays the field's — keys, the search by `label`, Enter that adds — and «Create…» and
|
|
38
|
+
* «Buscar más…» stay the field's too. Mark what was typed with `MatchedText`. The chips are
|
|
39
|
+
* not touched: a chosen tag is read by its name.
|
|
40
|
+
*/
|
|
41
|
+
renderOption?: (option: TagOption, state: {
|
|
42
|
+
query: string;
|
|
43
|
+
}) => ReactNode;
|
|
34
44
|
/** The input's placeholder while no tag is chosen. Defaults to the copy table's «Add…». */
|
|
35
45
|
placeholder?: string;
|
|
36
46
|
disabled?: boolean;
|
|
@@ -84,7 +84,7 @@ export function TagsField({ mode, ...props }) {
|
|
|
84
84
|
to be ABOVE the component that asks for it. */
|
|
85
85
|
return (_jsx(AriaLocale, { children: _jsx(EditableTagsField, { ...props }) }));
|
|
86
86
|
}
|
|
87
|
-
function EditableTagsField({ value, onChange, options, onCreate, onSearch, onBrowseAll, onOpen, openTarget, placeholder, disabled = false, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, "aria-label": ariaLabel, }) {
|
|
87
|
+
function EditableTagsField({ value, onChange, options, onCreate, onSearch, onBrowseAll, onOpen, openTarget, renderOption, placeholder, disabled = false, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, "aria-label": ariaLabel, }) {
|
|
88
88
|
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
89
89
|
id: ownId,
|
|
90
90
|
describedBy: ownDescribedBy,
|
|
@@ -184,7 +184,7 @@ function EditableTagsField({ value, onChange, options, onCreate, onSearch, onBro
|
|
|
184
184
|
onChange?.(withoutTag(value, last));
|
|
185
185
|
}, empty: text === "" }), _jsx(Popover, { triggerRef: boxRef, ...(width === null
|
|
186
186
|
? {}
|
|
187
|
-
: { style: { "--trigger-width": `${width}px` } }), placement: "bottom start", className: "w-(--trigger-width)", children: _jsx(ListBox, { items: listed, dependencies: [text], className: "max-h-[inherit] overflow-auto p-1 outline-0", children: (item) => (_jsx(ListBoxItem, { id: item.value, textValue: item.label, className: (state) => cn(listOptionStyles(state), item.value === CREATE || item.value === BROWSE ? "text-primary" : ""), children: item.value === CREATE ? (_jsxs(_Fragment, { children: [_jsx(Plus, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0", strokeWidth: 2 }), _jsx("span", { className: "truncate font-medium", children: item.label })] })) : item.value === BROWSE ? (_jsxs(_Fragment, { children: [_jsx(Search, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0", strokeWidth: 2 }), _jsx("span", { className: "truncate font-medium", children: item.label })] })) : (_jsxs(_Fragment, { children: [item.avatar === undefined ? null : (_jsx(Avatar, { name: item.label, size: "sm", ...(typeof item.avatar === "string" ? { src: item.avatar } : {}) })), _jsx("span", { className: "truncate", children: _jsx(MatchedText, { text: item.label, query: text, locale: copy.locale }) })] })) })) }) })] })] }));
|
|
187
|
+
: { style: { "--trigger-width": `${width}px` } }), placement: "bottom start", className: "w-(--trigger-width)", children: _jsx(ListBox, { items: listed, dependencies: [text, renderOption], className: "max-h-[inherit] overflow-auto p-1 outline-0", children: (item) => (_jsx(ListBoxItem, { id: item.value, textValue: item.label, className: (state) => cn(listOptionStyles(state), item.value === CREATE || item.value === BROWSE ? "text-primary" : ""), children: item.value === CREATE ? (_jsxs(_Fragment, { children: [_jsx(Plus, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0", strokeWidth: 2 }), _jsx("span", { className: "truncate font-medium", children: item.label })] })) : item.value === BROWSE ? (_jsxs(_Fragment, { children: [_jsx(Search, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0", strokeWidth: 2 }), _jsx("span", { className: "truncate font-medium", children: item.label })] })) : renderOption !== undefined ? (_jsx("span", { className: "min-w-0 flex-1", children: renderOption(item, { query: text }) })) : (_jsxs(_Fragment, { children: [item.avatar === undefined ? null : (_jsx(Avatar, { name: item.label, size: "sm", ...(typeof item.avatar === "string" ? { src: item.avatar } : {}) })), _jsxs("span", { className: "flex min-w-0 flex-col gap-0.5", children: [_jsx("span", { className: "truncate", children: _jsx(MatchedText, { text: item.label, query: text, locale: copy.locale }) }), item.meta === undefined ? null : (_jsx("span", { className: "truncate text-micro tabular-nums text-muted", children: _jsx(MatchedText, { text: item.meta, query: text, locale: copy.locale }) }))] })] })) })) }) })] })] }));
|
|
188
188
|
}
|
|
189
189
|
/** The input after the chips. It reads the combobox's state to know whether the keyboard
|
|
190
190
|
* marked an option — Enter with nothing marked takes the first match. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { Picker, type PickerItem, type PickerProps, } from "./Picker.js";
|
|
1
|
+
export { Picker, type PickerItem, type PickerOptionState, type PickerProps, } from "./Picker.js";
|
|
2
|
+
export { MatchedText } from "./MatchedText.js";
|
|
2
3
|
export { type TagOption } from "./tagChoices.js";
|
|
3
4
|
export { TagsField, type TagsFieldProps } from "./TagsField.js";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/* CHOOSING RECORDS — one (`Picker`, the many2one) or several (`TagsField`, the many2many).
|
|
2
2
|
The replacement for `<select>` when you have to SEE in order to choose. */
|
|
3
3
|
export { Picker, } from "./Picker.js";
|
|
4
|
+
/* Lo buscado, marcado: para que una opción propia (`renderOption`) lo diga como las demás. */
|
|
5
|
+
export { MatchedText } from "./MatchedText.js";
|
|
4
6
|
/* El many2many: las fichas elegidas más una entrada que agrega la siguiente. */
|
|
5
7
|
export {} from "./tagChoices.js";
|
|
6
8
|
export { TagsField } from "./TagsField.js";
|
|
@@ -13,6 +13,13 @@ export interface TagOption {
|
|
|
13
13
|
tone?: Tone;
|
|
14
14
|
/** A person: its face on the chip and in the list — `true` for initials, a URL for a photo. */
|
|
15
15
|
avatar?: true | string;
|
|
16
|
+
/**
|
|
17
|
+
* The datum that tells two options apart, already written — the NIT of a customer, the
|
|
18
|
+
* email of a follower. Drawn under the name in the list and searched like it; NOT on the
|
|
19
|
+
* chip: a chip is recognised by its name, and a NIT on every chip turns the strip into a
|
|
20
|
+
* table. Same role as `PickerItem.meta`.
|
|
21
|
+
*/
|
|
22
|
+
meta?: string;
|
|
16
23
|
}
|
|
17
24
|
/** How the typed text is compared — the locale's, handed in by the component. */
|
|
18
25
|
export type TagMatch = {
|
|
@@ -14,10 +14,15 @@ searched = false) {
|
|
|
14
14
|
const chosen = new Set(value);
|
|
15
15
|
const query = text.trim();
|
|
16
16
|
const available = options.filter((option) => !chosen.has(option.value) &&
|
|
17
|
-
(searched ||
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
(searched ||
|
|
18
|
+
query === "" ||
|
|
19
|
+
match.contains(option.label, query) ||
|
|
20
|
+
(option.meta !== undefined && match.contains(option.meta, query))));
|
|
21
|
+
/* Typing a NIT that one option carries names that option: creating a tag called «1023…»
|
|
22
|
+
next to it is the duplicate this rule exists to prevent. */
|
|
23
|
+
const named = options.some((option) => match.same(option.label, query) ||
|
|
24
|
+
(option.meta !== undefined && match.same(option.meta, query)));
|
|
25
|
+
const create = creatable && query !== "" && !named ? query : null;
|
|
21
26
|
return { available, create };
|
|
22
27
|
}
|
|
23
28
|
/** The value with one more id at the end; the same value when it is already there. */
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
|
-
import { pathKey, pileItems } from "../../foundations/grouping.js";
|
|
4
|
+
import { pathKey, pileFoot, pileItems, } from "../../foundations/grouping.js";
|
|
5
5
|
import { CellCopy } from "./CellCopy.js";
|
|
6
6
|
import { CellEditor } from "./CellEditor.js";
|
|
7
7
|
import { EditRow, firstControl } from "./EditableTable.js";
|
|
8
|
+
import { PileFootLine } from "./PileFootLine.js";
|
|
8
9
|
import { RowActions } from "./RowActions.js";
|
|
9
10
|
import { useRowWindow } from "./rowWindow.js";
|
|
10
11
|
import { SelectBox, Table, Td, Th, THead, Tr, } from "./Table.js";
|
|
@@ -140,21 +141,21 @@ function entriesOfPiles(piles, keyOf, collapsed, onToggle, onMore) {
|
|
|
140
141
|
if (pile.piles.length > 0) {
|
|
141
142
|
return [head, ...entriesOfPiles(pile.piles, keyOf, collapsed, onToggle, onMore)];
|
|
142
143
|
}
|
|
143
|
-
const
|
|
144
|
+
const foot = pileFoot(pile, onMore !== undefined);
|
|
144
145
|
return [
|
|
145
146
|
head,
|
|
146
147
|
...pile.items.map((item) => ({ kind: "row", id: keyOf(item), item })),
|
|
147
|
-
...(
|
|
148
|
-
? [
|
|
148
|
+
...(foot === null
|
|
149
|
+
? []
|
|
150
|
+
: [
|
|
149
151
|
{
|
|
150
152
|
kind: "more",
|
|
151
153
|
id: `\u0000more${key}`,
|
|
152
154
|
depth: pile.depth,
|
|
153
|
-
|
|
154
|
-
more: () => onMore(pile),
|
|
155
|
+
foot,
|
|
156
|
+
more: () => onMore?.(pile),
|
|
155
157
|
},
|
|
156
|
-
]
|
|
157
|
-
: []),
|
|
158
|
+
]),
|
|
158
159
|
];
|
|
159
160
|
});
|
|
160
161
|
}
|
|
@@ -385,9 +386,7 @@ export function DataTable({ data, columns, getRowId, sort, onSortChange, levels,
|
|
|
385
386
|
if (entry.kind === "more") {
|
|
386
387
|
return [
|
|
387
388
|
gap,
|
|
388
|
-
_jsx("tr", { ...windowed, className: "border-b border-line", children: _jsx("td", { colSpan: width, className: "px-3 py-1", style: indent, children: _jsx(
|
|
389
|
-
count: entry.left.toLocaleString(copy.locale),
|
|
390
|
-
}) }) }) }, entry.id),
|
|
389
|
+
_jsx("tr", { ...windowed, className: "border-b border-line", children: _jsx("td", { colSpan: width, className: "px-3 py-1", style: indent, children: _jsx(PileFootLine, { foot: entry.foot, onMore: entry.more }) }) }, entry.id),
|
|
391
390
|
];
|
|
392
391
|
}
|
|
393
392
|
const original = entry.item;
|