@sincpro/web-ui 0.2.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 +3 -3
- package/dist/components/activity/ActivityBadge.d.ts +16 -0
- package/dist/components/activity/ActivityBadge.js +29 -0
- package/dist/components/activity/index.d.ts +1 -0
- package/dist/components/activity/index.js +1 -0
- 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.js +5 -1
- 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 +3 -0
- package/dist/components/index.js +3 -0
- package/dist/components/lines/LineItems.d.ts +5 -2
- package/dist/components/lines/LineItems.js +3 -3
- package/dist/components/listing/BoardColumns.d.ts +39 -0
- package/dist/components/listing/BoardColumns.js +138 -0
- package/dist/components/listing/ListingBar.d.ts +5 -0
- package/dist/components/listing/ListingBar.js +5 -2
- 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 +21 -2
- package/dist/components/listing/Row.js +29 -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 +7 -1
- package/dist/components/record/RecordHeader.js +30 -18
- package/dist/components/record/RecordRibbon.d.ts +13 -0
- package/dist/components/record/RecordRibbon.js +22 -0
- 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 +106 -14
- package/dist/foundations/copy.es.js +83 -9
- package/dist/foundations/copy.js +83 -9
- 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.js +8 -4
- 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/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/dialog/Dialog.d.ts +9 -3
- package/dist/primitives/dialog/Dialog.js +41 -5
- package/dist/primitives/dialog/index.d.ts +0 -1
- package/dist/primitives/dialog/index.js +0 -2
- package/dist/primitives/notice/index.d.ts +1 -1
- package/dist/primitives/notice/index.js +1 -1
- package/dist/primitives/overlay/Preview.js +13 -4
- package/dist/primitives/picker/MatchedText.d.ts +10 -0
- package/dist/primitives/picker/MatchedText.js +12 -0
- package/dist/primitives/picker/Picker.d.ts +29 -7
- package/dist/primitives/picker/Picker.js +88 -23
- package/dist/primitives/table/EditableTable.d.ts +6 -1
- package/dist/primitives/table/EditableTable.js +70 -9
- package/dist/primitives/table/ReportTable.js +2 -1
- package/dist/primitives/table/RowActions.js +7 -2
- package/dist/primitives/table/Table.d.ts +5 -0
- package/dist/primitives/table/Table.js +17 -8
- package/dist/primitives/tag/Tags.d.ts +2 -0
- package/dist/primitives/tag/Tags.js +2 -1
- package/dist/primitives/tag/TagsField.d.ts +10 -0
- package/dist/primitives/tag/TagsField.js +34 -9
- package/dist/primitives/tag/choices.d.ts +2 -0
- package/dist/primitives/value/Avatar.d.ts +16 -0
- package/dist/primitives/value/Avatar.js +91 -0
- package/dist/primitives/value/Person.d.ts +6 -51
- package/dist/primitives/value/Person.js +9 -148
- package/dist/primitives/value/Relation.d.ts +21 -9
- package/dist/primitives/value/Relation.js +35 -24
- package/dist/primitives/value/contact.d.ts +30 -0
- package/dist/primitives/value/contact.js +51 -0
- package/dist/primitives/value/index.d.ts +1 -1
- package/dist/primitives/value/index.js +1 -1
- package/dist/tokens.css +136 -1
- package/dist/views/list/List.d.ts +25 -2
- package/dist/views/list/List.js +48 -8
- package/dist/views/list/ListView.d.ts +1 -1
- package/dist/views/list/ListView.js +39 -8
- package/dist/views/list/columns.js +8 -1
- package/dist/views/list/presentations.d.ts +5 -2
- package/dist/views/list/presentations.js +32 -2
- package/dist/views/list/types.d.ts +44 -0
- package/dist/views/list/useNarrowList.d.ts +10 -0
- package/dist/views/list/useNarrowList.js +20 -6
- package/dist/views/record/Record.d.ts +14 -0
- package/dist/views/record/Record.js +22 -7
- package/package.json +11 -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/primitives/dialog/Drill.d.ts +0 -90
- package/dist/primitives/dialog/Drill.js +0 -172
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { AriaLocale } from "../../foundations/aria.js";
|
|
4
4
|
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
5
|
+
import { matchedWord, matches } from "../../foundations/match.js";
|
|
5
6
|
import { cn, tv } from "../../foundations/variants.js";
|
|
6
7
|
import { IconButton } from "../button/Button.js";
|
|
7
8
|
import { FieldButton, FieldGroup } from "../field/aria.js";
|
|
8
9
|
import { useFieldIds } from "../field/context.js";
|
|
9
10
|
import { ReadValue } from "../field/Field.js";
|
|
10
11
|
import { useFormMode } from "../field/mode.js";
|
|
12
|
+
import { Menu } from "../menu/Menu.js";
|
|
11
13
|
import { Popover } from "../overlay/Popover.js";
|
|
12
|
-
import {
|
|
14
|
+
import { MatchedText } from "./MatchedText.js";
|
|
15
|
+
import { Avatar } from "../value/Person.js";
|
|
16
|
+
import { AppWindow, Check, ChevronDown, Ellipsis, Eye, PanelRight, PictureInPicture2, SquareArrowOutUpRight, } from "lucide-react";
|
|
13
17
|
import { useContext, useMemo, useRef, useState } from "react";
|
|
14
18
|
import { Collection, ComboBox as AriaComboBox, ComboBoxStateContext, Header, Input, ListBox, ListBoxItem, ListBoxLoadMoreItem, ListBoxSection, Text, useFilter, } from "react-aria-components";
|
|
15
|
-
/** The
|
|
19
|
+
/** The icon of each place a record opens — the same in every field and list. */
|
|
20
|
+
const OPEN_ICON = {
|
|
21
|
+
modal: PictureInPicture2,
|
|
22
|
+
peek: Eye,
|
|
23
|
+
page: SquareArrowOutUpRight,
|
|
24
|
+
side: PanelRight,
|
|
25
|
+
window: AppWindow,
|
|
26
|
+
};
|
|
27
|
+
/** The id of the «Buscar más…» row. Not a record id: it cannot collide with one. */
|
|
16
28
|
const BROWSE_ALL = "\u0000browse-all";
|
|
29
|
+
/** The rows that create from what was typed — quick, or with the full form. */
|
|
30
|
+
const CREATE = "\u0000create";
|
|
31
|
+
const CREATE_EDIT = "\u0000create-edit";
|
|
32
|
+
/** The rows that act instead of choosing: after the records, in this order. */
|
|
33
|
+
const ACTIONS = new Set([CREATE, CREATE_EDIT, BROWSE_ALL]);
|
|
17
34
|
const CAP = 8;
|
|
18
35
|
/**
|
|
19
36
|
* What the list shows and how many are left out of it. Capped at 8 with the footer count,
|
|
@@ -29,7 +46,7 @@ export function shownRows(filtered, { loading, incremental }) {
|
|
|
29
46
|
return { visible, remaining: filtered.length - visible.length };
|
|
30
47
|
}
|
|
31
48
|
/** Folds consecutive items of the same `section` into one run under a header. The ids of
|
|
32
|
-
* the runs cannot collide with a record id, like the «
|
|
49
|
+
* the runs cannot collide with a record id, like the «Buscar más…» one. */
|
|
33
50
|
function sectioned(items) {
|
|
34
51
|
const out = [];
|
|
35
52
|
for (const item of items) {
|
|
@@ -80,7 +97,7 @@ export function Picker({ mode, ...props }) {
|
|
|
80
97
|
to be ABOVE the component that asks for it. */
|
|
81
98
|
return (_jsx(AriaLocale, { children: _jsx(EditablePicker, { ...props }) }));
|
|
82
99
|
}
|
|
83
|
-
function EditablePicker({ id: ownId, items, value, onChange, placeholder, onBrowseAll, onOpen, emptyText, searchPlaceholder, loading = false, loadingText, onLoadMore, loadingMore = false, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, "aria-label": ariaLabel, }) {
|
|
100
|
+
function EditablePicker({ id: ownId, items, value, onChange, placeholder, onBrowseAll, onCreate, onCreateAndEdit, onOpen, openTargets = ["modal", "peek", "side", "page"], emptyText, searchPlaceholder, loading = false, loadingText, onLoadMore, loadingMore = false, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, "aria-label": ariaLabel, }) {
|
|
84
101
|
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
85
102
|
id: ownId,
|
|
86
103
|
describedBy: ownDescribedBy,
|
|
@@ -121,22 +138,46 @@ function EditablePicker({ id: ownId, items, value, onChange, placeholder, onBrow
|
|
|
121
138
|
/* «See all…» is an OPTION at the end of the list and not a button in a footer: the
|
|
122
139
|
arrows reach it like any other row. As a footer button it was out of the keyboard's
|
|
123
140
|
path — Tab leaves the field and closes the list before getting there. */
|
|
124
|
-
const browsable =
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
141
|
+
const browsable = onBrowseAll !== undefined;
|
|
142
|
+
const typed = query.trim();
|
|
143
|
+
/* Exactly the name of an option already: creating it again would make a twin. */
|
|
144
|
+
const named = typed !== "" &&
|
|
145
|
+
items.some((i) => i.name.localeCompare(typed, copy.locale, { sensitivity: "base" }) === 0);
|
|
146
|
+
const actions = loading
|
|
147
|
+
? []
|
|
148
|
+
: [
|
|
149
|
+
...(onCreate !== undefined && typed !== "" && !named
|
|
150
|
+
? [{ id: CREATE, name: fillCopy(copy.picker.create, { name: typed }) }]
|
|
151
|
+
: []),
|
|
152
|
+
...(onCreateAndEdit === undefined
|
|
153
|
+
? []
|
|
154
|
+
: [{ id: CREATE_EDIT, name: copy.picker.createAndEdit }]),
|
|
155
|
+
...(browsable ? [{ id: BROWSE_ALL, name: copy.picker.browseAll }] : []),
|
|
156
|
+
];
|
|
157
|
+
const listed = [...visible, ...actions];
|
|
128
158
|
const closeList = useRef(null);
|
|
129
159
|
const take = (key) => {
|
|
130
160
|
if (key === null)
|
|
131
161
|
return;
|
|
132
162
|
const next = String(key);
|
|
133
|
-
if (next
|
|
134
|
-
/* The full list opens somewhere else (a dialog, a panel):
|
|
135
|
-
field keeps the chosen name.
|
|
163
|
+
if (ACTIONS.has(next)) {
|
|
164
|
+
/* The full list, or the new record's form, opens somewhere else (a dialog, a panel):
|
|
165
|
+
this one closes and the field keeps the chosen name. A quick create chooses the new
|
|
166
|
+
record once it exists. */
|
|
167
|
+
const asked = query;
|
|
136
168
|
closeList.current?.();
|
|
137
169
|
setText(chosenName);
|
|
138
170
|
setQuery("");
|
|
139
|
-
|
|
171
|
+
if (next === BROWSE_ALL)
|
|
172
|
+
onBrowseAll?.(asked);
|
|
173
|
+
if (next === CREATE_EDIT)
|
|
174
|
+
onCreateAndEdit?.(asked.trim());
|
|
175
|
+
if (next === CREATE && onCreate !== undefined) {
|
|
176
|
+
/* The field keeps the new name from now on, not the old one until the record exists:
|
|
177
|
+
a controlled text that changes while the input has focus reopens the list. */
|
|
178
|
+
setText(asked.trim());
|
|
179
|
+
void Promise.resolve(onCreate(asked.trim())).then((created) => onChange?.(created));
|
|
180
|
+
}
|
|
140
181
|
return;
|
|
141
182
|
}
|
|
142
183
|
setText(items.find((i) => i.id === next)?.name ?? "");
|
|
@@ -160,43 +201,67 @@ function EditablePicker({ id: ownId, items, value, onChange, placeholder, onBrow
|
|
|
160
201
|
the options are the answer before the first letter. */
|
|
161
202
|
menuTrigger: "focus",
|
|
162
203
|
/* An empty list stays open to SAY it is empty (or loading) instead of vanishing. */
|
|
163
|
-
allowsEmptyCollection: true, isInvalid: invalid, isDisabled: disabled, className: "w-full min-w-0", children: [_jsx(CloseBridge, { into: closeList }), _jsxs(FieldGroup, { children: [_jsx(Input, { placeholder: loading ? shownLoading : open ? shownSearch : shownPlaceholder, title: shownSearch,
|
|
204
|
+
allowsEmptyCollection: true, isInvalid: invalid, isDisabled: disabled, className: "w-full min-w-0", children: [_jsx(CloseBridge, { into: closeList }), _jsxs(FieldGroup, { children: [chosen?.avatar === undefined || open ? null : (_jsx("span", { className: "flex shrink-0 items-center pl-2", children: _jsx(Avatar, { name: chosen.name, size: "sm", ...(typeof chosen.avatar === "string" ? { src: chosen.avatar } : {}) }) })), _jsx(Input, { placeholder: loading ? shownLoading : open ? shownSearch : shownPlaceholder, title: shownSearch,
|
|
164
205
|
/* Arriving selects the name, so the first letter typed replaces it. Only when
|
|
165
206
|
ARRIVING: React Aria hands the focus back to the input while the list is open,
|
|
166
207
|
and selecting again then would swallow every second letter. */
|
|
167
208
|
onFocus: (event) => {
|
|
168
209
|
if (!open)
|
|
169
210
|
event.currentTarget.select();
|
|
170
|
-
}, className: "h-full min-w-0 flex-1 bg-transparent px-2.5 text-meta text-ink outline-0 placeholder:text-muted disabled:cursor-not-allowed" }), value !== undefined &&
|
|
211
|
+
}, className: "h-full min-w-0 flex-1 bg-transparent px-2.5 text-meta text-ink outline-0 placeholder:text-muted disabled:cursor-not-allowed" }), value !== undefined &&
|
|
212
|
+
value !== "" &&
|
|
213
|
+
onOpen !== undefined &&
|
|
214
|
+
openTargets[0] !== undefined ? (_jsxs(_Fragment, { children: [_jsx(IconButton, { icon: OPEN_ICON[openTargets[0]], label: copy.picker.openIn[openTargets[0]], size: "sm", disabled: disabled, onClick: () => onOpen(value, openTargets[0]) }), openTargets.length < 2 || disabled ? null : (_jsx(Menu, { label: copy.picker.openMore, icon: Ellipsis, iconOnly: true, chevron: false, variant: "text", align: "end", groups: [
|
|
215
|
+
{
|
|
216
|
+
options: openTargets.slice(1).map((to) => ({
|
|
217
|
+
id: to,
|
|
218
|
+
label: copy.picker.openIn[to],
|
|
219
|
+
icon: OPEN_ICON[to],
|
|
220
|
+
})),
|
|
221
|
+
},
|
|
222
|
+
], onSelect: (to) => onOpen(value, to) }))] })) : null, _jsx(FieldButton, { className: "mr-1", children: _jsx(ChevronDown, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsxs(Popover, { placement: "bottom start", className: "flex w-(--trigger-width) flex-col overflow-hidden", children: [_jsx(PickerOptions, { items: listed, marks: hasMarks, hints: hasHints, remaining: remaining, query: query, emptyText: loading ? shownLoading : shownEmpty,
|
|
171
223
|
/* Not while the first page loads: the sentinel of an empty list is in view and
|
|
172
224
|
would ask for page two before page one arrived. */
|
|
173
225
|
onLoadMore: loading ? undefined : onLoadMore, loadingMore: loadingMore }), remaining > 0 && !browsable ? (_jsxs("div", { className: "flex shrink-0 items-center gap-2 border-t border-line bg-ground px-2.5 py-1.5", children: [_jsx("span", { className: "text-micro tabular-nums text-muted", children: fillCopy(copy.picker.more, { count: remaining.toLocaleString(copy.locale) }) }), _jsx("span", { className: "text-micro text-muted", children: copy.picker.refine })] })) : null] })] }));
|
|
174
226
|
}
|
|
175
227
|
/**
|
|
176
|
-
* The list of a `Picker`: its rows, the section headers, the «
|
|
228
|
+
* The list of a `Picker`: its rows, the section headers, the «Buscar más…» row and the loading
|
|
177
229
|
* row at the end. Internal — not exported from the family — and apart from the combobox so it
|
|
178
230
|
* can be rendered on the server without a popover, which is how the tests see the rows.
|
|
179
231
|
*/
|
|
180
|
-
export function PickerOptions({ items, marks, hints, remaining, emptyText, onLoadMore, loadingMore = false, "aria-label": ariaLabel, }) {
|
|
232
|
+
export function PickerOptions({ items, marks, hints, remaining, query = "", emptyText, onLoadMore, loadingMore = false, "aria-label": ariaLabel, }) {
|
|
181
233
|
const copy = useUiCopy();
|
|
182
234
|
const entries = useMemo(() => sectioned(items), [items]);
|
|
183
|
-
|
|
235
|
+
/* The actions are one block under the records: one hairline above the first of them. */
|
|
236
|
+
const firstAction = items.find((i) => ACTIONS.has(i.id))?.id;
|
|
237
|
+
const renderItem = (item) => ACTIONS.has(item.id) ? (_jsx(ListBoxItem, { id: item.id, textValue: item.name, className: (state) => cn(option({ ...state, hints: false }), "text-primary", item.id === firstAction && items[0]?.id !== item.id
|
|
238
|
+
? "mt-1 border-t border-line"
|
|
239
|
+
: ""), children: _jsxs("span", { className: "flex w-full items-center justify-between gap-2", children: [_jsx("span", { className: "min-w-0 truncate font-medium", children: item.name }), item.id !== BROWSE_ALL || remaining === 0 ? null : (_jsx("span", { className: "text-micro tabular-nums text-muted", children: fillCopy(copy.picker.more, {
|
|
184
240
|
count: remaining.toLocaleString(copy.locale),
|
|
185
|
-
}) })] }) })) : (_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 })) }));
|
|
241
|
+
}) }))] }) })) : (_jsx(ListBoxItem, { id: item.id, textValue: item.name, isDisabled: item.disabled, className: (state) => option({ ...state, hints }), children: ({ isSelected }) => (_jsx(Row, { item: item, selected: isSelected, marks: marks, hints: hints, query: query, locale: copy.locale })) }));
|
|
186
242
|
return (_jsxs(ListBox, { "aria-label": ariaLabel,
|
|
187
243
|
/* Nothing matched YET while the next page is coming: «nothing matches» above «loading
|
|
188
244
|
more…» says two contradicting things, so the empty list says only the second. */
|
|
189
245
|
renderEmptyState: () => (_jsx("p", { className: "px-3 py-3 text-center text-meta text-muted", children: loadingMore ? copy.picker.loadingMore : emptyText })),
|
|
190
246
|
/* Incremental, the list is not capped: it scrolls inside its own height and the
|
|
191
247
|
sentinel at its end is what asks for more. */
|
|
192
|
-
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, 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, 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 }))] }));
|
|
248
|
+
className: cn("min-h-0 flex-1 overflow-y-auto p-1 outline-0", onLoadMore === undefined ? "" : "max-h-(--size-dropdown)"), children: [_jsx(Collection, { items: entries, dependencies: [query], children: (entry) => "items" in entry ? (_jsxs(ListBoxSection, { id: entry.id, className: "not-first:mt-1 not-first:border-t not-first:border-line not-first:pt-1", children: [_jsxs(Header, { className: "flex items-center gap-2 px-2 pt-1 pb-0.5 text-micro font-medium text-muted", children: [marks ? (_jsx("span", { className: "size-(--size-icon-sm) shrink-0", "aria-hidden": true })) : null, entry.section] }), _jsx(Collection, { items: entry.items, dependencies: [query], children: renderItem })] })) : (renderItem(entry)) }), onLoadMore === undefined ? null : (_jsx(ListBoxLoadMoreItem, { onLoadMore: onLoadMore, isLoading: loadingMore && items.length > 0, className: "px-3 py-1.5 text-center text-micro text-muted", children: copy.picker.loadingMore }))] }));
|
|
193
249
|
}
|
|
194
|
-
function Row({ item, selected, marks, hints, }) {
|
|
250
|
+
function Row({ item, selected, marks, hints, query, locale, }) {
|
|
195
251
|
const Icon = item.icon;
|
|
196
|
-
|
|
252
|
+
const copy = useUiCopy();
|
|
253
|
+
/* Listed for a code that is not drawn — the barcode, the NIT —: say which, or the row reads
|
|
254
|
+
as a wrong match. */
|
|
255
|
+
const hidden = query.trim() === "" ||
|
|
256
|
+
matches(item.name, query, locale) ||
|
|
257
|
+
(item.meta !== undefined && matches(item.meta, query, locale)) ||
|
|
258
|
+
item.haystack === undefined
|
|
259
|
+
? null
|
|
260
|
+
: matchedWord(item.haystack, query, locale);
|
|
261
|
+
return (_jsxs(_Fragment, { children: [marks ? (_jsx("span", { className: cn(hints ? "mt-0.5" : "", "flex size-(--size-icon-sm) shrink-0 items-center justify-center"), children: selected ? (_jsx(Check, { "aria-hidden": true, className: "size-(--size-icon-sm) text-primary forced-colors:text-[HighlightText]", strokeWidth: 2.5 })) : Icon !== undefined ? (_jsx(Icon, { "aria-hidden": true, className: "size-(--size-icon-sm) text-muted", strokeWidth: 1.75 })) : null })) : null, item.avatar === undefined ? null : (_jsx(Avatar, { name: item.name, size: "sm", ...(typeof item.avatar === "string" ? { src: item.avatar } : {}) })), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx(Text, { slot: "label", className: cn("min-w-0 truncate text-meta text-ink", selected ? "font-medium" : ""), children: _jsx(MatchedText, { text: item.name, query: query, locale: locale }) }), item.tags] }), item.meta === undefined ? null : (_jsx(Text, { slot: "description", className: "truncate text-micro tabular-nums text-muted", children: _jsx(MatchedText, { text: item.meta, query: query, locale: locale }) })), hidden === null ? null : (_jsxs("span", { className: "truncate text-micro text-muted", children: [copy.picker.matched, " ", _jsx("span", { className: "font-mono", children: _jsx(MatchedText, { text: hidden, query: query, locale: locale }) })] }))] }), item.aside === undefined ? null : (_jsx("span", { className: "flex shrink-0 items-center gap-1.5 self-center text-meta text-ink tabular-nums", children: item.aside })), !marks && selected ? (_jsx(Check, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0 text-primary forced-colors:text-[HighlightText]", strokeWidth: 2.5 })) : null] }));
|
|
197
262
|
}
|
|
198
263
|
/** Hands the combobox's `close` to the code outside it: React Aria owns the open state and
|
|
199
|
-
* offers no controlled `isOpen`, and the «
|
|
264
|
+
* offers no controlled `isOpen`, and the «Buscar más…» row has to close the list it lives in. */
|
|
200
265
|
function CloseBridge({ into }) {
|
|
201
266
|
const state = useContext(ComboBoxStateContext);
|
|
202
267
|
into.current = state === null ? null : () => state.close();
|
|
@@ -135,7 +135,7 @@ totals, empty, grouping,
|
|
|
135
135
|
/** El total de una columna DENTRO de un montón, ya formateado. Lo calcula quien
|
|
136
136
|
* compone, por el mismo motivo que `totals`: sumar significa cosas distintas
|
|
137
137
|
* según el dominio, y un porcentaje se promedia, no se suma. */
|
|
138
|
-
groupTotal, height, layout, onOpen, onDiscard, bands, adds, flag, }: {
|
|
138
|
+
groupTotal, height, layout, onOpen, onDiscard, bands, adds, flag, onReorder, }: {
|
|
139
139
|
rows: readonly T[];
|
|
140
140
|
columns: readonly EditColumn<T>[];
|
|
141
141
|
getRowId: (row: T) => string;
|
|
@@ -188,4 +188,9 @@ groupTotal, height, layout, onOpen, onDiscard, bands, adds, flag, }: {
|
|
|
188
188
|
detail?: ReactNode;
|
|
189
189
|
owner?: string;
|
|
190
190
|
}) | null;
|
|
191
|
+
/** The rows have an order of their own — the lines of an order, Odoo's `sequence` — and it
|
|
192
|
+
* is changed by dragging the grip at the start of a row, or with Alt + ↑ / ↓ on it. `before`
|
|
193
|
+
* is the row it lands in front of; `null`, the end. Off while grouped (the piles decide the
|
|
194
|
+
* order) and in the stacked layout. */
|
|
195
|
+
onReorder?: (id: string, before: string | null) => void;
|
|
191
196
|
}): import("react").JSX.Element;
|
|
@@ -8,7 +8,8 @@ import { stickyTop, useScrollBox } from "../../foundations/sticky.js";
|
|
|
8
8
|
import { IconButton } from "../button/Button.js";
|
|
9
9
|
import { NoteCard } from "../field/note.js";
|
|
10
10
|
import { Flag } from "../notice/Flag.js";
|
|
11
|
-
import {
|
|
11
|
+
import { HEADER_TEXT } from "./Table.js";
|
|
12
|
+
import { GripVertical, Plus, Trash2 } from "lucide-react";
|
|
12
13
|
import { Fragment, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
13
14
|
import { useKeyboard } from "react-aria";
|
|
14
15
|
/** The lines each section heads: the rows after it up to the next section. Notes do not
|
|
@@ -239,7 +240,7 @@ totals, empty, grouping,
|
|
|
239
240
|
/** El total de una columna DENTRO de un montón, ya formateado. Lo calcula quien
|
|
240
241
|
* compone, por el mismo motivo que `totals`: sumar significa cosas distintas
|
|
241
242
|
* según el dominio, y un porcentaje se promedia, no se suma. */
|
|
242
|
-
groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, }) {
|
|
243
|
+
groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReorder, }) {
|
|
243
244
|
const copy = useUiCopy();
|
|
244
245
|
const bandOf = (row) => bands?.kind(row) ?? null;
|
|
245
246
|
/* The row's mark, before its first value — and, in a record's margin, its card. */
|
|
@@ -338,6 +339,51 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, }) {
|
|
|
338
339
|
/* The rows in the order they are drawn: what the arrows walk. */
|
|
339
340
|
const order = drawn.flatMap((entry) => entry.row === undefined ? [] : [getRowId(entry.row)]);
|
|
340
341
|
const stop = active !== null && order.includes(active) ? active : (order[0] ?? null);
|
|
342
|
+
/* REORDER — a grip at the start of each row. Dragged, the row shows where it would land (a
|
|
343
|
+
line over the row it goes in front of); Alt + ↑ / ↓ on the grip does the same by keyboard. */
|
|
344
|
+
const reorderable = onReorder !== undefined && grouping === undefined && !stacked;
|
|
345
|
+
/* A ref and not state: `dragover` can arrive before React re-renders after `dragstart`. */
|
|
346
|
+
const dragged = useRef(null);
|
|
347
|
+
const [landing, setLanding] = useState(undefined);
|
|
348
|
+
const after = (id) => order[order.indexOf(id) + 1] ?? null;
|
|
349
|
+
const land = (id, before) => {
|
|
350
|
+
if (before === id || before === after(id))
|
|
351
|
+
return;
|
|
352
|
+
onReorder?.(id, before);
|
|
353
|
+
};
|
|
354
|
+
const overRow = (event) => {
|
|
355
|
+
if (dragged.current === null)
|
|
356
|
+
return;
|
|
357
|
+
const tr = event.target.closest("tr[data-row-id]");
|
|
358
|
+
if (tr === null)
|
|
359
|
+
return;
|
|
360
|
+
event.preventDefault();
|
|
361
|
+
const id = tr.dataset["rowId"] ?? "";
|
|
362
|
+
const box = tr.getBoundingClientRect();
|
|
363
|
+
setLanding(event.clientY < box.top + box.height / 2 ? id : after(id));
|
|
364
|
+
};
|
|
365
|
+
const lineOver = (id) => (landing === id ? "border-t-2 border-t-primary" : "");
|
|
366
|
+
const grip = (id, mark) => (_jsx("td", { className: `w-0 py-1 pr-0 pl-1 align-middle ${mark} ${lineOver(id)}`, children: _jsx("button", { type: "button", draggable: true, "aria-label": copy.editable.reorder, title: copy.editable.reorderHint, "aria-keyshortcuts": "Alt+ArrowUp Alt+ArrowDown", ...(editable && stop !== id ? { tabIndex: -1 } : {}), onClick: (event) => event.stopPropagation(), onKeyDown: (event) => {
|
|
367
|
+
if (!event.altKey || (event.key !== "ArrowUp" && event.key !== "ArrowDown"))
|
|
368
|
+
return;
|
|
369
|
+
event.preventDefault();
|
|
370
|
+
event.stopPropagation();
|
|
371
|
+
const at = order.indexOf(id);
|
|
372
|
+
if (event.key === "ArrowUp" && at > 0)
|
|
373
|
+
land(id, order[at - 1] ?? null);
|
|
374
|
+
if (event.key === "ArrowDown" && at < order.length - 1)
|
|
375
|
+
land(id, order[at + 2] ?? null);
|
|
376
|
+
}, onDragStart: (event) => {
|
|
377
|
+
event.dataTransfer.setData("text/plain", id);
|
|
378
|
+
event.dataTransfer.effectAllowed = "move";
|
|
379
|
+
const tr = event.currentTarget.closest("tr");
|
|
380
|
+
if (tr !== null)
|
|
381
|
+
event.dataTransfer.setDragImage(tr, 16, 16);
|
|
382
|
+
dragged.current = id;
|
|
383
|
+
}, onDragEnd: () => {
|
|
384
|
+
dragged.current = null;
|
|
385
|
+
setLanding(undefined);
|
|
386
|
+
}, className: "flex cursor-grab items-center rounded-control p-0.5 text-muted opacity-40 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 hover:text-ink focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-primary active:cursor-grabbing", children: _jsx(GripVertical, { "aria-hidden": true, className: "size-(--size-icon-sm)", strokeWidth: 2 }) }) }));
|
|
341
387
|
useEffect(() => {
|
|
342
388
|
const target = focusOnOpen.current;
|
|
343
389
|
if (target === null || target.id !== editing)
|
|
@@ -556,7 +602,20 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, }) {
|
|
|
556
602
|
return null;
|
|
557
603
|
return (_jsxs("div", { className: "flex items-baseline justify-between gap-3 text-meta", children: [_jsx("dt", { className: "text-muted", children: c.header }), _jsx("dd", { className: "font-semibold text-ink tabular-nums", children: total })] }, c.id));
|
|
558
604
|
}) }))] }));
|
|
559
|
-
return (_jsxs("div", { ref: frame, className: "min-w-0", children: [_jsx("div", { ref: box.ref, className: `min-w-0 ${box.className}`, style: box.style, children: stacked ? (list) : (_jsxs("table", { className: "w-full border-separate border-spacing-0 px-1 text-left", children: [_jsx("thead", { className: "sticky z-10 bg-ground", style: stickyTop, children: _jsxs("tr", { children: [columns.map((c) => (_jsx("th", { scope: "col", style: c.width === undefined ? undefined : { width: c.width }, className: `border-b border-line-strong
|
|
605
|
+
return (_jsxs("div", { ref: frame, className: "min-w-0", children: [_jsx("div", { ref: box.ref, className: `min-w-0 ${box.className}`, style: box.style, children: stacked ? (list) : (_jsxs("table", { className: "w-full border-separate border-spacing-0 px-1 text-left", children: [_jsx("thead", { className: "sticky z-10 bg-ground", style: stickyTop, children: _jsxs("tr", { children: [reorderable ? _jsx("th", { className: "w-0 border-b border-line-strong" }) : null, columns.map((c) => (_jsx("th", { scope: "col", style: c.width === undefined ? undefined : { width: c.width }, className: `border-b border-line-strong ${HEADER_TEXT} ${cell} ${c.numeric === true ? "text-right" : "text-left"}`, children: c.header }, c.id))), onRemove === undefined ? null : (_jsx("th", { className: "w-0 border-b border-line-strong" }))] }) }), _jsx("tbody", { ...(reorderable
|
|
606
|
+
? {
|
|
607
|
+
onDragOver: overRow,
|
|
608
|
+
onDrop: (event) => {
|
|
609
|
+
event.preventDefault();
|
|
610
|
+
if (dragged.current !== null && landing !== undefined)
|
|
611
|
+
land(dragged.current, landing);
|
|
612
|
+
dragged.current = null;
|
|
613
|
+
setLanding(undefined);
|
|
614
|
+
},
|
|
615
|
+
}
|
|
616
|
+
: {}), children: rows.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: columns.length +
|
|
617
|
+
(onRemove === undefined ? 0 : 1) +
|
|
618
|
+
(reorderable ? 1 : 0), className: `text-meta text-muted italic ${cell}`, children: emptyText }) })) : (drawn.map((entry) => {
|
|
560
619
|
if (entry.pile !== undefined) {
|
|
561
620
|
const pile = entry.pile;
|
|
562
621
|
const inside = pile.piles.length === 0 ? pile.items : pileItems(pile.piles);
|
|
@@ -565,7 +624,7 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, }) {
|
|
|
565
624
|
columna y cada total bajo la suya. Un total en un rincón
|
|
566
625
|
obliga a buscar de qué columna es; alineado no hay nada que
|
|
567
626
|
buscar. */
|
|
568
|
-
_jsxs("tr", { children: [columns.map((c, ci) => (_jsx("td", { className: `bg-ground text-meta ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, style: ci === 0 && pile.depth > 0
|
|
627
|
+
_jsxs("tr", { children: [reorderable ? _jsx("td", { className: "w-0 bg-ground" }) : null, columns.map((c, ci) => (_jsx("td", { className: `bg-ground text-meta ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, style: ci === 0 && pile.depth > 0
|
|
569
628
|
? {
|
|
570
629
|
paddingLeft: `calc(var(--spacing-indent) * ${pile.depth} + 0.625rem)`,
|
|
571
630
|
}
|
|
@@ -586,7 +645,7 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, }) {
|
|
|
586
645
|
: "group-hover/row:bg-ink/4";
|
|
587
646
|
const bandMark = `border-b border-line first:rounded-l-control last:rounded-r-control ${bandWash}`;
|
|
588
647
|
const control = isEditing && editable && bands?.edit !== undefined;
|
|
589
|
-
return (_jsxs(EditRow, { as: "tr", ...rowWiring(id, row), className: `group/row ${editable ? RING : ""} ${editable && !isEditing ? "cursor-text" : ""}`, children: [_jsx("td", { colSpan: span, ...(control ? { "data-edit": "" } : {}), className: `text-meta ${bandMark} ${cell} ${band === "section"
|
|
648
|
+
return (_jsxs(EditRow, { as: "tr", ...rowWiring(id, row), className: `group/row ${editable ? RING : ""} ${editable && !isEditing ? "cursor-text" : ""}`, children: [reorderable ? grip(id, bandMark) : null, _jsx("td", { colSpan: span, ...(control ? { "data-edit": "" } : {}), className: `text-meta ${bandMark} ${lineOver(id)} ${cell} ${band === "section"
|
|
590
649
|
? "font-semibold text-ink"
|
|
591
650
|
: "text-muted italic"}`, children: control ? bands?.edit?.(row, setter(id)) : bands?.render(row) }), columns.slice(span).map((c, i) => (_jsx("td", { className: `text-meta text-ink ${bandMark} ${cell} ${c.numeric === true
|
|
592
651
|
? "text-right font-semibold tabular-nums"
|
|
@@ -600,12 +659,14 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, }) {
|
|
|
600
659
|
return (_jsxs(EditRow, { as: "tr", ...rowWiring(id, row),
|
|
601
660
|
/* The focus ring is drawn on the row, inset, so it is not cut by
|
|
602
661
|
the box and does not move the cells. */
|
|
603
|
-
className: `group/row ${editable ? RING : ""} ${editable && !isEditing ? "cursor-text" : ""}`, children: [columns.map((c, ci) => {
|
|
662
|
+
className: `group/row ${editable ? RING : ""} ${editable && !isEditing ? "cursor-text" : ""}`, children: [reorderable ? grip(id, mark) : null, columns.map((c, ci) => {
|
|
604
663
|
const control = isEditing && c.edit !== undefined && editable;
|
|
605
|
-
|
|
606
|
-
|
|
664
|
+
/* Not `mark`: that name is the row's border and wash, above. Shadowed,
|
|
665
|
+
the cells were drawn with the class «null» — no hairline, no hover. */
|
|
666
|
+
const flagged = ci === 0 ? flagOf(row, true) : null;
|
|
667
|
+
return (_jsx("td", { ...(control ? { "data-edit": "" } : {}), className: `text-meta text-ink ${mark} ${lineOver(id)} ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: flagged === null ? (control ? (c.edit?.(row, set)) : (c.render(row))) : (_jsxs("span", { className: "flex min-w-0 items-start gap-1.5", children: [flagged, _jsx("span", { className: "min-w-0 flex-1", children: control ? c.edit?.(row, set) : c.render(row) })] })) }, c.id));
|
|
607
668
|
}), onRemove === undefined ? null : (_jsx("td", { className: `w-0 py-1 pr-2 pl-1 align-middle ${mark}`, children: _jsx("span", { className: "flex opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 group-focus-visible/row:opacity-100 focus-within:opacity-100", onClick: (e) => e.stopPropagation(), children: _jsx(IconButton, { icon: Trash2, label: copy.editable.remove, size: "sm", tone: "danger", ...(editable && stop !== id ? { tabIndex: -1 } : {}), onClick: () => onRemove(id) }) }) }))] }, id));
|
|
608
|
-
})) }), totals === undefined ? null : (_jsx("tfoot", { children: _jsxs("tr", { children: [columns.map((c, i) => (_jsx("td", { className: `border-t border-line-strong text-meta font-semibold text-ink ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: totals[i] ?? null }, c.id))), onRemove === undefined ? null : (_jsx("td", { className: "border-t border-line-strong" }))] }) }))] })) }), onAdd === undefined && (adds === undefined || adds.length === 0) ? null : (
|
|
669
|
+
})) }), totals === undefined ? null : (_jsx("tfoot", { children: _jsxs("tr", { children: [reorderable ? _jsx("td", { className: "border-t border-line-strong" }) : null, columns.map((c, i) => (_jsx("td", { className: `border-t border-line-strong text-meta font-semibold text-ink ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: totals[i] ?? null }, c.id))), onRemove === undefined ? null : (_jsx("td", { className: "border-t border-line-strong" }))] }) }))] })) }), onAdd === undefined && (adds === undefined || adds.length === 0) ? null : (
|
|
609
670
|
/* The «add» goes under the grid, as text, not as a button away from it:
|
|
610
671
|
what is about to happen is that one more row shows up right here.
|
|
611
672
|
The hover is the same inset mark as a row: the shell lines the label
|
|
@@ -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,
|
|
@@ -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
|
*
|
|
@@ -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,6 +15,15 @@ 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;
|