@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,187 @@
1
+ "use client"
2
+
3
+ import { addMinutes, format, set } from "date-fns"
4
+ import { type ReactNode, useEffect, useMemo, useState } from "react"
5
+
6
+ import { Button } from "@monkey-mini-app/ui/components/button"
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogFooter,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ } from "@monkey-mini-app/ui/components/dialog"
14
+ import { Input } from "@monkey-mini-app/ui/components/input"
15
+ import { Label } from "@monkey-mini-app/ui/components/label"
16
+ import { Textarea } from "@monkey-mini-app/ui/components/textarea"
17
+ import { DateTimePicker } from "@monkey-mini-app/ui/composites/date-time-picker"
18
+ import { useLabels } from "@monkey-mini-app/ui/i18n/context"
19
+ import { cn } from "@monkey-mini-app/ui/lib/utils"
20
+
21
+ import { COLORS } from "../constants"
22
+ import { useCalendar } from "../contexts/calendar-context"
23
+ import { useDisclosure } from "../hooks"
24
+ import type { IEvent } from "../interfaces"
25
+ import type { TEventColor } from "../types"
26
+
27
+ interface IProps {
28
+ children?: ReactNode
29
+ startDate?: Date
30
+ startTime?: { hour: number; minute: number }
31
+ endTime?: { hour: number; minute: number }
32
+ event?: IEvent
33
+ /** Controlled open (e.g. after drag-select). */
34
+ open?: boolean
35
+ onOpenChange?: (open: boolean) => void
36
+ }
37
+
38
+ export function AddEditEventDialog({
39
+ children,
40
+ startDate,
41
+ startTime,
42
+ endTime,
43
+ event,
44
+ open: openProp,
45
+ onOpenChange,
46
+ }: IProps) {
47
+ const disclosure = useDisclosure()
48
+ const isControlled = openProp !== undefined
49
+ const isOpen = isControlled ? Boolean(openProp) : disclosure.isOpen
50
+ const onClose = () => {
51
+ if (isControlled) onOpenChange?.(false)
52
+ else disclosure.onClose()
53
+ }
54
+ const onToggle = () => {
55
+ if (isControlled) onOpenChange?.(!openProp)
56
+ else disclosure.onToggle()
57
+ }
58
+ const { addEvent, updateEvent, users } = useCalendar()
59
+ const t = useLabels("eventCalendar")
60
+ const isEditing = Boolean(event)
61
+
62
+ const initial = useMemo(() => {
63
+ if (event) {
64
+ return {
65
+ title: event.title,
66
+ description: event.description,
67
+ start: new Date(event.startDate),
68
+ end: new Date(event.endDate),
69
+ color: event.color,
70
+ }
71
+ }
72
+ const base = startDate ? new Date(startDate) : new Date()
73
+ const start = startTime
74
+ ? set(base, { hours: startTime.hour, minutes: startTime.minute, seconds: 0, milliseconds: 0 })
75
+ : base
76
+ const end = endTime
77
+ ? set(base, { hours: endTime.hour, minutes: endTime.minute, seconds: 0, milliseconds: 0 })
78
+ : addMinutes(start, 30)
79
+ return {
80
+ title: "",
81
+ description: "",
82
+ start,
83
+ end: end > start ? end : addMinutes(start, 30),
84
+ color: "blue" as TEventColor,
85
+ }
86
+ }, [event, startDate, startTime, endTime])
87
+
88
+ const [title, setTitle] = useState(initial.title)
89
+ const [description, setDescription] = useState(initial.description)
90
+ const [start, setStart] = useState(initial.start)
91
+ const [end, setEnd] = useState(initial.end)
92
+ const [color, setColor] = useState<TEventColor>(initial.color)
93
+
94
+ useEffect(() => {
95
+ setTitle(initial.title)
96
+ setDescription(initial.description)
97
+ setStart(initial.start)
98
+ setEnd(initial.end)
99
+ setColor(initial.color)
100
+ }, [initial])
101
+
102
+ const save = () => {
103
+ if (!title.trim() || !start || !end) return
104
+ const payload: IEvent = {
105
+ id: isEditing && event ? event.id : Date.now(),
106
+ title: title.trim(),
107
+ description: description.trim(),
108
+ startDate: format(start, "yyyy-MM-dd'T'HH:mm:ss"),
109
+ endDate: format(end, "yyyy-MM-dd'T'HH:mm:ss"),
110
+ color,
111
+ user: event?.user ?? users[0] ?? { id: "local", name: "Me", picturePath: null },
112
+ }
113
+ if (isEditing) updateEvent(payload)
114
+ else addEvent(payload)
115
+ onClose()
116
+ }
117
+
118
+ return (
119
+ <>
120
+ {children ? (
121
+ <span
122
+ onClick={(e) => {
123
+ e.stopPropagation()
124
+ onToggle()
125
+ }}
126
+ >
127
+ {children}
128
+ </span>
129
+ ) : null}
130
+ <Dialog
131
+ open={isOpen}
132
+ onOpenChange={(next) => {
133
+ if (!next) onClose()
134
+ else if (isControlled) onOpenChange?.(true)
135
+ else disclosure.onOpen()
136
+ }}
137
+ >
138
+ <DialogContent className="sm:max-w-lg" data-testid="event-dialog">
139
+ <DialogHeader>
140
+ <DialogTitle>{isEditing ? t.editEvent : t.newEvent}</DialogTitle>
141
+ </DialogHeader>
142
+ <div className="grid gap-3">
143
+ <Input
144
+ data-testid="event-title-input"
145
+ placeholder={t.titlePlaceholder}
146
+ value={title}
147
+ onChange={(e) => setTitle(e.target.value)}
148
+ onKeyDown={(e) => {
149
+ if (e.key === "Enter") save()
150
+ }}
151
+ />
152
+ <div className="grid gap-1">
153
+ <Label>{t.start}</Label>
154
+ <DateTimePicker value={start} onChange={(d) => d && setStart(d)} />
155
+ </div>
156
+ <div className="grid gap-1">
157
+ <Label>{t.end}</Label>
158
+ <DateTimePicker value={end} onChange={(d) => d && setEnd(d)} />
159
+ </div>
160
+ <div className="flex flex-wrap gap-1">
161
+ {COLORS.map((item) => (
162
+ <button
163
+ key={item}
164
+ type="button"
165
+ className={cn(
166
+ "size-6 rounded-full border",
167
+ color === item ? "ring-2 ring-ring" : "opacity-70",
168
+ `bg-${item}-600`,
169
+ )}
170
+ style={{ backgroundColor: item }}
171
+ onClick={() => setColor(item)}
172
+ aria-label={item}
173
+ />
174
+ ))}
175
+ </div>
176
+ <Textarea placeholder={t.descriptionPlaceholder} value={description} onChange={(e) => setDescription(e.target.value)} />
177
+ </div>
178
+ <DialogFooter>
179
+ <Button data-testid="event-save" onClick={save}>
180
+ {isEditing ? t.saveChanges : t.createEvent}
181
+ </Button>
182
+ </DialogFooter>
183
+ </DialogContent>
184
+ </Dialog>
185
+ </>
186
+ )
187
+ }
@@ -0,0 +1,39 @@
1
+ import { TrashIcon } from "lucide-react"
2
+
3
+ import {
4
+ AlertDialog,
5
+ AlertDialogAction,
6
+ AlertDialogCancel,
7
+ AlertDialogContent,
8
+ AlertDialogDescription,
9
+ AlertDialogFooter,
10
+ AlertDialogHeader,
11
+ AlertDialogTitle,
12
+ AlertDialogTrigger,
13
+ } from "@monkey-mini-app/ui/components/alert-dialog"
14
+ import { useCalendar } from "../contexts/calendar-context"
15
+
16
+ export default function DeleteEventDialog({ eventId }: { eventId: number }) {
17
+ const { removeEvent } = useCalendar()
18
+ if (!eventId) return null
19
+ return (
20
+ <AlertDialog>
21
+ <AlertDialogTrigger className="inline-flex h-8 items-center gap-1.5 rounded-lg bg-destructive/10 px-2.5 text-sm text-destructive">
22
+ <TrashIcon className="size-4" />
23
+ Delete
24
+ </AlertDialogTrigger>
25
+ <AlertDialogContent>
26
+ <AlertDialogHeader>
27
+ <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
28
+ <AlertDialogDescription>
29
+ This action cannot be undone. This will permanently delete your event.
30
+ </AlertDialogDescription>
31
+ </AlertDialogHeader>
32
+ <AlertDialogFooter>
33
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
34
+ <AlertDialogAction onClick={() => removeEvent(eventId)}>Continue</AlertDialogAction>
35
+ </AlertDialogFooter>
36
+ </AlertDialogContent>
37
+ </AlertDialog>
38
+ )
39
+ }
@@ -0,0 +1,100 @@
1
+ "use client"
2
+
3
+ import { format, parseISO } from "date-fns"
4
+ import { Calendar, Clock, Text, User } from "lucide-react"
5
+ import { type ReactNode, useState } from "react"
6
+
7
+ import { Button } from "@monkey-mini-app/ui/components/button"
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ } from "@monkey-mini-app/ui/components/dialog"
14
+ import { ScrollArea } from "@monkey-mini-app/ui/components/scroll-area"
15
+
16
+ import { useCalendar } from "../contexts/calendar-context"
17
+ import { formatTime } from "../helpers"
18
+ import type { IEvent } from "../interfaces"
19
+ import { AddEditEventDialog } from "./add-edit-event-dialog"
20
+
21
+ export function EventDetailsDialog({ event, children }: { event: IEvent; children: ReactNode }) {
22
+ const [open, setOpen] = useState(false)
23
+ const startDate = parseISO(event.startDate)
24
+ const endDate = parseISO(event.endDate)
25
+ const { use24HourFormat, removeEvent } = useCalendar()
26
+
27
+ return (
28
+ <>
29
+ <span
30
+ onClick={(e) => {
31
+ e.stopPropagation()
32
+ setOpen(true)
33
+ }}
34
+ >
35
+ {children}
36
+ </span>
37
+ <Dialog open={open} onOpenChange={setOpen}>
38
+ <DialogContent>
39
+ <DialogHeader>
40
+ <DialogTitle>{event.title}</DialogTitle>
41
+ </DialogHeader>
42
+ <ScrollArea className="max-h-[80vh]">
43
+ <div className="space-y-4 p-4">
44
+ <div className="flex items-start gap-2">
45
+ <User className="mt-1 size-4 shrink-0 text-muted-foreground" />
46
+ <div>
47
+ <p className="text-sm font-medium">Responsible</p>
48
+ <p className="text-sm text-muted-foreground">{event.user.name}</p>
49
+ </div>
50
+ </div>
51
+ <div className="flex items-start gap-2">
52
+ <Calendar className="mt-1 size-4 shrink-0 text-muted-foreground" />
53
+ <div>
54
+ <p className="text-sm font-medium">Start Date</p>
55
+ <p className="text-sm text-muted-foreground">
56
+ {format(startDate, "EEEE dd MMMM")}
57
+ <span className="mx-1">at</span>
58
+ {formatTime(parseISO(event.startDate), use24HourFormat)}
59
+ </p>
60
+ </div>
61
+ </div>
62
+ <div className="flex items-start gap-2">
63
+ <Clock className="mt-1 size-4 shrink-0 text-muted-foreground" />
64
+ <div>
65
+ <p className="text-sm font-medium">End Date</p>
66
+ <p className="text-sm text-muted-foreground">
67
+ {format(endDate, "EEEE dd MMMM")}
68
+ <span className="mx-1">at</span>
69
+ {formatTime(parseISO(event.endDate), use24HourFormat)}
70
+ </p>
71
+ </div>
72
+ </div>
73
+ <div className="flex items-start gap-2">
74
+ <Text className="mt-1 size-4 shrink-0 text-muted-foreground" />
75
+ <div>
76
+ <p className="text-sm font-medium">Description</p>
77
+ <p className="text-sm text-muted-foreground">{event.description}</p>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </ScrollArea>
82
+ <div className="flex justify-end gap-2">
83
+ <AddEditEventDialog event={event}>
84
+ <Button variant="outline">Edit</Button>
85
+ </AddEditEventDialog>
86
+ <Button
87
+ variant="destructive"
88
+ onClick={() => {
89
+ removeEvent(event.id)
90
+ setOpen(false)
91
+ }}
92
+ >
93
+ Delete
94
+ </Button>
95
+ </div>
96
+ </DialogContent>
97
+ </Dialog>
98
+ </>
99
+ )
100
+ }
@@ -0,0 +1,87 @@
1
+ "use client"
2
+
3
+ import { format } from "date-fns"
4
+ import { type ReactNode, useState } from "react"
5
+
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ } from "@monkey-mini-app/ui/components/dialog"
12
+ import { cn } from "@monkey-mini-app/ui/lib/utils"
13
+
14
+ import { useCalendar } from "../contexts/calendar-context"
15
+ import { formatTime } from "../helpers"
16
+ import type { IEvent } from "../interfaces"
17
+ import { dayCellVariants } from "../views/month-view/day-cell"
18
+ import { EventBullet } from "../views/month-view/event-bullet"
19
+ import { EventDetailsDialog } from "./event-details-dialog"
20
+
21
+ export function EventListDialog({
22
+ date,
23
+ events,
24
+ maxVisibleEvents = 3,
25
+ children,
26
+ }: {
27
+ date: Date
28
+ events: IEvent[]
29
+ maxVisibleEvents?: number
30
+ children?: ReactNode
31
+ }) {
32
+ const [open, setOpen] = useState(false)
33
+ const hiddenEventsCount = Math.max(events.length - maxVisibleEvents, 0)
34
+ const { badgeVariant, use24HourFormat } = useCalendar()
35
+ const defaultTrigger = (
36
+ <span className="cursor-pointer" data-testid="calendar-more">
37
+ <span className="sm:hidden">+{hiddenEventsCount}</span>
38
+ <span className="hidden rounded-xl border px-2 py-0.5 sm:inline">+{hiddenEventsCount} more</span>
39
+ </span>
40
+ )
41
+
42
+ return (
43
+ <>
44
+ <span
45
+ onClick={(e) => {
46
+ e.stopPropagation()
47
+ setOpen(true)
48
+ }}
49
+ >
50
+ {children || defaultTrigger}
51
+ </span>
52
+ <Dialog open={open} onOpenChange={setOpen}>
53
+ <DialogContent className="sm:max-w-[425px]">
54
+ <DialogHeader>
55
+ <DialogTitle>
56
+ <div className="flex items-center gap-2">
57
+ {events[0] ? <EventBullet color={events[0].color} /> : null}
58
+ <p className="text-sm font-medium">Events on {format(date, "EEEE, MMMM d, yyyy")}</p>
59
+ </div>
60
+ </DialogTitle>
61
+ </DialogHeader>
62
+ <div className="max-h-[60vh] space-y-2 overflow-y-auto">
63
+ {events.length > 0 ? (
64
+ events.map((event) => (
65
+ <EventDetailsDialog event={event} key={event.id}>
66
+ <div
67
+ className={cn("flex cursor-pointer items-center gap-2 rounded-md border p-2 hover:bg-muted", {
68
+ [dayCellVariants({ color: event.color })]: badgeVariant === "colored",
69
+ })}
70
+ >
71
+ <EventBullet color={event.color} />
72
+ <div className="flex w-full items-center justify-between">
73
+ <p className="text-sm font-medium">{event.title}</p>
74
+ <p className="text-xs">{formatTime(event.startDate, use24HourFormat)}</p>
75
+ </div>
76
+ </div>
77
+ </EventDetailsDialog>
78
+ ))
79
+ ) : (
80
+ <p className="text-sm text-muted-foreground">No events for this date.</p>
81
+ )}
82
+ </div>
83
+ </DialogContent>
84
+ </Dialog>
85
+ </>
86
+ )
87
+ }
@@ -0,0 +1,45 @@
1
+ import { motion } from "../shims/motion";
2
+ import type React from "react";
3
+ import type { ReactNode } from "react";
4
+ import { useDragDrop } from "../contexts/dnd-context";
5
+ import type { IEvent } from "../interfaces";
6
+
7
+ interface DraggableEventProps {
8
+ event: IEvent;
9
+ children: ReactNode;
10
+ className?: string;
11
+ }
12
+
13
+ export function DraggableEvent({
14
+ event,
15
+ children,
16
+ className,
17
+ }: DraggableEventProps) {
18
+ const { startDrag, endDrag, isDragging, draggedEvent } = useDragDrop();
19
+
20
+ const isCurrentlyDragged = isDragging && draggedEvent?.id === event.id;
21
+
22
+ const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
23
+ e.stopPropagation();
24
+ };
25
+
26
+ return (
27
+ <motion.div
28
+ className={`${className || ""} ${isCurrentlyDragged ? "opacity-50 cursor-grabbing" : "cursor-grab"}`}
29
+ draggable
30
+ onClick={(e: React.MouseEvent<HTMLDivElement>) => handleClick(e)}
31
+ onDragStart={(e: React.DragEvent<HTMLDivElement>) => {
32
+ e.dataTransfer.setData(
33
+ "text/plain",
34
+ event.id.toString(),
35
+ );
36
+ startDrag(event);
37
+ }}
38
+ onDragEnd={() => {
39
+ endDrag();
40
+ }}
41
+ >
42
+ {children}
43
+ </motion.div>
44
+ );
45
+ }
@@ -0,0 +1,45 @@
1
+ import { type ReactNode } from "react";
2
+ import { useDragDrop } from "../contexts/dnd-context";
3
+
4
+ interface DroppableAreaProps {
5
+ date: Date;
6
+ hour?: number;
7
+ minute?: number;
8
+ children: ReactNode;
9
+ className?: string;
10
+ }
11
+
12
+ export function DroppableArea({
13
+ date,
14
+ hour,
15
+ minute,
16
+ children,
17
+ className,
18
+ }: DroppableAreaProps) {
19
+ const { handleEventDrop, isDragging } = useDragDrop();
20
+
21
+ return (
22
+ <div
23
+ // biome-ignore lint/a11y/useSemanticElements: Using gridcell role for CSS grid layout where td is not applicable
24
+ role="gridcell"
25
+ aria-label="Droppable area"
26
+ tabIndex={-1}
27
+ className={`${className || ""} ${isDragging ? "drop-target" : ""}`}
28
+ onDragOver={(e) => {
29
+ // Prevent default to allow drop
30
+ e.preventDefault();
31
+ e.currentTarget.classList.add("bg-primary/10");
32
+ }}
33
+ onDragLeave={(e) => {
34
+ e.currentTarget.classList.remove("bg-primary/10");
35
+ }}
36
+ onDrop={(e) => {
37
+ e.preventDefault();
38
+ e.currentTarget.classList.remove("bg-primary/10");
39
+ handleEventDrop(date, hour, minute);
40
+ }}
41
+ >
42
+ {children}
43
+ </div>
44
+ );
45
+ }
@@ -0,0 +1,179 @@
1
+ "use client";
2
+
3
+ import {
4
+ addMinutes,
5
+ differenceInMinutes,
6
+ format,
7
+ isAfter,
8
+ isBefore,
9
+ parseISO,
10
+ } from "date-fns";
11
+ import { motion } from "../shims/motion";
12
+ import { Resizable, type ResizeCallback } from "../shims/re-resizable";
13
+ import type React from "react";
14
+ import { useCallback, useMemo, useState } from "react";
15
+ import { cn } from "@monkey-mini-app/ui/lib/utils";
16
+ import { useCalendar } from "../contexts/calendar-context";
17
+
18
+ import type { IEvent } from "../interfaces";
19
+
20
+ interface ResizableEventBlockProps {
21
+ event: IEvent;
22
+ children: React.ReactNode;
23
+ className?: string;
24
+ }
25
+
26
+ const PIXELS_PER_HOUR = 96;
27
+ const MINUTES_PER_PIXEL = 60 / PIXELS_PER_HOUR;
28
+ const MIN_DURATION = 15; // in minutes
29
+
30
+ export function ResizableEvent({
31
+ event,
32
+ children,
33
+ className,
34
+ }: ResizableEventBlockProps) {
35
+ const { updateEvent, use24HourFormat } = useCalendar();
36
+
37
+ const [isResizing, setIsResizing] = useState(false);
38
+ const [resizePreview, setResizePreview] = useState<{
39
+ start: string;
40
+ end: string;
41
+ } | null>(null);
42
+
43
+ const start = useMemo(() => parseISO(event.startDate), [event.startDate]);
44
+ const end = useMemo(() => parseISO(event.endDate), [event.endDate]);
45
+ const durationInMinutes = useMemo(
46
+ () => differenceInMinutes(end, start),
47
+ [start, end],
48
+ );
49
+
50
+ const resizeBoundaries = useMemo(() => {
51
+ const dayStart = new Date(start);
52
+ dayStart.setHours(0, 0, 0, 0);
53
+
54
+ const dayEnd = new Date(start);
55
+ dayEnd.setHours(23, 59, 59, 999);
56
+
57
+ return { dayStart, dayEnd };
58
+ }, [start]);
59
+
60
+ const handleResizeStart = useCallback(() => {
61
+ setIsResizing(true);
62
+ }, []);
63
+
64
+ const handleResize: ResizeCallback = useCallback(
65
+ (_, direction, ref) => {
66
+ const newHeight = parseInt(ref.style.height, 10);
67
+ const newDuration = Math.max(
68
+ MIN_DURATION,
69
+ Math.round((newHeight + 8) * MINUTES_PER_PIXEL),
70
+ );
71
+ const delta = newDuration - durationInMinutes;
72
+
73
+ let newStart = start;
74
+ let newEnd = end;
75
+
76
+ if (direction.includes("top")) {
77
+ newStart = addMinutes(start, -delta);
78
+ } else if (direction.includes("bottom")) {
79
+ newEnd = addMinutes(end, delta);
80
+ }
81
+
82
+ if (isBefore(newStart, resizeBoundaries.dayStart)) {
83
+ newStart = resizeBoundaries.dayStart;
84
+ }
85
+ if (isAfter(newEnd, resizeBoundaries.dayEnd)) {
86
+ newEnd = resizeBoundaries.dayEnd;
87
+ }
88
+
89
+ setResizePreview({
90
+ start: format(newStart, use24HourFormat ? "HH:mm" : "h:mm a"),
91
+ end: format(newEnd, use24HourFormat ? "HH:mm" : "h:mm a"),
92
+ });
93
+
94
+ updateEvent({
95
+ ...event,
96
+ startDate: newStart.toISOString(),
97
+ endDate: newEnd.toISOString(),
98
+ });
99
+ },
100
+ [
101
+ start,
102
+ end,
103
+ durationInMinutes,
104
+ resizeBoundaries,
105
+ use24HourFormat,
106
+ updateEvent,
107
+ event,
108
+ ],
109
+ );
110
+
111
+ const handleResizeStop = useCallback(() => {
112
+ setIsResizing(false);
113
+ setResizePreview(null);
114
+ }, []);
115
+
116
+ const resizeConfig = useMemo(
117
+ () => ({
118
+ minHeight: 15,
119
+ maxHeight: 1440,
120
+ enable: {
121
+ top: true,
122
+ bottom: true,
123
+ topRight: false,
124
+ bottomRight: false,
125
+ bottomLeft: false,
126
+ topLeft: false,
127
+ },
128
+ handleStyles: {
129
+ top: {
130
+ cursor: "ns-resize",
131
+ height: "8px",
132
+ top: "-4px",
133
+ backgroundColor: "transparent",
134
+ },
135
+ bottom: {
136
+ cursor: "ns-resize",
137
+ height: "8px",
138
+ bottom: "-4px",
139
+ backgroundColor: "transparent",
140
+ },
141
+ },
142
+ handleClasses: {
143
+ top: "transition-colors rounded-sm",
144
+ bottom: "transition-colors rounded-sm",
145
+ },
146
+ onResizeStart: handleResizeStart,
147
+ onResize: handleResize,
148
+ onResizeStop: handleResizeStop,
149
+ className: cn(
150
+ "transition-all duration-200",
151
+ isResizing && "z-50 shadow-lg",
152
+ ),
153
+ }),
154
+ [handleResizeStart, handleResize, handleResizeStop, isResizing],
155
+ );
156
+
157
+ return (
158
+ <motion.div
159
+ initial={{ opacity: 0, scale: 0.95 }}
160
+ animate={{ opacity: 1, scale: 1 }}
161
+ exit={{ opacity: 0, scale: 0.95 }}
162
+ transition={{ duration: 0.2 }}
163
+ className={cn("relative group", className)}
164
+ >
165
+ <Resizable {...resizeConfig}>{children}</Resizable>
166
+
167
+ {isResizing && resizePreview && (
168
+ <motion.div
169
+ initial={{ opacity: 0, y: -10 }}
170
+ animate={{ opacity: 1, y: 0 }}
171
+ exit={{ opacity: 0, y: -10 }}
172
+ className="absolute -top-8 left-1/2 transform -translate-x-1/2 bg-gray-900 text-white text-xs px-2 py-1 rounded shadow-lg z-50 whitespace-nowrap"
173
+ >
174
+ {resizePreview.start} - {resizePreview.end}
175
+ </motion.div>
176
+ )}
177
+ </motion.div>
178
+ );
179
+ }