@sincpro/web-ui 0.2.0 → 1.1.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 (132) hide show
  1. package/README.md +3 -3
  2. package/dist/components/activity/ActivityBadge.d.ts +16 -0
  3. package/dist/components/activity/ActivityBadge.js +29 -0
  4. package/dist/components/activity/index.d.ts +1 -0
  5. package/dist/components/activity/index.js +1 -0
  6. package/dist/components/calendar/RecordCalendar.d.ts +48 -0
  7. package/dist/components/calendar/RecordCalendar.js +101 -0
  8. package/dist/components/calendar/index.d.ts +1 -0
  9. package/dist/components/calendar/index.js +1 -0
  10. package/dist/components/form/Sheet.js +5 -1
  11. package/dist/components/gantt/RecordGantt.d.ts +44 -0
  12. package/dist/components/gantt/RecordGantt.js +110 -0
  13. package/dist/components/gantt/index.d.ts +1 -0
  14. package/dist/components/gantt/index.js +1 -0
  15. package/dist/components/index.d.ts +3 -0
  16. package/dist/components/index.js +3 -0
  17. package/dist/components/lines/LineItems.d.ts +5 -2
  18. package/dist/components/lines/LineItems.js +3 -3
  19. package/dist/components/listing/BoardColumns.d.ts +39 -0
  20. package/dist/components/listing/BoardColumns.js +138 -0
  21. package/dist/components/listing/ListingBar.d.ts +5 -0
  22. package/dist/components/listing/ListingBar.js +5 -2
  23. package/dist/components/listing/PivotExplorer.d.ts +34 -0
  24. package/dist/components/listing/PivotExplorer.js +86 -0
  25. package/dist/components/listing/RecordCard.d.ts +6 -8
  26. package/dist/components/listing/RecordCard.js +13 -7
  27. package/dist/components/listing/Row.d.ts +21 -2
  28. package/dist/components/listing/Row.js +29 -13
  29. package/dist/components/listing/index.d.ts +2 -0
  30. package/dist/components/listing/index.js +2 -0
  31. package/dist/components/listing/set.d.ts +1 -1
  32. package/dist/components/navigation/index.d.ts +0 -2
  33. package/dist/components/navigation/index.js +2 -3
  34. package/dist/components/record/RecordAudit.d.ts +17 -0
  35. package/dist/components/record/RecordAudit.js +27 -0
  36. package/dist/components/record/RecordHeader.d.ts +7 -1
  37. package/dist/components/record/RecordHeader.js +30 -18
  38. package/dist/components/record/RecordRibbon.d.ts +13 -0
  39. package/dist/components/record/RecordRibbon.js +22 -0
  40. package/dist/components/record/index.d.ts +2 -0
  41. package/dist/components/record/index.js +2 -0
  42. package/dist/components/search/CriteriaChips.d.ts +6 -1
  43. package/dist/components/search/CriteriaChips.js +71 -20
  44. package/dist/components/search/SearchBar.d.ts +15 -1
  45. package/dist/components/search/SearchBar.js +34 -16
  46. package/dist/components/search/query.d.ts +14 -11
  47. package/dist/components/search/query.js +36 -3
  48. package/dist/components/timeline/RecordTimeline.d.ts +60 -0
  49. package/dist/components/timeline/RecordTimeline.js +214 -0
  50. package/dist/components/timeline/TimelineToolbar.d.ts +30 -0
  51. package/dist/components/timeline/TimelineToolbar.js +31 -0
  52. package/dist/components/timeline/index.d.ts +3 -0
  53. package/dist/components/timeline/index.js +3 -0
  54. package/dist/components/timeline/time.d.ts +43 -0
  55. package/dist/components/timeline/time.js +135 -0
  56. package/dist/foundations/copy.d.ts +110 -14
  57. package/dist/foundations/copy.es.js +85 -9
  58. package/dist/foundations/copy.js +85 -9
  59. package/dist/foundations/grouping.d.ts +3 -0
  60. package/dist/foundations/index.d.ts +1 -1
  61. package/dist/foundations/match.d.ts +23 -0
  62. package/dist/foundations/match.js +63 -0
  63. package/dist/foundations/slots.d.ts +1 -1
  64. package/dist/foundations/slots.js +8 -3
  65. package/dist/foundations/types.d.ts +7 -0
  66. package/dist/foundations/types.js +0 -36
  67. package/dist/primitives/block/index.d.ts +1 -1
  68. package/dist/primitives/block/index.js +1 -1
  69. package/dist/primitives/cell/CellStack.d.ts +7 -1
  70. package/dist/primitives/cell/CellStack.js +5 -2
  71. package/dist/primitives/choice/ComboBox.js +8 -4
  72. package/dist/primitives/choice/KanbanState.d.ts +14 -0
  73. package/dist/primitives/choice/KanbanState.js +37 -0
  74. package/dist/primitives/choice/Priority.d.ts +20 -0
  75. package/dist/primitives/choice/Priority.js +41 -0
  76. package/dist/primitives/choice/Slider.d.ts +6 -1
  77. package/dist/primitives/choice/Slider.js +2 -2
  78. package/dist/primitives/choice/index.d.ts +2 -0
  79. package/dist/primitives/choice/index.js +2 -0
  80. package/dist/primitives/dialog/Dialog.d.ts +9 -3
  81. package/dist/primitives/dialog/Dialog.js +41 -5
  82. package/dist/primitives/dialog/index.d.ts +0 -1
  83. package/dist/primitives/dialog/index.js +0 -2
  84. package/dist/primitives/notice/index.d.ts +1 -1
  85. package/dist/primitives/notice/index.js +1 -1
  86. package/dist/primitives/overlay/Preview.js +13 -4
  87. package/dist/primitives/picker/MatchedText.d.ts +10 -0
  88. package/dist/primitives/picker/MatchedText.js +12 -0
  89. package/dist/primitives/picker/Picker.d.ts +42 -7
  90. package/dist/primitives/picker/Picker.js +107 -29
  91. package/dist/primitives/table/EditableTable.d.ts +6 -1
  92. package/dist/primitives/table/EditableTable.js +70 -9
  93. package/dist/primitives/table/ReportTable.js +2 -1
  94. package/dist/primitives/table/RowActions.js +7 -2
  95. package/dist/primitives/table/Table.d.ts +5 -0
  96. package/dist/primitives/table/Table.js +17 -8
  97. package/dist/primitives/tag/Tags.d.ts +2 -0
  98. package/dist/primitives/tag/Tags.js +2 -1
  99. package/dist/primitives/tag/TagsField.d.ts +17 -0
  100. package/dist/primitives/tag/TagsField.js +46 -11
  101. package/dist/primitives/tag/choices.d.ts +6 -1
  102. package/dist/primitives/tag/choices.js +6 -2
  103. package/dist/primitives/value/Avatar.d.ts +16 -0
  104. package/dist/primitives/value/Avatar.js +91 -0
  105. package/dist/primitives/value/Person.d.ts +6 -51
  106. package/dist/primitives/value/Person.js +9 -148
  107. package/dist/primitives/value/Relation.d.ts +21 -9
  108. package/dist/primitives/value/Relation.js +35 -24
  109. package/dist/primitives/value/contact.d.ts +30 -0
  110. package/dist/primitives/value/contact.js +51 -0
  111. package/dist/primitives/value/index.d.ts +1 -1
  112. package/dist/primitives/value/index.js +1 -1
  113. package/dist/tokens.css +136 -1
  114. package/dist/views/list/List.d.ts +25 -2
  115. package/dist/views/list/List.js +48 -8
  116. package/dist/views/list/ListView.d.ts +1 -1
  117. package/dist/views/list/ListView.js +40 -9
  118. package/dist/views/list/columns.js +8 -1
  119. package/dist/views/list/presentations.d.ts +5 -2
  120. package/dist/views/list/presentations.js +32 -2
  121. package/dist/views/list/types.d.ts +44 -0
  122. package/dist/views/list/useNarrowList.d.ts +10 -0
  123. package/dist/views/list/useNarrowList.js +20 -6
  124. package/dist/views/record/Record.d.ts +14 -0
  125. package/dist/views/record/Record.js +22 -7
  126. package/package.json +11 -4
  127. package/dist/components/navigation/CommandPalette.d.ts +0 -40
  128. package/dist/components/navigation/CommandPalette.js +0 -114
  129. package/dist/components/navigation/commands.d.ts +0 -30
  130. package/dist/components/navigation/commands.js +0 -30
  131. package/dist/primitives/dialog/Drill.d.ts +0 -90
  132. package/dist/primitives/dialog/Drill.js +0 -172
@@ -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
+ }
@@ -83,6 +83,11 @@ export declare function ListingBar({ actions, title, description, counters, sele
83
83
  count: number;
84
84
  onClear?: () => void;
85
85
  actions?: ReactNode;
86
+ /** More match than are chosen: offer to choose them all. */
87
+ matching?: {
88
+ total: number;
89
+ onSelect: () => void;
90
+ };
86
91
  };
87
92
  search?: ReactNode;
88
93
  pagination?: ReactNode;
@@ -88,7 +88,10 @@ export function ListingBar({ actions, title, description, counters, selection, s
88
88
  Views used to sit next to the pagination. On a narrow window the search
89
89
  field and «1–80 / 211» painted over each other; columns and the switcher
90
90
  were the extra weight on a line that only needed how MANY. */
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?.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 : (
92
95
  /* Selecting replaces the search box: what gets selected is over what is
93
96
  ALREADY filtered, so searching there would change the set underneath the
94
97
  selection. */
@@ -98,7 +101,7 @@ export function ListingBar({ actions, title, description, counters, selection, s
98
101
  (count === undefined ? null : (_jsx("span", { role: "status", className: "shrink-0 text-meta text-muted tabular-nums", children: count === 1
99
102
  ? copy.list.countOne
100
103
  : fillCopy(copy.list.count, { count: count.toLocaleString(copy.locale) }) })));
101
- 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: [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 }))] }) }));
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 }))] }) }));
102
105
  }
103
106
  /* El campo de búsqueda va en el CENTRO de la línea, y centrado contra la
104
107
  línea: pide `--container-search` y los dos lados se reparten lo que sobra
@@ -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;
@@ -0,0 +1,86 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { grainName } from "../../foundations/calendar.js";
4
+ import { fillCopy, useUiCopy } from "../../foundations/copy.js";
5
+ import { pivotFrom } from "../../foundations/pivot.js";
6
+ import { downloadPivot } from "../../foundations/pivotSheet.js";
7
+ import { Button, IconButton } from "../../primitives/button/Button.js";
8
+ import { Menu } from "../../primitives/menu/Menu.js";
9
+ import { PivotTable } from "../../primitives/table/PivotTable.js";
10
+ import { Tags } from "../../primitives/tag/Tags.js";
11
+ import { ArrowLeftRight, Download, Plus, Sigma } from "lucide-react";
12
+ import { useMemo, useState } from "react";
13
+ const levelId = (level) => `${level.field}:${level.grain ?? ""}`;
14
+ const fromId = (id) => {
15
+ const [field = "", grain = ""] = id.split(":");
16
+ return grain === "" ? { field } : { field, grain: grain };
17
+ };
18
+ /**
19
+ * A pivot people actually pivot: what goes down the rows, what goes across the columns, which
20
+ * measures — chosen on the grid, not fixed in code.
21
+ *
22
+ * Context: the fold (`pivotFrom`) was already a real pivot — N levels each way, margins folded
23
+ * from the base — but its axes and measures were written once by whoever composed it. Here
24
+ * they are the person's: «+» adds a field (a date with its grain) to an axis, a level's ×
25
+ * drops it, ⇄ swaps the axes, the measures are ticked, and the grid downloads as it is seen.
26
+ * Inside a list the rows FOLLOW the search bar's grouping and the records are the ones the
27
+ * search left: the pivot answers the same question the list is asking.
28
+ */
29
+ export function PivotExplorer({ items, of, fields, measures, defaultMeasures, rows: outerRows, defaultRows = [], defaultColumns = [], onCell, height, fileName, }) {
30
+ const copy = useUiCopy();
31
+ const words = copy.pivot;
32
+ const [ownRows, setOwnRows] = useState(defaultRows);
33
+ const [columns, setColumns] = useState(defaultColumns);
34
+ const [chosen, setChosen] = useState(() => new Set(defaultMeasures ?? measures.slice(0, 1).map((one) => one.id)));
35
+ const rows = outerRows ?? ownRows;
36
+ const following = outerRows !== undefined;
37
+ const labelOf = (level) => {
38
+ const field = fields.find((one) => one.field === level.field)?.label ?? level.field;
39
+ return level.grain === undefined
40
+ ? field
41
+ : `${field} · ${grainName(level.grain).toLowerCase()}`;
42
+ };
43
+ const shownMeasures = measures.filter((one) => chosen.has(one.id));
44
+ const pivot = useMemo(() => pivotFrom(items, { rows, columns, of, measures: shownMeasures, labelOf }),
45
+ // eslint-disable-next-line react-hooks/exhaustive-deps
46
+ [items, rows, columns, of, shownMeasures.map((one) => one.id).join("|")]);
47
+ /* What can be added to an axis: every field (each grain of a date) not on either axis. */
48
+ const used = new Set([...rows, ...columns].map(levelId));
49
+ const options = fields.flatMap((field) => (field.grains ?? [undefined]).map((grain) => {
50
+ const level = grain === undefined ? { field: field.field } : { field: field.field, grain };
51
+ return {
52
+ id: levelId(level),
53
+ label: labelOf(level),
54
+ disabled: used.has(levelId(level)),
55
+ };
56
+ }));
57
+ const axis = (name, levels, set) => (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-1.5", children: [_jsx("span", { className: "text-micro font-medium text-muted", children: name }), _jsx(Tags, { label: name, items: levels.map((level) => ({
58
+ id: levelId(level),
59
+ label: labelOf(level),
60
+ ...(set === null
61
+ ? {}
62
+ : {
63
+ onRemove: () => set(levels.filter((one) => levelId(one) !== levelId(level))),
64
+ removeLabel: fillCopy(words.removeLevel, { level: labelOf(level) }),
65
+ }),
66
+ })) }), set === null ? (_jsx("span", { className: "text-micro text-muted italic", children: words.fromSearch })) : (_jsx(Menu, { label: fillCopy(words.addLevel, { axis: name }), icon: Plus, iconOnly: true, chevron: false, variant: "text", groups: [{ options }], onSelect: (id) => set([...levels, fromId(id)]) }))] }));
67
+ return (_jsxs("div", { className: "flex min-w-0 flex-col gap-3", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-5 gap-y-2", children: [_jsx(Menu, { label: words.measures, icon: Sigma, groups: [
68
+ {
69
+ options: measures.map((one) => ({
70
+ id: one.id,
71
+ label: one.header,
72
+ checked: chosen.has(one.id),
73
+ })),
74
+ },
75
+ ], onSelect: (id) => setChosen((current) => {
76
+ const next = new Set(current);
77
+ if (next.has(id))
78
+ next.delete(id);
79
+ else
80
+ next.add(id);
81
+ return next;
82
+ }) }), axis(words.rowsAxis, rows, following ? null : setOwnRows), axis(words.columnsAxis, columns, setColumns), _jsxs("div", { className: "ml-auto flex items-center gap-1", children: [following ? null : (_jsx(IconButton, { icon: ArrowLeftRight, label: words.swap, onClick: () => {
83
+ setOwnRows(columns);
84
+ setColumns(rows);
85
+ } })), _jsx(Button, { role: "ghost", size: "sm", icon: Download, onClick: () => downloadPivot(pivot, fileName === undefined ? {} : { name: fileName }), children: words.download })] })] }), _jsx(PivotTable, { pivot: pivot, ...(onCell === undefined ? {} : { onCell }), ...(height === undefined ? {} : { height }) })] }));
86
+ }
@@ -12,22 +12,20 @@ import type { ReactNode } from "react";
12
12
  * Its hierarchy is the row's: the state first (what it is), the name as the entry point,
13
13
  * the facts below in the quiet size.
14
14
  */
15
- export declare function RecordCard({ title, tags, meta, figure, media, onOpen, }: {
16
- /** The record's name — the entry point of the card. */
15
+ export declare function RecordCard({ title, subtitle, tags, meta, figure, media, footer, onOpen, }: {
17
16
  title: ReactNode;
18
- /** The state, as a `Tag`: read before the name, the way a board is scanned. */
17
+ /** Who or what it is about, under the title: the customer of an opportunity. */
18
+ subtitle?: ReactNode;
19
19
  tags?: ReactNode;
20
- /** The facts that tell two cards apart — a number, a date. */
21
20
  meta?: ReactNode;
22
- /** The record's key figure, on the right of the name — an amount. */
23
21
  figure?: ReactNode;
24
- /** The record's picture — a product photo, a customer's logo — or its icon. A board of
25
- * products is recognised by the pictures before a single name is read. */
26
22
  media?: {
27
23
  src?: string;
28
24
  alt?: string;
29
25
  icon?: LucideIcon;
30
26
  };
31
- /** Opens the record. Without it the card is not pressable. */
27
+ /** The card's last line, its controls: priority, kanban state, next activity, the owner's
28
+ * avatar — each its own button, pressable without opening the card. */
29
+ footer?: ReactNode;
32
30
  onOpen?: () => void;
33
31
  }): import("react").JSX.Element;
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Thumb } from "../../primitives/cell/CellStack.js";
4
4
  /**
5
5
  * One record as a card — the board presentation of a list.
@@ -13,10 +13,16 @@ import { Thumb } from "../../primitives/cell/CellStack.js";
13
13
  * Its hierarchy is the row's: the state first (what it is), the name as the entry point,
14
14
  * the facts below in the quiet size.
15
15
  */
16
- export function RecordCard({ title, tags, meta, figure, media, onOpen, }) {
17
- const body = (_jsxs(_Fragment, { children: [media === undefined ? null : (_jsx(Thumb, { size: "lg", ...(media.src === undefined ? {} : { src: media.src }), ...(media.alt === undefined ? {} : { alt: media.alt }), ...(media.icon === undefined ? {} : { icon: media.icon }) })), tags, _jsxs("div", { className: "flex min-w-0 items-baseline justify-between gap-3", children: [_jsx("p", { className: "min-w-0 truncate text-lead font-medium text-ink", children: title }), figure === undefined ? null : (_jsx("p", { className: "shrink-0 text-lead font-semibold tabular-nums text-ink", children: figure }))] }), meta === undefined ? null : (_jsx("p", { className: "text-micro tabular-nums text-muted", children: meta }))] }));
18
- const box = "flex min-w-0 flex-col gap-2 rounded-box border border-line bg-surface p-4 text-left";
19
- if (onOpen === undefined)
20
- return _jsx("div", { className: box, children: body });
21
- return (_jsx("button", { type: "button", onClick: onOpen, className: `${box} transition-colors duration-(--duration-instant) hover:border-line-strong focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary`, children: body }));
16
+ export function RecordCard({ title, subtitle, tags, meta, figure, media, footer, onOpen, }) {
17
+ /* The card is a box, and the TITLE is the button that opens it, stretched over the whole
18
+ card (`after:inset-0`). It used to be a `<button>` around everything: a priority star or a
19
+ kanban state inside it would be a control inside a control, which is invalid and which a
20
+ screen reader announces as one thing. The footer's controls sit above the stretch. */
21
+ const titleText = _jsx("span", { className: "min-w-0 truncate", children: title });
22
+ return (_jsxs("div", { className: `relative flex w-full min-w-0 flex-col gap-2 rounded-box border border-line bg-surface p-4 text-left ${onOpen === undefined
23
+ ? ""
24
+ : "transition-colors duration-(--duration-instant) focus-within:border-line-strong hover:border-line-strong"}`, children: [media === undefined ? null : (_jsx(Thumb, { size: "lg", ...(media.src === undefined ? {} : { src: media.src }), ...(media.alt === undefined ? {} : { alt: media.alt }), ...(media.icon === undefined ? {} : { icon: media.icon }) })), tags === undefined ? null : _jsx("div", { className: "flex flex-wrap gap-1.5", children: tags }), _jsxs("div", { className: "flex min-w-0 items-baseline justify-between gap-3", children: [onOpen === undefined ? (_jsx("p", { className: "flex min-w-0 text-lead font-medium text-ink", children: titleText })) : (_jsx("button", { type: "button", onClick: onOpen, className: "flex min-w-0 text-left text-lead font-medium text-ink outline-none after:absolute after:inset-0 after:rounded-box focus-visible:after:outline-2 focus-visible:after:outline-primary", children: titleText })), figure === undefined ? null : (_jsx("p", { className: "shrink-0 text-lead font-semibold tabular-nums text-ink", children: figure }))] }), subtitle === undefined ? null : (
25
+ /* Above the stretched title, at its own width: the subtitle may be a link to ANOTHER
26
+ record (the customer), which must open that one and not the card. */
27
+ _jsx("p", { className: "relative z-10 -mt-1.5 w-fit max-w-full min-w-0 truncate text-meta text-muted", children: subtitle })), meta === undefined ? null : (_jsx("p", { className: "text-micro tabular-nums text-muted", children: meta })), footer === undefined ? null : (_jsx("div", { className: "relative z-10 flex min-w-0 items-center gap-1.5", children: footer }))] }));
22
28
  }
@@ -18,8 +18,13 @@ import type { ReactNode } from "react";
18
18
  *
19
19
  * «some metadata that might help is not visible» — and it was literal: to know
20
20
  * whether `FAC/2026/0118` was paid or overdue you had to open it. */
21
- export declare function Row({ icon: Icon, name, onOpen, onPeek, tags, meta, selected, onSelectedChange, actions, more, onMoreSelect, openHint, children, copy: copyAs, }: {
21
+ export declare function Row({ icon: Icon, avatar, name, onOpen, onPeek, click, openers, tags, meta, selected, onSelectedChange, actions, more, onMoreSelect, openHint, children, copy: copyAs, }: {
22
22
  icon?: LucideIcon;
23
+ /** A person or a company in the icon's place: who the row is, recognised before it is read. */
24
+ avatar?: {
25
+ name: string;
26
+ src?: string;
27
+ };
23
28
  name: string;
24
29
  /** The copy button among the row's actions: on by default, copying the row's name — the
25
30
  * number of an invoice, a customer — without opening it. A string copies that instead;
@@ -27,6 +32,10 @@ export declare function Row({ icon: Icon, name, onOpen, onPeek, tags, meta, sele
27
32
  copy?: string | false;
28
33
  onOpen?: () => void;
29
34
  onPeek?: () => void;
35
+ /** What a press on the body does when both exist: the look (default) or the record. */
36
+ click?: "peek" | "open";
37
+ /** The 👁 / ↗ buttons. Off in a list that chooses: there the row answers, it does not open. */
38
+ openers?: boolean;
30
39
  tags?: ReactNode;
31
40
  /** The secondary data, already formatted. In mono: it aligns across rows. */
32
41
  meta?: string;
@@ -67,7 +76,7 @@ export declare function Row({ icon: Icon, name, onOpen, onPeek, tags, meta, sele
67
76
  * espaciadores de casilla, ícono y acciones— en vez de declarar un ancho
68
77
  * propio. Si `Row` cambia su geometría, esto la sigue.
69
78
  */
70
- export declare function RowHeader({ name, meta, tags, withSelection, withIcon, withActions, }: {
79
+ export declare function RowHeader({ name, meta, tags, withSelection, withIcon, withAvatar, withActions, trail, }: {
71
80
  name: string;
72
81
  meta?: string;
73
82
  tags?: string;
@@ -75,10 +84,20 @@ export declare function RowHeader({ name, meta, tags, withSelection, withIcon, w
75
84
  * es el único que sabe si esta lista elige y si sus filas tienen ícono. */
76
85
  withSelection?: boolean;
77
86
  withIcon?: boolean;
87
+ /** The rows carry a face instead of an icon: a wider spacer, so the labels stay aligned. */
88
+ withAvatar?: boolean;
78
89
  /** La zona de acciones de la derecha ocupa su lugar aunque esté invisible
79
90
  * —así nada se corre al pasar el puntero—, así que el rótulo de las fichas
80
91
  * queda desalineado si acá no se reserva lo mismo. */
81
92
  withActions?: boolean;
93
+ /** What the rows have at their end — the same flags `Row` gets — drawn invisible here so
94
+ * the header reserves exactly that width. Takes over from `withActions`. */
95
+ trail?: {
96
+ actions?: ReactNode;
97
+ peek?: boolean;
98
+ open?: boolean;
99
+ copy?: boolean;
100
+ };
82
101
  }): import("react").JSX.Element;
83
102
  /**
84
103
  * The header of a group inside a list.
@@ -1,12 +1,28 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { RecordLink } from "./RecordLink.js";
4
4
  import { fillCopy, useUiCopy } from "../../foundations/copy.js";
5
5
  import { byDensity, useDensity } from "../../foundations/density.js";
6
6
  import { stickyTop } from "../../foundations/sticky.js";
7
7
  import { Menu } from "../../primitives/menu/Menu.js";
8
+ import { HEADER_TEXT } from "../../primitives/table/Table.js";
8
9
  import { CopyButton } from "../../primitives/value/Copyable.js";
9
- import { ChevronRight, MoreHorizontal, SquareArrowOutUpRight } from "lucide-react";
10
+ import { Avatar } from "../../primitives/value/Person.js";
11
+ import { ChevronRight, Eye, MoreHorizontal, SquareArrowOutUpRight } from "lucide-react";
12
+ const ICON_BUTTON = "inline-flex size-(--size-control-sm) items-center justify-center rounded-pill text-muted transition-colors duration-(--duration-instant) hover:bg-ink/6 hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary";
13
+ /** The buttons at the end of a row. Its own piece so `RowHeader` can draw the same one,
14
+ * invisible, and reserve exactly its width — a spacer of a guessed width drifted every time
15
+ * a button was added. */
16
+ function Trail({ name, openAs, actions, copy: copyAs, onPeek, onOpen, more, onMoreSelect, }) {
17
+ const copy = useUiCopy();
18
+ return (_jsxs(_Fragment, { children: [actions, copyAs === false ? null : (_jsx(CopyButton, { value: copyAs ?? name, label: fillCopy(copy.copyable.field, { label: name }) })), onPeek === undefined ? null : (_jsx("button", { type: "button", onClick: (e) => {
19
+ e.stopPropagation();
20
+ onPeek();
21
+ }, title: copy.list.peek, "aria-label": `${copy.list.peek}: ${name}`, className: ICON_BUTTON, children: _jsx(Eye, { className: "size-(--size-icon-sm)", strokeWidth: 2 }) })), onOpen === undefined ? null : (_jsx("button", { type: "button", onClick: (e) => {
22
+ e.stopPropagation();
23
+ onOpen();
24
+ }, title: openAs, "aria-label": `${openAs}: ${name}`, className: ICON_BUTTON, children: _jsx(SquareArrowOutUpRight, { className: "size-(--size-icon-sm)", strokeWidth: 2 }) })), more === undefined || more.length === 0 ? null : (_jsx("span", { onClick: (e) => e.stopPropagation(), role: "presentation", children: _jsx(Menu, { label: fillCopy(copy.selection.moreOf, { name }), groups: more, icon: MoreHorizontal, iconOnly: true, chevron: false, align: "end", width: "sm", variant: "text", ...(onMoreSelect === undefined ? {} : { onSelect: onMoreSelect }) }) }))] }));
25
+ }
10
26
  /* ---------------------------------------------------------------- *
11
27
  * THE RICH ROW — the other density
12
28
  * ---------------------------------------------------------------- */
@@ -27,10 +43,13 @@ import { ChevronRight, MoreHorizontal, SquareArrowOutUpRight } from "lucide-reac
27
43
  *
28
44
  * «some metadata that might help is not visible» — and it was literal: to know
29
45
  * whether `FAC/2026/0118` was paid or overdue you had to open it. */
30
- export function Row({ icon: Icon, name, onOpen, onPeek, tags, meta, selected, onSelectedChange, actions, more, onMoreSelect, openHint, children, copy: copyAs, }) {
46
+ export function Row({ icon: Icon, avatar, name, onOpen, onPeek, click = "peek", openers = true, tags, meta, selected, onSelectedChange, actions, more, onMoreSelect, openHint, children, copy: copyAs, }) {
31
47
  const d = useDensity();
32
48
  const copy = useUiCopy();
33
- const openAs = openHint ?? copy.shell.openAsPanel;
49
+ const openAs = openHint ?? copy.list.open;
50
+ /* A read-only row (nothing to peek, nothing to open) does not light up on hover: a wash
51
+ and a hand promise a click that does nothing. */
52
+ const interactive = (onPeek ?? onOpen) !== undefined;
34
53
  const row = (
35
54
  /* A `div` and not an `li`: whoever composes decides whether this is a list,
36
55
  a table or a branch of a tree. And the zone that peeks is a `div` with
@@ -42,21 +61,18 @@ export function Row({ icon: Icon, name, onOpen, onPeek, tags, meta, selected, on
42
61
  no rule between them read as a paragraph. The wash is the inner row,
43
62
  inset and rounded, so the hover is a mark and not a stripe. The inner
44
63
  padding is shorter by exactly that inset, so the text does not move. */
45
- className: `group/row flex min-w-0 items-center rounded-control transition-colors duration-(--duration-instant) hover:bg-ink/4 ${byDensity(d, "gap-3 px-2 py-2", "gap-2 px-1.5 py-1")}`,
64
+ className: `group/row flex min-w-0 items-center rounded-control transition-colors duration-(--duration-instant) ${interactive ? "cursor-pointer hover:bg-ink/4" : ""} ${byDensity(d, "gap-3 px-2 py-2", "gap-2 px-1.5 py-1")}`,
46
65
  /* `onPeek` con `onOpen` de reserva: quien sólo declara `onOpen` —lo
47
66
  natural si nunca oyó hablar de la vista previa— tiene que poder abrir
48
67
  igual haciendo click en cualquier parte de la fila, no sólo en el
49
68
  nombre. Con la vista previa apagada (`PeekLayer.tsx`) esto es hoy el
50
69
  caso de todos: un click en la fila abre, sin más. */
51
- onClick: onPeek ?? onOpen, role: "presentation", children: [onSelectedChange === undefined ? null : (_jsx("input", { type: "checkbox", checked: selected ?? false, "aria-label": fillCopy(copy.selection.named, { name }), onChange: (e) => onSelectedChange(e.target.checked), onClick: (e) => e.stopPropagation(), className: "size-3.5 shrink-0 cursor-pointer accent-primary" })), Icon === undefined ? null : (_jsx(Icon, { className: "size-(--size-icon-md) shrink-0 text-muted", strokeWidth: 1.75, "aria-hidden": true })), _jsxs("div", { className: `flex min-w-0 shrink grow basis-(--container-name) flex-wrap items-center gap-y-0 ${byDensity(d, "gap-x-3", "gap-x-2")}`, children: [_jsx("span", { className: "min-w-0 grow basis-(--container-name)", children: _jsx(RecordLink, { ...((onOpen ?? onPeek) === undefined ? {} : { onClick: (onOpen ?? onPeek) }), title: name, children: name }) }), meta === undefined ? null : (
70
+ onClick: click === "open" ? (onOpen ?? onPeek) : (onPeek ?? onOpen), role: "presentation", children: [onSelectedChange === undefined ? null : (_jsx("input", { type: "checkbox", checked: selected ?? false, "aria-label": fillCopy(copy.selection.named, { name }), onChange: (e) => onSelectedChange(e.target.checked), onClick: (e) => e.stopPropagation(), className: "size-3.5 shrink-0 cursor-pointer accent-primary" })), avatar !== undefined ? (_jsx(Avatar, { name: avatar.name, size: "sm", ...(avatar.src === undefined ? {} : { src: avatar.src }) })) : Icon === undefined ? null : (_jsx(Icon, { className: "size-(--size-icon-md) shrink-0 text-muted", strokeWidth: 1.75, "aria-hidden": true })), _jsxs("div", { className: `flex min-w-0 shrink grow basis-(--container-name) flex-wrap items-center gap-y-0 ${byDensity(d, "gap-x-3", "gap-x-2")}`, children: [_jsx("span", { className: "min-w-0 grow basis-(--container-name)", children: _jsx(RecordLink, { ...((onOpen ?? onPeek) === undefined ? {} : { onClick: (onOpen ?? onPeek) }), title: name, children: name }) }), meta === undefined ? null : (
52
71
  /* Wrapped or not, the metadata is still there —it is what saves
53
72
  having to open a set to know whether it is a thousand rows or a
54
73
  million— and it truncates instead of wrapping into itself: three
55
74
  lines per row in a 480 px drawer means three rows fit on screen. */
56
- _jsx("span", { className: "min-w-0 truncate text-micro tabular-nums text-muted", children: meta }))] }), tags === undefined ? null : (_jsx("span", { className: "flex min-w-fit shrink basis-(--container-tags) items-center justify-end gap-1.5", children: tags })), _jsxs("span", { className: "flex shrink-0 items-center gap-1 opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 focus-within:opacity-100 pointer-coarse:opacity-100", children: [actions, copyAs === false ? null : (_jsx(CopyButton, { value: copyAs ?? name, label: fillCopy(copy.copyable.field, { label: name }) })), onOpen === undefined ? null : (_jsx("button", { type: "button", onClick: (e) => {
57
- e.stopPropagation();
58
- onOpen();
59
- }, title: openAs, "aria-label": `${openAs}: ${name}`, className: "inline-flex size-(--size-control-sm) items-center justify-center rounded-pill text-muted transition-colors duration-(--duration-instant) hover:bg-ink/6 hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary", children: _jsx(SquareArrowOutUpRight, { className: "size-(--size-icon-sm)", strokeWidth: 2 }) })), more === undefined || more.length === 0 ? null : (_jsx("span", { onClick: (e) => e.stopPropagation(), role: "presentation", children: _jsx(Menu, { label: fillCopy(copy.selection.moreOf, { name }), groups: more, icon: MoreHorizontal, iconOnly: true, chevron: false, align: "end", width: "sm", variant: "text", ...(onMoreSelect === undefined ? {} : { onSelect: onMoreSelect }) }) }))] })] }) }));
75
+ _jsx("span", { className: "min-w-0 truncate text-micro tabular-nums text-muted", children: meta }))] }), tags === undefined ? null : (_jsx("span", { className: "flex min-w-fit shrink basis-(--container-tags) items-center justify-end gap-1.5", children: tags })), _jsx("span", { className: "flex shrink-0 items-center gap-1 opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 focus-within:opacity-100 pointer-coarse:opacity-100", children: _jsx(Trail, { name: name, openAs: openAs, ...(actions === undefined ? {} : { actions }), copy: copyAs, ...(onPeek === undefined || !openers ? {} : { onPeek }), ...(onOpen === undefined || !openers ? {} : { onOpen }), ...(more === undefined ? {} : { more }), ...(onMoreSelect === undefined ? {} : { onMoreSelect }) }) })] }) }));
60
76
  if (children === undefined)
61
77
  return row;
62
78
  /* What the resource produced, hanging below. The indent reaches up to where
@@ -87,13 +103,13 @@ export function Row({ icon: Icon, name, onOpen, onPeek, tags, meta, selected, on
87
103
  * espaciadores de casilla, ícono y acciones— en vez de declarar un ancho
88
104
  * propio. Si `Row` cambia su geometría, esto la sigue.
89
105
  */
90
- export function RowHeader({ name, meta, tags, withSelection = false, withIcon = false, withActions = false, }) {
106
+ export function RowHeader({ name, meta, tags, withSelection = false, withIcon = false, withAvatar = false, withActions = false, trail, }) {
91
107
  const d = useDensity();
92
- const rotulo = "truncate text-micro font-medium text-muted";
108
+ const rotulo = `truncate ${HEADER_TEXT}`;
93
109
  return (_jsxs("div", { "data-slot": "row-header",
94
110
  /* Se pega debajo de la barra de la lista, como el encabezado de la tabla
95
111
  y el de la planilla. Ver `system/sticky.tsx`. */
96
- className: `sticky z-10 flex min-w-0 items-center border-b border-line-strong bg-ground ${byDensity(d, "gap-3 px-3 py-2", "gap-2 px-2.5 py-1.5")}`, style: stickyTop, role: "presentation", children: [!withSelection ? null : _jsx("span", { className: "size-3.5 shrink-0", "aria-hidden": true }), !withIcon ? null : _jsx("span", { className: "size-(--size-icon-md) shrink-0", "aria-hidden": true }), _jsxs("div", { className: `flex min-w-0 shrink grow basis-(--container-name) flex-wrap items-center gap-y-0 ${byDensity(d, "gap-x-3", "gap-x-2")}`, children: [_jsx("span", { className: `min-w-0 grow basis-(--container-name) ${rotulo}`, children: name }), meta === undefined ? null : _jsx("span", { className: `min-w-0 ${rotulo}`, children: meta })] }), tags === undefined ? null : (_jsx("span", { className: "flex min-w-fit shrink basis-(--container-tags) items-center justify-end", children: _jsx("span", { className: rotulo, children: tags }) })), !withActions ? null : (_jsx("span", { className: "size-(--size-control-sm) shrink-0", "aria-hidden": true }))] }));
112
+ className: `sticky z-10 flex min-w-0 items-center border-b border-line-strong bg-ground ${byDensity(d, "gap-3 px-3 py-2", "gap-2 px-2.5 py-1.5")}`, style: stickyTop, role: "presentation", children: [!withSelection ? null : _jsx("span", { className: "size-3.5 shrink-0", "aria-hidden": true }), withAvatar ? (_jsx("span", { className: "size-5 shrink-0", "aria-hidden": true })) : !withIcon ? null : (_jsx("span", { className: "size-(--size-icon-md) shrink-0", "aria-hidden": true })), _jsxs("div", { className: `flex min-w-0 shrink grow basis-(--container-name) flex-wrap items-center gap-y-0 ${byDensity(d, "gap-x-3", "gap-x-2")}`, children: [_jsx("span", { className: `min-w-0 grow basis-(--container-name) ${rotulo}`, children: name }), meta === undefined ? null : _jsx("span", { className: `min-w-0 ${rotulo}`, children: meta })] }), tags === undefined ? null : (_jsx("span", { className: "flex min-w-fit shrink basis-(--container-tags) items-center justify-end", children: _jsx("span", { className: rotulo, children: tags }) })), trail !== undefined ? (_jsx("span", { inert: true, "aria-hidden": true, className: "invisible flex shrink-0 items-center gap-1", children: _jsx(Trail, { name: "", openAs: "", ...(trail.actions === undefined ? {} : { actions: trail.actions }), ...(trail.copy === false ? { copy: false } : {}), ...(trail.peek === true ? { onPeek: () => { } } : {}), ...(trail.open === true ? { onOpen: () => { } } : {}) }) })) : !withActions ? null : (_jsx("span", { className: "size-(--size-control-sm) shrink-0", "aria-hidden": true }))] }));
97
113
  }
98
114
  /**
99
115
  * The header of a group inside a list.
@@ -11,7 +11,9 @@ export { type Selection, useSelection } from "./selection.js";
11
11
  export { type SetControl, useSetControl } from "./set.js";
12
12
  export { toggleCollapsed, type TreeControl, useTreeControl, } from "./tree.js";
13
13
  export { type ViewOption, ViewSwitcher, } from "./ViewSwitcher.js";
14
+ export { type BoardColumn, BoardColumns, type BoardStages, } from "./BoardColumns.js";
14
15
  export { type GroupingControl, useGroupingControl, } from "./grouping.js";
15
16
  export { GroupingLevels, type GroupingLevelsProps, } from "./GroupingLevels.js";
17
+ export { PivotExplorer, type PivotExplorerProps, } from "./PivotExplorer.js";
16
18
  export { RecordCard } from "./RecordCard.js";
17
19
  export { type SortField, SortMenu, type SortMenuProps, } from "./SortMenu.js";
@@ -14,7 +14,9 @@ export { useSetControl } from "./set.js";
14
14
  export { toggleCollapsed, useTreeControl, } from "./tree.js";
15
15
  export { ViewSwitcher, } from "./ViewSwitcher.js";
16
16
  /* El control de los niveles. El vocabulario está en `system/grouping.ts`. */
17
+ export { BoardColumns, } from "./BoardColumns.js";
17
18
  export { useGroupingControl, } from "./grouping.js";
18
19
  export { GroupingLevels, } from "./GroupingLevels.js";
20
+ export { PivotExplorer, } from "./PivotExplorer.js";
19
21
  export { RecordCard } from "./RecordCard.js";
20
22
  export { SortMenu, } from "./SortMenu.js";
@@ -33,7 +33,7 @@ import { type Pages } from "./pages.js";
33
33
  * también; lo que se lee está en el cruce. Tampoco se
34
34
  * ordena: el orden de cada eje es el de sus claves.
35
35
  */
36
- export type ListPresent = "rows" | "table" | "board" | "sheet" | "report" | "pivot";
36
+ export type ListPresent = "rows" | "table" | "board" | "sheet" | "report" | "pivot" | "calendar" | "gantt" | "timeline";
37
37
  export interface SetControl<Row, Present extends string = ListPresent> extends Pages<Row> {
38
38
  present: Present;
39
39
  setPresent: (next: Present) => void;
@@ -1,3 +1 @@
1
- export { CommandPalette, useCommandPaletteShortcut, } from "./CommandPalette.js";
2
- export { type PaletteCommand } from "./commands.js";
3
1
  export { NavigationTree, type NavItem, } from "./NavigationTree.js";
@@ -1,4 +1,3 @@
1
- /* LA NAVEGACIÓN — el árbol lateral de una aplicación y la paleta de comandos. */
2
- export { CommandPalette, useCommandPaletteShortcut, } from "./CommandPalette.js";
3
- export {} from "./commands.js";
1
+ /* LA NAVEGACIÓN — el árbol lateral de una aplicación. La paleta de comandos (⌘K) es del
2
+ espacio de trabajo (composer), no de acá. */
4
3
  export { NavigationTree, } from "./NavigationTree.js";
@@ -0,0 +1,17 @@
1
+ import type { ReactNode } from "react";
2
+ /** Who did something to the record and when, already written: «Carla Rojas», «hace 3 días». */
3
+ export type RecordStamp = {
4
+ by: ReactNode;
5
+ at: ReactNode;
6
+ };
7
+ /**
8
+ * The last line of the paper: who created the record and who touched it last.
9
+ *
10
+ * Context: «¿quién cambió esto?» is the first question in front of a number that looks wrong,
11
+ * and the activity's timeline answers it only for what was logged. Small and at the end: it is
12
+ * consulted, not read.
13
+ */
14
+ export declare function RecordAudit({ created, updated, }: {
15
+ created?: RecordStamp;
16
+ updated?: RecordStamp;
17
+ }): import("react").JSX.Element | null;