@monkey-mini-app/ui 0.1.0 → 0.1.4

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 (203) hide show
  1. package/README.md +58 -0
  2. package/dist/globals.css +1 -1
  3. package/dist/index.js +12 -2
  4. package/dist/manifest.json +6 -6
  5. package/dist/runtime.js +9 -0
  6. package/dist/sdk.js +158 -0
  7. package/dist/src/blocks/activity-feed.tsx +7 -0
  8. package/dist/src/blocks/app-shell.tsx +1 -0
  9. package/dist/src/blocks/attachment-gallery.tsx +7 -0
  10. package/dist/src/blocks/bar-chart.tsx +15 -1
  11. package/dist/src/blocks/comment-thread.tsx +7 -0
  12. package/dist/src/blocks/dashboard-shell.tsx +109 -0
  13. package/dist/src/blocks/description-list.tsx +7 -0
  14. package/dist/src/blocks/detail-panel.tsx +1 -0
  15. package/dist/src/blocks/donut-chart.tsx +19 -2
  16. package/dist/src/blocks/env-badge.tsx +7 -0
  17. package/dist/src/blocks/env-table.tsx +316 -0
  18. package/dist/src/blocks/file-tree.tsx +7 -0
  19. package/dist/src/blocks/filter-bar.tsx +1 -0
  20. package/dist/src/blocks/form-sheet.tsx +160 -0
  21. package/dist/src/blocks/gauge.tsx +29 -6
  22. package/dist/src/blocks/list-detail.tsx +121 -0
  23. package/dist/src/blocks/notification-center.tsx +7 -0
  24. package/dist/src/blocks/page-header.tsx +1 -0
  25. package/dist/src/blocks/progress-ring.tsx +20 -2
  26. package/dist/src/blocks/radar-chart.tsx +18 -1
  27. package/dist/src/blocks/request-inspector.tsx +7 -0
  28. package/dist/src/blocks/reveal.tsx +56 -0
  29. package/dist/src/blocks/run-timeline.tsx +7 -0
  30. package/dist/src/blocks/scrollspy.tsx +7 -0
  31. package/dist/src/blocks/settings-split.tsx +154 -0
  32. package/dist/src/blocks/severity-chip.tsx +7 -0
  33. package/dist/src/blocks/sparkline.tsx +12 -3
  34. package/dist/src/blocks/stat-card.tsx +1 -0
  35. package/dist/src/blocks/status-badge.tsx +1 -0
  36. package/dist/src/blocks/table-page.tsx +83 -0
  37. package/dist/src/blocks/terminal.tsx +21 -3
  38. package/dist/src/blocks/test-step-list.tsx +7 -0
  39. package/dist/src/blocks/trend-card.tsx +7 -0
  40. package/dist/src/blocks/wizard-shell.tsx +132 -0
  41. package/dist/src/cdn-modules.d.ts +5 -0
  42. package/dist/src/components/accordion.tsx +7 -0
  43. package/dist/src/components/alert-dialog.tsx +7 -0
  44. package/dist/src/components/alert.tsx +7 -0
  45. package/dist/src/components/aspect-ratio.tsx +4 -0
  46. package/dist/src/components/attachment.tsx +4 -0
  47. package/dist/src/components/avatar.tsx +4 -0
  48. package/dist/src/components/badge.tsx +7 -0
  49. package/dist/src/components/breadcrumb.tsx +4 -0
  50. package/dist/src/components/bubble.tsx +4 -0
  51. package/dist/src/components/button-group.tsx +6 -0
  52. package/dist/src/components/button.tsx +7 -0
  53. package/dist/src/components/calendar.tsx +4 -0
  54. package/dist/src/components/card.tsx +8 -1
  55. package/dist/src/components/chart.tsx +7 -0
  56. package/dist/src/components/checkbox.tsx +5 -0
  57. package/dist/src/components/collapsible.tsx +4 -0
  58. package/dist/src/components/combobox.tsx +4 -0
  59. package/dist/src/components/command.tsx +6 -0
  60. package/dist/src/components/context-menu.tsx +4 -0
  61. package/dist/src/components/dialog.tsx +21 -0
  62. package/dist/src/components/drawer.tsx +4 -0
  63. package/dist/src/components/dropdown-menu.tsx +7 -0
  64. package/dist/src/components/empty.tsx +7 -0
  65. package/dist/src/components/field.tsx +6 -0
  66. package/dist/src/components/hover-card.tsx +4 -0
  67. package/dist/src/components/input-group.tsx +4 -0
  68. package/dist/src/components/input.tsx +7 -0
  69. package/dist/src/components/item.tsx +4 -0
  70. package/dist/src/components/kbd.tsx +4 -0
  71. package/dist/src/components/label.tsx +7 -0
  72. package/dist/src/components/marker.tsx +4 -0
  73. package/dist/src/components/menubar.tsx +4 -0
  74. package/dist/src/components/message-scroller.tsx +4 -0
  75. package/dist/src/components/message.tsx +4 -0
  76. package/dist/src/components/native-select.tsx +4 -0
  77. package/dist/src/components/navigation-menu.tsx +4 -0
  78. package/dist/src/components/pagination.tsx +4 -0
  79. package/dist/src/components/popover.tsx +7 -0
  80. package/dist/src/components/progress.tsx +7 -0
  81. package/dist/src/components/questionnaire.tsx +4 -0
  82. package/dist/src/components/radio-group.tsx +4 -0
  83. package/dist/src/components/resizable.tsx +7 -0
  84. package/dist/src/components/scroll-area.tsx +4 -0
  85. package/dist/src/components/select.tsx +7 -0
  86. package/dist/src/components/separator.tsx +5 -0
  87. package/dist/src/components/sheet.tsx +22 -0
  88. package/dist/src/components/sidebar.tsx +4 -0
  89. package/dist/src/components/skeleton.tsx +7 -0
  90. package/dist/src/components/slider.tsx +4 -0
  91. package/dist/src/components/spinner.tsx +6 -0
  92. package/dist/src/components/switch.tsx +7 -0
  93. package/dist/src/components/table.tsx +7 -0
  94. package/dist/src/components/tabs.tsx +7 -0
  95. package/dist/src/components/textarea.tsx +7 -0
  96. package/dist/src/components/toast.tsx +7 -0
  97. package/dist/src/components/toggle-group.tsx +4 -0
  98. package/dist/src/components/toggle.tsx +4 -0
  99. package/dist/src/components/tooltip.tsx +7 -0
  100. package/dist/src/composites/autocomplete.tsx +7 -0
  101. package/dist/src/composites/cascader.tsx +7 -0
  102. package/dist/src/composites/color-picker.tsx +7 -0
  103. package/dist/src/composites/confirm-dialog.tsx +7 -0
  104. package/dist/src/composites/copyable.tsx +9 -1
  105. package/dist/src/composites/currency-input.tsx +7 -0
  106. package/dist/src/composites/date-picker.tsx +7 -0
  107. package/dist/src/composites/date-range-picker.tsx +4 -0
  108. package/dist/src/composites/date-time-picker.tsx +7 -0
  109. package/dist/src/composites/date-time-range-picker.tsx +7 -0
  110. package/dist/src/composites/duration-input.tsx +7 -0
  111. package/dist/src/composites/mini-calendar.tsx +7 -0
  112. package/dist/src/composites/number-field.tsx +7 -0
  113. package/dist/src/composites/password-field.tsx +7 -0
  114. package/dist/src/composites/phone-input.tsx +7 -0
  115. package/dist/src/composites/rating.tsx +7 -0
  116. package/dist/src/composites/relative-date-picker.tsx +7 -0
  117. package/dist/src/composites/search-input.tsx +7 -0
  118. package/dist/src/composites/slider-range.tsx +7 -0
  119. package/dist/src/composites/tag-input.tsx +7 -0
  120. package/dist/src/composites/time-picker.tsx +11 -1
  121. package/dist/src/composites/time-range-picker.tsx +7 -0
  122. package/dist/src/composites/timezone-select.tsx +7 -0
  123. package/dist/src/composites/transfer.tsx +7 -0
  124. package/dist/src/composites/user-picker.tsx +7 -0
  125. package/dist/src/hooks/use-html-dark.ts +37 -0
  126. package/dist/src/hooks/use-mobile.ts +3 -0
  127. package/dist/src/i18n/en.ts +109 -2
  128. package/dist/src/i18n/zh.ts +99 -2
  129. package/dist/src/index.ts +147 -134
  130. package/dist/src/lib/clipboard.ts +10 -0
  131. package/dist/src/lib/illustrations.tsx +2 -2
  132. package/dist/src/products/code-block.tsx +35 -18
  133. package/dist/src/products/code-editor.tsx +156 -24
  134. package/dist/src/products/commit-graph.tsx +644 -0
  135. package/dist/src/products/data-grid.tsx +2 -1
  136. package/dist/src/products/diff-viewer.tsx +104 -8
  137. package/dist/src/products/event-calendar.tsx +28 -823
  138. package/dist/src/products/file-dropzone.tsx +7 -0
  139. package/dist/src/products/full-calendar/animations.ts +38 -0
  140. package/dist/src/products/full-calendar/calendar-body.tsx +77 -0
  141. package/dist/src/products/full-calendar/constants.ts +10 -0
  142. package/dist/src/products/full-calendar/contexts/calendar-context.tsx +255 -0
  143. package/dist/src/products/full-calendar/contexts/dnd-context.tsx +157 -0
  144. package/dist/src/products/full-calendar/dialogs/add-edit-event-dialog.tsx +187 -0
  145. package/dist/src/products/full-calendar/dialogs/delete-event-dialog.tsx +39 -0
  146. package/dist/src/products/full-calendar/dialogs/event-details-dialog.tsx +100 -0
  147. package/dist/src/products/full-calendar/dialogs/events-list-dialog.tsx +87 -0
  148. package/dist/src/products/full-calendar/dnd/draggable-event.tsx +45 -0
  149. package/dist/src/products/full-calendar/dnd/droppable-area.tsx +45 -0
  150. package/dist/src/products/full-calendar/dnd/resizable-event.tsx +179 -0
  151. package/dist/src/products/full-calendar/header/calendar-header.tsx +65 -0
  152. package/dist/src/products/full-calendar/header/date-navigator.tsx +110 -0
  153. package/dist/src/products/full-calendar/header/filter.tsx +58 -0
  154. package/dist/src/products/full-calendar/header/today-button.tsx +45 -0
  155. package/dist/src/products/full-calendar/header/user-select.tsx +64 -0
  156. package/dist/src/products/full-calendar/header/view-tabs.tsx +36 -0
  157. package/dist/src/products/full-calendar/helpers.ts +453 -0
  158. package/dist/src/products/full-calendar/hooks/use-time-range-create.ts +152 -0
  159. package/dist/src/products/full-calendar/hooks.ts +70 -0
  160. package/dist/src/products/full-calendar/interfaces.ts +23 -0
  161. package/dist/src/products/full-calendar/settings/settings.tsx +80 -0
  162. package/dist/src/products/full-calendar/shims/avatar-group.tsx +16 -0
  163. package/dist/src/products/full-calendar/shims/day-picker.tsx +1 -0
  164. package/dist/src/products/full-calendar/shims/motion.tsx +97 -0
  165. package/dist/src/products/full-calendar/shims/next-themes.ts +6 -0
  166. package/dist/src/products/full-calendar/shims/re-resizable.tsx +25 -0
  167. package/dist/src/products/full-calendar/shims/sonner.ts +4 -0
  168. package/dist/src/products/full-calendar/types.ts +8 -0
  169. package/dist/src/products/full-calendar/views/agenda-view/agenda-events.tsx +140 -0
  170. package/dist/src/products/full-calendar/views/month-view/calendar-month-view.tsx +69 -0
  171. package/dist/src/products/full-calendar/views/month-view/day-cell.tsx +201 -0
  172. package/dist/src/products/full-calendar/views/month-view/event-bullet.tsx +40 -0
  173. package/dist/src/products/full-calendar/views/month-view/month-event-badge.tsx +143 -0
  174. package/dist/src/products/full-calendar/views/week-and-day-view/calendar-day-view.tsx +295 -0
  175. package/dist/src/products/full-calendar/views/week-and-day-view/calendar-time-line.tsx +35 -0
  176. package/dist/src/products/full-calendar/views/week-and-day-view/calendar-week-view.tsx +253 -0
  177. package/dist/src/products/full-calendar/views/week-and-day-view/day-view-multi-day-events-row.tsx +73 -0
  178. package/dist/src/products/full-calendar/views/week-and-day-view/event-block.tsx +114 -0
  179. package/dist/src/products/full-calendar/views/week-and-day-view/render-grouped-events.tsx +49 -0
  180. package/dist/src/products/full-calendar/views/week-and-day-view/week-view-multi-day-events-row.tsx +143 -0
  181. package/dist/src/products/full-calendar/views/year-view/calendar-year-view.tsx +161 -0
  182. package/dist/src/products/gantt.tsx +7 -0
  183. package/dist/src/products/jira-wiki.tsx +1 -0
  184. package/dist/src/products/jql-input.tsx +340 -37
  185. package/dist/src/products/json-viewer.tsx +7 -0
  186. package/dist/src/products/kanban-issue-panel.tsx +7 -0
  187. package/dist/src/products/kanban.tsx +1 -0
  188. package/dist/src/products/log-viewer.tsx +390 -18
  189. package/dist/src/products/markdown-editor.tsx +21 -6
  190. package/dist/src/products/markdown.tsx +7 -0
  191. package/dist/src/products/rich-text-editor.tsx +65 -59
  192. package/dist/src/products/sortable-list.tsx +7 -0
  193. package/dist/src/products/stepper.tsx +219 -70
  194. package/dist/src/products/timeline.tsx +7 -0
  195. package/dist/src/products/tree-view.tsx +7 -0
  196. package/dist/src/sdk/app-error-boundary.tsx +240 -0
  197. package/dist/src/sdk/app-id.ts +7 -0
  198. package/dist/src/sdk/use-app.ts +126 -0
  199. package/dist/src/styles/globals.css +7 -7
  200. package/dist/vendors/lodash.js +20 -0
  201. package/dist/vendors/motion.js +12 -0
  202. package/package.json +23 -19
  203. package/dist/src/products/jql-language.ts +0 -70
@@ -0,0 +1,253 @@
1
+ import {addDays, format, isSameDay, parseISO, startOfWeek} from "date-fns";
2
+ import {motion} from "../../shims/motion";
3
+ import {ScrollArea} from "@monkey-mini-app/ui/components/scroll-area";
4
+ import {
5
+ fadeIn,
6
+ staggerContainer,
7
+ transition,
8
+ } from "../../animations";
9
+ import {useCalendar} from "../../contexts/calendar-context";
10
+ import {AddEditEventDialog} from "../../dialogs/add-edit-event-dialog";
11
+ import {DroppableArea} from "../../dnd/droppable-area";
12
+ import {groupEvents, HOUR_HEIGHT_PX} from "../../helpers";
13
+ import {useTimeRangeCreate} from "../../hooks/use-time-range-create";
14
+ import type {IEvent} from "../../interfaces";
15
+ import {CalendarTimeline} from "./calendar-time-line";
16
+ import {RenderGroupedEvents} from "./render-grouped-events";
17
+ import {
18
+ WeekViewMultiDayEventsRow
19
+ } from "./week-view-multi-day-events-row";
20
+
21
+ interface IProps {
22
+ singleDayEvents: IEvent[];
23
+ multiDayEvents: IEvent[];
24
+ }
25
+
26
+ export function CalendarWeekView({singleDayEvents, multiDayEvents}: IProps) {
27
+ const {selectedDate, use24HourFormat} = useCalendar();
28
+ const {
29
+ selection,
30
+ draft,
31
+ clearDraft,
32
+ overlayStyle,
33
+ onPointerDown,
34
+ onPointerMove,
35
+ onPointerUp,
36
+ } = useTimeRangeCreate();
37
+
38
+ const weekStart = startOfWeek(selectedDate);
39
+ const weekDays = Array.from({length: 7}, (_, i) => addDays(weekStart, i));
40
+ const hours = Array.from({length: 24}, (_, i) => i);
41
+
42
+ return (
43
+ <motion.div
44
+ data-testid="calendar-week-grid"
45
+ initial="initial"
46
+ animate="animate"
47
+ exit="exit"
48
+ variants={fadeIn}
49
+ transition={transition}
50
+ >
51
+ <motion.div
52
+ className="flex flex-col items-center justify-center border-b p-4 text-sm sm:hidden"
53
+ initial={{opacity: 0, y: -20}}
54
+ animate={{opacity: 1, y: 0}}
55
+ transition={transition}
56
+ >
57
+ <p>Weekly view is not recommended on smaller devices.</p>
58
+ <p>Please switch to a desktop device or use the daily view instead.</p>
59
+ </motion.div>
60
+
61
+ <motion.div
62
+ className="flex-col sm:flex"
63
+ variants={staggerContainer}
64
+ >
65
+ <div>
66
+ <WeekViewMultiDayEventsRow
67
+ selectedDate={selectedDate}
68
+ multiDayEvents={multiDayEvents}
69
+ />
70
+
71
+ {/* Week header */}
72
+ <motion.div
73
+ className="relative z-20 flex border-b"
74
+ initial={{opacity: 0, y: -20}}
75
+ animate={{opacity: 1, y: 0}}
76
+ transition={transition}
77
+ >
78
+ {/* Time column header - responsive width */}
79
+ <div className="w-18"></div>
80
+ <div className="grid flex-1 grid-cols-7 border-l">
81
+ {weekDays.map((day, index) => (
82
+ <motion.span
83
+ key={day.toISOString()}
84
+ className="py-1 sm:py-2 text-center text-xs font-medium text-t-quaternary"
85
+ initial={{opacity: 0, y: -10}}
86
+ animate={{opacity: 1, y: 0}}
87
+ transition={{delay: index * 0.05, ...transition}}
88
+ >
89
+ {/* Mobile: Show only day abbreviation and number */}
90
+ <span className="block sm:hidden">
91
+ {format(day, "EEE").charAt(0)}
92
+ <span className="block font-semibold text-t-secondary text-xs">
93
+ {format(day, "d")}
94
+ </span>
95
+ </span>
96
+ {/* Desktop: Show full format */}
97
+ <span className="hidden sm:inline">
98
+ {format(day, "EE")}{" "}
99
+ <span className="ml-1 font-semibold text-t-secondary">
100
+ {format(day, "d")}
101
+ </span>
102
+ </span>
103
+ </motion.span>
104
+ ))}
105
+ </div>
106
+ </motion.div>
107
+
108
+ </div>
109
+
110
+ <ScrollArea className="h-[736px]">
111
+ <div className="flex">
112
+ {/* Hours column */}
113
+ <motion.div className="relative w-18" variants={staggerContainer}>
114
+ {hours.map((hour, index) => (
115
+ <motion.div
116
+ key={hour}
117
+ className="relative"
118
+ style={{height: `${HOUR_HEIGHT_PX}px`}}
119
+ initial={{opacity: 0, x: -20}}
120
+ animate={{opacity: 1, x: 0}}
121
+ transition={{delay: index * 0.02, ...transition}}
122
+ >
123
+ <div className="absolute -top-3 right-2 flex h-6 items-center">
124
+ {index !== 0 && (
125
+ <span className="text-xs text-t-quaternary">
126
+ {format(
127
+ new Date().setHours(hour, 0, 0, 0),
128
+ use24HourFormat ? "HH:00" : "h a",
129
+ )}
130
+ </span>
131
+ )}
132
+ </div>
133
+ </motion.div>
134
+ ))}
135
+ </motion.div>
136
+
137
+ {/* Week grid */}
138
+ <motion.div
139
+ className="relative flex-1 border-l"
140
+ variants={staggerContainer}
141
+ >
142
+ <div className="grid grid-cols-7 divide-x">
143
+ {weekDays.map((day, dayIndex) => {
144
+ const dayEvents = singleDayEvents.filter(
145
+ (event) =>
146
+ isSameDay(parseISO(event.startDate), day) ||
147
+ isSameDay(parseISO(event.endDate), day),
148
+ );
149
+ const groupedEvents = groupEvents(dayEvents);
150
+
151
+ const showOverlay =
152
+ selection != null &&
153
+ isSameDay(selection.day, day) &&
154
+ overlayStyle != null;
155
+
156
+ return (
157
+ <motion.div
158
+ key={day.toISOString()}
159
+ className="relative touch-none select-none"
160
+ data-testid="calendar-day-column"
161
+ data-day={day.toISOString()}
162
+ initial={{opacity: 0}}
163
+ animate={{opacity: 1}}
164
+ transition={{delay: dayIndex * 0.1, ...transition}}
165
+ onPointerDown={(e) => onPointerDown(e, day, e.currentTarget)}
166
+ onPointerMove={onPointerMove}
167
+ onPointerUp={onPointerUp}
168
+ onPointerCancel={onPointerUp}
169
+ >
170
+ {hours.map((hour, index) => (
171
+ <motion.div
172
+ key={hour}
173
+ className="relative"
174
+ style={{height: `${HOUR_HEIGHT_PX}px`}}
175
+ initial={{opacity: 0}}
176
+ animate={{opacity: 1}}
177
+ transition={{delay: index * 0.01, ...transition}}
178
+ >
179
+ {index !== 0 && (
180
+ <div
181
+ className="pointer-events-none absolute inset-x-0 top-0 border-b"></div>
182
+ )}
183
+
184
+ <DroppableArea
185
+ date={day}
186
+ hour={hour}
187
+ minute={0}
188
+ className="absolute inset-x-0 top-0 h-[48px]"
189
+ >
190
+ <div className="absolute inset-0" />
191
+ </DroppableArea>
192
+
193
+ <div
194
+ className="pointer-events-none absolute inset-x-0 top-1/2 border-b border-dashed border-b-tertiary"></div>
195
+
196
+ <DroppableArea
197
+ date={day}
198
+ hour={hour}
199
+ minute={30}
200
+ className="absolute inset-x-0 bottom-0 h-[48px]"
201
+ >
202
+ <div className="absolute inset-0" />
203
+ </DroppableArea>
204
+ </motion.div>
205
+ ))}
206
+
207
+ {showOverlay ? (
208
+ <div
209
+ className="pointer-events-none absolute inset-x-0.5 z-10 rounded-md border border-primary/40 bg-primary/15"
210
+ data-testid="calendar-selection-overlay"
211
+ style={overlayStyle}
212
+ />
213
+ ) : null}
214
+
215
+ <div className="pointer-events-auto relative z-20">
216
+ <RenderGroupedEvents
217
+ groupedEvents={groupedEvents}
218
+ day={day}
219
+ />
220
+ </div>
221
+ </motion.div>
222
+ );
223
+ })}
224
+ </div>
225
+
226
+ <CalendarTimeline/>
227
+ </motion.div>
228
+
229
+ {draft ? (
230
+ <AddEditEventDialog
231
+ open
232
+ onOpenChange={(open) => {
233
+ if (!open) clearDraft();
234
+ }}
235
+ startDate={draft.start}
236
+ startTime={{
237
+ hour: draft.start.getHours(),
238
+ minute: draft.start.getMinutes(),
239
+ }}
240
+ endTime={{
241
+ hour: draft.end.getHours(),
242
+ minute: draft.end.getMinutes(),
243
+ }}
244
+ />
245
+ ) : null}
246
+ </div>
247
+ </ScrollArea>
248
+ </motion.div>
249
+ </motion.div>
250
+ );
251
+ }
252
+
253
+
@@ -0,0 +1,73 @@
1
+ import {
2
+ differenceInDays,
3
+ endOfDay,
4
+ isWithinInterval,
5
+ parseISO,
6
+ startOfDay,
7
+ } from "date-fns";
8
+ import type { IEvent } from "../../interfaces";
9
+ import { MonthEventBadge } from "../month-view/month-event-badge";
10
+
11
+ interface IProps {
12
+ selectedDate: Date;
13
+ multiDayEvents: IEvent[];
14
+ }
15
+
16
+ export function DayViewMultiDayEventsRow({
17
+ selectedDate,
18
+ multiDayEvents,
19
+ }: IProps) {
20
+ const dayStart = startOfDay(selectedDate);
21
+ const dayEnd = endOfDay(selectedDate);
22
+
23
+ const multiDayEventsInDay = multiDayEvents
24
+ .filter((event) => {
25
+ const eventStart = parseISO(event.startDate);
26
+ const eventEnd = parseISO(event.endDate);
27
+
28
+ return (
29
+ isWithinInterval(dayStart, { start: eventStart, end: eventEnd }) ||
30
+ isWithinInterval(dayEnd, { start: eventStart, end: eventEnd }) ||
31
+ (eventStart <= dayStart && eventEnd >= dayEnd)
32
+ );
33
+ })
34
+ .sort((a, b) => {
35
+ const durationA = differenceInDays(
36
+ parseISO(a.endDate),
37
+ parseISO(a.startDate),
38
+ );
39
+ const durationB = differenceInDays(
40
+ parseISO(b.endDate),
41
+ parseISO(b.startDate),
42
+ );
43
+ return durationB - durationA;
44
+ });
45
+
46
+ if (multiDayEventsInDay.length === 0) return null;
47
+
48
+ return (
49
+ <div className="flex border-b">
50
+ <div className="w-18"></div>
51
+ <div className="flex flex-1 flex-col gap-1 border-l py-1">
52
+ {multiDayEventsInDay.map((event) => {
53
+ const eventStart = startOfDay(parseISO(event.startDate));
54
+ const eventEnd = startOfDay(parseISO(event.endDate));
55
+ const currentDate = startOfDay(selectedDate);
56
+
57
+ const eventTotalDays = differenceInDays(eventEnd, eventStart) + 1;
58
+ const eventCurrentDay = differenceInDays(currentDate, eventStart) + 1;
59
+
60
+ return (
61
+ <MonthEventBadge
62
+ key={event.id}
63
+ event={event}
64
+ cellDate={selectedDate}
65
+ eventCurrentDay={eventCurrentDay}
66
+ eventTotalDays={eventTotalDays}
67
+ />
68
+ );
69
+ })}
70
+ </div>
71
+ </div>
72
+ );
73
+ }
@@ -0,0 +1,114 @@
1
+ import type { VariantProps } from "class-variance-authority";
2
+ import { cva } from "class-variance-authority";
3
+ import { differenceInMinutes, parseISO } from "date-fns";
4
+ import type { HTMLAttributes } from "react";
5
+ import { cn } from "@monkey-mini-app/ui/lib/utils";
6
+ import { useCalendar } from "../../contexts/calendar-context";
7
+ import { EventDetailsDialog } from "../../dialogs/event-details-dialog";
8
+ import { DraggableEvent } from "../../dnd/draggable-event";
9
+ import { ResizableEvent } from "../../dnd/resizable-event";
10
+ import { formatTime } from "../../helpers";
11
+ import type { IEvent } from "../../interfaces";
12
+
13
+ const calendarWeekEventCardVariants = cva(
14
+ "flex select-none flex-col gap-0.5 truncate whitespace-nowrap rounded-md border px-2 py-1.5 text-xs focus-visible:outline-offset-2",
15
+ {
16
+ variants: {
17
+ color: {
18
+ // Colored variants
19
+ blue: "border-blue-200 bg-blue-100/50 text-blue-700 hover:bg-blue-100 dark:border-blue-800 dark:bg-blue-950/50 dark:text-blue-300 dark:hover:bg-blue-950",
20
+ green:
21
+ "border-green-200 bg-green-100/50 text-green-700 hover:bg-green-100 dark:border-green-800 dark:bg-green-950/50 dark:text-green-300 dark:hover:bg-green-950",
22
+ red: "border-red-200 bg-red-100/50 text-red-700 hover:bg-red-100 dark:border-red-800 dark:bg-red-950/50 dark:text-red-300 dark:hover:bg-red-950",
23
+ yellow:
24
+ "border-yellow-200 bg-yellow-100/50 text-yellow-700 hover:bg-yellow-100 dark:border-yellow-800 dark:bg-yellow-950/50 dark:text-yellow-300 dark:hover:bg-yellow-950",
25
+ purple:
26
+ "border-purple-200 bg-purple-100/50 text-purple-700 hover:bg-purple-100 dark:border-purple-800 dark:bg-purple-950/50 dark:text-purple-300 dark:hover:bg-purple-950",
27
+ orange:
28
+ "border-orange-200 bg-orange-100/50 text-orange-700 hover:bg-orange-100 dark:border-orange-800 dark:bg-orange-950/50 dark:text-orange-300 dark:hover:bg-orange-950",
29
+
30
+ // Dot variants
31
+ "blue-dot":
32
+ "border-border bg-card text-foreground hover:bg-accent [&_svg]:fill-blue-600 dark:[&_svg]:fill-blue-500",
33
+ "green-dot":
34
+ "border-border bg-card text-foreground hover:bg-accent [&_svg]:fill-green-600 dark:[&_svg]:fill-green-500",
35
+ "red-dot":
36
+ "border-border bg-card text-foreground hover:bg-accent [&_svg]:fill-red-600 dark:[&_svg]:fill-red-500",
37
+ "orange-dot":
38
+ "border-border bg-card text-foreground hover:bg-accent [&_svg]:fill-orange-600 dark:[&_svg]:fill-orange-500",
39
+ "purple-dot":
40
+ "border-border bg-card text-foreground hover:bg-accent [&_svg]:fill-purple-600 dark:[&_svg]:fill-purple-500",
41
+ "yellow-dot":
42
+ "border-border bg-card text-foreground hover:bg-accent [&_svg]:fill-yellow-600 dark:[&_svg]:fill-yellow-500",
43
+ },
44
+ },
45
+ defaultVariants: {
46
+ color: "blue-dot",
47
+ },
48
+ },
49
+ );
50
+
51
+ interface IProps
52
+ extends
53
+ HTMLAttributes<HTMLDivElement>,
54
+ Omit<VariantProps<typeof calendarWeekEventCardVariants>, "color"> {
55
+ event: IEvent;
56
+ }
57
+
58
+ export function EventBlock({ event, className }: IProps) {
59
+ const { badgeVariant, use24HourFormat } = useCalendar();
60
+
61
+ const start = parseISO(event.startDate);
62
+ const end = parseISO(event.endDate);
63
+ const durationInMinutes = differenceInMinutes(end, start);
64
+ const heightInPixels = (durationInMinutes / 60) * 96 - 8;
65
+
66
+ const color = (
67
+ badgeVariant === "dot" ? `${event.color}-dot` : event.color
68
+ ) as VariantProps<typeof calendarWeekEventCardVariants>["color"];
69
+
70
+ const calendarWeekEventCardClasses = cn(
71
+ calendarWeekEventCardVariants({ color, className }),
72
+ durationInMinutes < 35 && "py-0 justify-center",
73
+ );
74
+
75
+ return (
76
+ <ResizableEvent event={event}>
77
+ <DraggableEvent event={event}>
78
+ <EventDetailsDialog event={event}>
79
+ <button
80
+ type="button"
81
+ data-calendar-event="1"
82
+ data-testid={`calendar-event-${event.id}`}
83
+ className={calendarWeekEventCardClasses}
84
+ style={{ height: `${heightInPixels}px` }}
85
+ >
86
+ <div className="flex items-center gap-1.5 truncate">
87
+ {badgeVariant === "dot" && (
88
+ <svg
89
+ width="8"
90
+ height="8"
91
+ viewBox="0 0 8 8"
92
+ xmlns="http://www.w3.org/2000/svg"
93
+ className="shrink-0"
94
+ aria-hidden="true"
95
+ >
96
+ <circle cx="4" cy="4" r="4" />
97
+ </svg>
98
+ )}
99
+
100
+ <p className="truncate font-semibold">{event.title}</p>
101
+ </div>
102
+
103
+ {durationInMinutes > 25 && (
104
+ <p>
105
+ {formatTime(start, use24HourFormat)} -{" "}
106
+ {formatTime(end, use24HourFormat)}
107
+ </p>
108
+ )}
109
+ </button>
110
+ </EventDetailsDialog>
111
+ </DraggableEvent>
112
+ </ResizableEvent>
113
+ );
114
+ }
@@ -0,0 +1,49 @@
1
+ import { areIntervalsOverlapping, parseISO } from "date-fns";
2
+ import { getEventBlockStyle } from "../../helpers";
3
+ import type { IEvent } from "../../interfaces";
4
+ import { EventBlock } from "./event-block";
5
+
6
+ interface RenderGroupedEventsProps {
7
+ groupedEvents: IEvent[][];
8
+ day: Date;
9
+ }
10
+
11
+ export function RenderGroupedEvents({
12
+ groupedEvents,
13
+ day,
14
+ }: RenderGroupedEventsProps) {
15
+ return groupedEvents.map((group, groupIndex) =>
16
+ group.map((event) => {
17
+ let style = getEventBlockStyle(
18
+ event,
19
+ day,
20
+ groupIndex,
21
+ groupedEvents.length,
22
+ );
23
+ const hasOverlap = groupedEvents.some(
24
+ (otherGroup, otherIndex) =>
25
+ otherIndex !== groupIndex &&
26
+ otherGroup.some((otherEvent) =>
27
+ areIntervalsOverlapping(
28
+ {
29
+ start: parseISO(event.startDate),
30
+ end: parseISO(event.endDate),
31
+ },
32
+ {
33
+ start: parseISO(otherEvent.startDate),
34
+ end: parseISO(otherEvent.endDate),
35
+ },
36
+ ),
37
+ ),
38
+ );
39
+
40
+ if (!hasOverlap) style = { ...style, width: "100%", left: "0%" };
41
+
42
+ return (
43
+ <div key={event.id} className="absolute p-1" style={style}>
44
+ <EventBlock event={event} />
45
+ </div>
46
+ );
47
+ }),
48
+ );
49
+ }
@@ -0,0 +1,143 @@
1
+ import {
2
+ addDays,
3
+ differenceInDays,
4
+ endOfWeek,
5
+ isAfter,
6
+ isBefore,
7
+ parseISO,
8
+ startOfDay,
9
+ startOfWeek,
10
+ } from "date-fns";
11
+ import { useMemo } from "react";
12
+ import type { IEvent } from "../../interfaces";
13
+ import { MonthEventBadge } from "../month-view/month-event-badge";
14
+
15
+ interface IProps {
16
+ selectedDate: Date;
17
+ multiDayEvents: IEvent[];
18
+ }
19
+
20
+ export function WeekViewMultiDayEventsRow({
21
+ selectedDate,
22
+ multiDayEvents,
23
+ }: IProps) {
24
+ const weekStart = startOfWeek(selectedDate);
25
+ const weekEnd = endOfWeek(selectedDate);
26
+ const weekDays = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i));
27
+
28
+ const processedEvents = useMemo(() => {
29
+ return multiDayEvents
30
+ .map((event) => {
31
+ const start = parseISO(event.startDate);
32
+ const end = parseISO(event.endDate);
33
+ const adjustedStart = isBefore(start, weekStart) ? weekStart : start;
34
+ const adjustedEnd = isAfter(end, weekEnd) ? weekEnd : end;
35
+ const startIndex = differenceInDays(adjustedStart, weekStart);
36
+ const endIndex = differenceInDays(adjustedEnd, weekStart);
37
+
38
+ return {
39
+ ...event,
40
+ adjustedStart,
41
+ adjustedEnd,
42
+ startIndex,
43
+ endIndex,
44
+ };
45
+ })
46
+ .sort((a, b) => {
47
+ const startDiff = a.adjustedStart.getTime() - b.adjustedStart.getTime();
48
+ if (startDiff !== 0) return startDiff;
49
+ return b.endIndex - b.startIndex - (a.endIndex - a.startIndex);
50
+ });
51
+ }, [multiDayEvents, weekStart, weekEnd]);
52
+
53
+ const eventRows = useMemo(() => {
54
+ const rows: (typeof processedEvents)[] = [];
55
+
56
+ processedEvents.forEach((event) => {
57
+ let rowIndex = rows.findIndex((row) =>
58
+ row.every(
59
+ (e) => e.endIndex < event.startIndex || e.startIndex > event.endIndex,
60
+ ),
61
+ );
62
+
63
+ if (rowIndex === -1) {
64
+ rowIndex = rows.length;
65
+ rows.push([]);
66
+ }
67
+
68
+ rows[rowIndex].push(event);
69
+ });
70
+
71
+ return rows;
72
+ }, [processedEvents]);
73
+
74
+ const hasEventsInWeek = useMemo(() => {
75
+ return multiDayEvents.some((event) => {
76
+ const start = parseISO(event.startDate);
77
+ const end = parseISO(event.endDate);
78
+
79
+ return (
80
+ // Event starts within the week
81
+ (start >= weekStart && start <= weekEnd) ||
82
+ // Event ends within the week
83
+ (end >= weekStart && end <= weekEnd) ||
84
+ // Event spans the entire week
85
+ (start <= weekStart && end >= weekEnd)
86
+ );
87
+ });
88
+ }, [multiDayEvents, weekStart, weekEnd]);
89
+
90
+ if (!hasEventsInWeek) {
91
+ return null;
92
+ }
93
+
94
+ return (
95
+ <div className="overflow-hidden flex" data-testid="calendar-all-day">
96
+ <div className="w-18 border-b"></div>
97
+ <div className="grid flex-1 grid-cols-7 divide-x border-b border-l">
98
+ {weekDays.map((day, dayIndex) => (
99
+ <div
100
+ key={day.toISOString()}
101
+ className="flex h-full flex-col gap-1 py-1"
102
+ >
103
+ {eventRows.map((row, rowIndex) => {
104
+ const event = row.find(
105
+ (e) => e.startIndex <= dayIndex && e.endIndex >= dayIndex,
106
+ );
107
+
108
+ if (!event) {
109
+ return (
110
+ <div key={`${rowIndex}-${dayIndex.toString()}`} className="h-6.5" />
111
+ );
112
+ }
113
+
114
+ let position: "first" | "middle" | "last" | "none" = "none";
115
+
116
+ if (
117
+ dayIndex === event.startIndex &&
118
+ dayIndex === event.endIndex
119
+ ) {
120
+ position = "none";
121
+ } else if (dayIndex === event.startIndex) {
122
+ position = "first";
123
+ } else if (dayIndex === event.endIndex) {
124
+ position = "last";
125
+ } else {
126
+ position = "middle";
127
+ }
128
+
129
+ return (
130
+ <MonthEventBadge
131
+ key={`${event.id}-${dayIndex}`}
132
+ event={event}
133
+ cellDate={startOfDay(day)}
134
+ position={position}
135
+ />
136
+ );
137
+ })}
138
+ </div>
139
+ ))}
140
+ </div>
141
+ </div>
142
+ );
143
+ }