@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
package/README.md CHANGED
@@ -143,8 +143,8 @@ theme switch, or open a story with `&globals=theme:dark`.
143
143
  ```bash
144
144
  make init # pre-commit + yarn install
145
145
  make dev # Storybook on :6006 — the living documentation
146
- make test # node --test: behaviour, tokens, contrast, client boundaries
147
- make smoke # every story opened in headless Chrome: no console errors, no blanks
146
+ make test # vitest: logic in Node + every story in Chrome (play, a11y), light and dark
147
+ make smoke # only the stories: their play, a11y and screen rules, light and dark
148
148
  make build # dist/ (JS + .d.ts + tokens.css)
149
149
  ```
150
150
 
@@ -160,7 +160,7 @@ src/
160
160
  ├── components/ one folder per family: listing, record, search, form, activity, frame, shell
161
161
  └── views/ list (N records) · record (one record)
162
162
  stories/ Storybook, by level — and templates/: whole pages built from the public API
163
- tests/ node --test over the TypeScript sources
163
+ tests/ vitest, `model` project: logic and static markup in Node
164
164
  ```
165
165
 
166
166
  `make doctor` rejects an import that climbs a level (a primitive importing a component, a
@@ -0,0 +1,16 @@
1
+ import type { ReactNode } from "react";
2
+ export type ActivityState = "overdue" | "today" | "planned" | "none";
3
+ /**
4
+ * The record's next planned activity, on its card or its row: a clock in the tone of how late
5
+ * it is — overdue, today, planned — and the word with it.
6
+ *
7
+ * Context: in a pipeline the question is not «what stage» but «who is waiting on us»; Odoo
8
+ * answers it with this clock on every card. A tone alone says nothing to a screen reader or to
9
+ * whoever does not see red, so the state is the name, and `detail` («Llamar · mañana 10:00») is
10
+ * the title. With `onClick` it opens the activities (a popover, the record's activity panel).
11
+ */
12
+ export declare function ActivityBadge({ state, detail, onClick, }: {
13
+ state: ActivityState;
14
+ detail?: ReactNode;
15
+ onClick?: () => void;
16
+ }): import("react").JSX.Element;
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useUiCopy } from "../../foundations/copy.js";
4
+ import { Clock } from "lucide-react";
5
+ const TONE = {
6
+ overdue: "text-bad",
7
+ today: "text-warn",
8
+ planned: "text-good",
9
+ none: "text-muted",
10
+ };
11
+ /**
12
+ * The record's next planned activity, on its card or its row: a clock in the tone of how late
13
+ * it is — overdue, today, planned — and the word with it.
14
+ *
15
+ * Context: in a pipeline the question is not «what stage» but «who is waiting on us»; Odoo
16
+ * answers it with this clock on every card. A tone alone says nothing to a screen reader or to
17
+ * whoever does not see red, so the state is the name, and `detail` («Llamar · mañana 10:00») is
18
+ * the title. With `onClick` it opens the activities (a popover, the record's activity panel).
19
+ */
20
+ export function ActivityBadge({ state, detail, onClick, }) {
21
+ const copy = useUiCopy();
22
+ const word = copy.activityState[state];
23
+ const title = typeof detail === "string" ? `${word} · ${detail}` : word;
24
+ const body = (_jsxs(_Fragment, { children: [_jsx(Clock, { "aria-hidden": true, className: `size-(--size-icon-sm) ${TONE[state]}`, strokeWidth: 2.25 }), _jsx("span", { className: "sr-only", children: word })] }));
25
+ if (onClick === undefined) {
26
+ return (_jsx("span", { title: title, className: "inline-flex items-center p-1", children: body }));
27
+ }
28
+ return (_jsx("button", { type: "button", title: title, onClick: onClick, className: "relative z-10 inline-flex items-center rounded-pill p-1 transition-colors duration-(--duration-instant) hover:bg-ink/8 focus-visible:outline-2 focus-visible:outline-primary", children: body }));
29
+ }
@@ -1,3 +1,4 @@
1
1
  export { Activity, type ActivityAlert, type ActivityEvent, type ActivityLayout, type ActivityProps, useActivity, } from "./Activity.js";
2
+ export { ActivityBadge, type ActivityState, } from "./ActivityBadge.js";
2
3
  export { type Message, MessageComposer, type MessageComposerProps, type MessageKind, writeANote, } from "./Messages.js";
3
4
  export { Timeline, type TimelineItemProps, type TimelineProps, } from "./Timeline.js";
@@ -1,4 +1,5 @@
1
1
  /* ACTIVITY — everything around a record: its notices, its conversation and its history. */
2
2
  export { Activity, useActivity, } from "./Activity.js";
3
+ export { ActivityBadge, } from "./ActivityBadge.js";
3
4
  export { MessageComposer, writeANote, } from "./Messages.js";
4
5
  export { Timeline, } from "./Timeline.js";
@@ -0,0 +1,48 @@
1
+ import type { Tone } from "../../foundations/types.js";
2
+ import type { ReactNode } from "react";
3
+ export type CalendarView = "month" | "week" | "day" | "agenda";
4
+ export type RecordCalendarProps<T> = {
5
+ items: readonly T[];
6
+ keyOf: (item: T) => string;
7
+ titleOf: (item: T) => string;
8
+ startOf: (item: T) => Date;
9
+ /** Without it the event lasts an hour, or the day when it is all-day. */
10
+ endOf?: (item: T) => Date | undefined;
11
+ allDayOf?: (item: T) => boolean;
12
+ /** What the event means: an overdue call `bad`, a confirmed meeting `good`. */
13
+ toneOf?: (item: T) => Tone;
14
+ /** Who it is with or whose it is — a face on the event, the photo or the initials. */
15
+ avatarOf?: (item: T) => {
16
+ name: string;
17
+ src?: string;
18
+ } | undefined;
19
+ /** A second line under the title: the customer, the place. */
20
+ metaOf?: (item: T) => string | undefined;
21
+ /** The whole event drawn by the app — the calendar's equivalent of a board's `card`. Gets
22
+ * the record and the time text the calendar computed («10:00 – 11:30»). */
23
+ renderEvent?: (item: T, time: string) => ReactNode;
24
+ /** The view it opens on. The calendar's own buttons switch it afterwards. */
25
+ view?: CalendarView;
26
+ /** The day it opens on. Today by default. */
27
+ date?: Date;
28
+ /** An event was dragged to another moment or stretched. The calendar does NOT keep the move:
29
+ * `items` is the truth — the app saves and passes the new dates back. */
30
+ onMove?: (item: T, start: Date, end: Date | null) => void;
31
+ /** A stretch of empty time was selected: create something there. */
32
+ onCreate?: (start: Date, end: Date, allDay: boolean) => void;
33
+ /** An event was pressed. */
34
+ onOpen?: (item: T) => void;
35
+ /** A height of its own: `auto` grows with the month, a number or `40rem` scrolls inside. */
36
+ height?: number | string;
37
+ };
38
+ /**
39
+ * Records on a calendar — Odoo's calendar view: meetings, deliveries, due dates.
40
+ *
41
+ * Context: drawn by FullCalendar v7 (MIT standard views: month, week, day, agenda), dressed by
42
+ * `tokens.css` (its `forma` palette points at web-ui's tokens, so it follows the theme). It is a
43
+ * CONTROLLED view of the records: dragging or stretching an event reports `onMove` and the
44
+ * library's own change is undone at once — the app persists and passes the new `items`, so the
45
+ * screen never shows a date the backend refused. Resource rows (per person, per room) are
46
+ * FullCalendar's premium scheduler: a licence away, not built here.
47
+ */
48
+ export declare function RecordCalendar<T>({ items, keyOf, titleOf, startOf, endOf, allDayOf, toneOf, avatarOf, metaOf, renderEvent, view, date, onMove, onCreate, onOpen, height, }: RecordCalendarProps<T>): import("react").JSX.Element;
@@ -0,0 +1,101 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import FullCalendar from "@fullcalendar/react";
4
+ import dayGridPlugin from "@fullcalendar/react/daygrid";
5
+ import interactionPlugin from "@fullcalendar/react/interaction";
6
+ import listPlugin from "@fullcalendar/react/list";
7
+ import esLocale from "@fullcalendar/react/locales/es";
8
+ import formaThemePlugin from "@fullcalendar/react/themes/forma";
9
+ import timeGridPlugin from "@fullcalendar/react/timegrid";
10
+ import { useUiCopy } from "../../foundations/copy.js";
11
+ import { Avatar } from "../../primitives/value/Person.js";
12
+ const VIEW = {
13
+ month: "dayGridMonth",
14
+ week: "timeGridWeek",
15
+ day: "timeGridDay",
16
+ agenda: "listWeek",
17
+ };
18
+ /* An event takes the colour of what it MEANS — the five tones — and the surface colour on top,
19
+ which keeps ≥ 5.7:1 on every tone in both themes (see `tokens.css`). */
20
+ const TONE_COLOR = {
21
+ neutral: "var(--color-muted)",
22
+ info: "var(--color-info)",
23
+ good: "var(--color-good)",
24
+ warn: "var(--color-warn)",
25
+ bad: "var(--color-bad)",
26
+ accent: "var(--color-primary)",
27
+ };
28
+ /**
29
+ * Records on a calendar — Odoo's calendar view: meetings, deliveries, due dates.
30
+ *
31
+ * Context: drawn by FullCalendar v7 (MIT standard views: month, week, day, agenda), dressed by
32
+ * `tokens.css` (its `forma` palette points at web-ui's tokens, so it follows the theme). It is a
33
+ * CONTROLLED view of the records: dragging or stretching an event reports `onMove` and the
34
+ * library's own change is undone at once — the app persists and passes the new `items`, so the
35
+ * screen never shows a date the backend refused. Resource rows (per person, per room) are
36
+ * FullCalendar's premium scheduler: a licence away, not built here.
37
+ */
38
+ export function RecordCalendar({ items, keyOf, titleOf, startOf, endOf, allDayOf, toneOf, avatarOf, metaOf, renderEvent, view = "month", date, onMove, onCreate, onOpen, height = "auto", }) {
39
+ const copy = useUiCopy();
40
+ const byId = new Map(items.map((item) => [keyOf(item), item]));
41
+ const events = items.map((item) => {
42
+ const tone = toneOf?.(item);
43
+ const end = endOf?.(item);
44
+ return {
45
+ id: keyOf(item),
46
+ title: titleOf(item),
47
+ start: startOf(item),
48
+ ...(end === undefined ? {} : { end }),
49
+ ...(allDayOf === undefined ? {} : { allDay: allDayOf(item) }),
50
+ ...(tone === undefined
51
+ ? {}
52
+ : { color: TONE_COLOR[tone], textColor: "var(--color-surface)" }),
53
+ };
54
+ });
55
+ /* A record on the calendar, not a bare title: its face, its time, its name and a second
56
+ line, in that reading order. Only when the app gave something more than a title. */
57
+ const rich = avatarOf !== undefined || metaOf !== undefined || renderEvent !== undefined;
58
+ const content = (info) => {
59
+ const item = byId.get(info.event.id);
60
+ if (item === undefined)
61
+ return null;
62
+ if (renderEvent !== undefined)
63
+ return renderEvent(item, info.timeText);
64
+ const person = avatarOf?.(item);
65
+ const meta = metaOf?.(item);
66
+ return (_jsxs("span", { className: "flex min-w-0 items-start gap-1.5 px-0.5 py-px", children: [person === undefined ? null : (_jsx(Avatar, { name: person.name, size: "sm", ...(person.src === undefined ? {} : { src: person.src }) })), _jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsxs("span", { className: "min-w-0 truncate", children: [info.timeText === "" ? null : (_jsxs("b", { className: "font-semibold tabular-nums", children: [info.timeText, " "] })), titleOf(item)] }), meta === undefined ? null : (_jsx("span", { className: "min-w-0 truncate opacity-80", children: meta }))] })] }));
67
+ };
68
+ const moved = (info) => {
69
+ const item = byId.get(info.event.id);
70
+ /* Undone first: the move is a request, and `items` answers it. */
71
+ info.revert();
72
+ if (item === undefined || info.event.start === null)
73
+ return;
74
+ onMove?.(item, info.event.start, info.event.end);
75
+ };
76
+ return (_jsx("div", { className: "min-w-0 text-meta", children: _jsx(FullCalendar, { plugins: [
77
+ dayGridPlugin,
78
+ timeGridPlugin,
79
+ listPlugin,
80
+ interactionPlugin,
81
+ formaThemePlugin,
82
+ ], locales: [esLocale], locale: copy.locale.toLowerCase().startsWith("es") ? "es" : "en", initialView: VIEW[view], ...(date === undefined ? {} : { initialDate: date }), headerToolbar: {
83
+ start: "prev,next today",
84
+ center: "title",
85
+ end: "dayGridMonth,timeGridWeek,timeGridDay,listWeek",
86
+ }, height: height, events: events, editable: onMove !== undefined, selectable: onCreate !== undefined, nowIndicator: true,
87
+ /* The week and the day open on the working day, not on midnight. */
88
+ scrollTime: "07:00:00", ...(rich ? { eventContent: content } : {}), ...(onMove === undefined ? {} : { eventDrop: moved, eventResize: moved }), ...(onCreate === undefined
89
+ ? {}
90
+ : {
91
+ select: (info) => onCreate(info.start, info.end, info.allDay),
92
+ }), ...(onOpen === undefined
93
+ ? {}
94
+ : {
95
+ eventClick: (info) => {
96
+ const item = byId.get(info.event.id);
97
+ if (item !== undefined)
98
+ onOpen(item);
99
+ },
100
+ }) }) }));
101
+ }
@@ -0,0 +1 @@
1
+ export { type CalendarView, RecordCalendar, type RecordCalendarProps, } from "./RecordCalendar.js";
@@ -0,0 +1 @@
1
+ export { RecordCalendar, } from "./RecordCalendar.js";
@@ -86,5 +86,9 @@ export function SheetGroups({ children, columns = 2, }) {
86
86
  export function SheetGroup({ title, hint, icon: Icon, children, }) {
87
87
  const d = useDensity();
88
88
  const withHeader = title !== undefined;
89
- return (_jsxs("section", { className: `flex min-w-0 flex-col ${byDensity(d, "gap-2.5", "gap-2")}`, children: [withHeader ? (_jsxs("header", { className: "flex min-w-0 items-center gap-2", children: [Icon === undefined ? null : (_jsx(Icon, { className: "size-(--size-icon-sm) shrink-0 text-muted", strokeWidth: 1.75, "aria-hidden": true })), _jsx("h3", { className: "min-w-0 truncate text-meta font-medium text-ink", children: title }), hint === undefined ? null : _jsx(Hint, { children: hint })] })) : hint === undefined ? null : (_jsx(Hint, { children: hint })), _jsx("div", { className: `flex min-w-0 flex-col ${byDensity(d, "gap-2.5", "gap-2")}`, children: children })] }));
89
+ return (_jsxs("section", { className: `flex min-w-0 flex-col ${byDensity(d, "gap-2.5", "gap-2")}`, children: [withHeader ? (
90
+ /* A group title is ABOVE its labels, not one of them: at the labels' size and weight
91
+ «Cliente» the group read like «Cliente» the field. One step up in size and weight,
92
+ and a hairline that says where the group starts. */
93
+ _jsxs("header", { className: "flex min-w-0 items-center gap-2 border-b border-line pb-1.5", children: [Icon === undefined ? null : (_jsx(Icon, { className: "size-(--size-icon-sm) shrink-0 text-muted", strokeWidth: 1.75, "aria-hidden": true })), _jsx("h3", { className: "min-w-0 truncate text-body font-semibold text-ink", children: title }), hint === undefined ? null : _jsx(Hint, { children: hint })] })) : hint === undefined ? null : (_jsx(Hint, { children: hint })), _jsx("div", { className: `flex min-w-0 flex-col ${byDensity(d, "gap-2.5", "gap-2")}`, children: children })] }));
90
94
  }
@@ -0,0 +1,44 @@
1
+ /** A dependency between two records: «B starts when A ends» (`e2s`, the usual one). */
2
+ export type GanttLink = {
3
+ id: string;
4
+ source: string;
5
+ target: string;
6
+ type?: "e2s" | "s2s" | "e2e" | "s2e";
7
+ };
8
+ export type GanttZoom = "day" | "week" | "month";
9
+ export type RecordGanttProps<T> = {
10
+ items: readonly T[];
11
+ keyOf: (item: T) => string;
12
+ titleOf: (item: T) => string;
13
+ startOf: (item: T) => Date;
14
+ endOf: (item: T) => Date;
15
+ /** 0 to 100. Without it the bar has no progress handle. */
16
+ progressOf?: (item: T) => number;
17
+ /** The record it hangs under — a task under its project. Summaries are drawn for parents. */
18
+ parentOf?: (item: T) => string | undefined;
19
+ /** A point in time rather than a span: a delivery, a sign-off. */
20
+ milestoneOf?: (item: T) => boolean;
21
+ links?: readonly GanttLink[];
22
+ /** The scale it opens on; the toolbar switches it. */
23
+ zoom?: GanttZoom;
24
+ /** A bar was dragged or stretched. The app persists and passes the new `items`. */
25
+ onMove?: (item: T, start: Date, end: Date) => void;
26
+ onProgress?: (item: T, progress: number) => void;
27
+ /** A bar was chosen. */
28
+ onOpen?: (item: T) => void;
29
+ /** The chart's height, toolbar included: it scrolls inside. */
30
+ height?: string;
31
+ /** The left grid's column title for the names. */
32
+ nameHeader?: string;
33
+ };
34
+ /**
35
+ * Records on a timeline — Odoo's Gantt view: projects, tasks, manufacturing orders.
36
+ *
37
+ * Context: drawn by SVAR React Gantt (MIT core: move, resize, progress, dependencies, zoom),
38
+ * dressed by `tokens.css` (its Willow theme's `--wx-*` variables point at web-ui's tokens).
39
+ * The chart keeps its own state while dragging; every finished change is reported (`onMove`,
40
+ * `onProgress`) and the app passes the saved `items` back, which the chart re-reads. Without
41
+ * `onMove` / `onProgress` it is read-only. Auto-scheduling, critical path and resource load are
42
+ * SVAR's PRO edition: a licence away, not built here.
43
+ */
44
+ export declare function RecordGantt<T>({ items, keyOf, titleOf, startOf, endOf, progressOf, parentOf, milestoneOf, links, zoom, onMove, onProgress, onOpen, height, nameHeader, }: RecordGanttProps<T>): import("react").JSX.Element;
@@ -0,0 +1,110 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { TimelineToolbar } from "../timeline/TimelineToolbar.js";
4
+ import { useUiCopy } from "../../foundations/copy.js";
5
+ import { Gantt, Willow } from "@svar-ui/react-gantt";
6
+ import { useEffect, useMemo, useRef, useState } from "react";
7
+ /* The two rows of the time scale per zoom, written with `Intl` in the system's language
8
+ (SVAR's own format strings would ignore it), and how wide one cell of the finer row is. */
9
+ function scalesOf(zoom, locale) {
10
+ const say = (options) => (date) => new Intl.DateTimeFormat(locale, options).format(date);
11
+ const week = (date) => {
12
+ const first = new Date(date.getFullYear(), 0, 1);
13
+ return `S${Math.ceil(((+date - +first) / 86_400_000 + first.getDay() + 1) / 7)}`;
14
+ };
15
+ return {
16
+ day: {
17
+ cellWidth: 36,
18
+ scales: [
19
+ { unit: "month", step: 1, format: say({ month: "long", year: "numeric" }) },
20
+ { unit: "day", step: 1, format: say({ day: "numeric" }) },
21
+ ],
22
+ },
23
+ week: {
24
+ cellWidth: 72,
25
+ scales: [
26
+ { unit: "month", step: 1, format: say({ month: "long", year: "numeric" }) },
27
+ { unit: "week", step: 1, format: week },
28
+ ],
29
+ },
30
+ month: {
31
+ cellWidth: 96,
32
+ scales: [
33
+ { unit: "year", step: 1, format: say({ year: "numeric" }) },
34
+ { unit: "month", step: 1, format: say({ month: "short" }) },
35
+ ],
36
+ },
37
+ }[zoom];
38
+ }
39
+ /**
40
+ * Records on a timeline — Odoo's Gantt view: projects, tasks, manufacturing orders.
41
+ *
42
+ * Context: drawn by SVAR React Gantt (MIT core: move, resize, progress, dependencies, zoom),
43
+ * dressed by `tokens.css` (its Willow theme's `--wx-*` variables point at web-ui's tokens).
44
+ * The chart keeps its own state while dragging; every finished change is reported (`onMove`,
45
+ * `onProgress`) and the app passes the saved `items` back, which the chart re-reads. Without
46
+ * `onMove` / `onProgress` it is read-only. Auto-scheduling, critical path and resource load are
47
+ * SVAR's PRO edition: a licence away, not built here.
48
+ */
49
+ export function RecordGantt({ items, keyOf, titleOf, startOf, endOf, progressOf, parentOf, milestoneOf, links = [], zoom = "day", onMove, onProgress, onOpen, height = "28rem", nameHeader, }) {
50
+ const copy = useUiCopy();
51
+ /* The toolbar's: the scale starts at `zoom`, the closeness in the middle. */
52
+ const [scale, setScale] = useState(zoom);
53
+ const [closeness, setCloseness] = useState(3);
54
+ const time = scalesOf(scale, copy.locale);
55
+ const cellWidth = Math.round(time.cellWidth * ([0.5, 0.75, 1, 1.5, 2.25][closeness - 1] ?? 1));
56
+ const root = useRef(null);
57
+ const [fullscreen, setFullscreen] = useState(false);
58
+ useEffect(() => {
59
+ const follow = () => setFullscreen(document.fullscreenElement === root.current);
60
+ document.addEventListener("fullscreenchange", follow);
61
+ return () => document.removeEventListener("fullscreenchange", follow);
62
+ }, []);
63
+ const byId = useMemo(() => new Map(items.map((item) => [keyOf(item), item])), [items, keyOf]);
64
+ const parents = new Set(items.map((item) => parentOf?.(item)).filter(Boolean));
65
+ const tasks = items.map((item) => {
66
+ const id = keyOf(item);
67
+ const parent = parentOf?.(item);
68
+ return {
69
+ id,
70
+ text: titleOf(item),
71
+ start: startOf(item),
72
+ end: endOf(item),
73
+ ...(progressOf === undefined ? {} : { progress: progressOf(item) }),
74
+ ...(parent === undefined ? {} : { parent }),
75
+ type: milestoneOf?.(item) === true ? "milestone" : parents.has(id) ? "summary" : "task",
76
+ open: true,
77
+ };
78
+ });
79
+ const editable = onMove !== undefined || onProgress !== undefined;
80
+ /* The chart reports through its store's actions: `update-task` for a move, a stretch or the
81
+ progress handle — the finished one, not every pixel of the drag (`inProgress`). */
82
+ const init = (api) => {
83
+ api.on("update-task", (ev) => {
84
+ if (ev.inProgress === true)
85
+ return;
86
+ const item = byId.get(String(ev.id));
87
+ if (item === undefined)
88
+ return;
89
+ const { start, end, progress } = ev.task;
90
+ if (start instanceof Date && end instanceof Date)
91
+ onMove?.(item, start, end);
92
+ if (typeof progress === "number")
93
+ onProgress?.(item, progress);
94
+ });
95
+ api.on("select-task", (ev) => {
96
+ const item = byId.get(String(ev.id));
97
+ if (item !== undefined)
98
+ onOpen?.(item);
99
+ });
100
+ };
101
+ return (_jsxs("div", { ref: root, className: "flex min-w-0 flex-col overflow-hidden rounded-box border border-line bg-surface", style: { height }, children: [_jsx(TimelineToolbar, { scale: scale, onScaleChange: (next) => {
102
+ if (next !== "year")
103
+ setScale(next);
104
+ }, scales: ["day", "week", "month"], zoom: closeness, onZoomChange: setCloseness, fullscreen: fullscreen, onFullscreenChange: (next) => {
105
+ if (next)
106
+ void root.current?.requestFullscreen?.();
107
+ else if (document.fullscreenElement !== null)
108
+ void document.exitFullscreen();
109
+ } }), _jsx("div", { className: "min-h-0 flex-1", children: _jsx(Willow, { fonts: false, children: _jsx(Gantt, { tasks: tasks, links: links.map((link) => ({ ...link, type: link.type ?? "e2s" })), scales: time.scales, cellWidth: cellWidth, readonly: !editable, columns: [{ id: "text", header: nameHeader ?? "", width: 260 }], init: init }) }) })] }));
110
+ }
@@ -0,0 +1 @@
1
+ export { type GanttLink, type GanttZoom, RecordGantt, type RecordGanttProps, } from "./RecordGantt.js";
@@ -0,0 +1 @@
1
+ export { RecordGantt, } from "./RecordGantt.js";
@@ -1,10 +1,13 @@
1
1
  export * from "./activity/index.js";
2
2
  export * from "./assist/index.js";
3
+ export * from "./calendar/index.js";
3
4
  export * from "./form/index.js";
4
5
  export * from "./frame/index.js";
6
+ export * from "./gantt/index.js";
5
7
  export * from "./lines/index.js";
6
8
  export * from "./listing/index.js";
7
9
  export * from "./navigation/index.js";
8
10
  export * from "./record/index.js";
9
11
  export * from "./search/index.js";
10
12
  export * from "./shell/index.js";
13
+ export * from "./timeline/index.js";
@@ -1,11 +1,14 @@
1
1
  /* COMPONENTS — primitives composed to answer ONE question of the product, by family. */
2
2
  export * from "./activity/index.js";
3
3
  export * from "./assist/index.js";
4
+ export * from "./calendar/index.js";
4
5
  export * from "./form/index.js";
5
6
  export * from "./frame/index.js";
7
+ export * from "./gantt/index.js";
6
8
  export * from "./lines/index.js";
7
9
  export * from "./listing/index.js";
8
10
  export * from "./navigation/index.js";
9
11
  export * from "./record/index.js";
10
12
  export * from "./search/index.js";
11
13
  export * from "./shell/index.js";
14
+ export * from "./timeline/index.js";
@@ -81,7 +81,7 @@ export type LineItemsProps<T> = {
81
81
  * `inline` puts it in the table, open, under the others (see the file's header).
82
82
  * By default the dialog when there is an `editor`, the table when there is not. */
83
83
  adding?: "inline" | "editor";
84
- /** Open the whole line somewhere else than this block's dialog — the app's `Drill`, so
84
+ /** Open the whole line somewhere else than this block's dialog — a modal of the workspace, so
85
85
  * the line opens as «S00142 › Línea 3» and can go deeper (its product, its lot) without
86
86
  * a modal over a modal. When given, it replaces `editor` for existing lines. */
87
87
  onOpenLine?: (id: string) => void;
@@ -95,6 +95,9 @@ export type LineItemsProps<T> = {
95
95
  } | null;
96
96
  /** Sections and notes among the lines. See the file's header. */
97
97
  structure?: LineStructure<T>;
98
+ /** The lines' order is the document's (Odoo's `sequence`): a grip at the start of each line
99
+ * drags it, Alt + ↑ / ↓ moves it. `EditableTable`'s `onReorder`, passed through. */
100
+ onReorder?: (id: string, before: string | null) => void;
98
101
  /** Which columns are on screen, when the owner remembers it (a preference). */
99
102
  visibleColumnIds?: readonly string[];
100
103
  onVisibleColumnIdsChange?: (ids: readonly string[]) => void;
@@ -108,4 +111,4 @@ export type LineItemsProps<T> = {
108
111
  * removing asked first, what an empty document said. One component, built on
109
112
  * `EditableTable` and `Dialog`, makes the one2many the same block everywhere.
110
113
  */
111
- export declare function LineItems<T>({ lines, columns, getLineId, lineLabel, mode, onChange, onAdd, newLine, onRemove, onRestore, editor, isValid, context, searchText, searchFrom, totals, layout, adding, onOpenLine, flag, structure, visibleColumnIds, onVisibleColumnIdsChange, }: LineItemsProps<T>): import("react").JSX.Element;
114
+ export declare function LineItems<T>({ lines, columns, getLineId, lineLabel, mode, onChange, onAdd, newLine, onRemove, onRestore, editor, isValid, context, searchText, searchFrom, totals, layout, adding, onOpenLine, flag, structure, onReorder, visibleColumnIds, onVisibleColumnIdsChange, }: LineItemsProps<T>): import("react").JSX.Element;
@@ -38,7 +38,7 @@ function plain(v) {
38
38
  * removing asked first, what an empty document said. One component, built on
39
39
  * `EditableTable` and `Dialog`, makes the one2many the same block everywhere.
40
40
  */
41
- export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange, onAdd, newLine, onRemove, onRestore, editor, isValid, context, searchText, searchFrom = 8, totals, layout = "auto", adding, onOpenLine, flag, structure, visibleColumnIds, onVisibleColumnIdsChange, }) {
41
+ export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange, onAdd, newLine, onRemove, onRestore, editor, isValid, context, searchText, searchFrom = 8, totals, layout = "auto", adding, onOpenLine, flag, structure, onReorder, visibleColumnIds, onVisibleColumnIdsChange, }) {
42
42
  const copy = useUiCopy();
43
43
  const toast = useOptionalToast();
44
44
  const read = useFormMode(mode) === "read";
@@ -58,7 +58,7 @@ export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange
58
58
  const canAdd = !read && onAdd !== undefined && newLine !== undefined;
59
59
  const canRemove = !read && onRemove !== undefined;
60
60
  /* The whole line opens either in this block's own dialog (`editor`) or wherever the app
61
- opens records (`onOpenLine`, a `Drill` context) — never both, so a line's form never
61
+ opens records (`onOpenLine`, a modal of the workspace) — never both, so a line's form never
62
62
  sits on top of another modal. */
63
63
  const canEdit = (canChange && editor !== undefined) || onOpenLine !== undefined;
64
64
  const openLine = (id, line) => {
@@ -283,7 +283,7 @@ export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange
283
283
  invalid: false,
284
284
  }, children: _jsx(SearchField, { value: query, onChange: setQuery, placeholder: copy.lines.search }) })] })) : null, _jsx("span", { className: "text-meta text-muted tabular-nums", "aria-live": "polite", children: count }), columnControl.hasOptional || addButton !== null ? (_jsxs("span", { className: "ml-auto flex items-center gap-2", children: [_jsx(ColumnsMenu, { control: columnControl }), addButton] })) : null] }), _jsx(EditableTable, { rows: shown, columns: tableColumns, getRowId: getLineId, empty: lines.length === 0
285
285
  ? copy.lines.empty
286
- : fillCopy(copy.lineItems.noMatch, { query: query.trim() }), layout: layout, ...(inline ? { onAdd: () => addInline(), addLabel: copy.lines.add } : {}), ...(bandAdds.length === 0 ? {} : { adds: bandAdds }), ...(bands === undefined ? {} : { bands }), ...(flag === undefined ? {} : { flag }), ...(inPlace && onRemove !== undefined
286
+ : fillCopy(copy.lineItems.noMatch, { query: query.trim() }), layout: layout, ...(inline ? { onAdd: () => addInline(), addLabel: copy.lines.add } : {}), ...(bandAdds.length === 0 ? {} : { adds: bandAdds }), ...(bands === undefined ? {} : { bands }), ...(flag === undefined ? {} : { flag }), ...(onReorder === undefined || read ? {} : { onReorder }), ...(inPlace && onRemove !== undefined
287
287
  ? /* A blank row nobody wrote in is taken back without a notice: nobody
288
288
  removed anything, so there is nothing to undo. */
289
289
  { onDiscard: onRemove }
@@ -0,0 +1,39 @@
1
+ import type { Tone } from "../../foundations/types.js";
2
+ import { type ReactNode } from "react";
3
+ /** One stage of a board: its id (what `of` returns), its name, and its tone. */
4
+ export type BoardColumn = {
5
+ id: string;
6
+ label: string;
7
+ tone?: Tone;
8
+ };
9
+ /** How a board lays its cards in stages, and what moving one means. */
10
+ export type BoardStages<T> = {
11
+ /** The column a record goes in: its stage — or ANY field it is grouped by (the salesperson,
12
+ * the priority, the tag): a board is a grouping you can drag across. */
13
+ of: (item: T) => string;
14
+ /** The columns, in order. Without it they come from the records themselves, in the order
15
+ * they appear, named by `labelOf`. */
16
+ columns?: readonly BoardColumn[];
17
+ labelOf?: (id: string) => string;
18
+ /** A card dropped in another stage. Without it the board is read-only: nothing drags. */
19
+ onMove?: (item: T, to: string) => void;
20
+ /** How a record is named when a move is announced: «S00142 pasó a pedido». */
21
+ nameOf?: (item: T) => string;
22
+ /** The figure on a column's head: the expected revenue of the opportunities in it. */
23
+ total?: (items: readonly T[]) => ReactNode;
24
+ /** The bar under a column's head: how its records stand, by tone — Odoo's activity bar
25
+ * (overdue red, today amber, planned green). `undefined` for a record with nothing to say. */
26
+ progress?: (item: T) => Tone | undefined;
27
+ /** «+» on a column's head: a name typed there creates a record in that column. */
28
+ onQuickCreate?: (column: string, name: string) => void;
29
+ /** A column at the end: «Agregar etapa». */
30
+ onAddColumn?: (label: string) => void;
31
+ /** Columns that start folded into a narrow strip (the won, the lost). */
32
+ defaultFolded?: readonly string[];
33
+ };
34
+ export declare function BoardColumns<T>({ items, keyOf, stages, card, }: {
35
+ items: readonly T[];
36
+ keyOf: (item: T) => string;
37
+ stages: BoardStages<T>;
38
+ card: (item: T) => ReactNode;
39
+ }): import("react").JSX.Element;