@milktop/react-ui 0.0.0-stage → 0.1.2

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 (250) hide show
  1. package/README.md +173 -2
  2. package/dist/accordion/Accordion.d.ts +65 -0
  3. package/dist/accordion/Accordion.js +61 -0
  4. package/dist/accordion/accordion.css +17 -0
  5. package/dist/action-bar/ActionBar.d.ts +33 -0
  6. package/dist/action-bar/ActionBar.js +48 -0
  7. package/dist/action-bar/action-bar.css +22 -0
  8. package/dist/alert/Alert.d.ts +28 -0
  9. package/dist/alert/Alert.js +12 -0
  10. package/dist/alert/alert.css +32 -0
  11. package/dist/app-shell/AppShell.d.ts +38 -0
  12. package/dist/app-shell/AppShell.js +92 -0
  13. package/dist/app-shell/Topbar.d.ts +20 -0
  14. package/dist/app-shell/Topbar.js +13 -0
  15. package/dist/app-shell/app-shell.css +41 -0
  16. package/dist/app-shell/shell.d.ts +9 -0
  17. package/dist/app-shell/shell.js +3 -0
  18. package/dist/attachments/Attachments.d.ts +84 -0
  19. package/dist/attachments/Attachments.js +235 -0
  20. package/dist/attachments/attachments.css +86 -0
  21. package/dist/avatar/Avatar.d.ts +31 -0
  22. package/dist/avatar/Avatar.js +41 -0
  23. package/dist/avatar/AvatarGroup.d.ts +41 -0
  24. package/dist/avatar/AvatarGroup.js +17 -0
  25. package/dist/avatar/avatar.css +73 -0
  26. package/dist/badge/Badge.d.ts +26 -0
  27. package/dist/badge/Badge.js +10 -0
  28. package/dist/badge/badge.css +24 -0
  29. package/dist/banner/Banner.d.ts +33 -0
  30. package/dist/banner/Banner.js +14 -0
  31. package/dist/banner/banner.css +25 -0
  32. package/dist/breadcrumbs/Breadcrumbs.d.ts +39 -0
  33. package/dist/breadcrumbs/Breadcrumbs.js +28 -0
  34. package/dist/breadcrumbs/breadcrumbs.css +18 -0
  35. package/dist/button/Button.d.ts +40 -0
  36. package/dist/button/Button.js +19 -0
  37. package/dist/button/button.css +63 -0
  38. package/dist/card/Card.d.ts +355 -0
  39. package/dist/card/Card.js +56 -0
  40. package/dist/card/card.css +43 -0
  41. package/dist/chart/AreaChart.d.ts +19 -0
  42. package/dist/chart/AreaChart.js +61 -0
  43. package/dist/chart/BarChart.d.ts +3 -0
  44. package/dist/chart/BarChart.js +60 -0
  45. package/dist/chart/ChartFrame.d.ts +25 -0
  46. package/dist/chart/ChartFrame.js +22 -0
  47. package/dist/chart/DonutChart.d.ts +20 -0
  48. package/dist/chart/DonutChart.js +41 -0
  49. package/dist/chart/Sparkline.d.ts +26 -0
  50. package/dist/chart/Sparkline.js +36 -0
  51. package/dist/chart/chart.css +51 -0
  52. package/dist/chart/chart.d.ts +23 -0
  53. package/dist/chart/chart.js +19 -0
  54. package/dist/checkbox/Checkbox.d.ts +50 -0
  55. package/dist/checkbox/Checkbox.js +19 -0
  56. package/dist/checkbox/CheckboxGroup.d.ts +44 -0
  57. package/dist/checkbox/CheckboxGroup.js +80 -0
  58. package/dist/checkbox/checkbox.css +49 -0
  59. package/dist/collapsible/Collapsible.d.ts +41 -0
  60. package/dist/collapsible/Collapsible.js +15 -0
  61. package/dist/collapsible/collapsible.css +20 -0
  62. package/dist/color-scheme.d.ts +18 -0
  63. package/dist/color-scheme.js +84 -0
  64. package/dist/command/Command.d.ts +64 -0
  65. package/dist/command/Command.js +162 -0
  66. package/dist/command/command.css +47 -0
  67. package/dist/confirm/confirm.css +5 -0
  68. package/dist/confirm/confirm.d.ts +42 -0
  69. package/dist/confirm/confirm.js +115 -0
  70. package/dist/copy-button/CopyButton.d.ts +30 -0
  71. package/dist/copy-button/CopyButton.js +23 -0
  72. package/dist/copy-button/copy-button.css +12 -0
  73. package/dist/css.d.ts +2 -0
  74. package/dist/data-list/DataList.d.ts +55 -0
  75. package/dist/data-list/DataList.js +43 -0
  76. package/dist/data-list/data-list.css +19 -0
  77. package/dist/date-picker/DatePicker.d.ts +95 -0
  78. package/dist/date-picker/DatePicker.js +237 -0
  79. package/dist/date-picker/date-picker.css +53 -0
  80. package/dist/dialog/Dialog.d.ts +98 -0
  81. package/dist/dialog/Dialog.js +92 -0
  82. package/dist/dialog/dialog.css +31 -0
  83. package/dist/divider/Divider.d.ts +27 -0
  84. package/dist/divider/Divider.js +8 -0
  85. package/dist/divider/divider.css +12 -0
  86. package/dist/editable/Editable.d.ts +47 -0
  87. package/dist/editable/Editable.js +51 -0
  88. package/dist/editable/editable.css +25 -0
  89. package/dist/editor/Editor.d.ts +59 -0
  90. package/dist/editor/Editor.js +220 -0
  91. package/dist/editor/editor.css +42 -0
  92. package/dist/editor/image.d.ts +1 -0
  93. package/dist/editor/image.js +23 -0
  94. package/dist/empty-state/EmptyState.d.ts +22 -0
  95. package/dist/empty-state/EmptyState.js +9 -0
  96. package/dist/empty-state/empty-state.css +14 -0
  97. package/dist/event-calendar/EventCalendar.d.ts +213 -0
  98. package/dist/event-calendar/EventCalendar.js +686 -0
  99. package/dist/event-calendar/dates.d.ts +8 -0
  100. package/dist/event-calendar/dates.js +21 -0
  101. package/dist/event-calendar/event-calendar.css +157 -0
  102. package/dist/field/Field.d.ts +66 -0
  103. package/dist/field/Field.js +23 -0
  104. package/dist/field/field.css +26 -0
  105. package/dist/file-upload/FileUpload.d.ts +60 -0
  106. package/dist/file-upload/FileUpload.js +102 -0
  107. package/dist/file-upload/file-upload.css +32 -0
  108. package/dist/format/FormatNumber.d.ts +42 -0
  109. package/dist/format/FormatNumber.js +59 -0
  110. package/dist/format/LocaleProvider.d.ts +33 -0
  111. package/dist/format/LocaleProvider.js +48 -0
  112. package/dist/format/format.d.ts +37 -0
  113. package/dist/format/format.js +74 -0
  114. package/dist/format/messages.d.ts +211 -0
  115. package/dist/format/messages.js +202 -0
  116. package/dist/highlight/Highlight.d.ts +11 -0
  117. package/dist/highlight/Highlight.js +14 -0
  118. package/dist/highlight/highlight.css +17 -0
  119. package/dist/hover-card/HoverCard.d.ts +47 -0
  120. package/dist/hover-card/HoverCard.js +18 -0
  121. package/dist/hover-card/hover-card.css +19 -0
  122. package/dist/index.d.ts +84 -0
  123. package/dist/index.js +85 -0
  124. package/dist/input/Input.d.ts +41 -0
  125. package/dist/input/Input.js +48 -0
  126. package/dist/input/input.css +25 -0
  127. package/dist/kbd/Kbd.d.ts +19 -0
  128. package/dist/kbd/Kbd.js +20 -0
  129. package/dist/kbd/kbd.css +8 -0
  130. package/dist/menu/Menu.d.ts +223 -0
  131. package/dist/menu/Menu.js +275 -0
  132. package/dist/menu/menu.css +38 -0
  133. package/dist/native-select/NativeSelect.d.ts +36 -0
  134. package/dist/native-select/NativeSelect.js +51 -0
  135. package/dist/native-select/native-select.css +16 -0
  136. package/dist/number-input/NumberInput.d.ts +56 -0
  137. package/dist/number-input/NumberInput.js +67 -0
  138. package/dist/number-input/number-input.css +21 -0
  139. package/dist/page/Page.d.ts +30 -0
  140. package/dist/page/Page.js +9 -0
  141. package/dist/page/page.css +17 -0
  142. package/dist/pagination/Pagination.d.ts +31 -0
  143. package/dist/pagination/Pagination.js +58 -0
  144. package/dist/pagination/pagination.css +15 -0
  145. package/dist/password-input/PasswordInput.d.ts +47 -0
  146. package/dist/password-input/PasswordInput.js +35 -0
  147. package/dist/password-input/password-input.css +18 -0
  148. package/dist/pin-input/PinInput.d.ts +45 -0
  149. package/dist/pin-input/PinInput.js +22 -0
  150. package/dist/pin-input/pin-input.css +13 -0
  151. package/dist/popover/Popover.d.ts +65 -0
  152. package/dist/popover/Popover.js +26 -0
  153. package/dist/popover/popover.css +16 -0
  154. package/dist/progress/Progress.d.ts +56 -0
  155. package/dist/progress/Progress.js +28 -0
  156. package/dist/progress/progress.css +24 -0
  157. package/dist/prose/Prose.d.ts +23 -0
  158. package/dist/prose/Prose.js +10 -0
  159. package/dist/prose/prose.css +29 -0
  160. package/dist/radio-group/RadioGroup.d.ts +59 -0
  161. package/dist/radio-group/RadioGroup.js +37 -0
  162. package/dist/radio-group/radio-group.css +30 -0
  163. package/dist/rolling-number/RollingNumber.d.ts +27 -0
  164. package/dist/rolling-number/RollingNumber.js +18 -0
  165. package/dist/rolling-number/rolling-number.css +10 -0
  166. package/dist/segment-group/Segmented.d.ts +68 -0
  167. package/dist/segment-group/Segmented.js +79 -0
  168. package/dist/segment-group/segment-group.css +25 -0
  169. package/dist/select/ListSelect.d.ts +5 -0
  170. package/dist/select/ListSelect.js +82 -0
  171. package/dist/select/SearchSelect.d.ts +43 -0
  172. package/dist/select/SearchSelect.js +219 -0
  173. package/dist/select/Select.d.ts +147 -0
  174. package/dist/select/Select.js +32 -0
  175. package/dist/select/items.d.ts +17 -0
  176. package/dist/select/items.js +14 -0
  177. package/dist/select/search-select.css +45 -0
  178. package/dist/select/select.css +52 -0
  179. package/dist/sheet/Sheet.d.ts +88 -0
  180. package/dist/sheet/Sheet.js +66 -0
  181. package/dist/sheet/sheet.css +44 -0
  182. package/dist/sidebar/NavGroup.d.ts +29 -0
  183. package/dist/sidebar/NavGroup.js +78 -0
  184. package/dist/sidebar/NavItem.d.ts +33 -0
  185. package/dist/sidebar/NavItem.js +50 -0
  186. package/dist/sidebar/NavSection.d.ts +20 -0
  187. package/dist/sidebar/NavSection.js +44 -0
  188. package/dist/sidebar/Sidebar.d.ts +65 -0
  189. package/dist/sidebar/Sidebar.js +59 -0
  190. package/dist/sidebar/SidebarUser.d.ts +31 -0
  191. package/dist/sidebar/SidebarUser.js +14 -0
  192. package/dist/sidebar/context.d.ts +13 -0
  193. package/dist/sidebar/context.js +6 -0
  194. package/dist/sidebar/sidebar.css +109 -0
  195. package/dist/skeleton/Skeleton.d.ts +25 -0
  196. package/dist/skeleton/Skeleton.js +8 -0
  197. package/dist/skeleton/skeleton.css +13 -0
  198. package/dist/slider/Slider.d.ts +70 -0
  199. package/dist/slider/Slider.js +65 -0
  200. package/dist/slider/slider.css +17 -0
  201. package/dist/sortable-list/SortableList.d.ts +46 -0
  202. package/dist/sortable-list/SortableList.js +85 -0
  203. package/dist/sortable-list/sortable-list.css +25 -0
  204. package/dist/spinner/Spinner.d.ts +18 -0
  205. package/dist/spinner/Spinner.js +11 -0
  206. package/dist/spinner/spinner.css +9 -0
  207. package/dist/stat/Stat.d.ts +42 -0
  208. package/dist/stat/Stat.js +21 -0
  209. package/dist/stat/Stats.d.ts +23 -0
  210. package/dist/stat/Stats.js +8 -0
  211. package/dist/stat/stat.css +34 -0
  212. package/dist/swatch-picker/SwatchPicker.d.ts +45 -0
  213. package/dist/swatch-picker/SwatchPicker.js +54 -0
  214. package/dist/swatch-picker/swatch-picker.css +38 -0
  215. package/dist/switch/Switch.d.ts +39 -0
  216. package/dist/switch/Switch.js +17 -0
  217. package/dist/switch/switch.css +13 -0
  218. package/dist/table/Table.d.ts +90 -0
  219. package/dist/table/Table.js +145 -0
  220. package/dist/table/table.css +44 -0
  221. package/dist/tabs/Tabs.d.ts +87 -0
  222. package/dist/tabs/Tabs.js +90 -0
  223. package/dist/tabs/tabs.css +53 -0
  224. package/dist/tags-input/TagsInput.d.ts +69 -0
  225. package/dist/tags-input/TagsInput.js +95 -0
  226. package/dist/tags-input/tags-input.css +37 -0
  227. package/dist/textarea/Textarea.d.ts +24 -0
  228. package/dist/textarea/Textarea.js +51 -0
  229. package/dist/textarea/textarea.css +14 -0
  230. package/dist/theme-toggle/ThemeToggle.d.ts +49 -0
  231. package/dist/theme-toggle/ThemeToggle.js +71 -0
  232. package/dist/theme-toggle/theme-toggle.css +23 -0
  233. package/dist/theme.css +121 -0
  234. package/dist/time-input/TimeInput.d.ts +25 -0
  235. package/dist/time-input/TimeInput.js +20 -0
  236. package/dist/timeline/Timeline.d.ts +50 -0
  237. package/dist/timeline/Timeline.js +15 -0
  238. package/dist/timeline/timeline.css +29 -0
  239. package/dist/toast/Toaster.d.ts +36 -0
  240. package/dist/toast/Toaster.js +25 -0
  241. package/dist/toast/toast.css +33 -0
  242. package/dist/tooltip/Tooltip.d.ts +55 -0
  243. package/dist/tooltip/Tooltip.js +48 -0
  244. package/dist/tooltip/tooltip.css +18 -0
  245. package/dist/tree-view/TreeView.d.ts +86 -0
  246. package/dist/tree-view/TreeView.js +74 -0
  247. package/dist/tree-view/tree-view.css +29 -0
  248. package/dist/utils.d.ts +19 -0
  249. package/dist/utils.js +59 -0
  250. package/package.json +99 -4
@@ -0,0 +1,686 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * A calendar of events by the hour: a day, a few days or a week across,
4
+ * hours down, and optionally a column per resource (a vet, a tutor, a room)
5
+ * within each day. Events sit by their times, side by side where they
6
+ * overlap; all-day and multi-day events sit in a strip along the top.
7
+ *
8
+ * <EventCalendar events={lessons} onSelect={book} onEventClick={open} onEventChange={update} />
9
+ * <EventCalendar view="day" resources={vets} slotMinutes={15} events={appointments} … />
10
+ *
11
+ * Times are local ISO strings ('2025-10-09T16:00'), as DatePicker's dates are;
12
+ * an all-day event has dates only ('2025-10-09', its `end` the last day).
13
+ *
14
+ * Events and resources
15
+ * - `events`: `{ id, title, start, end, allDay, resourceId, description,
16
+ * color, editable, blocked, pending }`; `color` is 'accent' (default),
17
+ * 'success', 'warning', 'danger', 'neutral' or any CSS colour;
18
+ * `editable: false` fixes one; `blocked` draws it hatched (time taken out,
19
+ * not an appointment); `pending` faded and fixed (saving)
20
+ * - `resources`: `{ id, title, color, hours, offLabel }` (and anything for
21
+ * `renderResource`): a column each, per day; events go in their
22
+ * `resourceId`'s column. Change the list to add or remove one (a vet
23
+ * present that day). `color` ('accent', 'success', 'warning', 'danger',
24
+ * 'neutral' or any CSS colour) tints its header, its hover, selections and
25
+ * drops, and is its events' colour unless they set their own. `hours`:
26
+ * when it's available on each day shown, as `[{ start: '08:00', end: '13:00' }]`
27
+ * (a morning; two for a lunch break; `[]` for not at all): the rest is
28
+ * shaded and refused, with `offLabel` as the reason
29
+ *
30
+ * What's shown
31
+ * - `view`, `defaultView`: 'day', 'week' (default), 'workweek', or a number
32
+ * of days from `date`; `onViewChange`. `views`: the toolbar's choices
33
+ * (['day', 'week']; [] for none). On a narrow screen it shows a day
34
+ * - `date`, `defaultDate`: a day in the range shown (today's by default);
35
+ * `onDateChange`, from the toolbar (its title opens a calendar)
36
+ * - `weekStartsOn`: 1 (Monday, default) or 0; `startHour`, `endHour` (7, 21);
37
+ * `slotMinutes`: what selecting, moving and resizing snap to (30);
38
+ * `hourHeight`: a CSS length ('3rem')
39
+ * - `height`: a CSS length: the hours scroll inside it under the headers;
40
+ * `scrollToTime`: where it opens scrolled to ('08:00', or 'now'), once
41
+ * - `rows`: extra rows above the all-day strip, a cell per day (or per
42
+ * column with `perColumn`), e.g. the day's messages:
43
+ * `[{ key, label, render: (date, resource) => …, onClick: (date, resource) => … }]`
44
+ * - `allDayMax`: all-day lanes shown before a "+2 more" (3)
45
+ *
46
+ * Changing things
47
+ * - `onSelect({ start, end, resourceId, allDay }, keys)`: an empty slot was
48
+ * clicked or a time dragged out, or (`allDay`) days in the all-day strip,
49
+ * e.g. to book it. `keys` says which of Shift, Alt, Ctrl and ⌘ were held
50
+ * (say, Shift to block time instead of booking it)
51
+ * - `onEventClick(event)`: an event was clicked (not dragged), e.g. to open it
52
+ * - `onDayClick(date)`: a day's header was clicked (an ISO date), e.g. to show
53
+ * that day; `onResourceClick(resource, date)`: a resource's header
54
+ * - `onRangeChange({ start, end, view })`: the days shown, as ISO dates (the
55
+ * last one included), when it first shows and whenever they change: load
56
+ * that range's events from your server
57
+ * - `onEventChange(event, { start, end, resourceId }, { kind })`: makes
58
+ * events movable and resizable: drag one to another time, day or resource,
59
+ * or its bottom edge to change its length; or focus it and use ↑↓ (a slot),
60
+ * ←→ (a column), Shift+↑↓ (its length). The change spreads straight into
61
+ * the event (`{ ...event, ...change }`); `kind` is 'move' or 'resize', and
62
+ * comparing `resourceId` with the event's shows a change of resource.
63
+ * Update your events in response (or don't, and it stays where it was)
64
+ * - `isAvailable({ start, end, resourceId, allDay, event })`: true, or false
65
+ * or the reason it isn't ('Dr Patel is away'): unavailable time is shaded
66
+ * (the reason on hover) and selecting, moving or resizing into it is
67
+ * refused, calling `onUnavailable(reason, attempt)` (to show a toast).
68
+ * `event` is the one being moved, so it doesn't clash with itself;
69
+ * `shading` is true when it's only asking what to shade (so clashes with
70
+ * other events, which show anyway, can be left to bookings and drops)
71
+ * - `pinned`: an event being placed (it can be from another week): while
72
+ * set, a click on a time moves it there (through `onEventChange`, keeping
73
+ * its length); Escape or the bar's Cancel call `onPinnedChange(null)`.
74
+ * Clear it once you've moved it
75
+ *
76
+ * - `eventMenu(event)`, `slotMenu(slot)`: items for a menu on right-click
77
+ * (or the Menu key, or Shift+F10, on a focused event): of an event, or of
78
+ * an empty time or all-day day (`slot` is `{ start, end, resourceId, allDay }`).
79
+ * Items are `{ label, icon, shortcut, danger, disabled, onSelect }`, or
80
+ * `{ separator: true }`, or `{ group: 'Heading' }`; return nothing for no menu
81
+ *
82
+ * Drawing and words
83
+ * - `renderEvent(event, { start, end, resource, compact })`, `renderResource(resource)`,
84
+ * `renderDayHeader(date)`: draw those yourself
85
+ * - `eventLabel(event)`: what screen readers call an event (its title, if text)
86
+ * - `translations`: its words (Today, All day, the views, the announcements…),
87
+ * over the LocaleProvider's (English and French are built in);
88
+ * `locale`: for the dates and times (the LocaleProvider's by default)
89
+ * - `actions`: in the toolbar; `toolbar`: false hides it
90
+ */
91
+ import { Fragment, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
92
+ import 'iconify-icon';
93
+ import { Button } from '../button/Button.js';
94
+ import { Popover } from '../popover/Popover.js';
95
+ import { DatePicker } from '../date-picker/DatePicker.js';
96
+ import { Segmented } from '../segment-group/Segmented.js';
97
+ import { Menu } from '../menu/Menu.js';
98
+ import { useLocale, useMessages } from '../format/LocaleProvider.js';
99
+ import { cx, partsOf, useControllable } from '../utils.js';
100
+ import { addDays, addMinutes, minutesOfDay, parse, sameDay, startOfWeek, toISODate, toISODateTime } from './dates.js';
101
+ import '../theme.css';
102
+ import './event-calendar.css';
103
+ const named = ['accent', 'success', 'warning', 'danger', 'neutral'];
104
+ // A named colour as its token, or a CSS colour as it is.
105
+ const cssColor = (color) => color === 'accent' ? 'var(--ui-accent)' : color === 'success' ? 'var(--ui-success)'
106
+ : color === 'warning' ? '#d97706' : color === 'danger' ? 'var(--ui-danger)' : color === 'neutral' ? 'var(--ui-muted)' : color;
107
+ // The menu's hidden trigger (Zag marks it as its context trigger, so no part of ours).
108
+ const menuAnchorStyle = { position: 'fixed', top: 0, left: 0, width: 0, height: 0, overflow: 'hidden', pointerEvents: 'none' };
109
+ const isAllDay = (event) => event.allDay || !event.start.includes('T')
110
+ || event.start.slice(0, 10) !== event.end.slice(0, 10);
111
+ const minutesBetween = (a, b) => Math.round((b.getTime() - a.getTime()) / 60000);
112
+ // A day's events, side by side where they overlap: each takes the first
113
+ // free column in its cluster of overlapping events.
114
+ function place(events) {
115
+ const sorted = events.map((event) => ({ event, start: parse(event.start), end: parse(event.end) }))
116
+ .sort((a, b) => a.start.getTime() - b.start.getTime() || b.end.getTime() - a.end.getTime());
117
+ const placed = [];
118
+ let cluster = [];
119
+ let clusterEnd = 0;
120
+ const close = () => {
121
+ const columns = Math.max(1, ...cluster.map((p) => p.column + 1));
122
+ cluster.forEach((p) => { p.columns = columns; });
123
+ cluster = [];
124
+ };
125
+ for (const item of sorted) {
126
+ if (cluster.length && item.start.getTime() >= clusterEnd)
127
+ close();
128
+ const taken = cluster.filter((p) => p.end > item.start).map((p) => p.column);
129
+ let column = 0;
130
+ while (taken.includes(column))
131
+ column++;
132
+ const entry = { ...item, column, columns: 1 };
133
+ cluster.push(entry);
134
+ placed.push(entry);
135
+ clusterEnd = Math.max(clusterEnd, item.end.getTime());
136
+ }
137
+ close();
138
+ return placed;
139
+ }
140
+ export function EventCalendar({ events = [], resources = null, view: viewProp, defaultView = 'week', onViewChange, views = ['day', 'week'], date: dateProp, defaultDate, onDateChange, weekStartsOn = 1, startHour = 7, endHour = 21, slotMinutes = 30, hourHeight = '3rem', height = null, scrollToTime = null, rows = [], allDayMax = 3, onSelect, onEventClick, onDayClick, onResourceClick, onRangeChange, onEventChange, isAvailable, onUnavailable, pinned = null, onPinnedChange, eventMenu, slotMenu, renderEvent, renderResource, renderDayHeader, eventLabel, translations, actions = null, toolbar = true, locale: localeProp, className, classes, style, ...rest }) {
141
+ const app = useLocale();
142
+ const locale = localeProp ?? app;
143
+ const part = partsOf('event-calendar', classes);
144
+ const messages = useMessages();
145
+ const t = { ...messages.calendar, ...translations };
146
+ const [today] = useState(() => toISODate(new Date()));
147
+ const [date, setDate] = useControllable(dateProp, defaultDate ?? today, onDateChange);
148
+ const [chosenView, setView] = useControllable(viewProp, defaultView, onViewChange);
149
+ // Narrow (a phone): a day at a time.
150
+ const root = useRef(null);
151
+ const [narrow, setNarrow] = useState(false);
152
+ useEffect(() => {
153
+ const el = root.current;
154
+ if (!el)
155
+ return;
156
+ const observer = new ResizeObserver(() => setNarrow(el.clientWidth < 640));
157
+ observer.observe(el);
158
+ return () => observer.disconnect();
159
+ }, []);
160
+ const view = narrow ? 'day' : chosenView;
161
+ const viewLabel = (v) => v === 'day' ? t.day : v === 'week' ? t.week : v === 'workweek' ? t.workweek : t.days(v);
162
+ // The days shown, and how far the arrows go.
163
+ const current = parse(date);
164
+ const first = view === 'week' ? startOfWeek(current, weekStartsOn) : view === 'workweek' ? startOfWeek(current, 1) : current;
165
+ const count = view === 'day' ? 1 : view === 'week' ? 7 : view === 'workweek' ? 5 : Math.max(1, view);
166
+ const step = view === 'week' || view === 'workweek' ? 7 : count;
167
+ const dates = Array.from({ length: count }, (_, i) => addDays(first, i));
168
+ const lanes = resources?.length ? resources : [null];
169
+ const columns = dates.flatMap((d) => lanes.map((resource) => ({ date: d, iso: toISODate(d), resource })));
170
+ const hours = Array.from({ length: endHour - startHour }, (_, i) => startHour + i);
171
+ const span = (endHour - startHour) * 60;
172
+ // The days shown, reported when they change (to load their events).
173
+ const rangeStart = toISODate(dates[0]);
174
+ const rangeEnd = toISODate(dates[dates.length - 1]);
175
+ const reportRange = useRef(onRangeChange);
176
+ useEffect(() => { reportRange.current = onRangeChange; });
177
+ useEffect(() => { reportRange.current?.({ start: rangeStart, end: rangeEnd, view }); }, [rangeStart, rangeEnd, view]);
178
+ // The time now, for the line across today; moved on each minute.
179
+ const [now, setNow] = useState(() => new Date());
180
+ useEffect(() => {
181
+ const timer = setInterval(() => setNow(new Date()), 60000);
182
+ return () => clearInterval(timer);
183
+ }, []);
184
+ const dayFormat = new Intl.DateTimeFormat(locale, { weekday: 'short' });
185
+ const longDay = new Intl.DateTimeFormat(locale, { weekday: 'long', day: 'numeric', month: 'long' });
186
+ const hourFormat = new Intl.DateTimeFormat(locale, { hour: 'numeric' });
187
+ const timeFormat = new Intl.DateTimeFormat(locale, { hour: 'numeric', minute: '2-digit' });
188
+ const title = count === 1
189
+ ? new Intl.DateTimeFormat(locale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }).format(first)
190
+ : new Intl.DateTimeFormat(locale, { day: 'numeric', month: 'long', year: 'numeric' }).formatRange(first, dates[count - 1]);
191
+ const top = (minutes) => `${((minutes - startHour * 60) / span) * 100}%`;
192
+ const length = (minutes) => `${(minutes / span) * 100}%`;
193
+ const snap = (minutes) => Math.floor(minutes / slotMinutes) * slotMinutes;
194
+ const nameOf = (event) => eventLabel ? eventLabel(event) : typeof event.title === 'string' ? event.title : 'Event';
195
+ const resourceName = (column) => column?.resource && typeof column.resource.title === 'string' ? `, ${column.resource.title}` : '';
196
+ const columnOf = (event, iso) => columns.findIndex((c) => c.iso === iso && (!resources?.length || c.resource?.id === event.resourceId));
197
+ const [picking, setPicking] = useState(false);
198
+ const [status, setStatus] = useState('');
199
+ const [allDayOpen, setAllDayOpen] = useState(false);
200
+ // ---- One menu, opened where it's wanted (right-click, or the keyboard).
201
+ // It opens through a hidden context-menu trigger: a contextmenu event sent to
202
+ // it at the point is how Zag places a menu there.
203
+ const [menu, setMenu] = useState({ entries: [], actions: [] });
204
+ const menuAnchor = useRef(null);
205
+ const menuPoint = useRef(null);
206
+ function openMenu(x, y, items) {
207
+ if (!items?.length)
208
+ return false;
209
+ const actions = [];
210
+ const entries = items.map((item) => item.separator ? { separator: true } : item.group ? { group: item.group } : {
211
+ label: item.label, icon: item.icon, shortcut: item.shortcut, danger: item.danger, disabled: item.disabled,
212
+ value: actions.push(item.onSelect ?? (() => { })) - 1,
213
+ });
214
+ setMenu({ entries, actions });
215
+ menuPoint.current = { x, y };
216
+ return true;
217
+ }
218
+ // Once the items have rendered, open it at the point.
219
+ useEffect(() => {
220
+ const point = menuPoint.current;
221
+ if (!point || !menuAnchor.current)
222
+ return;
223
+ menuPoint.current = null;
224
+ menuAnchor.current.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true, clientX: point.x, clientY: point.y }));
225
+ });
226
+ // ---- Availability: true, or the reason it isn't (false gives the default).
227
+ const resourceOf = (id) => (id === undefined ? undefined : resources?.find((r) => r.id === id));
228
+ const clock = (time) => Number(time.slice(0, 2)) * 60 + Number(time.slice(3, 5));
229
+ // Outside a resource's `hours` (timed only): its offLabel.
230
+ const hoursReason = (query) => {
231
+ const hours = resourceOf(query.resourceId)?.hours;
232
+ if (!hours || query.allDay)
233
+ return null;
234
+ const from = clock(query.start.slice(11));
235
+ const to = query.end.slice(0, 10) === query.start.slice(0, 10) ? clock(query.end.slice(11)) : 24 * 60;
236
+ const fits = hours.some((h) => from >= clock(h.start) && to <= clock(h.end));
237
+ return fits ? null : resourceOf(query.resourceId)?.offLabel ?? t.unavailable;
238
+ };
239
+ const reasonFor = (query) => {
240
+ const off = hoursReason(query);
241
+ if (off)
242
+ return off;
243
+ const answer = isAvailable?.(query) ?? true;
244
+ return answer === true ? null : answer === false ? t.unavailable : answer;
245
+ };
246
+ const timed = (column, from, to, event) => ({
247
+ start: toISODateTime(addMinutes(column.date, from)), end: toISODateTime(addMinutes(column.date, to)), allDay: false,
248
+ ...(column.resource && { resourceId: column.resource.id }), ...(event && { event }),
249
+ });
250
+ // A column's unavailable time, slot by slot, merged into runs with one reason.
251
+ const unavailableRuns = (column) => {
252
+ if (!isAvailable && !column.resource?.hours)
253
+ return [];
254
+ const runs = [];
255
+ for (let from = startHour * 60; from < endHour * 60; from += slotMinutes) {
256
+ const reason = reasonFor({ ...timed(column, from, from + slotMinutes), shading: true });
257
+ const last = runs[runs.length - 1];
258
+ if (!reason)
259
+ continue;
260
+ if (last && last.to === from && last.reason === reason)
261
+ last.to = from + slotMinutes;
262
+ else
263
+ runs.push({ from, to: from + slotMinutes, reason });
264
+ }
265
+ return runs;
266
+ };
267
+ // Worked out again only when what it depends on changes, not on every
268
+ // pointer move while dragging (keep `isAvailable` stable: useCallback).
269
+ const shadingKey = columns.map((c) => `${c.iso}:${c.resource?.id ?? ''}:${JSON.stringify(c.resource?.hours ?? null)}:${c.resource?.offLabel ?? ''}`).join('|');
270
+ const shading = useMemo(() => columns.map(unavailableRuns), [shadingKey, isAvailable, startHour, endHour, slotMinutes, t.unavailable]); // eslint-disable-line react-hooks/exhaustive-deps
271
+ const refuse = (reason, attempt) => {
272
+ setStatus(reason);
273
+ onUnavailable?.(reason, attempt);
274
+ };
275
+ // ---- Changing events: by pointer (previewed until it's dropped) or by keyboard.
276
+ const grid = useRef(null);
277
+ const gesture = useRef(null);
278
+ const [preview, setPreviewState] = useState(null);
279
+ // Also kept in a ref: a quick click ends before React renders the preview it set.
280
+ const latestPreview = useRef(null);
281
+ const setPreview = (next) => {
282
+ latestPreview.current = next;
283
+ setPreviewState(next);
284
+ };
285
+ const refocus = useRef(null);
286
+ const suppressClick = useRef(false);
287
+ // Where an event would go if moved by `byColumns` and `byMinutes` (or resized by `byMinutes`).
288
+ function target(event, kind, byColumns, byMinutes) {
289
+ const start = parse(event.start);
290
+ const end = parse(event.end);
291
+ const from = columnOf(event, event.start.slice(0, 10));
292
+ if (from < 0)
293
+ return null;
294
+ if (kind === 'resize') {
295
+ const newEnd = addMinutes(end, byMinutes);
296
+ if (minutesBetween(start, newEnd) < slotMinutes)
297
+ return null;
298
+ return { start, end: newEnd, column: columns[from] };
299
+ }
300
+ const column = columns[Math.max(0, Math.min(columns.length - 1, from + byColumns))];
301
+ const days = Math.round((column.date.getTime() - columns[from].date.getTime()) / 86400000);
302
+ return { start: addMinutes(addDays(start, days), byMinutes), end: addMinutes(addDays(end, days), byMinutes), column };
303
+ }
304
+ const queryFor = (event, next) => ({
305
+ start: toISODateTime(next.start), end: toISODateTime(next.end), allDay: false, event,
306
+ ...(next.column.resource && { resourceId: next.column.resource.id }),
307
+ });
308
+ // Applies a change (after checking it's allowed).
309
+ function commit(event, kind, next) {
310
+ const query = queryFor(event, next);
311
+ const reason = reasonFor(query);
312
+ if (reason)
313
+ return refuse(reason, query);
314
+ refocus.current = event.id;
315
+ setStatus(kind === 'resize'
316
+ ? t.resized(nameOf(event), timeFormat.format(next.end))
317
+ : t.moved(nameOf(event), `${longDay.format(next.start)}, ${timeFormat.format(next.start)}${resourceName(next.column)}`));
318
+ onEventChange?.(event, {
319
+ start: query.start, end: query.end, ...(next.column.resource && { resourceId: next.column.resource.id }),
320
+ }, { kind });
321
+ }
322
+ function change(event, kind, byColumns, byMinutes) {
323
+ const next = target(event, kind, byColumns, byMinutes);
324
+ if (next)
325
+ commit(event, kind, next);
326
+ }
327
+ const keysOf = (e) => ({ shiftKey: e.shiftKey, altKey: e.altKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey });
328
+ const minuteAt = (y, box) => startHour * 60 + snap(Math.max(0, Math.min(span - 1, ((y - box.top) / box.height) * span)));
329
+ function begin(e, kind, column, event) {
330
+ if (e.button !== 0)
331
+ return;
332
+ e.stopPropagation();
333
+ try {
334
+ e.currentTarget.setPointerCapture(e.pointerId);
335
+ }
336
+ catch { /* not a live pointer */ }
337
+ if (kind === 'all-day') {
338
+ // The strip spans every column: which one is under the pointer.
339
+ const box = e.currentTarget.getBoundingClientRect();
340
+ const width = box.width / columns.length;
341
+ const at = Math.max(0, Math.min(columns.length - 1, Math.floor((e.clientX - box.left) / width)));
342
+ gesture.current = { kind, column: at, x: e.clientX, y: e.clientY, top: box.top, height: box.height, width, left: box.left, from: at, moved: false, keys: keysOf(e) };
343
+ setPreview({ kind: 'all-day', columns: [at] });
344
+ return;
345
+ }
346
+ const box = e.currentTarget.closest('[data-part=day]').getBoundingClientRect();
347
+ const from = minuteAt(e.clientY, box);
348
+ gesture.current = { kind, column, x: e.clientX, y: e.clientY, top: box.top, height: box.height, width: box.width, left: box.left, from, moved: false, keys: keysOf(e), event };
349
+ if (kind === 'select')
350
+ setPreview({ kind: 'select', column, from, to: from + slotMinutes });
351
+ if (kind === 'place')
352
+ setPreview({ kind: 'place', column, from });
353
+ }
354
+ // The columns from one to another (with resources, only the first one's resource).
355
+ const columnRange = (a, b) => {
356
+ const lo = Math.min(a, b);
357
+ const hi = Math.max(a, b);
358
+ return columns.map((_, i) => i).filter((i) => i >= lo && i <= hi && columns[i].resource === columns[a].resource);
359
+ };
360
+ function track(e) {
361
+ const g = gesture.current;
362
+ if (!g)
363
+ return;
364
+ const dx = e.clientX - g.x;
365
+ const dy = e.clientY - g.y;
366
+ if (!g.moved && Math.hypot(dx, dy) < 4)
367
+ return;
368
+ g.moved = true;
369
+ const perMinute = g.height / span;
370
+ if (g.kind === 'all-day') {
371
+ const at = Math.max(0, Math.min(columns.length - 1, Math.floor((e.clientX - g.left) / g.width)));
372
+ setPreview({ kind: 'all-day', columns: columnRange(g.from, at) });
373
+ }
374
+ else if (g.kind === 'select') {
375
+ const at = minuteAt(e.clientY, g);
376
+ setPreview({ kind: 'select', column: g.column, from: Math.min(g.from, at), to: Math.max(g.from, at) + slotMinutes });
377
+ }
378
+ else if (g.kind === 'place') {
379
+ setPreview({ kind: 'place', column: g.column, from: minuteAt(e.clientY, g) });
380
+ }
381
+ else if (g.kind === 'move') {
382
+ const byColumns = Math.round(dx / g.width);
383
+ const byMinutes = Math.round(dy / perMinute / slotMinutes) * slotMinutes;
384
+ setPreview({ kind: 'move', id: g.event.id, columns: byColumns, minutes: byMinutes, x: byColumns * g.width, y: byMinutes * perMinute });
385
+ }
386
+ else {
387
+ setPreview({ kind: 'resize', id: g.event.id, minutes: Math.round(dy / perMinute / slotMinutes) * slotMinutes });
388
+ }
389
+ }
390
+ function end() {
391
+ const g = gesture.current;
392
+ gesture.current = null;
393
+ const p = latestPreview.current;
394
+ setPreview(null);
395
+ if (!g)
396
+ return;
397
+ if (g.kind === 'select' && p?.kind === 'select') {
398
+ const column = columns[g.column];
399
+ const query = timed(column, p.from, p.to);
400
+ const reason = reasonFor(query);
401
+ if (reason)
402
+ return refuse(reason, query);
403
+ setStatus(t.selected(`${longDay.format(addMinutes(column.date, p.from))}, ${timeFormat.format(addMinutes(column.date, p.from))} – ${timeFormat.format(addMinutes(column.date, p.to))}${resourceName(column)}`));
404
+ onSelect?.({ start: query.start, end: query.end, allDay: false, ...(column.resource && { resourceId: column.resource.id }) }, g.keys);
405
+ return;
406
+ }
407
+ if (g.kind === 'all-day' && p?.kind === 'all-day') {
408
+ const firstColumn = columns[p.columns[0]];
409
+ const lastColumn = columns[p.columns[p.columns.length - 1]];
410
+ const query = { start: firstColumn.iso, end: lastColumn.iso, allDay: true, ...(firstColumn.resource && { resourceId: firstColumn.resource.id }) };
411
+ const reason = reasonFor(query);
412
+ if (reason)
413
+ return refuse(reason, query);
414
+ setStatus(t.selected(`${longDay.format(firstColumn.date)}${p.columns.length > 1 ? ` – ${longDay.format(lastColumn.date)}` : ''}, ${t.allDay.toLowerCase()}${resourceName(firstColumn)}`));
415
+ onSelect?.({ start: query.start, end: query.end, allDay: true, ...(firstColumn.resource && { resourceId: firstColumn.resource.id }) }, g.keys);
416
+ return;
417
+ }
418
+ if (g.kind === 'place' && p?.kind === 'place' && pinned) {
419
+ const duration = minutesBetween(parse(pinned.start), parse(pinned.end)) || slotMinutes;
420
+ const column = columns[p.column];
421
+ commit(pinned, 'move', { start: addMinutes(column.date, p.from), end: addMinutes(column.date, p.from + duration), column });
422
+ return;
423
+ }
424
+ if (!g.moved || !g.event)
425
+ return;
426
+ // The click that follows a drag isn't a click on the event.
427
+ suppressClick.current = true;
428
+ if (p?.kind === 'move' && (p.columns || p.minutes))
429
+ change(g.event, 'move', p.columns, p.minutes);
430
+ if (p?.kind === 'resize' && p.minutes)
431
+ change(g.event, 'resize', 0, p.minutes);
432
+ }
433
+ function cancel() {
434
+ gesture.current = null;
435
+ setPreview(null);
436
+ }
437
+ function keyDown(e, event) {
438
+ if (!editable(event))
439
+ return;
440
+ const keys = { ArrowUp: [0, -slotMinutes], ArrowDown: [0, slotMinutes], ArrowLeft: [-1, 0], ArrowRight: [1, 0] };
441
+ const move = keys[e.key];
442
+ if (!move)
443
+ return;
444
+ e.preventDefault();
445
+ if (e.shiftKey && move[0] === 0)
446
+ change(event, 'resize', 0, move[1]);
447
+ else
448
+ change(event, 'move', move[0], move[1]);
449
+ }
450
+ // The slot under the pointer: tinted (in its resource's colour) when it can
451
+ // be selected; when placing a pinned event, its preview. Escape stops placing.
452
+ const [hoverSlot, setHoverSlotState] = useState(null);
453
+ const setHoverSlot = (next) => {
454
+ if (next?.column !== hoverSlot?.column || next?.from !== hoverSlot?.from)
455
+ setHoverSlotState(next);
456
+ };
457
+ function hover(e, column) {
458
+ if (gesture.current)
459
+ return setHoverSlot(null);
460
+ const box = e.currentTarget.closest('[data-part=day]').getBoundingClientRect();
461
+ const from = minuteAt(e.clientY, box);
462
+ if (pinned)
463
+ return setPreview({ kind: 'place', column, from });
464
+ // Over the empty column (or its shading), not an event.
465
+ const over = e.target;
466
+ setHoverSlot(onSelect && (over === e.currentTarget || over.dataset.part === 'unavailable') ? { column, from } : null);
467
+ }
468
+ useEffect(() => {
469
+ if (!pinned)
470
+ return;
471
+ const escape = (e) => { if (e.key === 'Escape')
472
+ onPinnedChange?.(null); };
473
+ addEventListener('keydown', escape);
474
+ return () => removeEventListener('keydown', escape);
475
+ }, [pinned, onPinnedChange]);
476
+ // A moved event lands in another column (a new element): focus it again.
477
+ useEffect(() => {
478
+ if (refocus.current == null)
479
+ return;
480
+ grid.current?.querySelector(`[data-event-id="${CSS.escape(String(refocus.current))}"]`)?.focus();
481
+ refocus.current = null;
482
+ });
483
+ // ---- Scrolling inside `height`: the header rows stick, each below the last.
484
+ useLayoutEffect(() => {
485
+ const el = grid.current;
486
+ if (!el || !height)
487
+ return;
488
+ let offset = 0;
489
+ const rowsSeen = new Set();
490
+ for (const cell of el.querySelectorAll('[data-sticky-row]')) {
491
+ const row = cell.dataset.stickyRow;
492
+ if (!rowsSeen.has(row)) {
493
+ rowsSeen.add(row);
494
+ el.style.setProperty(`--sticky-${row}`, `${offset}px`);
495
+ offset += cell.getBoundingClientRect().height;
496
+ }
497
+ }
498
+ el.style.setProperty('--sticky-total', `${offset}px`);
499
+ });
500
+ // Opens scrolled to `scrollToTime` ('now' when today's shown), once.
501
+ const scrolled = useRef(false);
502
+ useLayoutEffect(() => {
503
+ const el = grid.current;
504
+ if (!el || !height || !scrollToTime || scrolled.current)
505
+ return;
506
+ scrolled.current = true;
507
+ const day = el.querySelector('[data-part=day]');
508
+ if (!day)
509
+ return;
510
+ const [h, m] = scrollToTime === 'now' ? [now.getHours() - 1, 0] : scrollToTime.split(':').map(Number);
511
+ const minutes = Math.max(startHour * 60, Math.min(endHour * 60, h * 60 + (m || 0)));
512
+ const sticky = parseFloat(el.style.getPropertyValue('--sticky-total')) || 0;
513
+ el.scrollTop = day.offsetTop + ((minutes - startHour * 60) / span) * day.offsetHeight - sticky;
514
+ });
515
+ // ---- All-day and multi-day events: bars across the columns they cover, in lanes.
516
+ const allDay = events.filter(isAllDay).flatMap((event) => {
517
+ const from = event.start.slice(0, 10);
518
+ const to = event.end.slice(0, 10);
519
+ const covered = columns.map((c, i) => (c.iso >= from && c.iso <= to && (!resources?.length || c.resource?.id === event.resourceId) ? i : -1))
520
+ .filter((i) => i >= 0);
521
+ // Runs of neighbouring columns (with resources, a day's column for the event's resource).
522
+ const runs = [];
523
+ for (const i of covered) {
524
+ const run = runs[runs.length - 1];
525
+ if (run && run[run.length - 1] === i - 1)
526
+ run.push(i);
527
+ else
528
+ runs.push([i]);
529
+ }
530
+ return runs.map((run) => ({ event, first: run[0], length: run.length, lane: 0, continued: false }));
531
+ });
532
+ allDay.sort((a, b) => a.first - b.first || b.length - a.length);
533
+ const laneEnds = [];
534
+ for (const bar of allDay) {
535
+ let lane = laneEnds.findIndex((endAt) => endAt < bar.first);
536
+ if (lane < 0)
537
+ lane = laneEnds.length;
538
+ laneEnds[lane] = bar.first + bar.length - 1;
539
+ bar.lane = lane;
540
+ }
541
+ // Past `allDayMax` lanes, the rest fold into a "+2 more" per column (until opened).
542
+ const folding = laneEnds.length > allDayMax && !allDayOpen;
543
+ const shownLanes = folding ? allDayMax - 1 : laneEnds.length;
544
+ const shownBars = allDay.filter((bar) => bar.lane < shownLanes);
545
+ const hiddenCount = columns.map(() => 0);
546
+ if (folding) {
547
+ const taken = columns.map((_, i) => new Set(shownBars.filter((bar) => i >= bar.first && i < bar.first + bar.length).map((bar) => bar.lane)));
548
+ for (const bar of allDay.filter((b) => b.lane >= shownLanes)) {
549
+ let piece = null;
550
+ for (let i = bar.first; i < bar.first + bar.length; i++) {
551
+ const lane = Array.from({ length: shownLanes }, (_, l) => l).find((l) => !taken[i].has(l));
552
+ if (lane === undefined) {
553
+ hiddenCount[i]++;
554
+ piece = null;
555
+ continue;
556
+ }
557
+ if (piece && piece.lane === lane && piece.first + piece.length === i)
558
+ piece.length++;
559
+ // A piece after the bar's start continues it (drawn with a square left end).
560
+ else
561
+ shownBars.push(piece = { ...bar, first: i, length: 1, lane, continued: i > bar.first });
562
+ taken[i].add(lane);
563
+ }
564
+ }
565
+ }
566
+ const hiddenIn = (i) => hiddenCount[i];
567
+ const barColor = (event) => event.color ?? resourceOf(event.resourceId)?.color ?? 'accent';
568
+ const twoRows = !!resources?.length && count > 1;
569
+ const editable = (event) => !!onEventChange && event.editable !== false && !event.pending;
570
+ const pinnedMinutes = pinned ? minutesBetween(parse(pinned.start), parse(pinned.end)) || slotMinutes : 0;
571
+ // The header rows, numbered for sticking: days, resources, extra rows, the all-day strip.
572
+ let rowNumber = 0;
573
+ const dayRow = !resources?.length || count > 1 ? rowNumber++ : -1;
574
+ const resourceRow = resources?.length ? rowNumber++ : -1;
575
+ const extraRows = rows.map(() => rowNumber++);
576
+ const allDayRow = rowNumber;
577
+ // The preview's problem, if any, shown as it's dragged.
578
+ const previewReason = (() => {
579
+ if (!preview || !isAvailable)
580
+ return null;
581
+ if (preview.kind === 'select')
582
+ return reasonFor(timed(columns[preview.column], preview.from, preview.to));
583
+ if (preview.kind === 'place')
584
+ return pinned ? reasonFor(timed(columns[preview.column], preview.from, preview.from + pinnedMinutes, pinned)) : null;
585
+ if (preview.kind === 'all-day') {
586
+ const a = columns[preview.columns[0]];
587
+ const b = columns[preview.columns[preview.columns.length - 1]];
588
+ return reasonFor({ start: a.iso, end: b.iso, allDay: true, ...(a.resource && { resourceId: a.resource.id }) });
589
+ }
590
+ const event = events.find((e) => e.id === preview.id);
591
+ const next = event && (preview.kind === 'move' ? target(event, 'move', preview.columns, preview.minutes) : target(event, 'resize', 0, preview.minutes));
592
+ return next && event ? reasonFor(queryFor(event, next)) : null;
593
+ })();
594
+ return (_jsxs("div", { ...rest, ...part('root'), ref: root, className: cx(className, classes?.root), "data-placing": pinned ? '' : undefined, style: { ...style, '--columns': columns.length, '--hour-height': hourHeight }, children: [toolbar && (_jsxs("div", { ...part('toolbar'), children: [_jsxs("div", { ...part('nav'), children: [_jsx(Button, { size: "sm", onClick: () => setDate(today), children: t.today }), _jsx(Button, { size: "sm", variant: "ghost", icon: "lucide:chevron-left", "aria-label": t.previous, onClick: () => setDate(toISODate(addDays(current, -step))) }), _jsx(Button, { size: "sm", variant: "ghost", icon: "lucide:chevron-right", "aria-label": t.next, onClick: () => setDate(toISODate(addDays(current, step))) })] }), _jsx(Popover, { placement: "bottom-start", open: picking, onOpenChange: setPicking, trigger: _jsxs("button", { type: "button", ...part('title'), "aria-label": `${title}: ${t.pickDate}`, children: [title, _jsx("iconify-icon", { icon: "lucide:chevron-down", "aria-hidden": "true" })] }), children: _jsx(DatePicker, { inline: true, value: date, locale: locale, onValueChange: (value) => { if (value) {
595
+ setDate(value);
596
+ setPicking(false);
597
+ } } }) }), _jsxs("div", { ...part('actions'), children: [!narrow && views.length > 1 && (_jsx(Segmented, { size: "sm", "aria-label": t.view, items: views.map((v) => ({ value: String(v), label: viewLabel(v) })), value: String(chosenView), onValueChange: (v) => v && setView(/^\d+$/.test(v) ? Number(v) : v) })), actions] })] })), pinned && (_jsxs("div", { ...part('pinned'), role: "status", children: [_jsx("iconify-icon", { icon: "lucide:move", "aria-hidden": "true" }), _jsx("span", { children: t.placing(nameOf(pinned)) }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => onPinnedChange?.(null), children: t.cancel })] })), _jsxs("div", { ...part('grid'), ref: grid, role: "grid", "aria-label": title, "data-two-rows": twoRows || undefined, "data-scroll": height ? '' : undefined, style: height ? { maxHeight: height } : undefined, children: [_jsx("div", { ...part('corner'), "data-sticky-row": dayRow >= 0 ? dayRow : resourceRow, style: { gridRow: twoRows ? 'span 2' : undefined, top: `var(--sticky-${dayRow >= 0 ? dayRow : resourceRow})` } }), dayRow >= 0 && dates.map((d) => (_jsx("div", { ...part('day-header'), role: "columnheader", "data-today": sameDay(d, now) || undefined, "data-sticky-row": dayRow, style: { gridColumn: `span ${lanes.length}`, top: `var(--sticky-${dayRow})` }, children: (() => {
598
+ const content = renderDayHeader ? renderDayHeader(d) : _jsxs(_Fragment, { children: [_jsx("span", { children: dayFormat.format(d) }), _jsx("strong", { children: d.getDate() })] });
599
+ return onDayClick
600
+ ? _jsx("button", { type: "button", ...part('header-button'), onClick: () => onDayClick(toISODate(d)), "aria-label": longDay.format(d), children: content })
601
+ : content;
602
+ })() }, toISODate(d)))), resourceRow >= 0 && columns.map((c) => (_jsx("div", { ...part('resource-header'), role: "columnheader", "data-colored": c.resource.color ? '' : undefined, "data-sticky-row": resourceRow, style: { top: `var(--sticky-${resourceRow})`, ...(c.resource.color && { '--resource-color': cssColor(c.resource.color) }) }, children: onResourceClick
603
+ ? _jsx("button", { type: "button", ...part('header-button'), onClick: () => onResourceClick(c.resource, c.iso), children: renderResource ? renderResource(c.resource) : c.resource.title })
604
+ : renderResource ? renderResource(c.resource) : c.resource.title }, `${c.iso}-${c.resource.id}`))), rows.map((row, r) => _jsxs(Fragment, { children: [_jsx("div", { ...part('row-label'), "data-sticky-row": extraRows[r], style: { top: `var(--sticky-${extraRows[r]})` }, children: row.label }), (row.perColumn ? columns : dates.map((d) => ({ date: d, iso: toISODate(d), resource: null }))).map((cell) => {
605
+ const content = row.render?.(cell.date, cell.resource);
606
+ const key = `${cell.iso}-${cell.resource?.id ?? ''}`;
607
+ const cellStyle = { gridColumn: row.perColumn ? undefined : `span ${lanes.length}`, top: `var(--sticky-${extraRows[r]})` };
608
+ return row.onClick
609
+ // The whole cell adds (say, a message); what's in it opens its own (stopPropagation in your render).
610
+ ? _jsx("div", { ...part('row-cell'), style: cellStyle, "data-sticky-row": extraRows[r], "data-clickable": "", role: "button", tabIndex: 0, "aria-label": `${typeof row.label === 'string' ? row.label : ''} ${longDay.format(cell.date)}`, onClick: () => row.onClick(cell.date, cell.resource), onKeyDown: (e) => { if (e.key === 'Enter' || e.key === ' ') {
611
+ e.preventDefault();
612
+ row.onClick(cell.date, cell.resource);
613
+ } }, children: content }, key)
614
+ : _jsx("div", { ...part('row-cell'), style: cellStyle, "data-sticky-row": extraRows[r], children: content }, key);
615
+ })] }, row.key)), (allDay.length > 0 || onSelect) && _jsxs(_Fragment, { children: [_jsx("div", { ...part('all-day-label'), "data-sticky-row": allDayRow, style: { top: `var(--sticky-${allDayRow})` }, children: laneEnds.length > allDayMax
616
+ ? _jsxs("button", { type: "button", onClick: () => setAllDayOpen(!allDayOpen), "aria-expanded": allDayOpen, children: [t.allDay, _jsx("iconify-icon", { icon: allDayOpen ? 'lucide:chevron-up' : 'lucide:chevron-down', "aria-hidden": "true" })] })
617
+ : t.allDay }), _jsxs("div", { ...part('all-day'), "data-sticky-row": allDayRow, style: { '--lanes': Math.max(1, shownLanes + (folding ? 1 : 0)), top: `var(--sticky-${allDayRow})` }, "data-selectable": onSelect ? '' : undefined, onPointerDown: (e) => { if (onSelect && e.target === e.currentTarget)
618
+ begin(e, 'all-day', 0); }, onPointerMove: track, onPointerUp: end, onPointerCancel: cancel, onContextMenu: (e) => {
619
+ if (e.target !== e.currentTarget)
620
+ return;
621
+ const box = e.currentTarget.getBoundingClientRect();
622
+ const column = columns[Math.max(0, Math.min(columns.length - 1, Math.floor((e.clientX - box.left) / (box.width / columns.length))))];
623
+ const slot = { start: column.iso, end: column.iso, allDay: true, ...(column.resource && { resourceId: column.resource.id }) };
624
+ if (openMenu(e.clientX, e.clientY, slotMenu?.(slot)))
625
+ e.preventDefault();
626
+ }, children: [preview?.kind === 'all-day' && preview.columns.map((i) => (_jsx("div", { ...part('selection'), "data-all-day": "", "data-invalid": previewReason ? '' : undefined, style: { left: `${(i / columns.length) * 100}%`, width: `${100 / columns.length}%` } }, i))), shownBars.map(({ event, first, length: covers, lane, continued }) => (_jsx("button", { type: "button", ...part('all-day-event'), "data-continued": continued || undefined, "data-color": named.includes(barColor(event)) ? barColor(event) : 'custom', "data-blocked": event.blocked || undefined, style: {
627
+ left: `${(first / columns.length) * 100}%`, width: `${(covers / columns.length) * 100}%`, top: `calc(${lane} * 1.625rem + 0.25rem)`,
628
+ ...(!named.includes(barColor(event)) && { '--event-color': barColor(event) }),
629
+ }, onClick: () => onEventClick?.(event), onContextMenu: (e) => { if (openMenu(e.clientX, e.clientY, eventMenu?.(event)))
630
+ e.preventDefault(); }, children: renderEvent ? renderEvent(event, { start: parse(event.start), end: parse(event.end), resource: null, compact: true }) : event.title }, `${event.id}-${first}`))), folding && columns.map((_, i) => hiddenIn(i) > 0 && (_jsx("button", { type: "button", ...part('more'), onClick: () => setAllDayOpen(true), style: { left: `${(i / columns.length) * 100}%`, width: `${100 / columns.length}%`, top: `calc(${shownLanes} * 1.625rem + 0.25rem)` }, children: t.more(hiddenIn(i)) }, `more-${i}`)))] })] }), _jsx("div", { ...part('gutter'), "aria-hidden": "true", style: { '--hours': hours.length }, children: hours.map((hour) => _jsx("span", { ...part('hour'), style: { top: top(hour * 60) }, children: hourFormat.format(new Date(2000, 0, 1, hour)) }, hour)) }), columns.map((column, index) => {
631
+ const own = events.filter((event) => !isAllDay(event) && event.start.startsWith(column.iso)
632
+ && (!resources?.length || event.resourceId === column.resource?.id));
633
+ const selecting = preview?.kind === 'select' && preview.column === index ? preview : null;
634
+ const placing = preview?.kind === 'place' && preview.column === index ? preview : null;
635
+ const isToday = sameDay(column.date, now);
636
+ // A press on the column (or its unavailable shading): select, or place the pinned event.
637
+ const press = (e) => {
638
+ if (pinned)
639
+ begin(e, 'place', index);
640
+ else if (onSelect)
641
+ begin(e, 'select', index);
642
+ };
643
+ return (_jsxs("div", { ...part('day'), role: "gridcell", "aria-label": `${longDay.format(column.date)}${resourceName(column)}`, "data-today": isToday || undefined, "data-selectable": onSelect || pinned ? '' : undefined, "data-first": (!!resources?.length && column.resource === lanes[0]) || undefined, style: { '--hours': hours.length, ...(column.resource?.color && { '--resource-color': cssColor(column.resource.color) }) }, onPointerDown: (e) => { if (e.target === e.currentTarget)
644
+ press(e); }, onPointerMove: (e) => { track(e); hover(e, index); }, onPointerUp: end, onPointerCancel: cancel, onPointerLeave: () => { setHoverSlot(null); if (pinned && !gesture.current)
645
+ setPreview(null); }, onContextMenu: (e) => {
646
+ // An empty time (or its unavailable shading), not an event.
647
+ if (e.target !== e.currentTarget && e.target.dataset.part !== 'unavailable')
648
+ return;
649
+ const from = minuteAt(e.clientY, e.currentTarget.getBoundingClientRect());
650
+ const { event: _none, ...slot } = timed(column, from, from + slotMinutes);
651
+ if (openMenu(e.clientX, e.clientY, slotMenu?.(slot)))
652
+ e.preventDefault();
653
+ }, children: [shading[index].map((run) => (_jsx("div", { ...part('unavailable'), title: run.reason, style: { top: top(run.from), height: length(run.to - run.from) }, onPointerDown: press }, run.from))), place(own).map(({ event, start, end: finish, column: sub, columns: subs }) => {
654
+ const moving = preview?.kind === 'move' && preview.id === event.id ? preview : null;
655
+ const resizing = preview?.kind === 'resize' && preview.id === event.id ? preview : null;
656
+ const shownEnd = resizing ? addMinutes(finish, resizing.minutes) : finish;
657
+ const minutes = minutesBetween(start, shownEnd);
658
+ const color = event.color ?? column.resource?.color ?? 'accent';
659
+ const time = `${timeFormat.format(start)} – ${timeFormat.format(shownEnd)}`;
660
+ const box = { top: top(minutesOfDay(start)), left: `${(sub / subs) * 100}%`, width: `${100 / subs}%` };
661
+ return (_jsxs(Fragment, { children: [(moving || resizing) && _jsx("div", { ...part('ghost'), style: { ...box, height: length(minutesBetween(start, finish)) } }), _jsxs("button", { type: "button", ...part('event'), "data-event-id": event.id, "data-dragging": moving || resizing ? '' : undefined, "data-invalid": (moving || resizing) && previewReason ? '' : undefined, "data-editable": editable(event) || undefined, "data-compact": minutes <= 30 || undefined, "data-blocked": event.blocked || undefined, "data-pending": event.pending || undefined, "data-pinned": pinned?.id === event.id || undefined, "data-color": named.includes(color) ? color : 'custom', "aria-label": `${nameOf(event)}, ${time}`, "aria-busy": event.pending || undefined, title: (moving || resizing) && previewReason ? previewReason : undefined, style: {
662
+ ...box, height: length(Math.max(slotMinutes, minutes)),
663
+ ...(!named.includes(color) && { '--event-color': color }),
664
+ ...(moving && { transform: `translate(${moving.x}px, ${moving.y}px)` }),
665
+ }, onPointerDown: (e) => { if (editable(event) && !pinned)
666
+ begin(e, 'move', index, event);
667
+ else
668
+ e.stopPropagation(); }, onPointerMove: track, onPointerUp: end, onPointerCancel: cancel, onKeyDown: (e) => {
669
+ if (e.key === 'ContextMenu' || (e.shiftKey && e.key === 'F10')) {
670
+ const box = e.currentTarget.getBoundingClientRect();
671
+ if (openMenu(box.left + 8, box.bottom, eventMenu?.(event)))
672
+ e.preventDefault();
673
+ }
674
+ else
675
+ keyDown(e, event);
676
+ }, onContextMenu: (e) => { if (openMenu(e.clientX, e.clientY, eventMenu?.(event)))
677
+ e.preventDefault(); }, onClick: () => { if (suppressClick.current)
678
+ suppressClick.current = false;
679
+ else
680
+ onEventClick?.(event); }, children: [renderEvent ? renderEvent(event, { start, end: shownEnd, resource: column.resource, compact: minutes <= 30 }) : _jsxs(_Fragment, { children: [_jsx("span", { ...part('event-title'), children: event.title }), _jsx("span", { ...part('event-time'), children: time })] }), editable(event) && !pinned && (_jsx("span", { ...part('resize'), "aria-hidden": "true", onPointerDown: (e) => begin(e, 'resize', index, event), onPointerMove: track, onPointerUp: end, onPointerCancel: cancel }))] })] }, event.id));
681
+ }), hoverSlot?.column === index && !preview && (() => {
682
+ const off = shading[index].find((run) => hoverSlot.from >= run.from && hoverSlot.from < run.to);
683
+ return _jsx("div", { ...part('hover'), "data-invalid": off ? '' : undefined, "aria-hidden": "true", style: { top: top(hoverSlot.from), height: length(slotMinutes) } });
684
+ })(), selecting && (_jsx("div", { ...part('selection'), "data-invalid": previewReason ? '' : undefined, style: { top: top(selecting.from), height: length(selecting.to - selecting.from) }, children: previewReason ?? `${timeFormat.format(addMinutes(column.date, selecting.from))} – ${timeFormat.format(addMinutes(column.date, selecting.to))}` })), placing && pinned && (_jsx("div", { ...part('selection'), "data-placing": "", "data-invalid": previewReason ? '' : undefined, style: { top: top(placing.from), height: length(pinnedMinutes) }, children: previewReason ?? `${nameOf(pinned)}, ${timeFormat.format(addMinutes(column.date, placing.from))}` })), isToday && minutesOfDay(now) >= startHour * 60 && minutesOfDay(now) < endHour * 60 && (_jsx("div", { ...part('now'), style: { top: top(minutesOfDay(now)) }, "aria-hidden": "true" }))] }, `${column.iso}-${column.resource?.id ?? ''}`));
685
+ })] }), _jsx(Menu, { context: true, trigger: _jsx("span", { ref: menuAnchor, "aria-hidden": "true", style: menuAnchorStyle }), items: menu.entries, onSelect: (value) => menu.actions[value]?.() }), _jsx("div", { ...part('status'), "aria-live": "polite", role: "status", children: status })] }));
686
+ }