@sincpro/web-ui 0.2.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +3 -3
  2. package/dist/components/activity/ActivityBadge.d.ts +16 -0
  3. package/dist/components/activity/ActivityBadge.js +29 -0
  4. package/dist/components/activity/index.d.ts +1 -0
  5. package/dist/components/activity/index.js +1 -0
  6. package/dist/components/calendar/RecordCalendar.d.ts +48 -0
  7. package/dist/components/calendar/RecordCalendar.js +101 -0
  8. package/dist/components/calendar/index.d.ts +1 -0
  9. package/dist/components/calendar/index.js +1 -0
  10. package/dist/components/form/Sheet.js +5 -1
  11. package/dist/components/gantt/RecordGantt.d.ts +44 -0
  12. package/dist/components/gantt/RecordGantt.js +110 -0
  13. package/dist/components/gantt/index.d.ts +1 -0
  14. package/dist/components/gantt/index.js +1 -0
  15. package/dist/components/index.d.ts +3 -0
  16. package/dist/components/index.js +3 -0
  17. package/dist/components/lines/LineItems.d.ts +5 -2
  18. package/dist/components/lines/LineItems.js +3 -3
  19. package/dist/components/listing/BoardColumns.d.ts +39 -0
  20. package/dist/components/listing/BoardColumns.js +138 -0
  21. package/dist/components/listing/ListingBar.d.ts +5 -0
  22. package/dist/components/listing/ListingBar.js +5 -2
  23. package/dist/components/listing/PivotExplorer.d.ts +34 -0
  24. package/dist/components/listing/PivotExplorer.js +86 -0
  25. package/dist/components/listing/RecordCard.d.ts +6 -8
  26. package/dist/components/listing/RecordCard.js +13 -7
  27. package/dist/components/listing/Row.d.ts +21 -2
  28. package/dist/components/listing/Row.js +29 -13
  29. package/dist/components/listing/index.d.ts +2 -0
  30. package/dist/components/listing/index.js +2 -0
  31. package/dist/components/listing/set.d.ts +1 -1
  32. package/dist/components/navigation/index.d.ts +0 -2
  33. package/dist/components/navigation/index.js +2 -3
  34. package/dist/components/record/RecordAudit.d.ts +17 -0
  35. package/dist/components/record/RecordAudit.js +27 -0
  36. package/dist/components/record/RecordHeader.d.ts +7 -1
  37. package/dist/components/record/RecordHeader.js +30 -18
  38. package/dist/components/record/RecordRibbon.d.ts +13 -0
  39. package/dist/components/record/RecordRibbon.js +22 -0
  40. package/dist/components/record/index.d.ts +2 -0
  41. package/dist/components/record/index.js +2 -0
  42. package/dist/components/search/CriteriaChips.d.ts +6 -1
  43. package/dist/components/search/CriteriaChips.js +71 -20
  44. package/dist/components/search/SearchBar.d.ts +15 -1
  45. package/dist/components/search/SearchBar.js +34 -16
  46. package/dist/components/search/query.d.ts +14 -11
  47. package/dist/components/search/query.js +36 -3
  48. package/dist/components/timeline/RecordTimeline.d.ts +60 -0
  49. package/dist/components/timeline/RecordTimeline.js +214 -0
  50. package/dist/components/timeline/TimelineToolbar.d.ts +30 -0
  51. package/dist/components/timeline/TimelineToolbar.js +31 -0
  52. package/dist/components/timeline/index.d.ts +3 -0
  53. package/dist/components/timeline/index.js +3 -0
  54. package/dist/components/timeline/time.d.ts +43 -0
  55. package/dist/components/timeline/time.js +135 -0
  56. package/dist/foundations/copy.d.ts +110 -14
  57. package/dist/foundations/copy.es.js +85 -9
  58. package/dist/foundations/copy.js +85 -9
  59. package/dist/foundations/grouping.d.ts +3 -0
  60. package/dist/foundations/index.d.ts +1 -1
  61. package/dist/foundations/match.d.ts +23 -0
  62. package/dist/foundations/match.js +63 -0
  63. package/dist/foundations/slots.d.ts +1 -1
  64. package/dist/foundations/slots.js +8 -3
  65. package/dist/foundations/types.d.ts +7 -0
  66. package/dist/foundations/types.js +0 -36
  67. package/dist/primitives/block/index.d.ts +1 -1
  68. package/dist/primitives/block/index.js +1 -1
  69. package/dist/primitives/cell/CellStack.d.ts +7 -1
  70. package/dist/primitives/cell/CellStack.js +5 -2
  71. package/dist/primitives/choice/ComboBox.js +8 -4
  72. package/dist/primitives/choice/KanbanState.d.ts +14 -0
  73. package/dist/primitives/choice/KanbanState.js +37 -0
  74. package/dist/primitives/choice/Priority.d.ts +20 -0
  75. package/dist/primitives/choice/Priority.js +41 -0
  76. package/dist/primitives/choice/Slider.d.ts +6 -1
  77. package/dist/primitives/choice/Slider.js +2 -2
  78. package/dist/primitives/choice/index.d.ts +2 -0
  79. package/dist/primitives/choice/index.js +2 -0
  80. package/dist/primitives/dialog/Dialog.d.ts +9 -3
  81. package/dist/primitives/dialog/Dialog.js +41 -5
  82. package/dist/primitives/dialog/index.d.ts +0 -1
  83. package/dist/primitives/dialog/index.js +0 -2
  84. package/dist/primitives/notice/index.d.ts +1 -1
  85. package/dist/primitives/notice/index.js +1 -1
  86. package/dist/primitives/overlay/Preview.js +13 -4
  87. package/dist/primitives/picker/MatchedText.d.ts +10 -0
  88. package/dist/primitives/picker/MatchedText.js +12 -0
  89. package/dist/primitives/picker/Picker.d.ts +42 -7
  90. package/dist/primitives/picker/Picker.js +107 -29
  91. package/dist/primitives/table/EditableTable.d.ts +6 -1
  92. package/dist/primitives/table/EditableTable.js +70 -9
  93. package/dist/primitives/table/ReportTable.js +2 -1
  94. package/dist/primitives/table/RowActions.js +7 -2
  95. package/dist/primitives/table/Table.d.ts +5 -0
  96. package/dist/primitives/table/Table.js +17 -8
  97. package/dist/primitives/tag/Tags.d.ts +2 -0
  98. package/dist/primitives/tag/Tags.js +2 -1
  99. package/dist/primitives/tag/TagsField.d.ts +17 -0
  100. package/dist/primitives/tag/TagsField.js +46 -11
  101. package/dist/primitives/tag/choices.d.ts +6 -1
  102. package/dist/primitives/tag/choices.js +6 -2
  103. package/dist/primitives/value/Avatar.d.ts +16 -0
  104. package/dist/primitives/value/Avatar.js +91 -0
  105. package/dist/primitives/value/Person.d.ts +6 -51
  106. package/dist/primitives/value/Person.js +9 -148
  107. package/dist/primitives/value/Relation.d.ts +21 -9
  108. package/dist/primitives/value/Relation.js +35 -24
  109. package/dist/primitives/value/contact.d.ts +30 -0
  110. package/dist/primitives/value/contact.js +51 -0
  111. package/dist/primitives/value/index.d.ts +1 -1
  112. package/dist/primitives/value/index.js +1 -1
  113. package/dist/tokens.css +136 -1
  114. package/dist/views/list/List.d.ts +25 -2
  115. package/dist/views/list/List.js +48 -8
  116. package/dist/views/list/ListView.d.ts +1 -1
  117. package/dist/views/list/ListView.js +40 -9
  118. package/dist/views/list/columns.js +8 -1
  119. package/dist/views/list/presentations.d.ts +5 -2
  120. package/dist/views/list/presentations.js +32 -2
  121. package/dist/views/list/types.d.ts +44 -0
  122. package/dist/views/list/useNarrowList.d.ts +10 -0
  123. package/dist/views/list/useNarrowList.js +20 -6
  124. package/dist/views/record/Record.d.ts +14 -0
  125. package/dist/views/record/Record.js +22 -7
  126. package/package.json +11 -4
  127. package/dist/components/navigation/CommandPalette.d.ts +0 -40
  128. package/dist/components/navigation/CommandPalette.js +0 -114
  129. package/dist/components/navigation/commands.d.ts +0 -30
  130. package/dist/components/navigation/commands.js +0 -30
  131. package/dist/primitives/dialog/Drill.d.ts +0 -90
  132. package/dist/primitives/dialog/Drill.js +0 -172
@@ -0,0 +1,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
  }
@@ -1,4 +1,5 @@
1
1
  import { type CriteriaJoin, type CriteriaPath, type SearchCriteria } from "./criteria.js";
2
+ import type { BuiltCondition, BuiltFilter } from "./FilterBuilder.js";
2
3
  import type { ModelDefinition } from "./model.js";
3
4
  import type { Surface } from "../../foundations/types.js";
4
5
  import type { MenuGroup } from "../../primitives/menu/Menu.js";
@@ -70,9 +71,22 @@ export type SearchBarProps = {
70
71
  * obligar a buscar la que uno venía mirando entre doce.
71
72
  */
72
73
  onOpenBuilder?: (path?: CriteriaPath) => void;
74
+ /**
75
+ * The set's own search — what a free text means when it is not a question about one field:
76
+ * every field the set searches, at once (sincpro_framework's `LiteralSearch`, Odoo's «Search
77
+ * for: …»). Given the typed text, it returns what the chip asks; `null` offers nothing.
78
+ *
79
+ * With it, «Buscar «Globex»» is offered for every text. It goes FIRST — the one Enter takes —
80
+ * unless the model reads the text as a question of its own (an operator, a field's name, an
81
+ * enum's value, a number a numeric field answers): then that goes first and this after it.
82
+ */
83
+ literal?: (text: string) => BuiltCondition | BuiltFilter | null;
73
84
  /** Lo que va pegado al campo, a su derecha. */
74
85
  trailing?: ReactNode;
86
+ /** How many facets the bar shows before folding the rest into «+N»; the grouping chain
87
+ * counts as one. On a narrow bar only the first shows. `Infinity` never folds. */
88
+ collapseAfter?: number;
75
89
  /** Por default la hereda de la superficie que la envuelve. */
76
90
  surface?: Surface;
77
91
  };
78
- export declare function SearchBar({ facets, model, criteria, onCriteriaChange, placeholder, filters, grouping, onOpenGroup, favorites, onFilterSelect, onFavoriteSelect, onOpenBuilder, trailing, surface, }: SearchBarProps): import("react").JSX.Element;
92
+ export declare function SearchBar({ facets, model, criteria, onCriteriaChange, placeholder, filters, grouping, onOpenGroup, favorites, onFilterSelect, onFavoriteSelect, onOpenBuilder, literal, trailing, collapseAfter, surface, }: SearchBarProps): import("react").JSX.Element;
@@ -3,10 +3,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { at, criteriaId, filterCriteria, removeAt, replaceAt, } from "./criteria.js";
4
4
  import { CriteriaChips } from "./CriteriaChips.js";
5
5
  import { describeCondition, opLabel, queryFields, readTyped, suggest, withoutValue, } from "./query.js";
6
- import { useUiCopy } from "../../foundations/copy.js";
6
+ import { fillCopy, 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, literal, trailing, collapseAfter, surface, }) {
32
32
  const copy = useUiCopy();
33
33
  const sup = useSurface(surface);
34
34
  const [text, setText] = useState("");
@@ -102,7 +102,7 @@ export function SearchBar({ facets, model, criteria, onCriteriaChange, placehold
102
102
  ...(step.field === undefined ? {} : { field: step.field }),
103
103
  ...(step.op === undefined ? {} : { op: step.op }),
104
104
  text,
105
- }, fields);
105
+ }, fields, { literal: literal !== undefined });
106
106
  /* Lo escrito, ya leído: hace falta acá para saber si se está eligiendo un
107
107
  ATRIBUTO —lo que empieza con `#`— y dibujar fichas en vez de renglones. */
108
108
  const asked = readTyped(text);
@@ -155,12 +155,8 @@ export function SearchBar({ facets, model, criteria, onCriteriaChange, placehold
155
155
  * la lista si no. Es lo único que la barra decide sobre dónde van las cosas:
156
156
  * el resto lo decide quien tiene la lista.
157
157
  */
158
- function commit(field, op, value) {
159
- const one = filterCriteria(
160
- /* El que no lleva valor viaja con `true`, que es lo que el motor espera de
161
- «está vacío»: mandarle la cadena vacía sería preguntar por el campo que
162
- vale "". */
163
- { field: field.field, operator: op, value: withoutValue(op) ? true : value }, describeCondition(field, op, value));
158
+ /** A finished instance, where the next one goes: the end, or the group left open. */
159
+ function put(one) {
164
160
  onCriteriaChange(openGroup === null
165
161
  ? [...criteria, one]
166
162
  : replaceAt(criteria, openGroup, addTo(at(criteria, openGroup), one)));
@@ -168,6 +164,14 @@ export function SearchBar({ facets, model, criteria, onCriteriaChange, placehold
168
164
  setStep({});
169
165
  input.current?.focus();
170
166
  }
167
+ function commit(field, op, value) {
168
+ const one = filterCriteria(
169
+ /* El que no lleva valor viaja con `true`, que es lo que el motor espera de
170
+ «está vacío»: mandarle la cadena vacía sería preguntar por el campo que
171
+ vale "". */
172
+ { field: field.field, operator: op, value: withoutValue(op) ? true : value }, describeCondition(field, op, value));
173
+ put(one);
174
+ }
171
175
  /** Lo elegido en el desplegable, según de qué clase sea. */
172
176
  function pick(suggestion) {
173
177
  if (suggestion.kind === "field") {
@@ -199,6 +203,12 @@ export function SearchBar({ facets, model, criteria, onCriteriaChange, placehold
199
203
  input.current?.focus();
200
204
  return;
201
205
  }
206
+ if (suggestion.kind === "literal") {
207
+ const where = literal?.(suggestion.text) ?? null;
208
+ if (where !== null)
209
+ put(filterCriteria(where, fillCopy(copy.search.literalChip, { text: suggestion.text })));
210
+ return;
211
+ }
202
212
  commit(suggestion.field, suggestion.op, suggestion.value);
203
213
  }
204
214
  /** El nodo con una instancia más adentro. */
@@ -207,14 +217,18 @@ export function SearchBar({ facets, model, criteria, onCriteriaChange, placehold
207
217
  ? one
208
218
  : { ...node, of: [...node.of, one] };
209
219
  }
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",
220
+ 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
221
  /* Sobre el cromo lo que define al campo es el BORDE, no el relleno. El
212
222
  azul ya es casi negro, así que hundirlo no da contraste (1,05:1) y
213
223
  levantarlo da 1,21:1 — en los dos casos el campo no se lee como un
214
224
  campo, que era «no llama la atención». La tinta al 45 % es el primer
215
225
  borde que pasa 3:1 contra el relleno Y contra el cromo, que es lo que
216
226
  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) => {
227
+ "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) => {
228
+ /* From the deepest index up, so each removal leaves the next path valid. */
229
+ const sorted = [...paths].sort((a, b) => (b[0] ?? 0) - (a[0] ?? 0));
230
+ onCriteriaChange(sorted.reduce((left, path) => removeAt(left, path), [...criteria]));
231
+ }, onJoin: (path, join) => {
218
232
  const node = at(criteria, path);
219
233
  if (node?.kind !== "join")
220
234
  return;
@@ -276,7 +290,7 @@ export function SearchBar({ facets, model, criteria, onCriteriaChange, placehold
276
290
  removeLabel: copy.search.dropOp,
277
291
  },
278
292
  ]),
279
- ] }), _jsx("input", { ref: input, value: text, onChange: (e) => setText(e.target.value), onKeyDown: (e) => {
293
+ ] }), _jsx("input", { ref: input, "aria-label": copy.choice.search, value: text, onChange: (e) => setText(e.target.value), onKeyDown: (e) => {
280
294
  /* Retroceso con el campo vacío saca la ÚLTIMA instancia: el gesto
281
295
  que ya existe en cualquier campo de fichas y el único que se
282
296
  descubre sin que nadie lo explique. La última y no la última de
@@ -362,8 +376,12 @@ export function SearchBar({ facets, model, criteria, onCriteriaChange, placehold
362
376
  crezca con lo escrito en vez de reservar una banda vacía. Donde no
363
377
  esté, el input se estira como siempre. */
364
378
  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
379
+ ? `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")}`
380
+ : `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: () => {
381
+ onCriteriaChange([]);
382
+ setOpenGroup(null);
383
+ input.current?.focus();
384
+ }, "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
385
  ? bySurface(sup, "border-accent/40 bg-accent/18 text-ink", "border-chrome-link/35 bg-chrome-link/15 text-chrome-link")
368
386
  : 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
387
  /* UNA LISTA, no una tira. Se está ELIGIENDO: el ojo baja por una
@@ -379,7 +397,7 @@ export function SearchBar({ facets, model, criteria, onCriteriaChange, placehold
379
397
  : {}),
380
398
  active: at === active,
381
399
  onSelect: () => pick(one),
382
- })) }) })) : (_jsx("div", { className: "p-1", children: suggestions.map((one, at) => (_jsxs("button", { type: "button", onClick: () => pick(one), onMouseEnter: () => setActive(at), className: `flex w-full items-baseline gap-1.5 rounded-control px-2 py-1.5 text-left text-meta transition-colors duration-(--duration-instant) ${at === active ? "bg-primary/8" : "hover:bg-ink/4"}`, children: [_jsx("span", { className: "shrink-0 font-medium text-ink", children: one.label }), one.kind === "condition" ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "shrink-0 text-muted", children: ":" }), _jsx("span", { className: "min-w-0 truncate font-mono text-micro text-ink", children: one.hint })] })) : one.kind === "field" && one.hint !== undefined ? (_jsx("span", { className: "inline-flex shrink-0 items-center rounded-pill border border-line-strong bg-ink/5 px-2 py-0.5 text-micro text-muted", children: one.hint })) : null] }, one.id))) }))] })) : null, panel && (columns.length > 0 || onOpenBuilder !== undefined) ? (_jsxs("div", { id: id, className: "absolute top-full right-0 z-30 mt-1 flex w-max max-w-full flex-wrap overflow-hidden rounded-panel border border-line-strong bg-surface shadow-float", children: [columns.map((c, i) => (_jsxs("div", { className: `flex min-w-48 flex-1 flex-col ${i === 0 ? "" : "border-t border-line sm:border-t-0 sm:border-l"}`, children: [_jsx("div", { className: "border-b border-line px-2.5 py-1.5 text-micro font-medium text-muted", children: c.title }), _jsx("div", { className: "flex flex-col gap-0.5 p-1", children: c.content ??
400
+ })) }) })) : (_jsx("div", { className: "p-1", children: suggestions.map((one, at) => (_jsxs("button", { type: "button", onClick: () => pick(one), onMouseEnter: () => setActive(at), className: `flex w-full items-baseline gap-1.5 rounded-control px-2 py-1.5 text-left text-meta transition-colors duration-(--duration-instant) ${at === active ? "bg-primary/8" : "hover:bg-ink/4"}`, children: [_jsx("span", { className: "shrink-0 font-medium text-ink", children: one.label }), one.kind === "condition" || one.kind === "literal" ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "shrink-0 text-muted", children: ":" }), _jsx("span", { className: "min-w-0 truncate font-mono text-micro text-ink", children: one.hint })] })) : one.kind === "field" && one.hint !== undefined ? (_jsx("span", { className: "inline-flex shrink-0 items-center rounded-pill border border-line-strong bg-ink/5 px-2 py-0.5 text-micro text-muted", children: one.hint })) : null] }, one.id))) }))] })) : null, panel && (columns.length > 0 || onOpenBuilder !== undefined) ? (_jsxs("div", { id: id, className: "absolute top-full right-0 z-30 mt-1 flex w-max max-w-full flex-wrap overflow-hidden rounded-panel border border-line-strong bg-surface shadow-float", children: [columns.map((c, i) => (_jsxs("div", { className: `flex min-w-48 flex-1 flex-col ${i === 0 ? "" : "border-t border-line sm:border-t-0 sm:border-l"}`, children: [_jsx("div", { className: "border-b border-line px-2.5 py-1.5 text-micro font-medium text-muted", children: c.title }), _jsx("div", { className: "flex flex-col gap-0.5 p-1", children: c.content ??
383
401
  c.groups?.map((g, gi) => (_jsx(Group, { group: g, first: gi === 0, onSelect: c.onSelect }, g.label ?? gi))) })] }, c.title))), onOpenBuilder === undefined ? null : (_jsx("div", { className: "w-full border-t border-line p-1", children: _jsxs("button", { type: "button", onClick: () => {
384
402
  onOpenBuilder();
385
403
  setPanel(false);
@@ -122,6 +122,15 @@ export type QuerySuggestion =
122
122
  field: QueryField;
123
123
  op: string;
124
124
  value: string;
125
+ }
126
+ /** The set's own search over the text — every field it searches, at once. What it asks is
127
+ * the consumer's (`SearchBar literal`): the bar only offers it and puts the chip. */
128
+ | {
129
+ kind: "literal";
130
+ id: string;
131
+ label: string;
132
+ hint: string;
133
+ text: string;
125
134
  };
126
135
  /** Cómo se llama un operador para quien lo lee. Sale de la copia, así que un
127
136
  * operador que el servidor agregue aparece con su símbolo hasta que alguien le
@@ -139,17 +148,11 @@ export declare function withoutValue(op: string): boolean;
139
148
  * preguntan en el constructor, que tiene los dos campos.
140
149
  */
141
150
  export declare function withTwoValues(op: string): boolean;
142
- /**
143
- * Qué se ofrece ahora mismo.
144
- *
145
- * Es una sola lista y no tres pantallas: en el primer paso conviven los campos
146
- * que se llaman como lo tipeado —«nombr» → «nombre»— con las preguntas enteras
147
- * que lo tipeado ya permite armar —«test» en el nombre—. Quien escribe el nombre
148
- * de un campo quiere seguir por ahí; quien escribe un valor quiere preguntarlo
149
- * ya. Las dos intenciones se escriben igual, así que se ofrecen las dos, con los
150
- * campos primero: es el paso que todavía no decidió nada.
151
- */
152
- export declare function suggest(draft: QueryDraft, fields: readonly QueryField[]): QuerySuggestion[];
151
+ export declare function suggest(draft: QueryDraft, fields: readonly QueryField[],
152
+ /** The set has a literal search (`SearchBar literal`): offer it for free text. */
153
+ options?: {
154
+ literal?: boolean;
155
+ }): QuerySuggestion[];
153
156
  /**
154
157
  * CÓMO SE LEE UNA CONDICIÓN, en un solo lugar.
155
158
  *