@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,40 @@
1
+ import { cva } from "class-variance-authority";
2
+ import { motion } from "../../shims/motion";
3
+ import { cn } from "@monkey-mini-app/ui/lib/utils";
4
+ import { transition } from "../../animations";
5
+ import type { TEventColor } from "../../types";
6
+
7
+ const eventBulletVariants = cva("size-2 rounded-full", {
8
+ variants: {
9
+ color: {
10
+ blue: "bg-blue-600 dark:bg-blue-500",
11
+ green: "bg-green-600 dark:bg-green-500",
12
+ red: "bg-red-600 dark:bg-red-500",
13
+ yellow: "bg-yellow-600 dark:bg-yellow-500",
14
+ purple: "bg-purple-600 dark:bg-purple-500",
15
+ orange: "bg-orange-600 dark:bg-orange-500",
16
+ gray: "bg-gray-600 dark:bg-gray-500",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ color: "blue",
21
+ },
22
+ });
23
+
24
+ export function EventBullet({
25
+ color,
26
+ className,
27
+ }: {
28
+ color: TEventColor;
29
+ className?: string;
30
+ }) {
31
+ return (
32
+ <motion.div
33
+ className={cn(eventBulletVariants({ color, className }))}
34
+ initial={{ scale: 0, opacity: 0 }}
35
+ animate={{ scale: 1, opacity: 1 }}
36
+ whileHover={{ scale: 1.2 }}
37
+ transition={transition}
38
+ />
39
+ );
40
+ }
@@ -0,0 +1,143 @@
1
+ import type { VariantProps } from "class-variance-authority";
2
+ import { cva } from "class-variance-authority";
3
+ import { endOfDay, isSameDay, parseISO, startOfDay } from "date-fns";
4
+ import { cn } from "@monkey-mini-app/ui/lib/utils";
5
+ import { useCalendar } from "../../contexts/calendar-context";
6
+ import { EventDetailsDialog } from "../../dialogs/event-details-dialog";
7
+ import { DraggableEvent } from "../../dnd/draggable-event";
8
+ import { formatTime } from "../../helpers";
9
+ import type { IEvent } from "../../interfaces";
10
+ import { EventBullet } from "./event-bullet";
11
+
12
+ const eventBadgeVariants = cva(
13
+ "flex w-full h-6.5 select-none items-center justify-between gap-1.5 truncate whitespace-nowrap rounded-md border px-2 text-xs cursor-grab",
14
+ {
15
+ variants: {
16
+ color: {
17
+ // Colored variants
18
+ blue: "border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-300",
19
+ green:
20
+ "border-green-200 bg-green-50 text-green-700 dark:border-green-800 dark:bg-green-950 dark:text-green-300",
21
+ red: "border-red-200 bg-red-50 text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-300",
22
+ yellow:
23
+ "border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-800 dark:bg-yellow-950 dark:text-yellow-300",
24
+ purple:
25
+ "border-purple-200 bg-purple-50 text-purple-700 dark:border-purple-800 dark:bg-purple-950 dark:text-purple-300",
26
+ orange:
27
+ "border-orange-200 bg-orange-50 text-orange-700 dark:border-orange-800 dark:bg-orange-950 dark:text-orange-300",
28
+
29
+ // Dot variants
30
+ "blue-dot": "bg-bg-secondary text-t-primary [&_svg]:fill-blue-600",
31
+ "green-dot": "bg-bg-secondary text-t-primary [&_svg]:fill-green-600",
32
+ "red-dot": "bg-bg-secondary text-t-primary [&_svg]:fill-red-600",
33
+ "orange-dot": "bg-bg-secondary text-t-primary [&_svg]:fill-orange-600",
34
+ "purple-dot": "bg-bg-secondary text-t-primary [&_svg]:fill-purple-600",
35
+ "yellow-dot": "bg-bg-secondary text-t-primary [&_svg]:fill-yellow-600",
36
+ },
37
+ multiDayPosition: {
38
+ first: "relative z-10 mr-0 rounded-r-none border-r-0 [&>span]:mr-2.5",
39
+ middle:
40
+ "relative z-10 mx-0 w-[calc(100%_+_1px)] rounded-none border-x-0",
41
+ last: "ml-0 rounded-l-none border-l-0",
42
+ none: "",
43
+ },
44
+ },
45
+ defaultVariants: {
46
+ color: "blue-dot",
47
+ },
48
+ },
49
+ );
50
+
51
+ interface IProps extends Omit<
52
+ VariantProps<typeof eventBadgeVariants>,
53
+ "color" | "multiDayPosition"
54
+ > {
55
+ event: IEvent;
56
+ cellDate: Date;
57
+ eventCurrentDay?: number;
58
+ eventTotalDays?: number;
59
+ className?: string;
60
+ position?: "first" | "middle" | "last" | "none";
61
+ }
62
+
63
+ export function MonthEventBadge({
64
+ event,
65
+ cellDate,
66
+ eventCurrentDay,
67
+ eventTotalDays,
68
+ className,
69
+ position: propPosition,
70
+ }: IProps) {
71
+ const { badgeVariant, use24HourFormat } = useCalendar();
72
+
73
+ const itemStart = startOfDay(parseISO(event.startDate));
74
+ const itemEnd = endOfDay(parseISO(event.endDate));
75
+
76
+ if (cellDate < itemStart || cellDate > itemEnd) return null;
77
+
78
+ let position: "first" | "middle" | "last" | "none" | undefined;
79
+
80
+ if (propPosition) {
81
+ position = propPosition;
82
+ } else if (eventCurrentDay && eventTotalDays) {
83
+ position = "none";
84
+ } else if (isSameDay(itemStart, itemEnd)) {
85
+ position = "none";
86
+ } else if (isSameDay(cellDate, itemStart)) {
87
+ position = "first";
88
+ } else if (isSameDay(cellDate, itemEnd)) {
89
+ position = "last";
90
+ } else {
91
+ position = "middle";
92
+ }
93
+
94
+ const renderBadgeText = ["first", "none"].includes(position);
95
+ const renderBadgeTime = ["last", "none"].includes(position);
96
+
97
+ const color = (
98
+ badgeVariant === "dot" ? `${event.color}-dot` : event.color
99
+ ) as VariantProps<typeof eventBadgeVariants>["color"];
100
+
101
+ const eventBadgeClasses = cn(
102
+ eventBadgeVariants({ color, multiDayPosition: position, className }),
103
+ );
104
+
105
+ const marginClass = {
106
+ first: "ml-1 mr-0",
107
+ middle: "mx-0",
108
+ last: "ml-0 mr-1",
109
+ none: "mx-1",
110
+ }[position || "none"];
111
+
112
+ return (
113
+ <DraggableEvent event={event} className={marginClass}>
114
+ <EventDetailsDialog event={event}>
115
+ <button type="button" data-testid={`calendar-event-${event.id}`} className={eventBadgeClasses}>
116
+ <div className="flex items-center gap-1.5 truncate">
117
+ {!["middle", "last"].includes(position) &&
118
+ badgeVariant === "dot" && <EventBullet color={event.color} />}
119
+
120
+ {renderBadgeText && (
121
+ <p className="flex-1 truncate font-semibold">
122
+ {eventCurrentDay && (
123
+ <span className="text-xs">
124
+ Day {eventCurrentDay} of {eventTotalDays} •{" "}
125
+ </span>
126
+ )}
127
+ {event.title}
128
+ </p>
129
+ )}
130
+ </div>
131
+
132
+ <div className="hidden sm:block">
133
+ {renderBadgeTime && (
134
+ <span>
135
+ {formatTime(new Date(event.startDate), use24HourFormat)}
136
+ </span>
137
+ )}
138
+ </div>
139
+ </button>
140
+ </EventDetailsDialog>
141
+ </DraggableEvent>
142
+ );
143
+ }
@@ -0,0 +1,295 @@
1
+ import { format, isWithinInterval, parseISO } from "date-fns";
2
+ import { Calendar, Clock, User } from "lucide-react";
3
+ import { useEffect, useRef } from "react";
4
+ import { DayPicker } from "../../shims/day-picker";
5
+ import { ScrollArea } from "@monkey-mini-app/ui/components/scroll-area";
6
+ import { useDateLocale, useLabels } from "@monkey-mini-app/ui/i18n/context";
7
+ import { useCalendar } from "../../contexts/calendar-context";
8
+
9
+ import { AddEditEventDialog } from "../../dialogs/add-edit-event-dialog";
10
+ import { DroppableArea } from "../../dnd/droppable-area";
11
+ import { groupEvents, HOUR_HEIGHT_PX } from "../../helpers";
12
+ import { useTimeRangeCreate } from "../../hooks/use-time-range-create";
13
+ import type { IEvent } from "../../interfaces";
14
+ import { CalendarTimeline } from "./calendar-time-line";
15
+ import { DayViewMultiDayEventsRow } from "./day-view-multi-day-events-row";
16
+ import { RenderGroupedEvents } from "./render-grouped-events";
17
+
18
+ interface IProps {
19
+ singleDayEvents: IEvent[];
20
+ multiDayEvents: IEvent[];
21
+ }
22
+
23
+ export function CalendarDayView({ singleDayEvents, multiDayEvents }: IProps) {
24
+ const { selectedDate, setSelectedDate, users, use24HourFormat } =
25
+ useCalendar();
26
+ const t = useLabels("eventCalendar");
27
+ const dateLocale = useDateLocale();
28
+ const scrollAreaRef = useRef<HTMLDivElement>(null);
29
+ const {
30
+ draft,
31
+ clearDraft,
32
+ overlayStyle,
33
+ onPointerDown,
34
+ onPointerMove,
35
+ onPointerUp,
36
+ } = useTimeRangeCreate();
37
+
38
+ const hours = Array.from({ length: 24 }, (_, i) => i);
39
+
40
+ useEffect(() => {
41
+ const handleDragOver = (e: DragEvent) => {
42
+ if (!scrollAreaRef.current) return;
43
+
44
+ const scrollArea = scrollAreaRef.current;
45
+ const rect = scrollArea.getBoundingClientRect();
46
+ const scrollSpeed = 15;
47
+
48
+ const scrollContainer =
49
+ scrollArea.querySelector("[data-radix-scroll-area-viewport]") ||
50
+ scrollArea;
51
+
52
+ if (e.clientY < rect.top + 60) {
53
+ scrollContainer.scrollTop -= scrollSpeed;
54
+ }
55
+
56
+ if (e.clientY > rect.bottom - 60) {
57
+ scrollContainer.scrollTop += scrollSpeed;
58
+ }
59
+ };
60
+
61
+ document.addEventListener("dragover", handleDragOver);
62
+ return () => {
63
+ document.removeEventListener("dragover", handleDragOver);
64
+ };
65
+ }, []);
66
+
67
+ const getCurrentEvents = (events: IEvent[]) => {
68
+ const now = new Date();
69
+
70
+ return (
71
+ events.filter((event) =>
72
+ isWithinInterval(now, {
73
+ start: parseISO(event.startDate),
74
+ end: parseISO(event.endDate),
75
+ }),
76
+ ) || []
77
+ );
78
+ };
79
+
80
+ const currentEvents = getCurrentEvents(singleDayEvents);
81
+
82
+ const dayEvents = singleDayEvents.filter((event) => {
83
+ const eventDate = parseISO(event.startDate);
84
+ return (
85
+ eventDate.getDate() === selectedDate.getDate() &&
86
+ eventDate.getMonth() === selectedDate.getMonth() &&
87
+ eventDate.getFullYear() === selectedDate.getFullYear()
88
+ );
89
+ });
90
+
91
+ const groupedEvents = groupEvents(dayEvents);
92
+
93
+ return (
94
+ <div className="flex">
95
+ <div className="flex flex-1 flex-col">
96
+ <div>
97
+ <DayViewMultiDayEventsRow
98
+ selectedDate={selectedDate}
99
+ multiDayEvents={multiDayEvents}
100
+ />
101
+
102
+ {/* Day header */}
103
+ <div className="relative z-20 flex border-b">
104
+ <div className="w-18"></div>
105
+ <span className="flex-1 border-l py-2 text-center text-xs font-medium text-t-quaternary">
106
+ {format(selectedDate, "EE")}{" "}
107
+ <span className="font-semibold text-t-secondary">
108
+ {format(selectedDate, "d")}
109
+ </span>
110
+ </span>
111
+ </div>
112
+ </div>
113
+ <ScrollArea className="h-[800px]" ref={scrollAreaRef}>
114
+ <div className="flex">
115
+ {/* Hours column */}
116
+ <div className="relative w-18">
117
+ {hours.map((hour, index) => (
118
+ <div key={hour} className="relative" style={{ height: `${HOUR_HEIGHT_PX}px` }}>
119
+ <div className="absolute -top-3 right-2 flex h-6 items-center">
120
+ {index !== 0 && (
121
+ <span className="text-xs text-t-quaternary">
122
+ {format(
123
+ new Date().setHours(hour, 0, 0, 0),
124
+ use24HourFormat ? "HH:00" : "h a",
125
+ )}
126
+ </span>
127
+ )}
128
+ </div>
129
+ </div>
130
+ ))}
131
+ </div>
132
+
133
+ {/* Day grid — drag vertically to select a create range */}
134
+ <div className="relative flex-1 border-l">
135
+ <div
136
+ className="relative touch-none select-none"
137
+ data-testid="calendar-day-column"
138
+ onPointerDown={(e) => onPointerDown(e, selectedDate, e.currentTarget)}
139
+ onPointerMove={onPointerMove}
140
+ onPointerUp={onPointerUp}
141
+ onPointerCancel={onPointerUp}
142
+ >
143
+ {hours.map((hour, index) => (
144
+ <div
145
+ key={hour}
146
+ className="relative"
147
+ style={{ height: `${HOUR_HEIGHT_PX}px` }}
148
+ >
149
+ {index !== 0 && (
150
+ <div className="pointer-events-none absolute inset-x-0 top-0 border-b"></div>
151
+ )}
152
+
153
+ <DroppableArea
154
+ date={selectedDate}
155
+ hour={hour}
156
+ minute={0}
157
+ className="absolute inset-x-0 top-0 h-[48px]"
158
+ >
159
+ <div className="absolute inset-0" />
160
+ </DroppableArea>
161
+
162
+ <div className="pointer-events-none absolute inset-x-0 top-1/2 border-b border-dashed border-b-tertiary"></div>
163
+
164
+ <DroppableArea
165
+ date={selectedDate}
166
+ hour={hour}
167
+ minute={30}
168
+ className="absolute inset-x-0 bottom-0 h-[48px]"
169
+ >
170
+ <div className="absolute inset-0" />
171
+ </DroppableArea>
172
+ </div>
173
+ ))}
174
+
175
+ {overlayStyle ? (
176
+ <div
177
+ className="pointer-events-none absolute inset-x-1 z-10 rounded-md border border-primary/40 bg-primary/15"
178
+ data-testid="calendar-selection-overlay"
179
+ style={overlayStyle}
180
+ />
181
+ ) : null}
182
+
183
+ <div className="pointer-events-auto relative z-20">
184
+ <RenderGroupedEvents
185
+ groupedEvents={groupedEvents}
186
+ day={selectedDate}
187
+ />
188
+ </div>
189
+ </div>
190
+
191
+ <CalendarTimeline />
192
+ </div>
193
+
194
+ {draft ? (
195
+ <AddEditEventDialog
196
+ open
197
+ onOpenChange={(open) => {
198
+ if (!open) clearDraft();
199
+ }}
200
+ startDate={draft.start}
201
+ startTime={{
202
+ hour: draft.start.getHours(),
203
+ minute: draft.start.getMinutes(),
204
+ }}
205
+ endTime={{
206
+ hour: draft.end.getHours(),
207
+ minute: draft.end.getMinutes(),
208
+ }}
209
+ />
210
+ ) : null}
211
+ </div>
212
+ </ScrollArea>
213
+ </div>
214
+
215
+ <div className="hidden w-72 divide-y border-l md:block">
216
+ <DayPicker
217
+ className="mx-auto w-fit"
218
+ mode="single"
219
+ selected={selectedDate}
220
+ onSelect={(date) => date && setSelectedDate(date)}
221
+ />
222
+
223
+ <div className="flex-1 space-y-3">
224
+ {currentEvents.length > 0 ? (
225
+ <div className="flex items-start gap-2 px-4 pt-4">
226
+ <span className="relative mt-[5px] flex size-2.5">
227
+ <span className="absolute inline-flex size-full animate-ping rounded-full bg-green-400 opacity-75"></span>
228
+ <span className="relative inline-flex size-2.5 rounded-full bg-green-600"></span>
229
+ </span>
230
+
231
+ <p className="text-sm font-semibold text-t-secondary">
232
+ {t.happeningNow}
233
+ </p>
234
+ </div>
235
+ ) : (
236
+ <p className="p-4 text-center text-sm italic text-t-tertiary">
237
+ {t.noAppointments}
238
+ </p>
239
+ )}
240
+
241
+ {currentEvents.length > 0 && (
242
+ <ScrollArea className="h-[422px] px-4">
243
+ <div className="space-y-6 pb-4">
244
+ {currentEvents.map((event) => {
245
+ const user = users.find((user) => user.id === event.user.id);
246
+
247
+ return (
248
+ <div key={event.id} className="space-y-1.5">
249
+ <p className="line-clamp-2 text-sm font-semibold">
250
+ {event.title}
251
+ </p>
252
+
253
+ {user && (
254
+ <div className="flex items-center gap-1.5">
255
+ <User className="size-4 text-t-quinary" />
256
+ <span className="text-sm text-t-tertiary">
257
+ {user.name}
258
+ </span>
259
+ </div>
260
+ )}
261
+
262
+ <div className="flex items-center gap-1.5">
263
+ <Calendar className="size-4 text-t-quinary" />
264
+ <span className="text-sm text-t-tertiary">
265
+ {format(new Date(event.startDate), "MMM d, yyyy", {
266
+ locale: dateLocale,
267
+ })}
268
+ </span>
269
+ </div>
270
+
271
+ <div className="flex items-center gap-1.5">
272
+ <Clock className="size-4 text-t-quinary" />
273
+ <span className="text-sm text-t-tertiary">
274
+ {format(
275
+ parseISO(event.startDate),
276
+ use24HourFormat ? "HH:mm" : "hh:mm a",
277
+ )}{" "}
278
+ -
279
+ {format(
280
+ parseISO(event.endDate),
281
+ use24HourFormat ? "HH:mm" : "hh:mm a",
282
+ )}
283
+ </span>
284
+ </div>
285
+ </div>
286
+ );
287
+ })}
288
+ </div>
289
+ </ScrollArea>
290
+ )}
291
+ </div>
292
+ </div>
293
+ </div>
294
+ );
295
+ }
@@ -0,0 +1,35 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useCalendar } from "../../contexts/calendar-context";
3
+ import { formatTime } from "../../helpers";
4
+
5
+ export function CalendarTimeline() {
6
+ const { use24HourFormat } = useCalendar();
7
+ const [currentTime, setCurrentTime] = useState(new Date());
8
+
9
+ useEffect(() => {
10
+ const timer = setInterval(() => setCurrentTime(new Date()), 60 * 1000);
11
+ return () => clearInterval(timer);
12
+ }, []);
13
+
14
+ const getCurrentTimePosition = () => {
15
+ const minutes = currentTime.getHours() * 60 + currentTime.getMinutes();
16
+ return (minutes / 1440) * 100;
17
+ };
18
+
19
+ const formatCurrentTime = () => {
20
+ return formatTime(currentTime, use24HourFormat);
21
+ };
22
+
23
+ return (
24
+ <div
25
+ className="pointer-events-none absolute inset-x-0 z-50 border-t border-primary"
26
+ style={{ top: `${getCurrentTimePosition()}%` }}
27
+ >
28
+ <div className="absolute -left-1.5 -top-1.5 size-3 rounded-full bg-primary"></div>
29
+
30
+ <div className="absolute -left-18 flex w-16 -translate-y-1/2 justify-end bg-background pr-1 text-xs font-medium text-primary">
31
+ {formatCurrentTime()}
32
+ </div>
33
+ </div>
34
+ );
35
+ }