@sincpro/web-ui 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/README.md +40 -13
  2. package/dist/components/activity/Activity.d.ts +69 -0
  3. package/dist/components/activity/Activity.js +279 -0
  4. package/dist/components/activity/ActivityBadge.d.ts +16 -0
  5. package/dist/components/activity/ActivityBadge.js +29 -0
  6. package/dist/components/{chatter → activity}/Messages.d.ts +2 -2
  7. package/dist/components/{chatter → activity}/Messages.js +9 -6
  8. package/dist/components/activity/index.d.ts +4 -0
  9. package/dist/components/activity/index.js +5 -0
  10. package/dist/components/assist/AssistMark.d.ts +3 -3
  11. package/dist/components/assist/AssistMark.js +1 -1
  12. package/dist/components/assist/assist.d.ts +1 -1
  13. package/dist/components/assist/assist.js +1 -1
  14. package/dist/components/calendar/RecordCalendar.d.ts +48 -0
  15. package/dist/components/calendar/RecordCalendar.js +101 -0
  16. package/dist/components/calendar/index.d.ts +1 -0
  17. package/dist/components/calendar/index.js +1 -0
  18. package/dist/components/form/Sheet.d.ts +4 -0
  19. package/dist/components/form/Sheet.js +16 -5
  20. package/dist/components/gantt/RecordGantt.d.ts +44 -0
  21. package/dist/components/gantt/RecordGantt.js +110 -0
  22. package/dist/components/gantt/index.d.ts +1 -0
  23. package/dist/components/gantt/index.js +1 -0
  24. package/dist/components/index.d.ts +4 -1
  25. package/dist/components/index.js +4 -1
  26. package/dist/components/lines/LineItems.d.ts +45 -5
  27. package/dist/components/lines/LineItems.js +106 -18
  28. package/dist/components/listing/BoardColumns.d.ts +39 -0
  29. package/dist/components/listing/BoardColumns.js +138 -0
  30. package/dist/components/listing/Listing.js +1 -1
  31. package/dist/components/listing/ListingBar.d.ts +46 -2
  32. package/dist/components/listing/ListingBar.js +47 -5
  33. package/dist/components/listing/PivotExplorer.d.ts +34 -0
  34. package/dist/components/listing/PivotExplorer.js +86 -0
  35. package/dist/components/listing/RecordCard.d.ts +6 -8
  36. package/dist/components/listing/RecordCard.js +13 -7
  37. package/dist/components/listing/Row.d.ts +25 -2
  38. package/dist/components/listing/Row.js +30 -13
  39. package/dist/components/listing/index.d.ts +2 -0
  40. package/dist/components/listing/index.js +2 -0
  41. package/dist/components/listing/set.d.ts +1 -1
  42. package/dist/components/navigation/index.d.ts +0 -2
  43. package/dist/components/navigation/index.js +2 -3
  44. package/dist/components/record/RecordAudit.d.ts +17 -0
  45. package/dist/components/record/RecordAudit.js +27 -0
  46. package/dist/components/record/RecordHeader.d.ts +27 -17
  47. package/dist/components/record/RecordHeader.js +64 -20
  48. package/dist/components/record/RecordNotices.d.ts +37 -0
  49. package/dist/components/record/RecordNotices.js +75 -0
  50. package/dist/components/record/RecordRibbon.d.ts +13 -0
  51. package/dist/components/record/RecordRibbon.js +22 -0
  52. package/dist/components/record/ThumbBar.js +1 -1
  53. package/dist/components/record/index.d.ts +2 -0
  54. package/dist/components/record/index.js +2 -0
  55. package/dist/components/search/CriteriaChips.d.ts +6 -1
  56. package/dist/components/search/CriteriaChips.js +71 -20
  57. package/dist/components/search/SearchBar.d.ts +4 -1
  58. package/dist/components/search/SearchBar.js +15 -7
  59. package/dist/components/timeline/RecordTimeline.d.ts +60 -0
  60. package/dist/components/timeline/RecordTimeline.js +214 -0
  61. package/dist/components/timeline/TimelineToolbar.d.ts +30 -0
  62. package/dist/components/timeline/TimelineToolbar.js +31 -0
  63. package/dist/components/timeline/index.d.ts +3 -0
  64. package/dist/components/timeline/index.js +3 -0
  65. package/dist/components/timeline/time.d.ts +43 -0
  66. package/dist/components/timeline/time.js +135 -0
  67. package/dist/foundations/copy.d.ts +194 -18
  68. package/dist/foundations/copy.es.js +142 -15
  69. package/dist/foundations/copy.js +133 -11
  70. package/dist/foundations/index.d.ts +1 -1
  71. package/dist/foundations/match.d.ts +23 -0
  72. package/dist/foundations/match.js +63 -0
  73. package/dist/foundations/slots.d.ts +1 -1
  74. package/dist/foundations/slots.js +8 -3
  75. package/dist/foundations/types.d.ts +7 -0
  76. package/dist/foundations/types.js +0 -36
  77. package/dist/primitives/block/index.d.ts +1 -1
  78. package/dist/primitives/block/index.js +1 -1
  79. package/dist/primitives/cell/CellStack.d.ts +7 -1
  80. package/dist/primitives/cell/CellStack.js +5 -2
  81. package/dist/primitives/choice/ComboBox.d.ts +3 -0
  82. package/dist/primitives/choice/ComboBox.js +10 -6
  83. package/dist/primitives/choice/KanbanState.d.ts +14 -0
  84. package/dist/primitives/choice/KanbanState.js +37 -0
  85. package/dist/primitives/choice/Priority.d.ts +20 -0
  86. package/dist/primitives/choice/Priority.js +41 -0
  87. package/dist/primitives/choice/SearchField.d.ts +10 -1
  88. package/dist/primitives/choice/SearchField.js +21 -2
  89. package/dist/primitives/choice/Slider.d.ts +6 -1
  90. package/dist/primitives/choice/Slider.js +2 -2
  91. package/dist/primitives/choice/index.d.ts +2 -0
  92. package/dist/primitives/choice/index.js +2 -0
  93. package/dist/primitives/date/Calendar.js +30 -6
  94. package/dist/primitives/date/DatePicker.d.ts +11 -3
  95. package/dist/primitives/date/DatePicker.js +12 -8
  96. package/dist/primitives/date/DateTime.d.ts +35 -0
  97. package/dist/primitives/date/DateTime.js +100 -0
  98. package/dist/primitives/date/index.d.ts +1 -0
  99. package/dist/primitives/date/index.js +4 -2
  100. package/dist/primitives/dialog/Dialog.d.ts +9 -3
  101. package/dist/primitives/dialog/Dialog.js +41 -5
  102. package/dist/primitives/field/DropZone.js +1 -1
  103. package/dist/primitives/field/Field.d.ts +14 -2
  104. package/dist/primitives/field/Field.js +25 -14
  105. package/dist/primitives/field/copy.d.ts +16 -0
  106. package/dist/primitives/field/copy.js +69 -0
  107. package/dist/primitives/field/index.d.ts +2 -0
  108. package/dist/primitives/field/index.js +4 -0
  109. package/dist/primitives/field/note.d.ts +55 -0
  110. package/dist/primitives/field/note.js +117 -0
  111. package/dist/primitives/index.d.ts +1 -0
  112. package/dist/primitives/index.js +1 -0
  113. package/dist/primitives/menu/Menu.d.ts +28 -0
  114. package/dist/primitives/menu/Menu.js +47 -17
  115. package/dist/primitives/notice/Callout.js +1 -1
  116. package/dist/primitives/notice/Flag.d.ts +13 -0
  117. package/dist/primitives/notice/Flag.js +31 -0
  118. package/dist/primitives/notice/index.d.ts +2 -1
  119. package/dist/primitives/notice/index.js +2 -1
  120. package/dist/primitives/overlay/Preview.d.ts +19 -0
  121. package/dist/primitives/overlay/Preview.js +69 -0
  122. package/dist/primitives/overlay/index.d.ts +1 -0
  123. package/dist/primitives/overlay/index.js +3 -1
  124. package/dist/primitives/picker/MatchedText.d.ts +10 -0
  125. package/dist/primitives/picker/MatchedText.js +12 -0
  126. package/dist/primitives/picker/Picker.d.ts +67 -4
  127. package/dist/primitives/picker/Picker.js +142 -23
  128. package/dist/primitives/table/CellCopy.d.ts +20 -0
  129. package/dist/primitives/table/CellCopy.js +23 -0
  130. package/dist/primitives/table/DataTable.d.ts +55 -1
  131. package/dist/primitives/table/DataTable.js +126 -40
  132. package/dist/primitives/table/EditableTable.d.ts +102 -2
  133. package/dist/primitives/table/EditableTable.js +287 -32
  134. package/dist/primitives/table/ReportTable.js +2 -1
  135. package/dist/primitives/table/RowActions.js +7 -2
  136. package/dist/primitives/table/SheetTable.d.ts +4 -1
  137. package/dist/primitives/table/SheetTable.js +38 -5
  138. package/dist/primitives/table/Table.d.ts +5 -0
  139. package/dist/primitives/table/Table.js +17 -8
  140. package/dist/primitives/table/index.d.ts +2 -2
  141. package/dist/primitives/table/index.js +1 -1
  142. package/dist/primitives/tabs/Tabs.d.ts +2 -2
  143. package/dist/primitives/tabs/Tabs.js +1 -1
  144. package/dist/primitives/tag/Tags.d.ts +2 -0
  145. package/dist/primitives/tag/Tags.js +2 -1
  146. package/dist/primitives/tag/TagsField.d.ts +13 -0
  147. package/dist/primitives/tag/TagsField.js +39 -10
  148. package/dist/primitives/tag/choices.d.ts +2 -0
  149. package/dist/primitives/tree/Tree.d.ts +56 -0
  150. package/dist/primitives/tree/Tree.js +74 -0
  151. package/dist/primitives/tree/index.d.ts +1 -0
  152. package/dist/primitives/tree/index.js +3 -0
  153. package/dist/primitives/value/Avatar.d.ts +16 -0
  154. package/dist/primitives/value/Avatar.js +91 -0
  155. package/dist/primitives/value/Copyable.d.ts +11 -0
  156. package/dist/primitives/value/Copyable.js +27 -6
  157. package/dist/primitives/value/Fecha.js +10 -1
  158. package/dist/primitives/value/Person.d.ts +6 -51
  159. package/dist/primitives/value/Person.js +9 -148
  160. package/dist/primitives/value/ProgressBar.d.ts +31 -0
  161. package/dist/primitives/value/ProgressBar.js +53 -0
  162. package/dist/primitives/value/Relation.d.ts +22 -5
  163. package/dist/primitives/value/Relation.js +42 -14
  164. package/dist/primitives/value/Totals.d.ts +102 -7
  165. package/dist/primitives/value/Totals.js +52 -2
  166. package/dist/primitives/value/Value.d.ts +0 -37
  167. package/dist/primitives/value/Value.js +0 -39
  168. package/dist/primitives/value/contact.d.ts +30 -0
  169. package/dist/primitives/value/contact.js +51 -0
  170. package/dist/primitives/value/index.d.ts +5 -4
  171. package/dist/primitives/value/index.js +5 -4
  172. package/dist/tokens.css +179 -7
  173. package/dist/views/list/List.d.ts +32 -3
  174. package/dist/views/list/List.js +51 -7
  175. package/dist/views/list/ListView.d.ts +1 -1
  176. package/dist/views/list/ListView.js +70 -13
  177. package/dist/views/list/columns.js +8 -1
  178. package/dist/views/list/derive.d.ts +12 -1
  179. package/dist/views/list/derive.js +29 -3
  180. package/dist/views/list/presentations.d.ts +5 -2
  181. package/dist/views/list/presentations.js +35 -3
  182. package/dist/views/list/types.d.ts +57 -1
  183. package/dist/views/list/useNarrowList.d.ts +10 -0
  184. package/dist/views/list/useNarrowList.js +20 -6
  185. package/dist/views/record/Record.d.ts +41 -8
  186. package/dist/views/record/Record.js +113 -28
  187. package/dist/views/record/index.d.ts +1 -1
  188. package/package.json +15 -4
  189. package/dist/components/chatter/Chatter.d.ts +0 -161
  190. package/dist/components/chatter/Chatter.js +0 -232
  191. package/dist/components/chatter/index.d.ts +0 -3
  192. package/dist/components/chatter/index.js +0 -4
  193. package/dist/components/navigation/CommandPalette.d.ts +0 -40
  194. package/dist/components/navigation/CommandPalette.js +0 -114
  195. package/dist/components/navigation/commands.d.ts +0 -30
  196. package/dist/components/navigation/commands.js +0 -30
  197. /package/dist/components/{chatter → activity}/Timeline.d.ts +0 -0
  198. /package/dist/components/{chatter → activity}/Timeline.js +0 -0
@@ -1,16 +1,19 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { commitDraft, filterLines, planRemoval, } from "./lines.js";
4
+ import { useColumnControl, } from "../listing/columns.js";
5
+ import { ColumnsMenu } from "../listing/ColumnsMenu.js";
4
6
  import { fillCopy, useUiCopy } from "../../foundations/copy.js";
5
7
  import { Button, IconButton } from "../../primitives/button/Button.js";
6
8
  import { SearchField } from "../../primitives/choice/SearchField.js";
7
9
  import { Dialog } from "../../primitives/dialog/Dialog.js";
8
10
  import { FieldIdsContext } from "../../primitives/field/context.js";
11
+ import { TextInput } from "../../primitives/field/Field.js";
9
12
  import { useFormMode } from "../../primitives/field/mode.js";
10
13
  import { Empty } from "../../primitives/notice/Notice.js";
11
14
  import { useToast } from "../../primitives/notice/Toast.js";
12
15
  import { EditableTable, } from "../../primitives/table/EditableTable.js";
13
- import { Totals } from "../../primitives/value/Totals.js";
16
+ import { Totals, } from "../../primitives/value/Totals.js";
14
17
  import { Pencil, Plus, Trash2 } from "lucide-react";
15
18
  import { useEffect, useId, useMemo, useRef, useState } from "react";
16
19
  /** The notice is optional: without a `ToastProvider` the line is still removed, just
@@ -35,7 +38,7 @@ function plain(v) {
35
38
  * removing asked first, what an empty document said. One component, built on
36
39
  * `EditableTable` and `Dialog`, makes the one2many the same block everywhere.
37
40
  */
38
- export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange, onAdd, newLine, onRemove, onRestore, editor, isValid, context, searchText, searchFrom = 8, totals, layout = "auto", }) {
41
+ export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange, onAdd, newLine, onRemove, onRestore, editor, isValid, context, searchText, searchFrom = 8, totals, layout = "auto", adding, onOpenLine, flag, structure, onReorder, visibleColumnIds, onVisibleColumnIdsChange, }) {
39
42
  const copy = useUiCopy();
40
43
  const toast = useOptionalToast();
41
44
  const read = useFormMode(mode) === "read";
@@ -54,7 +57,17 @@ export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange
54
57
  const canChange = !read && onChange !== undefined;
55
58
  const canAdd = !read && onAdd !== undefined && newLine !== undefined;
56
59
  const canRemove = !read && onRemove !== undefined;
57
- const canEdit = canChange && editor !== undefined;
60
+ /* The whole line opens either in this block's own dialog (`editor`) or wherever the app
61
+ opens records (`onOpenLine`, a modal of the workspace) — never both, so a line's form never
62
+ sits on top of another modal. */
63
+ const canEdit = (canChange && editor !== undefined) || onOpenLine !== undefined;
64
+ const openLine = (id, line) => {
65
+ if (onOpenLine !== undefined)
66
+ onOpenLine(id);
67
+ else
68
+ openEditor(id, line);
69
+ };
70
+ const inline = canAdd && (adding ?? (editor === undefined ? "inline" : "editor")) === "inline";
58
71
  const labelOf = (line) => {
59
72
  if (lineLabel !== undefined)
60
73
  return lineLabel(line);
@@ -63,11 +76,35 @@ export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange
63
76
  return getLineId(line);
64
77
  return first.text?.(line) ?? plain(first.value(line));
65
78
  };
66
- const searchable = lines.length > searchFrom;
79
+ const kindOf = (line) => structure?.kind(line) ?? "line";
80
+ /* Sections and notes are not lines: they are not counted, and a search leaves only lines. */
81
+ const lineCount = structure === undefined ? lines.length : lines.filter((l) => kindOf(l) === "line").length;
82
+ const searchable = lineCount > searchFrom;
67
83
  const q = searchable ? query.trim() : "";
68
- const shown = useMemo(() => filterLines(lines, q, (line) => searchText !== undefined
69
- ? searchText(line)
70
- : columns.map((c) => c.text?.(line) ?? plain(c.value(line))).join(" "), copy.locale), [lines, columns, searchText, q, copy.locale]);
84
+ const shown = useMemo(() => {
85
+ const found = filterLines(lines, q, (line) => searchText !== undefined
86
+ ? searchText(line)
87
+ : columns.map((c) => c.text?.(line) ?? plain(c.value(line))).join(" "), copy.locale);
88
+ return q === "" || structure === undefined
89
+ ? found
90
+ : found.filter((l) => structure.kind(l) === "line");
91
+ }, [lines, columns, searchText, q, copy.locale, structure]);
92
+ /* ── which columns are on screen ── */
93
+ const specs = useMemo(() => columns.map((c) => ({
94
+ id: c.id,
95
+ label: c.header,
96
+ optional: c.optional === true,
97
+ ...(c.defaultHidden === true ? { defaultHidden: true } : {}),
98
+ })), [columns]);
99
+ /* `visibleColumnIds` alone is a fixed choice, the way a controlled input without
100
+ `onChange` is: the owner said which columns, and the menu cannot change them. */
101
+ const columnControl = useColumnControl(specs, visibleColumnIds === undefined && onVisibleColumnIdsChange === undefined
102
+ ? {}
103
+ : {
104
+ ...(visibleColumnIds === undefined ? {} : { visibleIds: visibleColumnIds }),
105
+ onVisibleIdsChange: onVisibleColumnIdsChange ?? (() => { }),
106
+ });
107
+ const onScreen = columns.filter((c) => columnControl.isVisible(c.id));
71
108
  useEffect(() => {
72
109
  const target = focusNext.current;
73
110
  if (target === null)
@@ -122,6 +159,49 @@ export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange
122
159
  else
123
160
  openEditor(null, blank);
124
161
  };
162
+ /* In place: the table opens the row whose id this returns. */
163
+ const addInline = (make = newLine) => {
164
+ if (make === undefined)
165
+ return undefined;
166
+ const blank = make();
167
+ setQuery("");
168
+ onAdd?.(blank);
169
+ return getLineId(blank);
170
+ };
171
+ /* A section and a note are always written in place, editor or not. */
172
+ const bandAdds = read || onAdd === undefined || structure === undefined
173
+ ? []
174
+ : [
175
+ ...(structure.newSection === undefined
176
+ ? []
177
+ : [
178
+ {
179
+ label: copy.lines.addSection,
180
+ onAdd: () => addInline(structure.newSection),
181
+ },
182
+ ]),
183
+ ...(structure.newNote === undefined
184
+ ? []
185
+ : [{ label: copy.lines.addNote, onAdd: () => addInline(structure.newNote) }]),
186
+ ];
187
+ const inPlace = inline || bandAdds.length > 0;
188
+ const bands = structure === undefined
189
+ ? undefined
190
+ : {
191
+ kind: (line) => {
192
+ const k = structure.kind(line);
193
+ return k === "line" ? null : k;
194
+ },
195
+ render: (line) => structure.text(line),
196
+ ...(canChange
197
+ ? {
198
+ edit: (line, set) => (_jsx(TextInput, { "aria-label": structure.kind(line) === "section"
199
+ ? copy.lines.section
200
+ : copy.lines.note, value: structure.text(line), onChange: (text) => set(structure.withText(line, text)) })),
201
+ }
202
+ : {}),
203
+ total: (column, under) => columns.find((c) => c.id === column.id)?.subtotal?.(under) ?? null,
204
+ };
125
205
  const remove = (line) => {
126
206
  const id = getLineId(line);
127
207
  const { index, focus } = planRemoval(lines, shown, id, getLineId);
@@ -140,7 +220,7 @@ export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange
140
220
  });
141
221
  };
142
222
  /* ── the table ──────────────────────────────────────────────────── */
143
- const tableColumns = columns.map((c) => ({
223
+ const tableColumns = onScreen.map((c) => ({
144
224
  id: c.id,
145
225
  header: c.header,
146
226
  ...(c.width === undefined ? {} : { width: c.width }),
@@ -170,6 +250,7 @@ export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange
170
250
  would be two more targets on a small screen. Without an editor the trash
171
251
  is the only way to remove, so it stays — at the card's top right, lit. */
172
252
  stack: canEdit ? "hidden" : "trail",
253
+ onBands: true,
173
254
  render: (line) => {
174
255
  const id = getLineId(line);
175
256
  const nameId = `${formId}-line-${id}`;
@@ -178,20 +259,21 @@ export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange
178
259
  and a trash can lit on each of twenty lines are forty invitations. They
179
260
  fade instead of unmounting, so Tab still reaches them. Pressing them does
180
261
  not put the row into in-place editing. */
181
- _jsxs("span", { "data-line": id, className: "flex items-center justify-end gap-0.5 whitespace-nowrap opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 focus-within:opacity-100 in-data-stacked:opacity-100", onClick: (e) => e.stopPropagation(), children: [_jsx("span", { id: nameId, className: "sr-only", children: labelOf(line) }), canEdit ? (_jsx(IconButton, { icon: Pencil, label: copy.lines.edit, "aria-describedby": nameId, haspopup: "dialog", onClick: () => openEditor(id, line) })) : null, canRemove ? (_jsx(IconButton, { icon: Trash2, label: copy.lines.remove, tone: "danger", "aria-describedby": nameId, onClick: () => remove(line) })) : null] }));
262
+ _jsxs("span", { "data-line": id, className: "flex items-center justify-end gap-0.5 whitespace-nowrap opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 focus-within:opacity-100 in-data-stacked:opacity-100", onClick: (e) => e.stopPropagation(), children: [_jsx("span", { id: nameId, className: "sr-only", children: labelOf(line) }), canEdit && kindOf(line) === "line" ? (_jsx(IconButton, { icon: Pencil, label: copy.lines.edit, "aria-describedby": nameId, haspopup: "dialog", onClick: () => openLine(id, line) })) : null, canRemove ? (_jsx(IconButton, { icon: Trash2, label: copy.lines.remove, tone: "danger", "aria-describedby": nameId, onClick: () => remove(line) })) : null] }));
182
263
  },
183
264
  });
184
265
  }
185
- const addButton = canAdd ? (_jsx(Button, { role: "secondary", size: "sm", icon: Plus, onClick: add, "data-line-add": "", children: copy.lines.add })) : null;
266
+ /* In place, «add» is the table's own row under the grid: where the line will appear. */
267
+ const addButton = canAdd && !inline ? (_jsx(Button, { role: "secondary", size: "sm", icon: Plus, onClick: add, "data-line-add": "", children: copy.lines.add })) : null;
186
268
  const count = q === ""
187
- ? fillCopy(lines.length === 1 ? copy.lineItems.one : copy.lineItems.many, {
188
- count: lines.length.toLocaleString(copy.locale),
269
+ ? fillCopy(lineCount === 1 ? copy.lineItems.one : copy.lineItems.many, {
270
+ count: lineCount.toLocaleString(copy.locale),
189
271
  })
190
272
  : fillCopy(copy.lineItems.shown, {
191
273
  shown: shown.length.toLocaleString(copy.locale),
192
- count: lines.length.toLocaleString(copy.locale),
274
+ count: lineCount.toLocaleString(copy.locale),
193
275
  });
194
- return (_jsxs("section", { ref: root, "aria-label": copy.lineItems.table, className: "flex min-w-0 flex-col gap-4", children: [lines.length === 0 ? (
276
+ return (_jsxs("section", { ref: root, "aria-label": copy.lineItems.table, className: "flex min-w-0 flex-col gap-4", children: [lines.length === 0 && !canAdd ? (
195
277
  /* Said, with its way out: an empty document is a question («what is sold
196
278
  here?»), not a blank box. In read mode there is no way out to offer. */
197
279
  _jsx(Empty, { align: "start", sentence: copy.lines.empty, action: addButton ?? undefined })) : (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2 px-1", children: [searchable ? (_jsxs("div", { className: "w-full max-w-(--container-search) min-w-0 flex-1", children: [_jsx("span", { id: searchLabelId, className: "sr-only", children: copy.lines.search }), _jsx(FieldIdsContext.Provider, { value: {
@@ -199,19 +281,25 @@ export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange
199
281
  labelId: searchLabelId,
200
282
  describedBy: undefined,
201
283
  invalid: false,
202
- }, children: _jsx(SearchField, { value: query, onChange: setQuery, placeholder: copy.lines.search }) })] })) : null, _jsx("span", { className: "text-meta text-muted tabular-nums", "aria-live": "polite", children: count }), addButton === null ? null : _jsx("span", { className: "ml-auto", children: addButton })] }), _jsx(EditableTable, { rows: shown, columns: tableColumns, getRowId: getLineId, empty: fillCopy(copy.lineItems.noMatch, { query: query.trim() }), layout: layout, ...(canEdit
284
+ }, children: _jsx(SearchField, { value: query, onChange: setQuery, placeholder: copy.lines.search }) })] })) : null, _jsx("span", { className: "text-meta text-muted tabular-nums", "aria-live": "polite", children: count }), columnControl.hasOptional || addButton !== null ? (_jsxs("span", { className: "ml-auto flex items-center gap-2", children: [_jsx(ColumnsMenu, { control: columnControl }), addButton] })) : null] }), _jsx(EditableTable, { rows: shown, columns: tableColumns, getRowId: getLineId, empty: lines.length === 0
285
+ ? copy.lines.empty
286
+ : fillCopy(copy.lineItems.noMatch, { query: query.trim() }), layout: layout, ...(inline ? { onAdd: () => addInline(), addLabel: copy.lines.add } : {}), ...(bandAdds.length === 0 ? {} : { adds: bandAdds }), ...(bands === undefined ? {} : { bands }), ...(flag === undefined ? {} : { flag }), ...(onReorder === undefined || read ? {} : { onReorder }), ...(inPlace && onRemove !== undefined
287
+ ? /* A blank row nobody wrote in is taken back without a notice: nobody
288
+ removed anything, so there is nothing to undo. */
289
+ { onDiscard: onRemove }
290
+ : {}), ...(canEdit
203
291
  ? {
204
292
  onOpen: (id) => {
205
293
  const line = lines.find((l) => getLineId(l) === id);
206
294
  if (line !== undefined)
207
- openEditor(id, line);
295
+ openLine(id, line);
208
296
  },
209
297
  }
210
298
  : {}), ...(canChange && columns.some((c) => c.edit !== undefined)
211
299
  ? { onChange: (id, next) => onChange?.(id, next) }
212
- : {}) })] })), totals === undefined || totals.length === 0 ? null : (
300
+ : {}) })] })), totals === undefined || ("length" in totals && totals.length === 0) ? null : (
213
301
  /* Inset like the table's cells, so the last figure lines up with the amounts. */
214
- _jsx("div", { className: "px-4", children: _jsx(Totals, { rows: totals }) })), editor === undefined || session === null ? null : (_jsx(Dialog, { open: open, width: "sm", title: session.id === null ? copy.lineItems.newLine : copy.lines.edit, ...(context === undefined ? {} : { context }), ...(session.id === null ? {} : { subtitle: labelOf(session.draft) }), onClose: () => setOpen(false), footer: _jsxs(_Fragment, { children: [session.id === null || !canRemove ? null : (
302
+ _jsx("div", { className: "px-4", children: "length" in totals ? _jsx(Totals, { rows: totals }) : _jsx(Totals, { ...totals }) })), editor === undefined || session === null ? null : (_jsx(Dialog, { open: open, width: "sm", title: session.id === null ? copy.lineItems.newLine : copy.lines.edit, ...(context === undefined ? {} : { context }), ...(session.id === null ? {} : { subtitle: labelOf(session.draft) }), onClose: () => setOpen(false), footer: _jsxs(_Fragment, { children: [session.id === null || !canRemove ? null : (
215
303
  /* Apart, at the other end from «Listo»: removing is not the way the
216
304
  form is meant to close. */
217
305
  _jsx("span", { className: "mr-auto", children: _jsx(IconButton, { icon: Trash2, label: copy.lines.remove, tone: "danger", onClick: () => {
@@ -0,0 +1,39 @@
1
+ import type { Tone } from "../../foundations/types.js";
2
+ import { type ReactNode } from "react";
3
+ /** One stage of a board: its id (what `of` returns), its name, and its tone. */
4
+ export type BoardColumn = {
5
+ id: string;
6
+ label: string;
7
+ tone?: Tone;
8
+ };
9
+ /** How a board lays its cards in stages, and what moving one means. */
10
+ export type BoardStages<T> = {
11
+ /** The column a record goes in: its stage — or ANY field it is grouped by (the salesperson,
12
+ * the priority, the tag): a board is a grouping you can drag across. */
13
+ of: (item: T) => string;
14
+ /** The columns, in order. Without it they come from the records themselves, in the order
15
+ * they appear, named by `labelOf`. */
16
+ columns?: readonly BoardColumn[];
17
+ labelOf?: (id: string) => string;
18
+ /** A card dropped in another stage. Without it the board is read-only: nothing drags. */
19
+ onMove?: (item: T, to: string) => void;
20
+ /** How a record is named when a move is announced: «S00142 pasó a pedido». */
21
+ nameOf?: (item: T) => string;
22
+ /** The figure on a column's head: the expected revenue of the opportunities in it. */
23
+ total?: (items: readonly T[]) => ReactNode;
24
+ /** The bar under a column's head: how its records stand, by tone — Odoo's activity bar
25
+ * (overdue red, today amber, planned green). `undefined` for a record with nothing to say. */
26
+ progress?: (item: T) => Tone | undefined;
27
+ /** «+» on a column's head: a name typed there creates a record in that column. */
28
+ onQuickCreate?: (column: string, name: string) => void;
29
+ /** A column at the end: «Agregar etapa». */
30
+ onAddColumn?: (label: string) => void;
31
+ /** Columns that start folded into a narrow strip (the won, the lost). */
32
+ defaultFolded?: readonly string[];
33
+ };
34
+ export declare function BoardColumns<T>({ items, keyOf, stages, card, }: {
35
+ items: readonly T[];
36
+ keyOf: (item: T) => string;
37
+ stages: BoardStages<T>;
38
+ card: (item: T) => ReactNode;
39
+ }): import("react").JSX.Element;
@@ -0,0 +1,138 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { fillCopy, useUiCopy } from "../../foundations/copy.js";
4
+ import { IconButton } from "../../primitives/button/Button.js";
5
+ import { StatusDot } from "../../primitives/tag/Tag.js";
6
+ import { ChevronsLeftRight, Plus } from "lucide-react";
7
+ import { useState } from "react";
8
+ /* The tones a progress bar draws, in the order they are read: what is wrong first. */
9
+ const PROGRESS_ORDER = ["bad", "warn", "good", "info", "neutral", "accent"];
10
+ const PROGRESS_FILL = {
11
+ bad: "bg-bad",
12
+ warn: "bg-warn",
13
+ good: "bg-good",
14
+ info: "bg-info",
15
+ neutral: "bg-muted",
16
+ accent: "bg-primary",
17
+ };
18
+ /** One-line input of a board: quick create, a new column. Enter keeps it, Escape leaves. */
19
+ function BoardInput({ label, placeholder, onCommit, onClose, }) {
20
+ const [text, setText] = useState("");
21
+ return (_jsx("input", { autoFocus: true, "aria-label": label, placeholder: placeholder, value: text, onChange: (event) => setText(event.target.value), onKeyDown: (event) => {
22
+ if (event.key === "Escape")
23
+ onClose();
24
+ if (event.key === "Enter" && text.trim() !== "") {
25
+ onCommit(text.trim());
26
+ setText("");
27
+ }
28
+ }, onBlur: () => {
29
+ if (text.trim() === "")
30
+ onClose();
31
+ }, className: "h-(--size-control-md) w-full min-w-0 rounded-control border border-primary/60 bg-surface px-2.5 text-meta text-ink outline-none placeholder:text-muted" }));
32
+ }
33
+ /** The column's bar: its records counted by tone, what is wrong first. Said in words too. */
34
+ function ProgressBar({ items, progress, }) {
35
+ const counts = new Map();
36
+ for (const item of items) {
37
+ const tone = progress(item);
38
+ if (tone !== undefined)
39
+ counts.set(tone, (counts.get(tone) ?? 0) + 1);
40
+ }
41
+ const parts = PROGRESS_ORDER.filter((tone) => (counts.get(tone) ?? 0) > 0);
42
+ if (parts.length === 0)
43
+ return null;
44
+ return (_jsx("div", { role: "img", "aria-label": parts.map((tone) => `${tone}: ${counts.get(tone)}`).join(", "), className: "mx-1 flex h-1.5 overflow-hidden rounded-pill bg-ink/8", children: parts.map((tone) => (_jsx("span", { className: PROGRESS_FILL[tone], style: { width: `${((counts.get(tone) ?? 0) / items.length) * 100}%` } }, tone))) }));
45
+ }
46
+ /* ==================================================================== *
47
+ * THE BOARD BY STAGES — Odoo's kanban.
48
+ *
49
+ * Context: a pipeline (quotations → orders → invoiced, leads → won) is read across: how many
50
+ * in each stage, and which is stuck. One column per stage, the count on its head, and a card
51
+ * moved by dragging it to another column — the gesture IS the change of stage.
52
+ *
53
+ * Dragging is not the only way: a focused card moves with Alt + ← / →, and the move is said
54
+ * out loud. A board that only drags is a board a keyboard cannot use.
55
+ * ==================================================================== */
56
+ export function BoardColumns({ items, keyOf, stages, card, }) {
57
+ const copy = useUiCopy();
58
+ const { of, onMove, nameOf, total, progress, onQuickCreate, onAddColumn } = stages;
59
+ const columns = stages.columns ??
60
+ [...new Set(items.map(of))].map((id) => ({ id, label: stages.labelOf?.(id) ?? id }));
61
+ const [folded, setFolded] = useState(() => new Set(stages.defaultFolded ?? []));
62
+ const [creating, setCreating] = useState(null);
63
+ const [adding, setAdding] = useState(false);
64
+ const [dragging, setDragging] = useState(null);
65
+ const [over, setOver] = useState(null);
66
+ const [said, setSaid] = useState("");
67
+ const byId = new Map(items.map((item) => [keyOf(item), item]));
68
+ const move = (id, to) => {
69
+ const item = byId.get(id);
70
+ if (item === undefined || of(item) === to || onMove === undefined)
71
+ return;
72
+ onMove(item, to);
73
+ const column = columns.find((one) => one.id === to)?.label ?? to;
74
+ setSaid(fillCopy(copy.board.moved, { name: nameOf?.(item) ?? id, column }));
75
+ };
76
+ const toggleFold = (id) => setFolded((current) => {
77
+ const next = new Set(current);
78
+ if (next.has(id))
79
+ next.delete(id);
80
+ else
81
+ next.add(id);
82
+ return next;
83
+ });
84
+ const dropOn = (id) => onMove === undefined
85
+ ? {}
86
+ : {
87
+ onDragOver: (event) => {
88
+ event.preventDefault();
89
+ setOver(id);
90
+ },
91
+ onDragLeave: () => setOver(null),
92
+ onDrop: (event) => {
93
+ event.preventDefault();
94
+ move(event.dataTransfer.getData("text/plain"), id);
95
+ setOver(null);
96
+ setDragging(null);
97
+ },
98
+ };
99
+ const byKeyboard = (event, item) => {
100
+ if (!event.altKey || (event.key !== "ArrowLeft" && event.key !== "ArrowRight"))
101
+ return;
102
+ const at = columns.findIndex((one) => one.id === of(item));
103
+ const next = columns[at + (event.key === "ArrowLeft" ? -1 : 1)];
104
+ if (next === undefined)
105
+ return;
106
+ event.preventDefault();
107
+ move(keyOf(item), next.id);
108
+ };
109
+ return (_jsxs("div", { className: "flex min-w-0 items-start gap-3 overflow-x-auto p-3", children: [columns.map((column) => {
110
+ const inside = items.filter((item) => of(item) === column.id);
111
+ if (folded.has(column.id)) {
112
+ /* Folded: a strip with its count and its name down the spine. Still a drop target —
113
+ dropping on «perdido» is how a lost deal is marked. */
114
+ return (_jsxs("section", { "aria-label": column.label, ...dropOn(column.id), className: `flex w-12 shrink-0 flex-col items-center gap-2 rounded-box border py-2 ${over === column.id ? "border-primary/50 bg-primary/6" : "border-line bg-ink/3"}`, children: [_jsx(IconButton, { icon: ChevronsLeftRight, label: fillCopy(copy.board.unfold, { column: column.label }), onClick: () => toggleFold(column.id) }), _jsx("span", { className: "text-micro text-muted tabular-nums", children: inside.length }), _jsx("span", { className: "text-vertical text-meta font-semibold text-ink", children: column.label })] }, column.id));
115
+ }
116
+ return (_jsxs("section", { "aria-label": column.label, ...dropOn(column.id), className: `flex w-(--container-lane) shrink-0 flex-col gap-2 rounded-box border p-2 transition-colors duration-(--duration-instant) ${over === column.id ? "border-primary/50 bg-primary/6" : "border-line bg-ink/3"}`, children: [_jsxs("header", { className: "flex min-w-0 items-center gap-1.5 px-1 py-0.5", children: [_jsx("h3", { className: "min-w-0 flex-1 truncate text-meta font-semibold text-ink", children: column.tone === undefined ? (column.label) : (_jsx(StatusDot, { tone: column.tone, label: column.label })) }), _jsx("span", { className: "shrink-0 text-micro text-muted tabular-nums", children: inside.length }), onQuickCreate === undefined ? null : (_jsx(IconButton, { icon: Plus, label: fillCopy(copy.board.quickCreate, { column: column.label }), onClick: () => setCreating(column.id) })), _jsx(IconButton, { icon: ChevronsLeftRight, label: fillCopy(copy.board.fold, { column: column.label }), onClick: () => toggleFold(column.id) })] }), total === undefined ? null : (_jsx("p", { className: "px-1 text-meta font-semibold text-ink tabular-nums", children: total(inside) })), progress === undefined || inside.length === 0 ? null : (_jsx(ProgressBar, { items: inside, progress: progress })), creating === column.id && onQuickCreate !== undefined ? (_jsx(BoardInput, { label: fillCopy(copy.board.quickCreate, { column: column.label }), placeholder: copy.board.quickPlaceholder, onCommit: (name) => onQuickCreate(column.id, name), onClose: () => setCreating(null) })) : null, inside.length === 0 ? (_jsx("p", { className: "px-1 py-3 text-micro text-muted", children: copy.board.emptyLane })) : (_jsx("ul", { className: "flex min-w-0 flex-col gap-2", children: inside.map((item) => {
117
+ const id = keyOf(item);
118
+ return (_jsx("li", { ...(onMove === undefined
119
+ ? {}
120
+ : {
121
+ draggable: true,
122
+ tabIndex: 0,
123
+ title: copy.board.moveHint,
124
+ "aria-keyshortcuts": "Alt+ArrowLeft Alt+ArrowRight",
125
+ onDragStart: (event) => {
126
+ event.dataTransfer.setData("text/plain", id);
127
+ event.dataTransfer.effectAllowed = "move";
128
+ setDragging(id);
129
+ },
130
+ onDragEnd: () => setDragging(null),
131
+ onKeyDown: (event) => byKeyboard(event, item),
132
+ }), className: `flex min-w-0 flex-col rounded-box focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary ${onMove === undefined ? "" : "cursor-grab active:cursor-grabbing"} ${dragging === id ? "opacity-50" : ""}`, children: card(item) }, id));
133
+ }) }))] }, column.id));
134
+ }), onAddColumn === undefined ? null : (_jsx("div", { className: "flex w-(--container-lane) shrink-0 flex-col gap-2 p-2", children: adding ? (_jsx(BoardInput, { label: copy.board.addColumn, placeholder: copy.board.addColumnPlaceholder, onCommit: (label) => {
135
+ onAddColumn(label);
136
+ setAdding(false);
137
+ }, onClose: () => setAdding(false) })) : (_jsxs("button", { type: "button", onClick: () => setAdding(true), className: "flex h-(--size-control-md) items-center gap-1.5 rounded-control px-2 text-meta text-muted transition-colors duration-(--duration-instant) hover:bg-ink/5 hover:text-ink focus-visible:outline-2 focus-visible:outline-primary", children: [_jsx(Plus, { "aria-hidden": true, className: "size-(--size-icon-sm)", strokeWidth: 2 }), copy.board.addColumn] })) })), _jsx("span", { role: "status", className: "sr-only", children: said })] }));
138
+ }
@@ -82,5 +82,5 @@ export function Listing({ bar, metrics, sticky = false, surface, children, }) {
82
82
  background is its white and its radius; a frame around a list that is
83
83
  already inside a framed panel is a nested rectangle, and nested
84
84
  rectangles are what make a product read like a cloud console. */
85
- _jsxs("div", { className: `flex min-w-0 flex-col ${byDensity(d, "gap-2.5", "gap-2")}`, children: [inSlot ? (createPortal(bar, slot)) : (_jsx("div", { ref: band.ref, className: `min-w-0 ${pegada ? `sticky z-30 ${surface === "chrome" ? "bg-chrome" : "bg-ground"}` : ""}`, style: pegada ? band.self : undefined, children: bar })), inSlot && metrics !== undefined ? _jsx("div", { className: "pt-2", children: metrics }) : metrics, _jsx("div", { className: "min-w-0 overflow-clip rounded-box bg-surface", style: pegada && !inSlot ? band.below : undefined, children: children })] }));
85
+ _jsxs("div", { className: `flex min-w-0 flex-col ${byDensity(d, "gap-2.5", "gap-2")}`, children: [bar === undefined || bar === null ? null : inSlot ? (createPortal(bar, slot)) : (_jsx("div", { ref: band.ref, className: `min-w-0 ${pegada ? `sticky z-30 ${surface === "chrome" ? "bg-chrome" : "bg-ground"}` : ""}`, style: pegada ? band.self : undefined, children: bar })), inSlot && metrics !== undefined ? _jsx("div", { className: "pt-2", children: metrics }) : metrics, _jsx("div", { className: "min-w-0 overflow-clip rounded-box bg-surface", style: pegada && !inSlot ? band.below : undefined, children: children })] }));
86
86
  }
@@ -32,8 +32,40 @@ import type { ReactNode } from "react";
32
32
  * checkboxes, and the row they were about to tick is no longer where it was.
33
33
  *
34
34
  * The actions on the left do NOT go away: «Nuevo» stays available with three
35
- * rows selected, because selecting is not entering a mode. */
36
- export declare function ListingBar({ actions, title, description, counters, selection, search, pagination, views, crumbs, surface, }: {
35
+ * rows selected, because selecting is not entering a mode.
36
+ *
37
+ * ── Inside a record it is ONE line (`inline`) ────────────────────────
38
+ *
39
+ * ┌───────────────────────────────────────────────────────────────┐
40
+ * │ 🔍 search the lines… 12 records [sort][cols][▤▦] │
41
+ * └───────────────────────────────────────────────────────────────┘
42
+ * WHAT I LOOK FOR (fills) HOW MANY HOW I LOOK
43
+ *
44
+ * A collection that is a FIELD of a record — the lines of an order, the
45
+ * contacts of a customer, the journal items of an invoice — is not a screen.
46
+ * Everything the two rows above answer is already answered by the record
47
+ * around it: where I am (the record's crumbs), what this is (the tab it sits
48
+ * in), what I can do (the record's actions). Drawn with the page bar, it
49
+ * spent two rows and a rule on the one question left —the search, alone and
50
+ * centred on the first row; «Ordenar» and the switcher alone on the second—
51
+ * and pushed the rows, which are what the tab is for, below the fold.
52
+ *
53
+ * So it is a single line, and the order is the order of the questions: the
54
+ * search FILLS from the left (it is the only thing with variable content),
55
+ * then how many there are, then how they are looked at, then what starts a
56
+ * new one. No band, no padding, no hairline: it sits on the record's paper,
57
+ * aligned with the fields above it, and a light band inside a white sheet is
58
+ * a rectangle inside a rectangle.
59
+ *
60
+ * On a phone the line breaks by itself, with no threshold written anywhere:
61
+ * the search asks for `--container-name` before it gives up its place, so
62
+ * when the controls do not fit next to it they drop to a second line, pushed
63
+ * to the right, and the search keeps the whole first one.
64
+ *
65
+ * The count is a live region: typing in the search changes it, and somebody
66
+ * who cannot see the rows thin out hears how many are left. With
67
+ * `pagination` the page range replaces it — it already says how many. */
68
+ export declare function ListingBar({ actions, title, description, counters, selection, search, pagination, views, crumbs, surface, inline, count, }: {
37
69
  /** What starts something new. It stays visible even when there is a selection. */
38
70
  actions?: ReactNode;
39
71
  title?: string;
@@ -51,6 +83,11 @@ export declare function ListingBar({ actions, title, description, counters, sele
51
83
  count: number;
52
84
  onClear?: () => void;
53
85
  actions?: ReactNode;
86
+ /** More match than are chosen: offer to choose them all. */
87
+ matching?: {
88
+ total: number;
89
+ onSelect: () => void;
90
+ };
54
91
  };
55
92
  search?: ReactNode;
56
93
  pagination?: ReactNode;
@@ -62,6 +99,13 @@ export declare function ListingBar({ actions, title, description, counters, sele
62
99
  /** `chrome` paints the whole band navy and everything inside inherits it:
63
100
  * the search box, the pagination and the switcher. See `system/surface.tsx`. */
64
101
  surface?: Surface;
102
+ /** One line, for a collection inside a record: search, count, views, actions. The
103
+ * title, the description, the counters and the crumbs are not drawn — the record
104
+ * around it already says them. Always on the canvas. See the header. */
105
+ inline?: boolean;
106
+ /** How many records there are, said on the inline line when there is no
107
+ * `pagination` to say it. Ignored by the two-row bar: its identity has counters. */
108
+ count?: number;
65
109
  }): import("react").JSX.Element;
66
110
  export type ListIdentity = {
67
111
  title?: string;
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useUiCopy } from "../../foundations/copy.js";
3
+ import { fillCopy, useUiCopy } from "../../foundations/copy.js";
4
4
  import { byDensity, useDensity } from "../../foundations/density.js";
5
5
  import { SurfaceProvider, useSurface } from "../../foundations/surface.js";
6
6
  import { Line } from "../../primitives/block/Line.js";
@@ -37,12 +37,44 @@ import { X } from "lucide-react";
37
37
  * checkboxes, and the row they were about to tick is no longer where it was.
38
38
  *
39
39
  * The actions on the left do NOT go away: «Nuevo» stays available with three
40
- * rows selected, because selecting is not entering a mode. */
41
- export function ListingBar({ actions, title, description, counters, selection, search, pagination, views, crumbs, surface, }) {
40
+ * rows selected, because selecting is not entering a mode.
41
+ *
42
+ * ── Inside a record it is ONE line (`inline`) ────────────────────────
43
+ *
44
+ * ┌───────────────────────────────────────────────────────────────┐
45
+ * │ 🔍 search the lines… 12 records [sort][cols][▤▦] │
46
+ * └───────────────────────────────────────────────────────────────┘
47
+ * WHAT I LOOK FOR (fills) HOW MANY HOW I LOOK
48
+ *
49
+ * A collection that is a FIELD of a record — the lines of an order, the
50
+ * contacts of a customer, the journal items of an invoice — is not a screen.
51
+ * Everything the two rows above answer is already answered by the record
52
+ * around it: where I am (the record's crumbs), what this is (the tab it sits
53
+ * in), what I can do (the record's actions). Drawn with the page bar, it
54
+ * spent two rows and a rule on the one question left —the search, alone and
55
+ * centred on the first row; «Ordenar» and the switcher alone on the second—
56
+ * and pushed the rows, which are what the tab is for, below the fold.
57
+ *
58
+ * So it is a single line, and the order is the order of the questions: the
59
+ * search FILLS from the left (it is the only thing with variable content),
60
+ * then how many there are, then how they are looked at, then what starts a
61
+ * new one. No band, no padding, no hairline: it sits on the record's paper,
62
+ * aligned with the fields above it, and a light band inside a white sheet is
63
+ * a rectangle inside a rectangle.
64
+ *
65
+ * On a phone the line breaks by itself, with no threshold written anywhere:
66
+ * the search asks for `--container-name` before it gives up its place, so
67
+ * when the controls do not fit next to it they drop to a second line, pushed
68
+ * to the right, and the search keeps the whole first one.
69
+ *
70
+ * The count is a live region: typing in the search changes it, and somebody
71
+ * who cannot see the rows thin out hears how many are left. With
72
+ * `pagination` the page range replaces it — it already says how many. */
73
+ export function ListingBar({ actions, title, description, counters, selection, search, pagination, views, crumbs, surface, inline = false, count, }) {
42
74
  const selecting = selection !== undefined && selection.count > 0;
43
75
  const d = useDensity();
44
76
  const copy = useUiCopy();
45
- const activeSurface = useSurface(surface);
77
+ const activeSurface = useSurface(inline ? "canvas" : surface);
46
78
  const isChrome = activeSurface === "chrome";
47
79
  /* In compact the title and the description are NOT drawn, and it is not for
48
80
  lack of room: in a preview or on a phone, the surface header already says
@@ -56,11 +88,21 @@ export function ListingBar({ actions, title, description, counters, selection, s
56
88
  Views used to sit next to the pagination. On a narrow window the search
57
89
  field and «1–80 / 211» painted over each other; columns and the switcher
58
90
  were the extra weight on a line that only needed how MANY. */
59
- const center = selecting ? (_jsxs("div", { className: "flex min-w-0 items-center gap-x-2", children: [_jsxs("span", { className: `inline-flex h-(--size-control-md) shrink-0 items-center gap-1.5 rounded-control pr-1 pl-2.5 text-meta font-medium ${isChrome ? "bg-chrome-ink/14 text-chrome-ink" : "bg-ink/8 text-ink"}`, children: [_jsx("span", { className: "tabular-nums", children: selection?.count.toLocaleString(copy.locale) }), copy.selection.chosen, _jsx("button", { type: "button", onClick: selection?.onClear, "aria-label": copy.selection.clear, title: copy.selection.clear, className: `inline-flex size-5 items-center justify-center rounded-pill transition-colors duration-(--duration-instant) ${isChrome ? "hover:bg-chrome-ink/20" : "hover:bg-primary/20"}`, children: _jsx(X, { className: "size-3", strokeWidth: 2.5 }) })] }), selection?.actions === undefined ? null : (_jsx("div", { className: "flex min-w-0 items-center gap-1.5", children: selection.actions }))] })) : search === undefined ? null : (
91
+ const center = selecting ? (_jsxs("div", { className: "flex min-w-0 items-center gap-x-2", children: [_jsxs("span", { className: `inline-flex h-(--size-control-md) shrink-0 items-center gap-1.5 rounded-control pr-1 pl-2.5 text-meta font-medium ${isChrome ? "bg-chrome-ink/14 text-chrome-ink" : "bg-ink/8 text-ink"}`, children: [_jsx("span", { className: "tabular-nums", children: selection?.count.toLocaleString(copy.locale) }), copy.selection.chosen, _jsx("button", { type: "button", onClick: selection?.onClear, "aria-label": copy.selection.clear, title: copy.selection.clear, className: `inline-flex size-5 items-center justify-center rounded-pill transition-colors duration-(--duration-instant) ${isChrome ? "hover:bg-chrome-ink/20" : "hover:bg-primary/20"}`, children: _jsx(X, { className: "size-3", strokeWidth: 2.5 }) })] }), selection?.matching === undefined ||
92
+ selection.matching.total <= selection.count ? null : (_jsx("button", { type: "button", onClick: selection.matching.onSelect, className: `shrink-0 rounded-control px-1 text-meta font-medium underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary ${isChrome ? "text-chrome-link" : "text-primary"}`, children: fillCopy(copy.selection.allMatching, {
93
+ count: selection.matching.total.toLocaleString(copy.locale),
94
+ }) })), selection?.actions === undefined ? null : (_jsx("div", { className: "flex min-w-0 items-center gap-1.5", children: selection.actions }))] })) : search === undefined ? null : (
60
95
  /* Selecting replaces the search box: what gets selected is over what is
61
96
  ALREADY filtered, so searching there would change the set underneath the
62
97
  selection. */
63
98
  _jsx("div", { className: "w-full min-w-0", children: search }));
99
+ if (inline) {
100
+ const howMany = pagination ??
101
+ (count === undefined ? null : (_jsx("span", { role: "status", className: "shrink-0 text-meta text-muted tabular-nums", children: count === 1
102
+ ? copy.list.countOne
103
+ : fillCopy(copy.list.count, { count: count.toLocaleString(copy.locale) }) })));
104
+ return (_jsx(SurfaceProvider, { value: "canvas", children: _jsxs("div", { className: `flex min-w-0 flex-wrap items-center gap-y-1.5 ${byDensity(d, "gap-x-3", "gap-x-2")}`, children: [title === undefined ? null : (_jsx("h3", { className: "min-w-0 shrink-0 truncate text-body font-semibold text-ink", children: title })), center === null ? null : (_jsx("div", { className: "flex min-w-0 flex-1 basis-(--container-name) items-center overflow-x-clip overflow-y-visible", children: center })), howMany, views === undefined ? null : (_jsx("div", { className: "ml-auto flex shrink-0 items-center gap-2", children: views })), actions === undefined ? null : (_jsx("div", { className: `flex shrink-0 items-center gap-1.5 ${views === undefined ? "ml-auto" : ""}`, children: actions }))] }) }));
105
+ }
64
106
  /* El campo de búsqueda va en el CENTRO de la línea, y centrado contra la
65
107
  línea: pide `--container-search` y los dos lados se reparten lo que sobra
66
108
  en mitades iguales. Antes tomaba el resto, así que un rastro largo a la
@@ -0,0 +1,34 @@
1
+ import type { GroupableField, GroupLevel, PileOf } from "../../foundations/grouping.js";
2
+ import { type PivotFold, type PivotKey } from "../../foundations/pivot.js";
3
+ export type PivotExplorerProps<T> = {
4
+ items: readonly T[];
5
+ /** Which pile a record falls in, at a level — the same contract that groups a list. */
6
+ of: PileOf<T>;
7
+ /** What can be split by: a field, and its grains when it is a date. */
8
+ fields: readonly GroupableField[];
9
+ /** Every measure that can be shown; `defaultMeasures` says which start on. */
10
+ measures: readonly PivotFold<T>[];
11
+ defaultMeasures?: readonly string[];
12
+ /** The rows' levels, CONTROLLED from outside — the search bar's grouping. While given, the
13
+ * rows follow it and are not edited here. */
14
+ rows?: readonly GroupLevel[];
15
+ defaultRows?: readonly GroupLevel[];
16
+ defaultColumns?: readonly GroupLevel[];
17
+ /** A cell was pressed: open the records that fell there. */
18
+ onCell?: (row: PivotKey, column: PivotKey) => void;
19
+ height?: number | string;
20
+ /** The file's name when downloaded. */
21
+ fileName?: string;
22
+ };
23
+ /**
24
+ * A pivot people actually pivot: what goes down the rows, what goes across the columns, which
25
+ * measures — chosen on the grid, not fixed in code.
26
+ *
27
+ * Context: the fold (`pivotFrom`) was already a real pivot — N levels each way, margins folded
28
+ * from the base — but its axes and measures were written once by whoever composed it. Here
29
+ * they are the person's: «+» adds a field (a date with its grain) to an axis, a level's ×
30
+ * drops it, ⇄ swaps the axes, the measures are ticked, and the grid downloads as it is seen.
31
+ * Inside a list the rows FOLLOW the search bar's grouping and the records are the ones the
32
+ * search left: the pivot answers the same question the list is asking.
33
+ */
34
+ export declare function PivotExplorer<T>({ items, of, fields, measures, defaultMeasures, rows: outerRows, defaultRows, defaultColumns, onCell, height, fileName, }: PivotExplorerProps<T>): import("react").JSX.Element;