@granularjs/ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/README.md +116 -0
  2. package/dist/fonts/Arimo-400.ttf +0 -0
  3. package/dist/fonts/Arimo-500.ttf +1449 -0
  4. package/dist/fonts/Arimo-600.ttf +1449 -0
  5. package/dist/fonts/Arimo-700.ttf +0 -0
  6. package/dist/fonts/Inter-400.woff2 +0 -0
  7. package/dist/fonts/Inter-500.woff2 +0 -0
  8. package/dist/fonts/Inter-600.woff2 +0 -0
  9. package/dist/fonts/Inter-700.woff2 +0 -0
  10. package/dist/fonts/Poppins-400.ttf +0 -0
  11. package/dist/fonts/Poppins-500.ttf +0 -0
  12. package/dist/fonts/Poppins-600.ttf +0 -0
  13. package/dist/fonts/Poppins-700.ttf +0 -0
  14. package/dist/granular-ui.min.js +3605 -0
  15. package/dist/granular-ui.min.js.map +7 -0
  16. package/package.json +55 -0
  17. package/src/components/Accordion.js +25 -0
  18. package/src/components/ActionIcon.js +20 -0
  19. package/src/components/Affix.js +11 -0
  20. package/src/components/Alert.js +33 -0
  21. package/src/components/Anchor.js +8 -0
  22. package/src/components/AppBar.js +14 -0
  23. package/src/components/Avatar.js +13 -0
  24. package/src/components/AvatarGroup.js +8 -0
  25. package/src/components/Badge.js +22 -0
  26. package/src/components/BadgeGroup.js +8 -0
  27. package/src/components/Blockquote.js +8 -0
  28. package/src/components/BottomBar.js +43 -0
  29. package/src/components/Breadcrumbs.js +19 -0
  30. package/src/components/Burger.js +13 -0
  31. package/src/components/Button.js +37 -0
  32. package/src/components/Calendar.js +109 -0
  33. package/src/components/Card.js +40 -0
  34. package/src/components/Center.js +8 -0
  35. package/src/components/Checkbox.js +46 -0
  36. package/src/components/CheckboxGroup.js +8 -0
  37. package/src/components/Chip.js +35 -0
  38. package/src/components/Code.js +8 -0
  39. package/src/components/Col.js +8 -0
  40. package/src/components/Collapse.js +8 -0
  41. package/src/components/Container.js +19 -0
  42. package/src/components/CopyButton.js +30 -0
  43. package/src/components/DateInput.js +123 -0
  44. package/src/components/DatePicker.js +7 -0
  45. package/src/components/Divider.js +22 -0
  46. package/src/components/Drawer.js +32 -0
  47. package/src/components/EventCalendar.js +972 -0
  48. package/src/components/Fieldset.js +12 -0
  49. package/src/components/Flex.js +25 -0
  50. package/src/components/Grid.js +8 -0
  51. package/src/components/GridTable.js +99 -0
  52. package/src/components/Group.js +29 -0
  53. package/src/components/HoverCard.js +24 -0
  54. package/src/components/Icon.js +19 -0
  55. package/src/components/Image.js +8 -0
  56. package/src/components/Indicator.js +21 -0
  57. package/src/components/Kbd.js +8 -0
  58. package/src/components/List.js +77 -0
  59. package/src/components/Loading.js +29 -0
  60. package/src/components/LoadingOverlay.js +9 -0
  61. package/src/components/Menu.js +129 -0
  62. package/src/components/Modal.js +61 -0
  63. package/src/components/MultiSelect.js +153 -0
  64. package/src/components/NavLink.js +72 -0
  65. package/src/components/Notification.js +42 -0
  66. package/src/components/Notifications.js +59 -0
  67. package/src/components/NumberField.js +389 -0
  68. package/src/components/NumberInput.js +5 -0
  69. package/src/components/Pagination.js +56 -0
  70. package/src/components/Paper.js +20 -0
  71. package/src/components/PasswordInput.js +29 -0
  72. package/src/components/PinInput.js +218 -0
  73. package/src/components/Popover.js +38 -0
  74. package/src/components/Popper.js +25 -0
  75. package/src/components/Progress.js +27 -0
  76. package/src/components/ProgressRing.js +11 -0
  77. package/src/components/Radio.js +22 -0
  78. package/src/components/RadioGroup.js +8 -0
  79. package/src/components/RangePicker.js +45 -0
  80. package/src/components/RangeSlider.js +143 -0
  81. package/src/components/Rating.js +42 -0
  82. package/src/components/ScrollArea.js +11 -0
  83. package/src/components/SearchInput.js +17 -0
  84. package/src/components/SegmentedControl.js +39 -0
  85. package/src/components/Select.js +71 -0
  86. package/src/components/SelectSearch.js +37 -0
  87. package/src/components/Sidebar.js +136 -0
  88. package/src/components/SimpleGrid.js +11 -0
  89. package/src/components/Skeleton.js +24 -0
  90. package/src/components/Slider.js +126 -0
  91. package/src/components/Space.js +8 -0
  92. package/src/components/Stack.js +27 -0
  93. package/src/components/Stepper.js +20 -0
  94. package/src/components/Switch.js +16 -0
  95. package/src/components/SwitchGroup.js +8 -0
  96. package/src/components/Table.js +42 -0
  97. package/src/components/Tabs.js +194 -0
  98. package/src/components/Tag.js +8 -0
  99. package/src/components/Text.js +42 -0
  100. package/src/components/TextInput.js +74 -0
  101. package/src/components/Textarea.js +15 -0
  102. package/src/components/Timeline.js +22 -0
  103. package/src/components/Title.js +18 -0
  104. package/src/components/Toast.js +16 -0
  105. package/src/components/ToastStack.js +21 -0
  106. package/src/components/Tooltip.js +12 -0
  107. package/src/hooks/useDisclosure.js +13 -0
  108. package/src/index.js +98 -0
  109. package/src/theme/fonts/Arimo-400.ttf +0 -0
  110. package/src/theme/fonts/Arimo-500.ttf +1449 -0
  111. package/src/theme/fonts/Arimo-600.ttf +1449 -0
  112. package/src/theme/fonts/Arimo-700.ttf +0 -0
  113. package/src/theme/fonts/Inter-400.woff2 +0 -0
  114. package/src/theme/fonts/Inter-500.woff2 +0 -0
  115. package/src/theme/fonts/Inter-600.woff2 +0 -0
  116. package/src/theme/fonts/Inter-700.woff2 +0 -0
  117. package/src/theme/fonts/Poppins-400.ttf +0 -0
  118. package/src/theme/fonts/Poppins-500.ttf +0 -0
  119. package/src/theme/fonts/Poppins-600.ttf +0 -0
  120. package/src/theme/fonts/Poppins-700.ttf +0 -0
  121. package/src/theme/icons.js +10 -0
  122. package/src/theme/styles.js +3630 -0
  123. package/src/theme/theme.js +71 -0
  124. package/src/utils.js +75 -0
  125. package/types/components/Accordion.d.ts +1 -0
  126. package/types/components/ActionIcon.d.ts +1 -0
  127. package/types/components/Affix.d.ts +1 -0
  128. package/types/components/Alert.d.ts +1 -0
  129. package/types/components/Anchor.d.ts +1 -0
  130. package/types/components/AppBar.d.ts +1 -0
  131. package/types/components/Avatar.d.ts +1 -0
  132. package/types/components/AvatarGroup.d.ts +1 -0
  133. package/types/components/Badge.d.ts +1 -0
  134. package/types/components/BadgeGroup.d.ts +1 -0
  135. package/types/components/Blockquote.d.ts +1 -0
  136. package/types/components/BottomBar.d.ts +4 -0
  137. package/types/components/Breadcrumbs.d.ts +1 -0
  138. package/types/components/Burger.d.ts +1 -0
  139. package/types/components/Button.d.ts +1 -0
  140. package/types/components/Calendar.d.ts +1 -0
  141. package/types/components/Card.d.ts +1 -0
  142. package/types/components/Center.d.ts +1 -0
  143. package/types/components/Checkbox.d.ts +1 -0
  144. package/types/components/CheckboxGroup.d.ts +1 -0
  145. package/types/components/Chip.d.ts +1 -0
  146. package/types/components/Code.d.ts +1 -0
  147. package/types/components/Col.d.ts +1 -0
  148. package/types/components/Collapse.d.ts +1 -0
  149. package/types/components/Container.d.ts +1 -0
  150. package/types/components/CopyButton.d.ts +1 -0
  151. package/types/components/DateInput.d.ts +1 -0
  152. package/types/components/DatePicker.d.ts +1 -0
  153. package/types/components/Divider.d.ts +1 -0
  154. package/types/components/Drawer.d.ts +1 -0
  155. package/types/components/EventCalendar.d.ts +1 -0
  156. package/types/components/Fieldset.d.ts +1 -0
  157. package/types/components/Flex.d.ts +1 -0
  158. package/types/components/Grid.d.ts +1 -0
  159. package/types/components/GridTable.d.ts +5 -0
  160. package/types/components/Group.d.ts +1 -0
  161. package/types/components/HoverCard.d.ts +1 -0
  162. package/types/components/Icon.d.ts +1 -0
  163. package/types/components/Image.d.ts +1 -0
  164. package/types/components/Indicator.d.ts +1 -0
  165. package/types/components/Kbd.d.ts +1 -0
  166. package/types/components/List.d.ts +5 -0
  167. package/types/components/Loading.d.ts +1 -0
  168. package/types/components/LoadingOverlay.d.ts +1 -0
  169. package/types/components/Menu.d.ts +2 -0
  170. package/types/components/Modal.d.ts +1 -0
  171. package/types/components/MultiSelect.d.ts +1 -0
  172. package/types/components/NavLink.d.ts +1 -0
  173. package/types/components/Notification.d.ts +1 -0
  174. package/types/components/Notifications.d.ts +1 -0
  175. package/types/components/NumberField.d.ts +1 -0
  176. package/types/components/NumberInput.d.ts +1 -0
  177. package/types/components/Pagination.d.ts +1 -0
  178. package/types/components/Paper.d.ts +1 -0
  179. package/types/components/PasswordInput.d.ts +1 -0
  180. package/types/components/PinInput.d.ts +1 -0
  181. package/types/components/Popover.d.ts +1 -0
  182. package/types/components/Popper.d.ts +1 -0
  183. package/types/components/Progress.d.ts +1 -0
  184. package/types/components/ProgressRing.d.ts +1 -0
  185. package/types/components/Radio.d.ts +1 -0
  186. package/types/components/RadioGroup.d.ts +1 -0
  187. package/types/components/RangePicker.d.ts +1 -0
  188. package/types/components/RangeSlider.d.ts +1 -0
  189. package/types/components/Rating.d.ts +1 -0
  190. package/types/components/ScrollArea.d.ts +1 -0
  191. package/types/components/SearchInput.d.ts +1 -0
  192. package/types/components/SegmentedControl.d.ts +1 -0
  193. package/types/components/Select.d.ts +1 -0
  194. package/types/components/SelectSearch.d.ts +1 -0
  195. package/types/components/Sidebar.d.ts +1 -0
  196. package/types/components/SimpleGrid.d.ts +1 -0
  197. package/types/components/Skeleton.d.ts +1 -0
  198. package/types/components/Slider.d.ts +5 -0
  199. package/types/components/Space.d.ts +1 -0
  200. package/types/components/Stack.d.ts +1 -0
  201. package/types/components/Stepper.d.ts +1 -0
  202. package/types/components/Switch.d.ts +1 -0
  203. package/types/components/SwitchGroup.d.ts +1 -0
  204. package/types/components/Table.d.ts +1 -0
  205. package/types/components/Tabs.d.ts +1 -0
  206. package/types/components/Tag.d.ts +1 -0
  207. package/types/components/Text.d.ts +1 -0
  208. package/types/components/TextInput.d.ts +1 -0
  209. package/types/components/Textarea.d.ts +1 -0
  210. package/types/components/Timeline.d.ts +1 -0
  211. package/types/components/Title.d.ts +1 -0
  212. package/types/components/Toast.d.ts +1 -0
  213. package/types/components/ToastStack.d.ts +1 -0
  214. package/types/components/Tooltip.d.ts +1 -0
  215. package/types/hooks/useDisclosure.d.ts +1 -0
  216. package/types/index.d.ts +93 -0
  217. package/types/theme/icons.d.ts +10 -0
  218. package/types/theme/styles.d.ts +1 -0
  219. package/types/theme/theme.d.ts +2 -0
  220. package/types/utils.d.ts +12 -0
@@ -0,0 +1,972 @@
1
+ import {
2
+ Div,
3
+ Button,
4
+ state,
5
+ after,
6
+ when,
7
+ portal,
8
+ Input,
9
+ Label,
10
+ Span,
11
+ } from '@granularjs/core';
12
+ import { cx, splitPropsChildren, resolveValue, classVar } from '../utils.js';
13
+ import { Modal } from './Modal.js';
14
+ import { ActionIcon } from './ActionIcon.js';
15
+ import { Icon } from './Icon.js';
16
+ import { TextInput } from './TextInput.js';
17
+ import { Textarea } from './Textarea.js';
18
+ import { Checkbox } from './Checkbox.js';
19
+ import {
20
+ forwardSvg,
21
+ backwardSvg,
22
+ closeSvg,
23
+ plusSvg,
24
+ editSvg,
25
+ deleteSvg,
26
+ calendarTodaySvg,
27
+ } from '../theme/icons.js';
28
+
29
+ const WEEKDAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
30
+ const VIEW_OPTIONS = [
31
+ { value: 'day', label: 'Day' },
32
+ { value: 'week', label: 'Week' },
33
+ { value: 'month', label: 'Month' },
34
+ ];
35
+
36
+ function toDate(v) {
37
+ if (v instanceof Date) return v;
38
+ if (v == null) return null;
39
+ const d = new Date(v);
40
+ return Number.isNaN(d.getTime()) ? null : d;
41
+ }
42
+
43
+ function normalizeEvent(ev) {
44
+ const start = toDate(ev.start);
45
+ const end = toDate(ev.end);
46
+ if (!start || !end) return null;
47
+ return {
48
+ ...ev,
49
+ id: ev.id ?? `${start.getTime()}-${ev.title ?? 'event'}`,
50
+ title: ev.title ?? '',
51
+ start,
52
+ end,
53
+ allDay: !!ev.allDay,
54
+ description: ev.description ?? '',
55
+ location: ev.location ?? '',
56
+ attendees: Array.isArray(ev.attendees) ? ev.attendees : [],
57
+ color: ev.color ?? null,
58
+ };
59
+ }
60
+
61
+ function getEventsInRange(events, rangeStart, rangeEnd) {
62
+ const start = toDate(rangeStart).getTime();
63
+ const end = toDate(rangeEnd).getTime();
64
+ return events
65
+ .map(normalizeEvent)
66
+ .filter(Boolean)
67
+ .filter((ev) => ev.end.getTime() > start && ev.start.getTime() < end);
68
+ }
69
+
70
+ function getEventsForDay(events, day) {
71
+ const d = new Date(day);
72
+ d.setHours(0, 0, 0, 0);
73
+ const start = d.getTime();
74
+ d.setHours(23, 59, 59, 999);
75
+ const end = d.getTime();
76
+ return getEventsInRange(events, start, end);
77
+ }
78
+
79
+ function weekStart(date, firstDayOfWeek) {
80
+ const d = new Date(date);
81
+ const day = d.getDay();
82
+ const diff = (day - firstDayOfWeek + 7) % 7;
83
+ d.setDate(d.getDate() - diff);
84
+ d.setHours(0, 0, 0, 0);
85
+ return d;
86
+ }
87
+
88
+ function formatTime(date, locale = 'default') {
89
+ return date.toLocaleTimeString(locale, { hour: 'numeric', minute: '2-digit', hour12: true });
90
+ }
91
+
92
+ function formatDate(date, locale = 'default') {
93
+ return date.toLocaleDateString(locale, { weekday: 'short', month: 'short', day: 'numeric' });
94
+ }
95
+
96
+ function formatMonthYear(date, locale = 'default') {
97
+ return date.toLocaleDateString(locale, { month: 'long', year: 'numeric' });
98
+ }
99
+
100
+ function formatWeekRange(weekStartDate, locale = 'default') {
101
+ const end = new Date(weekStartDate);
102
+ end.setDate(end.getDate() + 6);
103
+ return `${weekStartDate.toLocaleDateString(locale, { month: 'short', day: 'numeric' })} – ${end.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' })}`;
104
+ }
105
+
106
+ function timeToPct(date, dayStart, dayEnd) {
107
+ const d = new Date(date);
108
+ const start = new Date(d);
109
+ start.setHours(dayStart, 0, 0, 0);
110
+ const end = new Date(d);
111
+ end.setHours(dayEnd, 0, 0, 0);
112
+ const total = (dayEnd - dayStart) * 60;
113
+ const mins = (d.getTime() - start.getTime()) / 60000;
114
+ return Math.max(0, Math.min(1, mins / total)) * 100;
115
+ }
116
+
117
+ function durationPct(start, end, dayStart, dayEnd) {
118
+ const dayStartMs = new Date(start).setHours(dayStart, 0, 0, 0);
119
+ const dayEndMs = new Date(start).setHours(dayEnd, 0, 0, 0);
120
+ const total = (dayEnd - dayStart) * 60;
121
+ const s = Math.max(start.getTime(), dayStartMs);
122
+ const e = Math.min(end.getTime(), dayEndMs);
123
+ const mins = (e - s) / 60000;
124
+ const top = ((s - dayStartMs) / 60000 / total) * 100;
125
+ const height = (mins / total) * 100;
126
+ return { top, height };
127
+ }
128
+
129
+ export function EventCalendar(...args) {
130
+ const { props, rawProps } = splitPropsChildren(args, {
131
+ defaultView: 'month',
132
+ firstDayOfWeek: 0,
133
+ locale: 'default',
134
+ hourSlotDuration: 30,
135
+ minTime: 6,
136
+ maxTime: 22,
137
+ eventCreationColorOptions: [],
138
+ });
139
+ const {
140
+ events: eventsProp = [],
141
+ defaultView,
142
+ firstDayOfWeek,
143
+ locale,
144
+ hourSlotDuration,
145
+ minTime,
146
+ maxTime,
147
+ eventCreationColorOptions,
148
+ className,
149
+ ...rest
150
+ } = props;
151
+ const {
152
+ onCreateEventRequest,
153
+ onViewEventRequest,
154
+ onUpdateEventRequest,
155
+ onRemoveEventRequest,
156
+ eventCreationEmailLookup,
157
+ eventCreationLocationsCallback,
158
+ eventCreationCalendarsCallback,
159
+ onSlotClick,
160
+ onDateRangeChange,
161
+ } = rawProps;
162
+
163
+ const viewMode = state(resolveValue(defaultView) ?? 'month');
164
+ const currentDate = state(new Date());
165
+ const modalState = state(null);
166
+ const selectedEvent = state(null);
167
+ const createDraft = state({
168
+ title: '',
169
+ start: null,
170
+ end: null,
171
+ allDay: false,
172
+ description: '',
173
+ location: '',
174
+ attendees: [],
175
+ color: null,
176
+ });
177
+
178
+ const eventsList = after(eventsProp).compute((v) => (Array.isArray(v) ? v : []));
179
+ const viewDate = currentDate.get();
180
+ const weekStartDate = weekStart(viewDate, resolveValue(firstDayOfWeek) ?? 0);
181
+ const dayStart = resolveValue(minTime) ?? 6;
182
+ const dayEnd = resolveValue(maxTime) ?? 22;
183
+ const slotDuration = resolveValue(hourSlotDuration) ?? 30;
184
+
185
+ const goPrev = () => {
186
+ const d = new Date(currentDate.get());
187
+ if (viewMode.get() === 'month') d.setMonth(d.getMonth() - 1);
188
+ else if (viewMode.get() === 'week') d.setDate(d.getDate() - 7);
189
+ else d.setDate(d.getDate() - 1);
190
+ currentDate.set(d);
191
+ onDateRangeChange?.({ start: d, end: d, view: viewMode.get() });
192
+ };
193
+
194
+ const goNext = () => {
195
+ const d = new Date(currentDate.get());
196
+ if (viewMode.get() === 'month') d.setMonth(d.getMonth() + 1);
197
+ else if (viewMode.get() === 'week') d.setDate(d.getDate() + 7);
198
+ else d.setDate(d.getDate() + 1);
199
+ currentDate.set(d);
200
+ onDateRangeChange?.({ start: d, end: d, view: viewMode.get() });
201
+ };
202
+
203
+ const goToday = () => {
204
+ const d = new Date();
205
+ currentDate.set(d);
206
+ onDateRangeChange?.({ start: d, end: d, view: viewMode.get() });
207
+ };
208
+
209
+ const openCreateModal = (start, end, allDay = false) => {
210
+ const s = start ? new Date(start) : new Date(currentDate.get());
211
+ const e = end ? new Date(end) : new Date(s.getTime() + 60 * 60 * 1000);
212
+ if (allDay) {
213
+ s.setHours(0, 0, 0, 0);
214
+ e.setHours(23, 59, 59, 999);
215
+ }
216
+ createDraft.set({
217
+ title: '',
218
+ start: s,
219
+ end: e,
220
+ allDay,
221
+ description: '',
222
+ location: '',
223
+ attendees: [],
224
+ color: null,
225
+ });
226
+ modalState.set('create');
227
+ };
228
+
229
+ const openViewModal = (event) => {
230
+ selectedEvent.set(normalizeEvent(event));
231
+ modalState.set('view');
232
+ onViewEventRequest?.(event);
233
+ };
234
+
235
+ const openEditModal = () => {
236
+ const ev = selectedEvent.get();
237
+ if (!ev) return;
238
+ createDraft.set({
239
+ title: ev.title,
240
+ start: new Date(ev.start),
241
+ end: new Date(ev.end),
242
+ allDay: ev.allDay,
243
+ description: ev.description ?? '',
244
+ location: ev.location ?? '',
245
+ attendees: [...(ev.attendees || [])],
246
+ color: ev.color ?? null,
247
+ });
248
+ modalState.set('edit');
249
+ };
250
+
251
+ const closeModal = () => {
252
+ modalState.set(null);
253
+ selectedEvent.set(null);
254
+ };
255
+
256
+ const handleSlotClick = (date, hour, allDay) => {
257
+ if (onSlotClick) {
258
+ onSlotClick({ date, hour, allDay });
259
+ return;
260
+ }
261
+ const start = new Date(date);
262
+ if (allDay) {
263
+ start.setHours(0, 0, 0, 0);
264
+ const end = new Date(start);
265
+ end.setHours(23, 59, 59, 999);
266
+ openCreateModal(start, end, true);
267
+ } else {
268
+ start.setHours(hour, 0, 0, 0);
269
+ const end = new Date(start.getTime() + slotDuration * 60 * 1000);
270
+ openCreateModal(start, end, false);
271
+ }
272
+ };
273
+
274
+ const handleCreateSubmit = () => {
275
+ const draft = createDraft.get();
276
+ if (!draft.start || !draft.end) return;
277
+ onCreateEventRequest?.(
278
+ {
279
+ title: draft.title,
280
+ start: draft.start,
281
+ end: draft.end,
282
+ allDay: draft.allDay,
283
+ description: draft.description,
284
+ location: draft.location,
285
+ attendees: draft.attendees,
286
+ color: draft.color,
287
+ },
288
+ () => {
289
+ closeModal();
290
+ }
291
+ );
292
+ };
293
+
294
+ const handleUpdateSubmit = () => {
295
+ const ev = selectedEvent.get();
296
+ const draft = createDraft.get();
297
+ if (!ev || !draft.start || !draft.end) return;
298
+ onUpdateEventRequest?.(
299
+ ev,
300
+ {
301
+ title: draft.title,
302
+ start: draft.start,
303
+ end: draft.end,
304
+ allDay: draft.allDay,
305
+ description: draft.description,
306
+ location: draft.location,
307
+ attendees: draft.attendees,
308
+ color: draft.color,
309
+ },
310
+ () => closeModal()
311
+ );
312
+ };
313
+
314
+ const handleRemoveRequest = () => {
315
+ const ev = selectedEvent.get();
316
+ if (!ev) return;
317
+ onRemoveEventRequest?.(ev, () => closeModal());
318
+ };
319
+
320
+ const titleLabel = after(viewMode, currentDate).compute(([view, date]) => {
321
+ if (view === 'month') return formatMonthYear(date, resolveValue(locale));
322
+ if (view === 'week') return formatWeekRange(weekStart(date, resolveValue(firstDayOfWeek) ?? 0), resolveValue(locale));
323
+ return formatDate(date, resolveValue(locale));
324
+ });
325
+
326
+ const header = Div(
327
+ { className: 'g-ui-event-calendar-header' },
328
+ Div(
329
+ { className: 'g-ui-event-calendar-nav-group' },
330
+ ActionIcon(
331
+ { size: 'sm', variant: 'subtle', onClick: goPrev, className: 'g-ui-event-calendar-nav' },
332
+ Icon({ size: 'sm', innerHTML: backwardSvg })
333
+ ),
334
+ ActionIcon(
335
+ { size: 'sm', variant: 'subtle', onClick: goNext, className: 'g-ui-event-calendar-nav' },
336
+ Icon({ size: 'sm', innerHTML: forwardSvg })
337
+ ),
338
+ Button(
339
+ { type: 'button', variant: 'subtle', size: 'sm', className: 'g-ui-event-calendar-today', onClick: goToday },
340
+ Icon({ size: 'sm', className: 'g-ui-event-calendar-today-icon', innerHTML: calendarTodaySvg }),
341
+ 'Today'
342
+ )
343
+ ),
344
+ Div({ className: 'g-ui-event-calendar-title' }, titleLabel),
345
+ Div(
346
+ { className: 'g-ui-event-calendar-actions' },
347
+ VIEW_OPTIONS.map((opt) =>
348
+ Div(
349
+ {
350
+ className: cx(
351
+ 'g-ui-event-calendar-view-option',
352
+ after(viewMode).compute((v) => (v === opt.value ? 'g-ui-event-calendar-view-option-active' : ''))
353
+ ),
354
+ onClick: () => {
355
+ viewMode.set(opt.value);
356
+ onDateRangeChange?.({ start: currentDate.get(), end: currentDate.get(), view: opt.value });
357
+ },
358
+ },
359
+ opt.label
360
+ )
361
+ ),
362
+ Button(
363
+ {
364
+ type: 'button',
365
+ variant: 'filled',
366
+ size: 'sm',
367
+ className: 'g-ui-event-calendar-create-btn',
368
+ onClick: () => openCreateModal(currentDate.get(), null, false),
369
+ },
370
+ Icon({ size: 'sm', innerHTML: plusSvg }),
371
+ 'Create'
372
+ )
373
+ )
374
+ );
375
+
376
+ const monthGrid = after(currentDate, eventsList, firstDayOfWeek).compute(([date, events, firstDay]) => {
377
+ const year = date.getFullYear();
378
+ const month = date.getMonth();
379
+ const first = new Date(year, month, 1);
380
+ const startDow = (first.getDay() - (firstDay ?? 0) + 7) % 7;
381
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
382
+ const prevMonthDays = new Date(year, month, 0).getDate();
383
+ const rows = [];
384
+ let dayCount = 1;
385
+ let nextMonthDay = 1;
386
+ const totalCells = Math.ceil((startDow + daysInMonth) / 7) * 7;
387
+ const dayCells = [];
388
+ for (let i = 0; i < totalCells; i += 1) {
389
+ let day;
390
+ let isCurrentMonth;
391
+ let dateObj;
392
+ if (i < startDow) {
393
+ day = prevMonthDays - startDow + i + 1;
394
+ dateObj = new Date(year, month - 1, day);
395
+ isCurrentMonth = false;
396
+ } else if (dayCount <= daysInMonth) {
397
+ day = dayCount;
398
+ dateObj = new Date(year, month, day);
399
+ isCurrentMonth = true;
400
+ dayCount += 1;
401
+ } else {
402
+ day = nextMonthDay;
403
+ dateObj = new Date(year, month + 1, day);
404
+ isCurrentMonth = false;
405
+ nextMonthDay += 1;
406
+ }
407
+ const dayEvents = getEventsForDay(events, dateObj);
408
+ const isToday =
409
+ dateObj.getDate() === new Date().getDate() &&
410
+ dateObj.getMonth() === new Date().getMonth() &&
411
+ dateObj.getFullYear() === new Date().getFullYear();
412
+ dayCells.push(
413
+ Div(
414
+ {
415
+ key: dateObj.getTime(),
416
+ className: cx(
417
+ 'g-ui-event-calendar-month-cell',
418
+ !isCurrentMonth && 'g-ui-event-calendar-month-cell-outside',
419
+ isToday && 'g-ui-event-calendar-month-cell-today'
420
+ ),
421
+ },
422
+ Div(
423
+ {
424
+ className: 'g-ui-event-calendar-month-cell-header',
425
+ onClick: () => handleSlotClick(dateObj, 0, true),
426
+ },
427
+ Span({ className: 'g-ui-event-calendar-month-cell-day' }, dateObj.getDate())
428
+ ),
429
+ Div(
430
+ { className: 'g-ui-event-calendar-month-cell-events' },
431
+ dayEvents.slice(0, 3).map((ev) =>
432
+ Div(
433
+ {
434
+ key: ev.id,
435
+ className: 'g-ui-event-calendar-month-event',
436
+ style: ev.color ? { borderLeftColor: ev.color, backgroundColor: `${ev.color}20` } : {},
437
+ onClick: (e) => {
438
+ e.stopPropagation();
439
+ openViewModal(ev);
440
+ },
441
+ },
442
+ ev.title || '(No title)'
443
+ )
444
+ ),
445
+ when(dayEvents.length > 3, () =>
446
+ Div(
447
+ {
448
+ className: 'g-ui-event-calendar-month-more',
449
+ onClick: (e) => {
450
+ e.stopPropagation();
451
+ openViewModal(dayEvents[3]);
452
+ },
453
+ },
454
+ `+${dayEvents.length - 3} more`
455
+ )
456
+ )
457
+ )
458
+ )
459
+ );
460
+ }
461
+ return dayCells;
462
+ });
463
+
464
+ const monthView = Div(
465
+ { className: 'g-ui-event-calendar-month' },
466
+ Div(
467
+ { className: 'g-ui-event-calendar-weekdays' },
468
+ WEEKDAY_LABELS.map((label) => Div({ key: label, className: 'g-ui-event-calendar-weekday' }, label))
469
+ ),
470
+ Div({ className: 'g-ui-event-calendar-month-grid' }, monthGrid)
471
+ );
472
+
473
+ const timeSlots = [];
474
+ for (let h = dayStart; h < dayEnd; h += 1) {
475
+ for (let s = 0; s < 60; s += slotDuration) {
476
+ timeSlots.push(h + s / 60);
477
+ }
478
+ }
479
+
480
+ const weekDays = after(currentDate, firstDayOfWeek).compute(([date, firstDay]) => {
481
+ const start = weekStart(date, firstDay ?? 0);
482
+ const days = [];
483
+ for (let i = 0; i < 7; i += 1) {
484
+ const d = new Date(start);
485
+ d.setDate(d.getDate() + i);
486
+ days.push(d);
487
+ }
488
+ return days;
489
+ });
490
+
491
+ const weekView = Div(
492
+ { className: 'g-ui-event-calendar-week' },
493
+ after(weekDays).compute((days) =>
494
+ Div(
495
+ { className: 'g-ui-event-calendar-week-days' },
496
+ Div({ className: 'g-ui-event-calendar-week-time-col' }, ''),
497
+ (days || []).map((d) =>
498
+ Div(
499
+ { key: d.getTime(), className: 'g-ui-event-calendar-week-day-col' },
500
+ Div({ className: 'g-ui-event-calendar-week-day-header' }, formatDate(d, resolveValue(locale)))
501
+ )
502
+ )
503
+ )
504
+ ),
505
+ Div({ className: 'g-ui-event-calendar-week-body' },
506
+ Div(
507
+ { className: 'g-ui-event-calendar-week-time-col' },
508
+ timeSlots.map((h) => {
509
+ const hour = Math.floor(h);
510
+ const min = (h - hour) * 60;
511
+ const label = `${hour <= 12 ? hour : hour - 12}:${min === 0 ? '00' : min} ${hour < 12 ? 'AM' : 'PM'}`;
512
+ return Div({ key: h, className: 'g-ui-event-calendar-week-slot-label' }, label);
513
+ })
514
+ ),
515
+ after(weekDays, eventsList).compute(([days, events]) => {
516
+ if (!days || !days.length) return null;
517
+ const rangeStart = days[0];
518
+ const rangeEnd = new Date(days[6]);
519
+ rangeEnd.setHours(23, 59, 59, 999);
520
+ const inRange = getEventsInRange(events, rangeStart, rangeEnd);
521
+ return Div(
522
+ { className: 'g-ui-event-calendar-week-grid-cols' },
523
+ days.map((day) =>
524
+ Div(
525
+ { key: day.getTime(), className: 'g-ui-event-calendar-week-day-col g-ui-event-calendar-week-day-col-body' },
526
+ timeSlots.map((h) =>
527
+ Div({
528
+ key: h,
529
+ className: 'g-ui-event-calendar-week-slot',
530
+ onClick: () => handleSlotClick(day, h, false),
531
+ })
532
+ ),
533
+ inRange
534
+ .filter(
535
+ (ev) =>
536
+ !ev.allDay &&
537
+ ev.start.getDate() === day.getDate() &&
538
+ ev.start.getMonth() === day.getMonth() &&
539
+ ev.start.getFullYear() === day.getFullYear()
540
+ )
541
+ .map((ev) => {
542
+ const { top, height } = durationPct(ev.start, ev.end, dayStart, dayEnd);
543
+ return Div(
544
+ {
545
+ key: ev.id,
546
+ className: 'g-ui-event-calendar-week-event',
547
+ style: {
548
+ top: `${top}%`,
549
+ height: `${height}%`,
550
+ borderLeftColor: ev.color || 'var(--g-ui-primary)',
551
+ },
552
+ onClick: (e) => {
553
+ e.stopPropagation();
554
+ openViewModal(ev);
555
+ },
556
+ },
557
+ Div({ className: 'g-ui-event-calendar-week-event-title' }, ev.title || '(No title)'),
558
+ when(!ev.allDay, () =>
559
+ Span({ className: 'g-ui-event-calendar-week-event-time' }, `${formatTime(ev.start, resolveValue(locale))} – ${formatTime(ev.end, resolveValue(locale))}`)
560
+ )
561
+ );
562
+ })
563
+ )
564
+ )
565
+ );
566
+ })
567
+ )
568
+ );
569
+
570
+ const dayView = Div(
571
+ { className: 'g-ui-event-calendar-day' },
572
+ Div(
573
+ { className: 'g-ui-event-calendar-day-body' },
574
+ Div(
575
+ { className: 'g-ui-event-calendar-day-time-col' },
576
+ timeSlots.map((h) => {
577
+ const hour = Math.floor(h);
578
+ const min = (h - hour) * 60;
579
+ const label = `${hour <= 12 ? hour : hour - 12}:${min === 0 ? '00' : min} ${hour < 12 ? 'AM' : 'PM'}`;
580
+ return Div({ key: h, className: 'g-ui-event-calendar-day-slot-label' }, label);
581
+ })
582
+ ),
583
+ Div(
584
+ { className: 'g-ui-event-calendar-day-slots' },
585
+ timeSlots.map((h) =>
586
+ Div({
587
+ key: h,
588
+ className: 'g-ui-event-calendar-day-slot',
589
+ onClick: () => handleSlotClick(currentDate.get(), h, false),
590
+ })
591
+ ),
592
+ after(eventsList, currentDate).compute(([events, day]) => {
593
+ if (!day) return [];
594
+ const dayEvs = getEventsForDay(events, day).filter((e) => !e.allDay);
595
+ return dayEvs.map((ev) => {
596
+ const { top, height } = durationPct(ev.start, ev.end, dayStart, dayEnd);
597
+ return Div(
598
+ {
599
+ key: ev.id,
600
+ className: 'g-ui-event-calendar-day-event',
601
+ style: {
602
+ top: `${top}%`,
603
+ height: `${height}%`,
604
+ borderLeftColor: ev.color || 'var(--g-ui-primary)',
605
+ },
606
+ onClick: (e) => {
607
+ e.stopPropagation();
608
+ openViewModal(ev);
609
+ },
610
+ },
611
+ Div({ className: 'g-ui-event-calendar-day-event-title' }, ev.title || '(No title)'),
612
+ Span({ className: 'g-ui-event-calendar-day-event-time' }, `${formatTime(ev.start, resolveValue(locale))} – ${formatTime(ev.end, resolveValue(locale))}`)
613
+ );
614
+ });
615
+ })
616
+ )
617
+ )
618
+ );
619
+
620
+ const createEditForm = (isEdit) => {
621
+ const draft = createDraft.get();
622
+ const today = new Date();
623
+ const defaultStart = draft.start || today;
624
+ const defaultEnd = draft.end || new Date(today.getTime() + 60 * 60 * 1000);
625
+ const titleState = state(draft.title);
626
+ const startDateState = state(defaultStart.toISOString().slice(0, 10));
627
+ const startTimeState = state(
628
+ !draft.allDay && defaultStart ? defaultStart.toTimeString().slice(0, 5) : '09:00'
629
+ );
630
+ const endDateState = state(defaultEnd.toISOString().slice(0, 10));
631
+ const endTimeState = state(
632
+ !draft.allDay && defaultEnd ? defaultEnd.toTimeString().slice(0, 5) : '10:00'
633
+ );
634
+ const allDayState = state(draft.allDay);
635
+ const descState = state(draft.description);
636
+ const locationState = state(draft.location);
637
+ const attendeesState = state(draft.attendees || []);
638
+ const emailLookupQuery = state('');
639
+ const emailLookupResults = state([]);
640
+ const locationOptions = state([]);
641
+ const locationQuery = state('');
642
+ const colorOptions = Array.isArray(eventCreationColorOptions) ? eventCreationColorOptions : [];
643
+ const colorState = state(draft.color);
644
+
645
+ const applyDraft = () => {
646
+ const start = new Date(startDateState.get() + 'T' + (allDayState.get() ? '00:00:00' : startTimeState.get() + ':00'));
647
+ const end = new Date(endDateState.get() + 'T' + (allDayState.get() ? '23:59:59' : endTimeState.get() + ':00'));
648
+ createDraft.set({
649
+ title: titleState.get(),
650
+ start,
651
+ end,
652
+ allDay: allDayState.get(),
653
+ description: descState.get(),
654
+ location: locationState.get(),
655
+ attendees: attendeesState.get(),
656
+ color: colorState.get(),
657
+ });
658
+ };
659
+
660
+ const doEmailLookup = (query) => {
661
+ emailLookupQuery.set(query);
662
+ if (!eventCreationEmailLookup || !query.trim()) {
663
+ emailLookupResults.set([]);
664
+ return;
665
+ }
666
+ Promise.resolve(eventCreationEmailLookup(query.trim()))
667
+ .then((list) => emailLookupResults.set(Array.isArray(list) ? list : []))
668
+ .catch(() => emailLookupResults.set([]));
669
+ };
670
+
671
+ const addAttendee = (item) => {
672
+ const current = attendeesState.get();
673
+ if (current.some((a) => (a.email || a.id) === (item.email || item.id))) return;
674
+ attendeesState.set([...current, { id: item.id, email: item.email ?? item.label, label: item.label }]);
675
+ emailLookupQuery.set('');
676
+ emailLookupResults.set([]);
677
+ };
678
+
679
+ const removeAttendee = (index) => {
680
+ const next = [...attendeesState.get()];
681
+ next.splice(index, 1);
682
+ attendeesState.set(next);
683
+ };
684
+
685
+ const loadLocations = () => {
686
+ if (!eventCreationLocationsCallback) return;
687
+ Promise.resolve(eventCreationLocationsCallback(locationQuery.get()))
688
+ .then((list) => locationOptions.set(Array.isArray(list) ? list : []))
689
+ .catch(() => locationOptions.set([]));
690
+ };
691
+
692
+ return Div(
693
+ { className: 'g-ui-event-calendar-form' },
694
+ Div(
695
+ { className: 'g-ui-event-calendar-form-row' },
696
+ TextInput({
697
+ label: 'Title',
698
+ placeholder: 'Event title',
699
+ value: titleState,
700
+ onInput: (ev) => titleState.set(ev?.target?.value ?? ''),
701
+ })
702
+ ),
703
+ Div(
704
+ { className: 'g-ui-event-calendar-form-row g-ui-event-calendar-form-row-inline' },
705
+ Checkbox({
706
+ label: 'All day',
707
+ checked: allDayState,
708
+ onChange: (v) => allDayState.set(v),
709
+ })
710
+ ),
711
+ Div(
712
+ { className: 'g-ui-event-calendar-form-row g-ui-event-calendar-form-row-inline' },
713
+ Div({ className: 'g-ui-event-calendar-form-field' },
714
+ Label({ className: 'g-ui-event-calendar-form-label' }, 'Start'),
715
+ Input({
716
+ type: 'date',
717
+ className: 'g-ui-input g-ui-input-size-md',
718
+ value: startDateState,
719
+ onInput: (ev) => startDateState.set(ev?.target?.value ?? ''),
720
+ }),
721
+ when(!allDayState, () =>
722
+ Input({
723
+ type: 'time',
724
+ className: 'g-ui-input g-ui-input-size-md',
725
+ value: startTimeState,
726
+ onInput: (ev) => startTimeState.set(ev?.target?.value ?? ''),
727
+ })
728
+ )
729
+ ),
730
+ Div({ className: 'g-ui-event-calendar-form-field' },
731
+ Label({ className: 'g-ui-event-calendar-form-label' }, 'End'),
732
+ Input({
733
+ type: 'date',
734
+ className: 'g-ui-input g-ui-input-size-md',
735
+ value: endDateState,
736
+ onInput: (ev) => endDateState.set(ev?.target?.value ?? ''),
737
+ }),
738
+ when(!allDayState, () =>
739
+ Input({
740
+ type: 'time',
741
+ className: 'g-ui-input g-ui-input-size-md',
742
+ value: endTimeState,
743
+ onInput: (ev) => endTimeState.set(ev?.target?.value ?? ''),
744
+ })
745
+ )
746
+ )
747
+ ),
748
+ when(eventCreationLocationsCallback, () =>
749
+ Div(
750
+ { className: 'g-ui-event-calendar-form-row' },
751
+ Label({ className: 'g-ui-event-calendar-form-label' }, 'Location'),
752
+ TextInput({
753
+ placeholder: 'Search or type location',
754
+ value: locationQuery,
755
+ onInput: (ev) => {
756
+ locationQuery.set(ev?.target?.value ?? '');
757
+ loadLocations();
758
+ },
759
+ onFocus: loadLocations,
760
+ }),
761
+ when(after(locationOptions).compute((o) => o && o.length > 0), () =>
762
+ Div(
763
+ { className: 'g-ui-event-calendar-form-suggestions' },
764
+ locationOptions.get().map((loc) =>
765
+ Div(
766
+ {
767
+ key: loc.id ?? loc.label,
768
+ className: 'g-ui-event-calendar-form-suggestion-item',
769
+ onClick: () => {
770
+ locationState.set(loc.label ?? loc.id);
771
+ locationQuery.set('');
772
+ locationOptions.set([]);
773
+ },
774
+ },
775
+ loc.label ?? loc.id
776
+ )
777
+ )
778
+ )
779
+ ),
780
+ when(after(locationState).compute((v) => !!v), () =>
781
+ Span({ className: 'g-ui-event-calendar-form-chip' }, locationState)
782
+ )
783
+ )
784
+ ),
785
+ when(eventCreationEmailLookup, () =>
786
+ Div(
787
+ { className: 'g-ui-event-calendar-form-row' },
788
+ Label({ className: 'g-ui-event-calendar-form-label' }, 'Add guests'),
789
+ TextInput({
790
+ placeholder: 'Type email to search',
791
+ value: emailLookupQuery,
792
+ onInput: (ev) => doEmailLookup(ev?.target?.value ?? ''),
793
+ }),
794
+ Div(
795
+ { className: 'g-ui-event-calendar-form-chips' },
796
+ attendeesState.get().map((a, i) =>
797
+ Span(
798
+ {
799
+ key: (a.email || a.id) + i,
800
+ className: 'g-ui-event-calendar-form-chip g-ui-event-calendar-form-chip-removable',
801
+ onClick: () => removeAttendee(i),
802
+ },
803
+ a.label || a.email || a.id,
804
+ ' ×'
805
+ )
806
+ )
807
+ ),
808
+ when(after(emailLookupResults).compute((r) => r && r.length > 0), () =>
809
+ Div(
810
+ { className: 'g-ui-event-calendar-form-suggestions' },
811
+ emailLookupResults.get().map((item) =>
812
+ Div(
813
+ {
814
+ key: item.id ?? item.email,
815
+ className: 'g-ui-event-calendar-form-suggestion-item',
816
+ onClick: () => addAttendee(item),
817
+ },
818
+ item.label ?? item.email ?? item.id
819
+ )
820
+ )
821
+ )
822
+ )
823
+ )
824
+ ),
825
+ Div(
826
+ { className: 'g-ui-event-calendar-form-row' },
827
+ Textarea({
828
+ label: 'Description',
829
+ placeholder: 'Add description',
830
+ value: descState,
831
+ onInput: (ev) => descState.set(ev?.target?.value ?? ''),
832
+ })
833
+ ),
834
+ when(colorOptions.length > 0, () =>
835
+ Div(
836
+ { className: 'g-ui-event-calendar-form-row' },
837
+ Label({ className: 'g-ui-event-calendar-form-label' }, 'Color'),
838
+ Div(
839
+ { className: 'g-ui-event-calendar-form-colors' },
840
+ colorOptions.map((opt) =>
841
+ Div(
842
+ {
843
+ key: opt.value ?? opt.id ?? opt,
844
+ className: cx(
845
+ 'g-ui-event-calendar-form-color-swatch',
846
+ after(colorState).compute((c) => (c === (opt.value ?? opt.id ?? opt) ? 'g-ui-event-calendar-form-color-swatch-active' : ''))
847
+ ),
848
+ style: { backgroundColor: opt.color ?? opt.value ?? opt },
849
+ onClick: () => colorState.set(opt.value ?? opt.id ?? opt),
850
+ }
851
+ )
852
+ )
853
+ )
854
+ )
855
+ ),
856
+ Div(
857
+ { className: 'g-ui-event-calendar-form-actions' },
858
+ Button({ variant: 'subtle', onClick: closeModal }, 'Cancel'),
859
+ Button(
860
+ {
861
+ variant: 'filled',
862
+ onClick: () => {
863
+ applyDraft();
864
+ if (isEdit) handleUpdateSubmit();
865
+ else handleCreateSubmit();
866
+ },
867
+ },
868
+ isEdit ? 'Save' : 'Create'
869
+ )
870
+ )
871
+ );
872
+ };
873
+
874
+ const viewModalContent = () => {
875
+ const ev = selectedEvent.get();
876
+ if (!ev) return null;
877
+ return Div(
878
+ { className: 'g-ui-event-calendar-view-modal' },
879
+ Div({ className: 'g-ui-event-calendar-view-modal-title' }, ev.title || '(No title)'),
880
+ Div(
881
+ { className: 'g-ui-event-calendar-view-modal-meta' },
882
+ ev.allDay
883
+ ? Span({ className: 'g-ui-event-calendar-view-modal-date' }, formatDate(ev.start, resolveValue(locale)) + (ev.start.getTime() !== ev.end.getTime() ? ` – ${formatDate(ev.end, resolveValue(locale))}` : ''))
884
+ : Span(
885
+ { className: 'g-ui-event-calendar-view-modal-date' },
886
+ `${formatDate(ev.start, resolveValue(locale))} · ${formatTime(ev.start, resolveValue(locale))} – ${formatTime(ev.end, resolveValue(locale))}`
887
+ )
888
+ ),
889
+ when(ev.location, () => Div({ className: 'g-ui-event-calendar-view-modal-field' }, 'Location: ', ev.location)),
890
+ when(ev.description, () => Div({ className: 'g-ui-event-calendar-view-modal-field' }, ev.description)),
891
+ when(ev.attendees?.length, () =>
892
+ Div(
893
+ { className: 'g-ui-event-calendar-view-modal-field' },
894
+ 'Guests: ',
895
+ ev.attendees.map((a) => a.label || a.email || a.id).join(', ')
896
+ )
897
+ ),
898
+ Div(
899
+ { className: 'g-ui-event-calendar-view-modal-actions' },
900
+ Button({ variant: 'subtle', size: 'sm', onClick: openEditModal }, Icon({ size: 'sm', innerHTML: editSvg }), ' Edit'),
901
+ Button(
902
+ { variant: 'subtle', size: 'sm', onClick: handleRemoveRequest, className: 'g-ui-event-calendar-view-modal-delete' },
903
+ Icon({ size: 'sm', innerHTML: deleteSvg }),
904
+ ' Delete'
905
+ ),
906
+ Button({ variant: 'filled', size: 'sm', onClick: closeModal }, 'Close')
907
+ )
908
+ );
909
+ };
910
+
911
+ return Div(
912
+ { ...rest, className: cx('g-ui-event-calendar', className) },
913
+ header,
914
+ when(
915
+ viewMode,
916
+ (v) => v === 'month',
917
+ () => monthView
918
+ ),
919
+ when(
920
+ viewMode,
921
+ (v) => v === 'week',
922
+ () => weekView
923
+ ),
924
+ when(
925
+ viewMode,
926
+ (v) => v === 'day',
927
+ () => dayView
928
+ ),
929
+ when(
930
+ modalState,
931
+ (m) => m === 'create',
932
+ () =>
933
+ Modal(
934
+ {
935
+ opened: true,
936
+ title: 'New event',
937
+ size: 'lg',
938
+ onClose: closeModal,
939
+ },
940
+ createEditForm(false)
941
+ )
942
+ ),
943
+ when(
944
+ modalState,
945
+ (m) => m === 'view',
946
+ () =>
947
+ Modal(
948
+ {
949
+ opened: true,
950
+ title: 'Event',
951
+ size: 'md',
952
+ onClose: closeModal,
953
+ },
954
+ viewModalContent()
955
+ )
956
+ ),
957
+ when(
958
+ modalState,
959
+ (m) => m === 'edit',
960
+ () =>
961
+ Modal(
962
+ {
963
+ opened: true,
964
+ title: 'Edit event',
965
+ size: 'lg',
966
+ onClose: closeModal,
967
+ },
968
+ createEditForm(true)
969
+ )
970
+ )
971
+ );
972
+ }