@sincpro/web-ui 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -13
- package/dist/components/activity/Activity.d.ts +69 -0
- package/dist/components/activity/Activity.js +279 -0
- package/dist/components/activity/ActivityBadge.d.ts +16 -0
- package/dist/components/activity/ActivityBadge.js +29 -0
- package/dist/components/{chatter → activity}/Messages.d.ts +2 -2
- package/dist/components/{chatter → activity}/Messages.js +9 -6
- package/dist/components/activity/index.d.ts +4 -0
- package/dist/components/activity/index.js +5 -0
- package/dist/components/assist/AssistMark.d.ts +3 -3
- package/dist/components/assist/AssistMark.js +1 -1
- package/dist/components/assist/assist.d.ts +1 -1
- package/dist/components/assist/assist.js +1 -1
- package/dist/components/calendar/RecordCalendar.d.ts +48 -0
- package/dist/components/calendar/RecordCalendar.js +101 -0
- package/dist/components/calendar/index.d.ts +1 -0
- package/dist/components/calendar/index.js +1 -0
- package/dist/components/form/Sheet.d.ts +4 -0
- package/dist/components/form/Sheet.js +16 -5
- package/dist/components/gantt/RecordGantt.d.ts +44 -0
- package/dist/components/gantt/RecordGantt.js +110 -0
- package/dist/components/gantt/index.d.ts +1 -0
- package/dist/components/gantt/index.js +1 -0
- package/dist/components/index.d.ts +4 -1
- package/dist/components/index.js +4 -1
- package/dist/components/lines/LineItems.d.ts +45 -5
- package/dist/components/lines/LineItems.js +106 -18
- package/dist/components/listing/BoardColumns.d.ts +39 -0
- package/dist/components/listing/BoardColumns.js +138 -0
- package/dist/components/listing/Listing.js +1 -1
- package/dist/components/listing/ListingBar.d.ts +46 -2
- package/dist/components/listing/ListingBar.js +47 -5
- package/dist/components/listing/PivotExplorer.d.ts +34 -0
- package/dist/components/listing/PivotExplorer.js +86 -0
- package/dist/components/listing/RecordCard.d.ts +6 -8
- package/dist/components/listing/RecordCard.js +13 -7
- package/dist/components/listing/Row.d.ts +25 -2
- package/dist/components/listing/Row.js +30 -13
- package/dist/components/listing/index.d.ts +2 -0
- package/dist/components/listing/index.js +2 -0
- package/dist/components/listing/set.d.ts +1 -1
- package/dist/components/navigation/index.d.ts +0 -2
- package/dist/components/navigation/index.js +2 -3
- package/dist/components/record/RecordAudit.d.ts +17 -0
- package/dist/components/record/RecordAudit.js +27 -0
- package/dist/components/record/RecordHeader.d.ts +27 -17
- package/dist/components/record/RecordHeader.js +64 -20
- package/dist/components/record/RecordNotices.d.ts +37 -0
- package/dist/components/record/RecordNotices.js +75 -0
- package/dist/components/record/RecordRibbon.d.ts +13 -0
- package/dist/components/record/RecordRibbon.js +22 -0
- package/dist/components/record/ThumbBar.js +1 -1
- package/dist/components/record/index.d.ts +2 -0
- package/dist/components/record/index.js +2 -0
- package/dist/components/search/CriteriaChips.d.ts +6 -1
- package/dist/components/search/CriteriaChips.js +71 -20
- package/dist/components/search/SearchBar.d.ts +4 -1
- package/dist/components/search/SearchBar.js +15 -7
- package/dist/components/timeline/RecordTimeline.d.ts +60 -0
- package/dist/components/timeline/RecordTimeline.js +214 -0
- package/dist/components/timeline/TimelineToolbar.d.ts +30 -0
- package/dist/components/timeline/TimelineToolbar.js +31 -0
- package/dist/components/timeline/index.d.ts +3 -0
- package/dist/components/timeline/index.js +3 -0
- package/dist/components/timeline/time.d.ts +43 -0
- package/dist/components/timeline/time.js +135 -0
- package/dist/foundations/copy.d.ts +194 -18
- package/dist/foundations/copy.es.js +142 -15
- package/dist/foundations/copy.js +133 -11
- package/dist/foundations/index.d.ts +1 -1
- package/dist/foundations/match.d.ts +23 -0
- package/dist/foundations/match.js +63 -0
- package/dist/foundations/slots.d.ts +1 -1
- package/dist/foundations/slots.js +8 -3
- package/dist/foundations/types.d.ts +7 -0
- package/dist/foundations/types.js +0 -36
- package/dist/primitives/block/index.d.ts +1 -1
- package/dist/primitives/block/index.js +1 -1
- package/dist/primitives/cell/CellStack.d.ts +7 -1
- package/dist/primitives/cell/CellStack.js +5 -2
- package/dist/primitives/choice/ComboBox.d.ts +3 -0
- package/dist/primitives/choice/ComboBox.js +10 -6
- package/dist/primitives/choice/KanbanState.d.ts +14 -0
- package/dist/primitives/choice/KanbanState.js +37 -0
- package/dist/primitives/choice/Priority.d.ts +20 -0
- package/dist/primitives/choice/Priority.js +41 -0
- package/dist/primitives/choice/SearchField.d.ts +10 -1
- package/dist/primitives/choice/SearchField.js +21 -2
- package/dist/primitives/choice/Slider.d.ts +6 -1
- package/dist/primitives/choice/Slider.js +2 -2
- package/dist/primitives/choice/index.d.ts +2 -0
- package/dist/primitives/choice/index.js +2 -0
- package/dist/primitives/date/Calendar.js +30 -6
- package/dist/primitives/date/DatePicker.d.ts +11 -3
- package/dist/primitives/date/DatePicker.js +12 -8
- package/dist/primitives/date/DateTime.d.ts +35 -0
- package/dist/primitives/date/DateTime.js +100 -0
- package/dist/primitives/date/index.d.ts +1 -0
- package/dist/primitives/date/index.js +4 -2
- package/dist/primitives/dialog/Dialog.d.ts +9 -3
- package/dist/primitives/dialog/Dialog.js +41 -5
- package/dist/primitives/field/DropZone.js +1 -1
- package/dist/primitives/field/Field.d.ts +14 -2
- package/dist/primitives/field/Field.js +25 -14
- package/dist/primitives/field/copy.d.ts +16 -0
- package/dist/primitives/field/copy.js +69 -0
- package/dist/primitives/field/index.d.ts +2 -0
- package/dist/primitives/field/index.js +4 -0
- package/dist/primitives/field/note.d.ts +55 -0
- package/dist/primitives/field/note.js +117 -0
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/menu/Menu.d.ts +28 -0
- package/dist/primitives/menu/Menu.js +47 -17
- package/dist/primitives/notice/Callout.js +1 -1
- package/dist/primitives/notice/Flag.d.ts +13 -0
- package/dist/primitives/notice/Flag.js +31 -0
- package/dist/primitives/notice/index.d.ts +2 -1
- package/dist/primitives/notice/index.js +2 -1
- package/dist/primitives/overlay/Preview.d.ts +19 -0
- package/dist/primitives/overlay/Preview.js +69 -0
- package/dist/primitives/overlay/index.d.ts +1 -0
- package/dist/primitives/overlay/index.js +3 -1
- package/dist/primitives/picker/MatchedText.d.ts +10 -0
- package/dist/primitives/picker/MatchedText.js +12 -0
- package/dist/primitives/picker/Picker.d.ts +67 -4
- package/dist/primitives/picker/Picker.js +142 -23
- package/dist/primitives/table/CellCopy.d.ts +20 -0
- package/dist/primitives/table/CellCopy.js +23 -0
- package/dist/primitives/table/DataTable.d.ts +55 -1
- package/dist/primitives/table/DataTable.js +126 -40
- package/dist/primitives/table/EditableTable.d.ts +102 -2
- package/dist/primitives/table/EditableTable.js +287 -32
- package/dist/primitives/table/ReportTable.js +2 -1
- package/dist/primitives/table/RowActions.js +7 -2
- package/dist/primitives/table/SheetTable.d.ts +4 -1
- package/dist/primitives/table/SheetTable.js +38 -5
- package/dist/primitives/table/Table.d.ts +5 -0
- package/dist/primitives/table/Table.js +17 -8
- package/dist/primitives/table/index.d.ts +2 -2
- package/dist/primitives/table/index.js +1 -1
- package/dist/primitives/tabs/Tabs.d.ts +2 -2
- package/dist/primitives/tabs/Tabs.js +1 -1
- package/dist/primitives/tag/Tags.d.ts +2 -0
- package/dist/primitives/tag/Tags.js +2 -1
- package/dist/primitives/tag/TagsField.d.ts +13 -0
- package/dist/primitives/tag/TagsField.js +39 -10
- package/dist/primitives/tag/choices.d.ts +2 -0
- package/dist/primitives/tree/Tree.d.ts +56 -0
- package/dist/primitives/tree/Tree.js +74 -0
- package/dist/primitives/tree/index.d.ts +1 -0
- package/dist/primitives/tree/index.js +3 -0
- package/dist/primitives/value/Avatar.d.ts +16 -0
- package/dist/primitives/value/Avatar.js +91 -0
- package/dist/primitives/value/Copyable.d.ts +11 -0
- package/dist/primitives/value/Copyable.js +27 -6
- package/dist/primitives/value/Fecha.js +10 -1
- package/dist/primitives/value/Person.d.ts +6 -51
- package/dist/primitives/value/Person.js +9 -148
- package/dist/primitives/value/ProgressBar.d.ts +31 -0
- package/dist/primitives/value/ProgressBar.js +53 -0
- package/dist/primitives/value/Relation.d.ts +22 -5
- package/dist/primitives/value/Relation.js +42 -14
- package/dist/primitives/value/Totals.d.ts +102 -7
- package/dist/primitives/value/Totals.js +52 -2
- package/dist/primitives/value/Value.d.ts +0 -37
- package/dist/primitives/value/Value.js +0 -39
- package/dist/primitives/value/contact.d.ts +30 -0
- package/dist/primitives/value/contact.js +51 -0
- package/dist/primitives/value/index.d.ts +5 -4
- package/dist/primitives/value/index.js +5 -4
- package/dist/tokens.css +179 -7
- package/dist/views/list/List.d.ts +32 -3
- package/dist/views/list/List.js +51 -7
- package/dist/views/list/ListView.d.ts +1 -1
- package/dist/views/list/ListView.js +70 -13
- package/dist/views/list/columns.js +8 -1
- package/dist/views/list/derive.d.ts +12 -1
- package/dist/views/list/derive.js +29 -3
- package/dist/views/list/presentations.d.ts +5 -2
- package/dist/views/list/presentations.js +35 -3
- package/dist/views/list/types.d.ts +57 -1
- package/dist/views/list/useNarrowList.d.ts +10 -0
- package/dist/views/list/useNarrowList.js +20 -6
- package/dist/views/record/Record.d.ts +41 -8
- package/dist/views/record/Record.js +113 -28
- package/dist/views/record/index.d.ts +1 -1
- package/package.json +15 -4
- package/dist/components/chatter/Chatter.d.ts +0 -161
- package/dist/components/chatter/Chatter.js +0 -232
- package/dist/components/chatter/index.d.ts +0 -3
- package/dist/components/chatter/index.js +0 -4
- package/dist/components/navigation/CommandPalette.d.ts +0 -40
- package/dist/components/navigation/CommandPalette.js +0 -114
- package/dist/components/navigation/commands.d.ts +0 -30
- package/dist/components/navigation/commands.js +0 -30
- /package/dist/components/{chatter → activity}/Timeline.d.ts +0 -0
- /package/dist/components/{chatter → activity}/Timeline.js +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { Breadcrumb } from "./Breadcrumb.js";
|
|
4
4
|
import { DirtyActions, } from "./DirtyActions.js";
|
|
5
5
|
import { isPrimary, ThumbBar, thumbPlan } from "./ThumbBar.js";
|
|
@@ -7,8 +7,7 @@ import { byDensity, useDensity } from "../../foundations/density.js";
|
|
|
7
7
|
import { SurfaceProvider } from "../../foundations/surface.js";
|
|
8
8
|
import { Line } from "../../primitives/block/Line.js";
|
|
9
9
|
import { Tag } from "../../primitives/tag/Tag.js";
|
|
10
|
-
|
|
11
|
-
export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Icon, title, subtitle, tag, figure, railSurface = "canvas", smart, aspects, dirty, chatter, chatterLayout = "page", children, }) {
|
|
10
|
+
export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Icon, title, subtitle, tag, figure, railSurface = "canvas", smart, aspects, dirty, activity, activityLayout = "auto", width = "reading", ribbon, blocking, indicator, children, }) {
|
|
12
11
|
const d = useDensity();
|
|
13
12
|
const hasRail = crumbs !== undefined ||
|
|
14
13
|
pager !== undefined ||
|
|
@@ -24,6 +23,8 @@ export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Ico
|
|
|
24
23
|
only thing in the rail that belongs to the RECORD and not to my position in
|
|
25
24
|
front of it. */
|
|
26
25
|
const withRail = hasRail && d === "comfortable";
|
|
26
|
+
/* The activity column at the RIGHT of the sheet when the page is wide (THE PAGE). */
|
|
27
|
+
const beside = activity !== undefined && activityLayout === "auto";
|
|
27
28
|
/* The thumb bar's content (see THE THUMB BAR in `ThumbBar`): dirty first, else the primary. */
|
|
28
29
|
const { rail: railActions, primaries, mode: thumb, } = thumbPlan({ withRail, actions, dirty });
|
|
29
30
|
/* In compact the TITLE is not drawn either, for the same reason as in
|
|
@@ -45,7 +46,15 @@ export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Ico
|
|
|
45
46
|
pager !== undefined ||
|
|
46
47
|
actions !== undefined ||
|
|
47
48
|
dirty !== undefined);
|
|
48
|
-
|
|
49
|
+
/* ONE column for the rail and the page: the rail used to span the screen while the sheet
|
|
50
|
+
stopped at 72rem, so on a wide desk the buttons sat 250 px left of the paper they act on
|
|
51
|
+
and the record read as two things. */
|
|
52
|
+
const column = width === "full"
|
|
53
|
+
? ""
|
|
54
|
+
: beside
|
|
55
|
+
? "mx-auto w-full max-w-(--container-record)"
|
|
56
|
+
: "mx-auto w-full max-w-(--container-sheet)";
|
|
57
|
+
const identity = (_jsxs("div", { className: `flex flex-col ${byDensity(d, "gap-2.5 px-4 pt-3.5", "gap-2 px-0 pt-0")}`, children: [withRail || (smart === undefined && indicator === undefined) ? null : (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [smart, indicator] })), _jsxs("div", { className: "flex min-w-0 flex-wrap items-start gap-x-5 gap-y-2", children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2.5", children: [Icon === undefined || d === "compact" ? null : (_jsx("span", { className: "mt-1 flex size-9 shrink-0 items-center justify-center rounded-box bg-ink/5 text-muted", children: _jsx(Icon, { className: "size-(--size-icon-lg)", strokeWidth: 1.75 }) })), _jsxs("div", { className: "flex min-w-0 flex-col", children: [kicker === undefined || !withIdentity ? null : (_jsx("span", { className: "text-micro font-medium text-muted", children: kicker })), _jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1", children: [!withIdentity ? null : (
|
|
49
58
|
/* `display`, not `title`: the record's name is the entry point of the
|
|
50
59
|
screen. At the title size (19 px over a 13 px body, 1.46:1) nothing on
|
|
51
60
|
the sheet stood out and the eye had nowhere to start. */
|
|
@@ -53,20 +62,55 @@ export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Ico
|
|
|
53
62
|
/* The line is the sheet's mouth. In comfortable it walks to the
|
|
54
63
|
edges; the current tab sits on it and opens into the page below. */
|
|
55
64
|
_jsx("div", { className: `border-b border-line-strong ${byDensity(d, "-mx-4 px-4", "")}`, children: aspects }))] }));
|
|
56
|
-
return (_jsxs(_Fragment, { children: [withRail ? (_jsx(SurfaceProvider, { value: railSurface, children: _jsx("div", { className: "@container/record", children:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
return (_jsxs(_Fragment, { children: [withRail ? (_jsx(SurfaceProvider, { value: railSurface, children: _jsx("div", { className: "@container/record", children: _jsx("div", { className: `px-3 py-2.5 ${railSurface === "chrome" ? "bg-chrome" : "border-b border-line bg-surface"}`, children: _jsxs("div", { className: `flex flex-col gap-2 ${paper ? column : ""}`, children: [_jsx(Line, { start: crumbs === undefined || crumbs.length === 0 ? null : (_jsx(Breadcrumb, { crumbs: crumbs, surface: railSurface })), center:
|
|
66
|
+
/* The notices' indicator rides with the counters: both are about the
|
|
67
|
+
record and both open something; neither is my position in front of
|
|
68
|
+
it. The line wraps on a phone, so it is never pushed out of reach. */
|
|
69
|
+
smart === undefined && indicator === undefined ? undefined : (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center justify-center gap-2", children: [smart, indicator] })), end: pager }), actions === undefined &&
|
|
70
|
+
status === undefined &&
|
|
71
|
+
dirty === undefined ? null : (
|
|
72
|
+
/* Row 2 — WHAT I CAN DO and WHERE IT IS AT: actions stuck to the
|
|
73
|
+
left edge, the history and the state stuck to the right one. No
|
|
74
|
+
`flex-wrap`: wrapping split the row in two and the state ended up
|
|
75
|
+
below the actions, which is another hierarchy. */
|
|
76
|
+
_jsxs("div", { className: "flex min-w-0 items-start gap-x-4", children: [dirty !== undefined ? (_jsx("div", { className: "flex shrink-0 items-center gap-1.5 @max-handheld/record:hidden", children: _jsx(DirtyActions, { ...dirty }) })) : actions === undefined ? null : (_jsx("div", { className: "flex shrink-0 items-center gap-1.5", children: primaries.length === 0
|
|
77
|
+
? actions
|
|
78
|
+
: railActions.map((node) => isPrimary(node) ? (_jsx("span", { "data-thumb-moves": "", className: "contents @max-handheld/record:hidden", children: node }, node.key)) : (node)) })), status === undefined ? null : (_jsx("div", { className: "ml-auto flex min-w-0 flex-col items-end gap-1.5", children: status }))] }))] }) }) }) })) : null, paper ? (
|
|
79
|
+
/* ==================================================================== *
|
|
80
|
+
* THE PAGE — [sheet | Actividad], on the ground.
|
|
81
|
+
*
|
|
82
|
+
* Two columns, joined, placed together and centred: the sheet
|
|
83
|
+
* (≤ --container-sheet, 72rem) and, at its RIGHT, the record's ACTIVITY
|
|
84
|
+
* (--container-activity, 22rem) — no strip of ground between them, so the page
|
|
85
|
+
* reads left to right as one paper: the record, then what it has. The pair is at
|
|
86
|
+
* most --container-record (94rem) wide; on a 1680 px desk both fit whole.
|
|
87
|
+
*
|
|
88
|
+
* A note of a field or a row is not drawn in the column: its mark (`Flag`) stays
|
|
89
|
+
* by its label or at its row — WHERE — and the activity lists it with the record's
|
|
90
|
+
* notices above the timeline — WHAT —, taking you to it on press. Cards aligned in
|
|
91
|
+
* the column were tried and dropped: they and the history fought for one column,
|
|
92
|
+
* and the history is what everybody reads.
|
|
93
|
+
*
|
|
94
|
+
* As the page narrows: below --container-beside (76rem) the activity goes BELOW
|
|
95
|
+
* the sheet as a card at the sheet's width.
|
|
96
|
+
*
|
|
97
|
+
* Folded, the column is a slim strip with its counts; the pair re-centres and the
|
|
98
|
+
* sheet keeps its width. A container query, not the window: with four panels open
|
|
99
|
+
* each is 380 px on a 1,600 screen.
|
|
100
|
+
*
|
|
101
|
+
* The sheet clips with `overflow-clip`, not `overflow-hidden`: hidden would make it
|
|
102
|
+
* a scroll container and the line table header would stick inside it, not under
|
|
103
|
+
* the application bar.
|
|
104
|
+
* ==================================================================== */
|
|
105
|
+
_jsx("div", { className: "bg-ground px-3 pt-3 pb-3", children: _jsx("div", { className: "@container/page", children: _jsxs("div", { className: `flex min-w-0 flex-col gap-3 ${column} ${beside
|
|
106
|
+
? "@beside/page:flex-row @beside/page:items-stretch @beside/page:justify-center @beside/page:gap-0"
|
|
107
|
+
: ""}`, children: [_jsxs("div", { className: `flex min-w-0 flex-1 flex-col gap-3 ${beside && width === "reading"
|
|
108
|
+
? "mx-auto w-full max-w-(--container-sheet) @beside/page:mx-0"
|
|
109
|
+
: ""}`, children: [blocking, _jsxs("div", { className: `relative flex min-w-0 flex-col overflow-clip rounded-box border border-line bg-surface ${beside ? "@beside/page:rounded-r-none" : ""}`, children: [ribbon, identity, children] }), activity !== undefined && activityLayout === "below" ? activity : null] }), beside ? (
|
|
110
|
+
/* After the sheet: the record first, what is around it second — in the
|
|
111
|
+
reading order and on screen. */
|
|
112
|
+
_jsx("div", { className: "mx-auto w-full max-w-(--container-sheet) min-w-0 @beside/page:mx-0 @beside/page:flex @beside/page:w-auto @beside/page:max-w-none @beside/page:shrink-0", children: activity })) : null] }) }) })) : (
|
|
113
|
+
/* No paper (a modal, a drawer): one surface, no ground beside
|
|
114
|
+
it — the band first, the record, and the activity below. */
|
|
115
|
+
_jsxs("div", { className: "relative flex min-w-0 flex-1 flex-col overflow-clip bg-surface", children: [ribbon, blocking === undefined ? null : (_jsx("div", { className: byDensity(d, "px-4 pt-3.5", "pb-2"), children: blocking })), identity, children, activity === undefined ? null : (_jsx("div", { className: byDensity(d, "px-4 pb-4", "pb-2"), children: activity }))] })), thumb === null ? null : _jsx(ThumbBar, { mode: thumb, dirty: dirty, primaries: primaries })] }));
|
|
72
116
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type CalloutProps } from "../../primitives/notice/Callout.js";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
export type RecordNoticeSpec = Omit<CalloutProps, "dismissLabel">;
|
|
4
|
+
/** The notices in the order they are drawn: by urgency, written order inside a tone. */
|
|
5
|
+
export declare function orderNotices<T extends {
|
|
6
|
+
tone?: CalloutProps["tone"];
|
|
7
|
+
}>(notices: readonly T[]): T[];
|
|
8
|
+
/** Blocking (`bad`) and the rest, each already in urgency order. */
|
|
9
|
+
export declare function splitNotices(notices: readonly RecordNoticeSpec[]): {
|
|
10
|
+
blocking: RecordNoticeSpec[];
|
|
11
|
+
rest: RecordNoticeSpec[];
|
|
12
|
+
};
|
|
13
|
+
/** The band of what is wrong: the operation error first, then the `bad` notices. */
|
|
14
|
+
export declare function RecordBlocking({ notices, error, errorHeading, }: {
|
|
15
|
+
notices: readonly RecordNoticeSpec[];
|
|
16
|
+
/** The sentence the backend wrote about the last operation. Drawn first, whole. */
|
|
17
|
+
error?: string | null;
|
|
18
|
+
errorHeading?: string;
|
|
19
|
+
}): ReactNode;
|
|
20
|
+
/** The indicator in the rail: the count of the record's notices, toned by the most urgent,
|
|
21
|
+
* that opens their list on press. Nothing is drawn without notices. */
|
|
22
|
+
export declare function RecordNoticeIndicator({ notices, anchored, reveal, }: {
|
|
23
|
+
/** Not blocking, already in urgency order. */
|
|
24
|
+
notices: readonly RecordNoticeSpec[];
|
|
25
|
+
/** The notes of fields and rows: counted too when there is a panel that lists them —
|
|
26
|
+
* without one they are only the marks by their fields, and the popover leaves them out. */
|
|
27
|
+
anchored?: readonly {
|
|
28
|
+
tone?: "bad" | "warn" | "info" | "good";
|
|
29
|
+
}[];
|
|
30
|
+
/** The record has an ACTIVITY panel, and the notices live there: the indicator goes to
|
|
31
|
+
* them (opens the panel, scrolls to its notices, moves the focus) instead of floating
|
|
32
|
+
* a second copy in a popover. `target` is the id of that section. */
|
|
33
|
+
reveal?: {
|
|
34
|
+
target: string;
|
|
35
|
+
onReveal: () => void;
|
|
36
|
+
};
|
|
37
|
+
}): ReactNode;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { useSurface } from "../../foundations/surface.js";
|
|
5
|
+
import { focusRing, tv } from "../../foundations/variants.js";
|
|
6
|
+
import { Callout } from "../../primitives/notice/Callout.js";
|
|
7
|
+
import { Failed } from "../../primitives/notice/Notice.js";
|
|
8
|
+
import { Popover } from "../../primitives/overlay/Popover.js";
|
|
9
|
+
import { tagTone } from "../../primitives/tag/Tag.js";
|
|
10
|
+
import { CircleCheck, Info, TriangleAlert } from "lucide-react";
|
|
11
|
+
import { useState } from "react";
|
|
12
|
+
import { Button, Dialog, DialogTrigger, Heading } from "react-aria-components";
|
|
13
|
+
const URGENCY = {
|
|
14
|
+
bad: 0,
|
|
15
|
+
warn: 1,
|
|
16
|
+
info: 2,
|
|
17
|
+
good: 3,
|
|
18
|
+
};
|
|
19
|
+
/** The notices in the order they are drawn: by urgency, written order inside a tone. */
|
|
20
|
+
export function orderNotices(notices) {
|
|
21
|
+
return notices
|
|
22
|
+
.map((notice, at) => ({ notice, at }))
|
|
23
|
+
.sort((a, b) => URGENCY[a.notice.tone ?? "info"] - URGENCY[b.notice.tone ?? "info"] || a.at - b.at)
|
|
24
|
+
.map(({ notice }) => notice);
|
|
25
|
+
}
|
|
26
|
+
/** Blocking (`bad`) and the rest, each already in urgency order. */
|
|
27
|
+
export function splitNotices(notices) {
|
|
28
|
+
const ordered = orderNotices(notices);
|
|
29
|
+
return {
|
|
30
|
+
blocking: ordered.filter((n) => n.tone === "bad"),
|
|
31
|
+
rest: ordered.filter((n) => n.tone !== "bad"),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** The band of what is wrong: the operation error first, then the `bad` notices. */
|
|
35
|
+
export function RecordBlocking({ notices, error, errorHeading, }) {
|
|
36
|
+
const copy = useUiCopy();
|
|
37
|
+
const hasError = error !== undefined && error !== null;
|
|
38
|
+
if (notices.length === 0 && !hasError)
|
|
39
|
+
return null;
|
|
40
|
+
return (_jsxs("div", { role: "group", "aria-label": copy.record.blocking, "data-record-blocking": "", className: "flex min-w-0 flex-col gap-1.5", children: [hasError ? (_jsx(Failed, { heading: errorHeading ?? copy.failed.operation, sentence: error })) : null, notices.map(({ children, ...notice }, i) => (_jsx(Callout, { ...notice, dismissLabel: copy.shell.closeNotice, children: children }, i)))] }));
|
|
41
|
+
}
|
|
42
|
+
const ICON = {
|
|
43
|
+
warn: TriangleAlert,
|
|
44
|
+
info: Info,
|
|
45
|
+
good: CircleCheck,
|
|
46
|
+
};
|
|
47
|
+
const chip = tv({
|
|
48
|
+
extend: focusRing,
|
|
49
|
+
base: "inline-flex h-(--size-control-sm) shrink-0 cursor-default items-center gap-1.5 rounded-pill px-2.5 text-micro font-medium whitespace-nowrap transition-[filter] duration-(--duration-instant) hover:brightness-95 aria-expanded:brightness-95",
|
|
50
|
+
});
|
|
51
|
+
/** The indicator in the rail: the count of the record's notices, toned by the most urgent,
|
|
52
|
+
* that opens their list on press. Nothing is drawn without notices. */
|
|
53
|
+
export function RecordNoticeIndicator({ notices, anchored = [], reveal, }) {
|
|
54
|
+
const copy = useUiCopy();
|
|
55
|
+
const surface = useSurface();
|
|
56
|
+
const [open, setOpen] = useState(false);
|
|
57
|
+
const all = reveal === undefined ? notices : [...notices, ...anchored];
|
|
58
|
+
if (all.length === 0)
|
|
59
|
+
return null;
|
|
60
|
+
/* The most urgent of all of them gives the chip its tone; a field's `bad` note warns —
|
|
61
|
+
what blocks the record is the blocking band, not this chip. */
|
|
62
|
+
const order = ["bad", "warn", "info", "good"];
|
|
63
|
+
const worst = order.find((t) => all.some((n) => (n.tone ?? "info") === t)) ?? "info";
|
|
64
|
+
const tone = (worst === "bad" ? "warn" : worst);
|
|
65
|
+
const Icon = ICON[tone];
|
|
66
|
+
const count = fillCopy(all.length === 1 ? copy.record.noticeOne : copy.record.noticeMany, {
|
|
67
|
+
count: all.length.toLocaleString(copy.locale),
|
|
68
|
+
});
|
|
69
|
+
const face = (_jsxs(_Fragment, { children: [_jsx(Icon, { className: "size-(--size-icon-sm) shrink-0", strokeWidth: 2.25, "aria-hidden": true }), count] }));
|
|
70
|
+
const skin = tagTone(tone, surface === "chrome" ? "chrome" : "canvas");
|
|
71
|
+
if (reveal !== undefined) {
|
|
72
|
+
return (_jsx(Button, { "data-record-notices": "", "aria-controls": reveal.target, onPress: reveal.onReveal, className: ({ isFocusVisible }) => `${chip({ isFocusVisible })} ${skin}`, children: face }));
|
|
73
|
+
}
|
|
74
|
+
return (_jsxs(DialogTrigger, { isOpen: open, onOpenChange: setOpen, children: [_jsx(Button, { "data-record-notices": "", className: ({ isFocusVisible }) => `${chip({ isFocusVisible })} ${skin}`, children: face }), _jsx(Popover, { placement: "bottom", children: _jsxs(Dialog, { "aria-label": copy.record.notices, className: "flex max-h-[inherit] w-(--container-toast) flex-col gap-2 overflow-auto p-3 outline-0 max-sm:w-auto", children: [_jsx(Heading, { slot: "title", className: "text-meta font-semibold text-ink", children: copy.record.notices }), notices.map(({ children, ...notice }, i) => (_jsx(Callout, { ...notice, dismissLabel: copy.shell.closeNotice, children: children }, i)))] }) })] }));
|
|
75
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Tone } from "../../foundations/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The band across the paper's corner: «PAGADA», «ARCHIVADA», «CANCELADA».
|
|
4
|
+
*
|
|
5
|
+
* Context: a state that changes what the record IS — not where it is in its flow, which the
|
|
6
|
+
* status bar says — has to be seen before any field is read: an archived customer looks
|
|
7
|
+
* exactly like an active one otherwise. Odoo's `web_ribbon`. It is a word, never colour
|
|
8
|
+
* alone, and it is `role="status"` so it is also heard. The paper clips it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function RecordRibbon({ label, tone }: {
|
|
11
|
+
label: string;
|
|
12
|
+
tone?: Tone;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/* The solid ribbon per tone. The text is the surface colour: on the light theme white on the
|
|
3
|
+
tone (≥ 5.7:1 for all five), on the dark one the dark surface on the lighter tone. */
|
|
4
|
+
const RIBBON = {
|
|
5
|
+
neutral: "bg-muted text-surface",
|
|
6
|
+
info: "bg-info text-surface",
|
|
7
|
+
good: "bg-good text-surface",
|
|
8
|
+
warn: "bg-warn text-surface",
|
|
9
|
+
bad: "bg-bad text-surface",
|
|
10
|
+
accent: "bg-accent text-on-accent",
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The band across the paper's corner: «PAGADA», «ARCHIVADA», «CANCELADA».
|
|
14
|
+
*
|
|
15
|
+
* Context: a state that changes what the record IS — not where it is in its flow, which the
|
|
16
|
+
* status bar says — has to be seen before any field is read: an archived customer looks
|
|
17
|
+
* exactly like an active one otherwise. Odoo's `web_ribbon`. It is a word, never colour
|
|
18
|
+
* alone, and it is `role="status"` so it is also heard. The paper clips it.
|
|
19
|
+
*/
|
|
20
|
+
export function RecordRibbon({ label, tone = "neutral" }) {
|
|
21
|
+
return (_jsx("span", { role: "status", className: `pointer-events-none absolute top-5 -right-12 z-10 w-44 rotate-45 py-1 text-center text-micro font-semibold tracking-wide uppercase shadow-float ${RIBBON[tone]}`, children: label }));
|
|
22
|
+
}
|
|
@@ -38,7 +38,7 @@ import { cloneElement, isValidElement } from "react";
|
|
|
38
38
|
* Same reason as everything else here: a record in a 380 px panel is as
|
|
39
39
|
* narrow as one on a phone. The query is a NAMED container (`record`) on the
|
|
40
40
|
* rail and on the bar — never on the paper: an unnamed container around the
|
|
41
|
-
* sheet would change what `Sheet`, `SectionGrid` and the
|
|
41
|
+
* sheet would change what `Sheet`, `SectionGrid` and the activity panel measure
|
|
42
42
|
* against, and the desk layout with it.
|
|
43
43
|
*
|
|
44
44
|
* ── Why CSS, and why the primary is in the DOM twice ─────────────────
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { Breadcrumb, type Crumb } from "./Breadcrumb.js";
|
|
2
2
|
export { DirtyActions, type DirtyState, } from "./DirtyActions.js";
|
|
3
3
|
export { Pager } from "./Pager.js";
|
|
4
|
+
export { RecordAudit, type RecordStamp } from "./RecordAudit.js";
|
|
5
|
+
export { RecordRibbon } from "./RecordRibbon.js";
|
|
4
6
|
export { RecordSkeleton } from "./RecordSkeleton.js";
|
|
5
7
|
export { SmartButton, SmartButtons } from "./SmartButtons.js";
|
|
6
8
|
export { type Stage, StatusBar } from "./StatusBar.js";
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
export { Breadcrumb } from "./Breadcrumb.js";
|
|
4
4
|
export { DirtyActions, } from "./DirtyActions.js";
|
|
5
5
|
export { Pager } from "./Pager.js";
|
|
6
|
+
export { RecordAudit } from "./RecordAudit.js";
|
|
7
|
+
export { RecordRibbon } from "./RecordRibbon.js";
|
|
6
8
|
export { RecordSkeleton } from "./RecordSkeleton.js";
|
|
7
9
|
export { SmartButton, SmartButtons } from "./SmartButtons.js";
|
|
8
10
|
export { StatusBar } from "./StatusBar.js";
|
|
@@ -9,6 +9,8 @@ export type CriteriaChipsProps = {
|
|
|
9
9
|
onJoin?: (path: CriteriaPath, join: CriteriaJoin) => void;
|
|
10
10
|
/** Sacar una instancia, o una rama entera. */
|
|
11
11
|
onRemove?: (path: CriteriaPath) => void;
|
|
12
|
+
/** Sacar varias de una vez: the × of the grouping chain drops every level. */
|
|
13
|
+
onRemoveMany?: (paths: readonly CriteriaPath[]) => void;
|
|
12
14
|
/**
|
|
13
15
|
* Cuál nodo está ABIERTO: dónde va a caer lo próximo que se escriba.
|
|
14
16
|
*
|
|
@@ -17,6 +19,9 @@ export type CriteriaChipsProps = {
|
|
|
17
19
|
* único que sabía dónde era el que la escribió.
|
|
18
20
|
*/
|
|
19
21
|
open?: CriteriaPath;
|
|
22
|
+
/** How many facets show before the rest fold into «+N» (the grouping chain counts as one).
|
|
23
|
+
* Below `@md` of the bar only the first one shows. `Infinity` never folds. */
|
|
24
|
+
collapseAfter?: number;
|
|
20
25
|
surface?: Surface;
|
|
21
26
|
};
|
|
22
|
-
export declare function CriteriaChips({ criteria, onOpen, onJoin, onRemove, open, surface, }: CriteriaChipsProps): import("react").JSX.Element;
|
|
27
|
+
export declare function CriteriaChips({ criteria, onOpen, onJoin, onRemove, onRemoveMany, open, collapseAfter, surface, }: CriteriaChipsProps): import("react").JSX.Element;
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx,
|
|
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,
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/*
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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:
|
|
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: [
|
|
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")}` })] }),
|
|
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;
|