@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
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { currentCopyLocale, fillCopy, useUiCopy } from "../../foundations/copy.js";
4
4
  import { byDensity, useDensity } from "../../foundations/density.js";
5
5
  import { stickyTop, useScrollBox } from "../../foundations/sticky.js";
6
+ import { HEADER_TEXT } from "./Table.js";
6
7
  import { ChevronRight, Info } from "lucide-react";
7
8
  import { useState } from "react";
8
9
  /* `useGrouping: "always"` and not the default: without it, four-digit numbers
@@ -102,5 +103,5 @@ defaultOpen, caption, height, }) {
102
103
  desplazamiento de la barra y quedaba empujado 68 px hacia adentro de su
103
104
  propia caja — banda en blanco arriba y títulos de columna cayendo entre
104
105
  las filas. */
105
- _jsx("div", { ref: caja.ref, className: `min-w-0 ${caja.className}`, style: caja.style, children: _jsxs("table", { className: "w-full border-collapse px-1", children: [caption === undefined ? null : (_jsx("caption", { className: `text-left text-meta text-muted ${byDensity(d, "px-3 pb-2", "px-2 pb-1.5")}`, children: caption })), _jsx("thead", { className: "sticky z-20 bg-ground", style: stickyTop, children: _jsxs("tr", { className: "border-b border-line-strong", children: [_jsx("th", { className: `sticky left-0 z-10 bg-ground text-left ${compact ? "px-2 py-1.5" : "px-3 py-2"}`, children: _jsx("span", { className: "sr-only", children: copy.report.concept }) }), columns.map((c) => (_jsx("th", { scope: "col", className: `text-right text-meta font-medium whitespace-nowrap text-muted ${compact ? "px-2 py-1.5" : "px-3 py-2"}`, children: c.header }, c.id))), _jsx("th", { className: "w-0" })] }) }), _jsx("tbody", { children: rows.map((r) => (_jsx(ReportLine, { row: r, columns: columns, open: open, onToggle: toggle, compact: compact }, r.id))) })] }) }));
106
+ _jsx("div", { ref: caja.ref, className: `min-w-0 ${caja.className}`, style: caja.style, children: _jsxs("table", { className: "w-full border-collapse px-1", children: [caption === undefined ? null : (_jsx("caption", { className: `text-left text-meta text-muted ${byDensity(d, "px-3 pb-2", "px-2 pb-1.5")}`, children: caption })), _jsx("thead", { className: "sticky z-20 bg-ground", style: stickyTop, children: _jsxs("tr", { className: "border-b border-line-strong", children: [_jsx("th", { className: `sticky left-0 z-10 bg-ground text-left ${compact ? "px-2 py-1.5" : "px-3 py-2"}`, children: _jsx("span", { className: "sr-only", children: copy.report.concept }) }), columns.map((c) => (_jsx("th", { scope: "col", className: `text-right ${HEADER_TEXT} ${compact ? "px-2 py-1.5" : "px-3 py-2"}`, children: c.header }, c.id))), _jsx("th", { className: "w-0" })] }) }), _jsx("tbody", { children: rows.map((r) => (_jsx(ReportLine, { row: r, columns: columns, open: open, onToggle: toggle, compact: compact }, r.id))) })] }) }));
106
107
  }
@@ -3,19 +3,24 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useUiCopy } from "../../foundations/copy.js";
4
4
  import { IconButton } from "../button/Button.js";
5
5
  import { Menu } from "../menu/Menu.js";
6
+ import { useRowWash } from "./Table.js";
6
7
  import { MoreHorizontal } from "lucide-react";
7
8
  export function RowActions({ actions,
8
9
  /** How many stay visible before the rest fall into the `⋯`. */
9
10
  inline = 2, }) {
10
11
  const copy = useUiCopy();
12
+ const wash = useRowWash();
11
13
  if (actions.length === 0)
12
- return _jsx("td", { className: "w-0" });
14
+ return _jsx("td", { className: `w-0 border-b border-line ${wash}` });
13
15
  /* With only one left over there is no `⋯`: a menu with a single item is one
14
16
  extra click to reach the same button. */
15
17
  const inlineActions = actions.length <= inline + 1 ? actions : actions.slice(0, inline);
16
18
  const overflow = actions.slice(inlineActions.length);
17
19
  const byId = new Map(actions.map((a) => [a.id, a]));
18
- return (_jsx("td", { className: "w-0 py-1 pr-2 pl-1 align-middle whitespace-nowrap", children: _jsxs("div", { onClick: (e) => e.stopPropagation(), className: "flex items-center justify-end gap-0.5 opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 focus-within:opacity-100", children: [inlineActions.map((a) => (_jsx(IconButton, { icon: a.icon, label: a.label, size: "sm", ...(a.tone === undefined ? {} : { tone: a.tone }), ...(a.disabled === undefined ? {} : { disabled: a.disabled }), onClick: a.onSelect }, a.id))), overflow.length === 0 ? null : (_jsx(Menu, { label: copy.selection.more, icon: MoreHorizontal, iconOnly: true, chevron: false, align: "end", groups: [
20
+ return (
21
+ /* The row's hairline runs under its actions too: without it the separators stopped one
22
+ column short and the actions read as outside the table. */
23
+ _jsx("td", { className: `w-0 border-b border-line py-1 pr-2 pl-1 align-middle whitespace-nowrap last:rounded-r-control ${wash}`, children: _jsxs("div", { onClick: (e) => e.stopPropagation(), className: "flex items-center justify-end gap-0.5 opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 focus-within:opacity-100 pointer-coarse:opacity-100", children: [inlineActions.map((a) => (_jsx(IconButton, { icon: a.icon, label: a.label, size: "sm", ...(a.tone === undefined ? {} : { tone: a.tone }), ...(a.disabled === undefined ? {} : { disabled: a.disabled }), onClick: a.onSelect }, a.id))), overflow.length === 0 ? null : (_jsx(Menu, { label: copy.selection.more, icon: MoreHorizontal, iconOnly: true, chevron: false, align: "end", groups: [
19
24
  {
20
25
  options: overflow.map((a) => ({
21
26
  id: a.id,
@@ -20,7 +20,7 @@ value,
20
20
  * nothing. */
21
21
  tone,
22
22
  /** What the cell says on hover: «imputado por la mediana». */
23
- note, height, rowLabel, band, onBandToggle, caption, onCommit, editable, }: {
23
+ note, height, rowLabel, band, onBandToggle, caption, onCommit, editable, onAddRow, }: {
24
24
  columns: readonly SheetColumn[];
25
25
  rows: number;
26
26
  value: (row: number, column: SheetColumn, columnIndex: number) => string | number | null;
@@ -67,4 +67,7 @@ note, height, rowLabel, band, onBandToggle, caption, onCommit, editable, }: {
67
67
  /** Which cells are editable. Without this, all of them — but a computed column
68
68
  * or an identifier is not touched. */
69
69
  editable?: (row: number, column: SheetColumn, columnIndex: number) => boolean;
70
+ /** Present = rows can be added (see the file's header). Only with `onCommit`: a row
71
+ * that cannot be written in is not worth adding. */
72
+ onAddRow?: () => void;
70
73
  }): import("react").JSX.Element;
@@ -4,8 +4,8 @@ import { useUiCopy } from "../../foundations/copy.js";
4
4
  import { stickyTop, useScrollBox } from "../../foundations/sticky.js";
5
5
  import { Cell } from "../value/Value.js";
6
6
  import { useVirtualizer } from "@tanstack/react-virtual";
7
- import { ChevronRight } from "lucide-react";
8
- import { useState } from "react";
7
+ import { ChevronRight, Plus } from "lucide-react";
8
+ import { useEffect, useState } from "react";
9
9
  const TONE = {
10
10
  neutral: "bg-ink/6",
11
11
  info: "bg-info/12",
@@ -26,7 +26,7 @@ value,
26
26
  * nothing. */
27
27
  tone,
28
28
  /** What the cell says on hover: «imputado por la mediana». */
29
- note, height = 420, rowLabel, band, onBandToggle, caption, onCommit, editable, }) {
29
+ note, height = 420, rowLabel, band, onBandToggle, caption, onCommit, editable, onAddRow, }) {
30
30
  const copy = useUiCopy();
31
31
  const labelOf = rowLabel ?? ((i) => (i + 1).toLocaleString(copy.locale));
32
32
  /* `"auto"` es para un conjunto que ya vino CORTADO —una página de ochenta— y
@@ -78,6 +78,27 @@ note, height = 420, rowLabel, band, onBandToggle, caption, onCommit, editable, }
78
78
  if (dr !== 0 || dc !== 0)
79
79
  goTo(active.r + dr, active.c + dc);
80
80
  }
81
+ /* The column the active cell waits in until the added row arrives, and how many rows
82
+ there were when it was asked for. */
83
+ const canAdd = isEditable && onAddRow !== undefined;
84
+ const [arriving, setArriving] = useState(null);
85
+ function addRow(c) {
86
+ if (!canAdd)
87
+ return;
88
+ setArriving({ c, from: rows });
89
+ onAddRow?.();
90
+ }
91
+ useEffect(() => {
92
+ if (arriving === null || rows <= arriving.from)
93
+ return;
94
+ setArriving(null);
95
+ setActive({ r: rows - 1, c: arriving.c });
96
+ setDraft(null);
97
+ v.scrollToIndex(rows - 1);
98
+ caja.ref.current?.focus();
99
+ /* `v` and `caja` are new objects every render; the row count is what decides. */
100
+ // eslint-disable-next-line react-hooks/exhaustive-deps
101
+ }, [rows, arriving]);
81
102
  /* All the keyboard handling in one place. Splitting it between the box and
82
103
  each cell is how you end up with spreadsheets whose arrows sometimes move
83
104
  and sometimes scroll. */
@@ -92,7 +113,12 @@ note, height = 420, rowLabel, band, onBandToggle, caption, onCommit, editable, }
92
113
  }
93
114
  else if (e.key === "Enter") {
94
115
  e.preventDefault();
95
- 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;
@@ -14,6 +14,9 @@ height, }: {
14
14
  * system scale instead of pinning a size in pixels. */
15
15
  height?: number | string;
16
16
  }): import("react").JSX.Element;
17
+ /** The text of a column header, shared by every table of the family. Ink and semibold, not
18
+ * muted: in grey at the cells' weight the names of the columns read as one more row. */
19
+ export declare const HEADER_TEXT = "text-meta font-semibold whitespace-nowrap text-ink";
17
20
  export declare function THead({ children }: {
18
21
  children: ReactNode;
19
22
  }): import("react").JSX.Element;
@@ -71,6 +74,8 @@ export declare function TrDetail({ children, detail, open, onToggle, columns, la
71
74
  * a hovered row and a painted cell are two signals, and stacking them reads
72
75
  * as a third color. */
73
76
  export declare const CELL_TONE: Record<Tone, string>;
77
+ /** The wash of the row a cell sits in: for cells drawn outside `Td`, like the row's actions. */
78
+ export declare function useRowWash(): string;
74
79
  export declare function Td({ children, numeric, align,
75
80
  /** A CODE — an id, a hash, a key — set in the monospaced face. Off by default: a
76
81
  * number does not need it (a numeric cell gets tabular figures on its own), and a
@@ -15,6 +15,9 @@ height, }) {
15
15
  medición. */
16
16
  _jsx("div", { ref: caja.ref, className: `min-w-0 ${caja.className}`, style: caja.style, children: _jsx("table", { className: "w-full border-separate border-spacing-0 px-1 text-left", children: children }) }));
17
17
  }
18
+ /** The text of a column header, shared by every table of the family. Ink and semibold, not
19
+ * muted: in grey at the cells' weight the names of the columns read as one more row. */
20
+ export const HEADER_TEXT = "text-meta font-semibold whitespace-nowrap text-ink";
18
21
  export function THead({ children }) {
19
22
  return (
20
23
  /* Se pega DEBAJO de lo que ya esté pegado —la barra de la aplicación y la
@@ -43,19 +46,21 @@ export function Th({ children, sortKey, sort, onSort, numeric = false, align, wi
43
46
  onSort(null);
44
47
  }
45
48
  const Icon = dir === "asc" ? ArrowUp : dir === "desc" ? ArrowDown : ChevronsUpDown;
46
- return (_jsx("th", { scope: "col", style: width === undefined ? undefined : { width }, "aria-sort": active ? (dir === "asc" ? "ascending" : "descending") : "none", className: `border-b border-line-strong px-3 py-2 text-meta font-medium whitespace-nowrap text-muted ${(align ?? (numeric ? "right" : "left")) === "right" ? "text-right" : "text-left"}`, children: sortKey === undefined ? (children) : (_jsxs("button", { type: "button", onClick: nextSort, className: `inline-flex items-center gap-1 rounded-control transition-colors duration-(--duration-instant) hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary ${(align ?? (numeric ? "right" : "left")) === "right" ? "flex-row-reverse" : ""} ${active ? "text-ink" : ""}`, children: [children, _jsx(Icon, { className: `size-(--size-icon-sm) shrink-0 ${active ? "text-primary" : "text-line-strong"}`, strokeWidth: 2, "aria-hidden": true })] })) }));
49
+ return (_jsx("th", { scope: "col", style: width === undefined ? undefined : { width }, "aria-sort": active ? (dir === "asc" ? "ascending" : "descending") : "none", className: `border-b border-line-strong px-3 py-2 ${HEADER_TEXT} ${(align ?? (numeric ? "right" : "left")) === "right" ? "text-right" : "text-left"}`, children: sortKey === undefined ? (children) : (_jsxs("button", { type: "button", onClick: nextSort, className: `inline-flex items-center gap-1 rounded-control transition-colors duration-(--duration-instant) hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary ${(align ?? (numeric ? "right" : "left")) === "right" ? "flex-row-reverse" : ""} ${active ? "text-ink" : ""}`, children: [children, _jsx(Icon, { className: `size-(--size-icon-sm) shrink-0 ${active ? "text-primary" : "text-line-strong"}`, strokeWidth: 2, "aria-hidden": true })] })) }));
47
50
  }
48
51
  /** The wash of a row lives on the cells, not on the `<tr>`. A background on the
49
52
  * row paints edge to edge; on the cells the ends can round and the table's
50
53
  * padding keeps the mark off the card. */
51
- const RowMark = createContext(false);
54
+ const RowMark = createContext({ selected: false, interactive: false });
52
55
  const ROW_WASH = "group-hover/row:bg-ink/4";
53
56
  const ROW_ON = "bg-primary/8";
54
- function rowWash(selected) {
55
- return selected ? ROW_ON : ROW_WASH;
57
+ /* The hover wash only where a click does something: on a read-only row it promised a click
58
+ that did nothing. */
59
+ function rowWash({ selected, interactive }) {
60
+ return selected ? ROW_ON : interactive ? ROW_WASH : "";
56
61
  }
57
62
  export function Tr({ children, onClick, selected = false, }) {
58
- return (_jsx(RowMark.Provider, { value: selected, children: _jsx("tr", { onClick: onClick, className: `group/row ${onClick === undefined ? "" : "cursor-pointer"}`, children: children }) }));
63
+ return (_jsx(RowMark.Provider, { value: { selected, interactive: onClick !== undefined }, children: _jsx("tr", { onClick: onClick, className: `group/row ${onClick === undefined ? "" : "cursor-pointer"}`, children: children }) }));
59
64
  }
60
65
  /**
61
66
  * A row that opens its own detail underneath.
@@ -71,7 +76,7 @@ export function Tr({ children, onClick, selected = false, }) {
71
76
  */
72
77
  export function TrDetail({ children, detail, open, onToggle, columns, label, selected = false, }) {
73
78
  const id = useId();
74
- return (_jsx(RowMark.Provider, { value: selected, children: _jsxs(_Fragment, { children: [_jsxs("tr", { onClick: onToggle, className: "group/row cursor-pointer", children: [_jsx("td", { className: `border-b border-line py-1.5 pr-0 pl-2.5 align-middle first:rounded-l-control ${rowWash(selected)}`, children: _jsx("button", { type: "button", onClick: (e) => {
79
+ return (_jsx(RowMark.Provider, { value: { selected, interactive: true }, children: _jsxs(_Fragment, { children: [_jsxs("tr", { onClick: onToggle, className: "group/row cursor-pointer", children: [_jsx("td", { className: `border-b border-line py-1.5 pr-0 pl-2.5 align-middle first:rounded-l-control ${rowWash({ selected, interactive: true })}`, children: _jsx("button", { type: "button", onClick: (e) => {
75
80
  e.stopPropagation();
76
81
  onToggle();
77
82
  }, "aria-expanded": open, "aria-controls": id, "aria-label": label, className: "inline-flex size-5 items-center justify-center rounded-pill text-muted transition-colors duration-(--duration-instant) hover:bg-ink/6 hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary", children: _jsx(ChevronRight, { className: `size-(--size-icon-sm) transition-transform duration-(--duration-instant) ${open ? "rotate-90" : ""}`, strokeWidth: 2 }) }) }), children] }), _jsx("tr", { id: id, hidden: !open, children: _jsx("td", { colSpan: columns + 1, className: "border-b border-line bg-ground px-3 py-3", children: detail }) })] }) }));
@@ -87,6 +92,10 @@ export const CELL_TONE = {
87
92
  bad: "bg-bad/12",
88
93
  accent: "bg-accent/20",
89
94
  };
95
+ /** The wash of the row a cell sits in: for cells drawn outside `Td`, like the row's actions. */
96
+ export function useRowWash() {
97
+ return rowWash(useContext(RowMark));
98
+ }
90
99
  export function Td({ children, numeric = false, align,
91
100
  /** A CODE — an id, a hash, a key — set in the monospaced face. Off by default: a
92
101
  * number does not need it (a numeric cell gets tabular figures on its own), and a
@@ -100,8 +109,8 @@ mono = false,
100
109
  * something else — and in a table of forty rows by six columns, colored text
101
110
  * gets lost. */
102
111
  tone, title, }) {
103
- const selected = useContext(RowMark);
104
- return (_jsx("td", { title: title, className: `border-b border-line px-3 py-1.5 text-meta text-ink first:rounded-l-control last:rounded-r-control ${(align ?? (numeric ? "right" : "left")) === "right" ? "text-right" : "text-left"} ${mono ? "font-mono tabular-nums" : numeric ? "tabular-nums" : ""} ${tone === undefined ? rowWash(selected) : CELL_TONE[tone]}`, children: children }));
112
+ const mark = useContext(RowMark);
113
+ return (_jsx("td", { title: title, className: `border-b border-line px-3 py-1.5 text-meta text-ink first:rounded-l-control last:rounded-r-control ${(align ?? (numeric ? "right" : "left")) === "right" ? "text-right" : "text-left"} ${mono ? "font-mono tabular-nums" : numeric ? "tabular-nums" : ""} ${tone === undefined ? rowWash(mark) : CELL_TONE[tone]}`, children: children }));
105
114
  }
106
115
  /** The selection checkbox, with its third state.
107
116
  *
@@ -1,5 +1,5 @@
1
- export { type Aggregate, type DataColumn, DataTable, type GroupColumn, } from "./DataTable.js";
2
- export { EditableTable, type EditColumn, type EditLayout, type StackPlace, stackPlaces, } from "./EditableTable.js";
1
+ export { type Aggregate, type DataColumn, DataTable, type DataTableCreate, type GroupColumn, } from "./DataTable.js";
2
+ export { EditableTable, type EditAdd, type EditBands, type EditColumn, type EditLayout, sectionLines, type StackPlace, stackPlaces, type TabTarget, tabTarget, } from "./EditableTable.js";
3
3
  export { PivotTable, type PivotTableProps, } from "./PivotTable.js";
4
4
  export { type ReportColumn, type ReportRow, type ReportRowKind, ReportTable, } from "./ReportTable.js";
5
5
  export { type RowAction, RowActions } from "./RowActions.js";
@@ -1,7 +1,7 @@
1
1
  /* The presentation (`Table`) is ours and is used on its own; `DataTable` is the
2
2
  wiring to TanStack for grouping and sorting. See the note in `DataTable.tsx`. */
3
3
  export { DataTable, } from "./DataTable.js";
4
- export { EditableTable, stackPlaces, } from "./EditableTable.js";
4
+ export { EditableTable, sectionLines, stackPlaces, tabTarget, } from "./EditableTable.js";
5
5
  export { PivotTable, } from "./PivotTable.js";
6
6
  export { ReportTable, } from "./ReportTable.js";
7
7
  export { RowActions } from "./RowActions.js";
@@ -4,7 +4,7 @@ import type { LucideIcon } from "lucide-react";
4
4
  * UNA TIRA DE PESTAÑAS. La misma en todo el sistema.
5
5
  *
6
6
  * Context: esto vivía adentro del encabezado del registro y se llamaba
7
- * `Aspects`, así que 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
@@ -7,6 +7,8 @@ export type TagItem = {
7
7
  * comes from, which symbol a comparison is. It goes muted and inside. */
8
8
  note?: string;
9
9
  icon?: LucideIcon;
10
+ /** A person on the chip: its photo, or its initials when `true` — followers, assignees. */
11
+ avatar?: true | string;
10
12
  /** Choose it. Without this the chip is a value and is not pressed. */
11
13
  onSelect?: () => void;
12
14
  /** Remove it, when removing it means something. It is a separate button
@@ -4,6 +4,7 @@ import { AriaLocale } from "../../foundations/aria.js";
4
4
  import { bySurface, useSurface } from "../../foundations/surface.js";
5
5
  import { cn, focusRing } from "../../foundations/variants.js";
6
6
  import { tagTone } from "./Tag.js";
7
+ import { Avatar } from "../value/Person.js";
7
8
  import { X } from "lucide-react";
8
9
  import { Button as AriaButton, Tag as AriaTag, TagGroup, TagList, } from "react-aria-components";
9
10
  export function Tags({ items, surface, label, labelledBy, stack = false, multiple = false, }) {
@@ -95,7 +96,7 @@ function shell({ item, surface, stack, removable, pressable, }) {
95
96
  }
96
97
  function Inside({ item }) {
97
98
  const Icon = item.icon;
98
- return (_jsxs(_Fragment, { children: [Icon === undefined ? null : (_jsx(Icon, { className: "size-3 shrink-0 opacity-70", strokeWidth: 2, "aria-hidden": true })), _jsx("span", { className: "min-w-0 truncate", children: item.label }), item.note === undefined ? null : (_jsxs("span", { className: "shrink-0 opacity-60", children: ["\u00B7 ", item.note] }))] }));
99
+ return (_jsxs(_Fragment, { children: [Icon === undefined ? null : (_jsx(Icon, { className: "size-3 shrink-0 opacity-70", strokeWidth: 2, "aria-hidden": true })), item.avatar === undefined ? null : (_jsx("span", { "aria-hidden": true, className: "-my-0.5 -ml-1.5 flex shrink-0", children: _jsx(Avatar, { name: item.label, size: "sm", ...(typeof item.avatar === "string" ? { src: item.avatar } : {}) }) })), _jsx("span", { className: "min-w-0 truncate", children: item.label }), item.note === undefined ? null : (_jsxs("span", { className: "shrink-0 opacity-60", children: ["\u00B7 ", item.note] }))] }));
99
100
  }
100
101
  /* The × is React Aria's `remove` slot: it removes THIS chip and never presses it, so
101
102
  removing a pressable chip does not also choose it. */
@@ -1,3 +1,4 @@
1
+ import type { OpenTarget } from "../../foundations/types.js";
1
2
  import { type FormMode } from "../field/mode.js";
2
3
  import { type TagOption } from "./choices.js";
3
4
  export type TagsFieldProps = {
@@ -14,12 +15,24 @@ export type TagsFieldProps = {
14
15
  * backend (Odoo's `name_create`); the consumer also appends the new option to `options`.
15
16
  */
16
17
  onCreate?: (label: string) => string | Promise<string>;
18
+ /** «Buscar más…» at the end of the list: the tags to add chosen from a table
19
+ * (the list that chooses, `List onChoose` with `selection`, in a modal). It gets what was typed. */
20
+ onBrowseAll?: (query: string) => void;
21
+ /** Open the record behind a chip — a follower, a product, a category. Pressing the chip (or
22
+ * Enter on it) opens it in `openTarget`; its × still only removes. Absent, a chip is a value. */
23
+ onOpen?: (id: string, to: OpenTarget) => void;
24
+ /** Where a pressed chip opens. A peek by default: a chip is a glance at one of many — «who is
25
+ * this follower?» — not a place to go and work. */
26
+ openTarget?: OpenTarget;
17
27
  /** The input's placeholder while no tag is chosen. Defaults to the copy table's «Add…». */
18
28
  placeholder?: string;
19
29
  disabled?: boolean;
20
30
  id?: string;
21
31
  describedBy?: string;
22
32
  invalid?: boolean;
33
+ /** The name of the control when it is NOT inside a `Field` — a cell of an editable table.
34
+ * Inside a field, the field's label names it. */
35
+ "aria-label"?: string;
23
36
  /** Forces the mode. By default it inherits the form's: in read mode it draws `Tags`. */
24
37
  mode?: FormMode;
25
38
  };
@@ -8,9 +8,11 @@ import { useFieldIds } from "../field/context.js";
8
8
  import { ReadValue } from "../field/Field.js";
9
9
  import { useFormMode } from "../field/mode.js";
10
10
  import { Popover } from "../overlay/Popover.js";
11
+ import { MatchedText } from "../picker/MatchedText.js";
11
12
  import { tagChoices, withoutTag, withTag, } from "./choices.js";
12
13
  import { Tags } from "./Tags.js";
13
- import { Plus } from "lucide-react";
14
+ import { Avatar } from "../value/Person.js";
15
+ import { Plus, Search } from "lucide-react";
14
16
  import { useContext, useEffect, useMemo, useRef, useState, } from "react";
15
17
  import { ComboBox as AriaComboBox, ComboBoxStateContext, Group, Input, ListBox, ListBoxItem, useFilter, useLocale, } from "react-aria-components";
16
18
  /* ==================================================================== *
@@ -43,6 +45,15 @@ import { ComboBox as AriaComboBox, ComboBoxStateContext, Group, Input, ListBox,
43
45
  * ==================================================================== */
44
46
  /** The key of the «create» row. Not an id: it cannot collide with one. */
45
47
  const CREATE = "\u0000create";
48
+ /** The key of the «Buscar más…» row. */
49
+ const BROWSE = "\u0000browse";
50
+ /** What a chip needs to open its record: the press, and the place it opens named as its title. */
51
+ function useChipOpening(onOpen, to = "peek") {
52
+ const copy = useUiCopy();
53
+ return (id) => onOpen === undefined
54
+ ? {}
55
+ : { onSelect: () => onOpen(id, to), title: copy.picker.openIn[to] };
56
+ }
46
57
  /** The chosen tags as chips, in the order of `value`. An id missing from `options` is still
47
58
  * drawn (as itself): hiding it would hide data the record has. */
48
59
  function chosenOptions(options, value) {
@@ -56,6 +67,7 @@ function chosenOptions(options, value) {
56
67
  * chips, and its hint and error describe the input. In read mode it is `Tags`.
57
68
  */
58
69
  export function TagsField({ mode, ...props }) {
70
+ const opening = useChipOpening(props.onOpen, props.openTarget);
59
71
  if (useFormMode(mode) === "read") {
60
72
  const chosen = chosenOptions(props.options, props.value);
61
73
  if (chosen.length === 0)
@@ -64,19 +76,22 @@ export function TagsField({ mode, ...props }) {
64
76
  id: option.value,
65
77
  label: option.label,
66
78
  ...(option.tone === undefined ? {} : { tone: option.tone }),
79
+ ...(option.avatar === undefined ? {} : { avatar: option.avatar }),
80
+ ...opening(option.value),
67
81
  })) }) }));
68
82
  }
69
83
  /* The filter reads the locale from React Aria's context, so the box that provides it has
70
84
  to be ABOVE the component that asks for it. */
71
85
  return (_jsx(AriaLocale, { children: _jsx(EditableTagsField, { ...props }) }));
72
86
  }
73
- function EditableTagsField({ value, onChange, options, onCreate, placeholder, disabled = false, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, }) {
87
+ function EditableTagsField({ value, onChange, options, onCreate, onBrowseAll, onOpen, openTarget, placeholder, disabled = false, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, "aria-label": ariaLabel, }) {
74
88
  const { id, labelledBy, describedBy, invalid } = useFieldIds({
75
89
  id: ownId,
76
90
  describedBy: ownDescribedBy,
77
91
  invalid: ownInvalid,
78
92
  });
79
93
  const copy = useUiCopy();
94
+ const opening = useChipOpening(onOpen, openTarget);
80
95
  const { contains } = useFilter({ sensitivity: "base" });
81
96
  const { locale } = useLocale();
82
97
  const collator = useMemo(() => new Intl.Collator(locale, { sensitivity: "base", usage: "search" }), [locale]);
@@ -89,17 +104,25 @@ function EditableTagsField({ value, onChange, options, onCreate, placeholder, di
89
104
  const latest = useRef(value);
90
105
  latest.current = value;
91
106
  const { available, create } = tagChoices(options, value, text, { contains, same: (a, b) => collator.compare(a.trim(), b) === 0 }, onCreate !== undefined && !disabled);
92
- const listed = create === null
93
- ? available
94
- : [
95
- ...available,
96
- { value: CREATE, label: fillCopy(copy.inlineEdit.createTag, { name: create }) },
97
- ];
107
+ const listed = [
108
+ ...available,
109
+ ...(create === null
110
+ ? []
111
+ : [{ value: CREATE, label: fillCopy(copy.inlineEdit.createTag, { name: create }) }]),
112
+ ...(onBrowseAll === undefined || disabled
113
+ ? []
114
+ : [{ value: BROWSE, label: copy.picker.browseAll }]),
115
+ ];
98
116
  const commit = (key) => {
99
117
  if (key === null)
100
118
  return;
101
119
  const chosen = String(key);
120
+ const typed = text;
102
121
  setText("");
122
+ if (chosen === BROWSE) {
123
+ onBrowseAll?.(typed);
124
+ return;
125
+ }
103
126
  if (chosen !== CREATE) {
104
127
  onChange?.(withTag(latest.current, chosen));
105
128
  return;
@@ -120,6 +143,8 @@ function EditableTagsField({ value, onChange, options, onCreate, placeholder, di
120
143
  id: option.value,
121
144
  label: option.label,
122
145
  ...(option.tone === undefined ? {} : { tone: option.tone }),
146
+ ...(option.avatar === undefined ? {} : { avatar: option.avatar }),
147
+ ...opening(option.value),
123
148
  ...(disabled ? {} : { onRemove: () => remove(option.value) }),
124
149
  }));
125
150
  return (
@@ -132,7 +157,11 @@ function EditableTagsField({ value, onChange, options, onCreate, placeholder, di
132
157
  /* The box grows with the chips: five categories wrap to a second line instead of
133
158
  scrolling out of sight. */
134
159
  className: "h-auto min-h-(--size-control-md) flex-wrap gap-1 py-1 pl-1.5",
135
- }), children: [chips.length === 0 ? null : (_jsx(Tags, { items: chips, ...(labelledBy === undefined ? {} : { labelledBy }) })), _jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-describedby": describedBy, items: listed, selectedKey: null, onSelectionChange: commit, inputValue: text, onInputChange: setText,
160
+ }), children: [chips.length === 0 ? null : (_jsx(Tags, { items: chips, ...(labelledBy === undefined
161
+ ? ariaLabel === undefined
162
+ ? {}
163
+ : { label: ariaLabel }
164
+ : { labelledBy }) })), _jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, items: listed, selectedKey: null, onSelectionChange: commit, inputValue: text, onInputChange: setText,
136
165
  /* Arriving at the field shows what can still be added, the way a many2one opens its
137
166
  list on focus. With every tag chosen and no text there is nothing to show, and the
138
167
  list stays closed. */
@@ -145,7 +174,7 @@ function EditableTagsField({ value, onChange, options, onCreate, placeholder, di
145
174
  onChange?.(withoutTag(value, last));
146
175
  }, empty: text === "" }), _jsx(Popover, { triggerRef: boxRef, ...(width === null
147
176
  ? {}
148
- : { style: { "--trigger-width": `${width}px` } }), placement: "bottom start", className: "w-(--trigger-width)", children: _jsx(ListBox, { items: listed, className: "max-h-[inherit] overflow-auto p-1 outline-0", children: (item) => (_jsx(ListBoxItem, { id: item.value, textValue: item.label, className: (state) => cn(listOptionStyles(state), item.value === CREATE ? "text-primary" : ""), children: item.value === CREATE ? (_jsxs(_Fragment, { children: [_jsx(Plus, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0", strokeWidth: 2 }), _jsx("span", { className: "truncate font-medium", children: item.label })] })) : (_jsx("span", { className: "truncate", children: item.label })) })) }) })] })] }));
177
+ : { style: { "--trigger-width": `${width}px` } }), placement: "bottom start", className: "w-(--trigger-width)", children: _jsx(ListBox, { items: listed, dependencies: [text], className: "max-h-[inherit] overflow-auto p-1 outline-0", children: (item) => (_jsx(ListBoxItem, { id: item.value, textValue: item.label, className: (state) => cn(listOptionStyles(state), item.value === CREATE || item.value === BROWSE ? "text-primary" : ""), children: item.value === CREATE ? (_jsxs(_Fragment, { children: [_jsx(Plus, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0", strokeWidth: 2 }), _jsx("span", { className: "truncate font-medium", children: item.label })] })) : item.value === BROWSE ? (_jsxs(_Fragment, { children: [_jsx(Search, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0", strokeWidth: 2 }), _jsx("span", { className: "truncate font-medium", children: item.label })] })) : (_jsxs(_Fragment, { children: [item.avatar === undefined ? null : (_jsx(Avatar, { name: item.label, size: "sm", ...(typeof item.avatar === "string" ? { src: item.avatar } : {}) })), _jsx("span", { className: "truncate", children: _jsx(MatchedText, { text: item.label, query: text, locale: copy.locale }) })] })) })) }) })] })] }));
149
178
  }
150
179
  /** The input after the chips. It reads the combobox's state to know whether the keyboard
151
180
  * marked an option — Enter with nothing marked takes the first match. */
@@ -11,6 +11,8 @@ export interface TagOption {
11
11
  label: string;
12
12
  /** Neutral by default: «mayorista» classifies, it does not warn. */
13
13
  tone?: Tone;
14
+ /** A person: its face on the chip and in the list — `true` for initials, a URL for a photo. */
15
+ avatar?: true | string;
14
16
  }
15
17
  /** How the typed text is compared — the locale's, handed in by the component. */
16
18
  export type TagMatch = {
@@ -0,0 +1,56 @@
1
+ import { type LucideIcon } from "lucide-react";
2
+ /** One record of the hierarchy. With `children` it is a group that folds. */
3
+ export interface TreeItem {
4
+ id: string;
5
+ label: string;
6
+ /** What reads beside the name, at the right, in tabular figures: a balance, a stock, a
7
+ * count. Already formatted — the tree does not know a currency from a unit. */
8
+ meta?: string;
9
+ icon?: LucideIcon;
10
+ children?: readonly TreeItem[];
11
+ /** It cannot be chosen — a view account that groups others and takes no entries. It still
12
+ * folds and opens. */
13
+ disabled?: boolean;
14
+ }
15
+ /** One name or the other: a tree with no name is announced as a lone «tree». */
16
+ type TreeName = {
17
+ /** The accessible name: «Plan de cuentas». */
18
+ label: string;
19
+ labelledBy?: never;
20
+ } | {
21
+ /** The id of a visible heading that names the tree — the label of the `Field` it lives
22
+ * in. */
23
+ labelledBy: string;
24
+ label?: never;
25
+ };
26
+ export type TreeProps = TreeName & {
27
+ items: readonly TreeItem[];
28
+ /** The groups open right now. Pass it with `onExpandedChange` to own the expansion. */
29
+ expanded?: readonly string[];
30
+ /** The groups that start open, when the tree owns its expansion. */
31
+ defaultExpanded?: readonly string[];
32
+ onExpandedChange?: (ids: string[]) => void;
33
+ /** `none` reads; `single` chooses one record (a category, a location); `multiple` a set. */
34
+ selectionMode?: "none" | "single" | "multiple";
35
+ /** The chosen ids — a list in both modes, like every other control, so a screen does not
36
+ * switch shapes when one becomes many. */
37
+ selected?: readonly string[];
38
+ defaultSelected?: readonly string[];
39
+ onSelectedChange?: (ids: string[]) => void;
40
+ /** Open the record: Enter, or a click (a double click when the tree chooses one). Without
41
+ * it, a click on a group folds or opens it. */
42
+ onAction?: (id: string) => void;
43
+ /** What an empty tree says. By default, the copy table's `tree.empty`. */
44
+ empty?: string;
45
+ };
46
+ /**
47
+ * A hierarchy of records — the chart of accounts, product categories, warehouse locations —
48
+ * with its groups that fold, a figure per row, and one or many rows to choose.
49
+ *
50
+ * Context: the keyboard is React Aria's `Tree`: arrows walk the visible rows, → opens a group
51
+ * and ← folds it (or goes up to the parent), Home and End jump, and typing a name moves to it.
52
+ * Escape does NOT clear the selection: the tree often lives in a dialog or a popover, and
53
+ * Escape there means «close» — clearing the choice on the way out would lose it without a word.
54
+ */
55
+ export declare function Tree({ items, label, labelledBy, expanded, defaultExpanded, onExpandedChange, selectionMode, selected, defaultSelected, onSelectedChange, onAction, empty, }: TreeProps): import("react").JSX.Element;
56
+ export {};
@@ -0,0 +1,74 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { AriaLocale } from "../../foundations/aria.js";
4
+ import { useUiCopy } from "../../foundations/copy.js";
5
+ import { byDensity, useDensity } from "../../foundations/density.js";
6
+ import { cn, focusRing, tv } from "../../foundations/variants.js";
7
+ import { Check, ChevronRight } from "lucide-react";
8
+ import { Button as AriaButton, Checkbox as AriaCheckbox, Tree as AriaTree, TreeItem as AriaTreeItem, TreeItemContent, } from "react-aria-components";
9
+ const row = tv({
10
+ extend: focusRing,
11
+ base: "group relative flex min-w-0 cursor-default items-center rounded-control text-meta text-ink -outline-offset-2 select-none forced-color-adjust-none",
12
+ variants: {
13
+ isSelected: {
14
+ true: "bg-primary/10 forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]",
15
+ },
16
+ isHovered: { true: "" },
17
+ },
18
+ compoundVariants: [{ isSelected: false, isHovered: true, class: "bg-ink/4" }],
19
+ });
20
+ /* The chevron column is exactly one indent step wide, so a child's chevron lands under its
21
+ parent's name and the levels read as columns, not as a ragged edge. */
22
+ const chevronButton = tv({
23
+ extend: focusRing,
24
+ base: "inline-flex size-(--spacing-indent) shrink-0 items-center justify-center rounded-mark text-muted transition-colors duration-(--duration-instant) hover:bg-ink/6 hover:text-ink pressed:bg-ink/10",
25
+ });
26
+ /** Every id of the hierarchy that can be chosen — what «select all» means here. */
27
+ function choosable(items) {
28
+ return items.flatMap((item) => [
29
+ ...(item.disabled === true ? [] : [item.id]),
30
+ ...choosable(item.children ?? []),
31
+ ]);
32
+ }
33
+ /**
34
+ * A hierarchy of records — the chart of accounts, product categories, warehouse locations —
35
+ * with its groups that fold, a figure per row, and one or many rows to choose.
36
+ *
37
+ * Context: the keyboard is React Aria's `Tree`: arrows walk the visible rows, → opens a group
38
+ * and ← folds it (or goes up to the parent), Home and End jump, and typing a name moves to it.
39
+ * Escape does NOT clear the selection: the tree often lives in a dialog or a popover, and
40
+ * Escape there means «close» — clearing the choice on the way out would lose it without a word.
41
+ */
42
+ export function Tree({ items, label, labelledBy, expanded, defaultExpanded, onExpandedChange, selectionMode = "none", selected, defaultSelected, onSelectedChange, onAction, empty, }) {
43
+ const copy = useUiCopy();
44
+ const density = useDensity();
45
+ const pad = byDensity(density, "py-1.5 pe-2", "py-0.5 pe-1.5");
46
+ const many = selectionMode === "multiple";
47
+ const onSelectionChange = (keys) => onSelectedChange?.(keys === "all" ? choosable(items) : [...keys].map(String));
48
+ const render = (node, level) => {
49
+ const children = node.children ?? [];
50
+ const group = children.length > 0;
51
+ const Icon = node.icon;
52
+ /* Each level is one indent step; the first one keeps a sliver of the row's own padding so
53
+ the chevron is not glued to the edge of its tint. */
54
+ const indent = {
55
+ paddingInlineStart: `calc(var(--spacing) * 1 + var(--spacing-indent) * ${level - 1})`,
56
+ };
57
+ return (_jsxs(AriaTreeItem, { id: node.id, textValue: node.label, isDisabled: node.disabled === true, className: ({ isSelected, isHovered, isFocusVisible }) => cn(row({ isSelected, isHovered, isFocusVisible }), pad), style: indent, children: [_jsx(TreeItemContent, { children: ({ isExpanded, isSelected }) => (_jsxs(_Fragment, { children: [group ? (_jsx(AriaButton, { slot: "chevron", className: ({ isFocusVisible }) => chevronButton({ isFocusVisible }), children: _jsx(ChevronRight, { className: `size-(--size-icon-sm) transition-transform duration-(--duration-quick) ease-(--ease-out) ${isExpanded ? "rotate-90" : ""}`, strokeWidth: 2, "aria-hidden": true }) })) : (_jsx("span", { "aria-hidden": true, className: "size-(--spacing-indent) shrink-0" })), _jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-1.5", children: [many ? _jsx(SelectionCheck, {}) : null, Icon === undefined ? null : (_jsx(Icon, { className: "size-(--size-icon-md) shrink-0 text-muted", strokeWidth: 2, "aria-hidden": true })), _jsx("span", { className: cn("min-w-0 flex-1 truncate", group ? "font-medium" : "", isSelected ? "font-medium" : "", node.disabled === true && selectionMode !== "none" ? "text-muted" : ""), children: node.label }), node.meta === undefined ? null : (_jsx("span", { className: "shrink-0 ps-3 text-right tabular-nums whitespace-nowrap text-muted", children: node.meta })), selectionMode === "single" && isSelected ? (_jsx(Check, { className: "size-(--size-icon-sm) shrink-0 text-primary forced-colors:text-[HighlightText]", strokeWidth: 2.5, "aria-hidden": true })) : null] })] })) }), children.map((child) => render(child, level + 1))] }, node.id));
58
+ };
59
+ return (_jsx(AriaLocale, { children: _jsx(AriaTree, { ...(labelledBy === undefined
60
+ ? { "aria-label": label }
61
+ : { "aria-labelledby": labelledBy }), selectionMode: selectionMode,
62
+ /* One to choose: a click chooses and a double click opens. Many: the click opens and
63
+ the check chooses. See the header. */
64
+ selectionBehavior: selectionMode === "single" ? "replace" : "toggle", disabledBehavior: "selection", escapeKeyBehavior: "none", ...(selected === undefined ? {} : { selectedKeys: selected }), ...(defaultSelected === undefined ? {} : { defaultSelectedKeys: defaultSelected }), ...(expanded === undefined ? {} : { expandedKeys: expanded }), ...(defaultExpanded === undefined ? {} : { defaultExpandedKeys: defaultExpanded }), ...(onExpandedChange === undefined
65
+ ? {}
66
+ : { onExpandedChange: (keys) => onExpandedChange([...keys].map(String)) }), ...(onSelectedChange === undefined ? {} : { onSelectionChange }), ...(onAction === undefined ? {} : { onAction: (key) => onAction(String(key)) }), renderEmptyState: () => (_jsx("p", { className: "px-2 py-3 text-meta text-muted", children: empty ?? copy.tree.empty })), className: "flex w-full min-w-0 flex-col gap-px outline-0", children: items.map((item) => render(item, 1)) }) }));
67
+ }
68
+ /* The check of a row when many can be chosen. It is React Aria's `selection` slot: it toggles
69
+ the row and never opens it. Drawn like `Checkbox`'s box so a check reads as one thing. */
70
+ function SelectionCheck() {
71
+ return (_jsx(AriaCheckbox, { slot: "selection", className: "group/check inline-flex shrink-0 outline-0", children: ({ isSelected, isFocusVisible }) => (_jsx("span", { "aria-hidden": true, className: cn("inline-flex size-4 items-center justify-center rounded-mark border transition-colors duration-(--duration-instant) forced-color-adjust-none", isSelected
72
+ ? "border-primary bg-primary text-surface forced-colors:border-[Highlight] forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]"
73
+ : "border-line-strong bg-surface text-transparent forced-colors:border-[ButtonBorder]", focusRing({ isFocusVisible })), children: _jsx(Check, { className: "size-3", strokeWidth: 2.5 }) })) }));
74
+ }
@@ -0,0 +1 @@
1
+ export { Tree, type TreeItem, type TreeProps } from "./Tree.js";
@@ -0,0 +1,3 @@
1
+ /* A hierarchy of records — the chart of accounts, categories, locations: read, folded and
2
+ chosen from. Not the app's menu: that is `NavigationTree`. */
3
+ export { Tree } from "./Tree.js";