@sincpro/web-ui 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/README.md +40 -13
  2. package/dist/components/activity/Activity.d.ts +69 -0
  3. package/dist/components/activity/Activity.js +279 -0
  4. package/dist/components/activity/ActivityBadge.d.ts +16 -0
  5. package/dist/components/activity/ActivityBadge.js +29 -0
  6. package/dist/components/{chatter → activity}/Messages.d.ts +2 -2
  7. package/dist/components/{chatter → activity}/Messages.js +9 -6
  8. package/dist/components/activity/index.d.ts +4 -0
  9. package/dist/components/activity/index.js +5 -0
  10. package/dist/components/assist/AssistMark.d.ts +3 -3
  11. package/dist/components/assist/AssistMark.js +1 -1
  12. package/dist/components/assist/assist.d.ts +1 -1
  13. package/dist/components/assist/assist.js +1 -1
  14. package/dist/components/calendar/RecordCalendar.d.ts +48 -0
  15. package/dist/components/calendar/RecordCalendar.js +101 -0
  16. package/dist/components/calendar/index.d.ts +1 -0
  17. package/dist/components/calendar/index.js +1 -0
  18. package/dist/components/form/Sheet.d.ts +4 -0
  19. package/dist/components/form/Sheet.js +16 -5
  20. package/dist/components/gantt/RecordGantt.d.ts +44 -0
  21. package/dist/components/gantt/RecordGantt.js +110 -0
  22. package/dist/components/gantt/index.d.ts +1 -0
  23. package/dist/components/gantt/index.js +1 -0
  24. package/dist/components/index.d.ts +4 -1
  25. package/dist/components/index.js +4 -1
  26. package/dist/components/lines/LineItems.d.ts +45 -5
  27. package/dist/components/lines/LineItems.js +106 -18
  28. package/dist/components/listing/BoardColumns.d.ts +39 -0
  29. package/dist/components/listing/BoardColumns.js +138 -0
  30. package/dist/components/listing/Listing.js +1 -1
  31. package/dist/components/listing/ListingBar.d.ts +46 -2
  32. package/dist/components/listing/ListingBar.js +47 -5
  33. package/dist/components/listing/PivotExplorer.d.ts +34 -0
  34. package/dist/components/listing/PivotExplorer.js +86 -0
  35. package/dist/components/listing/RecordCard.d.ts +6 -8
  36. package/dist/components/listing/RecordCard.js +13 -7
  37. package/dist/components/listing/Row.d.ts +25 -2
  38. package/dist/components/listing/Row.js +30 -13
  39. package/dist/components/listing/index.d.ts +2 -0
  40. package/dist/components/listing/index.js +2 -0
  41. package/dist/components/listing/set.d.ts +1 -1
  42. package/dist/components/navigation/index.d.ts +0 -2
  43. package/dist/components/navigation/index.js +2 -3
  44. package/dist/components/record/RecordAudit.d.ts +17 -0
  45. package/dist/components/record/RecordAudit.js +27 -0
  46. package/dist/components/record/RecordHeader.d.ts +27 -17
  47. package/dist/components/record/RecordHeader.js +64 -20
  48. package/dist/components/record/RecordNotices.d.ts +37 -0
  49. package/dist/components/record/RecordNotices.js +75 -0
  50. package/dist/components/record/RecordRibbon.d.ts +13 -0
  51. package/dist/components/record/RecordRibbon.js +22 -0
  52. package/dist/components/record/ThumbBar.js +1 -1
  53. package/dist/components/record/index.d.ts +2 -0
  54. package/dist/components/record/index.js +2 -0
  55. package/dist/components/search/CriteriaChips.d.ts +6 -1
  56. package/dist/components/search/CriteriaChips.js +71 -20
  57. package/dist/components/search/SearchBar.d.ts +4 -1
  58. package/dist/components/search/SearchBar.js +15 -7
  59. package/dist/components/timeline/RecordTimeline.d.ts +60 -0
  60. package/dist/components/timeline/RecordTimeline.js +214 -0
  61. package/dist/components/timeline/TimelineToolbar.d.ts +30 -0
  62. package/dist/components/timeline/TimelineToolbar.js +31 -0
  63. package/dist/components/timeline/index.d.ts +3 -0
  64. package/dist/components/timeline/index.js +3 -0
  65. package/dist/components/timeline/time.d.ts +43 -0
  66. package/dist/components/timeline/time.js +135 -0
  67. package/dist/foundations/copy.d.ts +194 -18
  68. package/dist/foundations/copy.es.js +142 -15
  69. package/dist/foundations/copy.js +133 -11
  70. package/dist/foundations/index.d.ts +1 -1
  71. package/dist/foundations/match.d.ts +23 -0
  72. package/dist/foundations/match.js +63 -0
  73. package/dist/foundations/slots.d.ts +1 -1
  74. package/dist/foundations/slots.js +8 -3
  75. package/dist/foundations/types.d.ts +7 -0
  76. package/dist/foundations/types.js +0 -36
  77. package/dist/primitives/block/index.d.ts +1 -1
  78. package/dist/primitives/block/index.js +1 -1
  79. package/dist/primitives/cell/CellStack.d.ts +7 -1
  80. package/dist/primitives/cell/CellStack.js +5 -2
  81. package/dist/primitives/choice/ComboBox.d.ts +3 -0
  82. package/dist/primitives/choice/ComboBox.js +10 -6
  83. package/dist/primitives/choice/KanbanState.d.ts +14 -0
  84. package/dist/primitives/choice/KanbanState.js +37 -0
  85. package/dist/primitives/choice/Priority.d.ts +20 -0
  86. package/dist/primitives/choice/Priority.js +41 -0
  87. package/dist/primitives/choice/SearchField.d.ts +10 -1
  88. package/dist/primitives/choice/SearchField.js +21 -2
  89. package/dist/primitives/choice/Slider.d.ts +6 -1
  90. package/dist/primitives/choice/Slider.js +2 -2
  91. package/dist/primitives/choice/index.d.ts +2 -0
  92. package/dist/primitives/choice/index.js +2 -0
  93. package/dist/primitives/date/Calendar.js +30 -6
  94. package/dist/primitives/date/DatePicker.d.ts +11 -3
  95. package/dist/primitives/date/DatePicker.js +12 -8
  96. package/dist/primitives/date/DateTime.d.ts +35 -0
  97. package/dist/primitives/date/DateTime.js +100 -0
  98. package/dist/primitives/date/index.d.ts +1 -0
  99. package/dist/primitives/date/index.js +4 -2
  100. package/dist/primitives/dialog/Dialog.d.ts +9 -3
  101. package/dist/primitives/dialog/Dialog.js +41 -5
  102. package/dist/primitives/field/DropZone.js +1 -1
  103. package/dist/primitives/field/Field.d.ts +14 -2
  104. package/dist/primitives/field/Field.js +25 -14
  105. package/dist/primitives/field/copy.d.ts +16 -0
  106. package/dist/primitives/field/copy.js +69 -0
  107. package/dist/primitives/field/index.d.ts +2 -0
  108. package/dist/primitives/field/index.js +4 -0
  109. package/dist/primitives/field/note.d.ts +55 -0
  110. package/dist/primitives/field/note.js +117 -0
  111. package/dist/primitives/index.d.ts +1 -0
  112. package/dist/primitives/index.js +1 -0
  113. package/dist/primitives/menu/Menu.d.ts +28 -0
  114. package/dist/primitives/menu/Menu.js +47 -17
  115. package/dist/primitives/notice/Callout.js +1 -1
  116. package/dist/primitives/notice/Flag.d.ts +13 -0
  117. package/dist/primitives/notice/Flag.js +31 -0
  118. package/dist/primitives/notice/index.d.ts +2 -1
  119. package/dist/primitives/notice/index.js +2 -1
  120. package/dist/primitives/overlay/Preview.d.ts +19 -0
  121. package/dist/primitives/overlay/Preview.js +69 -0
  122. package/dist/primitives/overlay/index.d.ts +1 -0
  123. package/dist/primitives/overlay/index.js +3 -1
  124. package/dist/primitives/picker/MatchedText.d.ts +10 -0
  125. package/dist/primitives/picker/MatchedText.js +12 -0
  126. package/dist/primitives/picker/Picker.d.ts +67 -4
  127. package/dist/primitives/picker/Picker.js +142 -23
  128. package/dist/primitives/table/CellCopy.d.ts +20 -0
  129. package/dist/primitives/table/CellCopy.js +23 -0
  130. package/dist/primitives/table/DataTable.d.ts +55 -1
  131. package/dist/primitives/table/DataTable.js +126 -40
  132. package/dist/primitives/table/EditableTable.d.ts +102 -2
  133. package/dist/primitives/table/EditableTable.js +287 -32
  134. package/dist/primitives/table/ReportTable.js +2 -1
  135. package/dist/primitives/table/RowActions.js +7 -2
  136. package/dist/primitives/table/SheetTable.d.ts +4 -1
  137. package/dist/primitives/table/SheetTable.js +38 -5
  138. package/dist/primitives/table/Table.d.ts +5 -0
  139. package/dist/primitives/table/Table.js +17 -8
  140. package/dist/primitives/table/index.d.ts +2 -2
  141. package/dist/primitives/table/index.js +1 -1
  142. package/dist/primitives/tabs/Tabs.d.ts +2 -2
  143. package/dist/primitives/tabs/Tabs.js +1 -1
  144. package/dist/primitives/tag/Tags.d.ts +2 -0
  145. package/dist/primitives/tag/Tags.js +2 -1
  146. package/dist/primitives/tag/TagsField.d.ts +13 -0
  147. package/dist/primitives/tag/TagsField.js +39 -10
  148. package/dist/primitives/tag/choices.d.ts +2 -0
  149. package/dist/primitives/tree/Tree.d.ts +56 -0
  150. package/dist/primitives/tree/Tree.js +74 -0
  151. package/dist/primitives/tree/index.d.ts +1 -0
  152. package/dist/primitives/tree/index.js +3 -0
  153. package/dist/primitives/value/Avatar.d.ts +16 -0
  154. package/dist/primitives/value/Avatar.js +91 -0
  155. package/dist/primitives/value/Copyable.d.ts +11 -0
  156. package/dist/primitives/value/Copyable.js +27 -6
  157. package/dist/primitives/value/Fecha.js +10 -1
  158. package/dist/primitives/value/Person.d.ts +6 -51
  159. package/dist/primitives/value/Person.js +9 -148
  160. package/dist/primitives/value/ProgressBar.d.ts +31 -0
  161. package/dist/primitives/value/ProgressBar.js +53 -0
  162. package/dist/primitives/value/Relation.d.ts +22 -5
  163. package/dist/primitives/value/Relation.js +42 -14
  164. package/dist/primitives/value/Totals.d.ts +102 -7
  165. package/dist/primitives/value/Totals.js +52 -2
  166. package/dist/primitives/value/Value.d.ts +0 -37
  167. package/dist/primitives/value/Value.js +0 -39
  168. package/dist/primitives/value/contact.d.ts +30 -0
  169. package/dist/primitives/value/contact.js +51 -0
  170. package/dist/primitives/value/index.d.ts +5 -4
  171. package/dist/primitives/value/index.js +5 -4
  172. package/dist/tokens.css +179 -7
  173. package/dist/views/list/List.d.ts +32 -3
  174. package/dist/views/list/List.js +51 -7
  175. package/dist/views/list/ListView.d.ts +1 -1
  176. package/dist/views/list/ListView.js +70 -13
  177. package/dist/views/list/columns.js +8 -1
  178. package/dist/views/list/derive.d.ts +12 -1
  179. package/dist/views/list/derive.js +29 -3
  180. package/dist/views/list/presentations.d.ts +5 -2
  181. package/dist/views/list/presentations.js +35 -3
  182. package/dist/views/list/types.d.ts +57 -1
  183. package/dist/views/list/useNarrowList.d.ts +10 -0
  184. package/dist/views/list/useNarrowList.js +20 -6
  185. package/dist/views/record/Record.d.ts +41 -8
  186. package/dist/views/record/Record.js +113 -28
  187. package/dist/views/record/index.d.ts +1 -1
  188. package/package.json +15 -4
  189. package/dist/components/chatter/Chatter.d.ts +0 -161
  190. package/dist/components/chatter/Chatter.js +0 -232
  191. package/dist/components/chatter/index.d.ts +0 -3
  192. package/dist/components/chatter/index.js +0 -4
  193. package/dist/components/navigation/CommandPalette.d.ts +0 -40
  194. package/dist/components/navigation/CommandPalette.js +0 -114
  195. package/dist/components/navigation/commands.d.ts +0 -30
  196. package/dist/components/navigation/commands.js +0 -30
  197. /package/dist/components/{chatter → activity}/Timeline.d.ts +0 -0
  198. /package/dist/components/{chatter → activity}/Timeline.js +0 -0
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { Breadcrumb } from "./Breadcrumb.js";
4
4
  import { DirtyActions, } from "./DirtyActions.js";
5
5
  import { isPrimary, ThumbBar, thumbPlan } from "./ThumbBar.js";
@@ -7,8 +7,7 @@ import { byDensity, useDensity } from "../../foundations/density.js";
7
7
  import { SurfaceProvider } from "../../foundations/surface.js";
8
8
  import { Line } from "../../primitives/block/Line.js";
9
9
  import { Tag } from "../../primitives/tag/Tag.js";
10
- import { cloneElement, isValidElement } from "react";
11
- export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Icon, title, subtitle, tag, figure, railSurface = "canvas", smart, aspects, dirty, chatter, chatterLayout = "page", children, }) {
10
+ export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Icon, title, subtitle, tag, figure, railSurface = "canvas", smart, aspects, dirty, activity, activityLayout = "auto", width = "reading", ribbon, blocking, indicator, children, }) {
12
11
  const d = useDensity();
13
12
  const hasRail = crumbs !== undefined ||
14
13
  pager !== undefined ||
@@ -24,6 +23,8 @@ export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Ico
24
23
  only thing in the rail that belongs to the RECORD and not to my position in
25
24
  front of it. */
26
25
  const withRail = hasRail && d === "comfortable";
26
+ /* The activity column at the RIGHT of the sheet when the page is wide (THE PAGE). */
27
+ const beside = activity !== undefined && activityLayout === "auto";
27
28
  /* The thumb bar's content (see THE THUMB BAR in `ThumbBar`): dirty first, else the primary. */
28
29
  const { rail: railActions, primaries, mode: thumb, } = thumbPlan({ withRail, actions, dirty });
29
30
  /* In compact the TITLE is not drawn either, for the same reason as in
@@ -45,7 +46,15 @@ export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Ico
45
46
  pager !== undefined ||
46
47
  actions !== undefined ||
47
48
  dirty !== undefined);
48
- const identity = (_jsxs("div", { className: `flex flex-col ${byDensity(d, "gap-2.5 px-4 pt-3.5", "gap-2 px-0 pt-0")}`, children: [withRail || smart === undefined ? null : _jsx("div", { className: "min-w-0", children: smart }), _jsxs("div", { className: "flex min-w-0 flex-wrap items-start gap-x-5 gap-y-2", children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2.5", children: [Icon === undefined || d === "compact" ? null : (_jsx("span", { className: "mt-1 flex size-9 shrink-0 items-center justify-center rounded-box bg-ink/5 text-muted", children: _jsx(Icon, { className: "size-(--size-icon-lg)", strokeWidth: 1.75 }) })), _jsxs("div", { className: "flex min-w-0 flex-col", children: [kicker === undefined || !withIdentity ? null : (_jsx("span", { className: "text-micro font-medium text-muted", children: kicker })), _jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1", children: [!withIdentity ? null : (
49
+ /* ONE column for the rail and the page: the rail used to span the screen while the sheet
50
+ stopped at 72rem, so on a wide desk the buttons sat 250 px left of the paper they act on
51
+ and the record read as two things. */
52
+ const column = width === "full"
53
+ ? ""
54
+ : beside
55
+ ? "mx-auto w-full max-w-(--container-record)"
56
+ : "mx-auto w-full max-w-(--container-sheet)";
57
+ const identity = (_jsxs("div", { className: `flex flex-col ${byDensity(d, "gap-2.5 px-4 pt-3.5", "gap-2 px-0 pt-0")}`, children: [withRail || (smart === undefined && indicator === undefined) ? null : (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [smart, indicator] })), _jsxs("div", { className: "flex min-w-0 flex-wrap items-start gap-x-5 gap-y-2", children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2.5", children: [Icon === undefined || d === "compact" ? null : (_jsx("span", { className: "mt-1 flex size-9 shrink-0 items-center justify-center rounded-box bg-ink/5 text-muted", children: _jsx(Icon, { className: "size-(--size-icon-lg)", strokeWidth: 1.75 }) })), _jsxs("div", { className: "flex min-w-0 flex-col", children: [kicker === undefined || !withIdentity ? null : (_jsx("span", { className: "text-micro font-medium text-muted", children: kicker })), _jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1", children: [!withIdentity ? null : (
49
58
  /* `display`, not `title`: the record's name is the entry point of the
50
59
  screen. At the title size (19 px over a 13 px body, 1.46:1) nothing on
51
60
  the sheet stood out and the eye had nowhere to start. */
@@ -53,20 +62,55 @@ export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Ico
53
62
  /* The line is the sheet's mouth. In comfortable it walks to the
54
63
  edges; the current tab sits on it and opens into the page below. */
55
64
  _jsx("div", { className: `border-b border-line-strong ${byDensity(d, "-mx-4 px-4", "")}`, children: aspects }))] }));
56
- return (_jsxs(_Fragment, { children: [withRail ? (_jsx(SurfaceProvider, { value: railSurface, children: _jsx("div", { className: "@container/record", children: _jsxs("div", { className: `flex flex-col gap-2 px-3.5 py-2.5 ${railSurface === "chrome" ? "bg-chrome" : "border-b border-line bg-surface"}`, children: [_jsx(Line, { start: crumbs === undefined || crumbs.length === 0 ? null : (_jsx(Breadcrumb, { crumbs: crumbs, surface: railSurface })), center: smart, end: pager }), actions === undefined && status === undefined && dirty === undefined ? null : (
57
- /* Row 2 — WHAT I CAN DO and WHERE IT IS AT: actions stuck to the
58
- left edge, the history and the state stuck to the right one. No
59
- `flex-wrap`: wrapping split the row in two and the state ended up
60
- below the actions, which is another hierarchy. */
61
- _jsxs("div", { className: "flex min-w-0 items-start gap-x-4", children: [dirty !== undefined ? (_jsx("div", { className: "flex shrink-0 items-center gap-1.5 @max-handheld/record:hidden", children: _jsx(DirtyActions, { ...dirty }) })) : actions === undefined ? null : (_jsx("div", { className: "flex shrink-0 items-center gap-1.5", children: primaries.length === 0
62
- ? actions
63
- : railActions.map((node) => isPrimary(node) ? (_jsx("span", { "data-thumb-moves": "", className: "contents @max-handheld/record:hidden", children: node }, node.key)) : (node)) })), status === undefined ? null : (_jsx("div", { className: "ml-auto flex min-w-0 flex-col items-end gap-1.5", children: status }))] }))] }) }) })) : null, _jsx("div", { className: paper ? "bg-ground px-3 pt-3 pb-3" : "", children: _jsx("div", { className: chatter !== undefined && chatterLayout === "aside" ? "@container" : "", children: _jsxs("div", { className: chatter !== undefined && chatterLayout === "aside"
64
- ? "flex min-w-0 flex-col gap-3 @4xl:flex-row @4xl:items-start"
65
- : "", children: [_jsxs("div", { className: paper
66
- ? `flex w-full max-w-(--container-sheet) min-w-0 flex-1 flex-col overflow-clip rounded-box border border-line bg-surface ${chatter !== undefined && chatterLayout === "aside" ? "" : "mx-auto"}`
67
- : "flex min-w-0 flex-1 flex-col bg-surface", children: [identity, children, chatter !== undefined && chatterLayout === "page" ? (_jsx("div", { className: "border-t border-line", children: isValidElement(chatter)
68
- ? cloneElement(chatter, {
69
- embedded: true,
70
- })
71
- : chatter })) : null] }), chatter !== undefined && chatterLayout === "aside" ? chatter : null] }) }) }), thumb === null ? null : _jsx(ThumbBar, { mode: thumb, dirty: dirty, primaries: primaries })] }));
65
+ return (_jsxs(_Fragment, { children: [withRail ? (_jsx(SurfaceProvider, { value: railSurface, children: _jsx("div", { className: "@container/record", children: _jsx("div", { className: `px-3 py-2.5 ${railSurface === "chrome" ? "bg-chrome" : "border-b border-line bg-surface"}`, children: _jsxs("div", { className: `flex flex-col gap-2 ${paper ? column : ""}`, children: [_jsx(Line, { start: crumbs === undefined || crumbs.length === 0 ? null : (_jsx(Breadcrumb, { crumbs: crumbs, surface: railSurface })), center:
66
+ /* The notices' indicator rides with the counters: both are about the
67
+ record and both open something; neither is my position in front of
68
+ it. The line wraps on a phone, so it is never pushed out of reach. */
69
+ smart === undefined && indicator === undefined ? undefined : (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center justify-center gap-2", children: [smart, indicator] })), end: pager }), actions === undefined &&
70
+ status === undefined &&
71
+ dirty === undefined ? null : (
72
+ /* Row 2 — WHAT I CAN DO and WHERE IT IS AT: actions stuck to the
73
+ left edge, the history and the state stuck to the right one. No
74
+ `flex-wrap`: wrapping split the row in two and the state ended up
75
+ below the actions, which is another hierarchy. */
76
+ _jsxs("div", { className: "flex min-w-0 items-start gap-x-4", children: [dirty !== undefined ? (_jsx("div", { className: "flex shrink-0 items-center gap-1.5 @max-handheld/record:hidden", children: _jsx(DirtyActions, { ...dirty }) })) : actions === undefined ? null : (_jsx("div", { className: "flex shrink-0 items-center gap-1.5", children: primaries.length === 0
77
+ ? actions
78
+ : railActions.map((node) => isPrimary(node) ? (_jsx("span", { "data-thumb-moves": "", className: "contents @max-handheld/record:hidden", children: node }, node.key)) : (node)) })), status === undefined ? null : (_jsx("div", { className: "ml-auto flex min-w-0 flex-col items-end gap-1.5", children: status }))] }))] }) }) }) })) : null, paper ? (
79
+ /* ==================================================================== *
80
+ * THE PAGE — [sheet | Actividad], on the ground.
81
+ *
82
+ * Two columns, joined, placed together and centred: the sheet
83
+ * (≤ --container-sheet, 72rem) and, at its RIGHT, the record's ACTIVITY
84
+ * (--container-activity, 22rem) — no strip of ground between them, so the page
85
+ * reads left to right as one paper: the record, then what it has. The pair is at
86
+ * most --container-record (94rem) wide; on a 1680 px desk both fit whole.
87
+ *
88
+ * A note of a field or a row is not drawn in the column: its mark (`Flag`) stays
89
+ * by its label or at its row — WHERE — and the activity lists it with the record's
90
+ * notices above the timeline — WHAT —, taking you to it on press. Cards aligned in
91
+ * the column were tried and dropped: they and the history fought for one column,
92
+ * and the history is what everybody reads.
93
+ *
94
+ * As the page narrows: below --container-beside (76rem) the activity goes BELOW
95
+ * the sheet as a card at the sheet's width.
96
+ *
97
+ * Folded, the column is a slim strip with its counts; the pair re-centres and the
98
+ * sheet keeps its width. A container query, not the window: with four panels open
99
+ * each is 380 px on a 1,600 screen.
100
+ *
101
+ * The sheet clips with `overflow-clip`, not `overflow-hidden`: hidden would make it
102
+ * a scroll container and the line table header would stick inside it, not under
103
+ * the application bar.
104
+ * ==================================================================== */
105
+ _jsx("div", { className: "bg-ground px-3 pt-3 pb-3", children: _jsx("div", { className: "@container/page", children: _jsxs("div", { className: `flex min-w-0 flex-col gap-3 ${column} ${beside
106
+ ? "@beside/page:flex-row @beside/page:items-stretch @beside/page:justify-center @beside/page:gap-0"
107
+ : ""}`, children: [_jsxs("div", { className: `flex min-w-0 flex-1 flex-col gap-3 ${beside && width === "reading"
108
+ ? "mx-auto w-full max-w-(--container-sheet) @beside/page:mx-0"
109
+ : ""}`, children: [blocking, _jsxs("div", { className: `relative flex min-w-0 flex-col overflow-clip rounded-box border border-line bg-surface ${beside ? "@beside/page:rounded-r-none" : ""}`, children: [ribbon, identity, children] }), activity !== undefined && activityLayout === "below" ? activity : null] }), beside ? (
110
+ /* After the sheet: the record first, what is around it second — in the
111
+ reading order and on screen. */
112
+ _jsx("div", { className: "mx-auto w-full max-w-(--container-sheet) min-w-0 @beside/page:mx-0 @beside/page:flex @beside/page:w-auto @beside/page:max-w-none @beside/page:shrink-0", children: activity })) : null] }) }) })) : (
113
+ /* No paper (a modal, a drawer): one surface, no ground beside
114
+ it — the band first, the record, and the activity below. */
115
+ _jsxs("div", { className: "relative flex min-w-0 flex-1 flex-col overflow-clip bg-surface", children: [ribbon, blocking === undefined ? null : (_jsx("div", { className: byDensity(d, "px-4 pt-3.5", "pb-2"), children: blocking })), identity, children, activity === undefined ? null : (_jsx("div", { className: byDensity(d, "px-4 pb-4", "pb-2"), children: activity }))] })), thumb === null ? null : _jsx(ThumbBar, { mode: thumb, dirty: dirty, primaries: primaries })] }));
72
116
  }
@@ -0,0 +1,37 @@
1
+ import { type CalloutProps } from "../../primitives/notice/Callout.js";
2
+ import { type ReactNode } from "react";
3
+ export type RecordNoticeSpec = Omit<CalloutProps, "dismissLabel">;
4
+ /** The notices in the order they are drawn: by urgency, written order inside a tone. */
5
+ export declare function orderNotices<T extends {
6
+ tone?: CalloutProps["tone"];
7
+ }>(notices: readonly T[]): T[];
8
+ /** Blocking (`bad`) and the rest, each already in urgency order. */
9
+ export declare function splitNotices(notices: readonly RecordNoticeSpec[]): {
10
+ blocking: RecordNoticeSpec[];
11
+ rest: RecordNoticeSpec[];
12
+ };
13
+ /** The band of what is wrong: the operation error first, then the `bad` notices. */
14
+ export declare function RecordBlocking({ notices, error, errorHeading, }: {
15
+ notices: readonly RecordNoticeSpec[];
16
+ /** The sentence the backend wrote about the last operation. Drawn first, whole. */
17
+ error?: string | null;
18
+ errorHeading?: string;
19
+ }): ReactNode;
20
+ /** The indicator in the rail: the count of the record's notices, toned by the most urgent,
21
+ * that opens their list on press. Nothing is drawn without notices. */
22
+ export declare function RecordNoticeIndicator({ notices, anchored, reveal, }: {
23
+ /** Not blocking, already in urgency order. */
24
+ notices: readonly RecordNoticeSpec[];
25
+ /** The notes of fields and rows: counted too when there is a panel that lists them —
26
+ * without one they are only the marks by their fields, and the popover leaves them out. */
27
+ anchored?: readonly {
28
+ tone?: "bad" | "warn" | "info" | "good";
29
+ }[];
30
+ /** The record has an ACTIVITY panel, and the notices live there: the indicator goes to
31
+ * them (opens the panel, scrolls to its notices, moves the focus) instead of floating
32
+ * a second copy in a popover. `target` is the id of that section. */
33
+ reveal?: {
34
+ target: string;
35
+ onReveal: () => void;
36
+ };
37
+ }): ReactNode;
@@ -0,0 +1,75 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { fillCopy, useUiCopy } from "../../foundations/copy.js";
4
+ import { useSurface } from "../../foundations/surface.js";
5
+ import { focusRing, tv } from "../../foundations/variants.js";
6
+ import { Callout } from "../../primitives/notice/Callout.js";
7
+ import { Failed } from "../../primitives/notice/Notice.js";
8
+ import { Popover } from "../../primitives/overlay/Popover.js";
9
+ import { tagTone } from "../../primitives/tag/Tag.js";
10
+ import { CircleCheck, Info, TriangleAlert } from "lucide-react";
11
+ import { useState } from "react";
12
+ import { Button, Dialog, DialogTrigger, Heading } from "react-aria-components";
13
+ const URGENCY = {
14
+ bad: 0,
15
+ warn: 1,
16
+ info: 2,
17
+ good: 3,
18
+ };
19
+ /** The notices in the order they are drawn: by urgency, written order inside a tone. */
20
+ export function orderNotices(notices) {
21
+ return notices
22
+ .map((notice, at) => ({ notice, at }))
23
+ .sort((a, b) => URGENCY[a.notice.tone ?? "info"] - URGENCY[b.notice.tone ?? "info"] || a.at - b.at)
24
+ .map(({ notice }) => notice);
25
+ }
26
+ /** Blocking (`bad`) and the rest, each already in urgency order. */
27
+ export function splitNotices(notices) {
28
+ const ordered = orderNotices(notices);
29
+ return {
30
+ blocking: ordered.filter((n) => n.tone === "bad"),
31
+ rest: ordered.filter((n) => n.tone !== "bad"),
32
+ };
33
+ }
34
+ /** The band of what is wrong: the operation error first, then the `bad` notices. */
35
+ export function RecordBlocking({ notices, error, errorHeading, }) {
36
+ const copy = useUiCopy();
37
+ const hasError = error !== undefined && error !== null;
38
+ if (notices.length === 0 && !hasError)
39
+ return null;
40
+ return (_jsxs("div", { role: "group", "aria-label": copy.record.blocking, "data-record-blocking": "", className: "flex min-w-0 flex-col gap-1.5", children: [hasError ? (_jsx(Failed, { heading: errorHeading ?? copy.failed.operation, sentence: error })) : null, notices.map(({ children, ...notice }, i) => (_jsx(Callout, { ...notice, dismissLabel: copy.shell.closeNotice, children: children }, i)))] }));
41
+ }
42
+ const ICON = {
43
+ warn: TriangleAlert,
44
+ info: Info,
45
+ good: CircleCheck,
46
+ };
47
+ const chip = tv({
48
+ extend: focusRing,
49
+ base: "inline-flex h-(--size-control-sm) shrink-0 cursor-default items-center gap-1.5 rounded-pill px-2.5 text-micro font-medium whitespace-nowrap transition-[filter] duration-(--duration-instant) hover:brightness-95 aria-expanded:brightness-95",
50
+ });
51
+ /** The indicator in the rail: the count of the record's notices, toned by the most urgent,
52
+ * that opens their list on press. Nothing is drawn without notices. */
53
+ export function RecordNoticeIndicator({ notices, anchored = [], reveal, }) {
54
+ const copy = useUiCopy();
55
+ const surface = useSurface();
56
+ const [open, setOpen] = useState(false);
57
+ const all = reveal === undefined ? notices : [...notices, ...anchored];
58
+ if (all.length === 0)
59
+ return null;
60
+ /* The most urgent of all of them gives the chip its tone; a field's `bad` note warns —
61
+ what blocks the record is the blocking band, not this chip. */
62
+ const order = ["bad", "warn", "info", "good"];
63
+ const worst = order.find((t) => all.some((n) => (n.tone ?? "info") === t)) ?? "info";
64
+ const tone = (worst === "bad" ? "warn" : worst);
65
+ const Icon = ICON[tone];
66
+ const count = fillCopy(all.length === 1 ? copy.record.noticeOne : copy.record.noticeMany, {
67
+ count: all.length.toLocaleString(copy.locale),
68
+ });
69
+ const face = (_jsxs(_Fragment, { children: [_jsx(Icon, { className: "size-(--size-icon-sm) shrink-0", strokeWidth: 2.25, "aria-hidden": true }), count] }));
70
+ const skin = tagTone(tone, surface === "chrome" ? "chrome" : "canvas");
71
+ if (reveal !== undefined) {
72
+ return (_jsx(Button, { "data-record-notices": "", "aria-controls": reveal.target, onPress: reveal.onReveal, className: ({ isFocusVisible }) => `${chip({ isFocusVisible })} ${skin}`, children: face }));
73
+ }
74
+ return (_jsxs(DialogTrigger, { isOpen: open, onOpenChange: setOpen, children: [_jsx(Button, { "data-record-notices": "", className: ({ isFocusVisible }) => `${chip({ isFocusVisible })} ${skin}`, children: face }), _jsx(Popover, { placement: "bottom", children: _jsxs(Dialog, { "aria-label": copy.record.notices, className: "flex max-h-[inherit] w-(--container-toast) flex-col gap-2 overflow-auto p-3 outline-0 max-sm:w-auto", children: [_jsx(Heading, { slot: "title", className: "text-meta font-semibold text-ink", children: copy.record.notices }), notices.map(({ children, ...notice }, i) => (_jsx(Callout, { ...notice, dismissLabel: copy.shell.closeNotice, children: children }, i)))] }) })] }));
75
+ }
@@ -0,0 +1,13 @@
1
+ import type { Tone } from "../../foundations/types.js";
2
+ /**
3
+ * The band across the paper's corner: «PAGADA», «ARCHIVADA», «CANCELADA».
4
+ *
5
+ * Context: a state that changes what the record IS — not where it is in its flow, which the
6
+ * status bar says — has to be seen before any field is read: an archived customer looks
7
+ * exactly like an active one otherwise. Odoo's `web_ribbon`. It is a word, never colour
8
+ * alone, and it is `role="status"` so it is also heard. The paper clips it.
9
+ */
10
+ export declare function RecordRibbon({ label, tone }: {
11
+ label: string;
12
+ tone?: Tone;
13
+ }): import("react").JSX.Element;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /* The solid ribbon per tone. The text is the surface colour: on the light theme white on the
3
+ tone (≥ 5.7:1 for all five), on the dark one the dark surface on the lighter tone. */
4
+ const RIBBON = {
5
+ neutral: "bg-muted text-surface",
6
+ info: "bg-info text-surface",
7
+ good: "bg-good text-surface",
8
+ warn: "bg-warn text-surface",
9
+ bad: "bg-bad text-surface",
10
+ accent: "bg-accent text-on-accent",
11
+ };
12
+ /**
13
+ * The band across the paper's corner: «PAGADA», «ARCHIVADA», «CANCELADA».
14
+ *
15
+ * Context: a state that changes what the record IS — not where it is in its flow, which the
16
+ * status bar says — has to be seen before any field is read: an archived customer looks
17
+ * exactly like an active one otherwise. Odoo's `web_ribbon`. It is a word, never colour
18
+ * alone, and it is `role="status"` so it is also heard. The paper clips it.
19
+ */
20
+ export function RecordRibbon({ label, tone = "neutral" }) {
21
+ return (_jsx("span", { role: "status", className: `pointer-events-none absolute top-5 -right-12 z-10 w-44 rotate-45 py-1 text-center text-micro font-semibold tracking-wide uppercase shadow-float ${RIBBON[tone]}`, children: label }));
22
+ }
@@ -38,7 +38,7 @@ import { cloneElement, isValidElement } from "react";
38
38
  * Same reason as everything else here: a record in a 380 px panel is as
39
39
  * narrow as one on a phone. The query is a NAMED container (`record`) on the
40
40
  * rail and on the bar — never on the paper: an unnamed container around the
41
- * sheet would change what `Sheet`, `SectionGrid` and the chatter measure
41
+ * sheet would change what `Sheet`, `SectionGrid` and the activity panel measure
42
42
  * against, and the desk layout with it.
43
43
  *
44
44
  * ── Why CSS, and why the primary is in the DOM twice ─────────────────
@@ -1,6 +1,8 @@
1
1
  export { Breadcrumb, type Crumb } from "./Breadcrumb.js";
2
2
  export { DirtyActions, type DirtyState, } from "./DirtyActions.js";
3
3
  export { Pager } from "./Pager.js";
4
+ export { RecordAudit, type RecordStamp } from "./RecordAudit.js";
5
+ export { RecordRibbon } from "./RecordRibbon.js";
4
6
  export { RecordSkeleton } from "./RecordSkeleton.js";
5
7
  export { SmartButton, SmartButtons } from "./SmartButtons.js";
6
8
  export { type Stage, StatusBar } from "./StatusBar.js";
@@ -3,6 +3,8 @@
3
3
  export { Breadcrumb } from "./Breadcrumb.js";
4
4
  export { DirtyActions, } from "./DirtyActions.js";
5
5
  export { Pager } from "./Pager.js";
6
+ export { RecordAudit } from "./RecordAudit.js";
7
+ export { RecordRibbon } from "./RecordRibbon.js";
6
8
  export { RecordSkeleton } from "./RecordSkeleton.js";
7
9
  export { SmartButton, SmartButtons } from "./SmartButtons.js";
8
10
  export { StatusBar } from "./StatusBar.js";
@@ -9,6 +9,8 @@ export type CriteriaChipsProps = {
9
9
  onJoin?: (path: CriteriaPath, join: CriteriaJoin) => void;
10
10
  /** Sacar una instancia, o una rama entera. */
11
11
  onRemove?: (path: CriteriaPath) => void;
12
+ /** Sacar varias de una vez: the × of the grouping chain drops every level. */
13
+ onRemoveMany?: (paths: readonly CriteriaPath[]) => void;
12
14
  /**
13
15
  * Cuál nodo está ABIERTO: dónde va a caer lo próximo que se escriba.
14
16
  *
@@ -17,6 +19,9 @@ export type CriteriaChipsProps = {
17
19
  * único que sabía dónde era el que la escribió.
18
20
  */
19
21
  open?: CriteriaPath;
22
+ /** How many facets show before the rest fold into «+N» (the grouping chain counts as one).
23
+ * Below `@md` of the bar only the first one shows. `Infinity` never folds. */
24
+ collapseAfter?: number;
20
25
  surface?: Surface;
21
26
  };
22
- export declare function CriteriaChips({ criteria, onOpen, onJoin, onRemove, open, surface, }: CriteriaChipsProps): import("react").JSX.Element;
27
+ export declare function CriteriaChips({ criteria, onOpen, onJoin, onRemove, onRemoveMany, open, collapseAfter, surface, }: CriteriaChipsProps): import("react").JSX.Element;
@@ -1,13 +1,15 @@
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 { filtersOf, groupsOf, } from "./criteria.js";
4
4
  import { JoinWord } from "./JoinWord.js";
5
- import { useUiCopy } from "../../foundations/copy.js";
5
+ import { fillCopy, useUiCopy } from "../../foundations/copy.js";
6
6
  import { bySurface, useSurface } from "../../foundations/surface.js";
7
- import { ChevronRight, Layers, ListFilter, X } from "lucide-react";
8
- export function CriteriaChips({ criteria, onOpen, onJoin, onRemove, open, surface, }) {
7
+ import { ChevronRight, Layers, X } from "lucide-react";
8
+ import { Fragment, useState } from "react";
9
+ export function CriteriaChips({ criteria, onOpen, onJoin, onRemove, onRemoveMany, open, collapseAfter = 4, surface, }) {
9
10
  const sup = useSurface(surface);
10
11
  const copy = useUiCopy();
12
+ const [expanded, setExpanded] = useState(false);
11
13
  const groups = groupsOf(criteria);
12
14
  const filters = filtersOf(criteria);
13
15
  /* La ruta se cuenta sobre la lista ENTERA y no sobre la mitad dibujada: es la
@@ -16,7 +18,65 @@ export function CriteriaChips({ criteria, onOpen, onJoin, onRemove, open, surfac
16
18
  const pathOf = (one) => [
17
19
  criteria.findIndex((each) => each.id === one.id),
18
20
  ];
19
- return (_jsxs(_Fragment, { children: [groups.length === 0 ? null : (_jsx(Zone, { lead: copy.search.groupedBy, icon: Layers, surface: sup, children: groups.map((one, at) => (_jsxs("span", { className: "flex min-w-0 items-center gap-1", children: [at === 0 ? null : (_jsx(ChevronRight, { className: "size-3 shrink-0 text-muted", strokeWidth: 2, "aria-hidden": true })), _jsx(Chip, { label: one.label, path: pathOf(one), ...(onOpen === undefined ? {} : { onOpen }), ...(onRemove === undefined ? {} : { onRemove }), surface: sup })] }, one.id))) })), filters.length === 0 ? null : (_jsx(Zone, { lead: copy.search.narrowedBy, icon: ListFilter, surface: sup, children: filters.map((one) => (_jsx(Node, { node: one, path: pathOf(one), ...(onOpen === undefined ? {} : { onOpen }), ...(onJoin === undefined ? {} : { onJoin }), ...(onRemove === undefined ? {} : { onRemove }), ...(open === undefined ? {} : { open }), surface: sup }, one.id))) }))] }));
21
+ const facets = [
22
+ ...(groups.length === 0
23
+ ? []
24
+ : [
25
+ {
26
+ key: "grouping",
27
+ label: `${copy.search.groupedBy} ${groups.map((one) => one.label).join(" › ")}`,
28
+ node: (_jsx(GroupChain, { groups: groups.map((one) => ({ label: one.label, path: pathOf(one) })), ...(onOpen === undefined ? {} : { onOpen }), ...(onRemoveMany === undefined ? {} : { onRemoveMany }), surface: sup })),
29
+ },
30
+ ]),
31
+ ...filters.map((one) => ({
32
+ key: one.id,
33
+ label: labelOf(one, copy.search.joins),
34
+ node: (_jsx(Node, { node: one, path: pathOf(one), ...(onOpen === undefined ? {} : { onOpen }), ...(onJoin === undefined ? {} : { onJoin }), ...(onRemove === undefined ? {} : { onRemove }), ...(open === undefined ? {} : { open }), surface: sup })),
35
+ })),
36
+ ];
37
+ /* Folding hides, it does not remove: the open node is where the next one lands, so while
38
+ there is one everything shows. */
39
+ const unfolded = expanded || open !== undefined || !Number.isFinite(collapseAfter);
40
+ const beyondWide = Math.max(0, facets.length - collapseAfter);
41
+ const beyondNarrow = Math.max(0, facets.length - 1);
42
+ const more = (count, hidden, className) => (_jsx("button", { type: "button", onClick: () => setExpanded(true), title: hidden, "aria-label": `${fillCopy(copy.search.moreFacets, { count })}: ${hidden}`, className: `${className} shrink-0 items-center rounded-pill border px-2 py-0.5 text-micro font-medium tabular-nums transition-colors duration-(--duration-instant) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary ${bySurface(sup, "border-line-strong text-muted hover:bg-ink/5 hover:text-ink", "border-chrome-ink/30 text-chrome-muted hover:bg-chrome-ink/10 hover:text-chrome-ink")}`, children: fillCopy(copy.search.moreFacets, { count }) }));
43
+ const hiddenFrom = (from) => facets
44
+ .slice(from)
45
+ .map((one) => one.label)
46
+ .join(" · ");
47
+ const drawn = (facet, index) => (_jsx("span", { className: `min-w-0 max-w-full ${unfolded
48
+ ? "inline-flex"
49
+ : index >= collapseAfter
50
+ ? "hidden"
51
+ : index >= 1
52
+ ? "inline-flex @max-md/bar:hidden"
53
+ : "inline-flex"}`, children: facet.node }, facet.key));
54
+ const offset = groups.length === 0 ? 0 : 1;
55
+ return (_jsxs(_Fragment, { children: [offset === 0 ? null : drawn(facets[0], 0), filters.length === 0 ? null : (_jsx("span", { role: "group", "aria-label": copy.search.narrowedBy, className: "contents", children: facets.slice(offset).map((facet, at) => drawn(facet, at + offset)) })), unfolded ? (expanded && facets.length > 1 ? (_jsx("button", { type: "button", onClick: () => setExpanded(false), className: `shrink-0 rounded-control px-1 text-micro underline decoration-dotted underline-offset-4 transition-colors focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary ${bySurface(sup, "text-muted hover:text-ink", "text-chrome-muted hover:text-chrome-ink")}`, children: copy.search.fewerFacets })) : null) : (_jsxs(_Fragment, { children: [beyondWide === 0
56
+ ? null
57
+ : more(beyondWide, hiddenFrom(collapseAfter), "inline-flex @max-md/bar:hidden"), beyondNarrow === 0
58
+ ? null
59
+ : more(beyondNarrow, hiddenFrom(1), "hidden @max-md/bar:inline-flex")] }))] }));
60
+ }
61
+ /** How a facet reads when it is folded away: in the «+N» title and for a screen reader. */
62
+ function labelOf(node, joins) {
63
+ if (node.kind !== "join")
64
+ return node.label;
65
+ const word = joins[node.join === "not" ? "none" : node.join];
66
+ return `${word} (${node.of.map((child) => labelOf(child, joins)).join(", ")})`;
67
+ }
68
+ /**
69
+ * THE GROUPING, as ONE chip: «⧉ vendedor › trimestre › mes ×».
70
+ *
71
+ * Context: N chips with N crosses read as N filters, and the levels are not N things — they
72
+ * are one shape, nested in order. Neutral and not gold: the grouping changes how the rows
73
+ * are laid out, not which rows there are, and it must not read as one more filter. Each level
74
+ * still opens the builder on itself; the × drops the whole chain.
75
+ */
76
+ function GroupChain({ groups, onOpen, onRemoveMany, surface, }) {
77
+ const copy = useUiCopy();
78
+ const chain = groups.map((one) => one.label).join(" › ");
79
+ return (_jsxs("span", { role: "group", "aria-label": `${copy.search.groupedBy} ${chain}`, title: `${copy.search.groupedBy} ${chain}`, className: `inline-flex min-w-0 max-w-(--container-chain) items-center gap-1 rounded-pill border py-0.5 pl-1.5 text-micro ${bySurface(surface, "border-line-strong bg-surface text-ink", "border-chrome-ink/30 bg-chrome-ink/8 text-chrome-ink")} ${onRemoveMany === undefined ? "pr-2" : "pr-0.5"}`, children: [_jsx(Layers, { className: `size-3 shrink-0 ${bySurface(surface, "text-muted", "text-chrome-muted")}`, strokeWidth: 2, "aria-hidden": true }), _jsx("span", { className: "min-w-0 truncate", children: groups.map((one, at) => (_jsxs(Fragment, { children: [at === 0 ? null : (_jsx(ChevronRight, { className: `mx-0.5 inline size-3 align-middle ${bySurface(surface, "text-muted", "text-chrome-muted")}`, strokeWidth: 2, "aria-hidden": true })), onOpen === undefined ? (_jsx("span", { children: one.label })) : (_jsx("button", { type: "button", onClick: () => onOpen(one.path), title: copy.search.openBuilderHere, className: "inline rounded-control hover:underline hover:underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary", children: one.label }))] }, one.path.join(".")))) }), onRemoveMany === undefined ? null : (_jsx("button", { type: "button", onClick: () => onRemoveMany(groups.map((one) => one.path)), "aria-label": copy.search.removeGrouping, title: copy.search.removeGrouping, className: `inline-flex size-4 shrink-0 items-center justify-center rounded-pill transition-colors duration-(--duration-instant) ${bySurface(surface, "hover:bg-ink/10", "hover:bg-chrome-ink/20")}`, children: _jsx(X, { className: "size-3", strokeWidth: 2.5 }) }))] }));
20
80
  }
21
81
  function Node({ node, path, onOpen, onJoin, onRemove, open, surface, }) {
22
82
  const copy = useUiCopy();
@@ -35,14 +95,9 @@ function Node({ node, path, onOpen, onJoin, onRemove, open, surface, }) {
35
95
  lee como un error. Dice que está esperando. */
36
96
  _jsx("span", { className: `shrink-0 text-micro italic ${bySurface(surface, "text-muted", "text-chrome-muted")}`, children: copy.search.landsHere })) : null, node.of.map((child, at) => (_jsx(Node, { node: child, path: [...path, at], ...(onOpen === undefined ? {} : { onOpen }), ...(onJoin === undefined ? {} : { onJoin }), ...(onRemove === undefined ? {} : { onRemove }), ...(open === undefined ? {} : { open }), surface: surface }, child.id)))] }));
37
97
  }
38
- /** El contexto, con su palabra adelante. Sin la palabra, las fichas no dicen de
39
- * qué son. No lleva caja: una caja adentro del campo las separa de él. */
40
- function Zone({ lead, icon: Icon, surface, children, }) {
41
- return (_jsxs("span", { className: "inline-flex min-w-0 shrink items-center gap-1", children: [_jsx(Icon, { className: `size-3 shrink-0 ${bySurface(surface, "text-muted", "text-chrome-muted")}`, strokeWidth: 2, "aria-hidden": true }), _jsx("span", { className: `shrink-0 text-micro ${bySurface(surface, "text-muted", "text-chrome-muted")}`, children: lead }), _jsx("span", { className: "flex min-w-0 flex-wrap items-center gap-1", children: children })] }));
42
- }
43
98
  function Chip({ label, path, onOpen, onRemove, surface, }) {
44
99
  const copy = useUiCopy();
45
- const shell = `inline-flex min-w-0 shrink-0 items-center gap-1 rounded-pill border py-0.5 text-micro ${bySurface(surface, "border-accent/40 bg-accent/18 text-ink", "border-chrome-link/35 bg-chrome-link/18 text-chrome-link")} ${onRemove === undefined ? "px-2" : "pr-0.5 pl-2"}`;
100
+ const shell = `inline-flex min-w-0 max-w-(--container-facet) items-center gap-1 rounded-pill border py-0.5 text-micro ${bySurface(surface, "border-accent/40 bg-accent/18 text-ink", "border-chrome-link/35 bg-chrome-link/18 text-chrome-link")} ${onRemove === undefined ? "px-2" : "pr-0.5 pl-2"}`;
46
101
  const remove = onRemove === undefined ? null : (_jsx("button", { type: "button", onClick: (event) => {
47
102
  /* Sin esto, sacarla también la abre: el click llega acá y después
48
103
  sube. */
@@ -52,13 +107,9 @@ function Chip({ label, path, onOpen, onRemove, surface, }) {
52
107
  if (onOpen === undefined) {
53
108
  return (_jsxs("span", { className: shell, children: [_jsx("span", { className: "min-w-0 truncate", children: label }), remove] }));
54
109
  }
55
- /* Un botón adentro de otro es HTML inválido —el navegador cierra el de afuera
56
- antes de tiempo y el click deja de llegar—, así que la ficha que se abre Y se
57
- saca es un `span` con `role`, y la × adentro es el único botón de verdad. */
58
- return (_jsxs("span", { role: "button", tabIndex: 0, onClick: () => onOpen(path), onKeyDown: (event) => {
59
- if (event.key === "Enter" || event.key === " ") {
60
- event.preventDefault();
61
- onOpen(path);
62
- }
63
- }, title: copy.search.openBuilderHere, className: `${shell} cursor-pointer`, children: [_jsx("span", { className: "min-w-0 truncate", children: label }), remove] }));
110
+ /* Two SIBLING buttons in the chip — open and remove — and never one inside the other. It
111
+ used to be a `span role="button"` holding the × button: an interactive control inside
112
+ another one, which a screen reader announces as one thing and axe flags
113
+ (`nested-interactive`). Siblings, each with its own name, are both reachable. */
114
+ return (_jsxs("span", { className: shell, children: [_jsx("button", { type: "button", onClick: () => onOpen(path), title: copy.search.openBuilderHere, className: "min-w-0 truncate rounded-control text-left hover:underline hover:underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary", children: label }), remove] }));
64
115
  }
@@ -72,7 +72,10 @@ export type SearchBarProps = {
72
72
  onOpenBuilder?: (path?: CriteriaPath) => void;
73
73
  /** Lo que va pegado al campo, a su derecha. */
74
74
  trailing?: ReactNode;
75
+ /** How many facets the bar shows before folding the rest into «+N»; the grouping chain
76
+ * counts as one. On a narrow bar only the first shows. `Infinity` never folds. */
77
+ collapseAfter?: number;
75
78
  /** Por default la hereda de la superficie que la envuelve. */
76
79
  surface?: Surface;
77
80
  };
78
- export declare function SearchBar({ facets, model, criteria, onCriteriaChange, placeholder, filters, grouping, onOpenGroup, favorites, onFilterSelect, onFavoriteSelect, onOpenBuilder, trailing, surface, }: SearchBarProps): import("react").JSX.Element;
81
+ export declare function SearchBar({ facets, model, criteria, onCriteriaChange, placeholder, filters, grouping, onOpenGroup, favorites, onFilterSelect, onFavoriteSelect, onOpenBuilder, trailing, collapseAfter, surface, }: SearchBarProps): import("react").JSX.Element;
@@ -6,7 +6,7 @@ import { describeCondition, opLabel, queryFields, readTyped, suggest, withoutVal
6
6
  import { useUiCopy } from "../../foundations/copy.js";
7
7
  import { bySurface, useSurface } from "../../foundations/surface.js";
8
8
  import { Tags } from "../../primitives/tag/Tags.js";
9
- import { ChevronRight, CornerDownRight, ListFilter, Search, SlidersHorizontal, } from "lucide-react";
9
+ import { ChevronRight, CornerDownRight, ListFilter, Search, SlidersHorizontal, X, } from "lucide-react";
10
10
  import { Fragment, useEffect, useId, useRef, useState } from "react";
11
11
  function Option({ o, onSelect }) {
12
12
  const Icon = o.icon;
@@ -28,7 +28,7 @@ function Group({ group, first, onSelect, }) {
28
28
  ? "flex flex-col gap-0.5 pl-(--spacing-indent)"
29
29
  : "flex flex-col gap-0.5", children: options }))] }));
30
30
  }
31
- export function SearchBar({ facets, model, criteria, onCriteriaChange, placeholder, filters, grouping, onOpenGroup, favorites, onFilterSelect, onFavoriteSelect, onOpenBuilder, trailing, surface, }) {
31
+ export function SearchBar({ facets, model, criteria, onCriteriaChange, placeholder, filters, grouping, onOpenGroup, favorites, onFilterSelect, onFavoriteSelect, onOpenBuilder, trailing, collapseAfter, surface, }) {
32
32
  const copy = useUiCopy();
33
33
  const sup = useSurface(surface);
34
34
  const [text, setText] = useState("");
@@ -207,14 +207,18 @@ export function SearchBar({ facets, model, criteria, onCriteriaChange, placehold
207
207
  ? one
208
208
  : { ...node, of: [...node.of, one] };
209
209
  }
210
- return (_jsxs("div", { ref: box, className: "relative flex min-w-0 items-center gap-2", children: [_jsxs("div", { className: `flex min-h-(--size-control-md) w-full min-w-0 items-stretch rounded-box border transition-colors duration-(--duration-instant) ${bySurface(sup, "border-line-strong bg-surface focus-within:border-primary/60",
210
+ return (_jsxs("div", { ref: box, className: "relative flex min-w-0 items-center gap-2", children: [_jsxs("div", { className: `@container/bar flex min-h-(--size-control-md) w-full min-w-0 items-start rounded-box border transition-colors duration-(--duration-instant) ${bySurface(sup, "border-line-strong bg-surface focus-within:border-primary/60",
211
211
  /* Sobre el cromo lo que define al campo es el BORDE, no el relleno. El
212
212
  azul ya es casi negro, así que hundirlo no da contraste (1,05:1) y
213
213
  levantarlo da 1,21:1 — en los dos casos el campo no se lee como un
214
214
  campo, que era «no llama la atención». La tinta al 45 % es el primer
215
215
  borde que pasa 3:1 contra el relleno Y contra el cromo, que es lo que
216
216
  1.4.11 le pide a un control. */
217
- "border-chrome-ink/45 bg-chrome-ink/8 focus-within:border-chrome-link")}`, children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-wrap items-center gap-1.5 py-1 pl-2.5", children: [_jsx(Search, { className: "size-(--size-icon-md) shrink-0 text-muted", strokeWidth: 2, "aria-hidden": true }), _jsx(CriteriaChips, { criteria: criteria, surface: sup, ...(openGroup === null ? {} : { open: openGroup }), ...(onOpenBuilder === undefined ? {} : { onOpen: onOpenBuilder }), onJoin: (path, join) => {
217
+ "border-chrome-ink/45 bg-chrome-ink/8 focus-within:border-chrome-link")}`, children: [_jsx("span", { className: "mt-0.75 flex h-7 shrink-0 items-center pr-1.5 pl-2.5", children: _jsx(Search, { className: "size-(--size-icon-md) shrink-0 text-muted", strokeWidth: 2, "aria-hidden": true }) }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-wrap items-center gap-1 py-0.75", children: [_jsx(CriteriaChips, { criteria: criteria, surface: sup, ...(openGroup === null ? {} : { open: openGroup }), ...(onOpenBuilder === undefined ? {} : { onOpen: onOpenBuilder }), ...(collapseAfter === undefined ? {} : { collapseAfter }), onRemoveMany: (paths) => {
218
+ /* From the deepest index up, so each removal leaves the next path valid. */
219
+ const sorted = [...paths].sort((a, b) => (b[0] ?? 0) - (a[0] ?? 0));
220
+ onCriteriaChange(sorted.reduce((left, path) => removeAt(left, path), [...criteria]));
221
+ }, onJoin: (path, join) => {
218
222
  const node = at(criteria, path);
219
223
  if (node?.kind !== "join")
220
224
  return;
@@ -276,7 +280,7 @@ export function SearchBar({ facets, model, criteria, onCriteriaChange, placehold
276
280
  removeLabel: copy.search.dropOp,
277
281
  },
278
282
  ]),
279
- ] }), _jsx("input", { ref: input, value: text, onChange: (e) => setText(e.target.value), onKeyDown: (e) => {
283
+ ] }), _jsx("input", { ref: input, "aria-label": copy.choice.search, value: text, onChange: (e) => setText(e.target.value), onKeyDown: (e) => {
280
284
  /* Retroceso con el campo vacío saca la ÚLTIMA instancia: el gesto
281
285
  que ya existe en cualquier campo de fichas y el único que se
282
286
  descubre sin que nadie lo explique. La última y no la última de
@@ -362,8 +366,12 @@ export function SearchBar({ facets, model, criteria, onCriteriaChange, placehold
362
366
  crezca con lo escrito en vez de reservar una banda vacía. Donde no
363
367
  esté, el input se estira como siempre. */
364
368
  style: step.field === undefined ? undefined : { fieldSizing: "content" }, className: step.field === undefined
365
- ? `min-w-24 flex-1 bg-transparent text-meta outline-none ${bySurface(sup, "text-ink placeholder:text-muted", "text-chrome-ink placeholder:text-chrome-muted")}`
366
- : `min-w-16 rounded-pill px-2 py-0.5 text-micro outline-none ${bySurface(sup, "bg-accent/18 text-ink placeholder:text-muted", "bg-chrome-link/20 text-chrome-ink placeholder:text-chrome-muted")}` })] }), columns.length === 0 && onOpenBuilder === undefined ? null : (_jsxs("button", { type: "button", onClick: () => setPanel((v) => !v), "aria-expanded": panel, "aria-controls": panel ? id : undefined, title: copy.search.tools, className: `my-0.5 mr-0.5 inline-flex h-(--size-control-sm) shrink-0 items-center gap-1 self-center rounded-control border px-2 text-micro transition-colors duration-(--duration-instant) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary ${puestos > 0 || panel
369
+ ? `h-7 ${criteria.length === 0 ? "min-w-24" : "min-w-16"} flex-1 bg-transparent text-meta outline-none ${bySurface(sup, "text-ink placeholder:text-muted", "text-chrome-ink placeholder:text-chrome-muted")}`
370
+ : `min-w-16 rounded-pill px-2 py-0.5 text-micro outline-none ${bySurface(sup, "bg-accent/18 text-ink placeholder:text-muted", "bg-chrome-link/20 text-chrome-ink placeholder:text-chrome-muted")}` })] }), puestos === 0 ? null : (_jsx("button", { type: "button", onClick: () => {
371
+ onCriteriaChange([]);
372
+ setOpenGroup(null);
373
+ input.current?.focus();
374
+ }, "aria-label": copy.search.clearAll, title: copy.search.clearAll, className: `my-0.5 inline-flex h-(--size-control-sm) shrink-0 items-center rounded-control px-1.5 transition-colors duration-(--duration-instant) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary ${bySurface(sup, "text-muted hover:bg-ink/5 hover:text-ink", "text-chrome-muted hover:bg-chrome-ink/10 hover:text-chrome-ink")}`, children: _jsx(X, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true }) })), columns.length === 0 && onOpenBuilder === undefined ? null : (_jsxs("button", { type: "button", onClick: () => setPanel((v) => !v), "aria-expanded": panel, "aria-controls": panel ? id : undefined, title: copy.search.tools, className: `my-0.5 mr-0.5 inline-flex h-(--size-control-sm) shrink-0 items-center gap-1 rounded-control border px-2 text-micro transition-colors duration-(--duration-instant) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary ${puestos > 0 || panel
367
375
  ? bySurface(sup, "border-accent/40 bg-accent/18 text-ink", "border-chrome-link/35 bg-chrome-link/15 text-chrome-link")
368
376
  : bySurface(sup, "border-transparent text-muted hover:bg-ink/5 hover:text-ink", "border-transparent text-chrome-muted hover:bg-chrome-ink/10 hover:text-chrome-ink")}`, children: [_jsx(ListFilter, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true }), puestos > 0 ? _jsx("span", { className: "text-micro tabular-nums", children: puestos }) : null] }))] }), trailing, open ? (_jsxs("div", { className: `absolute top-full z-30 mt-1 overflow-hidden rounded-panel border border-line-strong bg-surface shadow-float ${asTags ? "left-0 w-max max-w-full" : "right-0 left-0"}`, children: [openJoin === undefined ? null : (_jsxs("div", { className: "flex items-center gap-1.5 border-line border-b px-3 py-1.5 text-micro text-muted", children: [_jsx(CornerDownRight, { className: "size-3 shrink-0", strokeWidth: 2, "aria-hidden": true }), _jsx("span", { className: "min-w-0 truncate", children: copy.search.landsHere }), _jsx("span", { className: "shrink-0 rounded-pill border border-accent/40 bg-accent/18 px-1.5 text-ink", children: copy.search.joins[openJoin === "not" ? "none" : openJoin] })] })), asTags ? (
369
377
  /* UNA LISTA, no una tira. Se está ELIGIENDO: el ojo baja por una
@@ -0,0 +1,60 @@
1
+ import { type TimelineScale } from "./time.js";
2
+ import type { Tone } from "../../foundations/types.js";
3
+ import { type ReactNode } from "react";
4
+ /** A row of the timeline: what the records are grouped by — a person, a room, a machine. */
5
+ export type TimelineRow = {
6
+ id: string;
7
+ label: string;
8
+ /** The row's face: `true` draws the initials of `label`, a string is a photo's URL. */
9
+ avatar?: boolean | string;
10
+ /** A second line, quiet: the role, the department. */
11
+ detail?: string;
12
+ };
13
+ export type RecordTimelineProps<T> = {
14
+ items: readonly T[];
15
+ keyOf: (item: T) => string;
16
+ /** The row a record goes in: its employee, its room. A grouping, as Odoo's «group by». */
17
+ rowOf: (item: T) => string;
18
+ /** The rows, in order — including the ones with nothing in this period (someone who did not
19
+ * come is part of the answer). Without it they come from the records. */
20
+ rows?: readonly TimelineRow[];
21
+ startOf: (item: T) => Date;
22
+ endOf: (item: T) => Date;
23
+ /** The record's name, on its bar. */
24
+ titleOf?: (item: T) => string;
25
+ /** What the bar says, replacing the default: the name, and on a day or a week its duration
26
+ * and its hours — «03:48 (09:12–13:13)». */
27
+ labelOf?: (item: T) => ReactNode;
28
+ toneOf?: (item: T) => Tone;
29
+ /** The head of the names column: «Empleado». */
30
+ rowHeader?: string;
31
+ /** Controlled scale, or the one it starts on. */
32
+ scale?: TimelineScale;
33
+ defaultScale?: TimelineScale;
34
+ onScaleChange?: (next: TimelineScale) => void;
35
+ scales?: readonly TimelineScale[];
36
+ /** Controlled date, or the one it starts on (today). */
37
+ date?: Date;
38
+ defaultDate?: Date;
39
+ onDateChange?: (next: Date) => void;
40
+ /** A bar was dragged (in time, or to another row) or stretched. The app saves and passes the
41
+ * new `items`; until then the bar goes back. */
42
+ onMove?: (item: T, start: Date, end: Date, row: string) => void;
43
+ /** An empty stretch of a row was pressed: create a record there, one column long. */
44
+ onCreate?: (row: string, start: Date, end: Date) => void;
45
+ onOpen?: (item: T) => void;
46
+ };
47
+ /**
48
+ * Records on a timeline, grouped in rows — Odoo's Gantt view: attendances by employee, shifts,
49
+ * room bookings, work orders by machine.
50
+ *
51
+ * Context: a project Gantt (`RecordGantt`) is one task per row with dependencies; THIS is the
52
+ * other one people mean by «Gantt» in an ERP — a grouping with N records per row, read across
53
+ * time. The scale says the period (a day by hours, a week, a month by days, a year by months);
54
+ * the toolbar moves it (‹ › date · today), zooms, switches scale and takes the whole screen.
55
+ *
56
+ * Controlled: a drag shows a draft and reports `onMove` on release; the bar stays where
57
+ * `items` says. Everything the pointer does has a key: Alt + ← / → moves by the snap, with
58
+ * Shift it stretches, Alt + ↑ / ↓ changes the row; every move is announced.
59
+ */
60
+ export declare function RecordTimeline<T>({ items, keyOf, rowOf, rows: givenRows, startOf, endOf, titleOf, labelOf, toneOf, rowHeader, scale: outerScale, defaultScale, onScaleChange, scales, date: outerDate, defaultDate, onDateChange, onMove, onCreate, onOpen, }: RecordTimelineProps<T>): import("react").JSX.Element;