@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Grouping } from "../../foundations/grouping.js";
|
|
2
|
+
import { type FlagProps } from "../notice/Flag.js";
|
|
2
3
|
import { type ReactNode } from "react";
|
|
3
4
|
/** Where a column goes when the row is drawn as a stacked card (a narrow box).
|
|
4
5
|
* `trail` sits at the top right, beside the title — a state, the row's buttons. */
|
|
@@ -28,7 +29,32 @@ export interface EditColumn<T> {
|
|
|
28
29
|
* discount. `null` or `""` leaves it out of that row's line — a zero discount is
|
|
29
30
|
* not news. Without it the line reads «<header> <cell>». */
|
|
30
31
|
brief?: (row: T) => ReactNode;
|
|
32
|
+
/** Drawn on section and note rows too, instead of being covered by their text — the
|
|
33
|
+
* row's buttons. `render` receives the band's row. */
|
|
34
|
+
onBands?: boolean;
|
|
31
35
|
}
|
|
36
|
+
/** What a row that is not a line is. See «sections and notes» in the file's header. */
|
|
37
|
+
export type EditBands<T> = {
|
|
38
|
+
/** `section` heads the lines under it; `note` is a sentence between lines; `null` is a
|
|
39
|
+
* line. */
|
|
40
|
+
kind: (row: T) => "section" | "note" | null;
|
|
41
|
+
/** The band's text as it reads. */
|
|
42
|
+
render: (row: T) => ReactNode;
|
|
43
|
+
/** Its control while the row is edited — a text field, named. */
|
|
44
|
+
edit?: (row: T, set: (next: T) => void) => ReactNode;
|
|
45
|
+
/** What a section's lines add up to in `column`, already formatted; `null` where the
|
|
46
|
+
* column does not add up. The lines are the ones under it until the next section. */
|
|
47
|
+
total?: (column: EditColumn<T>, lines: readonly T[]) => ReactNode | null;
|
|
48
|
+
};
|
|
49
|
+
/** One more way to add, beside «add a line»: a section, a note. */
|
|
50
|
+
export type EditAdd = {
|
|
51
|
+
label: string;
|
|
52
|
+
/** As `onAdd`: return the new row's id and it opens. */
|
|
53
|
+
onAdd: () => string | void | Promise<string | void>;
|
|
54
|
+
};
|
|
55
|
+
/** The lines each section heads: the rows after it up to the next section. Notes do not
|
|
56
|
+
* cut a section, and are not lines. */
|
|
57
|
+
export declare function sectionLines<T>(rows: readonly T[], getRowId: (row: T) => string, kind: (row: T) => "section" | "note" | null): Map<string, T[]>;
|
|
32
58
|
/**
|
|
33
59
|
* Where each column goes in the stacked card, defaults resolved: a declared `stack`
|
|
34
60
|
* wins; otherwise the first undeclared column is the title (unless one was declared),
|
|
@@ -39,6 +65,56 @@ export declare function stackPlaces(columns: readonly {
|
|
|
39
65
|
stack?: StackPlace;
|
|
40
66
|
numeric?: boolean;
|
|
41
67
|
}[]): StackPlace[];
|
|
68
|
+
/** The first control of a row being edited: the first one in an editable cell, else
|
|
69
|
+
* anything focusable in the row (a column without `edit` may still hold a button). */
|
|
70
|
+
export declare function firstControl(row: HTMLElement): HTMLElement | null;
|
|
71
|
+
/** Where `Tab` goes from the edge of an edited row: the next or previous row opened, a new
|
|
72
|
+
* row added, or out of the table the browser's way. */
|
|
73
|
+
export type TabTarget = {
|
|
74
|
+
kind: "row";
|
|
75
|
+
id: string;
|
|
76
|
+
} | {
|
|
77
|
+
kind: "add";
|
|
78
|
+
} | {
|
|
79
|
+
kind: "leave";
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Where `Tab` past the last control (`next`) or `Shift+Tab` before the first (`prev`) of
|
|
83
|
+
* the edited row `from` goes, over the rows in the order they are drawn.
|
|
84
|
+
*
|
|
85
|
+
* Past the last row it adds one when the table can — unless `from` is itself a row just
|
|
86
|
+
* added and still blank: one more `Tab` there means «I am done», not «another blank line».
|
|
87
|
+
*/
|
|
88
|
+
export declare function tabTarget(order: readonly string[], from: string, direction: "next" | "prev", { canAdd, blank }: {
|
|
89
|
+
canAdd: boolean;
|
|
90
|
+
blank: boolean;
|
|
91
|
+
}): TabTarget;
|
|
92
|
+
export type RowKey = "ArrowDown" | "ArrowUp" | "Home" | "End";
|
|
93
|
+
/**
|
|
94
|
+
* One body row. Its own component so that it can hold React Aria's `useKeyboard`
|
|
95
|
+
* (a hook per row) and the timer that decides whether the focus really left it.
|
|
96
|
+
* The same row is a `<tr>` in the table and an `<li>` in the stacked list — and the row
|
|
97
|
+
* of a new record in `DataTable`: one keyboard model, three shapes. Exported for that
|
|
98
|
+
* family sibling only; it is not in the barrel.
|
|
99
|
+
*/
|
|
100
|
+
export declare function EditRow({ as, rowId, rowRef, editable, editing, tabbable, className, onStart, onCommit, onCancel, onMove, onFocusRow, onTabOut, children, }: {
|
|
101
|
+
as: "tr" | "li";
|
|
102
|
+
rowId: string;
|
|
103
|
+
rowRef: (node: HTMLElement | null) => void;
|
|
104
|
+
editable: boolean;
|
|
105
|
+
editing: boolean;
|
|
106
|
+
tabbable: boolean;
|
|
107
|
+
className: string;
|
|
108
|
+
onStart: (viaKeyboard: boolean) => void;
|
|
109
|
+
onCommit: (refocus: boolean) => void;
|
|
110
|
+
onCancel: () => void;
|
|
111
|
+
onMove: (key: RowKey) => void;
|
|
112
|
+
onFocusRow: () => void;
|
|
113
|
+
/** `Tab` left the row by one of its edges. `true` = the table took it (opened another
|
|
114
|
+
* row, added one); `false` = the browser moves the focus as usual. */
|
|
115
|
+
onTabOut: (direction: "next" | "prev") => boolean;
|
|
116
|
+
children: ReactNode;
|
|
117
|
+
}): import("react").JSX.Element;
|
|
42
118
|
/**
|
|
43
119
|
* A table whose rows are edited in place, one row at a time.
|
|
44
120
|
*
|
|
@@ -59,13 +135,16 @@ totals, empty, grouping,
|
|
|
59
135
|
/** El total de una columna DENTRO de un montón, ya formateado. Lo calcula quien
|
|
60
136
|
* compone, por el mismo motivo que `totals`: sumar significa cosas distintas
|
|
61
137
|
* según el dominio, y un porcentaje se promedia, no se suma. */
|
|
62
|
-
groupTotal, height, layout, onOpen, }: {
|
|
138
|
+
groupTotal, height, layout, onOpen, onDiscard, bands, adds, flag, onReorder, }: {
|
|
63
139
|
rows: readonly T[];
|
|
64
140
|
columns: readonly EditColumn<T>[];
|
|
65
141
|
getRowId: (row: T) => string;
|
|
66
142
|
onChange?: (id: string, next: T) => void;
|
|
67
143
|
onRemove?: (id: string) => void;
|
|
68
|
-
|
|
144
|
+
/** «Add a line». Return the new row's id —or a promise of it, when the backend makes
|
|
145
|
+
* the record— and the table opens that row with the focus on its first control.
|
|
146
|
+
* Return nothing and the row just appears, closed. */
|
|
147
|
+
onAdd?: () => string | void | Promise<string | void>;
|
|
69
148
|
addLabel?: string;
|
|
70
149
|
totals?: readonly (ReactNode | null)[];
|
|
71
150
|
empty?: string;
|
|
@@ -93,4 +172,25 @@ groupTotal, height, layout, onOpen, }: {
|
|
|
93
172
|
* form of its own, a dialog that is a bottom sheet on a phone. The table ignores it:
|
|
94
173
|
* there the row is edited where it is read. */
|
|
95
174
|
onOpen?: (id: string) => void;
|
|
175
|
+
/** Take back a row `onAdd` opened when it is abandoned: `Escape` on it, or leaving it
|
|
176
|
+
* without a single change. Not `onRemove`: nobody removed a line, so there is nothing
|
|
177
|
+
* to undo or to announce. Without it the blank row stays. */
|
|
178
|
+
onDiscard?: (id: string) => void;
|
|
179
|
+
/** Rows that are not lines: sections and notes. See the file's header. */
|
|
180
|
+
bands?: EditBands<T>;
|
|
181
|
+
/** More ways to add, drawn beside «add» under the grid: «add a section», «add a note». */
|
|
182
|
+
adds?: readonly EditAdd[];
|
|
183
|
+
/** A warning about one row — stock, a price under cost — drawn as a `Flag` at its start.
|
|
184
|
+
* `null` for the rows with nothing to say, which should be most of them. `owner` is what
|
|
185
|
+
* the record's activity says it belongs to («Línea 3 · Impresora…»); without it, the
|
|
186
|
+
* row's first value. */
|
|
187
|
+
flag?: (row: T) => (Omit<FlagProps, "children"> & {
|
|
188
|
+
detail?: ReactNode;
|
|
189
|
+
owner?: string;
|
|
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;
|
|
96
196
|
}): import("react").JSX.Element;
|
|
@@ -6,9 +6,28 @@ import { pathKey, pileIsOpen, pileItems, pilesOf, } from "../../foundations/grou
|
|
|
6
6
|
import { HANDHELD_WIDTH } from "../../foundations/layout.js";
|
|
7
7
|
import { stickyTop, useScrollBox } from "../../foundations/sticky.js";
|
|
8
8
|
import { IconButton } from "../button/Button.js";
|
|
9
|
-
import {
|
|
9
|
+
import { NoteCard } from "../field/note.js";
|
|
10
|
+
import { Flag } from "../notice/Flag.js";
|
|
11
|
+
import { HEADER_TEXT } from "./Table.js";
|
|
12
|
+
import { GripVertical, Plus, Trash2 } from "lucide-react";
|
|
10
13
|
import { Fragment, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
11
14
|
import { useKeyboard } from "react-aria";
|
|
15
|
+
/** The lines each section heads: the rows after it up to the next section. Notes do not
|
|
16
|
+
* cut a section, and are not lines. */
|
|
17
|
+
export function sectionLines(rows, getRowId, kind) {
|
|
18
|
+
const out = new Map();
|
|
19
|
+
let current = null;
|
|
20
|
+
for (const row of rows) {
|
|
21
|
+
const k = kind(row);
|
|
22
|
+
if (k === "section") {
|
|
23
|
+
current = [];
|
|
24
|
+
out.set(getRowId(row), current);
|
|
25
|
+
}
|
|
26
|
+
else if (k === null)
|
|
27
|
+
current?.push(row);
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
12
31
|
/**
|
|
13
32
|
* Where each column goes in the stacked card, defaults resolved: a declared `stack`
|
|
14
33
|
* wins; otherwise the first undeclared column is the title (unless one was declared),
|
|
@@ -36,14 +55,47 @@ function pileTag(pile, locale) {
|
|
|
36
55
|
/** The focus ring of an operable row: drawn on the row, inset, so it is not cut by the
|
|
37
56
|
* box and does not move the cells. */
|
|
38
57
|
const RING = "outline-0 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary forced-colors:focus-visible:outline-[Highlight]";
|
|
39
|
-
/** What can take the focus in a row. Filtered again in `
|
|
40
|
-
*
|
|
58
|
+
/** What can take the focus in a row. Filtered again in `stopsIn`: React Aria's hidden form
|
|
59
|
+
* mirrors (a `<select>` behind a `Select`, an `<input hidden>` in a date picker) match this
|
|
60
|
+
* and are not stops. */
|
|
41
61
|
const FOCUSABLE = 'input:not([type="hidden"]):not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), a[href], [tabindex]';
|
|
62
|
+
/** What `Tab` stops on inside `scope` of a row: focusable, and not a hidden form mirror.
|
|
63
|
+
*
|
|
64
|
+
* Context: NOT filtered by `aria-hidden` / `inert`. While a list is open (a tags field, a
|
|
65
|
+
* picker) React Aria hides everything outside it that way — the rest of the row included —
|
|
66
|
+
* and the row then looked as if its open control were its last: `Tab` added a new row from
|
|
67
|
+
* the middle of this one. The mirrors carry their own marks instead. */
|
|
68
|
+
function stopsIn(row, scope) {
|
|
69
|
+
return [...row.querySelectorAll(scope)].filter((el) => el.tabIndex >= 0 && el.closest("[hidden], [data-react-aria-prevent-focus]") === null);
|
|
70
|
+
}
|
|
71
|
+
/** The controls of the editable cells of a row, in tab order. */
|
|
72
|
+
function editStops(row) {
|
|
73
|
+
return stopsIn(row, `[data-edit] :is(${FOCUSABLE})`);
|
|
74
|
+
}
|
|
42
75
|
/** The first control of a row being edited: the first one in an editable cell, else
|
|
43
76
|
* anything focusable in the row (a column without `edit` may still hold a button). */
|
|
44
|
-
function firstControl(row) {
|
|
45
|
-
|
|
46
|
-
|
|
77
|
+
export function firstControl(row) {
|
|
78
|
+
return editStops(row)[0] ?? stopsIn(row, FOCUSABLE)[0] ?? null;
|
|
79
|
+
}
|
|
80
|
+
/** The last control of a row being edited — where `Shift+Tab` from the next row lands. */
|
|
81
|
+
function lastControl(row) {
|
|
82
|
+
return editStops(row).at(-1) ?? stopsIn(row, FOCUSABLE).at(-1) ?? null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Where `Tab` past the last control (`next`) or `Shift+Tab` before the first (`prev`) of
|
|
86
|
+
* the edited row `from` goes, over the rows in the order they are drawn.
|
|
87
|
+
*
|
|
88
|
+
* Past the last row it adds one when the table can — unless `from` is itself a row just
|
|
89
|
+
* added and still blank: one more `Tab` there means «I am done», not «another blank line».
|
|
90
|
+
*/
|
|
91
|
+
export function tabTarget(order, from, direction, { canAdd, blank }) {
|
|
92
|
+
const at = order.indexOf(from);
|
|
93
|
+
if (at === -1)
|
|
94
|
+
return { kind: "leave" };
|
|
95
|
+
const to = order[direction === "next" ? at + 1 : at - 1];
|
|
96
|
+
if (to !== undefined)
|
|
97
|
+
return { kind: "row", id: to };
|
|
98
|
+
return direction === "next" && canAdd && !blank ? { kind: "add" } : { kind: "leave" };
|
|
47
99
|
}
|
|
48
100
|
/** Keys a control inside the row already answers with `Enter`: a button presses, a
|
|
49
101
|
* link follows, a text area breaks the line, an open list picks its option. */
|
|
@@ -54,10 +106,11 @@ function ownsEnter(target) {
|
|
|
54
106
|
/**
|
|
55
107
|
* One body row. Its own component so that it can hold React Aria's `useKeyboard`
|
|
56
108
|
* (a hook per row) and the timer that decides whether the focus really left it.
|
|
57
|
-
* The same row is a `<tr>` in the table and an `<li>` in the stacked list
|
|
58
|
-
* keyboard model,
|
|
109
|
+
* The same row is a `<tr>` in the table and an `<li>` in the stacked list — and the row
|
|
110
|
+
* of a new record in `DataTable`: one keyboard model, three shapes. Exported for that
|
|
111
|
+
* family sibling only; it is not in the barrel.
|
|
59
112
|
*/
|
|
60
|
-
function EditRow({ as, rowId, rowRef, editable, editing, tabbable, className, onStart, onCommit, onCancel, onMove, onFocusRow, children, }) {
|
|
113
|
+
export function EditRow({ as, rowId, rowRef, editable, editing, tabbable, className, onStart, onCommit, onCancel, onMove, onFocusRow, onTabOut, children, }) {
|
|
61
114
|
const leaving = useRef(null);
|
|
62
115
|
const stay = () => {
|
|
63
116
|
if (leaving.current !== null)
|
|
@@ -108,10 +161,23 @@ function EditRow({ as, rowId, rowRef, editable, editing, tabbable, className, on
|
|
|
108
161
|
that field write the value it was holding (it commits on blur). */
|
|
109
162
|
onKeyDownCapture: editing
|
|
110
163
|
? (e) => {
|
|
111
|
-
if (e.
|
|
164
|
+
if (e.nativeEvent.isComposing)
|
|
112
165
|
return;
|
|
113
166
|
if (!(e.target instanceof Node) || !e.currentTarget.contains(e.target))
|
|
114
167
|
return;
|
|
168
|
+
if (e.key === "Tab") {
|
|
169
|
+
/* Only the default is prevented, never the propagation: a combobox still
|
|
170
|
+
hears its `Tab` and takes the option it has marked before the row moves. */
|
|
171
|
+
const stops = editStops(e.currentTarget);
|
|
172
|
+
const edge = e.shiftKey ? stops[0] : stops.at(-1);
|
|
173
|
+
if (edge === undefined || !edge.contains(e.target))
|
|
174
|
+
return;
|
|
175
|
+
if (onTabOut(e.shiftKey ? "prev" : "next"))
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (e.key !== "Enter")
|
|
180
|
+
return;
|
|
115
181
|
if (ownsEnter(e.target))
|
|
116
182
|
return;
|
|
117
183
|
e.preventDefault();
|
|
@@ -174,8 +240,32 @@ totals, empty, grouping,
|
|
|
174
240
|
/** El total de una columna DENTRO de un montón, ya formateado. Lo calcula quien
|
|
175
241
|
* compone, por el mismo motivo que `totals`: sumar significa cosas distintas
|
|
176
242
|
* según el dominio, y un porcentaje se promedia, no se suma. */
|
|
177
|
-
groupTotal, height, layout = "auto", onOpen, }) {
|
|
243
|
+
groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReorder, }) {
|
|
178
244
|
const copy = useUiCopy();
|
|
245
|
+
const bandOf = (row) => bands?.kind(row) ?? null;
|
|
246
|
+
/* The row's mark, before its first value — and, in a record's margin, its card. */
|
|
247
|
+
const flagOf = (row, withCard = false) => {
|
|
248
|
+
const said = flag?.(row) ?? null;
|
|
249
|
+
if (said === null)
|
|
250
|
+
return null;
|
|
251
|
+
return (_jsxs(_Fragment, { children: [_jsx(Flag, { tone: said.tone, label: said.label, children: said.detail }), withCard ? (_jsx(NoteCard, { id: `flag-${getRowId(row)}`, ...(said.owner === undefined ? {} : { owner: said.owner }), note: {
|
|
252
|
+
tone: said.tone,
|
|
253
|
+
label: said.label,
|
|
254
|
+
...(said.detail === undefined ? {} : { detail: said.detail }),
|
|
255
|
+
} })) : null] }));
|
|
256
|
+
};
|
|
257
|
+
const underSection = useMemo(() => (bands === undefined ? null : sectionLines(rows, getRowId, bands.kind)), [rows, bands, getRowId]);
|
|
258
|
+
/* Where a band's text stops: the first column with something of its own on that row. */
|
|
259
|
+
const bandFigures = (row) => {
|
|
260
|
+
const lines = underSection?.get(getRowId(row)) ?? null;
|
|
261
|
+
const figures = columns.map((c) => c.onBands === true
|
|
262
|
+
? c.render(row)
|
|
263
|
+
: lines === null
|
|
264
|
+
? null
|
|
265
|
+
: (bands?.total?.(c, lines) ?? null));
|
|
266
|
+
const first = figures.findIndex((f, i) => columns[i]?.onBands === true || (f !== null && f !== undefined));
|
|
267
|
+
return { span: first <= 0 ? columns.length : first, figures };
|
|
268
|
+
};
|
|
179
269
|
const d = useDensity();
|
|
180
270
|
const box = useScrollBox(height === undefined ? {} : { height });
|
|
181
271
|
const [editing, setEditing] = useState(null);
|
|
@@ -185,9 +275,13 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
185
275
|
/* The row as it was when editing began, to put it back on `Escape`; `touched` says
|
|
186
276
|
whether any control wrote to it since, so a cancel without changes changes nothing. */
|
|
187
277
|
const before = useRef(null);
|
|
188
|
-
/* Set when the keyboard opened the row: its first control
|
|
189
|
-
the controls exist. A click opens it without
|
|
278
|
+
/* Set when the keyboard opened the row: its first control —or its last, coming back
|
|
279
|
+
with `Shift+Tab`— takes the focus once the controls exist. A click opens it without
|
|
280
|
+
moving the focus. */
|
|
190
281
|
const focusOnOpen = useRef(null);
|
|
282
|
+
/* The id `onAdd` returned, until that row shows up in `rows` and can be opened. */
|
|
283
|
+
const [arriving, setArriving] = useState(null);
|
|
284
|
+
const addButton = useRef(null);
|
|
191
285
|
/* La secuencia que se dibuja: montones y filas intercalados. Sin agrupado, las
|
|
192
286
|
filas tal cual — una tabla que no agrupa no paga nada por poder hacerlo. */
|
|
193
287
|
const drawn = useMemo(() => {
|
|
@@ -240,35 +334,127 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
240
334
|
const opens = stacked && onOpen !== undefined;
|
|
241
335
|
const operable = editable || opens;
|
|
242
336
|
const cell = byDensity(d, "px-3 py-1.5", "px-2 py-1");
|
|
243
|
-
const
|
|
337
|
+
const addText = addLabel ?? copy.editable.add;
|
|
244
338
|
const emptyText = empty ?? copy.editable.empty;
|
|
245
339
|
/* The rows in the order they are drawn: what the arrows walk. */
|
|
246
340
|
const order = drawn.flatMap((entry) => entry.row === undefined ? [] : [getRowId(entry.row)]);
|
|
247
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 }) }) }));
|
|
248
387
|
useEffect(() => {
|
|
249
|
-
const
|
|
250
|
-
if (
|
|
388
|
+
const target = focusOnOpen.current;
|
|
389
|
+
if (target === null || target.id !== editing)
|
|
251
390
|
return;
|
|
252
391
|
focusOnOpen.current = null;
|
|
253
|
-
const node = rowNodes.current.get(id);
|
|
254
|
-
if (node
|
|
255
|
-
|
|
392
|
+
const node = rowNodes.current.get(target.id);
|
|
393
|
+
if (node === undefined)
|
|
394
|
+
return;
|
|
395
|
+
const control = target.at === "last" ? lastControl(node) : firstControl(node);
|
|
396
|
+
(control ?? node).focus();
|
|
256
397
|
}, [editing]);
|
|
257
|
-
const start = (id, row, viaKeyboard) => {
|
|
258
|
-
|
|
398
|
+
const start = (id, row, viaKeyboard, { at = "first", added = false } = {}) => {
|
|
399
|
+
/* A row just added is written where it appeared, card or not: it was added in place
|
|
400
|
+
precisely so that it would not open somewhere else. */
|
|
401
|
+
if (opens && !added && bandOf(row) === null) {
|
|
259
402
|
setActive(id);
|
|
260
403
|
onOpen?.(id);
|
|
261
404
|
return;
|
|
262
405
|
}
|
|
263
|
-
before.current = { id, row, touched: false };
|
|
264
|
-
focusOnOpen.current = viaKeyboard ? id : null;
|
|
406
|
+
before.current = { id, row, touched: false, added };
|
|
407
|
+
focusOnOpen.current = viaKeyboard ? { id, at } : null;
|
|
265
408
|
setActive(id);
|
|
266
409
|
setEditing(id);
|
|
267
410
|
};
|
|
411
|
+
/* The added row is opened the render it appears in — which may be this one, or the one
|
|
412
|
+
after the backend answered. */
|
|
413
|
+
useEffect(() => {
|
|
414
|
+
if (arriving === null)
|
|
415
|
+
return;
|
|
416
|
+
const row = rows.find((r) => getRowId(r) === arriving);
|
|
417
|
+
if (row === undefined)
|
|
418
|
+
return;
|
|
419
|
+
setArriving(null);
|
|
420
|
+
start(arriving, row, true, { added: true });
|
|
421
|
+
/* `start` and `getRowId` are new every render; what decides is the id and the rows. */
|
|
422
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
423
|
+
}, [arriving, rows]);
|
|
424
|
+
const add = (maker = onAdd) => {
|
|
425
|
+
if (maker === undefined)
|
|
426
|
+
return;
|
|
427
|
+
const made = maker();
|
|
428
|
+
if (typeof made === "string")
|
|
429
|
+
setArriving(made);
|
|
430
|
+
else if (made instanceof Promise) {
|
|
431
|
+
void made.then((id) => {
|
|
432
|
+
if (typeof id === "string")
|
|
433
|
+
setArriving(id);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
};
|
|
268
437
|
/* Keyed by the row: a commit that arrives late (the blur of the row you clicked
|
|
269
438
|
away from) must not close the row the click just opened. */
|
|
270
439
|
const close = (id) => setEditing((current) => (current === id ? null : current));
|
|
440
|
+
/* A row added and abandoned goes away. `true` when it did. */
|
|
441
|
+
const discard = (id, untouchedOnly) => {
|
|
442
|
+
const was = before.current;
|
|
443
|
+
if (was === null || was.id !== id || !was.added || onDiscard === undefined)
|
|
444
|
+
return false;
|
|
445
|
+
if (untouchedOnly && was.touched)
|
|
446
|
+
return false;
|
|
447
|
+
before.current = null;
|
|
448
|
+
close(id);
|
|
449
|
+
onDiscard(id);
|
|
450
|
+
return true;
|
|
451
|
+
};
|
|
271
452
|
const commit = (id, refocus) => {
|
|
453
|
+
if (discard(id, true)) {
|
|
454
|
+
if (refocus)
|
|
455
|
+
addButton.current?.focus();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
272
458
|
if (refocus)
|
|
273
459
|
rowNodes.current.get(id)?.focus();
|
|
274
460
|
if (before.current?.id === id)
|
|
@@ -280,12 +466,34 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
280
466
|
(a number field) writes it now, and the old row is put back after it, not
|
|
281
467
|
before. */
|
|
282
468
|
rowNodes.current.get(id)?.focus();
|
|
469
|
+
if (discard(id, false)) {
|
|
470
|
+
/* The row is gone with the focus in it: «add» is where it came from. */
|
|
471
|
+
addButton.current?.focus();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
283
474
|
const was = before.current;
|
|
284
475
|
before.current = null;
|
|
285
476
|
if (was !== null && was.id === id && was.touched)
|
|
286
477
|
onChange?.(id, was.row);
|
|
287
478
|
close(id);
|
|
288
479
|
};
|
|
480
|
+
const tabOut = (from, direction) => {
|
|
481
|
+
const was = before.current;
|
|
482
|
+
const blank = was !== null && was.id === from && was.added && !was.touched;
|
|
483
|
+
const target = tabTarget(order, from, direction, { canAdd: onAdd !== undefined, blank });
|
|
484
|
+
if (target.kind === "leave")
|
|
485
|
+
return false;
|
|
486
|
+
commit(from, false);
|
|
487
|
+
if (target.kind === "add") {
|
|
488
|
+
add();
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
const row = rows.find((r) => getRowId(r) === target.id);
|
|
492
|
+
if (row === undefined)
|
|
493
|
+
return false;
|
|
494
|
+
start(target.id, row, true, { at: direction === "next" ? "first" : "last" });
|
|
495
|
+
return true;
|
|
496
|
+
};
|
|
289
497
|
/* What a control writes, marking the row as touched for `Escape`. */
|
|
290
498
|
const setter = (id) => (next) => {
|
|
291
499
|
if (before.current?.id === id)
|
|
@@ -309,6 +517,7 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
309
517
|
onCancel: () => cancel(id),
|
|
310
518
|
onMove: (key) => move(id, key),
|
|
311
519
|
onFocusRow: () => setActive(id),
|
|
520
|
+
onTabOut: (direction) => tabOut(id, direction),
|
|
312
521
|
});
|
|
313
522
|
const move = (from, key) => {
|
|
314
523
|
const at = order.indexOf(from);
|
|
@@ -349,7 +558,7 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
349
558
|
];
|
|
350
559
|
});
|
|
351
560
|
const controls = editable && isEditing ? columns.filter((c) => c.edit !== undefined) : [];
|
|
352
|
-
return (_jsxs(EditRow, { as: "li", ...rowWiring(id, row), className: `group/row flex min-w-0 flex-col gap-1 rounded-control border-b border-line ${pad} ${isEditing ? "bg-primary/5" : "hover:bg-ink/4"} ${operable ? RING : ""} ${operable && !isEditing ? (opens ? "cursor-pointer" : "cursor-text") : ""}`, children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [_jsx("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5 text-meta text-ink", children: titles.map((c) => (_jsx("div", { className: "min-w-0", children: c.render(row) }, c.id))) }), trails.map((c) => (_jsx("div", { className: "shrink-0", children: c.render(row) }, c.id))), isEditing && onRemove !== undefined ? (_jsx(IconButton, { icon: Trash2, label: copy.editable.remove, size: "sm", tone: "danger", onClick: () => onRemove(id) })) : null] }), parts.length === 0 && amounts.length === 0 ? null : (_jsxs("div", { className: "flex min-w-0 items-baseline gap-3 text-meta", children: [_jsx("span", { className: "min-w-0 flex-1 text-muted tabular-nums", children: parts.map((p, i) => (_jsxs(Fragment, { children: [i > 0 ? " · " : null, p.node] }, p.id))) }), amounts.map((c) => (
|
|
561
|
+
return (_jsxs(EditRow, { as: "li", ...rowWiring(id, row), className: `group/row flex min-w-0 flex-col gap-1 rounded-control border-b border-line ${pad} ${isEditing ? "bg-primary/5" : "hover:bg-ink/4"} ${operable ? RING : ""} ${operable && !isEditing ? (opens ? "cursor-pointer" : "cursor-text") : ""}`, children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [flagOf(row), _jsx("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5 text-meta text-ink", children: titles.map((c) => (_jsx("div", { className: "min-w-0", children: c.render(row) }, c.id))) }), trails.map((c) => (_jsx("div", { className: "shrink-0", children: c.render(row) }, c.id))), isEditing && onRemove !== undefined ? (_jsx(IconButton, { icon: Trash2, label: copy.editable.remove, size: "sm", tone: "danger", onClick: () => onRemove(id) })) : null] }), parts.length === 0 && amounts.length === 0 ? null : (_jsxs("div", { className: "flex min-w-0 items-baseline gap-3 text-meta", children: [_jsx("span", { className: "min-w-0 flex-1 text-muted tabular-nums", children: parts.map((p, i) => (_jsxs(Fragment, { children: [i > 0 ? " · " : null, p.node] }, p.id))) }), amounts.map((c) => (
|
|
353
562
|
/* The amount is read by its figure on screen and by its name aloud: a
|
|
354
563
|
lone «Bs. 1.296,00» at the end of a card does not say what it is. */
|
|
355
564
|
_jsxs("span", { className: "shrink-0 text-right font-medium whitespace-nowrap text-ink tabular-nums", children: [_jsxs("span", { className: "sr-only", children: [c.header, " "] }), c.render(row)] }, c.id)))] })), controls.length === 0 ? null : (
|
|
@@ -358,6 +567,17 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
358
567
|
guess. The name is drawn for the eye; the control carries its own label. */
|
|
359
568
|
_jsx("div", { className: "grid grid-cols-2 gap-x-3 gap-y-2 pt-1.5", children: controls.map((c) => (_jsxs("div", { "data-edit": "", className: `flex min-w-0 flex-col gap-1 ${c.numeric === true ? "" : "col-span-2"}`, children: [_jsx("span", { "aria-hidden": "true", className: "text-micro text-muted", children: c.header }), c.edit?.(row, set)] }, c.id))) }))] }, id));
|
|
360
569
|
};
|
|
570
|
+
/* A section or a note on a phone: the band and, for a section, what it adds up to in
|
|
571
|
+
the card's amount; editing, its control under it. */
|
|
572
|
+
const bandCard = (row) => {
|
|
573
|
+
const id = getRowId(row);
|
|
574
|
+
const band = bandOf(row);
|
|
575
|
+
const isEditing = editing === id;
|
|
576
|
+
const lines = underSection?.get(id) ?? null;
|
|
577
|
+
return (_jsxs(EditRow, { as: "li", ...rowWiring(id, row), className: `flex min-w-0 flex-col gap-1.5 rounded-control border-b border-line ${pad} ${isEditing ? "bg-primary/5" : band === "section" ? "bg-ground" : ""} ${operable ? RING : ""}`, children: [_jsxs("div", { className: "flex min-w-0 items-baseline gap-3 text-meta", children: [_jsx("span", { className: `min-w-0 flex-1 ${band === "section" ? "font-semibold text-ink" : "text-muted italic"}`, children: bands?.render(row) }), lines === null
|
|
578
|
+
? null
|
|
579
|
+
: amounts.map((c) => (_jsx("span", { className: "shrink-0 font-semibold text-ink tabular-nums", children: bands?.total?.(c, lines) ?? null }, c.id))), isEditing && onRemove !== undefined ? (_jsx(IconButton, { icon: Trash2, label: copy.editable.remove, size: "sm", tone: "danger", onClick: () => onRemove(id) })) : null] }), isEditing && editable && bands?.edit !== undefined ? (_jsx("div", { "data-edit": "", children: bands.edit(row, setter(id)) })) : null] }, id));
|
|
580
|
+
};
|
|
361
581
|
const list = (_jsxs(_Fragment, { children: [_jsx("ul", { "data-stacked": "", className: "flex min-w-0 flex-col px-1", children: rows.length === 0 ? (_jsx("li", { className: `text-meta text-muted italic ${pad}`, children: emptyText })) : (drawn.map((entry) => {
|
|
362
582
|
if (entry.pile !== undefined) {
|
|
363
583
|
const pile = entry.pile;
|
|
@@ -371,7 +591,8 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
371
591
|
}
|
|
372
592
|
: undefined, children: [_jsx("span", { className: "min-w-0 flex-1", children: pileTag(pile, copy.locale) }), amounts.map((c) => (_jsx("span", { className: "shrink-0 text-ink tabular-nums", children: groupTotal?.(c, inside) ?? null }, c.id)))] }, `pile:${pathKey(pile.path)}`));
|
|
373
593
|
}
|
|
374
|
-
|
|
594
|
+
const row = entry.row;
|
|
595
|
+
return bandOf(row) === null ? card(row) : bandCard(row);
|
|
375
596
|
})) }), totals === undefined ? null : (
|
|
376
597
|
/* The footer's aggregates, one per line with the column's name: under a card
|
|
377
598
|
there is no column to stand under. */
|
|
@@ -381,7 +602,20 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
381
602
|
return null;
|
|
382
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));
|
|
383
604
|
}) }))] }));
|
|
384
|
-
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) => {
|
|
385
619
|
if (entry.pile !== undefined) {
|
|
386
620
|
const pile = entry.pile;
|
|
387
621
|
const inside = pile.piles.length === 0 ? pile.items : pileItems(pile.piles);
|
|
@@ -390,7 +624,7 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
390
624
|
columna y cada total bajo la suya. Un total en un rincón
|
|
391
625
|
obliga a buscar de qué columna es; alineado no hay nada que
|
|
392
626
|
buscar. */
|
|
393
|
-
_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
|
|
394
628
|
? {
|
|
395
629
|
paddingLeft: `calc(var(--spacing-indent) * ${pile.depth} + 0.625rem)`,
|
|
396
630
|
}
|
|
@@ -401,6 +635,22 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
401
635
|
const row = entry.row;
|
|
402
636
|
const id = getRowId(row);
|
|
403
637
|
const isEditing = editing === id;
|
|
638
|
+
const band = bandOf(row);
|
|
639
|
+
if (band !== null) {
|
|
640
|
+
const { span, figures } = bandFigures(row);
|
|
641
|
+
const bandWash = isEditing
|
|
642
|
+
? "bg-primary/5"
|
|
643
|
+
: band === "section"
|
|
644
|
+
? "bg-ground group-hover/row:bg-ink/6"
|
|
645
|
+
: "group-hover/row:bg-ink/4";
|
|
646
|
+
const bandMark = `border-b border-line first:rounded-l-control last:rounded-r-control ${bandWash}`;
|
|
647
|
+
const control = isEditing && editable && bands?.edit !== undefined;
|
|
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"
|
|
649
|
+
? "font-semibold text-ink"
|
|
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
|
|
651
|
+
? "text-right font-semibold tabular-nums"
|
|
652
|
+
: ""}`, children: figures[span + i] ?? null }, c.id))), onRemove === undefined ? null : (_jsx("td", { className: `w-0 py-1 pr-2 pl-1 align-middle ${bandMark}`, children: _jsx("span", { className: "flex opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 group-focus-visible/row:opacity-100 focus-within:opacity-100", onClick: (e) => e.stopPropagation(), children: _jsx(IconButton, { icon: Trash2, label: copy.editable.remove, size: "sm", tone: "danger", ...(editable && stop !== id ? { tabIndex: -1 } : {}), onClick: () => onRemove(id) }) }) }))] }, id));
|
|
653
|
+
}
|
|
404
654
|
/* The wash lives on the cells. A background on the row paints
|
|
405
655
|
edge to edge, and `border-collapse` would ignore a radius. */
|
|
406
656
|
const wash = isEditing ? "bg-primary/5" : "group-hover/row:bg-ink/4";
|
|
@@ -409,15 +659,20 @@ groupTotal, height, layout = "auto", onOpen, }) {
|
|
|
409
659
|
return (_jsxs(EditRow, { as: "tr", ...rowWiring(id, row),
|
|
410
660
|
/* The focus ring is drawn on the row, inset, so it is not cut by
|
|
411
661
|
the box and does not move the cells. */
|
|
412
|
-
className: `group/row ${editable ? RING : ""} ${editable && !isEditing ? "cursor-text" : ""}`, children: [columns.map((c) => {
|
|
662
|
+
className: `group/row ${editable ? RING : ""} ${editable && !isEditing ? "cursor-text" : ""}`, children: [reorderable ? grip(id, mark) : null, columns.map((c, ci) => {
|
|
413
663
|
const control = isEditing && c.edit !== undefined && editable;
|
|
414
|
-
|
|
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));
|
|
415
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));
|
|
416
|
-
})) }), totals === undefined ? null : (_jsx("tfoot", { children: _jsxs("tr", { children: [columns.map((c, i) => (_jsx("td", { className: `border-t border-line-strong text-meta font-semibold text-ink ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: totals[i] ?? null }, c.id))), onRemove === undefined ? null : (_jsx("td", { className: "border-t border-line-strong" }))] }) }))] })) }), onAdd === undefined ? null : (
|
|
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 : (
|
|
417
670
|
/* The «add» goes under the grid, as text, not as a button away from it:
|
|
418
671
|
what is about to happen is that one more row shows up right here.
|
|
419
672
|
The hover is the same inset mark as a row: the shell lines the label
|
|
420
673
|
up with the cells, and the button itself is the rounded wash. It sits
|
|
421
|
-
outside the scroll box, so a table with its own height keeps it in view.
|
|
422
|
-
|
|
674
|
+
outside the scroll box, so a table with its own height keeps it in view.
|
|
675
|
+
A section and a note sit beside it, quieter: the line is what is added
|
|
676
|
+
most, so it keeps the lead and the whole free width. */
|
|
677
|
+
_jsxs("div", { className: "flex flex-wrap items-center gap-x-1 px-1 py-0.5", children: [onAdd === undefined ? null : (_jsxs("button", { ref: addButton, type: "button", onClick: () => add(), className: `flex items-center gap-1.5 rounded-control text-left text-meta text-primary transition-colors duration-(--duration-instant) hover:bg-primary/5 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary ${adds === undefined || adds.length === 0 ? "w-full" : ""} ${byDensity(d, "px-3 py-1", "px-2 py-0.5")}`, children: [_jsx(Plus, { className: "size-(--size-icon-sm)", strokeWidth: 2.5, "aria-hidden": true }), addText] })), (adds ?? []).map((extra) => (_jsx("button", { type: "button", onClick: () => add(extra.onAdd), className: `rounded-control text-left text-meta text-muted transition-colors duration-(--duration-instant) hover:bg-primary/5 hover:text-primary focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary ${byDensity(d, "px-3 py-1", "px-2 py-0.5")}`, children: extra.label }, extra.label)))] }))] }));
|
|
423
678
|
}
|