@sincpro/web-ui 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/README.md +40 -13
  2. package/dist/components/activity/Activity.d.ts +69 -0
  3. package/dist/components/activity/Activity.js +279 -0
  4. package/dist/components/activity/ActivityBadge.d.ts +16 -0
  5. package/dist/components/activity/ActivityBadge.js +29 -0
  6. package/dist/components/{chatter → activity}/Messages.d.ts +2 -2
  7. package/dist/components/{chatter → activity}/Messages.js +9 -6
  8. package/dist/components/activity/index.d.ts +4 -0
  9. package/dist/components/activity/index.js +5 -0
  10. package/dist/components/assist/AssistMark.d.ts +3 -3
  11. package/dist/components/assist/AssistMark.js +1 -1
  12. package/dist/components/assist/assist.d.ts +1 -1
  13. package/dist/components/assist/assist.js +1 -1
  14. package/dist/components/calendar/RecordCalendar.d.ts +48 -0
  15. package/dist/components/calendar/RecordCalendar.js +101 -0
  16. package/dist/components/calendar/index.d.ts +1 -0
  17. package/dist/components/calendar/index.js +1 -0
  18. package/dist/components/form/Sheet.d.ts +4 -0
  19. package/dist/components/form/Sheet.js +16 -5
  20. package/dist/components/gantt/RecordGantt.d.ts +44 -0
  21. package/dist/components/gantt/RecordGantt.js +110 -0
  22. package/dist/components/gantt/index.d.ts +1 -0
  23. package/dist/components/gantt/index.js +1 -0
  24. package/dist/components/index.d.ts +4 -1
  25. package/dist/components/index.js +4 -1
  26. package/dist/components/lines/LineItems.d.ts +45 -5
  27. package/dist/components/lines/LineItems.js +106 -18
  28. package/dist/components/listing/BoardColumns.d.ts +39 -0
  29. package/dist/components/listing/BoardColumns.js +138 -0
  30. package/dist/components/listing/Listing.js +1 -1
  31. package/dist/components/listing/ListingBar.d.ts +46 -2
  32. package/dist/components/listing/ListingBar.js +47 -5
  33. package/dist/components/listing/PivotExplorer.d.ts +34 -0
  34. package/dist/components/listing/PivotExplorer.js +86 -0
  35. package/dist/components/listing/RecordCard.d.ts +6 -8
  36. package/dist/components/listing/RecordCard.js +13 -7
  37. package/dist/components/listing/Row.d.ts +25 -2
  38. package/dist/components/listing/Row.js +30 -13
  39. package/dist/components/listing/index.d.ts +2 -0
  40. package/dist/components/listing/index.js +2 -0
  41. package/dist/components/listing/set.d.ts +1 -1
  42. package/dist/components/navigation/index.d.ts +0 -2
  43. package/dist/components/navigation/index.js +2 -3
  44. package/dist/components/record/RecordAudit.d.ts +17 -0
  45. package/dist/components/record/RecordAudit.js +27 -0
  46. package/dist/components/record/RecordHeader.d.ts +27 -17
  47. package/dist/components/record/RecordHeader.js +64 -20
  48. package/dist/components/record/RecordNotices.d.ts +37 -0
  49. package/dist/components/record/RecordNotices.js +75 -0
  50. package/dist/components/record/RecordRibbon.d.ts +13 -0
  51. package/dist/components/record/RecordRibbon.js +22 -0
  52. package/dist/components/record/ThumbBar.js +1 -1
  53. package/dist/components/record/index.d.ts +2 -0
  54. package/dist/components/record/index.js +2 -0
  55. package/dist/components/search/CriteriaChips.d.ts +6 -1
  56. package/dist/components/search/CriteriaChips.js +71 -20
  57. package/dist/components/search/SearchBar.d.ts +4 -1
  58. package/dist/components/search/SearchBar.js +15 -7
  59. package/dist/components/timeline/RecordTimeline.d.ts +60 -0
  60. package/dist/components/timeline/RecordTimeline.js +214 -0
  61. package/dist/components/timeline/TimelineToolbar.d.ts +30 -0
  62. package/dist/components/timeline/TimelineToolbar.js +31 -0
  63. package/dist/components/timeline/index.d.ts +3 -0
  64. package/dist/components/timeline/index.js +3 -0
  65. package/dist/components/timeline/time.d.ts +43 -0
  66. package/dist/components/timeline/time.js +135 -0
  67. package/dist/foundations/copy.d.ts +194 -18
  68. package/dist/foundations/copy.es.js +142 -15
  69. package/dist/foundations/copy.js +133 -11
  70. package/dist/foundations/index.d.ts +1 -1
  71. package/dist/foundations/match.d.ts +23 -0
  72. package/dist/foundations/match.js +63 -0
  73. package/dist/foundations/slots.d.ts +1 -1
  74. package/dist/foundations/slots.js +8 -3
  75. package/dist/foundations/types.d.ts +7 -0
  76. package/dist/foundations/types.js +0 -36
  77. package/dist/primitives/block/index.d.ts +1 -1
  78. package/dist/primitives/block/index.js +1 -1
  79. package/dist/primitives/cell/CellStack.d.ts +7 -1
  80. package/dist/primitives/cell/CellStack.js +5 -2
  81. package/dist/primitives/choice/ComboBox.d.ts +3 -0
  82. package/dist/primitives/choice/ComboBox.js +10 -6
  83. package/dist/primitives/choice/KanbanState.d.ts +14 -0
  84. package/dist/primitives/choice/KanbanState.js +37 -0
  85. package/dist/primitives/choice/Priority.d.ts +20 -0
  86. package/dist/primitives/choice/Priority.js +41 -0
  87. package/dist/primitives/choice/SearchField.d.ts +10 -1
  88. package/dist/primitives/choice/SearchField.js +21 -2
  89. package/dist/primitives/choice/Slider.d.ts +6 -1
  90. package/dist/primitives/choice/Slider.js +2 -2
  91. package/dist/primitives/choice/index.d.ts +2 -0
  92. package/dist/primitives/choice/index.js +2 -0
  93. package/dist/primitives/date/Calendar.js +30 -6
  94. package/dist/primitives/date/DatePicker.d.ts +11 -3
  95. package/dist/primitives/date/DatePicker.js +12 -8
  96. package/dist/primitives/date/DateTime.d.ts +35 -0
  97. package/dist/primitives/date/DateTime.js +100 -0
  98. package/dist/primitives/date/index.d.ts +1 -0
  99. package/dist/primitives/date/index.js +4 -2
  100. package/dist/primitives/dialog/Dialog.d.ts +9 -3
  101. package/dist/primitives/dialog/Dialog.js +41 -5
  102. package/dist/primitives/field/DropZone.js +1 -1
  103. package/dist/primitives/field/Field.d.ts +14 -2
  104. package/dist/primitives/field/Field.js +25 -14
  105. package/dist/primitives/field/copy.d.ts +16 -0
  106. package/dist/primitives/field/copy.js +69 -0
  107. package/dist/primitives/field/index.d.ts +2 -0
  108. package/dist/primitives/field/index.js +4 -0
  109. package/dist/primitives/field/note.d.ts +55 -0
  110. package/dist/primitives/field/note.js +117 -0
  111. package/dist/primitives/index.d.ts +1 -0
  112. package/dist/primitives/index.js +1 -0
  113. package/dist/primitives/menu/Menu.d.ts +28 -0
  114. package/dist/primitives/menu/Menu.js +47 -17
  115. package/dist/primitives/notice/Callout.js +1 -1
  116. package/dist/primitives/notice/Flag.d.ts +13 -0
  117. package/dist/primitives/notice/Flag.js +31 -0
  118. package/dist/primitives/notice/index.d.ts +2 -1
  119. package/dist/primitives/notice/index.js +2 -1
  120. package/dist/primitives/overlay/Preview.d.ts +19 -0
  121. package/dist/primitives/overlay/Preview.js +69 -0
  122. package/dist/primitives/overlay/index.d.ts +1 -0
  123. package/dist/primitives/overlay/index.js +3 -1
  124. package/dist/primitives/picker/MatchedText.d.ts +10 -0
  125. package/dist/primitives/picker/MatchedText.js +12 -0
  126. package/dist/primitives/picker/Picker.d.ts +67 -4
  127. package/dist/primitives/picker/Picker.js +142 -23
  128. package/dist/primitives/table/CellCopy.d.ts +20 -0
  129. package/dist/primitives/table/CellCopy.js +23 -0
  130. package/dist/primitives/table/DataTable.d.ts +55 -1
  131. package/dist/primitives/table/DataTable.js +126 -40
  132. package/dist/primitives/table/EditableTable.d.ts +102 -2
  133. package/dist/primitives/table/EditableTable.js +287 -32
  134. package/dist/primitives/table/ReportTable.js +2 -1
  135. package/dist/primitives/table/RowActions.js +7 -2
  136. package/dist/primitives/table/SheetTable.d.ts +4 -1
  137. package/dist/primitives/table/SheetTable.js +38 -5
  138. package/dist/primitives/table/Table.d.ts +5 -0
  139. package/dist/primitives/table/Table.js +17 -8
  140. package/dist/primitives/table/index.d.ts +2 -2
  141. package/dist/primitives/table/index.js +1 -1
  142. package/dist/primitives/tabs/Tabs.d.ts +2 -2
  143. package/dist/primitives/tabs/Tabs.js +1 -1
  144. package/dist/primitives/tag/Tags.d.ts +2 -0
  145. package/dist/primitives/tag/Tags.js +2 -1
  146. package/dist/primitives/tag/TagsField.d.ts +13 -0
  147. package/dist/primitives/tag/TagsField.js +39 -10
  148. package/dist/primitives/tag/choices.d.ts +2 -0
  149. package/dist/primitives/tree/Tree.d.ts +56 -0
  150. package/dist/primitives/tree/Tree.js +74 -0
  151. package/dist/primitives/tree/index.d.ts +1 -0
  152. package/dist/primitives/tree/index.js +3 -0
  153. package/dist/primitives/value/Avatar.d.ts +16 -0
  154. package/dist/primitives/value/Avatar.js +91 -0
  155. package/dist/primitives/value/Copyable.d.ts +11 -0
  156. package/dist/primitives/value/Copyable.js +27 -6
  157. package/dist/primitives/value/Fecha.js +10 -1
  158. package/dist/primitives/value/Person.d.ts +6 -51
  159. package/dist/primitives/value/Person.js +9 -148
  160. package/dist/primitives/value/ProgressBar.d.ts +31 -0
  161. package/dist/primitives/value/ProgressBar.js +53 -0
  162. package/dist/primitives/value/Relation.d.ts +22 -5
  163. package/dist/primitives/value/Relation.js +42 -14
  164. package/dist/primitives/value/Totals.d.ts +102 -7
  165. package/dist/primitives/value/Totals.js +52 -2
  166. package/dist/primitives/value/Value.d.ts +0 -37
  167. package/dist/primitives/value/Value.js +0 -39
  168. package/dist/primitives/value/contact.d.ts +30 -0
  169. package/dist/primitives/value/contact.js +51 -0
  170. package/dist/primitives/value/index.d.ts +5 -4
  171. package/dist/primitives/value/index.js +5 -4
  172. package/dist/tokens.css +179 -7
  173. package/dist/views/list/List.d.ts +32 -3
  174. package/dist/views/list/List.js +51 -7
  175. package/dist/views/list/ListView.d.ts +1 -1
  176. package/dist/views/list/ListView.js +70 -13
  177. package/dist/views/list/columns.js +8 -1
  178. package/dist/views/list/derive.d.ts +12 -1
  179. package/dist/views/list/derive.js +29 -3
  180. package/dist/views/list/presentations.d.ts +5 -2
  181. package/dist/views/list/presentations.js +35 -3
  182. package/dist/views/list/types.d.ts +57 -1
  183. package/dist/views/list/useNarrowList.d.ts +10 -0
  184. package/dist/views/list/useNarrowList.js +20 -6
  185. package/dist/views/record/Record.d.ts +41 -8
  186. package/dist/views/record/Record.js +113 -28
  187. package/dist/views/record/index.d.ts +1 -1
  188. package/package.json +15 -4
  189. package/dist/components/chatter/Chatter.d.ts +0 -161
  190. package/dist/components/chatter/Chatter.js +0 -232
  191. package/dist/components/chatter/index.d.ts +0 -3
  192. package/dist/components/chatter/index.js +0 -4
  193. package/dist/components/navigation/CommandPalette.d.ts +0 -40
  194. package/dist/components/navigation/CommandPalette.js +0 -114
  195. package/dist/components/navigation/commands.d.ts +0 -30
  196. package/dist/components/navigation/commands.js +0 -30
  197. /package/dist/components/{chatter → activity}/Timeline.d.ts +0 -0
  198. /package/dist/components/{chatter → activity}/Timeline.js +0 -0
@@ -0,0 +1,214 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { hoursMinutes, lanesOf, rangeOf, shiftDate, snap, snapOf, TIMELINE_SCALES, unitSpan, } from "./time.js";
4
+ import { TimelineToolbar } from "./TimelineToolbar.js";
5
+ import { fillCopy, useUiCopy } from "../../foundations/copy.js";
6
+ import { Avatar } from "../../primitives/value/Person.js";
7
+ import { useEffect, useRef, useState, } from "react";
8
+ const FILL = {
9
+ neutral: "bg-muted text-surface",
10
+ info: "bg-info text-surface",
11
+ good: "bg-good text-surface",
12
+ warn: "bg-warn text-surface",
13
+ bad: "bg-bad text-surface",
14
+ accent: "bg-primary text-surface",
15
+ };
16
+ /* Pixels a column asks for at the middle zoom; the zoom multiplies them. Past the screen's width
17
+ the track scrolls sideways — the names column stays. */
18
+ const BASE = { day: 44, week: 150, month: 44, year: 96 };
19
+ const ZOOM = [0.5, 0.75, 1, 1.5, 2.25];
20
+ const BAR = 26;
21
+ const GAP = 4;
22
+ /**
23
+ * Records on a timeline, grouped in rows — Odoo's Gantt view: attendances by employee, shifts,
24
+ * room bookings, work orders by machine.
25
+ *
26
+ * Context: a project Gantt (`RecordGantt`) is one task per row with dependencies; THIS is the
27
+ * other one people mean by «Gantt» in an ERP — a grouping with N records per row, read across
28
+ * time. The scale says the period (a day by hours, a week, a month by days, a year by months);
29
+ * the toolbar moves it (‹ › date · today), zooms, switches scale and takes the whole screen.
30
+ *
31
+ * Controlled: a drag shows a draft and reports `onMove` on release; the bar stays where
32
+ * `items` says. Everything the pointer does has a key: Alt + ← / → moves by the snap, with
33
+ * Shift it stretches, Alt + ↑ / ↓ changes the row; every move is announced.
34
+ */
35
+ export function RecordTimeline({ items, keyOf, rowOf, rows: givenRows, startOf, endOf, titleOf, labelOf, toneOf, rowHeader, scale: outerScale, defaultScale = "week", onScaleChange, scales = TIMELINE_SCALES, date: outerDate, defaultDate, onDateChange, onMove, onCreate, onOpen, }) {
36
+ const copy = useUiCopy();
37
+ const words = copy.timeline;
38
+ const [ownScale, setOwnScale] = useState(defaultScale);
39
+ const [ownDate, setOwnDate] = useState(() => defaultDate ?? new Date());
40
+ const scale = outerScale ?? ownScale;
41
+ const date = outerDate ?? ownDate;
42
+ const setScale = (next) => {
43
+ if (outerScale === undefined)
44
+ setOwnScale(next);
45
+ onScaleChange?.(next);
46
+ };
47
+ const setDate = (next) => {
48
+ if (outerDate === undefined)
49
+ setOwnDate(next);
50
+ onDateChange?.(next);
51
+ };
52
+ const [zoom, setZoom] = useState(3);
53
+ const [draft, setDraft] = useState(null);
54
+ const draftRef = useRef(null);
55
+ const justDragged = useRef(false);
56
+ const [said, setSaid] = useState("");
57
+ const root = useRef(null);
58
+ const [fullscreen, setFullscreen] = useState(false);
59
+ useEffect(() => {
60
+ const follow = () => setFullscreen(document.fullscreenElement === root.current);
61
+ document.addEventListener("fullscreenchange", follow);
62
+ return () => document.removeEventListener("fullscreenchange", follow);
63
+ }, []);
64
+ const range = rangeOf(scale, date, copy.locale);
65
+ const from = +range.start;
66
+ const span = +range.end - from;
67
+ const step = snapOf(scale);
68
+ const cell = BASE[scale] * (ZOOM[zoom - 1] ?? 1);
69
+ const time = new Intl.DateTimeFormat(copy.locale, {
70
+ hour: "2-digit",
71
+ minute: "2-digit",
72
+ /* 09:12–13:13 and not «09:12 a. m.–01:13 p. m.»: a bar has room for the hours. */
73
+ hourCycle: "h23",
74
+ });
75
+ const day = new Intl.DateTimeFormat(copy.locale, { day: "numeric", month: "short" });
76
+ const rows = givenRows ?? [...new Set(items.map(rowOf))].map((id) => ({ id, label: id }));
77
+ const shown = items.filter((item) => +endOf(item) > from && +startOf(item) < from + span);
78
+ /* Where each record is drawn: its own dates, or the draft while it is being dragged. */
79
+ const placed = (item) => {
80
+ const id = keyOf(item);
81
+ if (draft !== null && draft.id === id) {
82
+ return { start: new Date(draft.start), end: new Date(draft.end), row: draft.row };
83
+ }
84
+ return { start: startOf(item), end: endOf(item), row: rowOf(item) };
85
+ };
86
+ const describe = (start, end) => scale === "day" || scale === "week"
87
+ ? `${hoursMinutes(+end - +start)} (${time.format(start)}–${time.format(end)})`
88
+ : `${day.format(start)} – ${day.format(end)}`;
89
+ const report = (item, start, end, row) => {
90
+ onMove?.(item, start, end, row);
91
+ setSaid(fillCopy(words.moved, {
92
+ name: titleOf?.(item) ?? keyOf(item),
93
+ range: describe(start, end),
94
+ }));
95
+ };
96
+ const drag = (event, item, mode) => {
97
+ if (onMove === undefined || event.button !== 0)
98
+ return;
99
+ event.stopPropagation();
100
+ const track = event.currentTarget.closest("[data-track]");
101
+ if (track === null)
102
+ return;
103
+ const perPx = span / track.getBoundingClientRect().width;
104
+ const x0 = event.clientX;
105
+ const start0 = +startOf(item);
106
+ const end0 = +endOf(item);
107
+ const id = keyOf(item);
108
+ const row0 = rowOf(item);
109
+ const move = (next) => {
110
+ const delta = snap((next.clientX - x0) * perPx, step);
111
+ const under = document
112
+ .elementFromPoint(next.clientX, next.clientY)
113
+ ?.closest("[data-row]")?.dataset["row"];
114
+ const draftNow = mode === "move"
115
+ ? { id, start: start0 + delta, end: end0 + delta, row: under ?? row0 }
116
+ : { id, start: start0, end: Math.max(start0 + step, end0 + delta), row: row0 };
117
+ draftRef.current = draftNow;
118
+ setDraft(draftNow);
119
+ };
120
+ const stop = () => {
121
+ window.removeEventListener("pointermove", move);
122
+ window.removeEventListener("pointerup", stop);
123
+ const done = draftRef.current;
124
+ draftRef.current = null;
125
+ setDraft(null);
126
+ if (done === null)
127
+ return;
128
+ const changed = done.start !== start0 || done.end !== end0 || done.row !== row0;
129
+ if (!changed)
130
+ return;
131
+ justDragged.current = true;
132
+ report(item, new Date(done.start), new Date(done.end), done.row);
133
+ };
134
+ window.addEventListener("pointermove", move);
135
+ window.addEventListener("pointerup", stop);
136
+ };
137
+ const byKeyboard = (event, item) => {
138
+ if (onMove === undefined || !event.altKey)
139
+ return;
140
+ const start = +startOf(item);
141
+ const end = +endOf(item);
142
+ const row = rowOf(item);
143
+ const at = rows.findIndex((one) => one.id === row);
144
+ const dir = event.key === "ArrowLeft" ? -1 : event.key === "ArrowRight" ? 1 : 0;
145
+ if (dir !== 0) {
146
+ event.preventDefault();
147
+ if (event.shiftKey) {
148
+ report(item, new Date(start), new Date(Math.max(start + step, end + dir * step)), row);
149
+ }
150
+ else {
151
+ report(item, new Date(start + dir * step), new Date(end + dir * step), row);
152
+ }
153
+ return;
154
+ }
155
+ const vertical = event.key === "ArrowUp" ? -1 : event.key === "ArrowDown" ? 1 : 0;
156
+ const target = rows[at + vertical];
157
+ if (vertical !== 0 && target !== undefined) {
158
+ event.preventDefault();
159
+ report(item, new Date(start), new Date(end), target.id);
160
+ }
161
+ };
162
+ const create = (event, row) => {
163
+ if (onCreate === undefined)
164
+ return;
165
+ const rect = event.currentTarget.getBoundingClientRect();
166
+ const at = new Date(from + ((event.clientX - rect.left) / rect.width) * span);
167
+ const { start, end } = unitSpan(scale, at);
168
+ onCreate(row.id, start, end);
169
+ };
170
+ const now = Date.now();
171
+ const nowAt = now > from && now < from + span ? ((now - from) / span) * 100 : null;
172
+ const percent = (time) => ((Math.min(Math.max(time, from), from + span) - from) / span) * 100;
173
+ return (_jsxs("div", { ref: root, className: "flex min-w-0 flex-col overflow-hidden rounded-box border border-line bg-surface", children: [_jsx(TimelineToolbar, { title: range.title, date: date, onDateChange: setDate, onStep: (steps) => setDate(shiftDate(scale, date, steps)), scale: scale, onScaleChange: setScale, scales: scales, zoom: zoom, onZoomChange: setZoom, fullscreen: fullscreen, onFullscreenChange: (next) => {
174
+ if (next)
175
+ void root.current?.requestFullscreen?.();
176
+ else if (document.fullscreenElement !== null)
177
+ void document.exitFullscreen();
178
+ } }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: _jsxs("div", { style: {
179
+ minWidth: `calc(var(--container-timeline-label) + ${range.units.length * cell}px)`,
180
+ }, children: [_jsxs("div", { className: "sticky top-0 z-20 flex border-b border-line-strong bg-surface", children: [_jsx("div", { className: "sticky left-0 z-10 flex w-(--container-timeline-label) shrink-0 items-end border-r border-line bg-surface px-3 py-2 text-meta font-semibold text-ink", children: rowHeader }), _jsx("div", { className: "flex min-w-0 flex-1", children: range.units.map((unit) => (_jsx("div", { className: `flex min-w-0 flex-1 items-center justify-center border-l border-line py-2 text-micro font-semibold whitespace-nowrap text-ink tabular-nums first:border-l-0 ${unit.weekend ? "bg-ink/4" : ""}`, children: unit.label }, +unit.start))) })] }), rows.map((row) => {
181
+ const inRow = shown.filter((item) => placed(item).row === row.id);
182
+ const { lane, count } = lanesOf(inRow, (item) => placed(item).start, (item) => placed(item).end);
183
+ const height = count * (BAR + GAP) + GAP * 2;
184
+ return (_jsxs("div", { "data-row": row.id, className: "flex border-b border-line", children: [_jsxs("div", { className: "sticky left-0 z-10 flex w-(--container-timeline-label) shrink-0 items-center gap-2 border-r border-line bg-surface px-3", children: [row.avatar === undefined || row.avatar === false ? null : (_jsx(Avatar, { name: row.label, size: "sm", ...(typeof row.avatar === "string" ? { src: row.avatar } : {}) })), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate text-meta text-ink", children: row.label }), row.detail === undefined ? null : (_jsx("p", { className: "truncate text-micro text-muted", children: row.detail }))] })] }), _jsxs("div", { "data-track": "", onClick: (event) => create(event, row), className: `relative flex min-w-0 flex-1 ${onCreate === undefined ? "" : "cursor-cell"}`, style: { height }, children: [range.units.map((unit) => (_jsx("div", { "aria-hidden": true, className: `min-w-0 flex-1 border-l border-line first:border-l-0 ${unit.weekend ? "bg-ink/4" : ""}` }, +unit.start))), nowAt === null ? null : (_jsx("span", { "aria-hidden": true, className: "pointer-events-none absolute inset-y-0 w-0.5 bg-bad", style: { left: `${nowAt}%` } })), inRow.map((item) => {
185
+ const { start, end } = placed(item);
186
+ const left = percent(+start);
187
+ const width = Math.max(percent(+end) - left, 0.6);
188
+ const tone = toneOf?.(item) ?? "neutral";
189
+ const title = titleOf?.(item);
190
+ const text = labelOf?.(item) ??
191
+ [title, describe(start, end)]
192
+ .filter((part) => part !== undefined)
193
+ .join(" · ");
194
+ const dragging = draft !== null && draft.id === keyOf(item);
195
+ return (_jsxs("button", { type: "button", title: onMove === undefined ? undefined : words.moveHint, "aria-label": typeof text === "string" ? text : title, ...(onMove === undefined
196
+ ? {}
197
+ : {
198
+ "aria-keyshortcuts": "Alt+ArrowLeft Alt+ArrowRight Alt+ArrowUp Alt+ArrowDown",
199
+ }), onPointerDown: (event) => drag(event, item, "move"), onKeyDown: (event) => byKeyboard(event, item), onClick: (event) => {
200
+ event.stopPropagation();
201
+ if (justDragged.current) {
202
+ justDragged.current = false;
203
+ return;
204
+ }
205
+ onOpen?.(item);
206
+ }, className: `absolute z-1 flex items-center overflow-hidden rounded-control px-2 text-left text-micro font-medium whitespace-nowrap shadow-raised focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary ${FILL[tone]} ${onMove === undefined ? "" : "cursor-grab active:cursor-grabbing"} ${dragging ? "opacity-80 ring-2 ring-primary" : ""}`, style: {
207
+ left: `${left}%`,
208
+ width: `${width}%`,
209
+ top: GAP + (lane.get(item) ?? 0) * (BAR + GAP),
210
+ height: BAR,
211
+ }, children: [_jsx("span", { className: "min-w-0 truncate", children: text }), onMove === undefined ? null : (_jsx("span", { "aria-hidden": true, onPointerDown: (event) => drag(event, item, "resize"), className: "absolute inset-y-0 right-0 w-2 cursor-ew-resize" }))] }, keyOf(item)));
212
+ })] })] }, row.id));
213
+ })] }) }), _jsx("span", { role: "status", className: "sr-only", children: said })] }));
214
+ }
@@ -0,0 +1,30 @@
1
+ import { type TimelineScale } from "./time.js";
2
+ import type { ReactNode } from "react";
3
+ export type TimelineToolbarProps = {
4
+ /** The period being looked at, and what it says: «29 de octubre de 2026». */
5
+ title?: ReactNode;
6
+ /** Where it stands. Without `onDateChange` there is no date navigation (a project Gantt shows
7
+ * its whole span). */
8
+ date?: Date;
9
+ onDateChange?: (next: Date) => void;
10
+ /** The arrows: one period back or forward. */
11
+ onStep?: (steps: -1 | 1) => void;
12
+ scale?: TimelineScale;
13
+ onScaleChange?: (next: TimelineScale) => void;
14
+ /** The scales offered, in order. All four by default. */
15
+ scales?: readonly TimelineScale[];
16
+ /** 1 (widest view) to 5 (closest). */
17
+ zoom?: number;
18
+ onZoomChange?: (next: number) => void;
19
+ fullscreen?: boolean;
20
+ onFullscreenChange?: (next: boolean) => void;
21
+ };
22
+ /**
23
+ * The bar over a timeline — a Gantt, a planning, a schedule: where am I (‹ › a date · today),
24
+ * how close (zoom), at what scale (day · week · month · year) and the whole screen.
25
+ *
26
+ * Context: Odoo's Gantt toolbar, as one piece shared by `RecordTimeline` and `RecordGantt`, so
27
+ * the two read the same. Every part is optional: what has no handler is not drawn — a
28
+ * control that does nothing is a dead end.
29
+ */
30
+ export declare function TimelineToolbar({ title, date, onDateChange, onStep, scale, onScaleChange, scales, zoom, onZoomChange, fullscreen, onFullscreenChange, }: TimelineToolbarProps): import("react").JSX.Element;
@@ -0,0 +1,31 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { TIMELINE_SCALES, } from "./time.js";
4
+ import { useUiCopy } from "../../foundations/copy.js";
5
+ import { Button, IconButton } from "../../primitives/button/Button.js";
6
+ import { SegmentedControl } from "../../primitives/choice/SegmentedControl.js";
7
+ import { Slider } from "../../primitives/choice/Slider.js";
8
+ import { DatePicker } from "../../primitives/date/DatePicker.js";
9
+ import { ChevronLeft, ChevronRight, Maximize2, Minimize2, ZoomIn, ZoomOut, } from "lucide-react";
10
+ /** `2026-10-29` ↔ a local Date, the way `DatePicker` speaks. */
11
+ const isoDay = (date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
12
+ const fromIso = (iso) => {
13
+ const [y, m, d] = iso.split("-").map(Number);
14
+ return new Date(y ?? 1970, (m ?? 1) - 1, d ?? 1);
15
+ };
16
+ /**
17
+ * The bar over a timeline — a Gantt, a planning, a schedule: where am I (‹ › a date · today),
18
+ * how close (zoom), at what scale (day · week · month · year) and the whole screen.
19
+ *
20
+ * Context: Odoo's Gantt toolbar, as one piece shared by `RecordTimeline` and `RecordGantt`, so
21
+ * the two read the same. Every part is optional: what has no handler is not drawn — a
22
+ * control that does nothing is a dead end.
23
+ */
24
+ export function TimelineToolbar({ title, date, onDateChange, onStep, scale, onScaleChange, scales = TIMELINE_SCALES, zoom, onZoomChange, fullscreen = false, onFullscreenChange, }) {
25
+ const copy = useUiCopy();
26
+ const words = copy.timeline;
27
+ return (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2 border-b border-line px-3 py-2", children: [onStep === undefined && onDateChange === undefined ? null : (_jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [onStep === undefined ? null : (_jsxs(_Fragment, { children: [_jsx(IconButton, { icon: ChevronLeft, label: words.prev, onClick: () => onStep(-1) }), _jsx(IconButton, { icon: ChevronRight, label: words.next, onClick: () => onStep(1) })] })), onDateChange === undefined || date === undefined ? null : (_jsxs(_Fragment, { children: [_jsx("div", { className: "w-40", children: _jsx(DatePicker, { "aria-label": words.goTo, value: isoDay(date), onChange: (next) => {
28
+ if (next !== null)
29
+ onDateChange(fromIso(next));
30
+ } }) }), _jsx(Button, { role: "secondary", size: "sm", onClick: () => onDateChange(new Date()), children: words.today })] }))] })), title === undefined ? null : (_jsx("p", { className: "min-w-0 flex-1 truncate text-body font-semibold text-ink first-letter:uppercase", children: title })), _jsxs("div", { className: "ml-auto flex shrink-0 flex-wrap items-center gap-3", children: [zoom === undefined || onZoomChange === undefined ? null : (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(IconButton, { icon: ZoomOut, label: words.zoomOut, disabled: zoom <= 1, onClick: () => onZoomChange(Math.max(1, zoom - 1)) }), _jsx("div", { className: "w-24", children: _jsx(Slider, { "aria-label": words.zoom, value: zoom, min: 1, max: 5, output: false, onChange: onZoomChange }) }), _jsx(IconButton, { icon: ZoomIn, label: words.zoomIn, disabled: zoom >= 5, onClick: () => onZoomChange(Math.min(5, zoom + 1)) })] })), scale === undefined || onScaleChange === undefined ? null : (_jsx(SegmentedControl, { "aria-label": words.scale, size: "sm", value: scale, onChange: (next) => onScaleChange(next), options: scales.map((one) => ({ value: one, label: words.scales[one] })) })), onFullscreenChange === undefined ? null : (_jsx(IconButton, { icon: fullscreen ? Minimize2 : Maximize2, label: fullscreen ? words.exitFullscreen : words.fullscreen, onClick: () => onFullscreenChange(!fullscreen) }))] })] }));
31
+ }
@@ -0,0 +1,3 @@
1
+ export { RecordTimeline, type RecordTimelineProps, type TimelineRow, } from "./RecordTimeline.js";
2
+ export { TIMELINE_SCALES, type TimelineScale, } from "./time.js";
3
+ export { TimelineToolbar, type TimelineToolbarProps, } from "./TimelineToolbar.js";
@@ -0,0 +1,3 @@
1
+ export { RecordTimeline, } from "./RecordTimeline.js";
2
+ export { TIMELINE_SCALES, } from "./time.js";
3
+ export { TimelineToolbar, } from "./TimelineToolbar.js";
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Context: the arithmetic of a timeline, apart from any screen — what period a scale shows
3
+ * around a date, its columns, how far a drag snaps, and how bars that overlap in one row are
4
+ * stacked. Pure, so it is tested without a browser (`tests/timeline.test.ts`).
5
+ */
6
+ export type TimelineScale = "day" | "week" | "month" | "year";
7
+ export declare const TIMELINE_SCALES: readonly TimelineScale[];
8
+ /** One column of the scale: where it starts and what it says. */
9
+ export type TimeUnit = {
10
+ start: Date;
11
+ label: string;
12
+ weekend: boolean;
13
+ };
14
+ export type TimeRange = {
15
+ start: Date;
16
+ end: Date;
17
+ title: string;
18
+ units: TimeUnit[];
19
+ };
20
+ /** The period a scale shows around `date`, with its columns, written in `locale`. */
21
+ export declare function rangeOf(scale: TimelineScale, date: Date, locale: string): TimeRange;
22
+ /** The date one period before or after: the toolbar's arrows. */
23
+ export declare function shiftDate(scale: TimelineScale, date: Date, steps: number): Date;
24
+ /** How far a drag or an arrow key moves a bar: a quarter of an hour on a day, an hour on a
25
+ * week, a day on a month or a year. */
26
+ export declare function snapOf(scale: TimelineScale): number;
27
+ /** What a click on an empty column creates: that column. */
28
+ export declare function unitSpan(scale: TimelineScale, at: Date): {
29
+ start: Date;
30
+ end: Date;
31
+ };
32
+ export declare function snap(time: number, step: number): number;
33
+ /**
34
+ * The lane of each bar in a row: bars that overlap go one under the other, the way Odoo stacks
35
+ * two attendances of one morning. Greedy, in start order: a bar takes the first lane free when
36
+ * it starts.
37
+ */
38
+ export declare function lanesOf<T>(items: readonly T[], startOf: (item: T) => Date, endOf: (item: T) => Date): {
39
+ lane: Map<T, number>;
40
+ count: number;
41
+ };
42
+ /** «03:48»: a duration as hours and minutes. */
43
+ export declare function hoursMinutes(ms: number): string;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Context: the arithmetic of a timeline, apart from any screen — what period a scale shows
3
+ * around a date, its columns, how far a drag snaps, and how bars that overlap in one row are
4
+ * stacked. Pure, so it is tested without a browser (`tests/timeline.test.ts`).
5
+ */
6
+ export const TIMELINE_SCALES = ["day", "week", "month", "year"];
7
+ const MINUTE = 60_000;
8
+ const HOUR = 60 * MINUTE;
9
+ const DAY = 24 * HOUR;
10
+ const startOfDay = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate());
11
+ /** Monday: the week of a workplace in Bolivia, not the US Sunday. */
12
+ const startOfWeek = (date) => {
13
+ const day = startOfDay(date);
14
+ return new Date(day.getFullYear(), day.getMonth(), day.getDate() - ((day.getDay() + 6) % 7));
15
+ };
16
+ const isWeekend = (date) => date.getDay() === 0 || date.getDay() === 6;
17
+ /** The period a scale shows around `date`, with its columns, written in `locale`. */
18
+ export function rangeOf(scale, date, locale) {
19
+ const say = (options) => new Intl.DateTimeFormat(locale, options);
20
+ if (scale === "day") {
21
+ const start = startOfDay(date);
22
+ const units = Array.from({ length: 24 }, (_, hour) => ({
23
+ start: new Date(start.getFullYear(), start.getMonth(), start.getDate(), hour),
24
+ label: String(hour),
25
+ weekend: false,
26
+ }));
27
+ return {
28
+ start,
29
+ end: new Date(start.getFullYear(), start.getMonth(), start.getDate() + 1),
30
+ title: say({ weekday: "long", day: "numeric", month: "long", year: "numeric" }).format(start),
31
+ units,
32
+ };
33
+ }
34
+ if (scale === "week") {
35
+ const start = startOfWeek(date);
36
+ const units = Array.from({ length: 7 }, (_, day) => {
37
+ const at = new Date(start.getFullYear(), start.getMonth(), start.getDate() + day);
38
+ return {
39
+ start: at,
40
+ label: say({ weekday: "short", day: "numeric" }).format(at),
41
+ weekend: isWeekend(at),
42
+ };
43
+ });
44
+ const end = new Date(start.getFullYear(), start.getMonth(), start.getDate() + 7);
45
+ return {
46
+ start,
47
+ end,
48
+ title: say({ day: "numeric", month: "long", year: "numeric" }).formatRange(start, new Date(end.getTime() - DAY)),
49
+ units,
50
+ };
51
+ }
52
+ if (scale === "month") {
53
+ const start = new Date(date.getFullYear(), date.getMonth(), 1);
54
+ const end = new Date(date.getFullYear(), date.getMonth() + 1, 1);
55
+ const days = Math.round((end.getTime() - start.getTime()) / DAY);
56
+ const units = Array.from({ length: days }, (_, day) => {
57
+ const at = new Date(start.getFullYear(), start.getMonth(), day + 1);
58
+ return { start: at, label: String(day + 1), weekend: isWeekend(at) };
59
+ });
60
+ return {
61
+ start,
62
+ end,
63
+ title: say({ month: "long", year: "numeric" }).format(start),
64
+ units,
65
+ };
66
+ }
67
+ const start = new Date(date.getFullYear(), 0, 1);
68
+ const units = Array.from({ length: 12 }, (_, month) => {
69
+ const at = new Date(date.getFullYear(), month, 1);
70
+ return { start: at, label: say({ month: "short" }).format(at), weekend: false };
71
+ });
72
+ return {
73
+ start,
74
+ end: new Date(date.getFullYear() + 1, 0, 1),
75
+ title: String(date.getFullYear()),
76
+ units,
77
+ };
78
+ }
79
+ /** The date one period before or after: the toolbar's arrows. */
80
+ export function shiftDate(scale, date, steps) {
81
+ const y = date.getFullYear();
82
+ const m = date.getMonth();
83
+ const d = date.getDate();
84
+ if (scale === "day")
85
+ return new Date(y, m, d + steps);
86
+ if (scale === "week")
87
+ return new Date(y, m, d + 7 * steps);
88
+ if (scale === "month")
89
+ return new Date(y, m + steps, 1);
90
+ return new Date(y + steps, 0, 1);
91
+ }
92
+ /** How far a drag or an arrow key moves a bar: a quarter of an hour on a day, an hour on a
93
+ * week, a day on a month or a year. */
94
+ export function snapOf(scale) {
95
+ return { day: 15 * MINUTE, week: HOUR, month: DAY, year: DAY }[scale];
96
+ }
97
+ /** What a click on an empty column creates: that column. */
98
+ export function unitSpan(scale, at) {
99
+ if (scale === "day") {
100
+ const start = new Date(at.getFullYear(), at.getMonth(), at.getDate(), at.getHours());
101
+ return { start, end: new Date(start.getTime() + HOUR) };
102
+ }
103
+ if (scale === "year") {
104
+ const start = new Date(at.getFullYear(), at.getMonth(), 1);
105
+ return { start, end: new Date(at.getFullYear(), at.getMonth() + 1, 1) };
106
+ }
107
+ const start = startOfDay(at);
108
+ return { start, end: new Date(start.getTime() + DAY) };
109
+ }
110
+ export function snap(time, step) {
111
+ return Math.round(time / step) * step;
112
+ }
113
+ /**
114
+ * The lane of each bar in a row: bars that overlap go one under the other, the way Odoo stacks
115
+ * two attendances of one morning. Greedy, in start order: a bar takes the first lane free when
116
+ * it starts.
117
+ */
118
+ export function lanesOf(items, startOf, endOf) {
119
+ const ends = [];
120
+ const lane = new Map();
121
+ for (const item of [...items].sort((a, b) => +startOf(a) - +startOf(b))) {
122
+ const start = +startOf(item);
123
+ let at = ends.findIndex((end) => end <= start);
124
+ if (at === -1)
125
+ at = ends.length;
126
+ ends[at] = +endOf(item);
127
+ lane.set(item, at);
128
+ }
129
+ return { lane, count: Math.max(1, ends.length) };
130
+ }
131
+ /** «03:48»: a duration as hours and minutes. */
132
+ export function hoursMinutes(ms) {
133
+ const minutes = Math.round(ms / MINUTE);
134
+ return `${String(Math.floor(minutes / 60)).padStart(2, "0")}:${String(minutes % 60).padStart(2, "0")}`;
135
+ }