@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.
Files changed (198) hide show
  1. package/README.md +40 -13
  2. package/dist/components/activity/Activity.d.ts +69 -0
  3. package/dist/components/activity/Activity.js +279 -0
  4. package/dist/components/activity/ActivityBadge.d.ts +16 -0
  5. package/dist/components/activity/ActivityBadge.js +29 -0
  6. package/dist/components/{chatter → activity}/Messages.d.ts +2 -2
  7. package/dist/components/{chatter → activity}/Messages.js +9 -6
  8. package/dist/components/activity/index.d.ts +4 -0
  9. package/dist/components/activity/index.js +5 -0
  10. package/dist/components/assist/AssistMark.d.ts +3 -3
  11. package/dist/components/assist/AssistMark.js +1 -1
  12. package/dist/components/assist/assist.d.ts +1 -1
  13. package/dist/components/assist/assist.js +1 -1
  14. package/dist/components/calendar/RecordCalendar.d.ts +48 -0
  15. package/dist/components/calendar/RecordCalendar.js +101 -0
  16. package/dist/components/calendar/index.d.ts +1 -0
  17. package/dist/components/calendar/index.js +1 -0
  18. package/dist/components/form/Sheet.d.ts +4 -0
  19. package/dist/components/form/Sheet.js +16 -5
  20. package/dist/components/gantt/RecordGantt.d.ts +44 -0
  21. package/dist/components/gantt/RecordGantt.js +110 -0
  22. package/dist/components/gantt/index.d.ts +1 -0
  23. package/dist/components/gantt/index.js +1 -0
  24. package/dist/components/index.d.ts +4 -1
  25. package/dist/components/index.js +4 -1
  26. package/dist/components/lines/LineItems.d.ts +45 -5
  27. package/dist/components/lines/LineItems.js +106 -18
  28. package/dist/components/listing/BoardColumns.d.ts +39 -0
  29. package/dist/components/listing/BoardColumns.js +138 -0
  30. package/dist/components/listing/Listing.js +1 -1
  31. package/dist/components/listing/ListingBar.d.ts +46 -2
  32. package/dist/components/listing/ListingBar.js +47 -5
  33. package/dist/components/listing/PivotExplorer.d.ts +34 -0
  34. package/dist/components/listing/PivotExplorer.js +86 -0
  35. package/dist/components/listing/RecordCard.d.ts +6 -8
  36. package/dist/components/listing/RecordCard.js +13 -7
  37. package/dist/components/listing/Row.d.ts +25 -2
  38. package/dist/components/listing/Row.js +30 -13
  39. package/dist/components/listing/index.d.ts +2 -0
  40. package/dist/components/listing/index.js +2 -0
  41. package/dist/components/listing/set.d.ts +1 -1
  42. package/dist/components/navigation/index.d.ts +0 -2
  43. package/dist/components/navigation/index.js +2 -3
  44. package/dist/components/record/RecordAudit.d.ts +17 -0
  45. package/dist/components/record/RecordAudit.js +27 -0
  46. package/dist/components/record/RecordHeader.d.ts +27 -17
  47. package/dist/components/record/RecordHeader.js +64 -20
  48. package/dist/components/record/RecordNotices.d.ts +37 -0
  49. package/dist/components/record/RecordNotices.js +75 -0
  50. package/dist/components/record/RecordRibbon.d.ts +13 -0
  51. package/dist/components/record/RecordRibbon.js +22 -0
  52. package/dist/components/record/ThumbBar.js +1 -1
  53. package/dist/components/record/index.d.ts +2 -0
  54. package/dist/components/record/index.js +2 -0
  55. package/dist/components/search/CriteriaChips.d.ts +6 -1
  56. package/dist/components/search/CriteriaChips.js +71 -20
  57. package/dist/components/search/SearchBar.d.ts +4 -1
  58. package/dist/components/search/SearchBar.js +15 -7
  59. package/dist/components/timeline/RecordTimeline.d.ts +60 -0
  60. package/dist/components/timeline/RecordTimeline.js +214 -0
  61. package/dist/components/timeline/TimelineToolbar.d.ts +30 -0
  62. package/dist/components/timeline/TimelineToolbar.js +31 -0
  63. package/dist/components/timeline/index.d.ts +3 -0
  64. package/dist/components/timeline/index.js +3 -0
  65. package/dist/components/timeline/time.d.ts +43 -0
  66. package/dist/components/timeline/time.js +135 -0
  67. package/dist/foundations/copy.d.ts +194 -18
  68. package/dist/foundations/copy.es.js +142 -15
  69. package/dist/foundations/copy.js +133 -11
  70. package/dist/foundations/index.d.ts +1 -1
  71. package/dist/foundations/match.d.ts +23 -0
  72. package/dist/foundations/match.js +63 -0
  73. package/dist/foundations/slots.d.ts +1 -1
  74. package/dist/foundations/slots.js +8 -3
  75. package/dist/foundations/types.d.ts +7 -0
  76. package/dist/foundations/types.js +0 -36
  77. package/dist/primitives/block/index.d.ts +1 -1
  78. package/dist/primitives/block/index.js +1 -1
  79. package/dist/primitives/cell/CellStack.d.ts +7 -1
  80. package/dist/primitives/cell/CellStack.js +5 -2
  81. package/dist/primitives/choice/ComboBox.d.ts +3 -0
  82. package/dist/primitives/choice/ComboBox.js +10 -6
  83. package/dist/primitives/choice/KanbanState.d.ts +14 -0
  84. package/dist/primitives/choice/KanbanState.js +37 -0
  85. package/dist/primitives/choice/Priority.d.ts +20 -0
  86. package/dist/primitives/choice/Priority.js +41 -0
  87. package/dist/primitives/choice/SearchField.d.ts +10 -1
  88. package/dist/primitives/choice/SearchField.js +21 -2
  89. package/dist/primitives/choice/Slider.d.ts +6 -1
  90. package/dist/primitives/choice/Slider.js +2 -2
  91. package/dist/primitives/choice/index.d.ts +2 -0
  92. package/dist/primitives/choice/index.js +2 -0
  93. package/dist/primitives/date/Calendar.js +30 -6
  94. package/dist/primitives/date/DatePicker.d.ts +11 -3
  95. package/dist/primitives/date/DatePicker.js +12 -8
  96. package/dist/primitives/date/DateTime.d.ts +35 -0
  97. package/dist/primitives/date/DateTime.js +100 -0
  98. package/dist/primitives/date/index.d.ts +1 -0
  99. package/dist/primitives/date/index.js +4 -2
  100. package/dist/primitives/dialog/Dialog.d.ts +9 -3
  101. package/dist/primitives/dialog/Dialog.js +41 -5
  102. package/dist/primitives/field/DropZone.js +1 -1
  103. package/dist/primitives/field/Field.d.ts +14 -2
  104. package/dist/primitives/field/Field.js +25 -14
  105. package/dist/primitives/field/copy.d.ts +16 -0
  106. package/dist/primitives/field/copy.js +69 -0
  107. package/dist/primitives/field/index.d.ts +2 -0
  108. package/dist/primitives/field/index.js +4 -0
  109. package/dist/primitives/field/note.d.ts +55 -0
  110. package/dist/primitives/field/note.js +117 -0
  111. package/dist/primitives/index.d.ts +1 -0
  112. package/dist/primitives/index.js +1 -0
  113. package/dist/primitives/menu/Menu.d.ts +28 -0
  114. package/dist/primitives/menu/Menu.js +47 -17
  115. package/dist/primitives/notice/Callout.js +1 -1
  116. package/dist/primitives/notice/Flag.d.ts +13 -0
  117. package/dist/primitives/notice/Flag.js +31 -0
  118. package/dist/primitives/notice/index.d.ts +2 -1
  119. package/dist/primitives/notice/index.js +2 -1
  120. package/dist/primitives/overlay/Preview.d.ts +19 -0
  121. package/dist/primitives/overlay/Preview.js +69 -0
  122. package/dist/primitives/overlay/index.d.ts +1 -0
  123. package/dist/primitives/overlay/index.js +3 -1
  124. package/dist/primitives/picker/MatchedText.d.ts +10 -0
  125. package/dist/primitives/picker/MatchedText.js +12 -0
  126. package/dist/primitives/picker/Picker.d.ts +67 -4
  127. package/dist/primitives/picker/Picker.js +142 -23
  128. package/dist/primitives/table/CellCopy.d.ts +20 -0
  129. package/dist/primitives/table/CellCopy.js +23 -0
  130. package/dist/primitives/table/DataTable.d.ts +55 -1
  131. package/dist/primitives/table/DataTable.js +126 -40
  132. package/dist/primitives/table/EditableTable.d.ts +102 -2
  133. package/dist/primitives/table/EditableTable.js +287 -32
  134. package/dist/primitives/table/ReportTable.js +2 -1
  135. package/dist/primitives/table/RowActions.js +7 -2
  136. package/dist/primitives/table/SheetTable.d.ts +4 -1
  137. package/dist/primitives/table/SheetTable.js +38 -5
  138. package/dist/primitives/table/Table.d.ts +5 -0
  139. package/dist/primitives/table/Table.js +17 -8
  140. package/dist/primitives/table/index.d.ts +2 -2
  141. package/dist/primitives/table/index.js +1 -1
  142. package/dist/primitives/tabs/Tabs.d.ts +2 -2
  143. package/dist/primitives/tabs/Tabs.js +1 -1
  144. package/dist/primitives/tag/Tags.d.ts +2 -0
  145. package/dist/primitives/tag/Tags.js +2 -1
  146. package/dist/primitives/tag/TagsField.d.ts +13 -0
  147. package/dist/primitives/tag/TagsField.js +39 -10
  148. package/dist/primitives/tag/choices.d.ts +2 -0
  149. package/dist/primitives/tree/Tree.d.ts +56 -0
  150. package/dist/primitives/tree/Tree.js +74 -0
  151. package/dist/primitives/tree/index.d.ts +1 -0
  152. package/dist/primitives/tree/index.js +3 -0
  153. package/dist/primitives/value/Avatar.d.ts +16 -0
  154. package/dist/primitives/value/Avatar.js +91 -0
  155. package/dist/primitives/value/Copyable.d.ts +11 -0
  156. package/dist/primitives/value/Copyable.js +27 -6
  157. package/dist/primitives/value/Fecha.js +10 -1
  158. package/dist/primitives/value/Person.d.ts +6 -51
  159. package/dist/primitives/value/Person.js +9 -148
  160. package/dist/primitives/value/ProgressBar.d.ts +31 -0
  161. package/dist/primitives/value/ProgressBar.js +53 -0
  162. package/dist/primitives/value/Relation.d.ts +22 -5
  163. package/dist/primitives/value/Relation.js +42 -14
  164. package/dist/primitives/value/Totals.d.ts +102 -7
  165. package/dist/primitives/value/Totals.js +52 -2
  166. package/dist/primitives/value/Value.d.ts +0 -37
  167. package/dist/primitives/value/Value.js +0 -39
  168. package/dist/primitives/value/contact.d.ts +30 -0
  169. package/dist/primitives/value/contact.js +51 -0
  170. package/dist/primitives/value/index.d.ts +5 -4
  171. package/dist/primitives/value/index.js +5 -4
  172. package/dist/tokens.css +179 -7
  173. package/dist/views/list/List.d.ts +32 -3
  174. package/dist/views/list/List.js +51 -7
  175. package/dist/views/list/ListView.d.ts +1 -1
  176. package/dist/views/list/ListView.js +70 -13
  177. package/dist/views/list/columns.js +8 -1
  178. package/dist/views/list/derive.d.ts +12 -1
  179. package/dist/views/list/derive.js +29 -3
  180. package/dist/views/list/presentations.d.ts +5 -2
  181. package/dist/views/list/presentations.js +35 -3
  182. package/dist/views/list/types.d.ts +57 -1
  183. package/dist/views/list/useNarrowList.d.ts +10 -0
  184. package/dist/views/list/useNarrowList.js +20 -6
  185. package/dist/views/record/Record.d.ts +41 -8
  186. package/dist/views/record/Record.js +113 -28
  187. package/dist/views/record/index.d.ts +1 -1
  188. package/package.json +15 -4
  189. package/dist/components/chatter/Chatter.d.ts +0 -161
  190. package/dist/components/chatter/Chatter.js +0 -232
  191. package/dist/components/chatter/index.d.ts +0 -3
  192. package/dist/components/chatter/index.js +0 -4
  193. package/dist/components/navigation/CommandPalette.d.ts +0 -40
  194. package/dist/components/navigation/CommandPalette.js +0 -114
  195. package/dist/components/navigation/commands.d.ts +0 -30
  196. package/dist/components/navigation/commands.js +0 -30
  197. /package/dist/components/{chatter → activity}/Timeline.d.ts +0 -0
  198. /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
- onAdd?: () => void;
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 { Plus, Trash2 } from "lucide-react";
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 `firstControl`: React Aria's
40
- * hidden form mirrors (a `<select>` behind a `Select`) match this and are not stops. */
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
- const stops = (scope) => [...row.querySelectorAll(scope)].find((el) => el.tabIndex >= 0 && el.closest('[aria-hidden="true"], [inert]') === null) ?? null;
46
- return stops(`[data-edit] :is(${FOCUSABLE})`) ?? stops(FOCUSABLE);
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: one
58
- * keyboard model, two shapes.
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.key !== "Enter" || e.nativeEvent.isComposing)
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 takes the focus once
189
- the controls exist. A click opens it without moving the focus. */
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 add = addLabel ?? copy.editable.add;
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 id = focusOnOpen.current;
250
- if (id === null || id !== editing)
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 !== undefined)
255
- (firstControl(node) ?? node).focus();
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
- if (opens) {
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
- return card(entry.row);
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 text-meta font-medium whitespace-nowrap text-muted ${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", { children: rows.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: columns.length + (onRemove === undefined ? 0 : 1), className: `text-meta text-muted italic ${cell}`, children: emptyText }) })) : (drawn.map((entry) => {
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
- return (_jsx("td", { ...(control ? { "data-edit": "" } : {}), className: `text-meta text-ink ${mark} ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: control ? c.edit?.(row, set) : c.render(row) }, c.id));
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
- _jsx("div", { className: "px-1 py-0.5", children: _jsxs("button", { type: "button", onClick: onAdd, className: `flex w-full 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 ${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 }), add] }) }))] }));
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
  }