@sincpro/web-ui 1.2.0 → 1.4.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 (130) hide show
  1. package/dist/components/activity/Activity.js +1 -1
  2. package/dist/components/activity/Messages.d.ts +4 -2
  3. package/dist/components/activity/Messages.js +4 -4
  4. package/dist/components/activity/Timeline.js +1 -1
  5. package/dist/components/assist/AssistMark.js +1 -1
  6. package/dist/components/assist/AssistSuggestion.js +1 -1
  7. package/dist/components/assist/ChangeReview.js +2 -2
  8. package/dist/components/calendar/RecordCalendar.js +4 -1
  9. package/dist/components/form/Sheet.js +5 -2
  10. package/dist/components/lines/LineItems.d.ts +1 -1
  11. package/dist/components/lines/LineItems.js +2 -2
  12. package/dist/components/listing/ListingBar.js +1 -1
  13. package/dist/components/listing/RecordCard.d.ts +4 -0
  14. package/dist/components/listing/RecordCard.js +4 -1
  15. package/dist/components/listing/Row.d.ts +7 -0
  16. package/dist/components/listing/Row.js +7 -1
  17. package/dist/components/listing/ViewSwitcher.d.ts +7 -2
  18. package/dist/components/listing/index.d.ts +1 -1
  19. package/dist/components/listing/set.d.ts +4 -1
  20. package/dist/components/record/Breadcrumb.js +5 -1
  21. package/dist/components/record/DirtyActions.d.ts +20 -6
  22. package/dist/components/record/DirtyActions.js +55 -4
  23. package/dist/components/record/RecordHeader.d.ts +1 -1
  24. package/dist/components/record/RecordHeader.js +32 -13
  25. package/dist/components/record/RecordSkeleton.js +1 -1
  26. package/dist/components/search/CriteriaBuilder.d.ts +1 -1
  27. package/dist/components/search/CriteriaBuilder.js +3 -3
  28. package/dist/components/search/FilterBuilder.d.ts +1 -18
  29. package/dist/components/search/FilterBuilder.js +4 -4
  30. package/dist/components/search/JsonEditor.js +5 -1
  31. package/dist/components/search/SearchBar.d.ts +1 -2
  32. package/dist/components/search/SearchBar.js +12 -3
  33. package/dist/components/search/criteria.d.ts +17 -1
  34. package/dist/components/search/index.d.ts +2 -1
  35. package/dist/components/search/index.js +1 -0
  36. package/dist/components/shell/Panes.d.ts +18 -0
  37. package/dist/components/shell/Panes.js +36 -0
  38. package/dist/components/shell/index.d.ts +1 -0
  39. package/dist/components/shell/index.js +2 -0
  40. package/dist/foundations/calendar.d.ts +3 -1
  41. package/dist/foundations/catalog.d.ts +27 -0
  42. package/dist/foundations/catalog.js +48 -0
  43. package/dist/foundations/copy.d.ts +40 -62
  44. package/dist/foundations/copy.es.js +10 -0
  45. package/dist/foundations/copy.js +25 -93
  46. package/dist/{primitives/field/context.d.ts → foundations/form.d.ts} +9 -0
  47. package/dist/{primitives/field/mode.js → foundations/form.js} +11 -0
  48. package/dist/foundations/grouping.d.ts +9 -3
  49. package/dist/foundations/index.d.ts +2 -0
  50. package/dist/foundations/index.js +5 -0
  51. package/dist/foundations/sticky.d.ts +4 -0
  52. package/dist/foundations/sticky.js +1 -0
  53. package/dist/primitives/button/Button.d.ts +12 -2
  54. package/dist/primitives/button/Button.js +12 -2
  55. package/dist/primitives/cell/CellStack.js +1 -1
  56. package/dist/primitives/choice/ComboBox.d.ts +1 -1
  57. package/dist/primitives/choice/ComboBox.js +4 -4
  58. package/dist/primitives/choice/RadioGroup.d.ts +1 -1
  59. package/dist/primitives/choice/RadioGroup.js +3 -3
  60. package/dist/primitives/choice/SearchField.js +1 -1
  61. package/dist/primitives/choice/SegmentedControl.d.ts +1 -1
  62. package/dist/primitives/choice/SegmentedControl.js +2 -2
  63. package/dist/primitives/choice/Slider.d.ts +1 -1
  64. package/dist/primitives/choice/Slider.js +2 -2
  65. package/dist/primitives/choice/Switch.d.ts +1 -1
  66. package/dist/primitives/choice/Switch.js +2 -2
  67. package/dist/primitives/date/Calendar.js +1 -1
  68. package/dist/primitives/date/DatePicker.d.ts +1 -1
  69. package/dist/primitives/date/DatePicker.js +2 -2
  70. package/dist/primitives/date/DateTime.d.ts +1 -1
  71. package/dist/primitives/date/DateTime.js +2 -2
  72. package/dist/primitives/field/Checks.d.ts +1 -1
  73. package/dist/primitives/field/Checks.js +4 -4
  74. package/dist/primitives/field/DropZone.d.ts +1 -1
  75. package/dist/primitives/field/DropZone.js +2 -2
  76. package/dist/primitives/field/Field.d.ts +10 -5
  77. package/dist/primitives/field/Field.js +10 -6
  78. package/dist/primitives/field/Suggest.d.ts +1 -1
  79. package/dist/primitives/field/Suggest.js +2 -2
  80. package/dist/primitives/field/index.d.ts +1 -3
  81. package/dist/primitives/field/index.js +4 -4
  82. package/dist/primitives/notice/Notice.js +3 -1
  83. package/dist/primitives/picker/Picker.d.ts +1 -1
  84. package/dist/primitives/picker/Picker.js +4 -4
  85. package/dist/primitives/{tag → picker}/TagsField.d.ts +2 -2
  86. package/dist/primitives/{tag → picker}/TagsField.js +5 -5
  87. package/dist/primitives/picker/index.d.ts +2 -0
  88. package/dist/primitives/picker/index.js +5 -1
  89. package/dist/primitives/table/CellCopy.js +1 -1
  90. package/dist/primitives/table/CellEditor.js +2 -4
  91. package/dist/primitives/table/DataTable.d.ts +11 -1
  92. package/dist/primitives/table/DataTable.js +111 -38
  93. package/dist/primitives/table/EditableTable.js +6 -2
  94. package/dist/primitives/table/PivotTable.js +0 -0
  95. package/dist/primitives/table/ReportTable.js +4 -3
  96. package/dist/primitives/table/SheetTable.js +8 -14
  97. package/dist/primitives/table/Table.js +2 -2
  98. package/dist/primitives/tabs/Tabs.d.ts +22 -4
  99. package/dist/primitives/tabs/Tabs.js +77 -23
  100. package/dist/primitives/tabs/index.d.ts +1 -1
  101. package/dist/primitives/tabs/index.js +1 -1
  102. package/dist/primitives/tag/Tags.js +4 -1
  103. package/dist/primitives/tag/index.d.ts +0 -2
  104. package/dist/primitives/tag/index.js +0 -3
  105. package/dist/primitives/value/Avatar.js +8 -1
  106. package/dist/primitives/value/{Fecha.d.ts → DateValue.d.ts} +5 -2
  107. package/dist/primitives/value/{Fecha.js → DateValue.js} +9 -8
  108. package/dist/primitives/value/Relation.d.ts +1 -1
  109. package/dist/primitives/value/Relation.js +2 -2
  110. package/dist/primitives/value/Value.js +4 -1
  111. package/dist/primitives/value/index.d.ts +1 -1
  112. package/dist/primitives/value/index.js +1 -1
  113. package/dist/tokens.css +42 -0
  114. package/dist/views/list/List.d.ts +22 -2
  115. package/dist/views/list/List.js +28 -6
  116. package/dist/views/list/ListView.d.ts +1 -1
  117. package/dist/views/list/ListView.js +47 -16
  118. package/dist/views/list/index.d.ts +2 -2
  119. package/dist/views/list/presentations.js +3 -1
  120. package/dist/views/list/types.d.ts +57 -1
  121. package/dist/views/list/useNarrowList.d.ts +2 -2
  122. package/dist/views/list/useNarrowList.js +5 -5
  123. package/dist/views/record/Record.js +46 -42
  124. package/package.json +1 -1
  125. package/dist/primitives/field/context.js +0 -13
  126. package/dist/primitives/field/mode.d.ts +0 -9
  127. /package/dist/primitives/field/{copy.d.ts → clipboard.d.ts} +0 -0
  128. /package/dist/primitives/field/{copy.js → clipboard.js} +0 -0
  129. /package/dist/primitives/{tag/choices.d.ts → picker/tagChoices.d.ts} +0 -0
  130. /package/dist/primitives/{tag/choices.js → picker/tagChoices.js} +0 -0
@@ -9,9 +9,9 @@ import { Listing } from "../../components/listing/Listing.js";
9
9
  import { ListingBar } from "../../components/listing/ListingBar.js";
10
10
  import { PivotExplorer } from "../../components/listing/PivotExplorer.js";
11
11
  import { RecordCard } from "../../components/listing/RecordCard.js";
12
- import { GroupHeader, Row, RowHeader } from "../../components/listing/Row.js";
12
+ import { GroupHeader, GroupMore, Row, RowHeader, } from "../../components/listing/Row.js";
13
13
  import { sortItems, SortMenu, } from "../../components/listing/SortMenu.js";
14
- import { ViewSwitcher } from "../../components/listing/ViewSwitcher.js";
14
+ import { ViewSwitcher, } from "../../components/listing/ViewSwitcher.js";
15
15
  import { Breadcrumb } from "../../components/record/Breadcrumb.js";
16
16
  import { RecordTimeline } from "../../components/timeline/RecordTimeline.js";
17
17
  import { useUiCopy } from "../../foundations/copy.js";
@@ -22,7 +22,8 @@ import { Button } from "../../primitives/button/Button.js";
22
22
  import { Empty, Failed, Skeleton } from "../../primitives/notice/Notice.js";
23
23
  import { PivotTable } from "../../primitives/table/PivotTable.js";
24
24
  import { ReportTable } from "../../primitives/table/ReportTable.js";
25
- import { groupedColumnIds } from "./columns.js";
25
+ import { ProgressBar } from "../../primitives/value/ProgressBar.js";
26
+ import { groupedColumnIds, WIDE_PRESENTS } from "./columns.js";
26
27
  import { groupLevelsOf, reportColumnsOf, reportRowsOf, sheetFromColumns, sheetRowsOf, } from "./derive.js";
27
28
  import { SheetPresentation, TablePresentation, } from "./presentations.js";
28
29
  import { useNarrowList } from "./useNarrowList.js";
@@ -66,14 +67,20 @@ const PRESENT_ICONS = [
66
67
  { id: "gantt", icon: ChartGantt },
67
68
  { id: "timeline", icon: CalendarRange },
68
69
  ];
69
- export function ListView({ items, keyOf, identity, crumbs, embedded = false, surface: requestedSurface, metrics, search, pagination, views, actions, row, rowHeader, columns, visibleColumnIds, onVisibleColumnIdsChange, card, boardStages, calendar, gantt, timeline, sheet, report, pivot, present: requested = "rows", presents, onPresentChange, sort, onSortChange, sortable, totals = false, rowActions, create, group, selection, onOpen, onPeek, rowClick = "peek", onChoose, state, filtered = false, empty, children, }) {
70
- const { frame, offered, present } = useNarrowList(presents, requested);
70
+ export function ListView({ items, keyOf, identity, crumbs, embedded = false, surface: requestedSurface, metrics, search, pagination, views, actions, row, rowHeader, columns, visibleColumnIds, onVisibleColumnIdsChange, card, boardStages, calendar, gantt, timeline, sheet, report, pivot, faces, present: requested = "rows", presents, onPresentChange, sort, onSortChange, sortable, totals = false, rowActions, create, group, selection, onOpen, onPeek, rowClick = "peek", onChoose, state, filtered = false, empty, children, }) {
71
+ const wide = useMemo(() => [
72
+ ...WIDE_PRESENTS,
73
+ ...(faces ?? []).filter((f) => f.wide === true).map((f) => f.id),
74
+ ], [faces]);
75
+ const { frame, offered, present } = useNarrowList(presents, requested, wide);
76
+ const face = faces?.find((one) => one.id === present);
71
77
  /* Embedded, the list sits on the record's paper: there is no dark band to inherit, and a
72
78
  navy switcher inside a white sheet would be a second chrome on the screen. */
73
79
  const surface = embedded ? undefined : requestedSurface;
74
80
  const density = useDensity();
75
81
  const copy = useUiCopy();
76
82
  const loading = state?.loading === true;
83
+ const refreshing = state?.refreshing === true && !loading;
77
84
  const error = state?.error ?? null;
78
85
  const columnSpecs = useMemo(() => specsFromColumns(columns ?? [], false), [columns]);
79
86
  const columnControl = useColumnControl(columnSpecs, {
@@ -99,14 +106,18 @@ export function ListView({ items, keyOf, identity, crumbs, embedded = false, sur
99
106
  ];
100
107
  const shownColumns = columns === undefined ? undefined : visibleColumns(columns, tableIds);
101
108
  const groupLevels = useMemo(() => groupLevelsOf(group, columns), [group, columns]);
109
+ /* In the order the tree wrote them — the application's own among the package's. */
110
+ const options = offered.flatMap((id) => {
111
+ const own = faces?.find((one) => one.id === id);
112
+ if (own !== undefined)
113
+ return [{ id, label: own.label, icon: own.icon }];
114
+ const builtIn = PRESENT_ICONS.find((option) => option.id === id);
115
+ return builtIn === undefined
116
+ ? []
117
+ : [{ id, label: copy.present[builtIn.id], icon: builtIn.icon }];
118
+ });
102
119
  const switcher = views ??
103
- (offered.length > 1 ? (_jsx(ViewSwitcher, { options: PRESENT_ICONS.filter((option) => offered.includes(option.id)).map((option) => ({
104
- id: option.id,
105
- label: copy.present[option.id],
106
- icon: option.icon,
107
- })), current: present, ...(onPresentChange === undefined
108
- ? {}
109
- : { onSelect: (id) => onPresentChange(id) }), ...(surface === undefined ? {} : { surface }) })) : undefined);
120
+ (options.length > 1 ? (_jsx(ViewSwitcher, { options: options, current: present, ...(onPresentChange === undefined ? {} : { onSelect: onPresentChange }), ...(surface === undefined ? {} : { surface }) })) : undefined);
110
121
  const sheetFromDeclared = useMemo(() => sheetFromColumns(shownColumns), [shownColumns]);
111
122
  const sheetSpec = sheet ?? sheetFromDeclared;
112
123
  /* El menú de columnas vale en `table` y en la planilla DERIVADA —las dos
@@ -244,7 +255,10 @@ export function ListView({ items, keyOf, identity, crumbs, embedded = false, sur
244
255
  ? null
245
256
  : pile.piles.length > 0
246
257
  ? renderPiles(pile.piles, nodes)
247
- : nodes(pile.items)] }, key));
258
+ : nodes(pile.items), open &&
259
+ pile.piles.length === 0 &&
260
+ pile.items.length < pile.count &&
261
+ group?.onMore !== undefined ? (_jsx(GroupMore, { left: pile.count - pile.items.length, depth: pile.depth, onMore: () => group.onMore?.(pile) })) : null] }, key));
248
262
  });
249
263
  }
250
264
  const reportRows = useMemo(() => reportRowsOf(piles, columns, row), [piles, columns, row]);
@@ -260,7 +274,22 @@ export function ListView({ items, keyOf, identity, crumbs, embedded = false, sur
260
274
  const d = row(item);
261
275
  return (_jsx(RecordCard, { title: d.name, ...(d.tags === undefined ? {} : { tags: d.tags }), ...(d.meta === undefined ? {} : { meta: d.meta }), ...(press === undefined ? {} : { onOpen: () => press(item) }) }, keyOf(item)));
262
276
  };
263
- const body = present === "table" && shownColumns !== undefined ? (
277
+ const body = face !== undefined ? (face.render({
278
+ items: ordered,
279
+ keyOf,
280
+ piles,
281
+ isOpen,
282
+ ...(group?.onToggle === undefined
283
+ ? {}
284
+ : { toggle: (pile) => group.onToggle?.(pathKey(pile.path)) }),
285
+ ...(group?.onMore === undefined ? {} : { more: group.onMore }),
286
+ ...(selection === undefined
287
+ ? {}
288
+ : { selection: { has: selection.has, toggle: selection.toggle } }),
289
+ ...(press === undefined ? {} : { press }),
290
+ ...(onPeek === undefined || choosing ? {} : { peek: onPeek }),
291
+ ...(onOpen === undefined || choosing ? {} : { open: onOpen }),
292
+ })) : present === "table" && shownColumns !== undefined ? (
264
293
  /* The table receives EVERYTHING that belongs to the set: see `TablePresentation`. */
265
294
  _jsx(TablePresentation, { items: items, columns: shownColumns, keyOf: keyOf, levels: groupLevels, ...(sort === undefined ? {} : { sort }), ...(onSortChange === undefined ? {} : { onSortChange }), ...(group === undefined ? {} : { group }), ...(selection === undefined ? {} : { selection }), totals: totals, ...(rowActions === undefined ? {} : { rowActions }), ...(create === undefined ? {} : { create }), ...(onPeek === undefined || choosing ? {} : { onPeek }), ...(onOpen === undefined || choosing ? {} : { onOpen }), ...(choosing ? { onChoose } : {}), rowClick: rowClick })) : present === "sheet" && sheetSpec !== undefined ? (
266
295
  /* La planilla recibe el conjunto por ÍNDICE y no una matriz ya armada: ver `SheetPresentation`. */
@@ -304,7 +333,7 @@ export function ListView({ items, keyOf, identity, crumbs, embedded = false, sur
304
333
  /* The list is its own container (`@container`: the presentations adapt to the width
305
334
  the LIST has, not the window) and fills the space it is given. Before, every page
306
335
  wrapped it in a `div` to get both. */
307
- _jsxs("div", { ref: frame, className: "@container flex min-w-0 flex-1 flex-col bg-surface", children: [_jsxs(Listing, { bar: bar,
336
+ _jsxs("div", { ref: frame, "aria-busy": refreshing || loading, className: "@container flex min-w-0 flex-1 flex-col bg-surface", children: [_jsxs(Listing, { bar: bar,
308
337
  /* La barra se queda arriba: con 187 filas, scrollear para ver la última
309
338
  dejaba sin buscador, sin paginador y sin el conmutador.
310
339
  Embedded it does NOT: the record has its own sticky parts (its rail, its tabs),
@@ -317,7 +346,9 @@ export function ListView({ items, keyOf, identity, crumbs, embedded = false, sur
317
346
  metrics: _jsx("div", { className: "px-3.5", children: metrics }),
318
347
  }), children: [loading ? _jsx(Skeleton, { label: copy.failed.loading }) : null, isEmpty ? (filtered ? (_jsx(Empty, { sentence: empty?.whenFiltered?.sentence ?? copy.list.emptyFiltered, ...(empty?.whenFiltered?.action === undefined
319
348
  ? {}
320
- : { action: empty.whenFiltered.action }) })) : (_jsx(Empty, { sentence: empty?.sentence ?? copy.list.empty, ...(empty?.action === undefined ? {} : { action: empty.action }) }))) : null, loading || isEmpty ? null : body] }), error === null ? null : (_jsx("div", { className: "px-3 pt-(--spacing-block)", children: _jsx(Failed, { heading: copy.failed.list, sentence: error }) })), children] }));
349
+ : { action: empty.whenFiltered.action }) })) : (_jsx(Empty, { sentence: empty?.sentence ?? copy.list.empty, ...(empty?.action === undefined ? {} : { action: empty.action }) }))) : null, _jsxs("div", { className: "relative min-w-0", children: [refreshing ? (
350
+ /* Over the top edge of the rows, not above them: the rows do not move. */
351
+ _jsx("div", { className: "pointer-events-none absolute inset-x-0 top-0 z-20", children: _jsx(ProgressBar, { label: copy.list.refreshing, width: "w-full" }) })) : null, loading || isEmpty ? null : body] })] }), error === null ? null : (_jsx("div", { className: "px-3 pt-(--spacing-block)", children: _jsx(Failed, { heading: copy.failed.list, sentence: error }) })), children] }));
321
352
  }
322
353
  /** What a list offers when it fails and can be retried. */
323
354
  export function RetryAction({ onRetry }) {
@@ -1,2 +1,2 @@
1
- export { List, type ListProps } from "./List.js";
2
- export { type ListCrumb, type ListEmpty, type ListGroup, type ListPivot, type ListPresent, type ListReport, type ListSheet, type RowOf, } from "./types.js";
1
+ export { List, type ListPresentProps, type ListProps } from "./List.js";
2
+ export { type ListCrumb, type ListEmpty, type ListGroup, type ListPivot, type ListPresent, type ListReport, type ListSet, type ListSheet, type RowOf, } from "./types.js";
@@ -37,7 +37,9 @@ export function TablePresentation({ items, columns, keyOf, levels, group, totals
37
37
  ]),
38
38
  ...(rowActions?.(item) ?? []),
39
39
  ];
40
- return (_jsx(DataTable, { data: items, columns: columns, getRowId: keyOf, ...(sort === undefined ? {} : { sort }), ...(onSortChange === undefined ? {} : { onSortChange }), ...(levels.length === 0 ? {} : { levels: levels }), ...(group?.collapsed === undefined ? {} : { collapsed: group.collapsed }), ...(group?.onToggle === undefined ? {} : { onGroupToggle: group.onToggle }), ...(selection === undefined
40
+ /* Piles counted by whoever keeps the data go through as they are: the table draws those,
41
+ not the ones it would build from the page. */
42
+ return (_jsx(DataTable, { data: items, columns: columns, getRowId: keyOf, ...(sort === undefined ? {} : { sort }), ...(onSortChange === undefined ? {} : { onSortChange }), ...(levels.length === 0 ? {} : { levels: levels }), ...(group?.collapsed === undefined ? {} : { collapsed: group.collapsed }), ...(group?.onToggle === undefined ? {} : { onGroupToggle: group.onToggle }), ...(group?.piles === undefined || levels.length === 0 ? {} : { piles: group.piles }), ...(group?.onMore === undefined ? {} : { onMore: group.onMore }), ...(selection === undefined
41
43
  ? {}
42
44
  : {
43
45
  selected: new Set(items.filter((i) => selection.has(keyOf(i))).map(keyOf)),
@@ -4,9 +4,10 @@ import type { BoardStages } from "../../components/listing/BoardColumns.js";
4
4
  import type { ListIdentity } from "../../components/listing/ListingBar.js";
5
5
  import type { ListPresent } from "../../components/listing/set.js";
6
6
  import type { SortField } from "../../components/listing/SortMenu.js";
7
+ import type { ViewIcon } from "../../components/listing/ViewSwitcher.js";
7
8
  import type { RecordTimelineProps } from "../../components/timeline/RecordTimeline.js";
8
9
  import type { GroupableField } from "../../foundations/grouping.js";
9
- import type { Grouping, GroupLevel } from "../../foundations/grouping.js";
10
+ import type { Grouping, GroupLevel, Pile } from "../../foundations/grouping.js";
10
11
  import type { Pivot, PivotKey, PivotSpec } from "../../foundations/pivot.js";
11
12
  import type { Surface, Tone } from "../../foundations/types.js";
12
13
  import type { DataColumn, DataTableCreate } from "../../primitives/table/DataTable.js";
@@ -40,6 +41,51 @@ export type RowOf = {
40
41
  children?: ReactNode;
41
42
  actions?: ReactNode;
42
43
  };
44
+ /**
45
+ * The set as a presentation receives it — what a `List.Present` draws from.
46
+ *
47
+ * Context: the built-in presentations do not each sort, group and wire selection on their
48
+ * own; the list does it once and hands them the result. A presentation the application
49
+ * writes gets the same hand-off, so a map or a gallery is one more way of looking at the
50
+ * SAME set: the bar's sort, the search's grouping, the selection and the rule of what a
51
+ * press on a record does all hold, and switching to it and back loses none of them.
52
+ */
53
+ export type ListSet<T> = {
54
+ /** In the order the bar asks for. The page, when the list pages. */
55
+ items: readonly T[];
56
+ keyOf: (item: T) => string;
57
+ /** Grouped: the piles, outermost level first, each with its sub-piles or its items.
58
+ * `null` when the list is not grouped — draw `items`. */
59
+ piles: readonly Pile<T>[] | null;
60
+ /** Whether a pile is open, with the same keys every presentation folds by. */
61
+ isOpen: (pile: Pile<T>) => boolean;
62
+ /** Folds or unfolds a pile — present when the list lets it. */
63
+ toggle?: (pile: Pile<T>) => void;
64
+ /** Asks for the next page of a pile that holds fewer records than it counts
65
+ * (`pile.items.length < pile.count`) — present when whoever keeps the data can. */
66
+ more?: (pile: Pile<T>) => void;
67
+ selection?: {
68
+ has: (id: string) => boolean;
69
+ toggle: (id: string) => void;
70
+ };
71
+ /** What a press on a record does in THIS list: choose it, look at it or open it, as the
72
+ * list decided (`onChoose`, `rowClick`). Call this from the body of a card or a marker. */
73
+ press?: (item: T) => void;
74
+ /** The two ways in, for a presentation that offers both buttons (👁 and ↗). Absent while
75
+ * the list is choosing. */
76
+ peek?: (item: T) => void;
77
+ open?: (item: T) => void;
78
+ };
79
+ /** A presentation the application writes, as the list receives it from `List.Present`. */
80
+ export type ListFace<T> = {
81
+ id: string;
82
+ /** Its name in the switcher — the `title` and the accessible name of its button. */
83
+ label: string;
84
+ icon: ViewIcon;
85
+ /** It needs width (a grid of columns): a narrow list does not offer it. */
86
+ wide?: boolean;
87
+ render: (set: ListSet<T>) => ReactNode;
88
+ };
43
89
  /** A crumb of the list: where I came from. */
44
90
  export type ListCrumb = {
45
91
  label: string;
@@ -279,6 +325,8 @@ export type ListViewProps<T extends object> = {
279
325
  * elegirlo acá sería adivinar la pregunta.
280
326
  */
281
327
  pivot?: ListPivot<T>;
328
+ /** The presentations the application wrote (`List.Present`), by id. */
329
+ faces?: readonly ListFace<T>[];
282
330
  present?: ListPresent;
283
331
  /** Which presentations this list offers. Two or more draw the switcher. */
284
332
  presents?: readonly ListPresent[];
@@ -311,8 +359,16 @@ export type ListViewProps<T extends object> = {
311
359
  /** The three states, in a single place. Every view used to re-assemble them
312
360
  * with three nested ternaries, and that is why «empty because of a filter»
313
361
  * and «really empty» got mixed up in half the screens. */
362
+ /** Los estados del conjunto, en un solo lugar.
363
+ *
364
+ * `loading` es la PRIMERA lectura: no hay filas todavía, y se dibuja el esqueleto.
365
+ * `refreshing` es volver a preguntar con filas en la mano —se tipeó en la búsqueda, se
366
+ * cambió de página—: las filas se quedan y una barra discreta dice que se está
367
+ * preguntando. Cambiarlas por un esqueleto en cada pregunta hacía parpadear la tabla, y un
368
+ * clic podía caer sobre una fila que ya no estaba. */
314
369
  state?: {
315
370
  loading?: boolean;
371
+ refreshing?: boolean;
316
372
  error?: string | null;
317
373
  };
318
374
  filtered?: boolean;
@@ -13,7 +13,7 @@ import { type RefObject } from "react";
13
13
  * Measured BEFORE paint: waiting for the observer's first report drew the table and swapped it
14
14
  * for rows a frame later, so a click on a row of a list in a panel landed on a detached `<tr>`.
15
15
  */
16
- export declare function useNarrowList(presents: readonly ListPresent[] | undefined, requested: ListPresent): {
16
+ export declare function useNarrowList(presents: readonly ListPresent[] | undefined, requested: ListPresent, wide?: readonly ListPresent[]): {
17
17
  frame: RefObject<HTMLDivElement | null>;
18
18
  offered: readonly ListPresent[];
19
19
  present: ListPresent;
@@ -21,7 +21,7 @@ export declare function useNarrowList(presents: readonly ListPresent[] | undefin
21
21
  /** What a list offers and draws at its width. Only a list that HAS rows folds into them: one
22
22
  * that declares just a table fell back to rows it had no `row` for, and drew empty lines — a
23
23
  * table that scrolls sideways says more. */
24
- export declare function narrowed(presents: readonly ListPresent[] | undefined, requested: ListPresent, narrow: boolean): {
24
+ export declare function narrowed(presents: readonly ListPresent[] | undefined, requested: ListPresent, narrow: boolean, wide?: readonly ListPresent[]): {
25
25
  offered: readonly ListPresent[];
26
26
  present: ListPresent;
27
27
  };
@@ -15,7 +15,7 @@ import { useLayoutEffect, useRef, useState } from "react";
15
15
  * Measured BEFORE paint: waiting for the observer's first report drew the table and swapped it
16
16
  * for rows a frame later, so a click on a row of a list in a panel landed on a detached `<tr>`.
17
17
  */
18
- export function useNarrowList(presents, requested) {
18
+ export function useNarrowList(presents, requested, wide = WIDE_PRESENTS) {
19
19
  const frame = useRef(null);
20
20
  const [narrow, setNarrow] = useState(false);
21
21
  useLayoutEffect(() => {
@@ -32,15 +32,15 @@ export function useNarrowList(presents, requested) {
32
32
  watch.observe(el);
33
33
  return () => watch.disconnect();
34
34
  }, []);
35
- return { frame, ...narrowed(presents, requested, narrow) };
35
+ return { frame, ...narrowed(presents, requested, narrow, wide) };
36
36
  }
37
37
  /** What a list offers and draws at its width. Only a list that HAS rows folds into them: one
38
38
  * that declares just a table fell back to rows it had no `row` for, and drew empty lines — a
39
39
  * table that scrolls sideways says more. */
40
- export function narrowed(presents, requested, narrow) {
40
+ export function narrowed(presents, requested, narrow, wide = WIDE_PRESENTS) {
41
41
  const folds = narrow && (presents === undefined || presents.includes("rows"));
42
42
  return {
43
- offered: (presents ?? []).filter((p) => !folds || !WIDE_PRESENTS.includes(p)),
44
- present: folds && WIDE_PRESENTS.includes(requested) ? "rows" : requested,
43
+ offered: (presents ?? []).filter((p) => !folds || !wide.includes(p)),
44
+ present: folds && wide.includes(requested) ? "rows" : requested,
45
45
  };
46
46
  }
@@ -14,7 +14,7 @@ import { useUiCopy } from "../../foundations/copy.js";
14
14
  import { byDensity, useDensity } from "../../foundations/density.js";
15
15
  import { flatten, pick as pickSlot, pickAll, propsOf, slot, slotOf, } from "../../foundations/slots.js";
16
16
  import { createNoteRegistry, NoteRegistryContext, } from "../../primitives/field/note.js";
17
- import { Tabs } from "../../primitives/tabs/Tabs.js";
17
+ import { Tabs, TabsPanel, TabsScope, } from "../../primitives/tabs/Tabs.js";
18
18
  import { useEffect, useId, useState, useSyncExternalStore } from "react";
19
19
  const pick = (nodes, name) => pickSlot(nodes, name);
20
20
  /** El riel: mi lugar delante del registro. Sin él, no hay banda — y eso
@@ -68,6 +68,10 @@ const Facts = slot("facts", (_props) => null);
68
68
  * que no está escrita no tiene solapa, y ésa es toda la declaración. */
69
69
  const Pages = slot("pages", (_props) => null);
70
70
  const Page = slot("page", (_props) => null);
71
+ /** The notebook's tabs and its page share an id — only when there are tabs. */
72
+ function NotebookScope({ tabs, children }) {
73
+ return tabs ? _jsx(TabsScope, { children: children }) : children;
74
+ }
71
75
  export function Record({ identity, error = null, dirty, children }) {
72
76
  const d = useDensity();
73
77
  const copy = useUiCopy();
@@ -166,48 +170,48 @@ export function Record({ identity, error = null, dirty, children }) {
166
170
  shorter than the screen. The extra height has no paint of its own: on a desk
167
171
  nothing changes. */
168
172
  /* The record knows the notes of its fields and rows: their cards register here. */
169
- _jsx(NoteRegistryContext, { value: registry, children: _jsx("article", { className: "flex min-w-0 flex-1 flex-col", children: _jsx(RecordHeader, { railSurface: surface, ...(ribbon === undefined
170
- ? {}
171
- : {
172
- ribbon: (_jsx(RecordRibbon, { label: ribbon.label, ...(ribbon.tone === undefined ? {} : { tone: ribbon.tone }) })),
173
- }), ...(paper?.width === undefined ? {} : { width: paper.width }), ...(dirty === undefined ? {} : { dirty }), ...(crumbs === undefined ? {} : { crumbs }), ...(pager === undefined
174
- ? {}
175
- : {
176
- pager: (_jsx(PagerControl, { index: pager.index, total: pager.total, surface: surface, ...(pager.onPrev === undefined ? {} : { onPrev: pager.onPrev }), ...(pager.onNext === undefined ? {} : { onNext: pager.onNext }) })),
177
- }), ...(actions === undefined ? {} : { actions }), ...(statusBlock === undefined ? {} : { status: statusBlock }), ...(identity.icon === undefined ? {} : { icon: identity.icon }), title: identity.title, ...(identity.subtitle === undefined ? {} : { subtitle: identity.subtitle }), ...(identity.figure === undefined ? {} : { figure: identity.figure }), ...(identity.tag === undefined ? {} : { tag: identity.tag }), ...(smart === undefined || smart.length === 0
178
- ? {}
179
- : {
180
- smart: (_jsx(SmartButtons, { children: smart.map((s) => (_jsx(SmartButton, { icon: s.icon, ...(s.count === undefined ? {} : { count: s.count }), label: s.label, layout: onTheRail && d === "comfortable" ? "inline" : "stacked", ...(s.onClick === undefined ? {} : { onClick: s.onClick }) }, s.label))) })),
181
- }), ...(aspectsNode === undefined || globalSheet ? {} : { aspects: aspectsNode }), ...(activitySlot === undefined
182
- ? {}
183
- : {
184
- activity: (_jsx(Activity, { ...activitySlot, alerts: rest, alertsId: alertsId, open: activityOpen, onToggle: () => setActivityOpen(!activityOpen) })),
185
- activityLayout: activitySlot.layout ?? "auto",
186
- }), ...(blocking.length === 0 && error === null
187
- ? {}
188
- : {
189
- blocking: (_jsx(RecordBlocking, { notices: blocking, error: error, errorHeading: copy.failed.operation })),
190
- }), ...(rest.length === 0 && (activitySlot === undefined || notes.length === 0)
191
- ? {}
192
- : {
193
- indicator: (_jsx(RecordNoticeIndicator, { notices: rest, anchored: notes.map((entry) => ({ tone: entry.note.tone ?? "info" })), ...(activitySlot === undefined
194
- ? {}
195
- : {
196
- /* With a panel, the notices live there: the indicator goes to them
197
- instead of floating a second copy in a popover. */
198
- reveal: {
199
- target: alertsId,
200
- onReveal: () => {
201
- setActivityOpen(true);
202
- setReveal((n) => n + 1);
173
+ _jsx(NoteRegistryContext, { value: registry, children: _jsx("article", { className: "flex min-w-0 flex-1 flex-col", children: _jsx(NotebookScope, { tabs: tabs.length > 0, children: _jsx(RecordHeader, { railSurface: surface, ...(ribbon === undefined
174
+ ? {}
175
+ : {
176
+ ribbon: (_jsx(RecordRibbon, { label: ribbon.label, ...(ribbon.tone === undefined ? {} : { tone: ribbon.tone }) })),
177
+ }), ...(paper?.width === undefined ? {} : { width: paper.width }), ...(dirty === undefined ? {} : { dirty }), ...(crumbs === undefined ? {} : { crumbs }), ...(pager === undefined
178
+ ? {}
179
+ : {
180
+ pager: (_jsx(PagerControl, { index: pager.index, total: pager.total, surface: surface, ...(pager.onPrev === undefined ? {} : { onPrev: pager.onPrev }), ...(pager.onNext === undefined ? {} : { onNext: pager.onNext }) })),
181
+ }), ...(actions === undefined ? {} : { actions }), ...(statusBlock === undefined ? {} : { status: statusBlock }), ...(identity.icon === undefined ? {} : { icon: identity.icon }), title: identity.title, ...(identity.subtitle === undefined ? {} : { subtitle: identity.subtitle }), ...(identity.figure === undefined ? {} : { figure: identity.figure }), ...(identity.tag === undefined ? {} : { tag: identity.tag }), ...(smart === undefined || smart.length === 0
182
+ ? {}
183
+ : {
184
+ smart: (_jsx(SmartButtons, { children: smart.map((s) => (_jsx(SmartButton, { icon: s.icon, ...(s.count === undefined ? {} : { count: s.count }), label: s.label, layout: onTheRail && d === "comfortable" ? "inline" : "stacked", ...(s.onClick === undefined ? {} : { onClick: s.onClick }) }, s.label))) })),
185
+ }), ...(aspectsNode === undefined || globalSheet ? {} : { aspects: aspectsNode }), ...(activitySlot === undefined
186
+ ? {}
187
+ : {
188
+ activity: (_jsx(Activity, { ...activitySlot, alerts: rest, alertsId: alertsId, open: activityOpen, onToggle: () => setActivityOpen(!activityOpen) })),
189
+ activityLayout: activitySlot.layout ?? "auto",
190
+ }), ...(blocking.length === 0 && error === null
191
+ ? {}
192
+ : {
193
+ blocking: (_jsx(RecordBlocking, { notices: blocking, error: error, errorHeading: copy.failed.operation })),
194
+ }), ...(rest.length === 0 && (activitySlot === undefined || notes.length === 0)
195
+ ? {}
196
+ : {
197
+ indicator: (_jsx(RecordNoticeIndicator, { notices: rest, anchored: notes.map((entry) => ({ tone: entry.note.tone ?? "info" })), ...(activitySlot === undefined
198
+ ? {}
199
+ : {
200
+ /* With a panel, the notices live there: the indicator goes to them
201
+ instead of floating a second copy in a popover. */
202
+ reveal: {
203
+ target: alertsId,
204
+ onReveal: () => {
205
+ setActivityOpen(true);
206
+ setReveal((n) => n + 1);
207
+ },
203
208
  },
204
- },
205
- }) })),
206
- }), children: _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: `flex min-w-0 flex-col ${byDensity(d, "gap-(--spacing-block) px-4 pt-(--spacing-block) pb-4", "gap-2.5 pt-2.5 pb-2")}`, children: _jsxs(Sheet, { layout: paper?.layout ?? "stacked", columns: paper?.columns ?? 2, ...(hasFacts ? { fields } : {}), ...(above.length > 0 || (globalSheet && aspectsNode !== undefined)
207
- ? {
208
- head: (_jsxs(_Fragment, { children: [above, globalSheet && aspectsNode !== undefined ? (_jsx("div", { className: `border-b border-line-strong ${byDensity(d, "-mx-4 px-4", "")}`, children: aspectsNode })) : null] })),
209
- }
210
- : {}), children: [pageBody, below] }) }), audit === undefined ? null : _jsx(RecordAudit, { ...audit })] }) }) }) }));
209
+ }) })),
210
+ }), children: _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: `flex min-w-0 flex-col ${byDensity(d, "gap-(--spacing-block) px-4 pt-(--spacing-block) pb-4", "gap-2.5 pt-2.5 pb-2")}`, children: _jsxs(Sheet, { layout: paper?.layout ?? "stacked", columns: paper?.columns ?? 2, ...(hasFacts ? { fields } : {}), ...(above.length > 0 || (globalSheet && aspectsNode !== undefined)
211
+ ? {
212
+ head: (_jsxs(_Fragment, { children: [above, globalSheet && aspectsNode !== undefined ? (_jsx("div", { className: `border-b border-line-strong ${byDensity(d, "-mx-4 px-4", "")}`, children: aspectsNode })) : null] })),
213
+ }
214
+ : {}), children: [tabs.length === 0 ? (pageBody) : (_jsx(TabsPanel, { id: current, className: `flex min-w-0 flex-col ${byDensity(d, "gap-(--spacing-block)", "gap-2.5")}`, children: pageBody })), below] }) }), audit === undefined ? null : _jsx(RecordAudit, { ...audit })] }) }) }) }) }));
211
215
  }
212
216
  Record.Skeleton = RecordSkeleton;
213
217
  Record.Rail = Rail;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sincpro/web-ui",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "An enterprise web design system for business software — ERPs, back offices, management tools — in React and Tailwind: tokens, primitives, components and the two views every management screen is made of.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,13 +0,0 @@
1
- "use client";
2
- import { createContext, useContext } from "react";
3
- export const FieldIdsContext = createContext(null);
4
- /** The ids of the enclosing `Field`, resolved against the control's own props. */
5
- export function useFieldIds(own) {
6
- const field = useContext(FieldIdsContext);
7
- return {
8
- id: own.id ?? field?.id,
9
- labelledBy: field?.labelId,
10
- describedBy: own.describedBy ?? field?.describedBy,
11
- invalid: own.invalid ?? field?.invalid ?? false,
12
- };
13
- }
@@ -1,9 +0,0 @@
1
- import { type ReactNode } from "react";
2
- export type FormMode = "edit" | "read";
3
- export declare function FormModeProvider({ value, children, }: {
4
- value: FormMode;
5
- children: ReactNode;
6
- }): import("react").JSX.Element;
7
- /** The mode of this field. The prop beats the context — a computed field stays
8
- * in read mode inside a form that is being edited. */
9
- export declare function useFormMode(explicit?: FormMode): FormMode;
File without changes