@sincpro/web-ui 0.1.0 → 0.2.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 (119) hide show
  1. package/README.md +37 -10
  2. package/dist/components/activity/Activity.d.ts +69 -0
  3. package/dist/components/activity/Activity.js +279 -0
  4. package/dist/components/{chatter → activity}/Messages.d.ts +2 -2
  5. package/dist/components/{chatter → activity}/Messages.js +9 -6
  6. package/dist/components/{chatter → activity}/index.d.ts +1 -1
  7. package/dist/components/activity/index.js +4 -0
  8. package/dist/components/assist/AssistMark.d.ts +3 -3
  9. package/dist/components/assist/AssistMark.js +1 -1
  10. package/dist/components/assist/assist.d.ts +1 -1
  11. package/dist/components/assist/assist.js +1 -1
  12. package/dist/components/form/Sheet.d.ts +4 -0
  13. package/dist/components/form/Sheet.js +11 -4
  14. package/dist/components/index.d.ts +1 -1
  15. package/dist/components/index.js +1 -1
  16. package/dist/components/lines/LineItems.d.ts +42 -5
  17. package/dist/components/lines/LineItems.js +106 -18
  18. package/dist/components/listing/Listing.js +1 -1
  19. package/dist/components/listing/ListingBar.d.ts +41 -2
  20. package/dist/components/listing/ListingBar.js +43 -4
  21. package/dist/components/listing/Row.d.ts +5 -1
  22. package/dist/components/listing/Row.js +3 -2
  23. package/dist/components/record/RecordHeader.d.ts +21 -17
  24. package/dist/components/record/RecordHeader.js +46 -14
  25. package/dist/components/record/RecordNotices.d.ts +37 -0
  26. package/dist/components/record/RecordNotices.js +75 -0
  27. package/dist/components/record/ThumbBar.js +1 -1
  28. package/dist/foundations/copy.d.ts +96 -12
  29. package/dist/foundations/copy.es.js +63 -10
  30. package/dist/foundations/copy.js +55 -7
  31. package/dist/primitives/choice/ComboBox.d.ts +3 -0
  32. package/dist/primitives/choice/ComboBox.js +2 -2
  33. package/dist/primitives/choice/SearchField.d.ts +10 -1
  34. package/dist/primitives/choice/SearchField.js +21 -2
  35. package/dist/primitives/date/Calendar.js +30 -6
  36. package/dist/primitives/date/DatePicker.d.ts +11 -3
  37. package/dist/primitives/date/DatePicker.js +12 -8
  38. package/dist/primitives/date/DateTime.d.ts +35 -0
  39. package/dist/primitives/date/DateTime.js +100 -0
  40. package/dist/primitives/date/index.d.ts +1 -0
  41. package/dist/primitives/date/index.js +4 -2
  42. package/dist/primitives/dialog/Drill.d.ts +90 -0
  43. package/dist/primitives/dialog/Drill.js +172 -0
  44. package/dist/primitives/dialog/index.d.ts +1 -0
  45. package/dist/primitives/dialog/index.js +2 -0
  46. package/dist/primitives/field/DropZone.js +1 -1
  47. package/dist/primitives/field/Field.d.ts +14 -2
  48. package/dist/primitives/field/Field.js +25 -14
  49. package/dist/primitives/field/copy.d.ts +16 -0
  50. package/dist/primitives/field/copy.js +69 -0
  51. package/dist/primitives/field/index.d.ts +2 -0
  52. package/dist/primitives/field/index.js +4 -0
  53. package/dist/primitives/field/note.d.ts +55 -0
  54. package/dist/primitives/field/note.js +117 -0
  55. package/dist/primitives/index.d.ts +1 -0
  56. package/dist/primitives/index.js +1 -0
  57. package/dist/primitives/menu/Menu.d.ts +28 -0
  58. package/dist/primitives/menu/Menu.js +47 -17
  59. package/dist/primitives/notice/Callout.js +1 -1
  60. package/dist/primitives/notice/Flag.d.ts +13 -0
  61. package/dist/primitives/notice/Flag.js +31 -0
  62. package/dist/primitives/notice/index.d.ts +1 -0
  63. package/dist/primitives/notice/index.js +1 -0
  64. package/dist/primitives/overlay/Preview.d.ts +19 -0
  65. package/dist/primitives/overlay/Preview.js +60 -0
  66. package/dist/primitives/overlay/index.d.ts +1 -0
  67. package/dist/primitives/overlay/index.js +3 -1
  68. package/dist/primitives/picker/Picker.d.ts +41 -0
  69. package/dist/primitives/picker/Picker.js +62 -8
  70. package/dist/primitives/table/CellCopy.d.ts +20 -0
  71. package/dist/primitives/table/CellCopy.js +23 -0
  72. package/dist/primitives/table/DataTable.d.ts +55 -1
  73. package/dist/primitives/table/DataTable.js +126 -40
  74. package/dist/primitives/table/EditableTable.d.ts +97 -2
  75. package/dist/primitives/table/EditableTable.js +223 -29
  76. package/dist/primitives/table/SheetTable.d.ts +4 -1
  77. package/dist/primitives/table/SheetTable.js +38 -5
  78. package/dist/primitives/table/index.d.ts +2 -2
  79. package/dist/primitives/table/index.js +1 -1
  80. package/dist/primitives/tabs/Tabs.d.ts +2 -2
  81. package/dist/primitives/tabs/Tabs.js +1 -1
  82. package/dist/primitives/tag/TagsField.d.ts +3 -0
  83. package/dist/primitives/tag/TagsField.js +6 -2
  84. package/dist/primitives/tree/Tree.d.ts +56 -0
  85. package/dist/primitives/tree/Tree.js +74 -0
  86. package/dist/primitives/tree/index.d.ts +1 -0
  87. package/dist/primitives/tree/index.js +3 -0
  88. package/dist/primitives/value/Copyable.d.ts +11 -0
  89. package/dist/primitives/value/Copyable.js +27 -6
  90. package/dist/primitives/value/Fecha.js +10 -1
  91. package/dist/primitives/value/ProgressBar.d.ts +31 -0
  92. package/dist/primitives/value/ProgressBar.js +53 -0
  93. package/dist/primitives/value/Relation.d.ts +6 -1
  94. package/dist/primitives/value/Relation.js +27 -10
  95. package/dist/primitives/value/Totals.d.ts +102 -7
  96. package/dist/primitives/value/Totals.js +52 -2
  97. package/dist/primitives/value/Value.d.ts +0 -37
  98. package/dist/primitives/value/Value.js +0 -39
  99. package/dist/primitives/value/index.d.ts +4 -3
  100. package/dist/primitives/value/index.js +4 -3
  101. package/dist/tokens.css +44 -7
  102. package/dist/views/list/List.d.ts +8 -2
  103. package/dist/views/list/List.js +6 -2
  104. package/dist/views/list/ListView.d.ts +1 -1
  105. package/dist/views/list/ListView.js +34 -8
  106. package/dist/views/list/derive.d.ts +12 -1
  107. package/dist/views/list/derive.js +29 -3
  108. package/dist/views/list/presentations.d.ts +2 -2
  109. package/dist/views/list/presentations.js +5 -3
  110. package/dist/views/list/types.d.ts +13 -1
  111. package/dist/views/record/Record.d.ts +27 -8
  112. package/dist/views/record/Record.js +97 -27
  113. package/dist/views/record/index.d.ts +1 -1
  114. package/package.json +5 -1
  115. package/dist/components/chatter/Chatter.d.ts +0 -161
  116. package/dist/components/chatter/Chatter.js +0 -232
  117. package/dist/components/chatter/index.js +0 -4
  118. /package/dist/components/{chatter → activity}/Timeline.d.ts +0 -0
  119. /package/dist/components/{chatter → activity}/Timeline.js +0 -0
@@ -6,9 +6,27 @@ 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 { NoteCard } from "../field/note.js";
10
+ import { Flag } from "../notice/Flag.js";
9
11
  import { Plus, Trash2 } from "lucide-react";
10
12
  import { Fragment, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
11
13
  import { useKeyboard } from "react-aria";
14
+ /** The lines each section heads: the rows after it up to the next section. Notes do not
15
+ * cut a section, and are not lines. */
16
+ export function sectionLines(rows, getRowId, kind) {
17
+ const out = new Map();
18
+ let current = null;
19
+ for (const row of rows) {
20
+ const k = kind(row);
21
+ if (k === "section") {
22
+ current = [];
23
+ out.set(getRowId(row), current);
24
+ }
25
+ else if (k === null)
26
+ current?.push(row);
27
+ }
28
+ return out;
29
+ }
12
30
  /**
13
31
  * Where each column goes in the stacked card, defaults resolved: a declared `stack`
14
32
  * wins; otherwise the first undeclared column is the title (unless one was declared),
@@ -36,14 +54,47 @@ function pileTag(pile, locale) {
36
54
  /** The focus ring of an operable row: drawn on the row, inset, so it is not cut by the
37
55
  * box and does not move the cells. */
38
56
  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. */
57
+ /** What can take the focus in a row. Filtered again in `stopsIn`: React Aria's hidden form
58
+ * mirrors (a `<select>` behind a `Select`, an `<input hidden>` in a date picker) match this
59
+ * and are not stops. */
41
60
  const FOCUSABLE = 'input:not([type="hidden"]):not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), a[href], [tabindex]';
61
+ /** What `Tab` stops on inside `scope` of a row: focusable, and not a hidden form mirror.
62
+ *
63
+ * Context: NOT filtered by `aria-hidden` / `inert`. While a list is open (a tags field, a
64
+ * picker) React Aria hides everything outside it that way — the rest of the row included —
65
+ * and the row then looked as if its open control were its last: `Tab` added a new row from
66
+ * the middle of this one. The mirrors carry their own marks instead. */
67
+ function stopsIn(row, scope) {
68
+ return [...row.querySelectorAll(scope)].filter((el) => el.tabIndex >= 0 && el.closest("[hidden], [data-react-aria-prevent-focus]") === null);
69
+ }
70
+ /** The controls of the editable cells of a row, in tab order. */
71
+ function editStops(row) {
72
+ return stopsIn(row, `[data-edit] :is(${FOCUSABLE})`);
73
+ }
42
74
  /** The first control of a row being edited: the first one in an editable cell, else
43
75
  * 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);
76
+ export function firstControl(row) {
77
+ return editStops(row)[0] ?? stopsIn(row, FOCUSABLE)[0] ?? null;
78
+ }
79
+ /** The last control of a row being edited — where `Shift+Tab` from the next row lands. */
80
+ function lastControl(row) {
81
+ return editStops(row).at(-1) ?? stopsIn(row, FOCUSABLE).at(-1) ?? null;
82
+ }
83
+ /**
84
+ * Where `Tab` past the last control (`next`) or `Shift+Tab` before the first (`prev`) of
85
+ * the edited row `from` goes, over the rows in the order they are drawn.
86
+ *
87
+ * Past the last row it adds one when the table can — unless `from` is itself a row just
88
+ * added and still blank: one more `Tab` there means «I am done», not «another blank line».
89
+ */
90
+ export function tabTarget(order, from, direction, { canAdd, blank }) {
91
+ const at = order.indexOf(from);
92
+ if (at === -1)
93
+ return { kind: "leave" };
94
+ const to = order[direction === "next" ? at + 1 : at - 1];
95
+ if (to !== undefined)
96
+ return { kind: "row", id: to };
97
+ return direction === "next" && canAdd && !blank ? { kind: "add" } : { kind: "leave" };
47
98
  }
48
99
  /** Keys a control inside the row already answers with `Enter`: a button presses, a
49
100
  * link follows, a text area breaks the line, an open list picks its option. */
@@ -54,10 +105,11 @@ function ownsEnter(target) {
54
105
  /**
55
106
  * One body row. Its own component so that it can hold React Aria's `useKeyboard`
56
107
  * (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.
108
+ * The same row is a `<tr>` in the table and an `<li>` in the stacked list — and the row
109
+ * of a new record in `DataTable`: one keyboard model, three shapes. Exported for that
110
+ * family sibling only; it is not in the barrel.
59
111
  */
60
- function EditRow({ as, rowId, rowRef, editable, editing, tabbable, className, onStart, onCommit, onCancel, onMove, onFocusRow, children, }) {
112
+ export function EditRow({ as, rowId, rowRef, editable, editing, tabbable, className, onStart, onCommit, onCancel, onMove, onFocusRow, onTabOut, children, }) {
61
113
  const leaving = useRef(null);
62
114
  const stay = () => {
63
115
  if (leaving.current !== null)
@@ -108,10 +160,23 @@ function EditRow({ as, rowId, rowRef, editable, editing, tabbable, className, on
108
160
  that field write the value it was holding (it commits on blur). */
109
161
  onKeyDownCapture: editing
110
162
  ? (e) => {
111
- if (e.key !== "Enter" || e.nativeEvent.isComposing)
163
+ if (e.nativeEvent.isComposing)
112
164
  return;
113
165
  if (!(e.target instanceof Node) || !e.currentTarget.contains(e.target))
114
166
  return;
167
+ if (e.key === "Tab") {
168
+ /* Only the default is prevented, never the propagation: a combobox still
169
+ hears its `Tab` and takes the option it has marked before the row moves. */
170
+ const stops = editStops(e.currentTarget);
171
+ const edge = e.shiftKey ? stops[0] : stops.at(-1);
172
+ if (edge === undefined || !edge.contains(e.target))
173
+ return;
174
+ if (onTabOut(e.shiftKey ? "prev" : "next"))
175
+ e.preventDefault();
176
+ return;
177
+ }
178
+ if (e.key !== "Enter")
179
+ return;
115
180
  if (ownsEnter(e.target))
116
181
  return;
117
182
  e.preventDefault();
@@ -174,8 +239,32 @@ totals, empty, grouping,
174
239
  /** El total de una columna DENTRO de un montón, ya formateado. Lo calcula quien
175
240
  * compone, por el mismo motivo que `totals`: sumar significa cosas distintas
176
241
  * según el dominio, y un porcentaje se promedia, no se suma. */
177
- groupTotal, height, layout = "auto", onOpen, }) {
242
+ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, }) {
178
243
  const copy = useUiCopy();
244
+ const bandOf = (row) => bands?.kind(row) ?? null;
245
+ /* The row's mark, before its first value — and, in a record's margin, its card. */
246
+ const flagOf = (row, withCard = false) => {
247
+ const said = flag?.(row) ?? null;
248
+ if (said === null)
249
+ return null;
250
+ 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: {
251
+ tone: said.tone,
252
+ label: said.label,
253
+ ...(said.detail === undefined ? {} : { detail: said.detail }),
254
+ } })) : null] }));
255
+ };
256
+ const underSection = useMemo(() => (bands === undefined ? null : sectionLines(rows, getRowId, bands.kind)), [rows, bands, getRowId]);
257
+ /* Where a band's text stops: the first column with something of its own on that row. */
258
+ const bandFigures = (row) => {
259
+ const lines = underSection?.get(getRowId(row)) ?? null;
260
+ const figures = columns.map((c) => c.onBands === true
261
+ ? c.render(row)
262
+ : lines === null
263
+ ? null
264
+ : (bands?.total?.(c, lines) ?? null));
265
+ const first = figures.findIndex((f, i) => columns[i]?.onBands === true || (f !== null && f !== undefined));
266
+ return { span: first <= 0 ? columns.length : first, figures };
267
+ };
179
268
  const d = useDensity();
180
269
  const box = useScrollBox(height === undefined ? {} : { height });
181
270
  const [editing, setEditing] = useState(null);
@@ -185,9 +274,13 @@ groupTotal, height, layout = "auto", onOpen, }) {
185
274
  /* The row as it was when editing began, to put it back on `Escape`; `touched` says
186
275
  whether any control wrote to it since, so a cancel without changes changes nothing. */
187
276
  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. */
277
+ /* Set when the keyboard opened the row: its first control —or its last, coming back
278
+ with `Shift+Tab`— takes the focus once the controls exist. A click opens it without
279
+ moving the focus. */
190
280
  const focusOnOpen = useRef(null);
281
+ /* The id `onAdd` returned, until that row shows up in `rows` and can be opened. */
282
+ const [arriving, setArriving] = useState(null);
283
+ const addButton = useRef(null);
191
284
  /* La secuencia que se dibuja: montones y filas intercalados. Sin agrupado, las
192
285
  filas tal cual — una tabla que no agrupa no paga nada por poder hacerlo. */
193
286
  const drawn = useMemo(() => {
@@ -240,35 +333,82 @@ groupTotal, height, layout = "auto", onOpen, }) {
240
333
  const opens = stacked && onOpen !== undefined;
241
334
  const operable = editable || opens;
242
335
  const cell = byDensity(d, "px-3 py-1.5", "px-2 py-1");
243
- const add = addLabel ?? copy.editable.add;
336
+ const addText = addLabel ?? copy.editable.add;
244
337
  const emptyText = empty ?? copy.editable.empty;
245
338
  /* The rows in the order they are drawn: what the arrows walk. */
246
339
  const order = drawn.flatMap((entry) => entry.row === undefined ? [] : [getRowId(entry.row)]);
247
340
  const stop = active !== null && order.includes(active) ? active : (order[0] ?? null);
248
341
  useEffect(() => {
249
- const id = focusOnOpen.current;
250
- if (id === null || id !== editing)
342
+ const target = focusOnOpen.current;
343
+ if (target === null || target.id !== editing)
251
344
  return;
252
345
  focusOnOpen.current = null;
253
- const node = rowNodes.current.get(id);
254
- if (node !== undefined)
255
- (firstControl(node) ?? node).focus();
346
+ const node = rowNodes.current.get(target.id);
347
+ if (node === undefined)
348
+ return;
349
+ const control = target.at === "last" ? lastControl(node) : firstControl(node);
350
+ (control ?? node).focus();
256
351
  }, [editing]);
257
- const start = (id, row, viaKeyboard) => {
258
- if (opens) {
352
+ const start = (id, row, viaKeyboard, { at = "first", added = false } = {}) => {
353
+ /* A row just added is written where it appeared, card or not: it was added in place
354
+ precisely so that it would not open somewhere else. */
355
+ if (opens && !added && bandOf(row) === null) {
259
356
  setActive(id);
260
357
  onOpen?.(id);
261
358
  return;
262
359
  }
263
- before.current = { id, row, touched: false };
264
- focusOnOpen.current = viaKeyboard ? id : null;
360
+ before.current = { id, row, touched: false, added };
361
+ focusOnOpen.current = viaKeyboard ? { id, at } : null;
265
362
  setActive(id);
266
363
  setEditing(id);
267
364
  };
365
+ /* The added row is opened the render it appears in — which may be this one, or the one
366
+ after the backend answered. */
367
+ useEffect(() => {
368
+ if (arriving === null)
369
+ return;
370
+ const row = rows.find((r) => getRowId(r) === arriving);
371
+ if (row === undefined)
372
+ return;
373
+ setArriving(null);
374
+ start(arriving, row, true, { added: true });
375
+ /* `start` and `getRowId` are new every render; what decides is the id and the rows. */
376
+ // eslint-disable-next-line react-hooks/exhaustive-deps
377
+ }, [arriving, rows]);
378
+ const add = (maker = onAdd) => {
379
+ if (maker === undefined)
380
+ return;
381
+ const made = maker();
382
+ if (typeof made === "string")
383
+ setArriving(made);
384
+ else if (made instanceof Promise) {
385
+ void made.then((id) => {
386
+ if (typeof id === "string")
387
+ setArriving(id);
388
+ });
389
+ }
390
+ };
268
391
  /* Keyed by the row: a commit that arrives late (the blur of the row you clicked
269
392
  away from) must not close the row the click just opened. */
270
393
  const close = (id) => setEditing((current) => (current === id ? null : current));
394
+ /* A row added and abandoned goes away. `true` when it did. */
395
+ const discard = (id, untouchedOnly) => {
396
+ const was = before.current;
397
+ if (was === null || was.id !== id || !was.added || onDiscard === undefined)
398
+ return false;
399
+ if (untouchedOnly && was.touched)
400
+ return false;
401
+ before.current = null;
402
+ close(id);
403
+ onDiscard(id);
404
+ return true;
405
+ };
271
406
  const commit = (id, refocus) => {
407
+ if (discard(id, true)) {
408
+ if (refocus)
409
+ addButton.current?.focus();
410
+ return;
411
+ }
272
412
  if (refocus)
273
413
  rowNodes.current.get(id)?.focus();
274
414
  if (before.current?.id === id)
@@ -280,12 +420,34 @@ groupTotal, height, layout = "auto", onOpen, }) {
280
420
  (a number field) writes it now, and the old row is put back after it, not
281
421
  before. */
282
422
  rowNodes.current.get(id)?.focus();
423
+ if (discard(id, false)) {
424
+ /* The row is gone with the focus in it: «add» is where it came from. */
425
+ addButton.current?.focus();
426
+ return;
427
+ }
283
428
  const was = before.current;
284
429
  before.current = null;
285
430
  if (was !== null && was.id === id && was.touched)
286
431
  onChange?.(id, was.row);
287
432
  close(id);
288
433
  };
434
+ const tabOut = (from, direction) => {
435
+ const was = before.current;
436
+ const blank = was !== null && was.id === from && was.added && !was.touched;
437
+ const target = tabTarget(order, from, direction, { canAdd: onAdd !== undefined, blank });
438
+ if (target.kind === "leave")
439
+ return false;
440
+ commit(from, false);
441
+ if (target.kind === "add") {
442
+ add();
443
+ return true;
444
+ }
445
+ const row = rows.find((r) => getRowId(r) === target.id);
446
+ if (row === undefined)
447
+ return false;
448
+ start(target.id, row, true, { at: direction === "next" ? "first" : "last" });
449
+ return true;
450
+ };
289
451
  /* What a control writes, marking the row as touched for `Escape`. */
290
452
  const setter = (id) => (next) => {
291
453
  if (before.current?.id === id)
@@ -309,6 +471,7 @@ groupTotal, height, layout = "auto", onOpen, }) {
309
471
  onCancel: () => cancel(id),
310
472
  onMove: (key) => move(id, key),
311
473
  onFocusRow: () => setActive(id),
474
+ onTabOut: (direction) => tabOut(id, direction),
312
475
  });
313
476
  const move = (from, key) => {
314
477
  const at = order.indexOf(from);
@@ -349,7 +512,7 @@ groupTotal, height, layout = "auto", onOpen, }) {
349
512
  ];
350
513
  });
351
514
  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) => (
515
+ 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
516
  /* The amount is read by its figure on screen and by its name aloud: a
354
517
  lone «Bs. 1.296,00» at the end of a card does not say what it is. */
355
518
  _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 +521,17 @@ groupTotal, height, layout = "auto", onOpen, }) {
358
521
  guess. The name is drawn for the eye; the control carries its own label. */
359
522
  _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
523
  };
524
+ /* A section or a note on a phone: the band and, for a section, what it adds up to in
525
+ the card's amount; editing, its control under it. */
526
+ const bandCard = (row) => {
527
+ const id = getRowId(row);
528
+ const band = bandOf(row);
529
+ const isEditing = editing === id;
530
+ const lines = underSection?.get(id) ?? null;
531
+ 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
532
+ ? null
533
+ : 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));
534
+ };
361
535
  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
536
  if (entry.pile !== undefined) {
363
537
  const pile = entry.pile;
@@ -371,7 +545,8 @@ groupTotal, height, layout = "auto", onOpen, }) {
371
545
  }
372
546
  : 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
547
  }
374
- return card(entry.row);
548
+ const row = entry.row;
549
+ return bandOf(row) === null ? card(row) : bandCard(row);
375
550
  })) }), totals === undefined ? null : (
376
551
  /* The footer's aggregates, one per line with the column's name: under a card
377
552
  there is no column to stand under. */
@@ -401,6 +576,22 @@ groupTotal, height, layout = "auto", onOpen, }) {
401
576
  const row = entry.row;
402
577
  const id = getRowId(row);
403
578
  const isEditing = editing === id;
579
+ const band = bandOf(row);
580
+ if (band !== null) {
581
+ const { span, figures } = bandFigures(row);
582
+ const bandWash = isEditing
583
+ ? "bg-primary/5"
584
+ : band === "section"
585
+ ? "bg-ground group-hover/row:bg-ink/6"
586
+ : "group-hover/row:bg-ink/4";
587
+ const bandMark = `border-b border-line first:rounded-l-control last:rounded-r-control ${bandWash}`;
588
+ const control = isEditing && editable && bands?.edit !== undefined;
589
+ return (_jsxs(EditRow, { as: "tr", ...rowWiring(id, row), className: `group/row ${editable ? RING : ""} ${editable && !isEditing ? "cursor-text" : ""}`, children: [_jsx("td", { colSpan: span, ...(control ? { "data-edit": "" } : {}), className: `text-meta ${bandMark} ${cell} ${band === "section"
590
+ ? "font-semibold text-ink"
591
+ : "text-muted italic"}`, children: control ? bands?.edit?.(row, setter(id)) : bands?.render(row) }), columns.slice(span).map((c, i) => (_jsx("td", { className: `text-meta text-ink ${bandMark} ${cell} ${c.numeric === true
592
+ ? "text-right font-semibold tabular-nums"
593
+ : ""}`, 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));
594
+ }
404
595
  /* The wash lives on the cells. A background on the row paints
405
596
  edge to edge, and `border-collapse` would ignore a radius. */
406
597
  const wash = isEditing ? "bg-primary/5" : "group-hover/row:bg-ink/4";
@@ -409,15 +600,18 @@ groupTotal, height, layout = "auto", onOpen, }) {
409
600
  return (_jsxs(EditRow, { as: "tr", ...rowWiring(id, row),
410
601
  /* The focus ring is drawn on the row, inset, so it is not cut by
411
602
  the box and does not move the cells. */
412
- className: `group/row ${editable ? RING : ""} ${editable && !isEditing ? "cursor-text" : ""}`, children: [columns.map((c) => {
603
+ className: `group/row ${editable ? RING : ""} ${editable && !isEditing ? "cursor-text" : ""}`, children: [columns.map((c, ci) => {
413
604
  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));
605
+ const mark = ci === 0 ? flagOf(row, true) : null;
606
+ return (_jsx("td", { ...(control ? { "data-edit": "" } : {}), className: `text-meta text-ink ${mark} ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: mark === null ? (control ? (c.edit?.(row, set)) : (c.render(row))) : (_jsxs("span", { className: "flex min-w-0 items-start gap-1.5", children: [mark, _jsx("span", { className: "min-w-0 flex-1", children: control ? c.edit?.(row, set) : c.render(row) })] })) }, c.id));
415
607
  }), 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 : (
608
+ })) }), totals === undefined ? null : (_jsx("tfoot", { children: _jsxs("tr", { children: [columns.map((c, i) => (_jsx("td", { className: `border-t border-line-strong text-meta font-semibold text-ink ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: totals[i] ?? null }, c.id))), onRemove === undefined ? null : (_jsx("td", { className: "border-t border-line-strong" }))] }) }))] })) }), onAdd === undefined && (adds === undefined || adds.length === 0) ? null : (
417
609
  /* The «add» goes under the grid, as text, not as a button away from it:
418
610
  what is about to happen is that one more row shows up right here.
419
611
  The hover is the same inset mark as a row: the shell lines the label
420
612
  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] }) }))] }));
613
+ outside the scroll box, so a table with its own height keeps it in view.
614
+ A section and a note sit beside it, quieter: the line is what is added
615
+ most, so it keeps the lead and the whole free width. */
616
+ _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
617
  }
@@ -20,7 +20,7 @@ value,
20
20
  * nothing. */
21
21
  tone,
22
22
  /** What the cell says on hover: «imputado por la mediana». */
23
- note, height, rowLabel, band, onBandToggle, caption, onCommit, editable, }: {
23
+ note, height, rowLabel, band, onBandToggle, caption, onCommit, editable, onAddRow, }: {
24
24
  columns: readonly SheetColumn[];
25
25
  rows: number;
26
26
  value: (row: number, column: SheetColumn, columnIndex: number) => string | number | null;
@@ -67,4 +67,7 @@ note, height, rowLabel, band, onBandToggle, caption, onCommit, editable, }: {
67
67
  /** Which cells are editable. Without this, all of them — but a computed column
68
68
  * or an identifier is not touched. */
69
69
  editable?: (row: number, column: SheetColumn, columnIndex: number) => boolean;
70
+ /** Present = rows can be added (see the file's header). Only with `onCommit`: a row
71
+ * that cannot be written in is not worth adding. */
72
+ onAddRow?: () => void;
70
73
  }): import("react").JSX.Element;
@@ -4,8 +4,8 @@ import { useUiCopy } from "../../foundations/copy.js";
4
4
  import { stickyTop, useScrollBox } from "../../foundations/sticky.js";
5
5
  import { Cell } from "../value/Value.js";
6
6
  import { useVirtualizer } from "@tanstack/react-virtual";
7
- import { ChevronRight } from "lucide-react";
8
- import { useState } from "react";
7
+ import { ChevronRight, Plus } from "lucide-react";
8
+ import { useEffect, useState } from "react";
9
9
  const TONE = {
10
10
  neutral: "bg-ink/6",
11
11
  info: "bg-info/12",
@@ -26,7 +26,7 @@ value,
26
26
  * nothing. */
27
27
  tone,
28
28
  /** What the cell says on hover: «imputado por la mediana». */
29
- note, height = 420, rowLabel, band, onBandToggle, caption, onCommit, editable, }) {
29
+ note, height = 420, rowLabel, band, onBandToggle, caption, onCommit, editable, onAddRow, }) {
30
30
  const copy = useUiCopy();
31
31
  const labelOf = rowLabel ?? ((i) => (i + 1).toLocaleString(copy.locale));
32
32
  /* `"auto"` es para un conjunto que ya vino CORTADO —una página de ochenta— y
@@ -78,6 +78,27 @@ note, height = 420, rowLabel, band, onBandToggle, caption, onCommit, editable, }
78
78
  if (dr !== 0 || dc !== 0)
79
79
  goTo(active.r + dr, active.c + dc);
80
80
  }
81
+ /* The column the active cell waits in until the added row arrives, and how many rows
82
+ there were when it was asked for. */
83
+ const canAdd = isEditable && onAddRow !== undefined;
84
+ const [arriving, setArriving] = useState(null);
85
+ function addRow(c) {
86
+ if (!canAdd)
87
+ return;
88
+ setArriving({ c, from: rows });
89
+ onAddRow?.();
90
+ }
91
+ useEffect(() => {
92
+ if (arriving === null || rows <= arriving.from)
93
+ return;
94
+ setArriving(null);
95
+ setActive({ r: rows - 1, c: arriving.c });
96
+ setDraft(null);
97
+ v.scrollToIndex(rows - 1);
98
+ caja.ref.current?.focus();
99
+ /* `v` and `caja` are new objects every render; the row count is what decides. */
100
+ // eslint-disable-next-line react-hooks/exhaustive-deps
101
+ }, [rows, arriving]);
81
102
  /* All the keyboard handling in one place. Splitting it between the box and
82
103
  each cell is how you end up with spreadsheets whose arrows sometimes move
83
104
  and sometimes scroll. */
@@ -92,7 +113,12 @@ note, height = 420, rowLabel, band, onBandToggle, caption, onCommit, editable, }
92
113
  }
93
114
  else if (e.key === "Enter") {
94
115
  e.preventDefault();
95
- commit(e.shiftKey ? -1 : 1, 0);
116
+ if (!e.shiftKey && canAdd && r === rows - 1) {
117
+ commit(0, 0);
118
+ addRow(c);
119
+ }
120
+ else
121
+ commit(e.shiftKey ? -1 : 1, 0);
96
122
  }
97
123
  else if (e.key === "Tab") {
98
124
  e.preventDefault();
@@ -192,6 +218,13 @@ note, height = 420, rowLabel, band, onBandToggle, caption, onCommit, editable, }
192
218
  ? "cursor-cell"
193
219
  : ""}`, children: isEditingHere ? (_jsx("input", { autoFocus: true, value: draft, onChange: (ev) => setDraft(ev.target.value), onBlur: () => commit(0, 0), className: `absolute inset-0 w-full bg-surface px-2.5 text-meta tabular-nums text-ink outline-none ${c.numeric === true ? "text-right" : "text-left"}` })) : (_jsx(Cell, { value: value(item.index, c, ci), max: 24 })) }, c.id));
194
220
  }), _jsx("div", { className: "flex-1" })] }, item.key));
195
- }) })] }) })] }));
221
+ }) })] }) }), canAdd ? (
222
+ /* Under the grid and outside its scroll box, like the editable table's «add»: in
223
+ view however far down a long sheet is scrolled. The new row's first cell that
224
+ can be written takes the place. */
225
+ _jsx("div", { className: "px-1 py-0.5", children: _jsxs("button", { type: "button", onClick: () => {
226
+ const first = columns.findIndex((_, ci) => canEdit(rows, ci));
227
+ addRow(first === -1 ? 0 : first);
228
+ }, className: "flex w-full items-center gap-1.5 rounded-control px-2 py-0.5 text-left text-meta text-primary transition-colors duration-(--duration-instant) hover:bg-primary/5 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary", children: [_jsx(Plus, { className: "size-(--size-icon-sm)", strokeWidth: 2.5, "aria-hidden": true }), copy.editable.addRow] }) })) : null] }));
196
229
  }
197
230
  const ROW_LABEL_WIDTH = 64;
@@ -1,5 +1,5 @@
1
- export { type Aggregate, type DataColumn, DataTable, type GroupColumn, } from "./DataTable.js";
2
- export { EditableTable, type EditColumn, type EditLayout, type StackPlace, stackPlaces, } from "./EditableTable.js";
1
+ export { type Aggregate, type DataColumn, DataTable, type DataTableCreate, type GroupColumn, } from "./DataTable.js";
2
+ export { EditableTable, type EditAdd, type EditBands, type EditColumn, type EditLayout, sectionLines, type StackPlace, stackPlaces, type TabTarget, tabTarget, } from "./EditableTable.js";
3
3
  export { PivotTable, type PivotTableProps, } from "./PivotTable.js";
4
4
  export { type ReportColumn, type ReportRow, type ReportRowKind, ReportTable, } from "./ReportTable.js";
5
5
  export { type RowAction, RowActions } from "./RowActions.js";
@@ -1,7 +1,7 @@
1
1
  /* The presentation (`Table`) is ours and is used on its own; `DataTable` is the
2
2
  wiring to TanStack for grouping and sorting. See the note in `DataTable.tsx`. */
3
3
  export { DataTable, } from "./DataTable.js";
4
- export { EditableTable, stackPlaces, } from "./EditableTable.js";
4
+ export { EditableTable, sectionLines, stackPlaces, tabTarget, } from "./EditableTable.js";
5
5
  export { PivotTable, } from "./PivotTable.js";
6
6
  export { ReportTable, } from "./ReportTable.js";
7
7
  export { RowActions } from "./RowActions.js";
@@ -4,7 +4,7 @@ import type { LucideIcon } from "lucide-react";
4
4
  * UNA TIRA DE PESTAÑAS. La misma en todo el sistema.
5
5
  *
6
6
  * Context: esto vivía adentro del encabezado del registro y se llamaba
7
- * `Aspects`, así que el chatter —que necesita exactamente lo mismo— no podía
7
+ * `Aspects`, así que la actividad —que necesita exactamente lo mismo— no podía
8
8
  * usarlo: un contexto no importa de otro contexto. El resultado era una tira de
9
9
  * pestañas de segunda mano, con el activo marcado de otra manera, en la misma
10
10
  * pantalla que la buena.
@@ -76,7 +76,7 @@ export type TabItem = {
76
76
  notice?: TabNotice;
77
77
  };
78
78
  /**
79
- * The strip only: what each tab opens is drawn by whoever uses it (`Record`, `Chatter`).
79
+ * The strip only: what each tab opens is drawn by whoever uses it (`Record`, `Activity`).
80
80
  *
81
81
  * Built on React Aria's `Tabs`, so the strip is a real tab list for the keyboard: the
82
82
  * arrows move between tabs and select them, Home/End jump to the ends, and only the open
@@ -22,7 +22,7 @@ const NOTICE_DOT = {
22
22
  accent: "bg-accent",
23
23
  };
24
24
  /**
25
- * The strip only: what each tab opens is drawn by whoever uses it (`Record`, `Chatter`).
25
+ * The strip only: what each tab opens is drawn by whoever uses it (`Record`, `Activity`).
26
26
  *
27
27
  * Built on React Aria's `Tabs`, so the strip is a real tab list for the keyboard: the
28
28
  * arrows move between tabs and select them, Home/End jump to the ends, and only the open
@@ -20,6 +20,9 @@ export type TagsFieldProps = {
20
20
  id?: string;
21
21
  describedBy?: string;
22
22
  invalid?: boolean;
23
+ /** The name of the control when it is NOT inside a `Field` — a cell of an editable table.
24
+ * Inside a field, the field's label names it. */
25
+ "aria-label"?: string;
23
26
  /** Forces the mode. By default it inherits the form's: in read mode it draws `Tags`. */
24
27
  mode?: FormMode;
25
28
  };
@@ -70,7 +70,7 @@ export function TagsField({ mode, ...props }) {
70
70
  to be ABOVE the component that asks for it. */
71
71
  return (_jsx(AriaLocale, { children: _jsx(EditableTagsField, { ...props }) }));
72
72
  }
73
- function EditableTagsField({ value, onChange, options, onCreate, placeholder, disabled = false, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, }) {
73
+ function EditableTagsField({ value, onChange, options, onCreate, placeholder, disabled = false, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, "aria-label": ariaLabel, }) {
74
74
  const { id, labelledBy, describedBy, invalid } = useFieldIds({
75
75
  id: ownId,
76
76
  describedBy: ownDescribedBy,
@@ -132,7 +132,11 @@ function EditableTagsField({ value, onChange, options, onCreate, placeholder, di
132
132
  /* The box grows with the chips: five categories wrap to a second line instead of
133
133
  scrolling out of sight. */
134
134
  className: "h-auto min-h-(--size-control-md) flex-wrap gap-1 py-1 pl-1.5",
135
- }), children: [chips.length === 0 ? null : (_jsx(Tags, { items: chips, ...(labelledBy === undefined ? {} : { labelledBy }) })), _jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-describedby": describedBy, items: listed, selectedKey: null, onSelectionChange: commit, inputValue: text, onInputChange: setText,
135
+ }), children: [chips.length === 0 ? null : (_jsx(Tags, { items: chips, ...(labelledBy === undefined
136
+ ? ariaLabel === undefined
137
+ ? {}
138
+ : { label: ariaLabel }
139
+ : { labelledBy }) })), _jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, items: listed, selectedKey: null, onSelectionChange: commit, inputValue: text, onInputChange: setText,
136
140
  /* Arriving at the field shows what can still be added, the way a many2one opens its
137
141
  list on focus. With every tag chosen and no text there is nothing to show, and the
138
142
  list stays closed. */
@@ -0,0 +1,56 @@
1
+ import { type LucideIcon } from "lucide-react";
2
+ /** One record of the hierarchy. With `children` it is a group that folds. */
3
+ export interface TreeItem {
4
+ id: string;
5
+ label: string;
6
+ /** What reads beside the name, at the right, in tabular figures: a balance, a stock, a
7
+ * count. Already formatted — the tree does not know a currency from a unit. */
8
+ meta?: string;
9
+ icon?: LucideIcon;
10
+ children?: readonly TreeItem[];
11
+ /** It cannot be chosen — a view account that groups others and takes no entries. It still
12
+ * folds and opens. */
13
+ disabled?: boolean;
14
+ }
15
+ /** One name or the other: a tree with no name is announced as a lone «tree». */
16
+ type TreeName = {
17
+ /** The accessible name: «Plan de cuentas». */
18
+ label: string;
19
+ labelledBy?: never;
20
+ } | {
21
+ /** The id of a visible heading that names the tree — the label of the `Field` it lives
22
+ * in. */
23
+ labelledBy: string;
24
+ label?: never;
25
+ };
26
+ export type TreeProps = TreeName & {
27
+ items: readonly TreeItem[];
28
+ /** The groups open right now. Pass it with `onExpandedChange` to own the expansion. */
29
+ expanded?: readonly string[];
30
+ /** The groups that start open, when the tree owns its expansion. */
31
+ defaultExpanded?: readonly string[];
32
+ onExpandedChange?: (ids: string[]) => void;
33
+ /** `none` reads; `single` chooses one record (a category, a location); `multiple` a set. */
34
+ selectionMode?: "none" | "single" | "multiple";
35
+ /** The chosen ids — a list in both modes, like every other control, so a screen does not
36
+ * switch shapes when one becomes many. */
37
+ selected?: readonly string[];
38
+ defaultSelected?: readonly string[];
39
+ onSelectedChange?: (ids: string[]) => void;
40
+ /** Open the record: Enter, or a click (a double click when the tree chooses one). Without
41
+ * it, a click on a group folds or opens it. */
42
+ onAction?: (id: string) => void;
43
+ /** What an empty tree says. By default, the copy table's `tree.empty`. */
44
+ empty?: string;
45
+ };
46
+ /**
47
+ * A hierarchy of records — the chart of accounts, product categories, warehouse locations —
48
+ * with its groups that fold, a figure per row, and one or many rows to choose.
49
+ *
50
+ * Context: the keyboard is React Aria's `Tree`: arrows walk the visible rows, → opens a group
51
+ * and ← folds it (or goes up to the parent), Home and End jump, and typing a name moves to it.
52
+ * Escape does NOT clear the selection: the tree often lives in a dialog or a popover, and
53
+ * Escape there means «close» — clearing the choice on the way out would lose it without a word.
54
+ */
55
+ export declare function Tree({ items, label, labelledBy, expanded, defaultExpanded, onExpandedChange, selectionMode, selected, defaultSelected, onSelectedChange, onAction, empty, }: TreeProps): import("react").JSX.Element;
56
+ export {};