@spatika/react 1.1.0 → 1.3.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 (311) hide show
  1. package/AGENTS.md +71 -0
  2. package/README.md +4 -3
  3. package/dist/composites/Card.d.ts +7 -1
  4. package/dist/composites/Card.d.ts.map +1 -1
  5. package/dist/composites/Card.js +10 -0
  6. package/dist/composites/ChartDataGrid.d.ts +33 -0
  7. package/dist/composites/ChartDataGrid.d.ts.map +1 -0
  8. package/dist/composites/ChartDataGrid.js +77 -0
  9. package/dist/composites/CoverHero.d.ts +1 -1
  10. package/dist/composites/CoverHero.d.ts.map +1 -1
  11. package/dist/composites/CoverHero.js +2 -2
  12. package/dist/composites/EntityMediaCard.d.ts +1 -1
  13. package/dist/composites/EntityMediaCard.d.ts.map +1 -1
  14. package/dist/composites/EntityMediaCard.js +12 -4
  15. package/dist/composites/EventCalendar.d.ts +72 -0
  16. package/dist/composites/EventCalendar.d.ts.map +1 -0
  17. package/dist/composites/EventCalendar.js +371 -0
  18. package/dist/composites/EventEditor.d.ts +22 -0
  19. package/dist/composites/EventEditor.d.ts.map +1 -0
  20. package/dist/composites/EventEditor.js +86 -0
  21. package/dist/composites/EventTimeline.d.ts +29 -0
  22. package/dist/composites/EventTimeline.d.ts.map +1 -0
  23. package/dist/composites/EventTimeline.js +186 -0
  24. package/dist/composites/MapChart.d.ts +24 -0
  25. package/dist/composites/MapChart.d.ts.map +1 -0
  26. package/dist/composites/MapChart.js +58 -0
  27. package/dist/composites/ProfileHero.d.ts +6 -4
  28. package/dist/composites/ProfileHero.d.ts.map +1 -1
  29. package/dist/composites/ProfileHero.js +8 -8
  30. package/dist/composites/ScatterWebGL.d.ts +22 -0
  31. package/dist/composites/ScatterWebGL.d.ts.map +1 -0
  32. package/dist/composites/ScatterWebGL.js +45 -0
  33. package/dist/composites/SiteFooter.d.ts +1 -1
  34. package/dist/composites/SiteFooter.js +1 -1
  35. package/dist/composites/SiteNav.d.ts +7 -2
  36. package/dist/composites/SiteNav.d.ts.map +1 -1
  37. package/dist/composites/SiteNav.js +9 -5
  38. package/dist/composites/StatCard.d.ts +2 -1
  39. package/dist/composites/StatCard.d.ts.map +1 -1
  40. package/dist/composites/StatCard.js +3 -3
  41. package/dist/composites/StatusDot.d.ts +1 -1
  42. package/dist/composites/Wordmark.d.ts +2 -2
  43. package/dist/composites/Wordmark.d.ts.map +1 -1
  44. package/dist/composites/cartesian-charts.d.ts +138 -0
  45. package/dist/composites/cartesian-charts.d.ts.map +1 -0
  46. package/dist/composites/cartesian-charts.js +401 -0
  47. package/dist/composites/chart-composition.d.ts +139 -0
  48. package/dist/composites/chart-composition.d.ts.map +1 -0
  49. package/dist/composites/chart-composition.js +486 -0
  50. package/dist/composites/chart-ui.d.ts +83 -0
  51. package/dist/composites/chart-ui.d.ts.map +1 -0
  52. package/dist/composites/chart-ui.js +108 -0
  53. package/dist/composites/charts-3d.d.ts +37 -0
  54. package/dist/composites/charts-3d.d.ts.map +1 -0
  55. package/dist/composites/charts-3d.js +121 -0
  56. package/dist/composites/flow-charts.d.ts +89 -0
  57. package/dist/composites/flow-charts.d.ts.map +1 -0
  58. package/dist/composites/flow-charts.js +186 -0
  59. package/dist/composites/radial-charts.d.ts +109 -0
  60. package/dist/composites/radial-charts.d.ts.map +1 -0
  61. package/dist/composites/radial-charts.js +176 -0
  62. package/dist/composites/scheduler-ui.d.ts +44 -0
  63. package/dist/composites/scheduler-ui.d.ts.map +1 -0
  64. package/dist/composites/scheduler-ui.js +71 -0
  65. package/dist/index.d.ts +68 -7
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +58 -7
  68. package/dist/lib/chart-export.d.ts +10 -0
  69. package/dist/lib/chart-export.d.ts.map +1 -0
  70. package/dist/lib/chart-export.js +102 -0
  71. package/dist/lib/charts.d.ts +235 -0
  72. package/dist/lib/charts.d.ts.map +1 -0
  73. package/dist/lib/charts.js +744 -0
  74. package/dist/lib/geo.d.ts +39 -0
  75. package/dist/lib/geo.d.ts.map +1 -0
  76. package/dist/lib/geo.js +101 -0
  77. package/dist/lib/iso.d.ts +21 -0
  78. package/dist/lib/iso.d.ts.map +1 -0
  79. package/dist/lib/iso.js +56 -0
  80. package/dist/lib/rrule.d.ts +20 -0
  81. package/dist/lib/rrule.d.ts.map +1 -0
  82. package/dist/lib/rrule.js +236 -0
  83. package/dist/lib/scheduler.d.ts +156 -0
  84. package/dist/lib/scheduler.d.ts.map +1 -0
  85. package/dist/lib/scheduler.js +502 -0
  86. package/dist/lib/webgl-scatter.d.ts +44 -0
  87. package/dist/lib/webgl-scatter.d.ts.map +1 -0
  88. package/dist/lib/webgl-scatter.js +185 -0
  89. package/dist/primitives/Accordion.d.ts +44 -20
  90. package/dist/primitives/Accordion.d.ts.map +1 -1
  91. package/dist/primitives/Accordion.js +82 -22
  92. package/dist/primitives/AppBar.d.ts +13 -0
  93. package/dist/primitives/AppBar.d.ts.map +1 -0
  94. package/dist/primitives/AppBar.js +27 -0
  95. package/dist/primitives/Autocomplete.d.ts +28 -0
  96. package/dist/primitives/Autocomplete.d.ts.map +1 -0
  97. package/dist/primitives/Autocomplete.js +118 -0
  98. package/dist/primitives/Avatar.d.ts +13 -1
  99. package/dist/primitives/Avatar.d.ts.map +1 -1
  100. package/dist/primitives/Avatar.js +17 -2
  101. package/dist/primitives/Backdrop.d.ts +17 -0
  102. package/dist/primitives/Backdrop.d.ts.map +1 -0
  103. package/dist/primitives/Backdrop.js +17 -0
  104. package/dist/primitives/BottomNavigation.d.ts +23 -0
  105. package/dist/primitives/BottomNavigation.d.ts.map +1 -0
  106. package/dist/primitives/BottomNavigation.js +30 -0
  107. package/dist/primitives/ButtonBase.d.ts +14 -0
  108. package/dist/primitives/ButtonBase.d.ts.map +1 -0
  109. package/dist/primitives/ButtonBase.js +13 -0
  110. package/dist/primitives/ButtonGroup.d.ts +25 -0
  111. package/dist/primitives/ButtonGroup.d.ts.map +1 -0
  112. package/dist/primitives/ButtonGroup.js +44 -0
  113. package/dist/primitives/CircularProgress.d.ts +16 -0
  114. package/dist/primitives/CircularProgress.d.ts.map +1 -0
  115. package/dist/primitives/CircularProgress.js +17 -0
  116. package/dist/primitives/ClickAwayListener.d.ts +16 -0
  117. package/dist/primitives/ClickAwayListener.d.ts.map +1 -0
  118. package/dist/primitives/ClickAwayListener.js +33 -0
  119. package/dist/primitives/Collapse.d.ts +19 -0
  120. package/dist/primitives/Collapse.d.ts.map +1 -0
  121. package/dist/primitives/Collapse.js +14 -0
  122. package/dist/primitives/Container.d.ts +13 -0
  123. package/dist/primitives/Container.d.ts.map +1 -0
  124. package/dist/primitives/Container.js +26 -0
  125. package/dist/primitives/Fab.d.ts +22 -0
  126. package/dist/primitives/Fab.d.ts.map +1 -0
  127. package/dist/primitives/Fab.js +42 -0
  128. package/dist/primitives/FormControl.d.ts +27 -0
  129. package/dist/primitives/FormControl.d.ts.map +1 -0
  130. package/dist/primitives/FormControl.js +24 -0
  131. package/dist/primitives/FormControlLabel.d.ts +18 -0
  132. package/dist/primitives/FormControlLabel.d.ts.map +1 -0
  133. package/dist/primitives/FormControlLabel.js +19 -0
  134. package/dist/primitives/Grid.d.ts +32 -0
  135. package/dist/primitives/Grid.d.ts.map +1 -0
  136. package/dist/primitives/Grid.js +50 -0
  137. package/dist/primitives/IconButton.d.ts +22 -0
  138. package/dist/primitives/IconButton.d.ts.map +1 -0
  139. package/dist/primitives/IconButton.js +39 -0
  140. package/dist/primitives/ImageList.d.ts +31 -0
  141. package/dist/primitives/ImageList.d.ts.map +1 -0
  142. package/dist/primitives/ImageList.js +33 -0
  143. package/dist/primitives/InputAdornment.d.ts +28 -0
  144. package/dist/primitives/InputAdornment.d.ts.map +1 -0
  145. package/dist/primitives/InputAdornment.js +13 -0
  146. package/dist/primitives/Link.d.ts +17 -0
  147. package/dist/primitives/Link.d.ts.map +1 -0
  148. package/dist/primitives/Link.js +29 -0
  149. package/dist/primitives/List.d.ts +41 -0
  150. package/dist/primitives/List.d.ts.map +1 -0
  151. package/dist/primitives/List.js +22 -0
  152. package/dist/primitives/Masonry.d.ts +14 -0
  153. package/dist/primitives/Masonry.d.ts.map +1 -0
  154. package/dist/primitives/Masonry.js +15 -0
  155. package/dist/primitives/Menu.d.ts +28 -0
  156. package/dist/primitives/Menu.d.ts.map +1 -0
  157. package/dist/primitives/Menu.js +54 -0
  158. package/dist/primitives/MobileStepper.d.ts +15 -0
  159. package/dist/primitives/MobileStepper.d.ts.map +1 -0
  160. package/dist/primitives/MobileStepper.js +8 -0
  161. package/dist/primitives/Modal.d.ts +18 -0
  162. package/dist/primitives/Modal.d.ts.map +1 -0
  163. package/dist/primitives/Modal.js +29 -0
  164. package/dist/primitives/NoSsr.d.ts +15 -0
  165. package/dist/primitives/NoSsr.d.ts.map +1 -0
  166. package/dist/primitives/NoSsr.js +19 -0
  167. package/dist/primitives/Paper.d.ts +22 -0
  168. package/dist/primitives/Paper.d.ts.map +1 -0
  169. package/dist/primitives/Paper.js +33 -0
  170. package/dist/primitives/Progress.d.ts +6 -2
  171. package/dist/primitives/Progress.d.ts.map +1 -1
  172. package/dist/primitives/Progress.js +8 -3
  173. package/dist/primitives/Snackbar.d.ts +23 -0
  174. package/dist/primitives/Snackbar.d.ts.map +1 -0
  175. package/dist/primitives/Snackbar.js +33 -0
  176. package/dist/primitives/SpeedDial.d.ts +26 -0
  177. package/dist/primitives/SpeedDial.d.ts.map +1 -0
  178. package/dist/primitives/SpeedDial.js +26 -0
  179. package/dist/primitives/Stack.d.ts +15 -0
  180. package/dist/primitives/Stack.d.ts.map +1 -0
  181. package/dist/primitives/Stack.js +45 -0
  182. package/dist/primitives/StarRating.d.ts +19 -1
  183. package/dist/primitives/StarRating.d.ts.map +1 -1
  184. package/dist/primitives/StarRating.js +36 -6
  185. package/dist/primitives/Stepper.d.ts +36 -0
  186. package/dist/primitives/Stepper.d.ts.map +1 -0
  187. package/dist/primitives/Stepper.js +53 -0
  188. package/dist/primitives/TablePagination.d.ts +16 -0
  189. package/dist/primitives/TablePagination.d.ts.map +1 -0
  190. package/dist/primitives/TablePagination.js +12 -0
  191. package/dist/primitives/TextField.d.ts +24 -0
  192. package/dist/primitives/TextField.d.ts.map +1 -0
  193. package/dist/primitives/TextField.js +19 -0
  194. package/dist/primitives/TextareaAutosize.d.ts +11 -0
  195. package/dist/primitives/TextareaAutosize.d.ts.map +1 -0
  196. package/dist/primitives/TextareaAutosize.js +26 -0
  197. package/dist/primitives/ToggleButton.d.ts +28 -0
  198. package/dist/primitives/ToggleButton.d.ts.map +1 -0
  199. package/dist/primitives/ToggleButton.js +56 -0
  200. package/dist/primitives/TransferList.d.ts +21 -0
  201. package/dist/primitives/TransferList.d.ts.map +1 -0
  202. package/dist/primitives/TransferList.js +40 -0
  203. package/dist/primitives/Typography.d.ts +23 -0
  204. package/dist/primitives/Typography.d.ts.map +1 -0
  205. package/dist/primitives/Typography.js +69 -0
  206. package/dist/primitives/transitions.d.ts +29 -0
  207. package/dist/primitives/transitions.d.ts.map +1 -0
  208. package/dist/primitives/transitions.js +52 -0
  209. package/package.json +28 -4
  210. package/skills/spatika-ui/SKILL.md +96 -0
  211. package/src/composites/Card.tsx +51 -1
  212. package/src/composites/ChartDataGrid.test.tsx +57 -0
  213. package/src/composites/ChartDataGrid.tsx +196 -0
  214. package/src/composites/CoverHero.test.tsx +38 -0
  215. package/src/composites/CoverHero.tsx +29 -22
  216. package/src/composites/EntityMediaCard.test.tsx +58 -0
  217. package/src/composites/EntityMediaCard.tsx +31 -13
  218. package/src/composites/EventCalendar.tsx +1030 -0
  219. package/src/composites/EventEditor.tsx +268 -0
  220. package/src/composites/EventTimeline.tsx +366 -0
  221. package/src/composites/MapChart.test.tsx +39 -0
  222. package/src/composites/MapChart.tsx +130 -0
  223. package/src/composites/ProfileHero.test.tsx +33 -0
  224. package/src/composites/ProfileHero.tsx +96 -79
  225. package/src/composites/ScatterWebGL.test.tsx +25 -0
  226. package/src/composites/ScatterWebGL.tsx +79 -0
  227. package/src/composites/SiteFooter.test.tsx +37 -0
  228. package/src/composites/SiteFooter.tsx +1 -1
  229. package/src/composites/SiteNav.test.tsx +46 -0
  230. package/src/composites/SiteNav.tsx +14 -4
  231. package/src/composites/StatCard.test.tsx +30 -0
  232. package/src/composites/StatCard.tsx +4 -0
  233. package/src/composites/Wordmark.test.tsx +23 -0
  234. package/src/composites/Wordmark.tsx +2 -2
  235. package/src/composites/cartesian-charts.test.tsx +52 -0
  236. package/src/composites/cartesian-charts.tsx +1287 -0
  237. package/src/composites/chart-composition.test.tsx +53 -0
  238. package/src/composites/chart-composition.tsx +898 -0
  239. package/src/composites/chart-ui.tsx +328 -0
  240. package/src/composites/charts-3d.test.tsx +25 -0
  241. package/src/composites/charts-3d.tsx +318 -0
  242. package/src/composites/charts.test.tsx +254 -0
  243. package/src/composites/chrome.test.tsx +29 -0
  244. package/src/composites/flow-charts.test.tsx +79 -0
  245. package/src/composites/flow-charts.tsx +607 -0
  246. package/src/composites/marketing.test.tsx +38 -34
  247. package/src/composites/radial-charts.test.tsx +74 -0
  248. package/src/composites/radial-charts.tsx +617 -0
  249. package/src/composites/scheduler-ui.tsx +306 -0
  250. package/src/composites/scheduler.test.tsx +280 -0
  251. package/src/index.test.ts +26 -0
  252. package/src/index.ts +287 -5
  253. package/src/lib/chart-export.test.ts +20 -0
  254. package/src/lib/chart-export.ts +99 -0
  255. package/src/lib/charts-platform.test.ts +93 -0
  256. package/src/lib/charts.test.ts +132 -0
  257. package/src/lib/charts.ts +1021 -0
  258. package/src/lib/geo.test.ts +42 -0
  259. package/src/lib/geo.ts +138 -0
  260. package/src/lib/iso.test.ts +22 -0
  261. package/src/lib/iso.ts +102 -0
  262. package/src/lib/rrule.test.ts +53 -0
  263. package/src/lib/rrule.ts +281 -0
  264. package/src/lib/scheduler.test.ts +199 -0
  265. package/src/lib/scheduler.ts +693 -0
  266. package/src/lib/tokens.test.ts +2 -2
  267. package/src/lib/webgl-scatter.test.ts +36 -0
  268. package/src/lib/webgl-scatter.ts +230 -0
  269. package/src/primitives/Accordion.tsx +218 -63
  270. package/src/primitives/AppBar.tsx +39 -0
  271. package/src/primitives/Autocomplete.tsx +236 -0
  272. package/src/primitives/Avatar.tsx +52 -1
  273. package/src/primitives/Backdrop.tsx +48 -0
  274. package/src/primitives/BottomNavigation.tsx +102 -0
  275. package/src/primitives/ButtonBase.tsx +34 -0
  276. package/src/primitives/ButtonGroup.tsx +90 -0
  277. package/src/primitives/CircularProgress.tsx +104 -0
  278. package/src/primitives/ClickAwayListener.tsx +46 -0
  279. package/src/primitives/Collapse.tsx +39 -0
  280. package/src/primitives/Container.tsx +40 -0
  281. package/src/primitives/Fab.tsx +62 -0
  282. package/src/primitives/FormControl.tsx +90 -0
  283. package/src/primitives/FormControlLabel.tsx +51 -0
  284. package/src/primitives/Grid.tsx +101 -0
  285. package/src/primitives/IconButton.tsx +59 -0
  286. package/src/primitives/ImageList.test.tsx +48 -0
  287. package/src/primitives/ImageList.tsx +112 -0
  288. package/src/primitives/InputAdornment.tsx +86 -0
  289. package/src/primitives/Link.tsx +48 -0
  290. package/src/primitives/List.tsx +121 -0
  291. package/src/primitives/Masonry.tsx +32 -0
  292. package/src/primitives/Menu.tsx +112 -0
  293. package/src/primitives/MobileStepper.tsx +64 -0
  294. package/src/primitives/Modal.tsx +70 -0
  295. package/src/primitives/NoSsr.tsx +28 -0
  296. package/src/primitives/Paper.tsx +55 -0
  297. package/src/primitives/Progress.tsx +25 -7
  298. package/src/primitives/Snackbar.tsx +83 -0
  299. package/src/primitives/SpeedDial.tsx +110 -0
  300. package/src/primitives/Stack.tsx +88 -0
  301. package/src/primitives/StarRating.tsx +87 -28
  302. package/src/primitives/Stepper.tsx +184 -0
  303. package/src/primitives/TablePagination.tsx +58 -0
  304. package/src/primitives/TextField.tsx +105 -0
  305. package/src/primitives/TextareaAutosize.tsx +49 -0
  306. package/src/primitives/ToggleButton.tsx +148 -0
  307. package/src/primitives/TransferList.tsx +110 -0
  308. package/src/primitives/Typography.tsx +90 -0
  309. package/src/primitives/mui-more.test.tsx +76 -0
  310. package/src/primitives/mui.test.tsx +134 -0
  311. package/src/primitives/transitions.tsx +120 -0
@@ -0,0 +1,1030 @@
1
+ import {
2
+ useMemo,
3
+ useRef,
4
+ useState,
5
+ type KeyboardEvent as ReactKeyboardEvent,
6
+ type PointerEvent as ReactPointerEvent,
7
+ type ReactNode,
8
+ } from "react";
9
+ import { cn } from "../lib/cn";
10
+ import { expandVisibleEvents } from "../lib/rrule";
11
+ import { useControllableState } from "../lib/use-controllable-state";
12
+ import {
13
+ addDays,
14
+ addMonths,
15
+ agendaDays,
16
+ DEFAULT_SCHEDULER_PREFERENCES,
17
+ eventCanDrag,
18
+ eventCanResize,
19
+ eventColorStyle,
20
+ filterWeekendDays,
21
+ formatDateSpan,
22
+ formatDayHeading,
23
+ formatHour,
24
+ formatMonthYear,
25
+ formatTime,
26
+ formatWeekRange,
27
+ getMonthGrid,
28
+ getWeekDays,
29
+ hoursInView,
30
+ isMonthBar,
31
+ isSameDay,
32
+ isSameMonth,
33
+ isoDay,
34
+ layoutTimedEvents,
35
+ normalizeEvents,
36
+ occupiesDay,
37
+ occurrenceKey,
38
+ resizeEvent,
39
+ shiftByDays,
40
+ startOfDay,
41
+ timedEventsOnDay,
42
+ toDate,
43
+ visibleEvents,
44
+ weekNumber,
45
+ weekSpanLanesForDays,
46
+ type CalendarView,
47
+ type NormalizedEvent,
48
+ type SchedulerEvent,
49
+ type SchedulerPreferences,
50
+ type SchedulerResource,
51
+ } from "../lib/scheduler";
52
+ import { EventEditor, type EventEditorDraft } from "./EventEditor";
53
+ import {
54
+ CALENDAR_VIEW_LABEL,
55
+ EventResizeHandle,
56
+ ResourceLegend,
57
+ SchedulerPreferencesMenu,
58
+ SchedulerShell,
59
+ SchedulerToolbar,
60
+ } from "./scheduler-ui";
61
+ import { DropdownMenuItem } from "../primitives/DropdownMenu";
62
+
63
+ export type EventCalendarEventVariant =
64
+ | "month-bar"
65
+ | "month-timed"
66
+ | "all-day"
67
+ | "time-grid"
68
+ | "agenda";
69
+
70
+ export type EventCalendarEventRenderContext = {
71
+ view: CalendarView;
72
+ variant: EventCalendarEventVariant;
73
+ locale?: string;
74
+ hour12: boolean;
75
+ source?: SchedulerEvent;
76
+ defaultChildren: ReactNode;
77
+ };
78
+
79
+ export type EventCalendarToolbarContext = {
80
+ view: CalendarView;
81
+ date: Date;
82
+ title: string;
83
+ views: CalendarView[];
84
+ locale?: string;
85
+ preferences: Required<SchedulerPreferences>;
86
+ onViewChange: (view: CalendarView) => void;
87
+ onPrev: () => void;
88
+ onNext: () => void;
89
+ onToday: () => void;
90
+ onDateChange: (date: Date) => void;
91
+ onPreferencesChange: (next: Required<SchedulerPreferences>) => void;
92
+ };
93
+
94
+ export type EventCalendarProps = {
95
+ events?: SchedulerEvent[];
96
+ resources?: SchedulerResource[];
97
+ view?: CalendarView;
98
+ defaultView?: CalendarView;
99
+ onViewChange?: (view: CalendarView) => void;
100
+ date?: Date;
101
+ defaultDate?: Date;
102
+ onDateChange?: (date: Date) => void;
103
+ views?: CalendarView[];
104
+ weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
105
+ hourStart?: number;
106
+ hourEnd?: number;
107
+ locale?: string;
108
+ readOnly?: boolean;
109
+ areEventsDraggable?: boolean;
110
+ areEventsResizable?: boolean;
111
+ preferences?: SchedulerPreferences;
112
+ defaultPreferences?: SchedulerPreferences;
113
+ onPreferencesChange?: (preferences: Required<SchedulerPreferences>) => void;
114
+ onEventClick?: (event: SchedulerEvent) => void;
115
+ onEventChange?: (event: SchedulerEvent, next: Partial<SchedulerEvent>) => void;
116
+ onEventCreate?: (event: SchedulerEvent) => void;
117
+ onEventDelete?: (event: SchedulerEvent) => void;
118
+ onSlotClick?: (slot: { start: Date; end: Date; allDay: boolean }) => void;
119
+ /**
120
+ * Built-in create/edit dialog. Defaults to true.
121
+ * Set false when the host owns create/edit (custom schedule modal, etc.).
122
+ */
123
+ showEventEditor?: boolean;
124
+ /** Preferences gear in the default toolbar. Ignored when `renderToolbar` is set. */
125
+ showPreferences?: boolean;
126
+ /** Extra controls after the view menu. Ignored when `renderToolbar` is set. */
127
+ toolbarTrailing?: ReactNode;
128
+ /** Replace event chip contents. The kit still wraps with the click/drag trigger. */
129
+ renderEvent?: (
130
+ event: NormalizedEvent,
131
+ context: EventCalendarEventRenderContext,
132
+ ) => ReactNode;
133
+ /** Replace the toolbar. The default kit toolbar is used when omitted. */
134
+ renderToolbar?: (context: EventCalendarToolbarContext) => ReactNode;
135
+ className?: string;
136
+ };
137
+
138
+ const MAX_TIMED_IN_CELL = 3;
139
+ const AGENDA_LENGTH = 14;
140
+
141
+ function chunkWeeks<T>(items: T[]): T[][] {
142
+ const weeks: T[][] = [];
143
+ for (let i = 0; i < items.length; i += 7) weeks.push(items.slice(i, i + 7));
144
+ return weeks;
145
+ }
146
+
147
+ function sourceEvent(events: SchedulerEvent[], id: string): SchedulerEvent | undefined {
148
+ return events.find((event) => event.id === id);
149
+ }
150
+
151
+ function startOfToday(): Date {
152
+ const now = new Date();
153
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate());
154
+ }
155
+
156
+ function visibleRange(
157
+ view: CalendarView,
158
+ date: Date,
159
+ weekStartsOn: number,
160
+ ): { start: Date; end: Date } {
161
+ if (view === "month") {
162
+ const grid = getMonthGrid(date, weekStartsOn);
163
+ return { start: grid[0], end: addDays(grid[grid.length - 1], 1) };
164
+ }
165
+ if (view === "week") {
166
+ const days = getWeekDays(date, weekStartsOn);
167
+ return { start: days[0], end: addDays(days[6], 1) };
168
+ }
169
+ if (view === "agenda") {
170
+ const days = agendaDays(date, AGENDA_LENGTH);
171
+ return { start: days[0], end: addDays(days[days.length - 1], 1) };
172
+ }
173
+ const start = startOfDay(date);
174
+ return { start, end: addDays(start, 1) };
175
+ }
176
+
177
+ function draftFromSource(source: SchedulerEvent): EventEditorDraft {
178
+ return {
179
+ id: source.id,
180
+ title: source.title,
181
+ start: toDate(source.start),
182
+ end: toDate(source.end),
183
+ allDay: source.allDay,
184
+ resourceId: source.resourceId,
185
+ color: source.color,
186
+ description: source.description,
187
+ rrule: source.rrule,
188
+ };
189
+ }
190
+
191
+ /** Month / week / day / agenda calendar with editor, resize, recurrence, and preferences. */
192
+ export function EventCalendar({
193
+ events = [],
194
+ resources = [],
195
+ view: viewProp,
196
+ defaultView = "month",
197
+ onViewChange,
198
+ date: dateProp,
199
+ defaultDate,
200
+ onDateChange,
201
+ views = ["month", "week", "day", "agenda"],
202
+ weekStartsOn = 0,
203
+ hourStart = 7,
204
+ hourEnd = 21,
205
+ locale,
206
+ readOnly = false,
207
+ areEventsDraggable,
208
+ areEventsResizable,
209
+ preferences,
210
+ defaultPreferences,
211
+ onPreferencesChange,
212
+ onEventClick,
213
+ onEventChange,
214
+ onEventCreate,
215
+ onEventDelete,
216
+ onSlotClick,
217
+ showEventEditor = true,
218
+ showPreferences = true,
219
+ toolbarTrailing,
220
+ renderEvent,
221
+ renderToolbar,
222
+ className,
223
+ }: EventCalendarProps) {
224
+ const [view, setView] = useControllableState<CalendarView>({
225
+ prop: viewProp,
226
+ defaultProp: defaultView,
227
+ onChange: onViewChange,
228
+ });
229
+ const [date, setDate] = useControllableState<Date>({
230
+ prop: dateProp,
231
+ defaultProp: defaultDate ?? new Date(),
232
+ onChange: onDateChange,
233
+ });
234
+ const [prefs, setPrefs] = useControllableState<Required<SchedulerPreferences>>({
235
+ prop: preferences
236
+ ? { ...DEFAULT_SCHEDULER_PREFERENCES, weekStartsOn, ...defaultPreferences, ...preferences }
237
+ : undefined,
238
+ defaultProp: { ...DEFAULT_SCHEDULER_PREFERENCES, weekStartsOn, ...defaultPreferences },
239
+ onChange: onPreferencesChange,
240
+ });
241
+ const [hiddenIds, setHiddenIds] = useState<Set<string>>(new Set());
242
+ const [draft, setDraft] = useState<EventEditorDraft | null>(null);
243
+ const dragRef = useRef<{ id: string; x: number; y: number; active: boolean } | null>(null);
244
+
245
+ const resolvedView = view ?? "month";
246
+ const resolvedDate = date ?? new Date();
247
+ const resolvedPrefs = prefs ?? { ...DEFAULT_SCHEDULER_PREFERENCES, weekStartsOn };
248
+ const startOn = resolvedPrefs.weekStartsOn;
249
+ const today = startOfToday();
250
+ const hour12 = resolvedPrefs.ampm;
251
+ const canDrag = !readOnly && (areEventsDraggable ?? Boolean(onEventChange));
252
+ const canResize = !readOnly && (areEventsResizable ?? Boolean(onEventChange));
253
+ const editorLocked = readOnly || Boolean(draft?.id && sourceEvent(events, draft.id)?.readOnly);
254
+
255
+ const range = useMemo(
256
+ () => visibleRange(resolvedView, resolvedDate, startOn),
257
+ [resolvedView, resolvedDate, startOn],
258
+ );
259
+
260
+ const normalized = useMemo(() => {
261
+ const visible = visibleEvents(normalizeEvents(events, resources), hiddenIds);
262
+ return expandVisibleEvents(visible, range.start, range.end, events);
263
+ }, [events, resources, hiddenIds, range.start, range.end]);
264
+
265
+ const title =
266
+ resolvedView === "month"
267
+ ? formatMonthYear(resolvedDate, locale)
268
+ : resolvedView === "week"
269
+ ? formatWeekRange(resolvedDate, startOn, locale)
270
+ : resolvedView === "agenda"
271
+ ? formatDateSpan(range.start, addDays(range.end, -1), locale)
272
+ : formatDayHeading(resolvedDate, locale);
273
+
274
+ function go(direction: -1 | 1) {
275
+ if (resolvedView === "month") setDate(addMonths(resolvedDate, direction));
276
+ else if (resolvedView === "week") setDate(addDays(resolvedDate, direction * 7));
277
+ else if (resolvedView === "agenda") setDate(addDays(resolvedDate, direction * 7));
278
+ else setDate(addDays(resolvedDate, direction));
279
+ }
280
+
281
+ function toggleResource(id: string) {
282
+ setHiddenIds((prev) => {
283
+ const next = new Set(prev);
284
+ if (next.has(id)) next.delete(id);
285
+ else next.add(id);
286
+ return next;
287
+ });
288
+ }
289
+
290
+ function openEvent(event: NormalizedEvent) {
291
+ const source = sourceEvent(events, event.id);
292
+ if (source) onEventClick?.(source);
293
+ if (!showEventEditor) return;
294
+ setDraft(source ? draftFromSource(source) : {
295
+ id: event.id,
296
+ title: event.title,
297
+ start: event.start,
298
+ end: event.end,
299
+ allDay: event.allDay,
300
+ resourceId: event.resourceId,
301
+ color: event.color,
302
+ description: event.description,
303
+ rrule: event.rrule,
304
+ });
305
+ }
306
+
307
+ function openSlot(slot: { start: Date; end: Date; allDay: boolean }) {
308
+ onSlotClick?.(slot);
309
+ if (readOnly || !showEventEditor) return;
310
+ setDraft({ start: slot.start, end: slot.end, allDay: slot.allDay });
311
+ }
312
+
313
+ function handleEventPointerDown(event: NormalizedEvent, pointer: ReactPointerEvent) {
314
+ if (!onEventChange || !eventCanDrag(event, { readOnly, enabled: canDrag })) return;
315
+ (pointer.currentTarget as HTMLElement).setPointerCapture?.(pointer.pointerId);
316
+ dragRef.current = { id: event.id, x: pointer.clientX, y: pointer.clientY, active: false };
317
+ }
318
+
319
+ function handleEventPointerMove(pointer: ReactPointerEvent) {
320
+ const drag = dragRef.current;
321
+ if (!drag) return;
322
+ if (Math.hypot(pointer.clientX - drag.x, pointer.clientY - drag.y) > 6) {
323
+ drag.active = true;
324
+ }
325
+ }
326
+
327
+ function handleEventPointerUp(event: NormalizedEvent, pointer: ReactPointerEvent) {
328
+ const drag = dragRef.current;
329
+ dragRef.current = null;
330
+ if (!drag?.active || !onEventChange) return;
331
+ const target = document
332
+ .elementFromPoint(pointer.clientX, pointer.clientY)
333
+ ?.closest("[data-scheduler-day]") as HTMLElement | null;
334
+ const iso = target?.dataset.schedulerDay;
335
+ if (!iso) return;
336
+ const [year, month, day] = iso.split("-").map(Number);
337
+ const targetDay = new Date(year, month - 1, day);
338
+ const currentDay = startOfDay(event.start);
339
+ const delta = Math.round((targetDay.getTime() - currentDay.getTime()) / 86_400_000);
340
+ if (delta === 0) return;
341
+ const source = sourceEvent(events, event.id);
342
+ if (!source) return;
343
+ const master = normalizeEvents([source], resources)[0];
344
+ onEventChange(source, shiftByDays(master, delta));
345
+ }
346
+
347
+ function commitResize(event: NormalizedEvent, edge: "start" | "end", instant: Date) {
348
+ if (!onEventChange) return;
349
+ const next = resizeEvent(event, edge, instant);
350
+ const source = sourceEvent(events, event.id);
351
+ if (!source) return;
352
+ const master = normalizeEvents([source], resources)[0];
353
+ onEventChange(source, {
354
+ start: new Date(master.start.getTime() + (next.start.getTime() - event.start.getTime())),
355
+ end: new Date(master.end.getTime() + (next.end.getTime() - event.end.getTime())),
356
+ });
357
+ }
358
+
359
+ function handleKeyDown(keyboard: ReactKeyboardEvent<HTMLDivElement>) {
360
+ if (draft) return;
361
+ const target = keyboard.target as HTMLElement;
362
+ if (target.closest("input, textarea, select, [contenteditable='true']")) return;
363
+ if (keyboard.key === "ArrowLeft") {
364
+ keyboard.preventDefault();
365
+ go(-1);
366
+ } else if (keyboard.key === "ArrowRight") {
367
+ keyboard.preventDefault();
368
+ go(1);
369
+ } else if (keyboard.key === "t" || keyboard.key === "T") {
370
+ keyboard.preventDefault();
371
+ setDate(new Date());
372
+ }
373
+ }
374
+
375
+ function renderEventTrigger(
376
+ event: NormalizedEvent,
377
+ className: string,
378
+ defaultChildren: ReactNode,
379
+ options?: { resize?: "ns"; variant: EventCalendarEventVariant },
380
+ ) {
381
+ const source = sourceEvent(events, event.id);
382
+ const variant = options?.variant ?? "month-timed";
383
+ const children = renderEvent
384
+ ? renderEvent(event, {
385
+ view: resolvedView,
386
+ variant,
387
+ locale,
388
+ hour12,
389
+ source,
390
+ defaultChildren,
391
+ })
392
+ : defaultChildren;
393
+ if (renderEvent && children == null) return null;
394
+
395
+ const resizable =
396
+ options?.resize === "ns" &&
397
+ eventCanResize(event, { readOnly, enabled: canResize }) &&
398
+ Boolean(onEventChange);
399
+ const painted = variant === "month-bar" || variant === "all-day" || variant === "time-grid";
400
+
401
+ const button = (
402
+ <button
403
+ type="button"
404
+ draggable={false}
405
+ className={className}
406
+ style={
407
+ variant === "agenda"
408
+ ? eventColorStyle(event.color, "wash")
409
+ : painted
410
+ ? eventColorStyle(event.color, resolvedView === "month" ? "bar" : "block")
411
+ : undefined
412
+ }
413
+ onClick={() => {
414
+ if (dragRef.current?.active) return;
415
+ openEvent(event);
416
+ }}
417
+ onPointerDown={(pointer) => handleEventPointerDown(event, pointer)}
418
+ onPointerMove={handleEventPointerMove}
419
+ onPointerUp={(pointer) => handleEventPointerUp(event, pointer)}
420
+ >
421
+ {children}
422
+ </button>
423
+ );
424
+
425
+ if (!resizable) return button;
426
+
427
+ return (
428
+ <div className="relative h-full min-h-0">
429
+ {button}
430
+ <EventResizeHandle
431
+ edge="start"
432
+ axis="ns"
433
+ event={event}
434
+ hourStart={hourStart}
435
+ hourEnd={hourEnd}
436
+ onCommit={commitResize}
437
+ />
438
+ <EventResizeHandle
439
+ edge="end"
440
+ axis="ns"
441
+ event={event}
442
+ hourStart={hourStart}
443
+ hourEnd={hourEnd}
444
+ onCommit={commitResize}
445
+ />
446
+ </div>
447
+ );
448
+ }
449
+
450
+ return (
451
+ <SchedulerShell
452
+ slot="event-calendar"
453
+ className={className}
454
+ tabIndex={0}
455
+ aria-label="Event calendar"
456
+ onKeyDown={handleKeyDown}
457
+ >
458
+ {renderToolbar ? (
459
+ renderToolbar({
460
+ view: resolvedView,
461
+ date: resolvedDate,
462
+ title,
463
+ views,
464
+ locale,
465
+ preferences: resolvedPrefs,
466
+ onViewChange: (next) => setView(next),
467
+ onPrev: () => go(-1),
468
+ onNext: () => go(1),
469
+ onToday: () => setDate(new Date()),
470
+ onDateChange: (next) => setDate(next),
471
+ onPreferencesChange: (next) => setPrefs(next),
472
+ })
473
+ ) : (
474
+ <SchedulerToolbar
475
+ title={title}
476
+ onPrev={() => go(-1)}
477
+ onNext={() => go(1)}
478
+ onToday={() => setDate(new Date())}
479
+ menuLabel={CALENDAR_VIEW_LABEL[resolvedView]}
480
+ menu={views.map((item) => (
481
+ <DropdownMenuItem key={item} onClick={() => setView(item)}>
482
+ {CALENDAR_VIEW_LABEL[item]}
483
+ </DropdownMenuItem>
484
+ ))}
485
+ trailing={
486
+ <>
487
+ {toolbarTrailing}
488
+ {showPreferences ? (
489
+ <SchedulerPreferencesMenu
490
+ value={resolvedPrefs}
491
+ onChange={(next) => setPrefs(next)}
492
+ />
493
+ ) : null}
494
+ </>
495
+ }
496
+ />
497
+ )}
498
+
499
+ {resolvedView === "month" ? (
500
+ <MonthGrid
501
+ date={resolvedDate}
502
+ today={today}
503
+ events={normalized}
504
+ weekStartsOn={startOn}
505
+ showWeekends={resolvedPrefs.showWeekends}
506
+ showWeekNumber={resolvedPrefs.showWeekNumber}
507
+ locale={locale}
508
+ hour12={hour12}
509
+ onSlotClick={openSlot}
510
+ renderEventTrigger={renderEventTrigger}
511
+ />
512
+ ) : resolvedView === "agenda" ? (
513
+ <AgendaList
514
+ date={resolvedDate}
515
+ events={normalized}
516
+ showWeekends={resolvedPrefs.showWeekends}
517
+ locale={locale}
518
+ hour12={hour12}
519
+ onSlotClick={openSlot}
520
+ renderEventTrigger={renderEventTrigger}
521
+ />
522
+ ) : (
523
+ <TimeGrid
524
+ date={resolvedDate}
525
+ today={today}
526
+ events={normalized}
527
+ view={resolvedView === "day" ? "day" : "week"}
528
+ weekStartsOn={startOn}
529
+ showWeekends={resolvedPrefs.showWeekends}
530
+ hourStart={hourStart}
531
+ hourEnd={hourEnd}
532
+ locale={locale}
533
+ hour12={hour12}
534
+ onSlotClick={openSlot}
535
+ renderEventTrigger={renderEventTrigger}
536
+ />
537
+ )}
538
+
539
+ <ResourceLegend resources={resources} hiddenIds={hiddenIds} onToggle={toggleResource} />
540
+
541
+ {showEventEditor ? (
542
+ <EventEditor
543
+ open={draft != null}
544
+ onOpenChange={(open) => {
545
+ if (!open) setDraft(null);
546
+ }}
547
+ draft={draft}
548
+ resources={resources}
549
+ readOnly={editorLocked}
550
+ onSave={(event) => {
551
+ if (event.id && sourceEvent(events, event.id)) {
552
+ const source = sourceEvent(events, event.id)!;
553
+ onEventChange?.(source, event);
554
+ } else {
555
+ onEventCreate?.(event);
556
+ }
557
+ }}
558
+ onDelete={
559
+ onEventDelete
560
+ ? (id) => {
561
+ const source = sourceEvent(events, id);
562
+ if (source) onEventDelete(source);
563
+ }
564
+ : undefined
565
+ }
566
+ />
567
+ ) : null}
568
+ </SchedulerShell>
569
+ );
570
+ }
571
+
572
+ function MonthGrid({
573
+ date,
574
+ today,
575
+ events,
576
+ weekStartsOn,
577
+ showWeekends,
578
+ showWeekNumber,
579
+ locale,
580
+ hour12,
581
+ onSlotClick,
582
+ renderEventTrigger,
583
+ }: {
584
+ date: Date;
585
+ today: Date;
586
+ events: NormalizedEvent[];
587
+ weekStartsOn: number;
588
+ showWeekends: boolean;
589
+ showWeekNumber: boolean;
590
+ locale?: string;
591
+ hour12: boolean;
592
+ onSlotClick?: EventCalendarProps["onSlotClick"];
593
+ renderEventTrigger: (
594
+ event: NormalizedEvent,
595
+ className: string,
596
+ children: ReactNode,
597
+ options?: { resize?: "ns"; variant: EventCalendarEventVariant },
598
+ ) => ReactNode;
599
+ }) {
600
+ const weeks = chunkWeeks(getMonthGrid(date, weekStartsOn));
601
+ const headerDays = filterWeekendDays(getWeekDays(date, weekStartsOn), showWeekends);
602
+ const columns = headerDays.length;
603
+ const gridStyle = { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` };
604
+
605
+ return (
606
+ <div className="min-w-0">
607
+ <div className="flex border-b border-border/50">
608
+ {showWeekNumber ? <div className="w-8 shrink-0" /> : null}
609
+ <div className="grid min-w-0 flex-1" style={gridStyle}>
610
+ {headerDays.map((day) => (
611
+ <div
612
+ key={day.getDay()}
613
+ className="px-2 py-2 text-center text-[11px] font-bold uppercase tracking-wider text-muted-foreground"
614
+ >
615
+ {new Intl.DateTimeFormat(locale, { weekday: "short" }).format(day)}
616
+ </div>
617
+ ))}
618
+ </div>
619
+ </div>
620
+ <div>
621
+ {weeks.map((week) => {
622
+ const days = filterWeekendDays(week, showWeekends);
623
+ const lanes = weekSpanLanesForDays(days, events);
624
+ return (
625
+ <div key={week[0].toISOString()} className="flex border-b border-border/40 last:border-b-0">
626
+ {showWeekNumber ? (
627
+ <div className="flex w-8 shrink-0 items-start justify-center pt-2 text-[10px] font-bold text-muted-foreground">
628
+ {weekNumber(week[0])}
629
+ </div>
630
+ ) : null}
631
+ <div className="min-w-0 flex-1">
632
+ <div className="grid" style={gridStyle}>
633
+ {days.map((day) => {
634
+ const inMonth = isSameMonth(day, date);
635
+ const isToday = isSameDay(day, today);
636
+ return (
637
+ <div
638
+ key={day.toISOString()}
639
+ data-scheduler-day={isoDay(day)}
640
+ className={cn(
641
+ "flex items-start justify-end px-1.5 pt-1.5",
642
+ !inMonth && "bg-muted/15",
643
+ )}
644
+ >
645
+ <button
646
+ type="button"
647
+ onClick={() =>
648
+ onSlotClick?.({
649
+ start: day,
650
+ end: addDays(day, 1),
651
+ allDay: true,
652
+ })
653
+ }
654
+ className={cn(
655
+ "flex size-7 items-center justify-center rounded-full text-xs font-bold",
656
+ isToday && "bg-primary text-primary-foreground",
657
+ !isToday && inMonth && "text-foreground hover:bg-muted/50",
658
+ !isToday && !inMonth && "text-muted-foreground/70",
659
+ )}
660
+ >
661
+ {day.getDate()}
662
+ </button>
663
+ </div>
664
+ );
665
+ })}
666
+ </div>
667
+ {lanes.map((lane, laneIndex) => (
668
+ <div key={laneIndex} className="grid gap-px px-0.5 pb-0.5" style={gridStyle}>
669
+ {laneSlots(lane, columns).map((slot, slotIndex) =>
670
+ slot.kind === "empty" ? (
671
+ <div key={slotIndex} />
672
+ ) : (
673
+ <div
674
+ key={occurrenceKey(slot.event)}
675
+ className="min-w-0"
676
+ style={{ gridColumn: `span ${slot.span}` }}
677
+ >
678
+ {renderEventTrigger(
679
+ slot.event,
680
+ "flex h-5 w-full items-center truncate rounded-md px-1.5 text-[11px] font-semibold leading-none",
681
+ slot.event.title,
682
+ { variant: "month-bar" },
683
+ )}
684
+ </div>
685
+ ),
686
+ )}
687
+ </div>
688
+ ))}
689
+ <div className="grid min-h-[4.25rem]" style={gridStyle}>
690
+ {days.map((day) => {
691
+ const timed = timedEventsOnDay(events, day);
692
+ const visibleTimed = timed.slice(0, MAX_TIMED_IN_CELL);
693
+ const extra = timed.length - visibleTimed.length;
694
+ return (
695
+ <div
696
+ key={day.toISOString()}
697
+ data-scheduler-day={isoDay(day)}
698
+ className={cn(
699
+ "min-w-0 space-y-0.5 px-1 pb-2",
700
+ !isSameMonth(day, date) && "bg-muted/15",
701
+ )}
702
+ onClick={(click) => {
703
+ if ((click.target as HTMLElement).closest("button")) return;
704
+ onSlotClick?.({
705
+ start: new Date(day.getFullYear(), day.getMonth(), day.getDate(), 9),
706
+ end: new Date(day.getFullYear(), day.getMonth(), day.getDate(), 10),
707
+ allDay: false,
708
+ });
709
+ }}
710
+ >
711
+ {visibleTimed.map((event) => (
712
+ <div key={occurrenceKey(event)} className="min-w-0">
713
+ {renderEventTrigger(
714
+ event,
715
+ "flex w-full min-w-0 items-center gap-1 rounded-sm px-0.5 py-0.5 text-left text-[11px] leading-tight text-foreground hover:bg-muted/40",
716
+ <>
717
+ <span
718
+ className="size-1.5 shrink-0 rounded-full"
719
+ style={eventColorStyle(event.color, "dot")}
720
+ />
721
+ <span className="shrink-0 text-muted-foreground">
722
+ {formatTime(event.start, locale, hour12)}
723
+ </span>
724
+ <span className="min-w-0 truncate font-medium">{event.title}</span>
725
+ </>,
726
+ { variant: "month-timed" },
727
+ )}
728
+ </div>
729
+ ))}
730
+ {extra > 0 ? (
731
+ <p className="px-0.5 text-[10px] font-bold text-muted-foreground">+{extra} more</p>
732
+ ) : null}
733
+ </div>
734
+ );
735
+ })}
736
+ </div>
737
+ </div>
738
+ </div>
739
+ );
740
+ })}
741
+ </div>
742
+ </div>
743
+ );
744
+ }
745
+
746
+ function laneSlots(lane: { event: NormalizedEvent; startCol: number; span: number }[], columns: number) {
747
+ const slots: Array<
748
+ | { kind: "empty"; span: 1 }
749
+ | { kind: "event"; event: NormalizedEvent; span: number }
750
+ > = [];
751
+ let cursor = 0;
752
+ const ordered = [...lane].sort((a, b) => a.startCol - b.startCol);
753
+ for (const segment of ordered) {
754
+ while (cursor < segment.startCol) {
755
+ slots.push({ kind: "empty", span: 1 });
756
+ cursor += 1;
757
+ }
758
+ slots.push({ kind: "event", event: segment.event, span: segment.span });
759
+ cursor += segment.span;
760
+ }
761
+ while (cursor < columns) {
762
+ slots.push({ kind: "empty", span: 1 });
763
+ cursor += 1;
764
+ }
765
+ return slots;
766
+ }
767
+
768
+ function TimeGrid({
769
+ date,
770
+ today,
771
+ events,
772
+ view,
773
+ weekStartsOn,
774
+ showWeekends,
775
+ hourStart,
776
+ hourEnd,
777
+ locale,
778
+ hour12,
779
+ onSlotClick,
780
+ renderEventTrigger,
781
+ }: {
782
+ date: Date;
783
+ today: Date;
784
+ events: NormalizedEvent[];
785
+ view: "week" | "day";
786
+ weekStartsOn: number;
787
+ showWeekends: boolean;
788
+ hourStart: number;
789
+ hourEnd: number;
790
+ locale?: string;
791
+ hour12: boolean;
792
+ onSlotClick?: EventCalendarProps["onSlotClick"];
793
+ renderEventTrigger: (
794
+ event: NormalizedEvent,
795
+ className: string,
796
+ children: ReactNode,
797
+ options?: { resize?: "ns"; variant: EventCalendarEventVariant },
798
+ ) => ReactNode;
799
+ }) {
800
+ const days =
801
+ view === "day"
802
+ ? [date]
803
+ : filterWeekendDays(getWeekDays(date, weekStartsOn), showWeekends);
804
+ const hours = hoursInView(hourStart, hourEnd);
805
+ const hourHeight = 48;
806
+ const now = new Date();
807
+ const showNow = days.some((day) => isSameDay(day, now));
808
+ const nowTop =
809
+ ((now.getHours() + now.getMinutes() / 60 - hourStart) / (hourEnd - hourStart)) * (hours.length * hourHeight);
810
+
811
+ return (
812
+ <div className="overflow-x-auto">
813
+ <div
814
+ className="grid min-w-[640px]"
815
+ style={{ gridTemplateColumns: `3.5rem repeat(${days.length}, minmax(0, 1fr))` }}
816
+ >
817
+ <div className="border-b border-border/50" />
818
+ {days.map((day) => (
819
+ <div
820
+ key={day.toISOString()}
821
+ className={cn(
822
+ "border-b border-l border-border/50 px-2 py-2 text-center",
823
+ isSameDay(day, today) && "bg-primary/5",
824
+ )}
825
+ >
826
+ <p className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
827
+ {new Intl.DateTimeFormat(locale, { weekday: "short" }).format(day)}
828
+ </p>
829
+ <p
830
+ className={cn(
831
+ "mx-auto mt-0.5 flex size-7 items-center justify-center rounded-full text-sm font-black",
832
+ isSameDay(day, today) && "bg-primary text-primary-foreground",
833
+ )}
834
+ >
835
+ {day.getDate()}
836
+ </p>
837
+ </div>
838
+ ))}
839
+
840
+ <div className="border-b border-border/50" />
841
+ {days.map((day) => {
842
+ const allDay = events.filter((event) => isMonthBar(event) && occupiesDay(event, day));
843
+ return (
844
+ <div
845
+ key={`allday-${day.toISOString()}`}
846
+ data-scheduler-day={isoDay(day)}
847
+ className="min-h-10 space-y-0.5 border-b border-l border-border/50 p-1"
848
+ >
849
+ {allDay.map((event) => (
850
+ <div key={occurrenceKey(event)}>
851
+ {renderEventTrigger(
852
+ event,
853
+ "flex h-5 w-full items-center truncate rounded-md px-1.5 text-[11px] font-semibold",
854
+ event.title,
855
+ { variant: "all-day" },
856
+ )}
857
+ </div>
858
+ ))}
859
+ </div>
860
+ );
861
+ })}
862
+ </div>
863
+
864
+ <div className="relative min-w-[640px]" style={{ height: hours.length * hourHeight }}>
865
+ <div
866
+ className="grid h-full"
867
+ style={{ gridTemplateColumns: `3.5rem repeat(${days.length}, minmax(0, 1fr))` }}
868
+ >
869
+ <div className="relative">
870
+ {hours.map((hour) => (
871
+ <div
872
+ key={hour}
873
+ className="relative border-b border-border/30 pr-2 text-right text-[10px] font-bold text-muted-foreground"
874
+ style={{ height: hourHeight }}
875
+ >
876
+ <span className="-translate-y-2 absolute right-2">{formatHour(hour, locale, hour12)}</span>
877
+ </div>
878
+ ))}
879
+ </div>
880
+ {days.map((day) => {
881
+ const layout = layoutTimedEvents(events, day, hourStart, hourEnd);
882
+ return (
883
+ <div
884
+ key={day.toISOString()}
885
+ data-scheduler-day={isoDay(day)}
886
+ className="relative border-l border-border/40"
887
+ onClick={(click) => {
888
+ if ((click.target as HTMLElement).closest("button")) return;
889
+ const rect = click.currentTarget.getBoundingClientRect();
890
+ const ratio = (click.clientY - rect.top) / rect.height;
891
+ const hour = hourStart + ratio * (hourEnd - hourStart);
892
+ const startHour = Math.floor(hour);
893
+ const start = new Date(day.getFullYear(), day.getMonth(), day.getDate(), startHour);
894
+ onSlotClick?.({
895
+ start,
896
+ end: new Date(start.getTime() + 3_600_000),
897
+ allDay: false,
898
+ });
899
+ }}
900
+ >
901
+ {hours.map((hour) => (
902
+ <div key={hour} className="border-b border-border/30" style={{ height: hourHeight }} />
903
+ ))}
904
+ {layout.map((item) => (
905
+ <div
906
+ key={occurrenceKey(item.event)}
907
+ className="absolute px-0.5"
908
+ style={{
909
+ top: `${item.top}%`,
910
+ height: `${item.height}%`,
911
+ left: `${(item.col / item.cols) * 100}%`,
912
+ width: `${100 / item.cols}%`,
913
+ }}
914
+ >
915
+ {renderEventTrigger(
916
+ item.event,
917
+ "flex h-full w-full flex-col overflow-hidden rounded-md px-1.5 py-1 text-left text-[11px] font-semibold leading-tight",
918
+ <>
919
+ <span className="truncate">{item.event.title}</span>
920
+ <span className="truncate text-[10px] font-medium opacity-80">
921
+ {formatTime(item.event.start, locale, hour12)}
922
+ </span>
923
+ </>,
924
+ { resize: "ns", variant: "time-grid" },
925
+ )}
926
+ </div>
927
+ ))}
928
+ </div>
929
+ );
930
+ })}
931
+ </div>
932
+ {showNow && nowTop > 0 && nowTop < hours.length * hourHeight ? (
933
+ <div
934
+ className="pointer-events-none absolute right-0 left-14 z-10 h-px bg-destructive"
935
+ style={{ top: nowTop }}
936
+ >
937
+ <span className="absolute -top-1 -left-1 size-2 rounded-full bg-destructive" />
938
+ </div>
939
+ ) : null}
940
+ </div>
941
+ </div>
942
+ );
943
+ }
944
+
945
+ function AgendaList({
946
+ date,
947
+ events,
948
+ showWeekends,
949
+ locale,
950
+ hour12,
951
+ onSlotClick,
952
+ renderEventTrigger,
953
+ }: {
954
+ date: Date;
955
+ events: NormalizedEvent[];
956
+ showWeekends: boolean;
957
+ locale?: string;
958
+ hour12: boolean;
959
+ onSlotClick?: EventCalendarProps["onSlotClick"];
960
+ renderEventTrigger: (
961
+ event: NormalizedEvent,
962
+ className: string,
963
+ children: ReactNode,
964
+ options?: { resize?: "ns"; variant: EventCalendarEventVariant },
965
+ ) => ReactNode;
966
+ }) {
967
+ const days = filterWeekendDays(agendaDays(date, AGENDA_LENGTH), showWeekends);
968
+
969
+ return (
970
+ <div className="divide-y divide-border/50">
971
+ {days.map((day) => {
972
+ const items = events.filter((event) => occupiesDay(event, day));
973
+ return (
974
+ <div key={day.toISOString()} className="grid grid-cols-[7.5rem_minmax(0,1fr)] gap-3 px-3 py-3 sm:px-4">
975
+ <button
976
+ type="button"
977
+ data-scheduler-day={isoDay(day)}
978
+ className="text-left"
979
+ onClick={() =>
980
+ onSlotClick?.({
981
+ start: new Date(day.getFullYear(), day.getMonth(), day.getDate(), 9),
982
+ end: new Date(day.getFullYear(), day.getMonth(), day.getDate(), 10),
983
+ allDay: false,
984
+ })
985
+ }
986
+ >
987
+ <p className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
988
+ {new Intl.DateTimeFormat(locale, { weekday: "short" }).format(day)}
989
+ </p>
990
+ <p
991
+ className={cn(
992
+ "text-lg font-black",
993
+ isSameDay(day, startOfToday()) && "text-primary",
994
+ )}
995
+ >
996
+ {new Intl.DateTimeFormat(locale, { month: "short", day: "numeric" }).format(day)}
997
+ </p>
998
+ </button>
999
+ <div className="min-w-0 space-y-1.5">
1000
+ {items.length === 0 ? (
1001
+ <p className="pt-2 text-xs text-muted-foreground">No events</p>
1002
+ ) : (
1003
+ items.map((event) => (
1004
+ <div key={occurrenceKey(event)} data-scheduler-day={isoDay(day)}>
1005
+ {renderEventTrigger(
1006
+ event,
1007
+ "flex w-full min-w-0 items-start gap-2 rounded-md px-2 py-1.5 text-left hover:bg-muted/40",
1008
+ <>
1009
+ <span className="mt-1.5 size-2 shrink-0 rounded-full" style={eventColorStyle(event.color, "dot")} />
1010
+ <span className="min-w-0">
1011
+ <span className="block truncate text-sm font-semibold text-foreground">{event.title}</span>
1012
+ <span className="text-[11px] text-muted-foreground">
1013
+ {event.allDay || isMonthBar(event)
1014
+ ? "All day"
1015
+ : `${formatTime(event.start, locale, hour12)} – ${formatTime(event.end, locale, hour12)}`}
1016
+ </span>
1017
+ </span>
1018
+ </>,
1019
+ { variant: "agenda" },
1020
+ )}
1021
+ </div>
1022
+ ))
1023
+ )}
1024
+ </div>
1025
+ </div>
1026
+ );
1027
+ })}
1028
+ </div>
1029
+ );
1030
+ }