@sincpro/web-ui 0.2.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 (128) 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 +4 -1
  45. package/dist/components/search/SearchBar.js +15 -7
  46. package/dist/components/timeline/RecordTimeline.d.ts +60 -0
  47. package/dist/components/timeline/RecordTimeline.js +214 -0
  48. package/dist/components/timeline/TimelineToolbar.d.ts +30 -0
  49. package/dist/components/timeline/TimelineToolbar.js +31 -0
  50. package/dist/components/timeline/index.d.ts +3 -0
  51. package/dist/components/timeline/index.js +3 -0
  52. package/dist/components/timeline/time.d.ts +43 -0
  53. package/dist/components/timeline/time.js +135 -0
  54. package/dist/foundations/copy.d.ts +106 -14
  55. package/dist/foundations/copy.es.js +83 -9
  56. package/dist/foundations/copy.js +83 -9
  57. package/dist/foundations/index.d.ts +1 -1
  58. package/dist/foundations/match.d.ts +23 -0
  59. package/dist/foundations/match.js +63 -0
  60. package/dist/foundations/slots.d.ts +1 -1
  61. package/dist/foundations/slots.js +8 -3
  62. package/dist/foundations/types.d.ts +7 -0
  63. package/dist/foundations/types.js +0 -36
  64. package/dist/primitives/block/index.d.ts +1 -1
  65. package/dist/primitives/block/index.js +1 -1
  66. package/dist/primitives/cell/CellStack.d.ts +7 -1
  67. package/dist/primitives/cell/CellStack.js +5 -2
  68. package/dist/primitives/choice/ComboBox.js +8 -4
  69. package/dist/primitives/choice/KanbanState.d.ts +14 -0
  70. package/dist/primitives/choice/KanbanState.js +37 -0
  71. package/dist/primitives/choice/Priority.d.ts +20 -0
  72. package/dist/primitives/choice/Priority.js +41 -0
  73. package/dist/primitives/choice/Slider.d.ts +6 -1
  74. package/dist/primitives/choice/Slider.js +2 -2
  75. package/dist/primitives/choice/index.d.ts +2 -0
  76. package/dist/primitives/choice/index.js +2 -0
  77. package/dist/primitives/dialog/Dialog.d.ts +9 -3
  78. package/dist/primitives/dialog/Dialog.js +41 -5
  79. package/dist/primitives/dialog/index.d.ts +0 -1
  80. package/dist/primitives/dialog/index.js +0 -2
  81. package/dist/primitives/notice/index.d.ts +1 -1
  82. package/dist/primitives/notice/index.js +1 -1
  83. package/dist/primitives/overlay/Preview.js +13 -4
  84. package/dist/primitives/picker/MatchedText.d.ts +10 -0
  85. package/dist/primitives/picker/MatchedText.js +12 -0
  86. package/dist/primitives/picker/Picker.d.ts +29 -7
  87. package/dist/primitives/picker/Picker.js +88 -23
  88. package/dist/primitives/table/EditableTable.d.ts +6 -1
  89. package/dist/primitives/table/EditableTable.js +70 -9
  90. package/dist/primitives/table/ReportTable.js +2 -1
  91. package/dist/primitives/table/RowActions.js +7 -2
  92. package/dist/primitives/table/Table.d.ts +5 -0
  93. package/dist/primitives/table/Table.js +17 -8
  94. package/dist/primitives/tag/Tags.d.ts +2 -0
  95. package/dist/primitives/tag/Tags.js +2 -1
  96. package/dist/primitives/tag/TagsField.d.ts +10 -0
  97. package/dist/primitives/tag/TagsField.js +34 -9
  98. package/dist/primitives/tag/choices.d.ts +2 -0
  99. package/dist/primitives/value/Avatar.d.ts +16 -0
  100. package/dist/primitives/value/Avatar.js +91 -0
  101. package/dist/primitives/value/Person.d.ts +6 -51
  102. package/dist/primitives/value/Person.js +9 -148
  103. package/dist/primitives/value/Relation.d.ts +21 -9
  104. package/dist/primitives/value/Relation.js +35 -24
  105. package/dist/primitives/value/contact.d.ts +30 -0
  106. package/dist/primitives/value/contact.js +51 -0
  107. package/dist/primitives/value/index.d.ts +1 -1
  108. package/dist/primitives/value/index.js +1 -1
  109. package/dist/tokens.css +136 -1
  110. package/dist/views/list/List.d.ts +25 -2
  111. package/dist/views/list/List.js +48 -8
  112. package/dist/views/list/ListView.d.ts +1 -1
  113. package/dist/views/list/ListView.js +39 -8
  114. package/dist/views/list/columns.js +8 -1
  115. package/dist/views/list/presentations.d.ts +5 -2
  116. package/dist/views/list/presentations.js +32 -2
  117. package/dist/views/list/types.d.ts +44 -0
  118. package/dist/views/list/useNarrowList.d.ts +10 -0
  119. package/dist/views/list/useNarrowList.js +20 -6
  120. package/dist/views/record/Record.d.ts +14 -0
  121. package/dist/views/record/Record.js +22 -7
  122. package/package.json +11 -4
  123. package/dist/components/navigation/CommandPalette.d.ts +0 -40
  124. package/dist/components/navigation/CommandPalette.js +0 -114
  125. package/dist/components/navigation/commands.d.ts +0 -30
  126. package/dist/components/navigation/commands.js +0 -30
  127. package/dist/primitives/dialog/Drill.d.ts +0 -90
  128. package/dist/primitives/dialog/Drill.js +0 -172
@@ -0,0 +1,27 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { fillCopy, useUiCopy } from "../../foundations/copy.js";
4
+ /**
5
+ * The last line of the paper: who created the record and who touched it last.
6
+ *
7
+ * Context: «¿quién cambió esto?» is the first question in front of a number that looks wrong,
8
+ * and the activity's timeline answers it only for what was logged. Small and at the end: it is
9
+ * consulted, not read.
10
+ */
11
+ export function RecordAudit({ created, updated, }) {
12
+ const copy = useUiCopy();
13
+ const stamp = (template, one) => {
14
+ const [before, middle, after] = splitStamp(template);
15
+ return (_jsxs("span", { children: [before, _jsx("span", { className: "text-ink", children: one.by }), middle, _jsx("span", { className: "tabular-nums", children: one.at }), after] }));
16
+ };
17
+ if (created === undefined && updated === undefined)
18
+ return null;
19
+ return (_jsxs("p", { className: "flex flex-wrap gap-x-4 gap-y-1 border-t border-line px-4 py-2.5 text-micro text-muted", children: [created === undefined ? null : stamp(copy.record.created, created), updated === undefined ? null : stamp(copy.record.updated, updated)] }));
20
+ }
21
+ /** «Creado por {by} · {at}» cut around its two holes, so `by` and `at` can be nodes. */
22
+ function splitStamp(template) {
23
+ const marked = fillCopy(template, { by: "\u0000", at: "\u0001" });
24
+ const [before = "", rest = ""] = marked.split("\u0000");
25
+ const [middle = "", after = ""] = rest.split("\u0001");
26
+ return [before, middle, after];
27
+ }
@@ -4,7 +4,7 @@ import type { Surface, Tone } from "../../foundations/types.js";
4
4
  import type { LucideIcon } from "lucide-react";
5
5
  import type { ReactNode } from "react";
6
6
  export type { Surface };
7
- export declare function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Icon, title, subtitle, tag, figure, railSurface, smart, aspects, dirty, activity, activityLayout, blocking, indicator, children, }: {
7
+ export declare function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Icon, title, subtitle, tag, figure, railSurface, smart, aspects, dirty, activity, activityLayout, width, ribbon, blocking, indicator, children, }: {
8
8
  crumbs?: readonly Crumb[];
9
9
  pager?: ReactNode;
10
10
  /** The action buttons. At most three visible + a `Menu` with `iconOnly`, and
@@ -28,6 +28,12 @@ export declare function RecordHeader({ crumbs, pager, actions, status, kicker, i
28
28
  * itself out breaks the next time someone wraps the panel in something else.
29
29
  */
30
30
  activityLayout?: "auto" | "below";
31
+ /** How wide the page column gets. `reading` (the default) stops the sheet at
32
+ * `--container-sheet`, centred, and lines the rail up with it; `full` takes the whole page
33
+ * — a record whose body is a wide table. */
34
+ width?: "reading" | "full";
35
+ /** The band across the paper's corner (`RecordRibbon`): what the record IS now. */
36
+ ribbon?: ReactNode;
31
37
  /** `StatusBar` (where is it at?), the activity's `Timeline` (what happened to get
32
38
  * here?), or both: they are different questions and many records only have one. */
33
39
  status?: ReactNode;
@@ -7,7 +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
- export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Icon, title, subtitle, tag, figure, railSurface = "canvas", smart, aspects, dirty, activity, activityLayout = "auto", blocking, indicator, 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, }) {
11
11
  const d = useDensity();
12
12
  const hasRail = crumbs !== undefined ||
13
13
  pager !== undefined ||
@@ -46,6 +46,14 @@ export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Ico
46
46
  pager !== undefined ||
47
47
  actions !== undefined ||
48
48
  dirty !== undefined);
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)";
49
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 : (
50
58
  /* `display`, not `title`: the record's name is the entry point of the
51
59
  screen. At the title size (19 px over a 13 px body, 1.46:1) nothing on
@@ -54,18 +62,20 @@ export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Ico
54
62
  /* The line is the sheet's mouth. In comfortable it walks to the
55
63
  edges; the current tab sits on it and opens into the page below. */
56
64
  _jsx("div", { className: `border-b border-line-strong ${byDensity(d, "-mx-4 px-4", "")}`, children: aspects }))] }));
57
- 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:
58
- /* The notices' indicator rides with the counters: both are about the
59
- record and both open something; neither is my position in front of
60
- it. The line wraps on a phone, so it is never pushed out of reach. */
61
- 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 && status === undefined && dirty === undefined ? null : (
62
- /* Row 2 — WHAT I CAN DO and WHERE IT IS AT: actions stuck to the
63
- left edge, the history and the state stuck to the right one. No
64
- `flex-wrap`: wrapping split the row in two and the state ended up
65
- below the actions, which is another hierarchy. */
66
- _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
67
- ? actions
68
- : 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 ? (
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 ? (
69
79
  /* ==================================================================== *
70
80
  * THE PAGE — [sheet | Actividad], on the ground.
71
81
  *
@@ -92,13 +102,15 @@ export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Ico
92
102
  * a scroll container and the line table header would stick inside it, not under
93
103
  * the application bar.
94
104
  * ==================================================================== */
95
- _jsx("div", { className: "bg-ground px-3 pt-3 pb-3", children: _jsx("div", { className: "@container/page", children: _jsxs("div", { className: `mx-auto flex min-w-0 flex-col gap-3 ${beside
96
- ? "max-w-(--container-record) @beside/page:flex-row @beside/page:items-stretch @beside/page:justify-center @beside/page:gap-0"
97
- : "max-w-(--container-sheet)"}`, children: [_jsxs("div", { className: `flex min-w-0 flex-1 flex-col gap-3 ${beside ? "mx-auto w-full max-w-(--container-sheet) @beside/page:mx-0" : ""}`, children: [blocking, _jsxs("div", { className: `flex min-w-0 flex-col overflow-clip rounded-box border border-line bg-surface ${beside ? "@beside/page:rounded-r-none" : ""}`, children: [identity, children] }), activity !== undefined && activityLayout === "below" ? activity : null] }), beside ? (
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 ? (
98
110
  /* After the sheet: the record first, what is around it second — in the
99
111
  reading order and on screen. */
100
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] }) }) })) : (
101
- /* No paper (a dialog, a drawer, a `Drill` context): one surface, no ground beside
113
+ /* No paper (a modal, a drawer): one surface, no ground beside
102
114
  it — the band first, the record, and the activity below. */
103
- _jsxs("div", { className: "flex min-w-0 flex-1 flex-col bg-surface", children: [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 })] }));
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 })] }));
104
116
  }
@@ -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
+ }
@@ -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;