@sincpro/web-ui 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -13
- package/dist/components/activity/Activity.d.ts +69 -0
- package/dist/components/activity/Activity.js +279 -0
- package/dist/components/activity/ActivityBadge.d.ts +16 -0
- package/dist/components/activity/ActivityBadge.js +29 -0
- package/dist/components/{chatter → activity}/Messages.d.ts +2 -2
- package/dist/components/{chatter → activity}/Messages.js +9 -6
- package/dist/components/activity/index.d.ts +4 -0
- package/dist/components/activity/index.js +5 -0
- package/dist/components/assist/AssistMark.d.ts +3 -3
- package/dist/components/assist/AssistMark.js +1 -1
- package/dist/components/assist/assist.d.ts +1 -1
- package/dist/components/assist/assist.js +1 -1
- package/dist/components/calendar/RecordCalendar.d.ts +48 -0
- package/dist/components/calendar/RecordCalendar.js +101 -0
- package/dist/components/calendar/index.d.ts +1 -0
- package/dist/components/calendar/index.js +1 -0
- package/dist/components/form/Sheet.d.ts +4 -0
- package/dist/components/form/Sheet.js +16 -5
- package/dist/components/gantt/RecordGantt.d.ts +44 -0
- package/dist/components/gantt/RecordGantt.js +110 -0
- package/dist/components/gantt/index.d.ts +1 -0
- package/dist/components/gantt/index.js +1 -0
- package/dist/components/index.d.ts +4 -1
- package/dist/components/index.js +4 -1
- package/dist/components/lines/LineItems.d.ts +45 -5
- package/dist/components/lines/LineItems.js +106 -18
- package/dist/components/listing/BoardColumns.d.ts +39 -0
- package/dist/components/listing/BoardColumns.js +138 -0
- package/dist/components/listing/Listing.js +1 -1
- package/dist/components/listing/ListingBar.d.ts +46 -2
- package/dist/components/listing/ListingBar.js +47 -5
- package/dist/components/listing/PivotExplorer.d.ts +34 -0
- package/dist/components/listing/PivotExplorer.js +86 -0
- package/dist/components/listing/RecordCard.d.ts +6 -8
- package/dist/components/listing/RecordCard.js +13 -7
- package/dist/components/listing/Row.d.ts +25 -2
- package/dist/components/listing/Row.js +30 -13
- package/dist/components/listing/index.d.ts +2 -0
- package/dist/components/listing/index.js +2 -0
- package/dist/components/listing/set.d.ts +1 -1
- package/dist/components/navigation/index.d.ts +0 -2
- package/dist/components/navigation/index.js +2 -3
- package/dist/components/record/RecordAudit.d.ts +17 -0
- package/dist/components/record/RecordAudit.js +27 -0
- package/dist/components/record/RecordHeader.d.ts +27 -17
- package/dist/components/record/RecordHeader.js +64 -20
- package/dist/components/record/RecordNotices.d.ts +37 -0
- package/dist/components/record/RecordNotices.js +75 -0
- package/dist/components/record/RecordRibbon.d.ts +13 -0
- package/dist/components/record/RecordRibbon.js +22 -0
- package/dist/components/record/ThumbBar.js +1 -1
- package/dist/components/record/index.d.ts +2 -0
- package/dist/components/record/index.js +2 -0
- package/dist/components/search/CriteriaChips.d.ts +6 -1
- package/dist/components/search/CriteriaChips.js +71 -20
- package/dist/components/search/SearchBar.d.ts +4 -1
- package/dist/components/search/SearchBar.js +15 -7
- package/dist/components/timeline/RecordTimeline.d.ts +60 -0
- package/dist/components/timeline/RecordTimeline.js +214 -0
- package/dist/components/timeline/TimelineToolbar.d.ts +30 -0
- package/dist/components/timeline/TimelineToolbar.js +31 -0
- package/dist/components/timeline/index.d.ts +3 -0
- package/dist/components/timeline/index.js +3 -0
- package/dist/components/timeline/time.d.ts +43 -0
- package/dist/components/timeline/time.js +135 -0
- package/dist/foundations/copy.d.ts +194 -18
- package/dist/foundations/copy.es.js +142 -15
- package/dist/foundations/copy.js +133 -11
- package/dist/foundations/index.d.ts +1 -1
- package/dist/foundations/match.d.ts +23 -0
- package/dist/foundations/match.js +63 -0
- package/dist/foundations/slots.d.ts +1 -1
- package/dist/foundations/slots.js +8 -3
- package/dist/foundations/types.d.ts +7 -0
- package/dist/foundations/types.js +0 -36
- package/dist/primitives/block/index.d.ts +1 -1
- package/dist/primitives/block/index.js +1 -1
- package/dist/primitives/cell/CellStack.d.ts +7 -1
- package/dist/primitives/cell/CellStack.js +5 -2
- package/dist/primitives/choice/ComboBox.d.ts +3 -0
- package/dist/primitives/choice/ComboBox.js +10 -6
- package/dist/primitives/choice/KanbanState.d.ts +14 -0
- package/dist/primitives/choice/KanbanState.js +37 -0
- package/dist/primitives/choice/Priority.d.ts +20 -0
- package/dist/primitives/choice/Priority.js +41 -0
- package/dist/primitives/choice/SearchField.d.ts +10 -1
- package/dist/primitives/choice/SearchField.js +21 -2
- package/dist/primitives/choice/Slider.d.ts +6 -1
- package/dist/primitives/choice/Slider.js +2 -2
- package/dist/primitives/choice/index.d.ts +2 -0
- package/dist/primitives/choice/index.js +2 -0
- package/dist/primitives/date/Calendar.js +30 -6
- package/dist/primitives/date/DatePicker.d.ts +11 -3
- package/dist/primitives/date/DatePicker.js +12 -8
- package/dist/primitives/date/DateTime.d.ts +35 -0
- package/dist/primitives/date/DateTime.js +100 -0
- package/dist/primitives/date/index.d.ts +1 -0
- package/dist/primitives/date/index.js +4 -2
- package/dist/primitives/dialog/Dialog.d.ts +9 -3
- package/dist/primitives/dialog/Dialog.js +41 -5
- package/dist/primitives/field/DropZone.js +1 -1
- package/dist/primitives/field/Field.d.ts +14 -2
- package/dist/primitives/field/Field.js +25 -14
- package/dist/primitives/field/copy.d.ts +16 -0
- package/dist/primitives/field/copy.js +69 -0
- package/dist/primitives/field/index.d.ts +2 -0
- package/dist/primitives/field/index.js +4 -0
- package/dist/primitives/field/note.d.ts +55 -0
- package/dist/primitives/field/note.js +117 -0
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/menu/Menu.d.ts +28 -0
- package/dist/primitives/menu/Menu.js +47 -17
- package/dist/primitives/notice/Callout.js +1 -1
- package/dist/primitives/notice/Flag.d.ts +13 -0
- package/dist/primitives/notice/Flag.js +31 -0
- package/dist/primitives/notice/index.d.ts +2 -1
- package/dist/primitives/notice/index.js +2 -1
- package/dist/primitives/overlay/Preview.d.ts +19 -0
- package/dist/primitives/overlay/Preview.js +69 -0
- package/dist/primitives/overlay/index.d.ts +1 -0
- package/dist/primitives/overlay/index.js +3 -1
- package/dist/primitives/picker/MatchedText.d.ts +10 -0
- package/dist/primitives/picker/MatchedText.js +12 -0
- package/dist/primitives/picker/Picker.d.ts +67 -4
- package/dist/primitives/picker/Picker.js +142 -23
- package/dist/primitives/table/CellCopy.d.ts +20 -0
- package/dist/primitives/table/CellCopy.js +23 -0
- package/dist/primitives/table/DataTable.d.ts +55 -1
- package/dist/primitives/table/DataTable.js +126 -40
- package/dist/primitives/table/EditableTable.d.ts +102 -2
- package/dist/primitives/table/EditableTable.js +287 -32
- package/dist/primitives/table/ReportTable.js +2 -1
- package/dist/primitives/table/RowActions.js +7 -2
- package/dist/primitives/table/SheetTable.d.ts +4 -1
- package/dist/primitives/table/SheetTable.js +38 -5
- package/dist/primitives/table/Table.d.ts +5 -0
- package/dist/primitives/table/Table.js +17 -8
- package/dist/primitives/table/index.d.ts +2 -2
- package/dist/primitives/table/index.js +1 -1
- package/dist/primitives/tabs/Tabs.d.ts +2 -2
- package/dist/primitives/tabs/Tabs.js +1 -1
- package/dist/primitives/tag/Tags.d.ts +2 -0
- package/dist/primitives/tag/Tags.js +2 -1
- package/dist/primitives/tag/TagsField.d.ts +13 -0
- package/dist/primitives/tag/TagsField.js +39 -10
- package/dist/primitives/tag/choices.d.ts +2 -0
- package/dist/primitives/tree/Tree.d.ts +56 -0
- package/dist/primitives/tree/Tree.js +74 -0
- package/dist/primitives/tree/index.d.ts +1 -0
- package/dist/primitives/tree/index.js +3 -0
- package/dist/primitives/value/Avatar.d.ts +16 -0
- package/dist/primitives/value/Avatar.js +91 -0
- package/dist/primitives/value/Copyable.d.ts +11 -0
- package/dist/primitives/value/Copyable.js +27 -6
- package/dist/primitives/value/Fecha.js +10 -1
- package/dist/primitives/value/Person.d.ts +6 -51
- package/dist/primitives/value/Person.js +9 -148
- package/dist/primitives/value/ProgressBar.d.ts +31 -0
- package/dist/primitives/value/ProgressBar.js +53 -0
- package/dist/primitives/value/Relation.d.ts +22 -5
- package/dist/primitives/value/Relation.js +42 -14
- package/dist/primitives/value/Totals.d.ts +102 -7
- package/dist/primitives/value/Totals.js +52 -2
- package/dist/primitives/value/Value.d.ts +0 -37
- package/dist/primitives/value/Value.js +0 -39
- package/dist/primitives/value/contact.d.ts +30 -0
- package/dist/primitives/value/contact.js +51 -0
- package/dist/primitives/value/index.d.ts +5 -4
- package/dist/primitives/value/index.js +5 -4
- package/dist/tokens.css +179 -7
- package/dist/views/list/List.d.ts +32 -3
- package/dist/views/list/List.js +51 -7
- package/dist/views/list/ListView.d.ts +1 -1
- package/dist/views/list/ListView.js +70 -13
- package/dist/views/list/columns.js +8 -1
- package/dist/views/list/derive.d.ts +12 -1
- package/dist/views/list/derive.js +29 -3
- package/dist/views/list/presentations.d.ts +5 -2
- package/dist/views/list/presentations.js +35 -3
- package/dist/views/list/types.d.ts +57 -1
- package/dist/views/list/useNarrowList.d.ts +10 -0
- package/dist/views/list/useNarrowList.js +20 -6
- package/dist/views/record/Record.d.ts +41 -8
- package/dist/views/record/Record.js +113 -28
- package/dist/views/record/index.d.ts +1 -1
- package/package.json +15 -4
- package/dist/components/chatter/Chatter.d.ts +0 -161
- package/dist/components/chatter/Chatter.js +0 -232
- package/dist/components/chatter/index.d.ts +0 -3
- package/dist/components/chatter/index.js +0 -4
- package/dist/components/navigation/CommandPalette.d.ts +0 -40
- package/dist/components/navigation/CommandPalette.js +0 -114
- package/dist/components/navigation/commands.d.ts +0 -30
- package/dist/components/navigation/commands.js +0 -30
- /package/dist/components/{chatter → activity}/Timeline.d.ts +0 -0
- /package/dist/components/{chatter → activity}/Timeline.js +0 -0
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import { currentCopyLocale, fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
4
|
import { byDensity, useDensity } from "../../foundations/density.js";
|
|
5
5
|
import { stickyTop, useScrollBox } from "../../foundations/sticky.js";
|
|
6
|
+
import { HEADER_TEXT } from "./Table.js";
|
|
6
7
|
import { ChevronRight, Info } from "lucide-react";
|
|
7
8
|
import { useState } from "react";
|
|
8
9
|
/* `useGrouping: "always"` and not the default: without it, four-digit numbers
|
|
@@ -102,5 +103,5 @@ defaultOpen, caption, height, }) {
|
|
|
102
103
|
desplazamiento de la barra y quedaba empujado 68 px hacia adentro de su
|
|
103
104
|
propia caja — banda en blanco arriba y títulos de columna cayendo entre
|
|
104
105
|
las filas. */
|
|
105
|
-
_jsx("div", { ref: caja.ref, className: `min-w-0 ${caja.className}`, style: caja.style, children: _jsxs("table", { className: "w-full border-collapse px-1", children: [caption === undefined ? null : (_jsx("caption", { className: `text-left text-meta text-muted ${byDensity(d, "px-3 pb-2", "px-2 pb-1.5")}`, children: caption })), _jsx("thead", { className: "sticky z-20 bg-ground", style: stickyTop, children: _jsxs("tr", { className: "border-b border-line-strong", children: [_jsx("th", { className: `sticky left-0 z-10 bg-ground text-left ${compact ? "px-2 py-1.5" : "px-3 py-2"}`, children: _jsx("span", { className: "sr-only", children: copy.report.concept }) }), columns.map((c) => (_jsx("th", { scope: "col", className: `text-right
|
|
106
|
+
_jsx("div", { ref: caja.ref, className: `min-w-0 ${caja.className}`, style: caja.style, children: _jsxs("table", { className: "w-full border-collapse px-1", children: [caption === undefined ? null : (_jsx("caption", { className: `text-left text-meta text-muted ${byDensity(d, "px-3 pb-2", "px-2 pb-1.5")}`, children: caption })), _jsx("thead", { className: "sticky z-20 bg-ground", style: stickyTop, children: _jsxs("tr", { className: "border-b border-line-strong", children: [_jsx("th", { className: `sticky left-0 z-10 bg-ground text-left ${compact ? "px-2 py-1.5" : "px-3 py-2"}`, children: _jsx("span", { className: "sr-only", children: copy.report.concept }) }), columns.map((c) => (_jsx("th", { scope: "col", className: `text-right ${HEADER_TEXT} ${compact ? "px-2 py-1.5" : "px-3 py-2"}`, children: c.header }, c.id))), _jsx("th", { className: "w-0" })] }) }), _jsx("tbody", { children: rows.map((r) => (_jsx(ReportLine, { row: r, columns: columns, open: open, onToggle: toggle, compact: compact }, r.id))) })] }) }));
|
|
106
107
|
}
|
|
@@ -3,19 +3,24 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
4
|
import { IconButton } from "../button/Button.js";
|
|
5
5
|
import { Menu } from "../menu/Menu.js";
|
|
6
|
+
import { useRowWash } from "./Table.js";
|
|
6
7
|
import { MoreHorizontal } from "lucide-react";
|
|
7
8
|
export function RowActions({ actions,
|
|
8
9
|
/** How many stay visible before the rest fall into the `⋯`. */
|
|
9
10
|
inline = 2, }) {
|
|
10
11
|
const copy = useUiCopy();
|
|
12
|
+
const wash = useRowWash();
|
|
11
13
|
if (actions.length === 0)
|
|
12
|
-
return _jsx("td", { className:
|
|
14
|
+
return _jsx("td", { className: `w-0 border-b border-line ${wash}` });
|
|
13
15
|
/* With only one left over there is no `⋯`: a menu with a single item is one
|
|
14
16
|
extra click to reach the same button. */
|
|
15
17
|
const inlineActions = actions.length <= inline + 1 ? actions : actions.slice(0, inline);
|
|
16
18
|
const overflow = actions.slice(inlineActions.length);
|
|
17
19
|
const byId = new Map(actions.map((a) => [a.id, a]));
|
|
18
|
-
return (
|
|
20
|
+
return (
|
|
21
|
+
/* The row's hairline runs under its actions too: without it the separators stopped one
|
|
22
|
+
column short and the actions read as outside the table. */
|
|
23
|
+
_jsx("td", { className: `w-0 border-b border-line py-1 pr-2 pl-1 align-middle whitespace-nowrap last:rounded-r-control ${wash}`, children: _jsxs("div", { onClick: (e) => e.stopPropagation(), className: "flex items-center justify-end gap-0.5 opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 focus-within:opacity-100 pointer-coarse:opacity-100", children: [inlineActions.map((a) => (_jsx(IconButton, { icon: a.icon, label: a.label, size: "sm", ...(a.tone === undefined ? {} : { tone: a.tone }), ...(a.disabled === undefined ? {} : { disabled: a.disabled }), onClick: a.onSelect }, a.id))), overflow.length === 0 ? null : (_jsx(Menu, { label: copy.selection.more, icon: MoreHorizontal, iconOnly: true, chevron: false, align: "end", groups: [
|
|
19
24
|
{
|
|
20
25
|
options: overflow.map((a) => ({
|
|
21
26
|
id: a.id,
|
|
@@ -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;
|
|
@@ -14,6 +14,9 @@ height, }: {
|
|
|
14
14
|
* system scale instead of pinning a size in pixels. */
|
|
15
15
|
height?: number | string;
|
|
16
16
|
}): import("react").JSX.Element;
|
|
17
|
+
/** The text of a column header, shared by every table of the family. Ink and semibold, not
|
|
18
|
+
* muted: in grey at the cells' weight the names of the columns read as one more row. */
|
|
19
|
+
export declare const HEADER_TEXT = "text-meta font-semibold whitespace-nowrap text-ink";
|
|
17
20
|
export declare function THead({ children }: {
|
|
18
21
|
children: ReactNode;
|
|
19
22
|
}): import("react").JSX.Element;
|
|
@@ -71,6 +74,8 @@ export declare function TrDetail({ children, detail, open, onToggle, columns, la
|
|
|
71
74
|
* a hovered row and a painted cell are two signals, and stacking them reads
|
|
72
75
|
* as a third color. */
|
|
73
76
|
export declare const CELL_TONE: Record<Tone, string>;
|
|
77
|
+
/** The wash of the row a cell sits in: for cells drawn outside `Td`, like the row's actions. */
|
|
78
|
+
export declare function useRowWash(): string;
|
|
74
79
|
export declare function Td({ children, numeric, align,
|
|
75
80
|
/** A CODE — an id, a hash, a key — set in the monospaced face. Off by default: a
|
|
76
81
|
* number does not need it (a numeric cell gets tabular figures on its own), and a
|
|
@@ -15,6 +15,9 @@ height, }) {
|
|
|
15
15
|
medición. */
|
|
16
16
|
_jsx("div", { ref: caja.ref, className: `min-w-0 ${caja.className}`, style: caja.style, children: _jsx("table", { className: "w-full border-separate border-spacing-0 px-1 text-left", children: children }) }));
|
|
17
17
|
}
|
|
18
|
+
/** The text of a column header, shared by every table of the family. Ink and semibold, not
|
|
19
|
+
* muted: in grey at the cells' weight the names of the columns read as one more row. */
|
|
20
|
+
export const HEADER_TEXT = "text-meta font-semibold whitespace-nowrap text-ink";
|
|
18
21
|
export function THead({ children }) {
|
|
19
22
|
return (
|
|
20
23
|
/* Se pega DEBAJO de lo que ya esté pegado —la barra de la aplicación y la
|
|
@@ -43,19 +46,21 @@ export function Th({ children, sortKey, sort, onSort, numeric = false, align, wi
|
|
|
43
46
|
onSort(null);
|
|
44
47
|
}
|
|
45
48
|
const Icon = dir === "asc" ? ArrowUp : dir === "desc" ? ArrowDown : ChevronsUpDown;
|
|
46
|
-
return (_jsx("th", { scope: "col", style: width === undefined ? undefined : { width }, "aria-sort": active ? (dir === "asc" ? "ascending" : "descending") : "none", className: `border-b border-line-strong px-3 py-2
|
|
49
|
+
return (_jsx("th", { scope: "col", style: width === undefined ? undefined : { width }, "aria-sort": active ? (dir === "asc" ? "ascending" : "descending") : "none", className: `border-b border-line-strong px-3 py-2 ${HEADER_TEXT} ${(align ?? (numeric ? "right" : "left")) === "right" ? "text-right" : "text-left"}`, children: sortKey === undefined ? (children) : (_jsxs("button", { type: "button", onClick: nextSort, className: `inline-flex items-center gap-1 rounded-control transition-colors duration-(--duration-instant) hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary ${(align ?? (numeric ? "right" : "left")) === "right" ? "flex-row-reverse" : ""} ${active ? "text-ink" : ""}`, children: [children, _jsx(Icon, { className: `size-(--size-icon-sm) shrink-0 ${active ? "text-primary" : "text-line-strong"}`, strokeWidth: 2, "aria-hidden": true })] })) }));
|
|
47
50
|
}
|
|
48
51
|
/** The wash of a row lives on the cells, not on the `<tr>`. A background on the
|
|
49
52
|
* row paints edge to edge; on the cells the ends can round and the table's
|
|
50
53
|
* padding keeps the mark off the card. */
|
|
51
|
-
const RowMark = createContext(false);
|
|
54
|
+
const RowMark = createContext({ selected: false, interactive: false });
|
|
52
55
|
const ROW_WASH = "group-hover/row:bg-ink/4";
|
|
53
56
|
const ROW_ON = "bg-primary/8";
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
/* The hover wash only where a click does something: on a read-only row it promised a click
|
|
58
|
+
that did nothing. */
|
|
59
|
+
function rowWash({ selected, interactive }) {
|
|
60
|
+
return selected ? ROW_ON : interactive ? ROW_WASH : "";
|
|
56
61
|
}
|
|
57
62
|
export function Tr({ children, onClick, selected = false, }) {
|
|
58
|
-
return (_jsx(RowMark.Provider, { value: selected, children: _jsx("tr", { onClick: onClick, className: `group/row ${onClick === undefined ? "" : "cursor-pointer"}`, children: children }) }));
|
|
63
|
+
return (_jsx(RowMark.Provider, { value: { selected, interactive: onClick !== undefined }, children: _jsx("tr", { onClick: onClick, className: `group/row ${onClick === undefined ? "" : "cursor-pointer"}`, children: children }) }));
|
|
59
64
|
}
|
|
60
65
|
/**
|
|
61
66
|
* A row that opens its own detail underneath.
|
|
@@ -71,7 +76,7 @@ export function Tr({ children, onClick, selected = false, }) {
|
|
|
71
76
|
*/
|
|
72
77
|
export function TrDetail({ children, detail, open, onToggle, columns, label, selected = false, }) {
|
|
73
78
|
const id = useId();
|
|
74
|
-
return (_jsx(RowMark.Provider, { value: selected, children: _jsxs(_Fragment, { children: [_jsxs("tr", { onClick: onToggle, className: "group/row cursor-pointer", children: [_jsx("td", { className: `border-b border-line py-1.5 pr-0 pl-2.5 align-middle first:rounded-l-control ${rowWash(selected)}`, children: _jsx("button", { type: "button", onClick: (e) => {
|
|
79
|
+
return (_jsx(RowMark.Provider, { value: { selected, interactive: true }, children: _jsxs(_Fragment, { children: [_jsxs("tr", { onClick: onToggle, className: "group/row cursor-pointer", children: [_jsx("td", { className: `border-b border-line py-1.5 pr-0 pl-2.5 align-middle first:rounded-l-control ${rowWash({ selected, interactive: true })}`, children: _jsx("button", { type: "button", onClick: (e) => {
|
|
75
80
|
e.stopPropagation();
|
|
76
81
|
onToggle();
|
|
77
82
|
}, "aria-expanded": open, "aria-controls": id, "aria-label": label, className: "inline-flex size-5 items-center justify-center rounded-pill text-muted transition-colors duration-(--duration-instant) hover:bg-ink/6 hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary", children: _jsx(ChevronRight, { className: `size-(--size-icon-sm) transition-transform duration-(--duration-instant) ${open ? "rotate-90" : ""}`, strokeWidth: 2 }) }) }), children] }), _jsx("tr", { id: id, hidden: !open, children: _jsx("td", { colSpan: columns + 1, className: "border-b border-line bg-ground px-3 py-3", children: detail }) })] }) }));
|
|
@@ -87,6 +92,10 @@ export const CELL_TONE = {
|
|
|
87
92
|
bad: "bg-bad/12",
|
|
88
93
|
accent: "bg-accent/20",
|
|
89
94
|
};
|
|
95
|
+
/** The wash of the row a cell sits in: for cells drawn outside `Td`, like the row's actions. */
|
|
96
|
+
export function useRowWash() {
|
|
97
|
+
return rowWash(useContext(RowMark));
|
|
98
|
+
}
|
|
90
99
|
export function Td({ children, numeric = false, align,
|
|
91
100
|
/** A CODE — an id, a hash, a key — set in the monospaced face. Off by default: a
|
|
92
101
|
* number does not need it (a numeric cell gets tabular figures on its own), and a
|
|
@@ -100,8 +109,8 @@ mono = false,
|
|
|
100
109
|
* something else — and in a table of forty rows by six columns, colored text
|
|
101
110
|
* gets lost. */
|
|
102
111
|
tone, title, }) {
|
|
103
|
-
const
|
|
104
|
-
return (_jsx("td", { title: title, className: `border-b border-line px-3 py-1.5 text-meta text-ink first:rounded-l-control last:rounded-r-control ${(align ?? (numeric ? "right" : "left")) === "right" ? "text-right" : "text-left"} ${mono ? "font-mono tabular-nums" : numeric ? "tabular-nums" : ""} ${tone === undefined ? rowWash(
|
|
112
|
+
const mark = useContext(RowMark);
|
|
113
|
+
return (_jsx("td", { title: title, className: `border-b border-line px-3 py-1.5 text-meta text-ink first:rounded-l-control last:rounded-r-control ${(align ?? (numeric ? "right" : "left")) === "right" ? "text-right" : "text-left"} ${mono ? "font-mono tabular-nums" : numeric ? "tabular-nums" : ""} ${tone === undefined ? rowWash(mark) : CELL_TONE[tone]}`, children: children }));
|
|
105
114
|
}
|
|
106
115
|
/** The selection checkbox, with its third state.
|
|
107
116
|
*
|
|
@@ -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
|
|
@@ -7,6 +7,8 @@ export type TagItem = {
|
|
|
7
7
|
* comes from, which symbol a comparison is. It goes muted and inside. */
|
|
8
8
|
note?: string;
|
|
9
9
|
icon?: LucideIcon;
|
|
10
|
+
/** A person on the chip: its photo, or its initials when `true` — followers, assignees. */
|
|
11
|
+
avatar?: true | string;
|
|
10
12
|
/** Choose it. Without this the chip is a value and is not pressed. */
|
|
11
13
|
onSelect?: () => void;
|
|
12
14
|
/** Remove it, when removing it means something. It is a separate button
|
|
@@ -4,6 +4,7 @@ import { AriaLocale } from "../../foundations/aria.js";
|
|
|
4
4
|
import { bySurface, useSurface } from "../../foundations/surface.js";
|
|
5
5
|
import { cn, focusRing } from "../../foundations/variants.js";
|
|
6
6
|
import { tagTone } from "./Tag.js";
|
|
7
|
+
import { Avatar } from "../value/Person.js";
|
|
7
8
|
import { X } from "lucide-react";
|
|
8
9
|
import { Button as AriaButton, Tag as AriaTag, TagGroup, TagList, } from "react-aria-components";
|
|
9
10
|
export function Tags({ items, surface, label, labelledBy, stack = false, multiple = false, }) {
|
|
@@ -95,7 +96,7 @@ function shell({ item, surface, stack, removable, pressable, }) {
|
|
|
95
96
|
}
|
|
96
97
|
function Inside({ item }) {
|
|
97
98
|
const Icon = item.icon;
|
|
98
|
-
return (_jsxs(_Fragment, { children: [Icon === undefined ? null : (_jsx(Icon, { className: "size-3 shrink-0 opacity-70", strokeWidth: 2, "aria-hidden": true })), _jsx("span", { className: "min-w-0 truncate", children: item.label }), item.note === undefined ? null : (_jsxs("span", { className: "shrink-0 opacity-60", children: ["\u00B7 ", item.note] }))] }));
|
|
99
|
+
return (_jsxs(_Fragment, { children: [Icon === undefined ? null : (_jsx(Icon, { className: "size-3 shrink-0 opacity-70", strokeWidth: 2, "aria-hidden": true })), item.avatar === undefined ? null : (_jsx("span", { "aria-hidden": true, className: "-my-0.5 -ml-1.5 flex shrink-0", children: _jsx(Avatar, { name: item.label, size: "sm", ...(typeof item.avatar === "string" ? { src: item.avatar } : {}) }) })), _jsx("span", { className: "min-w-0 truncate", children: item.label }), item.note === undefined ? null : (_jsxs("span", { className: "shrink-0 opacity-60", children: ["\u00B7 ", item.note] }))] }));
|
|
99
100
|
}
|
|
100
101
|
/* The × is React Aria's `remove` slot: it removes THIS chip and never presses it, so
|
|
101
102
|
removing a pressable chip does not also choose it. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { OpenTarget } from "../../foundations/types.js";
|
|
1
2
|
import { type FormMode } from "../field/mode.js";
|
|
2
3
|
import { type TagOption } from "./choices.js";
|
|
3
4
|
export type TagsFieldProps = {
|
|
@@ -14,12 +15,24 @@ export type TagsFieldProps = {
|
|
|
14
15
|
* backend (Odoo's `name_create`); the consumer also appends the new option to `options`.
|
|
15
16
|
*/
|
|
16
17
|
onCreate?: (label: string) => string | Promise<string>;
|
|
18
|
+
/** «Buscar más…» at the end of the list: the tags to add chosen from a table
|
|
19
|
+
* (the list that chooses, `List onChoose` with `selection`, in a modal). It gets what was typed. */
|
|
20
|
+
onBrowseAll?: (query: string) => void;
|
|
21
|
+
/** Open the record behind a chip — a follower, a product, a category. Pressing the chip (or
|
|
22
|
+
* Enter on it) opens it in `openTarget`; its × still only removes. Absent, a chip is a value. */
|
|
23
|
+
onOpen?: (id: string, to: OpenTarget) => void;
|
|
24
|
+
/** Where a pressed chip opens. A peek by default: a chip is a glance at one of many — «who is
|
|
25
|
+
* this follower?» — not a place to go and work. */
|
|
26
|
+
openTarget?: OpenTarget;
|
|
17
27
|
/** The input's placeholder while no tag is chosen. Defaults to the copy table's «Add…». */
|
|
18
28
|
placeholder?: string;
|
|
19
29
|
disabled?: boolean;
|
|
20
30
|
id?: string;
|
|
21
31
|
describedBy?: string;
|
|
22
32
|
invalid?: boolean;
|
|
33
|
+
/** The name of the control when it is NOT inside a `Field` — a cell of an editable table.
|
|
34
|
+
* Inside a field, the field's label names it. */
|
|
35
|
+
"aria-label"?: string;
|
|
23
36
|
/** Forces the mode. By default it inherits the form's: in read mode it draws `Tags`. */
|
|
24
37
|
mode?: FormMode;
|
|
25
38
|
};
|
|
@@ -8,9 +8,11 @@ import { useFieldIds } from "../field/context.js";
|
|
|
8
8
|
import { ReadValue } from "../field/Field.js";
|
|
9
9
|
import { useFormMode } from "../field/mode.js";
|
|
10
10
|
import { Popover } from "../overlay/Popover.js";
|
|
11
|
+
import { MatchedText } from "../picker/MatchedText.js";
|
|
11
12
|
import { tagChoices, withoutTag, withTag, } from "./choices.js";
|
|
12
13
|
import { Tags } from "./Tags.js";
|
|
13
|
-
import {
|
|
14
|
+
import { Avatar } from "../value/Person.js";
|
|
15
|
+
import { Plus, Search } from "lucide-react";
|
|
14
16
|
import { useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
15
17
|
import { ComboBox as AriaComboBox, ComboBoxStateContext, Group, Input, ListBox, ListBoxItem, useFilter, useLocale, } from "react-aria-components";
|
|
16
18
|
/* ==================================================================== *
|
|
@@ -43,6 +45,15 @@ import { ComboBox as AriaComboBox, ComboBoxStateContext, Group, Input, ListBox,
|
|
|
43
45
|
* ==================================================================== */
|
|
44
46
|
/** The key of the «create» row. Not an id: it cannot collide with one. */
|
|
45
47
|
const CREATE = "\u0000create";
|
|
48
|
+
/** The key of the «Buscar más…» row. */
|
|
49
|
+
const BROWSE = "\u0000browse";
|
|
50
|
+
/** What a chip needs to open its record: the press, and the place it opens named as its title. */
|
|
51
|
+
function useChipOpening(onOpen, to = "peek") {
|
|
52
|
+
const copy = useUiCopy();
|
|
53
|
+
return (id) => onOpen === undefined
|
|
54
|
+
? {}
|
|
55
|
+
: { onSelect: () => onOpen(id, to), title: copy.picker.openIn[to] };
|
|
56
|
+
}
|
|
46
57
|
/** The chosen tags as chips, in the order of `value`. An id missing from `options` is still
|
|
47
58
|
* drawn (as itself): hiding it would hide data the record has. */
|
|
48
59
|
function chosenOptions(options, value) {
|
|
@@ -56,6 +67,7 @@ function chosenOptions(options, value) {
|
|
|
56
67
|
* chips, and its hint and error describe the input. In read mode it is `Tags`.
|
|
57
68
|
*/
|
|
58
69
|
export function TagsField({ mode, ...props }) {
|
|
70
|
+
const opening = useChipOpening(props.onOpen, props.openTarget);
|
|
59
71
|
if (useFormMode(mode) === "read") {
|
|
60
72
|
const chosen = chosenOptions(props.options, props.value);
|
|
61
73
|
if (chosen.length === 0)
|
|
@@ -64,19 +76,22 @@ export function TagsField({ mode, ...props }) {
|
|
|
64
76
|
id: option.value,
|
|
65
77
|
label: option.label,
|
|
66
78
|
...(option.tone === undefined ? {} : { tone: option.tone }),
|
|
79
|
+
...(option.avatar === undefined ? {} : { avatar: option.avatar }),
|
|
80
|
+
...opening(option.value),
|
|
67
81
|
})) }) }));
|
|
68
82
|
}
|
|
69
83
|
/* The filter reads the locale from React Aria's context, so the box that provides it has
|
|
70
84
|
to be ABOVE the component that asks for it. */
|
|
71
85
|
return (_jsx(AriaLocale, { children: _jsx(EditableTagsField, { ...props }) }));
|
|
72
86
|
}
|
|
73
|
-
function EditableTagsField({ value, onChange, options, onCreate, placeholder, disabled = false, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, }) {
|
|
87
|
+
function EditableTagsField({ value, onChange, options, onCreate, onBrowseAll, onOpen, openTarget, placeholder, disabled = false, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, "aria-label": ariaLabel, }) {
|
|
74
88
|
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
75
89
|
id: ownId,
|
|
76
90
|
describedBy: ownDescribedBy,
|
|
77
91
|
invalid: ownInvalid,
|
|
78
92
|
});
|
|
79
93
|
const copy = useUiCopy();
|
|
94
|
+
const opening = useChipOpening(onOpen, openTarget);
|
|
80
95
|
const { contains } = useFilter({ sensitivity: "base" });
|
|
81
96
|
const { locale } = useLocale();
|
|
82
97
|
const collator = useMemo(() => new Intl.Collator(locale, { sensitivity: "base", usage: "search" }), [locale]);
|
|
@@ -89,17 +104,25 @@ function EditableTagsField({ value, onChange, options, onCreate, placeholder, di
|
|
|
89
104
|
const latest = useRef(value);
|
|
90
105
|
latest.current = value;
|
|
91
106
|
const { available, create } = tagChoices(options, value, text, { contains, same: (a, b) => collator.compare(a.trim(), b) === 0 }, onCreate !== undefined && !disabled);
|
|
92
|
-
const listed =
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
{ value: CREATE, label: fillCopy(copy.inlineEdit.createTag, { name: create }) },
|
|
97
|
-
|
|
107
|
+
const listed = [
|
|
108
|
+
...available,
|
|
109
|
+
...(create === null
|
|
110
|
+
? []
|
|
111
|
+
: [{ value: CREATE, label: fillCopy(copy.inlineEdit.createTag, { name: create }) }]),
|
|
112
|
+
...(onBrowseAll === undefined || disabled
|
|
113
|
+
? []
|
|
114
|
+
: [{ value: BROWSE, label: copy.picker.browseAll }]),
|
|
115
|
+
];
|
|
98
116
|
const commit = (key) => {
|
|
99
117
|
if (key === null)
|
|
100
118
|
return;
|
|
101
119
|
const chosen = String(key);
|
|
120
|
+
const typed = text;
|
|
102
121
|
setText("");
|
|
122
|
+
if (chosen === BROWSE) {
|
|
123
|
+
onBrowseAll?.(typed);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
103
126
|
if (chosen !== CREATE) {
|
|
104
127
|
onChange?.(withTag(latest.current, chosen));
|
|
105
128
|
return;
|
|
@@ -120,6 +143,8 @@ function EditableTagsField({ value, onChange, options, onCreate, placeholder, di
|
|
|
120
143
|
id: option.value,
|
|
121
144
|
label: option.label,
|
|
122
145
|
...(option.tone === undefined ? {} : { tone: option.tone }),
|
|
146
|
+
...(option.avatar === undefined ? {} : { avatar: option.avatar }),
|
|
147
|
+
...opening(option.value),
|
|
123
148
|
...(disabled ? {} : { onRemove: () => remove(option.value) }),
|
|
124
149
|
}));
|
|
125
150
|
return (
|
|
@@ -132,7 +157,11 @@ function EditableTagsField({ value, onChange, options, onCreate, placeholder, di
|
|
|
132
157
|
/* The box grows with the chips: five categories wrap to a second line instead of
|
|
133
158
|
scrolling out of sight. */
|
|
134
159
|
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
|
|
160
|
+
}), children: [chips.length === 0 ? null : (_jsx(Tags, { items: chips, ...(labelledBy === undefined
|
|
161
|
+
? ariaLabel === undefined
|
|
162
|
+
? {}
|
|
163
|
+
: { label: ariaLabel }
|
|
164
|
+
: { 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
165
|
/* Arriving at the field shows what can still be added, the way a many2one opens its
|
|
137
166
|
list on focus. With every tag chosen and no text there is nothing to show, and the
|
|
138
167
|
list stays closed. */
|
|
@@ -145,7 +174,7 @@ function EditableTagsField({ value, onChange, options, onCreate, placeholder, di
|
|
|
145
174
|
onChange?.(withoutTag(value, last));
|
|
146
175
|
}, empty: text === "" }), _jsx(Popover, { triggerRef: boxRef, ...(width === null
|
|
147
176
|
? {}
|
|
148
|
-
: { style: { "--trigger-width": `${width}px` } }), placement: "bottom start", className: "w-(--trigger-width)", children: _jsx(ListBox, { items: listed, 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 ? "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 })] })) : (_jsx("span", { className: "truncate", children: item.label })) })) }) })] })] }));
|
|
177
|
+
: { 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 }) })] })) })) }) })] })] }));
|
|
149
178
|
}
|
|
150
179
|
/** The input after the chips. It reads the combobox's state to know whether the keyboard
|
|
151
180
|
* marked an option — Enter with nothing marked takes the first match. */
|
|
@@ -11,6 +11,8 @@ export interface TagOption {
|
|
|
11
11
|
label: string;
|
|
12
12
|
/** Neutral by default: «mayorista» classifies, it does not warn. */
|
|
13
13
|
tone?: Tone;
|
|
14
|
+
/** A person: its face on the chip and in the list — `true` for initials, a URL for a photo. */
|
|
15
|
+
avatar?: true | string;
|
|
14
16
|
}
|
|
15
17
|
/** How the typed text is compared — the locale's, handed in by the component. */
|
|
16
18
|
export type TagMatch = {
|
|
@@ -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 {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { AriaLocale } from "../../foundations/aria.js";
|
|
4
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
5
|
+
import { byDensity, useDensity } from "../../foundations/density.js";
|
|
6
|
+
import { cn, focusRing, tv } from "../../foundations/variants.js";
|
|
7
|
+
import { Check, ChevronRight } from "lucide-react";
|
|
8
|
+
import { Button as AriaButton, Checkbox as AriaCheckbox, Tree as AriaTree, TreeItem as AriaTreeItem, TreeItemContent, } from "react-aria-components";
|
|
9
|
+
const row = tv({
|
|
10
|
+
extend: focusRing,
|
|
11
|
+
base: "group relative flex min-w-0 cursor-default items-center rounded-control text-meta text-ink -outline-offset-2 select-none forced-color-adjust-none",
|
|
12
|
+
variants: {
|
|
13
|
+
isSelected: {
|
|
14
|
+
true: "bg-primary/10 forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]",
|
|
15
|
+
},
|
|
16
|
+
isHovered: { true: "" },
|
|
17
|
+
},
|
|
18
|
+
compoundVariants: [{ isSelected: false, isHovered: true, class: "bg-ink/4" }],
|
|
19
|
+
});
|
|
20
|
+
/* The chevron column is exactly one indent step wide, so a child's chevron lands under its
|
|
21
|
+
parent's name and the levels read as columns, not as a ragged edge. */
|
|
22
|
+
const chevronButton = tv({
|
|
23
|
+
extend: focusRing,
|
|
24
|
+
base: "inline-flex size-(--spacing-indent) shrink-0 items-center justify-center rounded-mark text-muted transition-colors duration-(--duration-instant) hover:bg-ink/6 hover:text-ink pressed:bg-ink/10",
|
|
25
|
+
});
|
|
26
|
+
/** Every id of the hierarchy that can be chosen — what «select all» means here. */
|
|
27
|
+
function choosable(items) {
|
|
28
|
+
return items.flatMap((item) => [
|
|
29
|
+
...(item.disabled === true ? [] : [item.id]),
|
|
30
|
+
...choosable(item.children ?? []),
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A hierarchy of records — the chart of accounts, product categories, warehouse locations —
|
|
35
|
+
* with its groups that fold, a figure per row, and one or many rows to choose.
|
|
36
|
+
*
|
|
37
|
+
* Context: the keyboard is React Aria's `Tree`: arrows walk the visible rows, → opens a group
|
|
38
|
+
* and ← folds it (or goes up to the parent), Home and End jump, and typing a name moves to it.
|
|
39
|
+
* Escape does NOT clear the selection: the tree often lives in a dialog or a popover, and
|
|
40
|
+
* Escape there means «close» — clearing the choice on the way out would lose it without a word.
|
|
41
|
+
*/
|
|
42
|
+
export function Tree({ items, label, labelledBy, expanded, defaultExpanded, onExpandedChange, selectionMode = "none", selected, defaultSelected, onSelectedChange, onAction, empty, }) {
|
|
43
|
+
const copy = useUiCopy();
|
|
44
|
+
const density = useDensity();
|
|
45
|
+
const pad = byDensity(density, "py-1.5 pe-2", "py-0.5 pe-1.5");
|
|
46
|
+
const many = selectionMode === "multiple";
|
|
47
|
+
const onSelectionChange = (keys) => onSelectedChange?.(keys === "all" ? choosable(items) : [...keys].map(String));
|
|
48
|
+
const render = (node, level) => {
|
|
49
|
+
const children = node.children ?? [];
|
|
50
|
+
const group = children.length > 0;
|
|
51
|
+
const Icon = node.icon;
|
|
52
|
+
/* Each level is one indent step; the first one keeps a sliver of the row's own padding so
|
|
53
|
+
the chevron is not glued to the edge of its tint. */
|
|
54
|
+
const indent = {
|
|
55
|
+
paddingInlineStart: `calc(var(--spacing) * 1 + var(--spacing-indent) * ${level - 1})`,
|
|
56
|
+
};
|
|
57
|
+
return (_jsxs(AriaTreeItem, { id: node.id, textValue: node.label, isDisabled: node.disabled === true, className: ({ isSelected, isHovered, isFocusVisible }) => cn(row({ isSelected, isHovered, isFocusVisible }), pad), style: indent, children: [_jsx(TreeItemContent, { children: ({ isExpanded, isSelected }) => (_jsxs(_Fragment, { children: [group ? (_jsx(AriaButton, { slot: "chevron", className: ({ isFocusVisible }) => chevronButton({ isFocusVisible }), children: _jsx(ChevronRight, { className: `size-(--size-icon-sm) transition-transform duration-(--duration-quick) ease-(--ease-out) ${isExpanded ? "rotate-90" : ""}`, strokeWidth: 2, "aria-hidden": true }) })) : (_jsx("span", { "aria-hidden": true, className: "size-(--spacing-indent) shrink-0" })), _jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-1.5", children: [many ? _jsx(SelectionCheck, {}) : null, Icon === undefined ? null : (_jsx(Icon, { className: "size-(--size-icon-md) shrink-0 text-muted", strokeWidth: 2, "aria-hidden": true })), _jsx("span", { className: cn("min-w-0 flex-1 truncate", group ? "font-medium" : "", isSelected ? "font-medium" : "", node.disabled === true && selectionMode !== "none" ? "text-muted" : ""), children: node.label }), node.meta === undefined ? null : (_jsx("span", { className: "shrink-0 ps-3 text-right tabular-nums whitespace-nowrap text-muted", children: node.meta })), selectionMode === "single" && isSelected ? (_jsx(Check, { className: "size-(--size-icon-sm) shrink-0 text-primary forced-colors:text-[HighlightText]", strokeWidth: 2.5, "aria-hidden": true })) : null] })] })) }), children.map((child) => render(child, level + 1))] }, node.id));
|
|
58
|
+
};
|
|
59
|
+
return (_jsx(AriaLocale, { children: _jsx(AriaTree, { ...(labelledBy === undefined
|
|
60
|
+
? { "aria-label": label }
|
|
61
|
+
: { "aria-labelledby": labelledBy }), selectionMode: selectionMode,
|
|
62
|
+
/* One to choose: a click chooses and a double click opens. Many: the click opens and
|
|
63
|
+
the check chooses. See the header. */
|
|
64
|
+
selectionBehavior: selectionMode === "single" ? "replace" : "toggle", disabledBehavior: "selection", escapeKeyBehavior: "none", ...(selected === undefined ? {} : { selectedKeys: selected }), ...(defaultSelected === undefined ? {} : { defaultSelectedKeys: defaultSelected }), ...(expanded === undefined ? {} : { expandedKeys: expanded }), ...(defaultExpanded === undefined ? {} : { defaultExpandedKeys: defaultExpanded }), ...(onExpandedChange === undefined
|
|
65
|
+
? {}
|
|
66
|
+
: { onExpandedChange: (keys) => onExpandedChange([...keys].map(String)) }), ...(onSelectedChange === undefined ? {} : { onSelectionChange }), ...(onAction === undefined ? {} : { onAction: (key) => onAction(String(key)) }), renderEmptyState: () => (_jsx("p", { className: "px-2 py-3 text-meta text-muted", children: empty ?? copy.tree.empty })), className: "flex w-full min-w-0 flex-col gap-px outline-0", children: items.map((item) => render(item, 1)) }) }));
|
|
67
|
+
}
|
|
68
|
+
/* The check of a row when many can be chosen. It is React Aria's `selection` slot: it toggles
|
|
69
|
+
the row and never opens it. Drawn like `Checkbox`'s box so a check reads as one thing. */
|
|
70
|
+
function SelectionCheck() {
|
|
71
|
+
return (_jsx(AriaCheckbox, { slot: "selection", className: "group/check inline-flex shrink-0 outline-0", children: ({ isSelected, isFocusVisible }) => (_jsx("span", { "aria-hidden": true, className: cn("inline-flex size-4 items-center justify-center rounded-mark border transition-colors duration-(--duration-instant) forced-color-adjust-none", isSelected
|
|
72
|
+
? "border-primary bg-primary text-surface forced-colors:border-[Highlight] forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]"
|
|
73
|
+
: "border-line-strong bg-surface text-transparent forced-colors:border-[ButtonBorder]", focusRing({ isFocusVisible })), children: _jsx(Check, { className: "size-3", strokeWidth: 2.5 }) })) }));
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Tree, type TreeItem, type TreeProps } from "./Tree.js";
|