@arbidocs/blocks 0.4.137 → 0.4.139

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/dist/agents/definitions/AgentConnectInstructions.d.ts +12 -0
  2. package/dist/agents/definitions/defaults.d.ts +12 -0
  3. package/dist/agents/definitions/index.d.ts +2 -0
  4. package/dist/agents/index.cjs +457 -418
  5. package/dist/agents/index.js +12 -13
  6. package/dist/agents/skills/SkillLibraryDetail.d.ts +26 -0
  7. package/dist/agents/skills/index.d.ts +2 -0
  8. package/dist/agents/skills/skillLibrary.d.ts +46 -0
  9. package/dist/calendar/CalendarFormDialog.d.ts +13 -0
  10. package/dist/calendar/ProposeTimeDialog.d.ts +22 -0
  11. package/dist/calendar/WeeklyHoursEditor.d.ts +7 -0
  12. package/dist/calendar/access-setup.d.ts +8 -0
  13. package/dist/calendar/api-mapping.d.ts +22 -0
  14. package/dist/calendar/booking-form.d.ts +46 -0
  15. package/dist/calendar/conference.d.ts +11 -0
  16. package/dist/calendar/errors.d.ts +7 -0
  17. package/dist/calendar/feeds.d.ts +2 -0
  18. package/dist/calendar/freebusy.d.ts +22 -0
  19. package/dist/calendar/ics.d.ts +33 -0
  20. package/dist/calendar/index.cjs +472 -4293
  21. package/dist/calendar/index.cjs.map +1 -1
  22. package/dist/calendar/index.d.ts +17 -1
  23. package/dist/calendar/index.js +1 -4227
  24. package/dist/calendar/index.js.map +1 -1
  25. package/dist/calendar/requests.d.ts +42 -0
  26. package/dist/calendar/room-window.d.ts +6 -0
  27. package/dist/calendar/settings-value.d.ts +4 -0
  28. package/dist/calls/index.cjs +4 -4
  29. package/dist/calls/index.js +3 -3
  30. package/dist/chat/index.cjs +141 -141
  31. package/dist/chat/index.js +6 -6
  32. package/dist/{chunk-M33QUNFH.js → chunk-22WFGXG2.js} +3 -3
  33. package/dist/{chunk-M33QUNFH.js.map → chunk-22WFGXG2.js.map} +1 -1
  34. package/dist/{chunk-VMESMX7N.js → chunk-453PNNNW.js} +3 -3
  35. package/dist/{chunk-VMESMX7N.js.map → chunk-453PNNNW.js.map} +1 -1
  36. package/dist/{chunk-2KUCVSFF.js → chunk-45LRMW54.js} +3 -3
  37. package/dist/{chunk-2KUCVSFF.js.map → chunk-45LRMW54.js.map} +1 -1
  38. package/dist/chunk-5FTIHRZH.cjs +177 -0
  39. package/dist/chunk-5FTIHRZH.cjs.map +1 -0
  40. package/dist/{chunk-FUTMU4FR.js → chunk-5HFPRI3W.js} +3 -3
  41. package/dist/{chunk-FUTMU4FR.js.map → chunk-5HFPRI3W.js.map} +1 -1
  42. package/dist/{chunk-JY73DJOS.cjs → chunk-5UXDUOOB.cjs} +84 -1211
  43. package/dist/chunk-5UXDUOOB.cjs.map +1 -0
  44. package/dist/{chunk-ATJUVHVB.js → chunk-6QMWP5IU.js} +1445 -11
  45. package/dist/chunk-6QMWP5IU.js.map +1 -0
  46. package/dist/chunk-6YTHQ73P.cjs +5334 -0
  47. package/dist/chunk-6YTHQ73P.cjs.map +1 -0
  48. package/dist/{chunk-KMNZ2RVO.js → chunk-7QUUZBQZ.js} +15 -1109
  49. package/dist/chunk-7QUUZBQZ.js.map +1 -0
  50. package/dist/{chunk-C3ZRLULA.cjs → chunk-7X255TOM.cjs} +5 -5
  51. package/dist/{chunk-C3ZRLULA.cjs.map → chunk-7X255TOM.cjs.map} +1 -1
  52. package/dist/chunk-A3XPS2EU.js +245 -0
  53. package/dist/chunk-A3XPS2EU.js.map +1 -0
  54. package/dist/chunk-B3AHCIV5.js +5206 -0
  55. package/dist/chunk-B3AHCIV5.js.map +1 -0
  56. package/dist/{chunk-2Q7FDZAY.js → chunk-BPBSSMRB.js} +5 -163
  57. package/dist/chunk-BPBSSMRB.js.map +1 -0
  58. package/dist/{chunk-KSHKTOY2.js → chunk-CKCBBYTA.js} +205 -5
  59. package/dist/chunk-CKCBBYTA.js.map +1 -0
  60. package/dist/chunk-CYNF5PZI.js +170 -0
  61. package/dist/chunk-CYNF5PZI.js.map +1 -0
  62. package/dist/{chunk-OPTBQSCV.cjs → chunk-DJM7DLCT.cjs} +3 -3
  63. package/dist/{chunk-OPTBQSCV.cjs.map → chunk-DJM7DLCT.cjs.map} +1 -1
  64. package/dist/{chunk-E5OYHJMU.js → chunk-DN55PRGS.js} +4 -4
  65. package/dist/{chunk-E5OYHJMU.js.map → chunk-DN55PRGS.js.map} +1 -1
  66. package/dist/{chunk-VHJHY7TT.js → chunk-ECERC6TI.js} +3 -3
  67. package/dist/{chunk-VHJHY7TT.js.map → chunk-ECERC6TI.js.map} +1 -1
  68. package/dist/{chunk-KDUGTKVI.cjs → chunk-GFNELPDP.cjs} +17 -6
  69. package/dist/chunk-GFNELPDP.cjs.map +1 -0
  70. package/dist/{chunk-OGBXV326.cjs → chunk-GFRQG36Y.cjs} +2 -243
  71. package/dist/chunk-GFRQG36Y.cjs.map +1 -0
  72. package/dist/{chunk-WXYQ66KC.cjs → chunk-HEMU73OP.cjs} +2 -166
  73. package/dist/chunk-HEMU73OP.cjs.map +1 -0
  74. package/dist/{chunk-UUHUQHXR.cjs → chunk-JZNXRMLH.cjs} +6 -6
  75. package/dist/{chunk-UUHUQHXR.cjs.map → chunk-JZNXRMLH.cjs.map} +1 -1
  76. package/dist/{chunk-HDYWXEQU.cjs → chunk-K3HNLWZZ.cjs} +8 -8
  77. package/dist/{chunk-HDYWXEQU.cjs.map → chunk-K3HNLWZZ.cjs.map} +1 -1
  78. package/dist/{chunk-7M4C6CK7.js → chunk-KTFDUSON.js} +4 -4
  79. package/dist/{chunk-7M4C6CK7.js.map → chunk-KTFDUSON.js.map} +1 -1
  80. package/dist/{chunk-HLB4DIYV.cjs → chunk-MABUMUCA.cjs} +6 -6
  81. package/dist/{chunk-HLB4DIYV.cjs.map → chunk-MABUMUCA.cjs.map} +1 -1
  82. package/dist/{chunk-RQ2Y7USX.cjs → chunk-MIDXCYZB.cjs} +235 -15
  83. package/dist/chunk-MIDXCYZB.cjs.map +1 -0
  84. package/dist/{chunk-L3T7OJRX.js → chunk-MTPWZQTL.js} +4 -4
  85. package/dist/{chunk-L3T7OJRX.js.map → chunk-MTPWZQTL.js.map} +1 -1
  86. package/dist/{chunk-BUNZWOPX.cjs → chunk-O4ZIQDUT.cjs} +5 -5
  87. package/dist/{chunk-BUNZWOPX.cjs.map → chunk-O4ZIQDUT.cjs.map} +1 -1
  88. package/dist/{chunk-W3VYZXNS.js → chunk-O6W6RW3B.js} +5 -240
  89. package/dist/chunk-O6W6RW3B.js.map +1 -0
  90. package/dist/{chunk-WS5EX7DN.cjs → chunk-OGP6MQXQ.cjs} +3 -3
  91. package/dist/{chunk-WS5EX7DN.cjs.map → chunk-OGP6MQXQ.cjs.map} +1 -1
  92. package/dist/{chunk-6F7SZCVL.cjs → chunk-OWCEQKAR.cjs} +3 -3
  93. package/dist/{chunk-6F7SZCVL.cjs.map → chunk-OWCEQKAR.cjs.map} +1 -1
  94. package/dist/{chunk-OQ7RUNOC.js → chunk-PC5KVHX3.js} +22 -6
  95. package/dist/chunk-PC5KVHX3.js.map +1 -0
  96. package/dist/{chunk-CHVCTKF4.cjs → chunk-ULEDHANM.cjs} +1543 -66
  97. package/dist/chunk-ULEDHANM.cjs.map +1 -0
  98. package/dist/{chunk-L2PAPOZ5.js → chunk-US3S4DCB.js} +17 -6
  99. package/dist/chunk-US3S4DCB.js.map +1 -0
  100. package/dist/chunk-V46W7YLI.cjs +252 -0
  101. package/dist/chunk-V46W7YLI.cjs.map +1 -0
  102. package/dist/{chunk-3RJN2VTL.js → chunk-XDQXDWE7.js} +3 -3
  103. package/dist/{chunk-3RJN2VTL.js.map → chunk-XDQXDWE7.js.map} +1 -1
  104. package/dist/{chunk-EHOPZBAX.cjs → chunk-XOC44HQY.cjs} +12 -12
  105. package/dist/{chunk-EHOPZBAX.cjs.map → chunk-XOC44HQY.cjs.map} +1 -1
  106. package/dist/{chunk-YOMPLCP6.js → chunk-Y5KTM3V5.js} +3 -3
  107. package/dist/{chunk-YOMPLCP6.js.map → chunk-Y5KTM3V5.js.map} +1 -1
  108. package/dist/{chunk-DWZXSPMB.cjs → chunk-ZAVEMQ37.cjs} +3 -3
  109. package/dist/{chunk-DWZXSPMB.cjs.map → chunk-ZAVEMQ37.cjs.map} +1 -1
  110. package/dist/{chunk-AAFX64WD.cjs → chunk-ZXAWOLBQ.cjs} +22 -5
  111. package/dist/chunk-ZXAWOLBQ.cjs.map +1 -0
  112. package/dist/citations/QuietMarks.d.ts +5 -2
  113. package/dist/citations/index.cjs +15 -14
  114. package/dist/citations/index.js +3 -2
  115. package/dist/collaboration/index.cjs +51 -51
  116. package/dist/collaboration/index.js +3 -3
  117. package/dist/connect/docx.cjs +6 -6
  118. package/dist/connect/docx.js +5 -5
  119. package/dist/connect/index.cjs +6595 -1600
  120. package/dist/connect/index.cjs.map +1 -1
  121. package/dist/connect/index.d.ts +5 -0
  122. package/dist/connect/index.js +5064 -90
  123. package/dist/connect/index.js.map +1 -1
  124. package/dist/connection/ConnectedAgentCreator.d.ts +37 -0
  125. package/dist/connection/ConnectedSkillsLibrary.d.ts +26 -0
  126. package/dist/connection/ConnectedWorkflowHistory.d.ts +45 -0
  127. package/dist/connection/ConnectionProvider.d.ts +3 -1
  128. package/dist/connection/calendar/AddCalendarDialogs.d.ts +9 -0
  129. package/dist/connection/calendar/BookingPagesPanel.d.ts +13 -0
  130. package/dist/connection/calendar/CalendarAccountsPanel.d.ts +5 -0
  131. package/dist/connection/calendar/CalendarSettingsDialog.d.ts +12 -0
  132. package/dist/connection/calendar/ConnectedCalendar.d.ts +13 -0
  133. package/dist/connection/calendar/ConnectedMailInviteCard.d.ts +12 -0
  134. package/dist/connection/calendar/ConnectedPublicBooking.d.ts +9 -0
  135. package/dist/connection/calendar/ConnectedUpcomingEvents.d.ts +12 -0
  136. package/dist/connection/calendar/CopyField.d.ts +9 -0
  137. package/dist/connection/calendar/EventEditorDialog.d.ts +29 -0
  138. package/dist/connection/calendar/EventScheduling.d.ts +8 -0
  139. package/dist/connection/calendar/InvitationsPanel.d.ts +5 -0
  140. package/dist/connection/calendar/OpenAccessPanel.d.ts +7 -0
  141. package/dist/connection/calendar/PublishCalendarDialog.d.ts +8 -0
  142. package/dist/connection/calendar/RoomsDirectoryPanel.d.ts +8 -0
  143. package/dist/connection/calendar/ShareCalendarDialog.d.ts +6 -0
  144. package/dist/connection/calendar/copy.d.ts +2 -0
  145. package/dist/connection/calendar/index.d.ts +6 -0
  146. package/dist/connection/calendar/store.d.ts +47 -0
  147. package/dist/connection/calendar/types.d.ts +36 -0
  148. package/dist/connection/calendar/useWebinarAttendee.d.ts +3 -0
  149. package/dist/connection/notification-center/ConnectedNotificationCenter.d.ts +15 -0
  150. package/dist/connection/notification-center/NotificationCenter.d.ts +37 -0
  151. package/dist/connection/notification-center/NotificationEvent.d.ts +33 -0
  152. package/dist/connection/notification-center/index.d.ts +4 -0
  153. package/dist/connection/notification-center/items.d.ts +44 -0
  154. package/dist/document-tools/index.cjs +1 -1
  155. package/dist/document-tools/index.js +1 -1
  156. package/dist/facts/index.cjs +6 -6
  157. package/dist/facts/index.js +6 -6
  158. package/dist/files/index.cjs +6 -6
  159. package/dist/files/index.js +6 -6
  160. package/dist/home/index.cjs +20 -20
  161. package/dist/home/index.js +5 -5
  162. package/dist/index.cjs +969 -928
  163. package/dist/index.cjs.map +1 -1
  164. package/dist/index.js +26 -25
  165. package/dist/index.js.map +1 -1
  166. package/dist/retrieval/index.cjs +1 -1
  167. package/dist/retrieval/index.js +1 -1
  168. package/dist/settings/index.cjs +58 -72
  169. package/dist/settings/index.cjs.map +1 -1
  170. package/dist/settings/index.js +6 -23
  171. package/dist/settings/index.js.map +1 -1
  172. package/dist/shell/index.cjs +61 -60
  173. package/dist/shell/index.js +5 -4
  174. package/dist/site/index.cjs +5 -5
  175. package/dist/site/index.js +1 -1
  176. package/dist/studio/index.cjs +11 -11
  177. package/dist/studio/index.js +9 -9
  178. package/dist/workflow-canvas/index.cjs +69 -70
  179. package/dist/workflow-canvas/index.cjs.map +1 -1
  180. package/dist/workflow-canvas/index.js +14 -15
  181. package/dist/workflow-canvas/index.js.map +1 -1
  182. package/dist/workspaces/index.cjs +2 -2
  183. package/dist/workspaces/index.js +2 -2
  184. package/node_modules/@arbidocs/coedit/package.json +1 -1
  185. package/package.json +4 -4
  186. package/dist/chunk-2Q7FDZAY.js.map +0 -1
  187. package/dist/chunk-AAFX64WD.cjs.map +0 -1
  188. package/dist/chunk-ATJUVHVB.js.map +0 -1
  189. package/dist/chunk-CHVCTKF4.cjs.map +0 -1
  190. package/dist/chunk-CQCC7LE7.js +0 -210
  191. package/dist/chunk-CQCC7LE7.js.map +0 -1
  192. package/dist/chunk-JY73DJOS.cjs.map +0 -1
  193. package/dist/chunk-KDUGTKVI.cjs.map +0 -1
  194. package/dist/chunk-KMNZ2RVO.js.map +0 -1
  195. package/dist/chunk-KSHKTOY2.js.map +0 -1
  196. package/dist/chunk-L2PAPOZ5.js.map +0 -1
  197. package/dist/chunk-OGBXV326.cjs.map +0 -1
  198. package/dist/chunk-OQ7RUNOC.js.map +0 -1
  199. package/dist/chunk-RFIBUWLF.cjs +0 -232
  200. package/dist/chunk-RFIBUWLF.cjs.map +0 -1
  201. package/dist/chunk-RQ2Y7USX.cjs.map +0 -1
  202. package/dist/chunk-W3VYZXNS.js.map +0 -1
  203. package/dist/chunk-WXYQ66KC.cjs.map +0 -1
@@ -1,4232 +1,6 @@
1
1
  "use client";
2
- import { safeZone, isValidTimeZone, utcToDate, addDays, convertWall, utcToTime, zoneOffsetLabel, formatInZone, zoneCity, todayInZone, startOfWeek, formatPlainDate, addMonths, isoWeekNumber, diffDays, utcToWall, listTimeZones, zoneOffsetMinutes, wallToEpochMs, dayOfWeekSun0, detectTimeZone, wallArrayOffset, epochToWallArray, wallToUtc } from '../chunk-GTCC2VAR.js';
2
+ export { ARBI_WEBINAR_LABEL, CALENDAR_COLORS, CALENDAR_DEFAULT_LABELS, CALENDAR_NARROW_WIDTH, CalendarFormDialog, CalendarGrid, CalendarList, CalendarPrint, CalendarSettingsForm, DEFAULT_EVENT_COLOR, DEFAULT_INTERVAL, DEFAULT_RECURRENCE_DESCRIBE_LABELS, DEFAULT_RECURRENCE_EDITOR_LABELS, DEFAULT_REMINDER_FORMAT_LABELS, EventDetails, EventEditor, JOIN_EARLY_MS, MiniMonth, OVERLAY_COLORS, PENDING_EVENT_COLOR, ProposeTimeDialog, REMINDER_PRESETS, RRULE_WEEKDAYS, RecurrenceEditor, RoomFinder, SchedulingAssistant, ScopeDialog, TimeZonePicker, WEEKDAY_NAMES, WeeklyHoursEditor, allDayInclusiveEnd, applySuggestion, attendeeKey, bookingLink, bookingToForm, buildCreateBody, buildDeleteTarget, buildEditRequest, buildGridEvents, buildRRule, buildTimeChangeRequest, calendarGroup, calendarToListItem, canJoinEvent, conferenceUrl, contrastText, davHost, dayWindow, defaultRecurrenceForm, defaultRoomWindow, defaultWeeklyHours, describeCalendarError, describeRecurrence, diffDraft, draftFromSlot, draftRangeUtc, effectiveCalendarView, emptyBookingForm, eventDays, eventStateClasses, eventToDraft, eventToGrid, eventsByDay, formToBookingBody, formatBusy, formatDayLabel, formatEventWhen, formatReminder, formatSlotRange, formatSlotTime, formatZoneLabel, freeBusyToRows, getTemporalTimeZonePlugin, groupSlotsByDay, hourTicks, icsProps, isAllDayDraft, isCalendarAttachment, isWebinarConference, needsScope, newEventDraft, normalizeFeedUrl, outsideIntervals, overlayToGrid, parseDurations, parseIcsInvite, parseOverlayLink, parseRRule, pendingCreateToGrid, placeIntervals, printCalendar, printRange, publicBookingMessage, reminderToMinutes, scopedTarget, setDraftAllDay, setDraftEnd, setDraftStart, setPosOf, setupSteps, slugify, specDate, specTime, splitReminderMinutes, toBusinessHours, toSettingsValue, toSuggestionViews, upcomingOf, validateBookingForm, validateDraft, validateWeekly, weekdayMon0, weekdayOf, weeklyFromApi, weeklyToPayload, workWeekHiddenDays, workingIntervals, zoneOptions } from '../chunk-B3AHCIV5.js';
3
3
  export { addDays, addMonths, convertWall, detectTimeZone, diffDays, formatInZone, formatPlainDate, isValidTimeZone, listTimeZones, safeZone, startOfWeek, todayInZone, utcToDate, utcToTime, utcToWall, wallToUtc, zoneCity, zoneOffsetLabel, zoneOffsetMinutes } from '../chunk-GTCC2VAR.js';
4
4
  import '../chunk-VUNV25KB.js';
5
- import { forwardRef, useMemo, useRef, useState, useCallback, useEffect, useId } from 'react';
6
- import FullCalendar from '@fullcalendar/react';
7
- import dayGridPlugin from '@fullcalendar/daygrid';
8
- import timeGridPlugin from '@fullcalendar/timegrid';
9
- import listPlugin from '@fullcalendar/list';
10
- import interactionPlugin from '@fullcalendar/interaction';
11
- import multiMonthPlugin from '@fullcalendar/multimonth';
12
- import { ChevronLeft, ChevronRight, Plus, Lightbulb, Workflow, ListTodo, Lock, EllipsisVertical, Globe, ChevronDown, Check, Clock, Repeat, CalendarDays, X, Video, MapPin, Users, TextAlignStart, Bell, Tag, Palette, Paperclip, Trash2, Pencil, CircleCheck, CircleX, DoorOpen, User, Sparkles } from 'lucide-react';
13
- import { cn, Button, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, Input, Popover, PopoverTrigger, PopoverContent, Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, Switch, Textarea, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@arbidocs/react/ui';
14
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
15
- import { createPlugin } from '@fullcalendar/core';
16
- import { NamedTimeZoneImpl } from '@fullcalendar/core/internal';
17
- import { Temporal } from 'temporal-polyfill';
18
- import { flushSync } from 'react-dom';
19
- import { createRoot } from 'react-dom/client';
20
-
21
- // src/calendar/labels.ts
22
- var CALENDAR_DEFAULT_LABELS = {
23
- today: "Today",
24
- view: "View",
25
- previous: "Previous",
26
- next: "Next",
27
- day: "Day",
28
- week: "Week",
29
- workWeek: "Work week",
30
- month: "Month",
31
- agenda: "Agenda",
32
- year: "Year",
33
- allDay: "All day",
34
- busy: "Busy",
35
- noTitle: "(No title)",
36
- noEvents: "No events",
37
- weekAbbrev: "W",
38
- more: "more",
39
- newEvent: "New event",
40
- myCalendars: "My calendars",
41
- sharedCalendars: "Shared with me",
42
- workspaceCalendars: "Workspace calendars",
43
- subscribedCalendars: "Subscribed",
44
- overlays: "Also show",
45
- facts: "Dated facts",
46
- workflows: "Workflows",
47
- tasks: "Tasks",
48
- createCalendar: "New calendar",
49
- editCalendar: "Settings and sharing",
50
- importCalendar: "Import .ics",
51
- exportCalendar: "Export .ics",
52
- deleteCalendar: "Delete calendar",
53
- locked: "Locked calendar",
54
- showCalendar: "Show",
55
- hideCalendar: "Hide",
56
- calendarColor: "Colour",
57
- title: "Title",
58
- titlePlaceholder: "Add a title",
59
- calendar: "Calendar",
60
- start: "Start",
61
- end: "End",
62
- startTime: "Start time",
63
- endTime: "End time",
64
- arbiCall: "ARBI call",
65
- arbiWebinar: "ARBI webinar",
66
- startTimeZone: "Start time zone",
67
- endTimeZone: "End time zone",
68
- separateTimeZones: "Time zones",
69
- repeat: "Repeat",
70
- location: "Location",
71
- description: "Description",
72
- attendees: "Guests",
73
- addAttendee: "Add guests by email",
74
- required: "Required",
75
- optional: "Optional",
76
- showAs: "Show as",
77
- sensitivity: "Visibility",
78
- categories: "Categories",
79
- addCategory: "Add a category",
80
- color: "Colour",
81
- defaultColor: "Calendar colour",
82
- reminders: "Reminders",
83
- addReminder: "Add reminder",
84
- useDefaultReminders: "Use calendar default reminders",
85
- conference: "Video call",
86
- addArbiCall: "Add ARBI call",
87
- addArbiWebinar: "Add ARBI webinar",
88
- webinar: "Webinar",
89
- webinarLinkOnSave: "The link is created when you save",
90
- webinarRole: "Webinar role",
91
- cohost: "Co-host",
92
- panelist: "Panelist",
93
- webinarAttendee: "Attendee",
94
- joinWebinar: "Join webinar",
95
- addMeetingLink: "Add meeting link",
96
- meetingLinkPlaceholder: "https://\u2026",
97
- removeConference: "Remove video call",
98
- attachments: "Attachments",
99
- addAttachment: "Add link",
100
- attachmentUrlPlaceholder: "https://\u2026",
101
- attachmentTitlePlaceholder: "Title (optional)",
102
- status: "Status",
103
- save: "Save",
104
- saving: "Saving\u2026",
105
- cancel: "Cancel",
106
- delete: "Delete",
107
- edit: "Edit",
108
- remove: "Remove",
109
- createTitle: "New event",
110
- editTitle: "Edit event",
111
- minutes: "minutes",
112
- hours: "hours",
113
- days: "days",
114
- weeks: "weeks",
115
- beforeStart: "before",
116
- popup: "Notification",
117
- email: "Email",
118
- atStart: "At time of event",
119
- free: "Free",
120
- tentative: "Tentative",
121
- oof: "Out of office",
122
- workingElsewhere: "Working elsewhere",
123
- normal: "Default visibility",
124
- private: "Private",
125
- confidential: "Confidential",
126
- confirmed: "Confirmed",
127
- cancelled: "Cancelled",
128
- join: "Join",
129
- openMeetingChat: "Open meeting chat",
130
- accept: "Yes",
131
- decline: "No",
132
- maybe: "Maybe",
133
- going: "Going?",
134
- organizer: "Organiser",
135
- needsAction: "Awaiting reply",
136
- accepted: "Accepted",
137
- declined: "Declined",
138
- delegated: "Delegated",
139
- recurring: "Repeats",
140
- scopeEditTitle: "Edit recurring event",
141
- scopeDeleteTitle: "Delete recurring event",
142
- scopeThis: "This event",
143
- scopeFollowing: "This and following events",
144
- scopeAll: "All events",
145
- settingsTitle: "Calendar settings",
146
- timeZone: "Time zone",
147
- secondaryTimeZone: "Secondary time zone",
148
- none: "None",
149
- weekStart: "Week starts on",
150
- workingHours: "Working hours",
151
- defaultView: "Default view",
152
- defaultDuration: "Default event length",
153
- defaultCalendar: "Default calendar",
154
- showDeclined: "Show declined events",
155
- showWeekNumbers: "Show week numbers",
156
- autoAddInvitations: "Add invitations to my calendar",
157
- autoAddAll: "From everyone",
158
- autoAddKnown: "Only from people I know",
159
- autoAddNone: "Only after I respond",
160
- declineMessage: "Default decline message",
161
- searchTimeZones: "Search time zones",
162
- invalidRange: "The end must be after the start.",
163
- titleRequired: "Add a title.",
164
- removeFromCalendar: "Remove from calendar",
165
- proposeNewTime: "Propose new time",
166
- scopeRespondTitle: "Respond to a recurring event",
167
- send: "Send",
168
- focusTime: "Focus time",
169
- autoDecline: "Decline new invitations",
170
- declineMessagePlaceholder: "Message for declined invitations (optional)",
171
- schedulingAssistant: "Scheduling assistant",
172
- findATime: "Find a time",
173
- findingTimes: "Finding times\u2026",
174
- noSuggestions: "No free times found in this range.",
175
- everyoneFree: "Everyone available",
176
- unavailableCount: "unavailable",
177
- unknownAvailability: "Availability not shared",
178
- outsideWorkingHours: "Outside working hours",
179
- rooms: "Rooms",
180
- findRoom: "Find a room",
181
- minCapacity: "Min. capacity",
182
- featureFilter: "Feature",
183
- addRoom: "Add",
184
- roomAdded: "Added",
185
- roomFree: "Free",
186
- roomBusy: "Busy",
187
- noRooms: "No rooms match.",
188
- room: "Room",
189
- you: "You",
190
- addCalendar: "Add calendar",
191
- subscribeFromUrl: "Subscribe from URL",
192
- addHolidays: "Holidays",
193
- importIcs: "Import .ics file",
194
- shareCalendar: "Share\u2026",
195
- refreshCalendar: "Refresh",
196
- publishTo: "Publish to\u2026",
197
- suggestions: "Suggestions"
198
- };
199
- var mergeLabels = (labels) => ({
200
- ...CALENDAR_DEFAULT_LABELS,
201
- ...labels
202
- });
203
-
204
- // src/calendar/colors.ts
205
- var CALENDAR_COLORS = [
206
- "#3b82f6",
207
- "#0ea5e9",
208
- "#14b8a6",
209
- "#22c55e",
210
- "#84cc16",
211
- "#eab308",
212
- "#f97316",
213
- "#ef4444",
214
- "#ec4899",
215
- "#a855f7",
216
- "#6366f1",
217
- "#64748b"
218
- ];
219
- function contrastText(hex) {
220
- const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
221
- if (!m) return "#ffffff";
222
- const n = parseInt(m[1], 16);
223
- const ch = [n >> 16 & 255, n >> 8 & 255, n & 255].map((v) => {
224
- const c = v / 255;
225
- return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
226
- });
227
- const lum = 0.2126 * ch[0] + 0.7152 * ch[1] + 0.0722 * ch[2];
228
- return 1.05 / (lum + 0.05) >= (lum + 0.05) / 0.05 ? "#ffffff" : "#111827";
229
- }
230
- var NativeSelect = forwardRef(
231
- function NativeSelect2({ className, children, ...props }, ref) {
232
- return /* @__PURE__ */ jsx(
233
- "select",
234
- {
235
- ref,
236
- className: cn(
237
- "h-9 rounded-md border border-input bg-background px-2 text-sm text-foreground shadow-sm",
238
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50",
239
- className
240
- ),
241
- ...props,
242
- children
243
- }
244
- );
245
- }
246
- );
247
- function FieldRow({
248
- icon,
249
- label,
250
- children,
251
- className
252
- }) {
253
- return /* @__PURE__ */ jsxs("div", { className: cn("flex items-start gap-3", className), children: [
254
- /* @__PURE__ */ jsx(
255
- "div",
256
- {
257
- className: "mt-2 flex h-5 w-5 shrink-0 items-center justify-center text-muted-foreground",
258
- "aria-hidden": true,
259
- children: icon
260
- }
261
- ),
262
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [
263
- label && /* @__PURE__ */ jsx("div", { className: "text-xs font-medium text-muted-foreground", children: label }),
264
- children
265
- ] })
266
- ] });
267
- }
268
- function ColorSwatches({
269
- value,
270
- onChange,
271
- allowNone,
272
- noneLabel,
273
- testIdPrefix
274
- }) {
275
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5", role: "radiogroup", children: [
276
- allowNone && /* @__PURE__ */ jsx(
277
- "button",
278
- {
279
- type: "button",
280
- role: "radio",
281
- "aria-checked": !value,
282
- "aria-label": noneLabel,
283
- title: noneLabel,
284
- onClick: () => onChange(null),
285
- className: cn(
286
- "h-6 rounded-full border border-border px-2 text-xs text-muted-foreground",
287
- !value && "ring-2 ring-ring ring-offset-1 ring-offset-background"
288
- ),
289
- children: noneLabel
290
- }
291
- ),
292
- CALENDAR_COLORS.map((c) => /* @__PURE__ */ jsx(
293
- "button",
294
- {
295
- type: "button",
296
- role: "radio",
297
- "aria-checked": value?.toLowerCase() === c,
298
- "aria-label": c,
299
- title: c,
300
- "data-testid": testIdPrefix ? `${testIdPrefix}-${c.slice(1)}` : void 0,
301
- onClick: () => onChange(c),
302
- className: cn(
303
- "h-6 w-6 rounded-full border border-black/10",
304
- value?.toLowerCase() === c && "ring-2 ring-ring ring-offset-1 ring-offset-background"
305
- ),
306
- style: { backgroundColor: c }
307
- },
308
- c
309
- ))
310
- ] });
311
- }
312
- var temporalZonePlugin = null;
313
- function getTemporalTimeZonePlugin() {
314
- if (!temporalZonePlugin) {
315
- class TemporalNamedTimeZone extends NamedTimeZoneImpl {
316
- offsetForArray(a) {
317
- return wallArrayOffset(a, this.timeZoneName);
318
- }
319
- timestampToArray(ms) {
320
- return epochToWallArray(ms, this.timeZoneName);
321
- }
322
- }
323
- temporalZonePlugin = createPlugin({
324
- name: "arbi-temporal-timezone",
325
- namedTimeZonedImpl: TemporalNamedTimeZone
326
- });
327
- }
328
- return temporalZonePlugin;
329
- }
330
- var FC_VIEW = {
331
- day: "timeGridDay",
332
- week: "timeGridWeek",
333
- work_week: "arbiWorkWeek",
334
- month: "dayGridMonth",
335
- agenda: "arbiAgenda",
336
- year: "multiMonthYear"
337
- };
338
- var VIEW_OF_FC = Object.fromEntries(
339
- Object.entries(FC_VIEW).map(([k, v]) => [v, k])
340
- );
341
- var VIEW_ORDER = [
342
- "day",
343
- "work_week",
344
- "week",
345
- "month",
346
- "agenda",
347
- "year"
348
- ];
349
- var NARROW_VIEWS = ["day", "month", "agenda"];
350
- var CALENDAR_NARROW_WIDTH = 640;
351
- function effectiveCalendarView(view, narrow) {
352
- if (!narrow) return view;
353
- return view === "week" || view === "work_week" || view === "year" ? "day" : view;
354
- }
355
- var viewLabel = (v, l) => ({
356
- day: l.day,
357
- week: l.week,
358
- work_week: l.workWeek,
359
- month: l.month,
360
- agenda: l.agenda,
361
- year: l.year
362
- })[v];
363
- function toBusinessHours(hours) {
364
- if (!hours?.length) return false;
365
- return hours.map((h) => ({
366
- daysOfWeek: [(h.day + 1) % 7],
367
- startTime: h.start,
368
- endTime: h.end === "24:00" ? "24:00:00" : h.end
369
- }));
370
- }
371
- function workWeekHiddenDays(hours) {
372
- const shown = new Set((hours ?? []).map((h) => (h.day + 1) % 7));
373
- if (shown.size === 0) return [0, 6];
374
- return [0, 1, 2, 3, 4, 5, 6].filter((d) => !shown.has(d));
375
- }
376
- function eventStateClasses(e) {
377
- const out = ["arbi-cal-event", `arbi-cal-layer-${e.layer}`];
378
- if (e.layer !== "event") out.push("arbi-cal-overlay");
379
- if (e.status === "cancelled") out.push("arbi-cal-cancelled");
380
- if (e.myPartstat === "declined") out.push("arbi-cal-declined");
381
- else if (e.myPartstat === "needs_action") out.push("arbi-cal-needs-action");
382
- else if (e.myPartstat === "tentative" || e.status === "tentative" || e.showAs === "tentative")
383
- out.push("arbi-cal-tentative");
384
- if (e.showAs === "free") out.push("arbi-cal-free");
385
- if (e.showAs === "oof") out.push("arbi-cal-oof");
386
- if (e.restricted) out.push("arbi-cal-restricted");
387
- return out;
388
- }
389
- function toEventInput(e, zone) {
390
- const editable = e.editable && e.layer === "event" && !e.restricted;
391
- const base = {
392
- id: e.instanceId,
393
- title: e.title,
394
- editable,
395
- startEditable: editable,
396
- durationEditable: editable,
397
- classNames: eventStateClasses(e),
398
- // The inner content paints the block (see calendar.css); the harness only carries the border
399
- // colour (list-view dot, needs-action outline).
400
- backgroundColor: "transparent",
401
- borderColor: e.color,
402
- textColor: "inherit",
403
- extendedProps: { grid: e }
404
- };
405
- if (e.allDay) {
406
- const start = e.startDate ?? utcToDate(e.startUtc, zone);
407
- const end = e.endDate ?? (e.endUtc ? utcToDate(e.endUtc, zone) : addDays(start, 1));
408
- return { ...base, allDay: true, start, end: end > start ? end : addDays(start, 1) };
409
- }
410
- return { ...base, allDay: false, start: e.startUtc, end: e.endUtc };
411
- }
412
- var LAYER_ICON = { facts: Lightbulb, workflows: Workflow, tasks: ListTodo };
413
- function EventBody({ arg, labels }) {
414
- const g = arg.event.extendedProps.grid;
415
- if (!g) return /* @__PURE__ */ jsx("span", { children: arg.event.title });
416
- const title = g.restricted ? labels.busy : g.title.trim() || labels.noTitle;
417
- const type = arg.view.type;
418
- const dot = type === "dayGridMonth" && !arg.event.allDay;
419
- const list = type.startsWith("list") || type === "arbiAgenda";
420
- const style = {
421
- "--arbi-cal-color": g.color,
422
- "--arbi-cal-fg": contrastText(g.color)
423
- };
424
- const OverlayIcon = g.layer !== "event" ? LAYER_ICON[g.layer] : null;
425
- const icons = /* @__PURE__ */ jsxs(Fragment, { children: [
426
- OverlayIcon && /* @__PURE__ */ jsx(OverlayIcon, { className: "arbi-cal-icon", "aria-hidden": true }),
427
- g.restricted && /* @__PURE__ */ jsx(Lock, { className: "arbi-cal-icon", "aria-hidden": true }),
428
- g.isRecurring && /* @__PURE__ */ jsx(Repeat, { className: "arbi-cal-icon", "aria-label": labels.recurring }),
429
- g.hasConference && /* @__PURE__ */ jsx(Video, { className: "arbi-cal-icon", "aria-hidden": true })
430
- ] });
431
- if (list) {
432
- return /* @__PURE__ */ jsxs(
433
- "div",
434
- {
435
- className: "arbi-cal-list-item",
436
- style,
437
- "data-testid": "calendar-event",
438
- "data-instance-id": g.instanceId,
439
- children: [
440
- /* @__PURE__ */ jsx("span", { className: "arbi-cal-title", children: title }),
441
- icons,
442
- g.location && !g.restricted && /* @__PURE__ */ jsxs("span", { className: "arbi-cal-location", children: [
443
- /* @__PURE__ */ jsx(MapPin, { className: "arbi-cal-icon", "aria-hidden": true }),
444
- g.location
445
- ] })
446
- ]
447
- }
448
- );
449
- }
450
- if (dot) {
451
- return /* @__PURE__ */ jsxs(
452
- "div",
453
- {
454
- className: "arbi-cal-dot-item",
455
- style,
456
- "data-testid": "calendar-event",
457
- "data-instance-id": g.instanceId,
458
- children: [
459
- /* @__PURE__ */ jsx("span", { className: "arbi-cal-dot", "aria-hidden": true }),
460
- arg.timeText && /* @__PURE__ */ jsx("span", { className: "arbi-cal-time", children: arg.timeText }),
461
- /* @__PURE__ */ jsx("span", { className: "arbi-cal-title", children: title })
462
- ]
463
- }
464
- );
465
- }
466
- return /* @__PURE__ */ jsxs(
467
- "div",
468
- {
469
- className: "arbi-cal-event-inner",
470
- style,
471
- "data-testid": "calendar-event",
472
- "data-instance-id": g.instanceId,
473
- children: [
474
- /* @__PURE__ */ jsxs("div", { className: "arbi-cal-event-line", children: [
475
- /* @__PURE__ */ jsx("span", { className: "arbi-cal-title", children: title }),
476
- icons
477
- ] }),
478
- !arg.event.allDay && arg.timeText && /* @__PURE__ */ jsx("div", { className: "arbi-cal-time", children: arg.timeText }),
479
- !arg.event.allDay && g.location && !g.restricted && /* @__PURE__ */ jsx("div", { className: "arbi-cal-location", children: g.location })
480
- ]
481
- }
482
- );
483
- }
484
- function isTypingTarget(t) {
485
- if (!(t instanceof HTMLElement)) return false;
486
- const tag = t.tagName;
487
- return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || t.isContentEditable;
488
- }
489
- function CalendarGrid({
490
- events,
491
- view,
492
- date,
493
- timeZone,
494
- secondaryTimeZone,
495
- weekStart,
496
- workingHours,
497
- showWeekNumbers = false,
498
- nowIndicator = true,
499
- onRangeChange,
500
- onSelectSlot,
501
- onEventClick,
502
- onEventChange,
503
- onViewChange,
504
- onDateChange,
505
- onCreateShortcut,
506
- height,
507
- labels: labelOverrides,
508
- hideToolbar,
509
- toolbarEnd,
510
- className,
511
- keyboardShortcuts = true
512
- }) {
513
- const labels = useMemo(() => mergeLabels(labelOverrides), [labelOverrides]);
514
- const zone = safeZone(timeZone);
515
- const secondZone = secondaryTimeZone && secondaryTimeZone !== zone && isValidTimeZone(secondaryTimeZone) ? secondaryTimeZone : "";
516
- const rootRef = useRef(null);
517
- const calRef = useRef(null);
518
- const [narrow, setNarrow] = useState(false);
519
- const [title, setTitle] = useState("");
520
- const lastRange = useRef("");
521
- const effective = effectiveCalendarView(view, narrow);
522
- const cb = useRef({
523
- onRangeChange,
524
- onSelectSlot,
525
- onEventClick,
526
- onEventChange,
527
- onViewChange,
528
- onDateChange,
529
- onCreateShortcut
530
- });
531
- cb.current = {
532
- onRangeChange,
533
- onSelectSlot,
534
- onEventClick,
535
- onEventChange,
536
- onViewChange,
537
- onDateChange,
538
- onCreateShortcut
539
- };
540
- const api = useCallback(() => calRef.current?.getApi() ?? null, []);
541
- useEffect(() => {
542
- const el = rootRef.current;
543
- if (!el || typeof ResizeObserver === "undefined") return;
544
- const ro = new ResizeObserver(([entry]) => {
545
- setNarrow(entry.contentRect.width > 0 && entry.contentRect.width < CALENDAR_NARROW_WIDTH);
546
- });
547
- ro.observe(el);
548
- return () => ro.disconnect();
549
- }, []);
550
- useEffect(() => {
551
- const a = api();
552
- if (!a) return;
553
- const fcView = FC_VIEW[effective];
554
- if (a.view.type !== fcView) {
555
- a.changeView(fcView, date);
556
- return;
557
- }
558
- if (utcToDate(a.getDate(), zone) !== date) a.gotoDate(date);
559
- }, [api, effective, date, zone]);
560
- const fcEvents = useMemo(() => events.map((e) => toEventInput(e, zone)), [events, zone]);
561
- const changeView = useCallback(
562
- (v) => {
563
- const a = api();
564
- if (a) a.changeView(FC_VIEW[effectiveCalendarView(v, narrow)]);
565
- cb.current.onViewChange?.(v);
566
- },
567
- [api, narrow]
568
- );
569
- const nav = useCallback(
570
- (dir) => {
571
- const a = api();
572
- if (!a) return;
573
- if (dir === "prev") a.prev();
574
- else if (dir === "next") a.next();
575
- else a.gotoDate(utcToDate(Date.now(), zone));
576
- },
577
- [api, zone]
578
- );
579
- useEffect(() => {
580
- if (!keyboardShortcuts) return;
581
- const onKey = (e) => {
582
- if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.altKey || isTypingTarget(e.target))
583
- return;
584
- if (typeof document !== "undefined" && document.querySelector('[role="dialog"], [role="alertdialog"]'))
585
- return;
586
- const viewKey = {
587
- d: "day",
588
- w: "week",
589
- x: "work_week",
590
- m: "month",
591
- a: "agenda",
592
- y: "year"
593
- };
594
- const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
595
- if (k === "t") nav("today");
596
- else if (k === "j" || k === "n" || k === "ArrowRight") nav("next");
597
- else if (k === "k" || k === "p" || k === "ArrowLeft") nav("prev");
598
- else if (viewKey[k]) changeView(viewKey[k]);
599
- else if (k === "c" && cb.current.onCreateShortcut) cb.current.onCreateShortcut();
600
- else return;
601
- e.preventDefault();
602
- };
603
- window.addEventListener("keydown", onKey);
604
- return () => window.removeEventListener("keydown", onKey);
605
- }, [keyboardShortcuts, nav, changeView]);
606
- const onDatesSet = useCallback(
607
- (arg) => {
608
- setTitle(arg.view.title);
609
- const anchor = utcToDate(arg.view.calendar.getDate(), zone);
610
- const kind = VIEW_OF_FC[arg.view.type] ?? effective;
611
- const key = `${kind}|${arg.start.toISOString()}|${arg.end.toISOString()}|${anchor}`;
612
- if (key === lastRange.current) return;
613
- lastRange.current = key;
614
- cb.current.onRangeChange({ start: arg.start, end: arg.end, view: kind, date: anchor });
615
- if (anchor !== date) cb.current.onDateChange?.(anchor);
616
- },
617
- [zone, effective, date]
618
- );
619
- const onSelect = useCallback(
620
- (arg) => {
621
- if (!cb.current.onSelectSlot) return;
622
- if (arg.allDay) {
623
- cb.current.onSelectSlot({
624
- startUtc: arg.start.toISOString(),
625
- endUtc: arg.end.toISOString(),
626
- allDay: true,
627
- startDate: arg.startStr.slice(0, 10),
628
- endDate: arg.endStr.slice(0, 10)
629
- });
630
- } else {
631
- cb.current.onSelectSlot({
632
- startUtc: arg.start.toISOString(),
633
- endUtc: arg.end.toISOString(),
634
- allDay: false
635
- });
636
- }
637
- api()?.unselect();
638
- },
639
- [api]
640
- );
641
- const emitChange = useCallback((event, revert) => {
642
- const g = event.extendedProps.grid;
643
- if (!g || !cb.current.onEventChange || !event.start) {
644
- revert();
645
- return;
646
- }
647
- const start = event.start;
648
- if (event.allDay) {
649
- const startDate = event.startStr.slice(0, 10);
650
- const endDate = event.endStr ? event.endStr.slice(0, 10) : addDays(startDate, 1);
651
- const end2 = event.end ?? new Date(start.getTime() + 864e5);
652
- cb.current.onEventChange({
653
- instanceId: g.instanceId,
654
- eventId: g.eventId,
655
- startUtc: start.toISOString(),
656
- endUtc: end2.toISOString(),
657
- allDay: true,
658
- startDate,
659
- endDate,
660
- revert
661
- });
662
- return;
663
- }
664
- const end = event.end ?? new Date(start.getTime() + 36e5);
665
- cb.current.onEventChange({
666
- instanceId: g.instanceId,
667
- eventId: g.eventId,
668
- startUtc: start.toISOString(),
669
- endUtc: end.toISOString(),
670
- allDay: false,
671
- revert
672
- });
673
- }, []);
674
- const slotLabelContent = useCallback(
675
- (arg) => {
676
- if (!secondZone) return arg.text;
677
- return /* @__PURE__ */ jsxs("div", { className: "arbi-cal-slot-labels", children: [
678
- /* @__PURE__ */ jsx("span", { className: "arbi-cal-slot-secondary", children: (() => {
679
- const t = convertWall(`${date}T${utcToTime(arg.date, zone)}`, zone, secondZone).slice(
680
- 11,
681
- 16
682
- );
683
- return t.endsWith(":00") ? t.slice(0, 2) : t;
684
- })() }),
685
- /* @__PURE__ */ jsx("span", { className: "arbi-cal-slot-primary", children: arg.text })
686
- ] });
687
- },
688
- [secondZone, zone, date]
689
- );
690
- const allDayContent = useCallback(
691
- (arg) => {
692
- if (!secondZone) return arg.text;
693
- return /* @__PURE__ */ jsxs("div", { className: "arbi-cal-slot-labels arbi-cal-zone-labels", children: [
694
- /* @__PURE__ */ jsx("span", { className: "arbi-cal-slot-secondary", title: secondZone, children: zoneOffsetLabel(secondZone) }),
695
- /* @__PURE__ */ jsx("span", { className: "arbi-cal-slot-primary", title: zone, children: zoneOffsetLabel(zone) })
696
- ] });
697
- },
698
- [secondZone, zone]
699
- );
700
- const dayHeaderContent = useCallback(
701
- (arg) => {
702
- const t = arg.view.type;
703
- if (!t.startsWith("timeGrid") && t !== "arbiWorkWeek") return arg.text;
704
- return /* @__PURE__ */ jsxs("div", { className: cn("arbi-cal-dayhead", arg.isToday && "arbi-cal-dayhead-today"), children: [
705
- /* @__PURE__ */ jsx("span", { className: "arbi-cal-dayhead-dow", children: formatInZone(arg.date, zone, { weekday: "short" }) }),
706
- /* @__PURE__ */ jsx("span", { className: "arbi-cal-dayhead-num", children: formatInZone(arg.date, zone, { day: "numeric" }) })
707
- ] });
708
- },
709
- [zone]
710
- );
711
- const scrollTime = useMemo(() => {
712
- const starts = (workingHours ?? []).map((h2) => h2.start).sort();
713
- const first = starts[0] ?? "08:00";
714
- const h = Math.max(0, Number(first.slice(0, 2)) - 1);
715
- return `${String(h).padStart(2, "0")}:00:00`;
716
- }, [workingHours]);
717
- const views = useMemo(
718
- () => ({
719
- arbiWorkWeek: {
720
- type: "timeGrid",
721
- duration: { weeks: 1 },
722
- hiddenDays: workWeekHiddenDays(workingHours),
723
- buttonText: labels.workWeek
724
- },
725
- arbiAgenda: {
726
- type: "list",
727
- duration: { days: 14 },
728
- dateAlignment: "day",
729
- buttonText: labels.agenda
730
- },
731
- multiMonthYear: { multiMonthMaxColumns: narrow ? 1 : 3 },
732
- // Growing rows (height auto) never overflow, so cap explicitly there; else fit the cell.
733
- dayGridMonth: { dayMaxEvents: height === "auto" ? 3 : true }
734
- }),
735
- [workingHours, labels, narrow, height]
736
- );
737
- const plugins = useMemo(
738
- () => [
739
- dayGridPlugin,
740
- timeGridPlugin,
741
- listPlugin,
742
- interactionPlugin,
743
- multiMonthPlugin,
744
- getTemporalTimeZonePlugin()
745
- ],
746
- []
747
- );
748
- const viewChoices = narrow ? NARROW_VIEWS : VIEW_ORDER;
749
- return /* @__PURE__ */ jsxs(
750
- "div",
751
- {
752
- ref: rootRef,
753
- className: cn(
754
- "arbi-calendar flex h-full min-h-0 flex-col",
755
- narrow && "arbi-calendar-narrow",
756
- secondZone && "arbi-cal-dual",
757
- className
758
- ),
759
- "data-testid": "calendar-grid",
760
- "data-view": effective,
761
- "data-time-zone": zone,
762
- children: [
763
- !hideToolbar && /* @__PURE__ */ jsxs(
764
- "div",
765
- {
766
- className: "arbi-cal-toolbar flex flex-wrap items-center gap-2 px-1 pb-2",
767
- "data-testid": "calendar-toolbar",
768
- children: [
769
- /* @__PURE__ */ jsx(
770
- Button,
771
- {
772
- type: "button",
773
- variant: "outline",
774
- size: "sm",
775
- onClick: () => nav("today"),
776
- "data-testid": "calendar-today",
777
- children: labels.today
778
- }
779
- ),
780
- /* @__PURE__ */ jsxs("div", { className: "flex items-center", children: [
781
- /* @__PURE__ */ jsx(
782
- Button,
783
- {
784
- type: "button",
785
- variant: "ghost",
786
- size: "icon",
787
- className: "h-8 w-8",
788
- "aria-label": labels.previous,
789
- onClick: () => nav("prev"),
790
- "data-testid": "calendar-prev",
791
- children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" })
792
- }
793
- ),
794
- /* @__PURE__ */ jsx(
795
- Button,
796
- {
797
- type: "button",
798
- variant: "ghost",
799
- size: "icon",
800
- className: "h-8 w-8",
801
- "aria-label": labels.next,
802
- onClick: () => nav("next"),
803
- "data-testid": "calendar-next",
804
- children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" })
805
- }
806
- )
807
- ] }),
808
- /* @__PURE__ */ jsx(
809
- "h2",
810
- {
811
- className: "min-w-0 flex-1 truncate text-base font-semibold sm:text-lg",
812
- "data-testid": "calendar-title",
813
- "aria-live": "polite",
814
- children: title
815
- }
816
- ),
817
- secondZone && !narrow && /* @__PURE__ */ jsxs(
818
- "span",
819
- {
820
- className: "hidden text-xs text-muted-foreground md:inline",
821
- "data-testid": "calendar-zones",
822
- children: [
823
- zoneCity(zone),
824
- " ",
825
- zoneOffsetLabel(zone),
826
- " \xB7 ",
827
- zoneCity(secondZone),
828
- " ",
829
- zoneOffsetLabel(secondZone)
830
- ]
831
- }
832
- ),
833
- narrow ? /* @__PURE__ */ jsx(
834
- NativeSelect,
835
- {
836
- "aria-label": labels.view,
837
- value: effective,
838
- onChange: (e) => changeView(e.target.value),
839
- "data-testid": "calendar-view-select",
840
- className: "h-8",
841
- children: viewChoices.map((v) => /* @__PURE__ */ jsx("option", { value: v, children: viewLabel(v, labels) }, v))
842
- }
843
- ) : /* @__PURE__ */ jsx(
844
- "div",
845
- {
846
- className: "inline-flex rounded-md border border-border p-0.5",
847
- role: "group",
848
- "data-testid": "calendar-view-switch",
849
- children: viewChoices.map((v) => /* @__PURE__ */ jsx(
850
- "button",
851
- {
852
- type: "button",
853
- "aria-pressed": effective === v,
854
- onClick: () => changeView(v),
855
- "data-testid": `calendar-view-${v}`,
856
- className: cn(
857
- "rounded px-2.5 py-1 text-xs font-medium transition-colors",
858
- effective === v ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground"
859
- ),
860
- children: viewLabel(v, labels)
861
- },
862
- v
863
- ))
864
- }
865
- ),
866
- toolbarEnd
867
- ]
868
- }
869
- ),
870
- /* @__PURE__ */ jsx("div", { className: "arbi-cal-body min-h-0 flex-1", children: /* @__PURE__ */ jsx(
871
- FullCalendar,
872
- {
873
- ref: calRef,
874
- plugins,
875
- timeZone: zone,
876
- initialView: FC_VIEW[effective],
877
- initialDate: date,
878
- headerToolbar: false,
879
- height: height ?? "100%",
880
- views,
881
- events: fcEvents,
882
- firstDay: weekStart,
883
- weekNumbers: showWeekNumbers,
884
- weekNumberCalculation: weekStart === 1 ? "ISO" : "local",
885
- weekText: labels.weekAbbrev,
886
- nowIndicator,
887
- businessHours: toBusinessHours(workingHours),
888
- scrollTime,
889
- slotDuration: "00:30:00",
890
- snapDuration: "00:15:00",
891
- slotLabelFormat: {
892
- hour: "numeric",
893
- minute: "2-digit",
894
- omitZeroMinute: true,
895
- meridiem: "short"
896
- },
897
- eventTimeFormat: {
898
- hour: "numeric",
899
- minute: "2-digit",
900
- omitZeroMinute: true,
901
- meridiem: "short"
902
- },
903
- allDayText: labels.allDay,
904
- noEventsText: labels.noEvents,
905
- moreLinkText: labels.more,
906
- navLinks: true,
907
- navLinkDayClick: (d) => {
908
- const a = api();
909
- a?.changeView(FC_VIEW.day, d);
910
- cb.current.onViewChange?.("day");
911
- },
912
- selectable: !!onSelectSlot,
913
- selectMirror: true,
914
- unselectAuto: true,
915
- editable: !!onEventChange,
916
- eventResizableFromStart: true,
917
- eventStartEditable: !!onEventChange,
918
- eventDurationEditable: !!onEventChange,
919
- stickyHeaderDates: true,
920
- expandRows: true,
921
- dayMaxEvents: height === "auto" ? 3 : true,
922
- eventDisplay: "auto",
923
- datesSet: onDatesSet,
924
- select: onSelect,
925
- eventClick: (arg) => {
926
- arg.jsEvent.preventDefault();
927
- const g = arg.event.extendedProps.grid;
928
- if (g) cb.current.onEventClick?.(g.instanceId, g.eventId);
929
- },
930
- eventDrop: (arg) => emitChange(arg.event, arg.revert),
931
- eventResize: (arg) => emitChange(arg.event, arg.revert),
932
- eventContent: (arg) => /* @__PURE__ */ jsx(EventBody, { arg, labels }),
933
- slotLabelContent,
934
- allDayContent,
935
- dayHeaderContent
936
- }
937
- ) })
938
- ]
939
- }
940
- );
941
- }
942
- function MiniMonth({
943
- date,
944
- weekStart,
945
- timeZone,
946
- onSelect,
947
- highlightRange,
948
- showWeekNumbers,
949
- labels: labelOverrides,
950
- className
951
- }) {
952
- const labels = mergeLabels(labelOverrides);
953
- const [view, setView] = useState(() => ({ for: date, month: `${date.slice(0, 7)}-01` }));
954
- if (view.for !== date) setView({ for: date, month: `${date.slice(0, 7)}-01` });
955
- const month = view.for === date ? view.month : `${date.slice(0, 7)}-01`;
956
- const setMonth = (m) => setView({ for: date, month: m });
957
- const today = todayInZone(timeZone);
958
- const days = useMemo(() => {
959
- const first = startOfWeek(month, weekStart);
960
- return Array.from({ length: 42 }, (_, i) => addDays(first, i));
961
- }, [month, weekStart]);
962
- const weekdays = days.slice(0, 7).map((d) => formatPlainDate(d, { weekday: "narrow" }));
963
- const monthTitle = formatPlainDate(month, { month: "long", year: "numeric" });
964
- return /* @__PURE__ */ jsxs("div", { className: cn("select-none text-xs", className), "data-testid": "calendar-mini-month", children: [
965
- /* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center justify-between px-1", children: [
966
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", "aria-live": "polite", children: monthTitle }),
967
- /* @__PURE__ */ jsxs("div", { className: "flex", children: [
968
- /* @__PURE__ */ jsx(
969
- "button",
970
- {
971
- type: "button",
972
- className: "rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground",
973
- "aria-label": labels.previous,
974
- onClick: () => setMonth(addMonths(month, -1)),
975
- "data-testid": "calendar-mini-prev",
976
- children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" })
977
- }
978
- ),
979
- /* @__PURE__ */ jsx(
980
- "button",
981
- {
982
- type: "button",
983
- className: "rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground",
984
- "aria-label": labels.next,
985
- onClick: () => setMonth(addMonths(month, 1)),
986
- "data-testid": "calendar-mini-next",
987
- children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" })
988
- }
989
- )
990
- ] })
991
- ] }),
992
- /* @__PURE__ */ jsxs(
993
- "div",
994
- {
995
- className: cn(
996
- "grid gap-y-0.5 text-center",
997
- showWeekNumbers ? "grid-cols-8" : "grid-cols-7"
998
- ),
999
- role: "grid",
1000
- "aria-label": monthTitle,
1001
- children: [
1002
- showWeekNumbers && /* @__PURE__ */ jsx("span", { className: "py-1 text-[10px] text-muted-foreground", children: labels.weekAbbrev }),
1003
- weekdays.map((w, i) => /* @__PURE__ */ jsx(
1004
- "span",
1005
- {
1006
- className: "py-1 text-[10px] font-medium uppercase text-muted-foreground",
1007
- role: "columnheader",
1008
- children: w
1009
- },
1010
- i
1011
- )),
1012
- days.map((d, i) => {
1013
- const inMonth = d.slice(0, 7) === month.slice(0, 7);
1014
- const inRange = !!highlightRange && d >= highlightRange.start && d <= highlightRange.end;
1015
- const selected = d === date;
1016
- const isToday = d === today;
1017
- return /* @__PURE__ */ jsx(
1018
- MiniDay,
1019
- {
1020
- d,
1021
- weekCell: showWeekNumbers && i % 7 === 0 ? isoWeekNumber(d) : null,
1022
- inMonth,
1023
- inRange,
1024
- selected,
1025
- isToday,
1026
- onSelect
1027
- },
1028
- d
1029
- );
1030
- })
1031
- ]
1032
- }
1033
- )
1034
- ] });
1035
- }
1036
- function MiniDay({
1037
- d,
1038
- weekCell,
1039
- inMonth,
1040
- inRange,
1041
- selected,
1042
- isToday,
1043
- onSelect
1044
- }) {
1045
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1046
- weekCell !== null && /* @__PURE__ */ jsx("span", { className: "flex items-center justify-center text-[10px] text-muted-foreground", children: weekCell }),
1047
- /* @__PURE__ */ jsx("span", { className: cn("flex items-center justify-center", inRange && "bg-primary/10"), children: /* @__PURE__ */ jsx(
1048
- "button",
1049
- {
1050
- type: "button",
1051
- role: "gridcell",
1052
- "aria-selected": selected,
1053
- "aria-current": isToday ? "date" : void 0,
1054
- "aria-label": formatPlainDate(d, { dateStyle: "full" }),
1055
- onClick: () => onSelect(d),
1056
- "data-testid": `calendar-mini-day-${d}`,
1057
- className: cn(
1058
- "flex h-7 w-7 items-center justify-center rounded-full tabular-nums transition-colors",
1059
- !inMonth && "text-muted-foreground/60",
1060
- isToday && !selected && "font-semibold text-primary",
1061
- selected ? "bg-primary text-primary-foreground" : "hover:bg-muted"
1062
- ),
1063
- children: Number(d.slice(8, 10))
1064
- }
1065
- ) })
1066
- ] });
1067
- }
1068
- var GROUPS = ["mine", "shared", "workspace", "subscribed", "rooms"];
1069
- var groupLabel = (g, l) => ({
1070
- mine: l.myCalendars,
1071
- shared: l.sharedCalendars,
1072
- workspace: l.workspaceCalendars,
1073
- subscribed: l.subscribedCalendars,
1074
- rooms: l.rooms
1075
- })[g];
1076
- function Swatch({ color, checked }) {
1077
- return /* @__PURE__ */ jsx(
1078
- "span",
1079
- {
1080
- className: "flex h-4 w-4 shrink-0 items-center justify-center rounded-[4px] border-2",
1081
- style: { borderColor: color, backgroundColor: checked ? color : "transparent" },
1082
- "aria-hidden": true,
1083
- children: checked && /* @__PURE__ */ jsx("svg", { viewBox: "0 0 12 12", className: "h-3 w-3", fill: "none", stroke: "white", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { d: "M2.5 6.5l2.5 2.5 4.5-5" }) })
1084
- }
1085
- );
1086
- }
1087
- function CalendarList({
1088
- calendars,
1089
- overlays,
1090
- onToggleHidden,
1091
- onColorChange,
1092
- onToggleOverlay,
1093
- onCreate,
1094
- onEdit,
1095
- onImport,
1096
- onExport,
1097
- onDelete,
1098
- onShare,
1099
- onRefresh,
1100
- onPublish,
1101
- onSubscribe,
1102
- onAddHolidays,
1103
- onImportFile,
1104
- labels: labelOverrides,
1105
- className
1106
- }) {
1107
- const l = mergeLabels(labelOverrides);
1108
- const hasAddMenu = !!(onSubscribe || onAddHolidays || onImportFile);
1109
- const plusButton = /* @__PURE__ */ jsx(
1110
- "button",
1111
- {
1112
- type: "button",
1113
- className: "rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground",
1114
- "aria-label": hasAddMenu ? l.addCalendar : l.createCalendar,
1115
- title: hasAddMenu ? l.addCalendar : l.createCalendar,
1116
- onClick: hasAddMenu ? void 0 : onCreate,
1117
- "data-testid": "calendar-list-create",
1118
- children: /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" })
1119
- }
1120
- );
1121
- const overlayRows = [
1122
- ["facts", l.facts, Lightbulb],
1123
- ["workflows", l.workflows, Workflow],
1124
- ["tasks", l.tasks, ListTodo]
1125
- ];
1126
- return /* @__PURE__ */ jsxs("div", { className: cn("space-y-3 text-sm", className), "data-testid": "calendar-list", children: [
1127
- GROUPS.map((g) => {
1128
- const items = calendars.filter((c) => c.group === g);
1129
- if (!items.length && g !== "mine") return null;
1130
- return /* @__PURE__ */ jsxs("section", { "aria-label": groupLabel(g, l), children: [
1131
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-1 pb-1", children: [
1132
- /* @__PURE__ */ jsx("h4", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: groupLabel(g, l) }),
1133
- g === "mine" && (hasAddMenu ? /* @__PURE__ */ jsxs(DropdownMenu, { children: [
1134
- /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: plusButton }),
1135
- /* @__PURE__ */ jsxs(
1136
- DropdownMenuContent,
1137
- {
1138
- align: "end",
1139
- className: "w-56",
1140
- "data-testid": "calendar-add-menu",
1141
- children: [
1142
- /* @__PURE__ */ jsx(DropdownMenuItem, { onSelect: onCreate, "data-testid": "calendar-add-new", children: l.createCalendar }),
1143
- onSubscribe && /* @__PURE__ */ jsx(
1144
- DropdownMenuItem,
1145
- {
1146
- onSelect: onSubscribe,
1147
- "data-testid": "calendar-add-subscribe",
1148
- children: l.subscribeFromUrl
1149
- }
1150
- ),
1151
- onAddHolidays && /* @__PURE__ */ jsx(
1152
- DropdownMenuItem,
1153
- {
1154
- onSelect: onAddHolidays,
1155
- "data-testid": "calendar-add-holidays",
1156
- children: l.addHolidays
1157
- }
1158
- ),
1159
- onImportFile && /* @__PURE__ */ jsx(DropdownMenuItem, { onSelect: onImportFile, "data-testid": "calendar-add-import", children: l.importIcs })
1160
- ]
1161
- }
1162
- )
1163
- ] }) : plusButton)
1164
- ] }),
1165
- /* @__PURE__ */ jsx("ul", { className: "space-y-0.5", children: items.map((c) => {
1166
- const name = c.locked ? l.locked : c.name || l.noTitle;
1167
- const isOwner = c.role === "owner";
1168
- return /* @__PURE__ */ jsxs(
1169
- "li",
1170
- {
1171
- className: "group flex items-center gap-2 rounded-md px-1 py-1 hover:bg-muted/60",
1172
- "data-testid": `calendar-list-item-${c.id}`,
1173
- children: [
1174
- /* @__PURE__ */ jsxs("label", { className: "flex min-w-0 flex-1 cursor-pointer items-center gap-2", children: [
1175
- /* @__PURE__ */ jsx(
1176
- "input",
1177
- {
1178
- type: "checkbox",
1179
- className: "sr-only",
1180
- checked: !c.hidden,
1181
- onChange: () => onToggleHidden(c.id),
1182
- "aria-label": `${c.hidden ? l.showCalendar : l.hideCalendar} ${name}`,
1183
- "data-testid": `calendar-list-toggle-${c.id}`
1184
- }
1185
- ),
1186
- /* @__PURE__ */ jsx(Swatch, { color: c.color, checked: !c.hidden }),
1187
- /* @__PURE__ */ jsx(
1188
- "span",
1189
- {
1190
- className: cn(
1191
- "min-w-0 truncate",
1192
- c.locked && "italic text-muted-foreground"
1193
- ),
1194
- children: name
1195
- }
1196
- ),
1197
- c.locked && /* @__PURE__ */ jsx(Lock, { className: "h-3 w-3 shrink-0 text-muted-foreground", "aria-hidden": true }),
1198
- c.provider && c.provider !== "native" && /* @__PURE__ */ jsx(
1199
- "span",
1200
- {
1201
- className: "shrink-0 rounded border border-border px-1 text-[10px] leading-4 text-muted-foreground",
1202
- "data-testid": `calendar-list-provider-${c.id}`,
1203
- children: {
1204
- google: "Google",
1205
- microsoft: "Outlook",
1206
- caldav: "CalDAV",
1207
- ics: "ICS"
1208
- }[c.provider] ?? c.provider
1209
- }
1210
- )
1211
- ] }),
1212
- /* @__PURE__ */ jsxs(DropdownMenu, { children: [
1213
- /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
1214
- "button",
1215
- {
1216
- type: "button",
1217
- className: "rounded p-0.5 text-muted-foreground opacity-60 hover:bg-muted hover:text-foreground group-hover:opacity-100 focus-visible:opacity-100",
1218
- "aria-label": `${l.editCalendar}: ${name}`,
1219
- "data-testid": `calendar-list-menu-${c.id}`,
1220
- children: /* @__PURE__ */ jsx(EllipsisVertical, { className: "h-4 w-4" })
1221
- }
1222
- ) }),
1223
- /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", className: "w-56", children: [
1224
- /* @__PURE__ */ jsx(DropdownMenuLabel, { className: "text-xs", children: l.calendarColor }),
1225
- /* @__PURE__ */ jsx("div", { className: "px-2 pb-2", children: /* @__PURE__ */ jsx(
1226
- ColorSwatches,
1227
- {
1228
- value: c.color,
1229
- onChange: (color) => color && onColorChange(c.id, color)
1230
- }
1231
- ) }),
1232
- /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
1233
- /* @__PURE__ */ jsx(DropdownMenuItem, { onSelect: () => onEdit(c.id), disabled: c.locked, children: l.editCalendar }),
1234
- onShare && isOwner && c.group !== "subscribed" && /* @__PURE__ */ jsx(
1235
- DropdownMenuItem,
1236
- {
1237
- onSelect: () => onShare(c.id),
1238
- disabled: c.locked,
1239
- "data-testid": `calendar-list-share-${c.id}`,
1240
- children: l.shareCalendar
1241
- }
1242
- ),
1243
- onPublish && isOwner && c.group === "mine" && (!c.provider || c.provider === "native") && /* @__PURE__ */ jsx(
1244
- DropdownMenuItem,
1245
- {
1246
- onSelect: () => onPublish(c.id),
1247
- disabled: c.locked,
1248
- "data-testid": `calendar-list-publish-${c.id}`,
1249
- children: l.publishTo
1250
- }
1251
- ),
1252
- onRefresh && c.group === "subscribed" && /* @__PURE__ */ jsx(
1253
- DropdownMenuItem,
1254
- {
1255
- onSelect: () => onRefresh(c.id),
1256
- "data-testid": `calendar-list-refresh-${c.id}`,
1257
- children: l.refreshCalendar
1258
- }
1259
- ),
1260
- c.canWrite && /* @__PURE__ */ jsx(DropdownMenuItem, { onSelect: () => onImport(c.id), disabled: c.locked, children: l.importCalendar }),
1261
- /* @__PURE__ */ jsx(DropdownMenuItem, { onSelect: () => onExport(c.id), disabled: c.locked, children: l.exportCalendar }),
1262
- isOwner && !c.isDefault && /* @__PURE__ */ jsxs(Fragment, { children: [
1263
- /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
1264
- /* @__PURE__ */ jsx(
1265
- DropdownMenuItem,
1266
- {
1267
- className: "text-destructive focus:text-destructive",
1268
- onSelect: () => onDelete(c.id),
1269
- children: l.deleteCalendar
1270
- }
1271
- )
1272
- ] })
1273
- ] })
1274
- ] })
1275
- ]
1276
- },
1277
- c.id
1278
- );
1279
- }) })
1280
- ] }, g);
1281
- }),
1282
- /* @__PURE__ */ jsxs("section", { "aria-label": l.overlays, children: [
1283
- /* @__PURE__ */ jsx("h4", { className: "px-1 pb-1 text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: l.overlays }),
1284
- /* @__PURE__ */ jsx("ul", { className: "space-y-0.5", children: overlayRows.map(([layer, label, Icon]) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2 rounded-md px-1 py-1 hover:bg-muted/60", children: [
1285
- /* @__PURE__ */ jsx(
1286
- "input",
1287
- {
1288
- type: "checkbox",
1289
- className: "h-4 w-4 accent-[hsl(var(--primary))]",
1290
- checked: overlays[layer],
1291
- onChange: () => onToggleOverlay(layer),
1292
- "data-testid": `calendar-overlay-${layer}`
1293
- }
1294
- ),
1295
- /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5 text-muted-foreground", "aria-hidden": true }),
1296
- /* @__PURE__ */ jsx("span", { children: label })
1297
- ] }) }, layer)) })
1298
- ] })
1299
- ] });
1300
- }
1301
-
1302
- // src/calendar/draft.ts
1303
- var isAllDayDraft = (d) => !!d.start.date;
1304
- var datePart = (spec) => spec.date ?? (spec.date_time ?? "").slice(0, 10);
1305
- var timePart = (spec) => (spec.date_time ?? "").slice(11, 16);
1306
- var specZone = (spec, defaultZone) => safeZone(spec.time_zone, defaultZone);
1307
- var specDate = datePart;
1308
- var specTime = timePart;
1309
- function specEpochMs(spec, defaultZone) {
1310
- if (spec.date) return wallToEpochMs(spec.date, defaultZone);
1311
- return wallToEpochMs(spec.date_time ?? "", specZone(spec, defaultZone));
1312
- }
1313
- function draftRangeUtc(d, defaultZone) {
1314
- return { startMs: specEpochMs(d.start, defaultZone), endMs: specEpochMs(d.end, defaultZone) };
1315
- }
1316
- function allDayInclusiveEnd(d) {
1317
- const start = d.start.date ?? "";
1318
- const end = d.end.date ?? start;
1319
- const last = addDays(end, -1);
1320
- return last < start ? start : last;
1321
- }
1322
- function timedSpec(date, time, zone) {
1323
- return { date: null, date_time: `${date}T${time || "00:00"}`, time_zone: zone ?? null };
1324
- }
1325
- function setDraftStart(d, change, defaultZone) {
1326
- if (isAllDayDraft(d)) {
1327
- const oldStart = d.start.date ?? change.date ?? "";
1328
- const start2 = change.date ?? oldStart;
1329
- const days = Math.max(1, diffDays(oldStart, d.end.date ?? addDays(oldStart, 1)));
1330
- return { ...d, start: { date: start2 }, end: { date: addDays(start2, days) } };
1331
- }
1332
- const { startMs, endMs } = draftRangeUtc(d, defaultZone);
1333
- const duration = Math.max(0, endMs - startMs);
1334
- const startZone = change.timeZone !== void 0 ? change.timeZone : d.start.time_zone ?? null;
1335
- const start = timedSpec(
1336
- change.date ?? datePart(d.start),
1337
- change.time ?? timePart(d.start),
1338
- startZone
1339
- );
1340
- const newStartMs = specEpochMs(start, defaultZone);
1341
- const endZone = specZone(d.end, defaultZone);
1342
- const endWall = utcToWall(newStartMs + duration, endZone);
1343
- return {
1344
- ...d,
1345
- start,
1346
- end: { date: null, date_time: endWall, time_zone: d.end.time_zone ?? null }
1347
- };
1348
- }
1349
- function setDraftEnd(d, change) {
1350
- if (isAllDayDraft(d)) {
1351
- const last = change.date ?? allDayInclusiveEnd(d);
1352
- return { ...d, end: { date: addDays(last, 1) } };
1353
- }
1354
- const zone = change.timeZone !== void 0 ? change.timeZone : d.end.time_zone ?? null;
1355
- return {
1356
- ...d,
1357
- end: timedSpec(change.date ?? datePart(d.end), change.time ?? timePart(d.end), zone)
1358
- };
1359
- }
1360
- function setDraftAllDay(d, allDay, defaultZone, defaultStartTime = "09:00", defaultDurationMinutes = 60) {
1361
- if (allDay === isAllDayDraft(d)) return d;
1362
- if (allDay) {
1363
- const start2 = datePart(d.start);
1364
- const endDay = datePart(d.end) || start2;
1365
- const lastDay = timePart(d.end) === "00:00" && endDay > start2 ? addDays(endDay, -1) : endDay;
1366
- return {
1367
- ...d,
1368
- start: { date: start2 },
1369
- end: { date: addDays(lastDay < start2 ? start2 : lastDay, 1) }
1370
- };
1371
- }
1372
- const zone = safeZone(defaultZone);
1373
- const start = timedSpec(d.start.date ?? "", defaultStartTime, zone);
1374
- const startMs = specEpochMs(start, zone);
1375
- const end = {
1376
- date: null,
1377
- date_time: utcToWall(startMs + defaultDurationMinutes * 6e4, zone),
1378
- time_zone: zone
1379
- };
1380
- return { ...d, start, end };
1381
- }
1382
- function draftFromSlot(slot, timeZone, calendarId, title = "") {
1383
- const zone = safeZone(timeZone);
1384
- if (slot.allDay) {
1385
- const start = slot.startDate ?? utcToDate(slot.startUtc, zone);
1386
- const end = slot.endDate ?? addDays(start, 1);
1387
- return { calendar_id: calendarId, title, start: { date: start }, end: { date: end } };
1388
- }
1389
- return {
1390
- calendar_id: calendarId,
1391
- title,
1392
- start: { date: null, date_time: utcToWall(slot.startUtc, zone), time_zone: zone },
1393
- end: { date: null, date_time: utcToWall(slot.endUtc, zone), time_zone: zone }
1394
- };
1395
- }
1396
- function validateDraft(d, defaultZone) {
1397
- if (!d.title.trim()) return "titleRequired";
1398
- if (isAllDayDraft(d)) {
1399
- if (!d.start.date || !d.end.date || d.end.date <= d.start.date) return "invalidRange";
1400
- return null;
1401
- }
1402
- if (!d.start.date_time || !d.end.date_time) return "invalidRange";
1403
- const { startMs, endMs } = draftRangeUtc(d, defaultZone);
1404
- if (!(endMs >= startMs)) return "invalidRange";
1405
- return null;
1406
- }
1407
- var ARBI_WEBINAR_LABEL = "ARBI webinar";
1408
- var isWebinarConference = (c) => c?.type === "arbi" && (c.kind === "webinar" || c.label === ARBI_WEBINAR_LABEL);
1409
- var attendeeKey = (a) => a.email?.toLowerCase() || a.user_id || "";
1410
- var RRULE_WEEKDAYS = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
1411
- function weekdayOf(date) {
1412
- return RRULE_WEEKDAYS[Temporal.PlainDate.from(date).dayOfWeek - 1];
1413
- }
1414
- function setPosOf(date, preferLast = false) {
1415
- const d = Temporal.PlainDate.from(date);
1416
- const isLast = d.day + 7 > d.daysInMonth;
1417
- const nth = Math.ceil(d.day / 7);
1418
- if (isLast && (preferLast || nth > 4)) return -1;
1419
- return Math.min(nth, 4);
1420
- }
1421
- function defaultRecurrenceForm(startDate, freq = "none") {
1422
- const d = Temporal.PlainDate.from(startDate);
1423
- return {
1424
- freq,
1425
- interval: 1,
1426
- weekdays: [weekdayOf(startDate)],
1427
- monthlyMode: "day",
1428
- monthDay: d.day,
1429
- setPos: setPosOf(startDate),
1430
- setPosDay: weekdayOf(startDate),
1431
- month: d.month,
1432
- end: "never",
1433
- until: null,
1434
- count: 10,
1435
- raw: ""
1436
- };
1437
- }
1438
- function untilValue(until, ctx) {
1439
- if (ctx.allDay) return until.replace(/-/g, "");
1440
- const zone = ctx.timeZone || "UTC";
1441
- const utc = wallToUtc(`${until}T23:59:59`, zone);
1442
- const t = Temporal.Instant.from(utc).toZonedDateTimeISO("UTC");
1443
- const p = (n, w = 2) => String(n).padStart(w, "0");
1444
- return `${p(t.year, 4)}${p(t.month)}${p(t.day)}T${p(t.hour)}${p(t.minute)}${p(t.second)}Z`;
1445
- }
1446
- function buildRRule(form, ctx = {}) {
1447
- if (form.freq === "none") return null;
1448
- if (form.freq === "custom") {
1449
- const raw = form.raw.trim().replace(/^RRULE:/i, "");
1450
- return raw || null;
1451
- }
1452
- const parts = [`FREQ=${form.freq.toUpperCase()}`];
1453
- const interval = Math.max(1, Math.floor(form.interval || 1));
1454
- if (interval > 1) parts.push(`INTERVAL=${interval}`);
1455
- if (form.freq === "weekly") {
1456
- const days = RRULE_WEEKDAYS.filter((d) => form.weekdays.includes(d));
1457
- if (days.length) parts.push(`BYDAY=${days.join(",")}`);
1458
- } else if (form.freq === "monthly" || form.freq === "yearly") {
1459
- if (form.freq === "yearly") parts.push(`BYMONTH=${form.month}`);
1460
- if (form.monthlyMode === "day") {
1461
- parts.push(`BYMONTHDAY=${form.monthDay}`);
1462
- } else {
1463
- parts.push(`BYDAY=${form.setPosDay}`);
1464
- parts.push(`BYSETPOS=${form.setPos}`);
1465
- }
1466
- }
1467
- if (form.end === "until" && form.until) parts.push(`UNTIL=${untilValue(form.until, ctx)}`);
1468
- else if (form.end === "count") parts.push(`COUNT=${Math.max(1, Math.floor(form.count || 1))}`);
1469
- return parts.join(";");
1470
- }
1471
- function tokenize(rrule) {
1472
- const out = /* @__PURE__ */ new Map();
1473
- for (const seg of rrule.replace(/^RRULE:/i, "").split(";")) {
1474
- const i = seg.indexOf("=");
1475
- if (i > 0)
1476
- out.set(
1477
- seg.slice(0, i).trim().toUpperCase(),
1478
- seg.slice(i + 1).trim().toUpperCase()
1479
- );
1480
- }
1481
- return out;
1482
- }
1483
- function parseUntil(value, ctx) {
1484
- const m = /^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})(Z?))?$/.exec(value);
1485
- if (!m) return null;
1486
- const date = `${m[1]}-${m[2]}-${m[3]}`;
1487
- if (!m[4] || m[7] !== "Z") return date;
1488
- const inst = Temporal.Instant.from(`${date}T${m[4]}:${m[5]}:${m[6]}Z`);
1489
- return inst.toZonedDateTimeISO(ctx.timeZone || "UTC").toPlainDate().toString();
1490
- }
1491
- var SUPPORTED = /* @__PURE__ */ new Set([
1492
- "FREQ",
1493
- "INTERVAL",
1494
- "BYDAY",
1495
- "BYMONTHDAY",
1496
- "BYMONTH",
1497
- "BYSETPOS",
1498
- "UNTIL",
1499
- "COUNT",
1500
- "WKST"
1501
- ]);
1502
- function parseRRule(rrule, startDate, ctx = {}) {
1503
- const base = defaultRecurrenceForm(startDate);
1504
- if (!rrule || !rrule.trim()) return base;
1505
- const raw = rrule.trim().replace(/^RRULE:/i, "");
1506
- const custom = { ...base, freq: "custom", raw };
1507
- const t = tokenize(raw);
1508
- for (const key of t.keys()) if (!SUPPORTED.has(key)) return custom;
1509
- const freq = t.get("FREQ");
1510
- const form = { ...base, raw };
1511
- const interval = Number(t.get("INTERVAL") ?? "1");
1512
- if (!Number.isInteger(interval) || interval < 1) return custom;
1513
- form.interval = interval;
1514
- if (t.has("UNTIL") && t.has("COUNT")) return custom;
1515
- if (t.has("UNTIL")) {
1516
- const until = parseUntil(t.get("UNTIL") ?? "", ctx);
1517
- if (!until) return custom;
1518
- form.end = "until";
1519
- form.until = until;
1520
- } else if (t.has("COUNT")) {
1521
- const count = Number(t.get("COUNT"));
1522
- if (!Number.isInteger(count) || count < 1) return custom;
1523
- form.end = "count";
1524
- form.count = count;
1525
- }
1526
- const byday = t.get("BYDAY");
1527
- const bymonthday = t.get("BYMONTHDAY");
1528
- const bymonth = t.get("BYMONTH");
1529
- const bysetpos = t.get("BYSETPOS");
1530
- const nthWeekday = () => {
1531
- if (!byday || bymonthday) return false;
1532
- const m = /^([+-]?\d)?(MO|TU|WE|TH|FR|SA|SU)$/.exec(byday);
1533
- if (!m) return false;
1534
- const pos = Number(m[1] ?? bysetpos ?? NaN);
1535
- if (m[1] && bysetpos) return false;
1536
- if (![1, 2, 3, 4, -1].includes(pos)) return false;
1537
- form.monthlyMode = "weekday";
1538
- form.setPos = pos;
1539
- form.setPosDay = m[2];
1540
- return true;
1541
- };
1542
- const monthDay = () => {
1543
- if (!bymonthday || byday || bysetpos) return false;
1544
- const n = Number(bymonthday);
1545
- if (!Number.isInteger(n) || n < 1 || n > 31) return false;
1546
- form.monthlyMode = "day";
1547
- form.monthDay = n;
1548
- return true;
1549
- };
1550
- switch (freq) {
1551
- case "DAILY":
1552
- if (byday || bymonthday || bymonth || bysetpos) return custom;
1553
- form.freq = "daily";
1554
- return form;
1555
- case "WEEKLY": {
1556
- if (bymonthday || bymonth || bysetpos) return custom;
1557
- form.freq = "weekly";
1558
- if (byday) {
1559
- const days = byday.split(",");
1560
- if (!days.every((d) => RRULE_WEEKDAYS.includes(d))) return custom;
1561
- form.weekdays = days;
1562
- }
1563
- return form;
1564
- }
1565
- case "MONTHLY":
1566
- if (bymonth) return custom;
1567
- form.freq = "monthly";
1568
- if (!byday && !bymonthday && !bysetpos) return form;
1569
- return nthWeekday() || monthDay() ? form : custom;
1570
- case "YEARLY": {
1571
- form.freq = "yearly";
1572
- if (bymonth) {
1573
- const n = Number(bymonth);
1574
- if (!Number.isInteger(n) || n < 1 || n > 12) return custom;
1575
- form.month = n;
1576
- }
1577
- if (!byday && !bymonthday && !bysetpos) return form;
1578
- if (!bymonth) return custom;
1579
- return nthWeekday() || monthDay() ? form : custom;
1580
- }
1581
- default:
1582
- return custom;
1583
- }
1584
- }
1585
- var DEFAULT_RECURRENCE_DESCRIBE_LABELS = {
1586
- every: "Every",
1587
- day: "day",
1588
- days: "days",
1589
- week: "week",
1590
- weeks: "weeks",
1591
- month: "month",
1592
- months: "months",
1593
- year: "year",
1594
- years: "years",
1595
- on: "on",
1596
- onDay: "on day",
1597
- until: "until",
1598
- times: "times",
1599
- ordinals: { 1: "first", 2: "second", 3: "third", 4: "fourth", [-1]: "last" },
1600
- weekdayNames: {
1601
- MO: "Monday",
1602
- TU: "Tuesday",
1603
- WE: "Wednesday",
1604
- TH: "Thursday",
1605
- FR: "Friday",
1606
- SA: "Saturday",
1607
- SU: "Sunday"
1608
- },
1609
- monthNames: [
1610
- "January",
1611
- "February",
1612
- "March",
1613
- "April",
1614
- "May",
1615
- "June",
1616
- "July",
1617
- "August",
1618
- "September",
1619
- "October",
1620
- "November",
1621
- "December"
1622
- ],
1623
- doesNotRepeat: "Does not repeat",
1624
- custom: "Custom rule"
1625
- };
1626
- function describeRecurrence(form, labels = DEFAULT_RECURRENCE_DESCRIBE_LABELS) {
1627
- const l = labels;
1628
- if (form.freq === "none") return l.doesNotRepeat;
1629
- if (form.freq === "custom") return `${l.custom}: ${form.raw}`;
1630
- const unit = {
1631
- daily: [l.day, l.days],
1632
- weekly: [l.week, l.weeks],
1633
- monthly: [l.month, l.months],
1634
- yearly: [l.year, l.years]
1635
- }[form.freq];
1636
- let text = form.interval > 1 ? `${l.every} ${form.interval} ${unit[1]}` : `${l.every} ${unit[0]}`;
1637
- if (form.freq === "weekly" && form.weekdays.length) {
1638
- const days = RRULE_WEEKDAYS.filter((d) => form.weekdays.includes(d)).map(
1639
- (d) => l.weekdayNames[d]
1640
- );
1641
- text += ` ${l.on} ${days.join(", ")}`;
1642
- }
1643
- if (form.freq === "monthly" || form.freq === "yearly") {
1644
- const monthName = form.freq === "yearly" ? ` ${l.monthNames[form.month - 1]}` : "";
1645
- text += form.monthlyMode === "day" ? form.freq === "yearly" ? ` ${l.on}${monthName} ${form.monthDay}` : ` ${l.onDay} ${form.monthDay}` : ` ${l.on} ${l.ordinals[form.setPos]} ${l.weekdayNames[form.setPosDay]}${monthName ? ` of${monthName}` : ""}`;
1646
- }
1647
- if (form.end === "until" && form.until) text += `, ${l.until} ${form.until}`;
1648
- if (form.end === "count") text += `, ${form.count} ${l.times}`;
1649
- return text;
1650
- }
1651
-
1652
- // src/calendar/recurrence-labels.ts
1653
- var DEFAULT_RECURRENCE_EDITOR_LABELS = {
1654
- doesNotRepeat: "Does not repeat",
1655
- daily: "Daily",
1656
- weekly: "Weekly",
1657
- monthly: "Monthly",
1658
- yearly: "Yearly",
1659
- custom: "Custom rule\u2026",
1660
- every: "Every",
1661
- units: { daily: "day(s)", weekly: "week(s)", monthly: "month(s)", yearly: "year(s)" },
1662
- onDayOfMonth: "On day",
1663
- onThe: "On the",
1664
- ends: "Ends",
1665
- never: "Never",
1666
- onDate: "On",
1667
- after: "After",
1668
- occurrences: "occurrences",
1669
- rrule: "iCalendar rule (RRULE)",
1670
- rrulePlaceholder: "FREQ=WEEKLY;BYDAY=MO,WE",
1671
- weekdayShort: { MO: "M", TU: "T", WE: "W", TH: "T", FR: "F", SA: "S", SU: "S" }
1672
- };
1673
- var SETPOS_OPTIONS = [1, 2, 3, 4, -1];
1674
- function RecurrenceEditor({
1675
- rrule,
1676
- onChange,
1677
- startDate,
1678
- allDay,
1679
- timeZone,
1680
- labels: labelOverrides,
1681
- idPrefix = "calendar-recurrence"
1682
- }) {
1683
- const l = { ...DEFAULT_RECURRENCE_EDITOR_LABELS, ...labelOverrides };
1684
- const ctx = { allDay, timeZone };
1685
- const [form, setForm] = useState(() => parseRRule(rrule, startDate, ctx));
1686
- const [emitted, setEmitted] = useState(rrule || null);
1687
- if ((rrule || null) !== emitted) {
1688
- setEmitted(rrule || null);
1689
- setForm(parseRRule(rrule, startDate, ctx));
1690
- }
1691
- const update = (patch) => {
1692
- const next = { ...form, ...patch };
1693
- setForm(next);
1694
- const out = buildRRule(next, ctx);
1695
- setEmitted(out);
1696
- onChange(out);
1697
- };
1698
- const setFreq = (freq) => {
1699
- if (freq === "custom") {
1700
- update({ freq, raw: buildRRule(form, ctx) ?? form.raw ?? "" });
1701
- return;
1702
- }
1703
- const seeded = { freq };
1704
- if (form.freq === "none" || form.freq === "custom") {
1705
- seeded.weekdays = [weekdayOf(startDate)];
1706
- seeded.monthDay = Number(startDate.slice(8, 10));
1707
- seeded.setPos = setPosOf(startDate);
1708
- seeded.setPosDay = weekdayOf(startDate);
1709
- seeded.month = Number(startDate.slice(5, 7));
1710
- seeded.interval = 1;
1711
- }
1712
- update(seeded);
1713
- };
1714
- const weekdayName = (d) => DEFAULT_RECURRENCE_DESCRIBE_LABELS.weekdayNames[d];
1715
- const ordinal = (p) => DEFAULT_RECURRENCE_DESCRIBE_LABELS.ordinals[p];
1716
- const monthNames = DEFAULT_RECURRENCE_DESCRIBE_LABELS.monthNames;
1717
- return /* @__PURE__ */ jsxs("div", { className: "space-y-2", "data-testid": "calendar-recurrence", children: [
1718
- /* @__PURE__ */ jsxs(
1719
- NativeSelect,
1720
- {
1721
- id: `${idPrefix}-freq`,
1722
- "aria-label": l.doesNotRepeat,
1723
- value: form.freq,
1724
- onChange: (e) => setFreq(e.target.value),
1725
- "data-testid": "calendar-recurrence-freq",
1726
- className: "w-full sm:w-56",
1727
- children: [
1728
- /* @__PURE__ */ jsx("option", { value: "none", children: l.doesNotRepeat }),
1729
- /* @__PURE__ */ jsx("option", { value: "daily", children: l.daily }),
1730
- /* @__PURE__ */ jsx("option", { value: "weekly", children: l.weekly }),
1731
- /* @__PURE__ */ jsx("option", { value: "monthly", children: l.monthly }),
1732
- /* @__PURE__ */ jsx("option", { value: "yearly", children: l.yearly }),
1733
- /* @__PURE__ */ jsx("option", { value: "custom", children: l.custom })
1734
- ]
1735
- }
1736
- ),
1737
- form.freq === "custom" && /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
1738
- /* @__PURE__ */ jsx("label", { htmlFor: `${idPrefix}-raw`, className: "text-xs text-muted-foreground", children: l.rrule }),
1739
- /* @__PURE__ */ jsx(
1740
- Input,
1741
- {
1742
- id: `${idPrefix}-raw`,
1743
- value: form.raw,
1744
- placeholder: l.rrulePlaceholder,
1745
- onChange: (e) => update({ raw: e.target.value }),
1746
- "data-testid": "calendar-recurrence-raw",
1747
- className: "font-mono text-xs"
1748
- }
1749
- )
1750
- ] }),
1751
- form.freq !== "none" && form.freq !== "custom" && /* @__PURE__ */ jsxs("div", { className: "space-y-2 rounded-md border border-border p-2", children: [
1752
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 text-sm", children: [
1753
- /* @__PURE__ */ jsx("span", { children: l.every }),
1754
- /* @__PURE__ */ jsx(
1755
- Input,
1756
- {
1757
- type: "number",
1758
- min: 1,
1759
- max: 999,
1760
- "aria-label": l.every,
1761
- className: "h-8 w-16",
1762
- value: form.interval,
1763
- onChange: (e) => update({ interval: Math.max(1, Number(e.target.value) || 1) }),
1764
- "data-testid": "calendar-recurrence-interval"
1765
- }
1766
- ),
1767
- /* @__PURE__ */ jsx("span", { children: l.units[form.freq] })
1768
- ] }),
1769
- form.freq === "weekly" && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", role: "group", "aria-label": l.weekly, children: RRULE_WEEKDAYS.map((d) => {
1770
- const on = form.weekdays.includes(d);
1771
- return /* @__PURE__ */ jsx(
1772
- "button",
1773
- {
1774
- type: "button",
1775
- "aria-pressed": on,
1776
- "aria-label": weekdayName(d),
1777
- title: weekdayName(d),
1778
- onClick: () => {
1779
- const next = on ? form.weekdays.filter((x) => x !== d) : [...form.weekdays, d];
1780
- update({ weekdays: next.length ? next : form.weekdays });
1781
- },
1782
- "data-testid": `calendar-recurrence-day-${d}`,
1783
- className: cn(
1784
- "h-8 w-8 rounded-full border text-xs font-medium transition-colors",
1785
- on ? "border-primary bg-primary text-primary-foreground" : "border-border hover:bg-muted"
1786
- ),
1787
- children: l.weekdayShort[d]
1788
- },
1789
- d
1790
- );
1791
- }) }),
1792
- (form.freq === "monthly" || form.freq === "yearly") && /* @__PURE__ */ jsxs("div", { className: "space-y-1.5 text-sm", children: [
1793
- form.freq === "yearly" && /* @__PURE__ */ jsx(
1794
- NativeSelect,
1795
- {
1796
- "aria-label": l.yearly,
1797
- value: form.month,
1798
- onChange: (e) => update({ month: Number(e.target.value) }),
1799
- "data-testid": "calendar-recurrence-month",
1800
- className: "h-8",
1801
- children: monthNames.map((m, i) => /* @__PURE__ */ jsx("option", { value: i + 1, children: m }, m))
1802
- }
1803
- ),
1804
- /* @__PURE__ */ jsxs("label", { className: "flex flex-wrap items-center gap-2", children: [
1805
- /* @__PURE__ */ jsx(
1806
- "input",
1807
- {
1808
- type: "radio",
1809
- name: `${idPrefix}-monthly`,
1810
- checked: form.monthlyMode === "day",
1811
- onChange: () => update({ monthlyMode: "day" }),
1812
- "data-testid": "calendar-recurrence-mode-day"
1813
- }
1814
- ),
1815
- /* @__PURE__ */ jsx("span", { children: l.onDayOfMonth }),
1816
- /* @__PURE__ */ jsx(
1817
- Input,
1818
- {
1819
- type: "number",
1820
- min: 1,
1821
- max: 31,
1822
- "aria-label": l.onDayOfMonth,
1823
- className: "h-8 w-16",
1824
- value: form.monthDay,
1825
- disabled: form.monthlyMode !== "day",
1826
- onChange: (e) => update({ monthDay: Math.min(31, Math.max(1, Number(e.target.value) || 1)) }),
1827
- "data-testid": "calendar-recurrence-monthday"
1828
- }
1829
- )
1830
- ] }),
1831
- /* @__PURE__ */ jsxs("label", { className: "flex flex-wrap items-center gap-2", children: [
1832
- /* @__PURE__ */ jsx(
1833
- "input",
1834
- {
1835
- type: "radio",
1836
- name: `${idPrefix}-monthly`,
1837
- checked: form.monthlyMode === "weekday",
1838
- onChange: () => update({ monthlyMode: "weekday" }),
1839
- "data-testid": "calendar-recurrence-mode-weekday"
1840
- }
1841
- ),
1842
- /* @__PURE__ */ jsx("span", { children: l.onThe }),
1843
- /* @__PURE__ */ jsx(
1844
- NativeSelect,
1845
- {
1846
- "aria-label": l.onThe,
1847
- className: "h-8",
1848
- value: form.setPos,
1849
- disabled: form.monthlyMode !== "weekday",
1850
- onChange: (e) => update({ setPos: Number(e.target.value) }),
1851
- "data-testid": "calendar-recurrence-setpos",
1852
- children: SETPOS_OPTIONS.map((p) => /* @__PURE__ */ jsx("option", { value: p, children: ordinal(p) }, p))
1853
- }
1854
- ),
1855
- /* @__PURE__ */ jsx(
1856
- NativeSelect,
1857
- {
1858
- "aria-label": l.weekly,
1859
- className: "h-8",
1860
- value: form.setPosDay,
1861
- disabled: form.monthlyMode !== "weekday",
1862
- onChange: (e) => update({ setPosDay: e.target.value }),
1863
- "data-testid": "calendar-recurrence-setpos-day",
1864
- children: RRULE_WEEKDAYS.map((d) => /* @__PURE__ */ jsx("option", { value: d, children: weekdayName(d) }, d))
1865
- }
1866
- )
1867
- ] })
1868
- ] }),
1869
- /* @__PURE__ */ jsxs("div", { className: "space-y-1.5 text-sm", children: [
1870
- /* @__PURE__ */ jsx("div", { className: "text-xs font-medium text-muted-foreground", children: l.ends }),
1871
- /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2", children: [
1872
- /* @__PURE__ */ jsx(
1873
- "input",
1874
- {
1875
- type: "radio",
1876
- name: `${idPrefix}-end`,
1877
- checked: form.end === "never",
1878
- onChange: () => update({ end: "never" }),
1879
- "data-testid": "calendar-recurrence-end-never"
1880
- }
1881
- ),
1882
- l.never
1883
- ] }),
1884
- /* @__PURE__ */ jsxs("label", { className: "flex flex-wrap items-center gap-2", children: [
1885
- /* @__PURE__ */ jsx(
1886
- "input",
1887
- {
1888
- type: "radio",
1889
- name: `${idPrefix}-end`,
1890
- checked: form.end === "until",
1891
- onChange: () => update({ end: "until", until: form.until ?? startDate }),
1892
- "data-testid": "calendar-recurrence-end-until"
1893
- }
1894
- ),
1895
- l.onDate,
1896
- /* @__PURE__ */ jsx(
1897
- Input,
1898
- {
1899
- type: "date",
1900
- "aria-label": l.onDate,
1901
- className: "h-8 w-40",
1902
- value: form.until ?? "",
1903
- min: startDate,
1904
- disabled: form.end !== "until",
1905
- onChange: (e) => update({ until: e.target.value || null }),
1906
- "data-testid": "calendar-recurrence-until"
1907
- }
1908
- )
1909
- ] }),
1910
- /* @__PURE__ */ jsxs("label", { className: "flex flex-wrap items-center gap-2", children: [
1911
- /* @__PURE__ */ jsx(
1912
- "input",
1913
- {
1914
- type: "radio",
1915
- name: `${idPrefix}-end`,
1916
- checked: form.end === "count",
1917
- onChange: () => update({ end: "count" }),
1918
- "data-testid": "calendar-recurrence-end-count"
1919
- }
1920
- ),
1921
- l.after,
1922
- /* @__PURE__ */ jsx(
1923
- Input,
1924
- {
1925
- type: "number",
1926
- min: 1,
1927
- max: 999,
1928
- "aria-label": l.occurrences,
1929
- className: "h-8 w-20",
1930
- value: form.count,
1931
- disabled: form.end !== "count",
1932
- onChange: (e) => update({ count: Math.max(1, Number(e.target.value) || 1) }),
1933
- "data-testid": "calendar-recurrence-count"
1934
- }
1935
- ),
1936
- l.occurrences
1937
- ] })
1938
- ] }),
1939
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", "data-testid": "calendar-recurrence-summary", children: describeRecurrence(form) })
1940
- ] })
1941
- ] });
1942
- }
1943
-
1944
- // src/calendar/format.ts
1945
- function formatZoneLabel(zone, at = Date.now()) {
1946
- return `${zoneCity(zone)} (${zoneOffsetLabel(zone, at)})`;
1947
- }
1948
- function formatEventWhen(e, zone, locale) {
1949
- const z = safeZone(zone);
1950
- const dayFmt = {
1951
- weekday: "short",
1952
- day: "numeric",
1953
- month: "short",
1954
- year: "numeric"
1955
- };
1956
- if (e.all_day) {
1957
- const start = e.start.date ?? utcToDate(e.start_utc, z);
1958
- const endExcl = e.end.date ?? utcToDate(e.end_utc, z);
1959
- const last = endExcl > start ? addDays(endExcl, -1) : start;
1960
- const a2 = formatPlainDate(start, dayFmt, locale);
1961
- return last === start ? a2 : `${a2} \u2013 ${formatPlainDate(last, dayFmt, locale)}`;
1962
- }
1963
- const timeFmt = { hour: "numeric", minute: "2-digit" };
1964
- const sameDay = utcToDate(e.start_utc, z) === utcToDate(e.end_utc, z);
1965
- const a = `${formatInZone(e.start_utc, z, dayFmt, locale)} \xB7 ${formatInZone(e.start_utc, z, timeFmt, locale)}`;
1966
- return sameDay ? `${a} \u2013 ${formatInZone(e.end_utc, z, timeFmt, locale)}` : `${a} \u2013 ${formatInZone(e.end_utc, z, dayFmt, locale)} \xB7 ${formatInZone(e.end_utc, z, timeFmt, locale)}`;
1967
- }
1968
- function TimeZonePicker({
1969
- value,
1970
- onChange,
1971
- zones,
1972
- allowNone,
1973
- onClear,
1974
- id,
1975
- ariaLabel,
1976
- disabled,
1977
- labels: labelOverrides,
1978
- className
1979
- }) {
1980
- const labels = mergeLabels(labelOverrides);
1981
- const [open, setOpen] = useState(false);
1982
- const options = useMemo(() => {
1983
- const now = Date.now();
1984
- const list = zones?.length ? zones : listTimeZones();
1985
- return list.map((z) => {
1986
- let offset = 0;
1987
- try {
1988
- offset = zoneOffsetMinutes(z, now);
1989
- } catch {
1990
- return null;
1991
- }
1992
- return { zone: z, offset, label: zoneOffsetLabel(z, now), city: zoneCity(z) };
1993
- }).filter((o) => o !== null).sort((a, b) => a.offset - b.offset || a.zone.localeCompare(b.zone));
1994
- }, [zones, open]);
1995
- return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
1996
- /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
1997
- "button",
1998
- {
1999
- type: "button",
2000
- id,
2001
- disabled,
2002
- "aria-label": ariaLabel ?? labels.timeZone,
2003
- "aria-haspopup": "listbox",
2004
- "aria-expanded": open,
2005
- "data-testid": "calendar-tz-picker",
2006
- className: cn(
2007
- "inline-flex h-9 min-w-0 items-center gap-2 rounded-md border border-input bg-background px-2 text-left text-sm shadow-sm",
2008
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50",
2009
- className
2010
- ),
2011
- children: [
2012
- /* @__PURE__ */ jsx(Globe, { className: "h-4 w-4 shrink-0 text-muted-foreground", "aria-hidden": true }),
2013
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: value ? formatZoneLabel(value) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: labels.none }) }),
2014
- /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 shrink-0 text-muted-foreground", "aria-hidden": true })
2015
- ]
2016
- }
2017
- ) }),
2018
- /* @__PURE__ */ jsx(PopoverContent, { className: "w-[320px] p-0", align: "start", children: /* @__PURE__ */ jsxs(
2019
- Command,
2020
- {
2021
- filter: (itemValue, search) => itemValue.toLowerCase().includes(search.toLowerCase().trim()) ? 1 : 0,
2022
- children: [
2023
- /* @__PURE__ */ jsx(CommandInput, { placeholder: labels.searchTimeZones, "data-testid": "calendar-tz-search" }),
2024
- /* @__PURE__ */ jsxs(CommandList, { className: "max-h-72", children: [
2025
- /* @__PURE__ */ jsx(CommandEmpty, { children: "\u2014" }),
2026
- /* @__PURE__ */ jsxs(CommandGroup, { children: [
2027
- allowNone && /* @__PURE__ */ jsxs(
2028
- CommandItem,
2029
- {
2030
- value: `__none__ ${labels.none}`,
2031
- onSelect: () => {
2032
- onClear?.();
2033
- setOpen(false);
2034
- },
2035
- children: [
2036
- /* @__PURE__ */ jsx(Check, { className: cn("mr-2 h-4 w-4", value ? "opacity-0" : "opacity-100") }),
2037
- labels.none
2038
- ]
2039
- }
2040
- ),
2041
- options.map((o) => /* @__PURE__ */ jsxs(
2042
- CommandItem,
2043
- {
2044
- value: `${o.zone} ${o.city} ${o.label}`,
2045
- onSelect: () => {
2046
- onChange(o.zone);
2047
- setOpen(false);
2048
- },
2049
- "data-testid": `calendar-tz-option-${o.zone}`,
2050
- children: [
2051
- /* @__PURE__ */ jsx(
2052
- Check,
2053
- {
2054
- className: cn(
2055
- "mr-2 h-4 w-4 shrink-0",
2056
- value === o.zone ? "opacity-100" : "opacity-0"
2057
- )
2058
- }
2059
- ),
2060
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: o.zone.replace(/_/g, " ") }),
2061
- /* @__PURE__ */ jsx("span", { className: "ml-2 shrink-0 text-xs tabular-nums text-muted-foreground", children: o.label })
2062
- ]
2063
- },
2064
- o.zone
2065
- ))
2066
- ] })
2067
- ] })
2068
- ]
2069
- }
2070
- ) })
2071
- ] });
2072
- }
2073
-
2074
- // src/calendar/reminders.ts
2075
- var UNIT_MINUTES = {
2076
- minutes: 1,
2077
- hours: 60,
2078
- days: 24 * 60,
2079
- weeks: 7 * 24 * 60
2080
- };
2081
- function splitReminderMinutes(minutes) {
2082
- const abs = Math.abs(minutes);
2083
- if (abs === 0) return { value: 0, unit: "minutes" };
2084
- for (const unit of ["weeks", "days", "hours"]) {
2085
- if (abs % UNIT_MINUTES[unit] === 0) return { value: minutes / UNIT_MINUTES[unit], unit };
2086
- }
2087
- return { value: minutes, unit: "minutes" };
2088
- }
2089
- function reminderToMinutes(value, unit) {
2090
- return Math.round(value * UNIT_MINUTES[unit]);
2091
- }
2092
- var DEFAULT_REMINDER_FORMAT_LABELS = {
2093
- atStart: "At time of event",
2094
- before: "before",
2095
- after: "after",
2096
- minute: "minute",
2097
- minutes: "minutes",
2098
- hour: "hour",
2099
- hours: "hours",
2100
- day: "day",
2101
- days: "days",
2102
- week: "week",
2103
- weeks: "weeks"
2104
- };
2105
- function formatReminder(minutesBefore, labels = DEFAULT_REMINDER_FORMAT_LABELS) {
2106
- if (minutesBefore === 0) return labels.atStart;
2107
- const { value, unit } = splitReminderMinutes(Math.abs(minutesBefore));
2108
- const singular = {
2109
- minutes: labels.minute,
2110
- hours: labels.hour,
2111
- days: labels.day,
2112
- weeks: labels.week
2113
- }[unit];
2114
- const word = value === 1 ? singular : labels[unit];
2115
- return `${value} ${word} ${minutesBefore > 0 ? labels.before : labels.after}`;
2116
- }
2117
- var REMINDER_PRESETS = [0, 5, 10, 15, 30, 60, 120, 24 * 60, 2 * 24 * 60, 7 * 24 * 60];
2118
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2119
- function Chip({
2120
- children,
2121
- onRemove,
2122
- removeLabel
2123
- }) {
2124
- return /* @__PURE__ */ jsxs("span", { className: "inline-flex max-w-full items-center gap-1 rounded-full border border-border bg-muted/60 py-0.5 pl-2 pr-1 text-xs", children: [
2125
- children,
2126
- /* @__PURE__ */ jsx(
2127
- "button",
2128
- {
2129
- type: "button",
2130
- onClick: onRemove,
2131
- "aria-label": removeLabel,
2132
- className: "rounded-full p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground",
2133
- children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
2134
- }
2135
- )
2136
- ] });
2137
- }
2138
- function showAsLabel(v, l) {
2139
- return {
2140
- free: l.free,
2141
- tentative: l.tentative,
2142
- busy: l.busy,
2143
- oof: l.oof,
2144
- working_elsewhere: l.workingElsewhere
2145
- }[v];
2146
- }
2147
- function EventEditor({
2148
- value,
2149
- onChange,
2150
- onSave,
2151
- onCancel,
2152
- onDelete,
2153
- calendars,
2154
- mode,
2155
- canAddCall,
2156
- onAddCall,
2157
- canAddWebinar,
2158
- isWebinarAttendee,
2159
- defaultTimeZone,
2160
- schedulingSlot,
2161
- attendeeSuggestions,
2162
- onAttendeeQueryChange,
2163
- saving,
2164
- error,
2165
- labels: labelOverrides,
2166
- zones,
2167
- className
2168
- }) {
2169
- const l = useMemo(() => mergeLabels(labelOverrides), [labelOverrides]);
2170
- const uid = useId();
2171
- const id = (s) => `${uid}-${s}`;
2172
- const allDay = isAllDayDraft(value);
2173
- const startZone = value.start.time_zone ?? defaultTimeZone;
2174
- const endZone = value.end.time_zone ?? startZone;
2175
- const [showZones, setShowZones] = useState(
2176
- () => !allDay && (startZone !== defaultTimeZone || endZone !== startZone)
2177
- );
2178
- const [attendeeInput, setAttendeeInput] = useState("");
2179
- const [categoryInput, setCategoryInput] = useState("");
2180
- const [linkInput, setLinkInput] = useState(null);
2181
- const [attachUrl, setAttachUrl] = useState("");
2182
- const [attachTitle, setAttachTitle] = useState("");
2183
- const [addingCall, setAddingCall] = useState(false);
2184
- const [webinar, setWebinar] = useState(false);
2185
- const [touched, setTouched] = useState(false);
2186
- const [focus, setFocus] = useState(() => !!value.auto_decline && value.show_as !== "oof");
2187
- const showDecline = value.show_as === "oof" || focus;
2188
- const set = (patch) => onChange({ ...value, ...patch });
2189
- const problem = validateDraft(value, defaultTimeZone);
2190
- const shownProblem = problem === "invalidRange" || touched && problem ? problem : null;
2191
- const writable = calendars.filter((c) => c.canWrite && !c.locked);
2192
- const calendarColor = calendars.find((c) => c.id === value.calendar_id)?.color;
2193
- const attendees = value.attendees ?? [];
2194
- const addAttendees = (raw) => {
2195
- const emails = raw.split(/[\s,;]+/).map((s) => s.trim()).filter((s) => EMAIL_RE.test(s));
2196
- if (!emails.length) return false;
2197
- const have = new Set(attendees.map((a) => (a.email ?? "").toLowerCase()));
2198
- const next = [...attendees];
2199
- for (const email of emails) {
2200
- if (have.has(email.toLowerCase())) continue;
2201
- have.add(email.toLowerCase());
2202
- next.push({ email, role: "required", kind: "individual" });
2203
- }
2204
- set({ attendees: next });
2205
- return true;
2206
- };
2207
- const onAttendeeKey = (e) => {
2208
- if (e.key === "Enter" || e.key === "," || e.key === ";" || e.key === "Tab") {
2209
- if (attendeeInput.trim() && addAttendees(attendeeInput)) {
2210
- e.preventDefault();
2211
- setAttendeeInput("");
2212
- }
2213
- } else if (e.key === "Backspace" && !attendeeInput && attendees.length) {
2214
- set({ attendees: attendees.slice(0, -1) });
2215
- }
2216
- };
2217
- const categories = value.categories ?? [];
2218
- const addCategory = () => {
2219
- const c = categoryInput.trim();
2220
- if (c && !categories.includes(c)) set({ categories: [...categories, c] });
2221
- setCategoryInput("");
2222
- };
2223
- const reminders = value.reminders ?? [];
2224
- const setReminder = (i, r) => set({ reminders: reminders.map((x, j) => j === i ? r : x) });
2225
- const addArbiCall = async () => {
2226
- if (!onAddCall) return;
2227
- setAddingCall(true);
2228
- try {
2229
- const call = await onAddCall();
2230
- if (call)
2231
- set({
2232
- conference: { type: "arbi", url: call.url, call_id: call.call_id, label: l.arbiCall }
2233
- });
2234
- } finally {
2235
- setAddingCall(false);
2236
- }
2237
- };
2238
- const addArbiWebinar = () => set({
2239
- conference: { type: "arbi", kind: "webinar", url: "", label: ARBI_WEBINAR_LABEL, roles: {} }
2240
- });
2241
- const isWebinar = isWebinarConference(value.conference);
2242
- const roles = value.conference?.roles ?? {};
2243
- const setRole = (a, role) => value.conference && set({ conference: { ...value.conference, roles: { ...roles, [attendeeKey(a)]: role } } });
2244
- const attachments = value.attachments ?? [];
2245
- const addAttachment = () => {
2246
- const url = attachUrl.trim();
2247
- if (!url) return;
2248
- set({ attachments: [...attachments, { url, title: attachTitle.trim() || null }] });
2249
- setAttachUrl("");
2250
- setAttachTitle("");
2251
- };
2252
- const startDate = specDate(value.start);
2253
- const submit = () => {
2254
- setTouched(true);
2255
- if (problem || saving) return;
2256
- onSave();
2257
- };
2258
- return /* @__PURE__ */ jsxs(
2259
- "form",
2260
- {
2261
- className: cn("flex h-full min-h-0 flex-col", className),
2262
- onSubmit: (e) => {
2263
- e.preventDefault();
2264
- submit();
2265
- },
2266
- "data-testid": "calendar-event-editor",
2267
- "aria-label": mode === "create" ? l.createTitle : l.editTitle,
2268
- children: [
2269
- /* @__PURE__ */ jsxs("div", { className: "min-h-0 flex-1 space-y-4 overflow-y-auto p-1 scrollbar-themed", children: [
2270
- /* @__PURE__ */ jsx(
2271
- Input,
2272
- {
2273
- id: id("title"),
2274
- "aria-label": l.title,
2275
- placeholder: l.titlePlaceholder,
2276
- value: value.title,
2277
- autoFocus: mode === "create",
2278
- onChange: (e) => set({ title: e.target.value }),
2279
- className: "h-11 border-0 border-b border-border rounded-none px-1 text-lg font-medium shadow-none focus-visible:ring-0 focus-visible:border-primary",
2280
- "data-testid": "calendar-editor-title"
2281
- }
2282
- ),
2283
- /* @__PURE__ */ jsxs(FieldRow, { icon: /* @__PURE__ */ jsx(Clock, { className: "h-4 w-4" }), children: [
2284
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
2285
- /* @__PURE__ */ jsx(
2286
- Input,
2287
- {
2288
- type: "date",
2289
- "aria-label": l.start,
2290
- className: "h-9 w-[9.5rem]",
2291
- value: startDate,
2292
- onChange: (e) => e.target.value && onChange(setDraftStart(value, { date: e.target.value }, defaultTimeZone)),
2293
- "data-testid": "calendar-editor-start-date"
2294
- }
2295
- ),
2296
- !allDay && /* @__PURE__ */ jsx(
2297
- Input,
2298
- {
2299
- type: "time",
2300
- "aria-label": l.startTime,
2301
- className: "h-9 w-[7rem]",
2302
- step: 300,
2303
- value: specTime(value.start),
2304
- onChange: (e) => e.target.value && onChange(setDraftStart(value, { time: e.target.value }, defaultTimeZone)),
2305
- "data-testid": "calendar-editor-start-time"
2306
- }
2307
- ),
2308
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2013" }),
2309
- !allDay && /* @__PURE__ */ jsx(
2310
- Input,
2311
- {
2312
- type: "time",
2313
- "aria-label": l.endTime,
2314
- className: "h-9 w-[7rem]",
2315
- step: 300,
2316
- value: specTime(value.end),
2317
- onChange: (e) => e.target.value && onChange(setDraftEnd(value, { time: e.target.value })),
2318
- "data-testid": "calendar-editor-end-time"
2319
- }
2320
- ),
2321
- /* @__PURE__ */ jsx(
2322
- Input,
2323
- {
2324
- type: "date",
2325
- "aria-label": l.end,
2326
- className: "h-9 w-[9.5rem]",
2327
- value: allDay ? allDayInclusiveEnd(value) : specDate(value.end),
2328
- min: startDate,
2329
- onChange: (e) => e.target.value && onChange(setDraftEnd(value, { date: e.target.value })),
2330
- "data-testid": "calendar-editor-end-date"
2331
- }
2332
- )
2333
- ] }),
2334
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-4 pt-1 text-sm", children: [
2335
- /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2", children: [
2336
- /* @__PURE__ */ jsx(
2337
- Switch,
2338
- {
2339
- checked: allDay,
2340
- onCheckedChange: (on) => onChange(setDraftAllDay(value, on, startZone)),
2341
- "aria-label": l.allDay,
2342
- "data-testid": "calendar-editor-all-day"
2343
- }
2344
- ),
2345
- l.allDay
2346
- ] }),
2347
- !allDay && /* @__PURE__ */ jsxs(
2348
- "button",
2349
- {
2350
- type: "button",
2351
- className: "inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground",
2352
- onClick: () => setShowZones((v) => !v),
2353
- "aria-expanded": showZones,
2354
- "data-testid": "calendar-editor-zones-toggle",
2355
- children: [
2356
- /* @__PURE__ */ jsx(Globe, { className: "h-3.5 w-3.5" }),
2357
- l.separateTimeZones
2358
- ]
2359
- }
2360
- )
2361
- ] }),
2362
- !allDay && showZones && /* @__PURE__ */ jsxs("div", { className: "grid gap-2 pt-1 sm:grid-cols-2", children: [
2363
- /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
2364
- /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: l.startTimeZone }),
2365
- /* @__PURE__ */ jsx(
2366
- TimeZonePicker,
2367
- {
2368
- value: startZone,
2369
- zones,
2370
- ariaLabel: l.startTimeZone,
2371
- labels: labelOverrides,
2372
- className: "w-full",
2373
- onChange: (z) => {
2374
- const moved = setDraftStart(value, { timeZone: z }, defaultTimeZone);
2375
- const endFollows = (value.end.time_zone ?? startZone) === startZone;
2376
- onChange(endFollows ? { ...moved, end: { ...moved.end, time_zone: z } } : moved);
2377
- }
2378
- }
2379
- )
2380
- ] }),
2381
- /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
2382
- /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: l.endTimeZone }),
2383
- /* @__PURE__ */ jsx(
2384
- TimeZonePicker,
2385
- {
2386
- value: endZone,
2387
- zones,
2388
- ariaLabel: l.endTimeZone,
2389
- labels: labelOverrides,
2390
- className: "w-full",
2391
- onChange: (z) => onChange(setDraftEnd(value, { timeZone: z }))
2392
- }
2393
- )
2394
- ] })
2395
- ] })
2396
- ] }),
2397
- /* @__PURE__ */ jsx(FieldRow, { icon: /* @__PURE__ */ jsx(Repeat, { className: "h-4 w-4" }), children: /* @__PURE__ */ jsx(
2398
- RecurrenceEditor,
2399
- {
2400
- rrule: value.recurrence?.rrule ?? null,
2401
- startDate,
2402
- allDay,
2403
- timeZone: startZone,
2404
- idPrefix: id("rrule"),
2405
- onChange: (rrule) => set({
2406
- recurrence: rrule ? { ...value.recurrence ?? {}, rrule } : value.recurrence?.rdates?.length ? { ...value.recurrence, rrule: null } : null
2407
- })
2408
- }
2409
- ) }),
2410
- writable.length > 0 && /* @__PURE__ */ jsx(FieldRow, { icon: /* @__PURE__ */ jsx(CalendarDays, { className: "h-4 w-4" }), children: /* @__PURE__ */ jsxs(
2411
- NativeSelect,
2412
- {
2413
- "aria-label": l.calendar,
2414
- value: value.calendar_id ?? "",
2415
- onChange: (e) => set({ calendar_id: e.target.value || void 0 }),
2416
- "data-testid": "calendar-editor-calendar",
2417
- className: "w-full sm:w-72",
2418
- children: [
2419
- !value.calendar_id && /* @__PURE__ */ jsx("option", { value: "", children: l.calendar }),
2420
- writable.map((c) => /* @__PURE__ */ jsx("option", { value: c.id, children: c.name ?? c.id }, c.id))
2421
- ]
2422
- }
2423
- ) }),
2424
- /* @__PURE__ */ jsx(FieldRow, { icon: /* @__PURE__ */ jsx(Video, { className: "h-4 w-4" }), children: value.conference ? /* @__PURE__ */ jsxs(
2425
- "div",
2426
- {
2427
- className: "flex min-w-0 items-center gap-2 text-sm",
2428
- "data-testid": "calendar-editor-conference",
2429
- children: [
2430
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: isWebinar ? l.arbiWebinar : value.conference.label || (value.conference.type === "arbi" ? l.arbiCall : l.conference) }),
2431
- value.conference.url ? /* @__PURE__ */ jsx(
2432
- "a",
2433
- {
2434
- href: value.conference.url,
2435
- target: "_blank",
2436
- rel: "noreferrer",
2437
- className: "min-w-0 truncate text-xs text-primary underline-offset-2 hover:underline",
2438
- children: value.conference.url.replace(/#.*$/, "")
2439
- }
2440
- ) : /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate text-xs text-muted-foreground", children: l.webinarLinkOnSave }),
2441
- /* @__PURE__ */ jsx(
2442
- "button",
2443
- {
2444
- type: "button",
2445
- className: "rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground",
2446
- "aria-label": l.removeConference,
2447
- onClick: () => set({ conference: null }),
2448
- "data-testid": "calendar-editor-conference-remove",
2449
- children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
2450
- }
2451
- )
2452
- ]
2453
- }
2454
- ) : linkInput !== null ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2455
- /* @__PURE__ */ jsx(
2456
- Input,
2457
- {
2458
- type: "url",
2459
- "aria-label": l.addMeetingLink,
2460
- placeholder: l.meetingLinkPlaceholder,
2461
- value: linkInput,
2462
- autoFocus: true,
2463
- onChange: (e) => setLinkInput(e.target.value),
2464
- onKeyDown: (e) => {
2465
- if (e.key === "Enter") {
2466
- e.preventDefault();
2467
- if (linkInput.trim())
2468
- set({ conference: { type: "external", url: linkInput.trim() } });
2469
- setLinkInput(null);
2470
- } else if (e.key === "Escape") setLinkInput(null);
2471
- },
2472
- "data-testid": "calendar-editor-link-input"
2473
- }
2474
- ),
2475
- /* @__PURE__ */ jsx(
2476
- Button,
2477
- {
2478
- type: "button",
2479
- size: "sm",
2480
- variant: "outline",
2481
- onClick: () => {
2482
- if (linkInput.trim())
2483
- set({ conference: { type: "external", url: linkInput.trim() } });
2484
- setLinkInput(null);
2485
- },
2486
- children: /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" })
2487
- }
2488
- )
2489
- ] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
2490
- canAddCall && onAddCall && /* @__PURE__ */ jsxs(
2491
- Button,
2492
- {
2493
- type: "button",
2494
- size: "sm",
2495
- variant: "secondary",
2496
- disabled: addingCall,
2497
- onClick: webinar ? addArbiWebinar : addArbiCall,
2498
- "data-testid": "calendar-editor-add-call",
2499
- children: [
2500
- /* @__PURE__ */ jsx(Video, { className: "mr-1 h-4 w-4" }),
2501
- webinar ? l.addArbiWebinar : l.addArbiCall
2502
- ]
2503
- }
2504
- ),
2505
- canAddCall && onAddCall && canAddWebinar && /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-sm", children: [
2506
- /* @__PURE__ */ jsx(
2507
- Switch,
2508
- {
2509
- checked: webinar,
2510
- onCheckedChange: setWebinar,
2511
- "aria-label": l.webinar,
2512
- "data-testid": "calendar-editor-webinar"
2513
- }
2514
- ),
2515
- l.webinar
2516
- ] }),
2517
- /* @__PURE__ */ jsx(
2518
- Button,
2519
- {
2520
- type: "button",
2521
- size: "sm",
2522
- variant: "ghost",
2523
- onClick: () => setLinkInput(""),
2524
- "data-testid": "calendar-editor-add-link",
2525
- children: l.addMeetingLink
2526
- }
2527
- )
2528
- ] }) }),
2529
- /* @__PURE__ */ jsx(FieldRow, { icon: /* @__PURE__ */ jsx(MapPin, { className: "h-4 w-4" }), children: /* @__PURE__ */ jsx(
2530
- Input,
2531
- {
2532
- "aria-label": l.location,
2533
- placeholder: l.location,
2534
- value: value.location ?? "",
2535
- onChange: (e) => set({ location: e.target.value || null }),
2536
- "data-testid": "calendar-editor-location"
2537
- }
2538
- ) }),
2539
- /* @__PURE__ */ jsxs(FieldRow, { icon: /* @__PURE__ */ jsx(Users, { className: "h-4 w-4" }), children: [
2540
- /* @__PURE__ */ jsx(
2541
- Input,
2542
- {
2543
- type: "email",
2544
- multiple: true,
2545
- "aria-label": l.addAttendee,
2546
- placeholder: l.addAttendee,
2547
- value: attendeeInput,
2548
- onChange: (e) => {
2549
- setAttendeeInput(e.target.value);
2550
- onAttendeeQueryChange?.(e.target.value);
2551
- },
2552
- onKeyDown: onAttendeeKey,
2553
- onBlur: () => {
2554
- if (attendeeInput.trim() && addAttendees(attendeeInput)) setAttendeeInput("");
2555
- },
2556
- "data-testid": "calendar-editor-attendee-input"
2557
- }
2558
- ),
2559
- attendeeInput.trim() && (attendeeSuggestions?.length ?? 0) > 0 && /* @__PURE__ */ jsx(
2560
- "ul",
2561
- {
2562
- role: "listbox",
2563
- "aria-label": l.suggestions,
2564
- className: "mt-1 max-h-48 overflow-y-auto rounded-md border border-border bg-popover shadow-sm",
2565
- "data-testid": "calendar-editor-attendee-suggestions",
2566
- children: attendeeSuggestions?.filter(
2567
- (s) => !attendees.some((a) => (a.email ?? "").toLowerCase() === s.email.toLowerCase())
2568
- ).slice(0, 8).map((s) => /* @__PURE__ */ jsx("li", { role: "option", "aria-selected": false, children: /* @__PURE__ */ jsxs(
2569
- "button",
2570
- {
2571
- type: "button",
2572
- className: "flex w-full items-baseline gap-2 px-3 py-1.5 text-left text-sm hover:bg-muted",
2573
- onMouseDown: (e) => e.preventDefault(),
2574
- onClick: () => {
2575
- set({
2576
- attendees: [
2577
- ...attendees,
2578
- {
2579
- email: s.email,
2580
- name: s.name ?? null,
2581
- role: "required",
2582
- kind: "individual"
2583
- }
2584
- ]
2585
- });
2586
- setAttendeeInput("");
2587
- onAttendeeQueryChange?.("");
2588
- },
2589
- "data-testid": "calendar-editor-attendee-suggestion",
2590
- children: [
2591
- /* @__PURE__ */ jsx("span", { className: "truncate font-medium", children: s.name || s.email }),
2592
- s.name && /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-muted-foreground", children: s.email }),
2593
- s.detail && /* @__PURE__ */ jsx("span", { className: "ml-auto shrink-0 text-xs text-muted-foreground", children: s.detail })
2594
- ]
2595
- }
2596
- ) }, s.email))
2597
- }
2598
- ),
2599
- attendees.length > 0 && /* @__PURE__ */ jsx("ul", { className: "space-y-1", "data-testid": "calendar-editor-attendees", children: attendees.map((a, i) => /* @__PURE__ */ jsxs(
2600
- "li",
2601
- {
2602
- className: "flex items-center gap-2 text-sm",
2603
- children: [
2604
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", title: a.email ?? void 0, children: a.name || a.email || a.user_id }),
2605
- /* @__PURE__ */ jsxs(
2606
- NativeSelect,
2607
- {
2608
- "aria-label": `${a.email ?? ""} ${l.required}`,
2609
- className: "h-7 text-xs",
2610
- value: a.role ?? "required",
2611
- onChange: (e) => set({
2612
- attendees: attendees.map(
2613
- (x, j) => j === i ? { ...x, role: e.target.value } : x
2614
- )
2615
- }),
2616
- children: [
2617
- /* @__PURE__ */ jsx("option", { value: "required", children: l.required }),
2618
- /* @__PURE__ */ jsx("option", { value: "optional", children: l.optional })
2619
- ]
2620
- }
2621
- ),
2622
- isWebinar && (isWebinarAttendee?.(a) ?? true) && /* @__PURE__ */ jsxs(
2623
- NativeSelect,
2624
- {
2625
- "aria-label": `${a.email ?? ""} ${l.webinarRole}`,
2626
- className: "h-7 text-xs",
2627
- value: roles[attendeeKey(a)] ?? "attendee",
2628
- onChange: (e) => setRole(a, e.target.value),
2629
- "data-testid": "calendar-editor-webinar-role",
2630
- children: [
2631
- /* @__PURE__ */ jsx("option", { value: "cohost", children: l.cohost }),
2632
- /* @__PURE__ */ jsx("option", { value: "panelist", children: l.panelist }),
2633
- /* @__PURE__ */ jsx("option", { value: "attendee", children: l.webinarAttendee })
2634
- ]
2635
- }
2636
- ),
2637
- /* @__PURE__ */ jsx(
2638
- "button",
2639
- {
2640
- type: "button",
2641
- className: "rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground",
2642
- "aria-label": `${l.remove} ${a.email ?? ""}`,
2643
- onClick: () => set({ attendees: attendees.filter((_, j) => j !== i) }),
2644
- children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
2645
- }
2646
- )
2647
- ]
2648
- },
2649
- `${a.email ?? a.user_id ?? i}`
2650
- )) })
2651
- ] }),
2652
- schedulingSlot && /* @__PURE__ */ jsx("div", { className: "pl-8", children: schedulingSlot }),
2653
- /* @__PURE__ */ jsx(FieldRow, { icon: /* @__PURE__ */ jsx(TextAlignStart, { className: "h-4 w-4" }), children: /* @__PURE__ */ jsx(
2654
- Textarea,
2655
- {
2656
- "aria-label": l.description,
2657
- placeholder: l.description,
2658
- value: value.description ?? "",
2659
- rows: 4,
2660
- onChange: (e) => set({ description: e.target.value || null }),
2661
- "data-testid": "calendar-editor-description"
2662
- }
2663
- ) }),
2664
- /* @__PURE__ */ jsxs(FieldRow, { icon: /* @__PURE__ */ jsx(Bell, { className: "h-4 w-4" }), label: l.reminders, children: [
2665
- /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-sm", children: [
2666
- /* @__PURE__ */ jsx(
2667
- "input",
2668
- {
2669
- type: "checkbox",
2670
- checked: !!value.use_default_reminders,
2671
- onChange: (e) => set({ use_default_reminders: e.target.checked }),
2672
- "data-testid": "calendar-editor-default-reminders"
2673
- }
2674
- ),
2675
- l.useDefaultReminders
2676
- ] }),
2677
- !value.use_default_reminders && /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
2678
- reminders.map((r, i) => {
2679
- const { value: n, unit } = splitReminderMinutes(r.minutes_before);
2680
- return /* @__PURE__ */ jsxs(
2681
- "div",
2682
- {
2683
- className: "flex flex-wrap items-center gap-2 text-sm",
2684
- "data-testid": "calendar-editor-reminder",
2685
- children: [
2686
- /* @__PURE__ */ jsxs(
2687
- NativeSelect,
2688
- {
2689
- "aria-label": l.reminders,
2690
- className: "h-8",
2691
- value: r.action,
2692
- onChange: (e) => setReminder(i, { ...r, action: e.target.value }),
2693
- children: [
2694
- /* @__PURE__ */ jsx("option", { value: "display", children: l.popup }),
2695
- /* @__PURE__ */ jsx("option", { value: "email", children: l.email })
2696
- ]
2697
- }
2698
- ),
2699
- /* @__PURE__ */ jsx(
2700
- Input,
2701
- {
2702
- type: "number",
2703
- min: 0,
2704
- "aria-label": formatReminder(r.minutes_before),
2705
- className: "h-8 w-20",
2706
- value: n,
2707
- onChange: (e) => setReminder(i, {
2708
- ...r,
2709
- minutes_before: reminderToMinutes(
2710
- Math.max(0, Number(e.target.value) || 0),
2711
- unit
2712
- )
2713
- })
2714
- }
2715
- ),
2716
- /* @__PURE__ */ jsxs(
2717
- NativeSelect,
2718
- {
2719
- "aria-label": l.minutes,
2720
- className: "h-8",
2721
- value: unit,
2722
- onChange: (e) => setReminder(i, {
2723
- ...r,
2724
- minutes_before: reminderToMinutes(n, e.target.value)
2725
- }),
2726
- children: [
2727
- /* @__PURE__ */ jsx("option", { value: "minutes", children: l.minutes }),
2728
- /* @__PURE__ */ jsx("option", { value: "hours", children: l.hours }),
2729
- /* @__PURE__ */ jsx("option", { value: "days", children: l.days }),
2730
- /* @__PURE__ */ jsx("option", { value: "weeks", children: l.weeks })
2731
- ]
2732
- }
2733
- ),
2734
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: l.beforeStart }),
2735
- /* @__PURE__ */ jsx(
2736
- "button",
2737
- {
2738
- type: "button",
2739
- className: "rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground",
2740
- "aria-label": `${l.remove} ${formatReminder(r.minutes_before)}`,
2741
- onClick: () => set({ reminders: reminders.filter((_, j) => j !== i) }),
2742
- children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
2743
- }
2744
- )
2745
- ]
2746
- },
2747
- i
2748
- );
2749
- }),
2750
- reminders.length < 5 && /* @__PURE__ */ jsxs(
2751
- Button,
2752
- {
2753
- type: "button",
2754
- size: "sm",
2755
- variant: "ghost",
2756
- onClick: () => set({
2757
- reminders: [
2758
- ...reminders,
2759
- { minutes_before: allDay ? 24 * 60 : 10, action: "display" }
2760
- ]
2761
- }),
2762
- "data-testid": "calendar-editor-add-reminder",
2763
- children: [
2764
- /* @__PURE__ */ jsx(Plus, { className: "mr-1 h-4 w-4" }),
2765
- l.addReminder
2766
- ]
2767
- }
2768
- )
2769
- ] })
2770
- ] }),
2771
- /* @__PURE__ */ jsxs(FieldRow, { icon: /* @__PURE__ */ jsx(Lock, { className: "h-4 w-4" }), children: [
2772
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
2773
- /* @__PURE__ */ jsx(
2774
- NativeSelect,
2775
- {
2776
- "aria-label": l.showAs,
2777
- value: value.show_as ?? "busy",
2778
- onChange: (e) => set({ show_as: e.target.value }),
2779
- "data-testid": "calendar-editor-show-as",
2780
- children: ["busy", "free", "tentative", "oof", "working_elsewhere"].map((v) => /* @__PURE__ */ jsx("option", { value: v, children: showAsLabel(v, l) }, v))
2781
- }
2782
- ),
2783
- /* @__PURE__ */ jsxs(
2784
- NativeSelect,
2785
- {
2786
- "aria-label": l.sensitivity,
2787
- value: value.sensitivity ?? "normal",
2788
- onChange: (e) => set({ sensitivity: e.target.value }),
2789
- "data-testid": "calendar-editor-sensitivity",
2790
- children: [
2791
- /* @__PURE__ */ jsx("option", { value: "normal", children: l.normal }),
2792
- /* @__PURE__ */ jsx("option", { value: "private", children: l.private }),
2793
- /* @__PURE__ */ jsx("option", { value: "confidential", children: l.confidential })
2794
- ]
2795
- }
2796
- ),
2797
- /* @__PURE__ */ jsxs(
2798
- NativeSelect,
2799
- {
2800
- "aria-label": l.status,
2801
- value: value.status ?? "confirmed",
2802
- onChange: (e) => set({ status: e.target.value }),
2803
- "data-testid": "calendar-editor-status",
2804
- children: [
2805
- /* @__PURE__ */ jsx("option", { value: "confirmed", children: l.confirmed }),
2806
- /* @__PURE__ */ jsx("option", { value: "tentative", children: l.tentative }),
2807
- /* @__PURE__ */ jsx("option", { value: "cancelled", children: l.cancelled })
2808
- ]
2809
- }
2810
- )
2811
- ] }),
2812
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-4 pt-2 text-sm", children: [
2813
- value.show_as !== "oof" && /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2", children: [
2814
- /* @__PURE__ */ jsx(
2815
- "input",
2816
- {
2817
- type: "checkbox",
2818
- checked: focus,
2819
- onChange: (e) => {
2820
- setFocus(e.target.checked);
2821
- if (!e.target.checked) set({ auto_decline: false });
2822
- else if (!value.show_as || value.show_as === "free") set({ show_as: "busy" });
2823
- },
2824
- "data-testid": "calendar-editor-focus"
2825
- }
2826
- ),
2827
- l.focusTime
2828
- ] }),
2829
- showDecline && /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2", children: [
2830
- /* @__PURE__ */ jsx(
2831
- "input",
2832
- {
2833
- type: "checkbox",
2834
- checked: !!value.auto_decline,
2835
- onChange: (e) => set({ auto_decline: e.target.checked }),
2836
- "data-testid": "calendar-editor-auto-decline"
2837
- }
2838
- ),
2839
- l.autoDecline
2840
- ] })
2841
- ] }),
2842
- showDecline && value.auto_decline && /* @__PURE__ */ jsx(
2843
- Input,
2844
- {
2845
- className: "mt-2",
2846
- "aria-label": l.declineMessage,
2847
- placeholder: l.declineMessagePlaceholder,
2848
- value: value.decline_message ?? "",
2849
- maxLength: 1e3,
2850
- onChange: (e) => set({ decline_message: e.target.value || null }),
2851
- "data-testid": "calendar-editor-decline-message"
2852
- }
2853
- )
2854
- ] }),
2855
- /* @__PURE__ */ jsx(FieldRow, { icon: /* @__PURE__ */ jsx(Tag, { className: "h-4 w-4" }), children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [
2856
- categories.map((c) => /* @__PURE__ */ jsx(
2857
- Chip,
2858
- {
2859
- removeLabel: `${l.remove} ${c}`,
2860
- onRemove: () => set({ categories: categories.filter((x) => x !== c) }),
2861
- children: c
2862
- },
2863
- c
2864
- )),
2865
- /* @__PURE__ */ jsx(
2866
- Input,
2867
- {
2868
- "aria-label": l.addCategory,
2869
- placeholder: l.addCategory,
2870
- className: "h-8 w-40",
2871
- value: categoryInput,
2872
- onChange: (e) => setCategoryInput(e.target.value),
2873
- onKeyDown: (e) => {
2874
- if (e.key === "Enter" || e.key === ",") {
2875
- e.preventDefault();
2876
- addCategory();
2877
- }
2878
- },
2879
- onBlur: addCategory,
2880
- "data-testid": "calendar-editor-category-input"
2881
- }
2882
- )
2883
- ] }) }),
2884
- /* @__PURE__ */ jsxs(FieldRow, { icon: /* @__PURE__ */ jsx(Palette, { className: "h-4 w-4" }), children: [
2885
- /* @__PURE__ */ jsx(
2886
- ColorSwatches,
2887
- {
2888
- value: value.color ?? null,
2889
- onChange: (c) => set({ color: c }),
2890
- allowNone: true,
2891
- noneLabel: l.defaultColor,
2892
- testIdPrefix: "calendar-editor-color"
2893
- }
2894
- ),
2895
- !value.color && calendarColor && /* @__PURE__ */ jsx("span", { className: "sr-only", children: calendarColor })
2896
- ] }),
2897
- /* @__PURE__ */ jsxs(FieldRow, { icon: /* @__PURE__ */ jsx(Paperclip, { className: "h-4 w-4" }), label: l.attachments, children: [
2898
- attachments.length > 0 && /* @__PURE__ */ jsx("ul", { className: "space-y-1", children: attachments.map((a, i) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-2 text-sm", children: [
2899
- /* @__PURE__ */ jsx(
2900
- "a",
2901
- {
2902
- href: a.url,
2903
- target: "_blank",
2904
- rel: "noreferrer",
2905
- className: "min-w-0 flex-1 truncate text-primary hover:underline",
2906
- children: a.title || a.url
2907
- }
2908
- ),
2909
- /* @__PURE__ */ jsx(
2910
- "button",
2911
- {
2912
- type: "button",
2913
- className: "rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground",
2914
- "aria-label": `${l.remove} ${a.title || a.url}`,
2915
- onClick: () => set({ attachments: attachments.filter((_, j) => j !== i) }),
2916
- children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
2917
- }
2918
- )
2919
- ] }, `${a.url}-${i}`)) }),
2920
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
2921
- /* @__PURE__ */ jsx(
2922
- Input,
2923
- {
2924
- type: "url",
2925
- "aria-label": l.attachmentUrlPlaceholder,
2926
- placeholder: l.attachmentUrlPlaceholder,
2927
- className: "h-8 min-w-0 flex-1",
2928
- value: attachUrl,
2929
- onChange: (e) => setAttachUrl(e.target.value),
2930
- "data-testid": "calendar-editor-attachment-url"
2931
- }
2932
- ),
2933
- /* @__PURE__ */ jsx(
2934
- Input,
2935
- {
2936
- "aria-label": l.attachmentTitlePlaceholder,
2937
- placeholder: l.attachmentTitlePlaceholder,
2938
- className: "h-8 w-40",
2939
- value: attachTitle,
2940
- onChange: (e) => setAttachTitle(e.target.value)
2941
- }
2942
- ),
2943
- /* @__PURE__ */ jsx(
2944
- Button,
2945
- {
2946
- type: "button",
2947
- size: "sm",
2948
- variant: "outline",
2949
- onClick: addAttachment,
2950
- disabled: !attachUrl.trim(),
2951
- children: l.addAttachment
2952
- }
2953
- )
2954
- ] })
2955
- ] })
2956
- ] }),
2957
- (error || shownProblem) && /* @__PURE__ */ jsx(
2958
- "p",
2959
- {
2960
- className: "px-1 pt-2 text-sm text-destructive",
2961
- role: "alert",
2962
- "data-testid": "calendar-editor-error",
2963
- children: error || (shownProblem === "invalidRange" ? l.invalidRange : l.titleRequired)
2964
- }
2965
- ),
2966
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-t border-border pt-3", children: [
2967
- mode === "edit" && onDelete && /* @__PURE__ */ jsxs(
2968
- Button,
2969
- {
2970
- type: "button",
2971
- variant: "ghost",
2972
- className: "text-destructive hover:text-destructive",
2973
- onClick: onDelete,
2974
- "data-testid": "calendar-editor-delete",
2975
- children: [
2976
- /* @__PURE__ */ jsx(Trash2, { className: "mr-1 h-4 w-4" }),
2977
- l.delete
2978
- ]
2979
- }
2980
- ),
2981
- /* @__PURE__ */ jsx("div", { className: "flex-1" }),
2982
- /* @__PURE__ */ jsx(
2983
- Button,
2984
- {
2985
- type: "button",
2986
- variant: "outline",
2987
- onClick: onCancel,
2988
- "data-testid": "calendar-editor-cancel",
2989
- children: l.cancel
2990
- }
2991
- ),
2992
- /* @__PURE__ */ jsx(
2993
- Button,
2994
- {
2995
- type: "submit",
2996
- disabled: !!saving || problem === "invalidRange",
2997
- "data-testid": "calendar-editor-save",
2998
- children: saving ? l.saving : l.save
2999
- }
3000
- )
3001
- ] })
3002
- ]
3003
- }
3004
- );
3005
- }
3006
- var partstatLabel = (p, l) => ({
3007
- needs_action: l.needsAction,
3008
- accepted: l.accepted,
3009
- declined: l.declined,
3010
- tentative: l.tentative,
3011
- delegated: l.delegated
3012
- })[p ?? "needs_action"];
3013
- function Row({ icon, children }) {
3014
- return /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 text-sm", children: [
3015
- /* @__PURE__ */ jsx(
3016
- "span",
3017
- {
3018
- className: "mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground",
3019
- "aria-hidden": true,
3020
- children: icon
3021
- }
3022
- ),
3023
- /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children })
3024
- ] });
3025
- }
3026
- function EventDetails({
3027
- event,
3028
- timeZone,
3029
- calendarName,
3030
- calendarColor,
3031
- onEdit,
3032
- onDelete,
3033
- onRespond,
3034
- onProposeTime,
3035
- onJoin,
3036
- onOpenMeetingChat,
3037
- onClose,
3038
- canJoin = true,
3039
- labels: labelOverrides,
3040
- className
3041
- }) {
3042
- const l = mergeLabels(labelOverrides);
3043
- const zone = safeZone(timeZone);
3044
- const color = event.color || calendarColor || "#3b82f6";
3045
- const restricted = !!event.restricted;
3046
- const title = restricted ? l.busy : event.title?.trim() || l.noTitle;
3047
- const eventZone = event.start.time_zone;
3048
- const otherZone = !event.all_day && eventZone && eventZone !== zone ? safeZone(eventZone, zone) : null;
3049
- const attendees = event.attendees ?? [];
3050
- const counts = attendees.reduce((acc, a) => {
3051
- const k = a.partstat ?? "needs_action";
3052
- acc[k] = (acc[k] ?? 0) + 1;
3053
- return acc;
3054
- }, {});
3055
- const canRespond = !!onRespond && !event.is_organizer && event.my_partstat != null && !restricted;
3056
- const invitee = event.is_organizer === false && event.my_partstat != null;
3057
- const conference = !restricted ? event.conference : null;
3058
- return /* @__PURE__ */ jsxs("div", { className: cn("space-y-3", className), "data-testid": "calendar-event-details", children: [
3059
- /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
3060
- /* @__PURE__ */ jsx(
3061
- "span",
3062
- {
3063
- className: "mt-1.5 h-3 w-3 shrink-0 rounded-sm",
3064
- style: { backgroundColor: color },
3065
- "aria-hidden": true
3066
- }
3067
- ),
3068
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
3069
- /* @__PURE__ */ jsx(
3070
- "h3",
3071
- {
3072
- className: cn(
3073
- "break-words text-lg font-semibold leading-snug",
3074
- event.status === "cancelled" && "line-through"
3075
- ),
3076
- "data-testid": "calendar-details-title",
3077
- children: title
3078
- }
3079
- ),
3080
- /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", "data-testid": "calendar-details-when", children: formatEventWhen(event, zone) }),
3081
- otherZone && /* @__PURE__ */ jsxs("div", { className: "text-xs text-muted-foreground", children: [
3082
- formatInZone(event.start_utc, otherZone, { hour: "numeric", minute: "2-digit" }),
3083
- " \u2013",
3084
- " ",
3085
- formatInZone(event.end_utc, otherZone, { hour: "numeric", minute: "2-digit" }),
3086
- " ",
3087
- zoneCity(otherZone),
3088
- " (",
3089
- zoneOffsetLabel(otherZone, event.start_utc),
3090
- ")"
3091
- ] })
3092
- ] }),
3093
- /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center", children: [
3094
- onEdit && !invitee && event.can_edit !== false && !restricted && /* @__PURE__ */ jsx(
3095
- Button,
3096
- {
3097
- type: "button",
3098
- variant: "ghost",
3099
- size: "icon",
3100
- className: "h-8 w-8",
3101
- "aria-label": l.edit,
3102
- onClick: onEdit,
3103
- "data-testid": "calendar-details-edit",
3104
- children: /* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4" })
3105
- }
3106
- ),
3107
- onDelete && !invitee && event.can_edit !== false && !restricted && /* @__PURE__ */ jsx(
3108
- Button,
3109
- {
3110
- type: "button",
3111
- variant: "ghost",
3112
- size: "icon",
3113
- className: "h-8 w-8",
3114
- "aria-label": l.delete,
3115
- onClick: onDelete,
3116
- "data-testid": "calendar-details-delete",
3117
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
3118
- }
3119
- ),
3120
- onDelete && invitee && /* @__PURE__ */ jsx(
3121
- Button,
3122
- {
3123
- type: "button",
3124
- variant: "ghost",
3125
- size: "icon",
3126
- className: "h-8 w-8",
3127
- "aria-label": l.removeFromCalendar,
3128
- title: l.removeFromCalendar,
3129
- onClick: onDelete,
3130
- "data-testid": "calendar-details-remove",
3131
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
3132
- }
3133
- ),
3134
- onClose && /* @__PURE__ */ jsx(
3135
- Button,
3136
- {
3137
- type: "button",
3138
- variant: "ghost",
3139
- size: "icon",
3140
- className: "h-8 w-8",
3141
- "aria-label": l.cancel,
3142
- onClick: onClose,
3143
- children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
3144
- }
3145
- )
3146
- ] })
3147
- ] }),
3148
- event.is_recurring && /* @__PURE__ */ jsx(Row, { icon: /* @__PURE__ */ jsx(Repeat, { className: "h-4 w-4" }), children: /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: l.recurring }) }),
3149
- conference?.url && /* @__PURE__ */ jsx(Row, { icon: /* @__PURE__ */ jsx(Video, { className: "h-4 w-4" }), children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
3150
- /* @__PURE__ */ jsx(
3151
- Button,
3152
- {
3153
- type: "button",
3154
- size: "sm",
3155
- disabled: !canJoin,
3156
- onClick: () => onJoin ? onJoin() : window.open(conference.url ?? "", "_blank", "noopener"),
3157
- "data-testid": "calendar-details-join",
3158
- children: isWebinarConference(conference) ? l.joinWebinar : `${l.join}${conference.type === "arbi" ? ` ${l.arbiCall}` : ""}`
3159
- }
3160
- ),
3161
- conference.type === "arbi" && onOpenMeetingChat && /* @__PURE__ */ jsx(
3162
- Button,
3163
- {
3164
- type: "button",
3165
- size: "sm",
3166
- variant: "outline",
3167
- onClick: onOpenMeetingChat,
3168
- "data-testid": "calendar-details-meeting-chat",
3169
- children: l.openMeetingChat
3170
- }
3171
- ),
3172
- conference.type === "external" && /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate text-xs text-muted-foreground", children: conference.url })
3173
- ] }) }),
3174
- !restricted && event.location && /* @__PURE__ */ jsx(Row, { icon: /* @__PURE__ */ jsx(MapPin, { className: "h-4 w-4" }), children: /* @__PURE__ */ jsx("span", { className: "break-words", children: event.location }) }),
3175
- calendarName && /* @__PURE__ */ jsx(Row, { icon: /* @__PURE__ */ jsx(CalendarDays, { className: "h-4 w-4" }), children: /* @__PURE__ */ jsx("span", { children: calendarName }) }),
3176
- restricted && /* @__PURE__ */ jsx(Row, { icon: /* @__PURE__ */ jsx(Lock, { className: "h-4 w-4" }), children: /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: l.private }) }),
3177
- !restricted && (event.organizer || attendees.length > 0) && /* @__PURE__ */ jsx(Row, { icon: /* @__PURE__ */ jsx(Users, { className: "h-4 w-4" }), children: /* @__PURE__ */ jsxs("div", { className: "space-y-1", "data-testid": "calendar-details-attendees", children: [
3178
- attendees.length > 0 && /* @__PURE__ */ jsxs("div", { className: "text-xs text-muted-foreground", children: [
3179
- attendees.length,
3180
- " \xB7",
3181
- " ",
3182
- Object.entries(counts).map(([k, n]) => `${n} ${partstatLabel(k, l).toLowerCase()}`).join(", ")
3183
- ] }),
3184
- event.organizer && /* @__PURE__ */ jsxs("div", { className: "truncate", children: [
3185
- event.organizer.name || event.organizer.email,
3186
- " ",
3187
- /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
3188
- "\xB7 ",
3189
- l.organizer
3190
- ] })
3191
- ] }),
3192
- /* @__PURE__ */ jsx("ul", { className: "space-y-0.5", children: attendees.filter((a) => !a.is_organizer).map((a, i) => /* @__PURE__ */ jsxs(
3193
- "li",
3194
- {
3195
- className: "flex items-center gap-1.5",
3196
- "data-testid": "calendar-details-attendee",
3197
- "data-partstat": a.partstat ?? "needs_action",
3198
- "data-kind": a.kind ?? "individual",
3199
- children: [
3200
- a.partstat === "accepted" ? /* @__PURE__ */ jsx(CircleCheck, { className: "h-3.5 w-3.5 text-green-600", "aria-label": l.accepted }) : a.partstat === "declined" ? /* @__PURE__ */ jsx(CircleX, { className: "h-3.5 w-3.5 text-destructive", "aria-label": l.declined }) : /* @__PURE__ */ jsx(
3201
- Clock,
3202
- {
3203
- className: "h-3.5 w-3.5 text-muted-foreground",
3204
- "aria-label": partstatLabel(a.partstat, l)
3205
- }
3206
- ),
3207
- (a.kind === "room" || a.kind === "resource") && /* @__PURE__ */ jsx(DoorOpen, { className: "h-3.5 w-3.5 text-muted-foreground", "aria-label": l.room }),
3208
- /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: a.name || a.email }),
3209
- a.role === "optional" && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
3210
- "(",
3211
- l.optional,
3212
- ")"
3213
- ] }),
3214
- /* @__PURE__ */ jsx(
3215
- "span",
3216
- {
3217
- className: cn(
3218
- "ml-auto shrink-0 rounded-full px-1.5 py-0.5 text-[10px]",
3219
- a.partstat === "accepted" ? "bg-green-500/15 text-green-700 dark:text-green-400" : a.partstat === "declined" ? "bg-destructive/15 text-destructive" : "bg-muted text-muted-foreground"
3220
- ),
3221
- children: partstatLabel(a.partstat, l)
3222
- }
3223
- )
3224
- ]
3225
- },
3226
- `${a.email ?? a.user_id ?? i}`
3227
- )) })
3228
- ] }) }),
3229
- !restricted && event.description && /* @__PURE__ */ jsx(
3230
- "div",
3231
- {
3232
- className: "whitespace-pre-wrap break-words rounded-md bg-muted/40 p-2 text-sm",
3233
- "data-testid": "calendar-details-description",
3234
- children: event.description
3235
- }
3236
- ),
3237
- !restricted && (event.attachments?.length ?? 0) > 0 && /* @__PURE__ */ jsx(Row, { icon: /* @__PURE__ */ jsx(Paperclip, { className: "h-4 w-4" }), children: /* @__PURE__ */ jsx("ul", { className: "space-y-0.5", children: event.attachments?.map((a, i) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
3238
- "a",
3239
- {
3240
- href: a.url,
3241
- target: "_blank",
3242
- rel: "noreferrer",
3243
- className: "text-primary hover:underline",
3244
- children: a.title || a.url
3245
- }
3246
- ) }, `${a.url}-${i}`)) }) }),
3247
- !restricted && (event.categories?.length ?? 0) > 0 && /* @__PURE__ */ jsx(Row, { icon: /* @__PURE__ */ jsx(Tag, { className: "h-4 w-4" }), children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: event.categories?.map((c) => /* @__PURE__ */ jsx("span", { className: "rounded-full bg-muted px-2 py-0.5 text-xs", children: c }, c)) }) }),
3248
- !restricted && (event.reminders?.length ?? 0) > 0 && /* @__PURE__ */ jsx(Row, { icon: /* @__PURE__ */ jsx(Bell, { className: "h-4 w-4" }), children: /* @__PURE__ */ jsx("div", { className: "text-muted-foreground", children: event.reminders?.map((r) => formatReminder(r.minutes_before)).join(", ") }) }),
3249
- canRespond && /* @__PURE__ */ jsxs(
3250
- "div",
3251
- {
3252
- className: "flex flex-wrap items-center gap-2 border-t border-border pt-3",
3253
- "data-testid": "calendar-details-rsvp",
3254
- children: [
3255
- /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: l.going }),
3256
- [
3257
- ["accepted", l.accept],
3258
- ["declined", l.decline],
3259
- ["tentative", l.maybe]
3260
- ].map(([p, label]) => /* @__PURE__ */ jsx(
3261
- Button,
3262
- {
3263
- type: "button",
3264
- size: "sm",
3265
- variant: event.my_partstat === p ? "default" : "outline",
3266
- "aria-pressed": event.my_partstat === p,
3267
- onClick: () => onRespond?.(p),
3268
- "data-testid": `calendar-details-rsvp-${p}`,
3269
- children: label
3270
- },
3271
- p
3272
- )),
3273
- onProposeTime && /* @__PURE__ */ jsx(
3274
- Button,
3275
- {
3276
- type: "button",
3277
- size: "sm",
3278
- variant: "ghost",
3279
- onClick: onProposeTime,
3280
- "data-testid": "calendar-details-propose",
3281
- children: l.proposeNewTime
3282
- }
3283
- )
3284
- ]
3285
- }
3286
- )
3287
- ] });
3288
- }
3289
- function ScopeDialog({
3290
- open,
3291
- action,
3292
- onChoose,
3293
- onCancel,
3294
- allowFollowing = true,
3295
- labels: labelOverrides
3296
- }) {
3297
- const l = mergeLabels(labelOverrides);
3298
- const [scope, setScope] = useState("this");
3299
- const options = [
3300
- ["this", l.scopeThis],
3301
- ...allowFollowing ? [["following", l.scopeFollowing]] : [],
3302
- ["all", l.scopeAll]
3303
- ];
3304
- const title = action === "edit" ? l.scopeEditTitle : action === "respond" ? l.scopeRespondTitle : l.scopeDeleteTitle;
3305
- return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (o) => !o && onCancel(), children: /* @__PURE__ */ jsxs(DialogContent, { className: "max-w-sm", "data-testid": "calendar-scope-dialog", children: [
3306
- /* @__PURE__ */ jsxs(DialogHeader, { children: [
3307
- /* @__PURE__ */ jsx(DialogTitle, { children: title }),
3308
- /* @__PURE__ */ jsx(DialogDescription, { className: "sr-only", children: title })
3309
- ] }),
3310
- /* @__PURE__ */ jsxs(
3311
- "form",
3312
- {
3313
- onSubmit: (e) => {
3314
- e.preventDefault();
3315
- onChoose(scope);
3316
- },
3317
- children: [
3318
- /* @__PURE__ */ jsxs("fieldset", { className: "space-y-2 py-2", children: [
3319
- /* @__PURE__ */ jsx("legend", { className: "sr-only", children: title }),
3320
- options.map(([value, label]) => /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2 text-sm", children: [
3321
- /* @__PURE__ */ jsx(
3322
- "input",
3323
- {
3324
- type: "radio",
3325
- name: "calendar-scope",
3326
- value,
3327
- checked: scope === value,
3328
- onChange: () => setScope(value),
3329
- "data-testid": `calendar-scope-${value}`
3330
- }
3331
- ),
3332
- label
3333
- ] }, value))
3334
- ] }),
3335
- /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 pt-2", children: [
3336
- /* @__PURE__ */ jsx(
3337
- Button,
3338
- {
3339
- type: "button",
3340
- variant: "outline",
3341
- onClick: onCancel,
3342
- "data-testid": "calendar-scope-cancel",
3343
- children: l.cancel
3344
- }
3345
- ),
3346
- /* @__PURE__ */ jsx(
3347
- Button,
3348
- {
3349
- type: "submit",
3350
- variant: action === "delete" ? "destructive" : "default",
3351
- "data-testid": "calendar-scope-ok",
3352
- children: action === "delete" ? l.delete : action === "respond" ? l.send : l.save
3353
- }
3354
- )
3355
- ] })
3356
- ]
3357
- }
3358
- )
3359
- ] }) });
3360
- }
3361
-
3362
- // src/calendar/scheduling.ts
3363
- var pad2 = (n) => String(n).padStart(2, "0");
3364
- function dayWindow(date, zone, startHour = 7, endHour = 20) {
3365
- const startMs = wallToEpochMs(`${date}T${pad2(startHour)}:00`, zone);
3366
- const endMs = endHour >= 24 ? wallToEpochMs(`${date}T23:59`, zone) + 6e4 : wallToEpochMs(`${date}T${pad2(endHour)}:00`, zone);
3367
- return { startMs, endMs };
3368
- }
3369
- function placeIntervals(intervals, win) {
3370
- const span = win.endMs - win.startMs;
3371
- if (span <= 0) return [];
3372
- const out = [];
3373
- for (const i of intervals) {
3374
- const s = Math.max(Date.parse(i.startUtc), win.startMs);
3375
- const e = Math.min(Date.parse(i.endUtc), win.endMs);
3376
- if (!(e > s)) continue;
3377
- out.push({
3378
- startMs: s,
3379
- endMs: e,
3380
- leftPct: (s - win.startMs) / span * 100,
3381
- widthPct: (e - s) / span * 100
3382
- });
3383
- }
3384
- return out.sort((a, b) => a.startMs - b.startMs);
3385
- }
3386
- function outsideIntervals(working, win) {
3387
- const span = win.endMs - win.startMs;
3388
- const placed = placeIntervals(working, win);
3389
- const gaps = [];
3390
- let cursor = win.startMs;
3391
- for (const w of placed) {
3392
- if (w.startMs > cursor) gaps.push(mk(cursor, w.startMs));
3393
- cursor = Math.max(cursor, w.endMs);
3394
- }
3395
- if (cursor < win.endMs) gaps.push(mk(cursor, win.endMs));
3396
- return gaps;
3397
- function mk(s, e) {
3398
- return {
3399
- startMs: s,
3400
- endMs: e,
3401
- leftPct: (s - win.startMs) / span * 100,
3402
- widthPct: (e - s) / span * 100
3403
- };
3404
- }
3405
- }
3406
- function hourTicks(date, zone, startHour, endHour) {
3407
- const win = dayWindow(date, zone, startHour, endHour);
3408
- const span = win.endMs - win.startMs;
3409
- const ticks = [];
3410
- for (let h = startHour; h < endHour; h++) {
3411
- const ms = wallToEpochMs(`${date}T${pad2(h)}:00`, zone);
3412
- ticks.push({ hour: h, leftPct: (ms - win.startMs) / span * 100 });
3413
- }
3414
- return ticks;
3415
- }
3416
- function SchedulingAssistant({
3417
- date,
3418
- timeZone,
3419
- rows,
3420
- selection,
3421
- dayStartHour = 7,
3422
- dayEndHour = 20,
3423
- loading,
3424
- suggestions,
3425
- finding,
3426
- onFindTime,
3427
- onPickSuggestion,
3428
- onDateChange,
3429
- labels: labelOverrides,
3430
- className
3431
- }) {
3432
- const l = mergeLabels(labelOverrides);
3433
- const zone = safeZone(timeZone);
3434
- const win = dayWindow(date, zone, dayStartHour, dayEndHour);
3435
- const ticks = hourTicks(date, zone, dayStartHour, dayEndHour);
3436
- const sel = selection ? placeIntervals([selection], win)[0] : void 0;
3437
- const time = (iso) => formatInZone(iso, zone, { hour: "numeric", minute: "2-digit" });
3438
- return /* @__PURE__ */ jsxs("div", { className: cn("space-y-2", className), "data-testid": "calendar-scheduling", children: [
3439
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
3440
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: l.schedulingAssistant }),
3441
- onDateChange && /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-1", children: [
3442
- /* @__PURE__ */ jsx(
3443
- Button,
3444
- {
3445
- type: "button",
3446
- variant: "ghost",
3447
- size: "icon",
3448
- className: "h-7 w-7",
3449
- "aria-label": l.previous,
3450
- onClick: () => onDateChange(addDays(date, -1)),
3451
- children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" })
3452
- }
3453
- ),
3454
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", "data-testid": "calendar-scheduling-date", children: formatPlainDate(date, { weekday: "short", day: "numeric", month: "short" }) }),
3455
- /* @__PURE__ */ jsx(
3456
- Button,
3457
- {
3458
- type: "button",
3459
- variant: "ghost",
3460
- size: "icon",
3461
- className: "h-7 w-7",
3462
- "aria-label": l.next,
3463
- onClick: () => onDateChange(addDays(date, 1)),
3464
- children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" })
3465
- }
3466
- )
3467
- ] })
3468
- ] }),
3469
- /* @__PURE__ */ jsxs("div", { className: cn("rounded-md border border-border", loading && "opacity-60"), children: [
3470
- /* @__PURE__ */ jsxs("div", { className: "flex border-b border-border text-[10px] text-muted-foreground", children: [
3471
- /* @__PURE__ */ jsx("div", { className: "w-32 shrink-0 sm:w-40" }),
3472
- /* @__PURE__ */ jsx("div", { className: "relative h-5 flex-1", children: ticks.map((t) => /* @__PURE__ */ jsx(
3473
- "span",
3474
- {
3475
- className: "absolute top-0.5 -translate-x-1/2",
3476
- style: { left: `${t.leftPct}%` },
3477
- children: t.hour
3478
- },
3479
- t.hour
3480
- )) })
3481
- ] }),
3482
- rows.map((row) => /* @__PURE__ */ jsxs(
3483
- "div",
3484
- {
3485
- className: "flex items-stretch border-b border-border last:border-b-0",
3486
- "data-testid": "calendar-scheduling-row",
3487
- "data-known": row.known,
3488
- children: [
3489
- /* @__PURE__ */ jsxs("div", { className: "flex w-32 shrink-0 items-center gap-1.5 truncate px-2 py-1 text-xs sm:w-40", children: [
3490
- row.kind === "room" ? /* @__PURE__ */ jsx(
3491
- DoorOpen,
3492
- {
3493
- className: "h-3.5 w-3.5 shrink-0 text-muted-foreground",
3494
- "aria-label": l.room
3495
- }
3496
- ) : /* @__PURE__ */ jsx(User, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground", "aria-hidden": true }),
3497
- /* @__PURE__ */ jsx("span", { className: "truncate", title: row.label, children: row.label })
3498
- ] }),
3499
- /* @__PURE__ */ jsxs("div", { className: "relative h-7 flex-1 overflow-hidden", children: [
3500
- !row.known ? /* @__PURE__ */ jsx(
3501
- "div",
3502
- {
3503
- className: "absolute inset-0 flex items-center justify-center bg-[repeating-linear-gradient(45deg,transparent,transparent_4px,hsl(var(--muted))_4px,hsl(var(--muted))_8px)] text-[10px] text-muted-foreground",
3504
- title: l.unknownAvailability,
3505
- children: l.unknownAvailability
3506
- }
3507
- ) : /* @__PURE__ */ jsxs(Fragment, { children: [
3508
- row.working && outsideIntervals(row.working, win).map((g, i) => /* @__PURE__ */ jsx(
3509
- "div",
3510
- {
3511
- className: "absolute inset-y-0 bg-muted",
3512
- style: { left: `${g.leftPct}%`, width: `${g.widthPct}%` },
3513
- title: l.outsideWorkingHours,
3514
- "data-testid": "calendar-scheduling-outside"
3515
- },
3516
- `o${i}`
3517
- )),
3518
- placeIntervals(row.busy, win).map((b, i) => {
3519
- const src = row.busy.find(
3520
- (x) => Math.max(Date.parse(x.startUtc), win.startMs) === b.startMs && Math.min(Date.parse(x.endUtc), win.endMs) === b.endMs
3521
- );
3522
- const tentative = src?.showAs === "tentative";
3523
- const oof = src?.showAs === "oof";
3524
- return /* @__PURE__ */ jsx(
3525
- "div",
3526
- {
3527
- className: cn(
3528
- "absolute inset-y-1 rounded-sm",
3529
- oof ? "bg-purple-500/70" : tentative ? "bg-primary/35" : "bg-primary/75"
3530
- ),
3531
- style: { left: `${b.leftPct}%`, width: `${Math.max(b.widthPct, 0.5)}%` },
3532
- title: `${src?.title ? `${src.title} \xB7 ` : ""}${time(new Date(b.startMs).toISOString())} \u2013 ${time(new Date(b.endMs).toISOString())}`,
3533
- "data-testid": "calendar-scheduling-busy"
3534
- },
3535
- `b${i}`
3536
- );
3537
- })
3538
- ] }),
3539
- sel && /* @__PURE__ */ jsx(
3540
- "div",
3541
- {
3542
- className: "pointer-events-none absolute inset-y-0 border-x-2 border-green-600 bg-green-500/15",
3543
- style: { left: `${sel.leftPct}%`, width: `${sel.widthPct}%` },
3544
- "aria-hidden": true
3545
- }
3546
- )
3547
- ] })
3548
- ]
3549
- },
3550
- row.id
3551
- ))
3552
- ] }),
3553
- onFindTime && /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
3554
- /* @__PURE__ */ jsxs(
3555
- Button,
3556
- {
3557
- type: "button",
3558
- size: "sm",
3559
- variant: "outline",
3560
- onClick: onFindTime,
3561
- disabled: finding,
3562
- "data-testid": "calendar-find-time",
3563
- children: [
3564
- /* @__PURE__ */ jsx(Sparkles, { className: "mr-1 h-4 w-4" }),
3565
- finding ? l.findingTimes : l.findATime
3566
- ]
3567
- }
3568
- ),
3569
- suggestions && suggestions.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: l.noSuggestions }),
3570
- suggestions && suggestions.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", "data-testid": "calendar-suggestions", children: suggestions.map((s) => /* @__PURE__ */ jsxs(
3571
- "button",
3572
- {
3573
- type: "button",
3574
- onClick: () => onPickSuggestion?.(s),
3575
- className: "rounded-md border border-border px-2 py-1 text-left text-xs hover:bg-muted",
3576
- "data-testid": "calendar-suggestion",
3577
- children: [
3578
- /* @__PURE__ */ jsxs("div", { className: "font-medium", children: [
3579
- formatInZone(s.startUtc, zone, {
3580
- weekday: "short",
3581
- day: "numeric",
3582
- month: "short"
3583
- }),
3584
- " ",
3585
- time(s.startUtc),
3586
- " \u2013 ",
3587
- time(s.endUtc)
3588
- ] }),
3589
- /* @__PURE__ */ jsx("div", { className: "text-muted-foreground", children: s.unavailable.length === 0 ? l.everyoneFree : `${s.unavailable.length} ${l.unavailableCount}` })
3590
- ]
3591
- },
3592
- s.startUtc
3593
- )) })
3594
- ] })
3595
- ] });
3596
- }
3597
- function RoomFinder({
3598
- rooms,
3599
- loading,
3600
- minCapacity,
3601
- feature,
3602
- onFiltersChange,
3603
- addedEmails,
3604
- onAdd,
3605
- labels: labelOverrides,
3606
- className
3607
- }) {
3608
- const l = mergeLabels(labelOverrides);
3609
- const added = new Set(addedEmails.map((e) => e.toLowerCase()));
3610
- return /* @__PURE__ */ jsxs("div", { className: cn("space-y-2", className), "data-testid": "calendar-room-finder", children: [
3611
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
3612
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: l.findRoom }),
3613
- /* @__PURE__ */ jsx(
3614
- Input,
3615
- {
3616
- type: "number",
3617
- min: 1,
3618
- className: "h-8 w-28",
3619
- placeholder: l.minCapacity,
3620
- "aria-label": l.minCapacity,
3621
- value: minCapacity ?? "",
3622
- onChange: (e) => onFiltersChange({
3623
- minCapacity: e.target.value ? Math.max(1, Number(e.target.value)) : null,
3624
- feature
3625
- }),
3626
- "data-testid": "calendar-room-capacity"
3627
- }
3628
- ),
3629
- /* @__PURE__ */ jsx(
3630
- Input,
3631
- {
3632
- className: "h-8 w-32",
3633
- placeholder: l.featureFilter,
3634
- "aria-label": l.featureFilter,
3635
- value: feature,
3636
- onChange: (e) => onFiltersChange({ minCapacity, feature: e.target.value }),
3637
- "data-testid": "calendar-room-feature"
3638
- }
3639
- )
3640
- ] }),
3641
- /* @__PURE__ */ jsxs(
3642
- "ul",
3643
- {
3644
- className: cn(
3645
- "divide-y divide-border rounded-md border border-border",
3646
- loading && "opacity-60"
3647
- ),
3648
- children: [
3649
- rooms.length === 0 && /* @__PURE__ */ jsx("li", { className: "px-3 py-2 text-xs text-muted-foreground", children: l.noRooms }),
3650
- rooms.map((r) => {
3651
- const isAdded = added.has(r.email.toLowerCase());
3652
- return /* @__PURE__ */ jsxs(
3653
- "li",
3654
- {
3655
- className: "flex items-center gap-2 px-3 py-1.5 text-sm",
3656
- "data-testid": "calendar-room",
3657
- "data-room-email": r.email,
3658
- children: [
3659
- /* @__PURE__ */ jsx(DoorOpen, { className: "h-4 w-4 shrink-0 text-muted-foreground", "aria-hidden": true }),
3660
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
3661
- /* @__PURE__ */ jsx("div", { className: "truncate font-medium", children: r.name }),
3662
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-x-2 text-xs text-muted-foreground", children: [
3663
- r.capacity != null && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-0.5", children: [
3664
- /* @__PURE__ */ jsx(Users, { className: "h-3 w-3", "aria-hidden": true }),
3665
- r.capacity
3666
- ] }),
3667
- (r.building || r.floor) && /* @__PURE__ */ jsx("span", { children: [r.building, r.floor].filter(Boolean).join(" \xB7 ") }),
3668
- (r.features ?? []).length > 0 && /* @__PURE__ */ jsx("span", { children: r.features?.join(", ") })
3669
- ] })
3670
- ] }),
3671
- r.free != null && /* @__PURE__ */ jsx(
3672
- "span",
3673
- {
3674
- className: cn(
3675
- "rounded-full px-2 py-0.5 text-xs",
3676
- r.free ? "bg-green-500/15 text-green-700 dark:text-green-400" : "bg-destructive/15 text-destructive"
3677
- ),
3678
- "data-testid": "calendar-room-availability",
3679
- children: r.free ? l.roomFree : l.roomBusy
3680
- }
3681
- ),
3682
- /* @__PURE__ */ jsx(
3683
- Button,
3684
- {
3685
- type: "button",
3686
- size: "sm",
3687
- variant: "outline",
3688
- disabled: isAdded,
3689
- onClick: () => onAdd(r),
3690
- "data-testid": "calendar-room-add",
3691
- children: isAdded ? l.roomAdded : l.addRoom
3692
- }
3693
- )
3694
- ]
3695
- },
3696
- r.id
3697
- );
3698
- })
3699
- ]
3700
- }
3701
- )
3702
- ] });
3703
- }
3704
- var mondayFirstName = (day) => formatPlainDate(`2024-01-0${day + 1}`, { weekday: "long" });
3705
- var sundayFirstName = (day) => formatPlainDate(day === 0 ? "2023-12-31" : `2024-01-0${day}`, { weekday: "long" });
3706
- var VIEWS = ["day", "work_week", "week", "month", "agenda", "year"];
3707
- var DURATIONS = [15, 30, 45, 60, 90, 120];
3708
- var viewName = (v, l) => ({
3709
- day: l.day,
3710
- week: l.week,
3711
- work_week: l.workWeek,
3712
- month: l.month,
3713
- agenda: l.agenda,
3714
- year: l.year
3715
- })[v];
3716
- function Field({
3717
- label,
3718
- htmlFor,
3719
- children
3720
- }) {
3721
- return /* @__PURE__ */ jsxs("div", { className: "grid gap-1.5 sm:grid-cols-[12rem_1fr] sm:items-center", children: [
3722
- /* @__PURE__ */ jsx("label", { htmlFor, className: "text-sm font-medium", children: label }),
3723
- /* @__PURE__ */ jsx("div", { className: "min-w-0", children })
3724
- ] });
3725
- }
3726
- function CalendarSettingsForm({
3727
- value,
3728
- onChange,
3729
- onSave,
3730
- onCancel,
3731
- calendars,
3732
- zones,
3733
- saving,
3734
- error,
3735
- labels: labelOverrides,
3736
- className
3737
- }) {
3738
- const l = mergeLabels(labelOverrides);
3739
- const uid = useId();
3740
- const set = (patch) => onChange({ ...value, ...patch });
3741
- const hoursFor = (day) => value.working_hours.find((h) => h.day === day);
3742
- const setHours = (day, patch) => {
3743
- const rest = value.working_hours.filter((h) => h.day !== day);
3744
- if (patch === null) return set({ working_hours: rest });
3745
- const cur = hoursFor(day) ?? { day, start: "09:00", end: "17:00" };
3746
- set({ working_hours: [...rest, { ...cur, ...patch }].sort((a, b) => a.day - b.day) });
3747
- };
3748
- const overlays = value.overlays ?? [];
3749
- const toggleOverlay = (o) => set({ overlays: overlays.includes(o) ? overlays.filter((x) => x !== o) : [...overlays, o] });
3750
- const writable = (calendars ?? []).filter((c) => c.canWrite && !c.locked);
3751
- return /* @__PURE__ */ jsxs(
3752
- "form",
3753
- {
3754
- className: cn("space-y-5", className),
3755
- onSubmit: (e) => {
3756
- e.preventDefault();
3757
- onSave();
3758
- },
3759
- "data-testid": "calendar-settings-form",
3760
- "aria-label": l.settingsTitle,
3761
- children: [
3762
- /* @__PURE__ */ jsx(Field, { label: l.timeZone, children: /* @__PURE__ */ jsx(
3763
- TimeZonePicker,
3764
- {
3765
- value: value.time_zone,
3766
- zones,
3767
- labels: labelOverrides,
3768
- className: "w-full sm:w-80",
3769
- onChange: (z) => set({ time_zone: z })
3770
- }
3771
- ) }),
3772
- /* @__PURE__ */ jsx(Field, { label: l.secondaryTimeZone, children: /* @__PURE__ */ jsx(
3773
- TimeZonePicker,
3774
- {
3775
- value: value.secondary_time_zone ?? null,
3776
- zones,
3777
- allowNone: true,
3778
- ariaLabel: l.secondaryTimeZone,
3779
- labels: labelOverrides,
3780
- className: "w-full sm:w-80",
3781
- onChange: (z) => set({ secondary_time_zone: z }),
3782
- onClear: () => set({ secondary_time_zone: null })
3783
- }
3784
- ) }),
3785
- /* @__PURE__ */ jsx(Field, { label: l.weekStart, htmlFor: `${uid}-week-start`, children: /* @__PURE__ */ jsx(
3786
- NativeSelect,
3787
- {
3788
- id: `${uid}-week-start`,
3789
- value: value.week_start,
3790
- onChange: (e) => set({ week_start: Number(e.target.value) }),
3791
- "data-testid": "calendar-settings-week-start",
3792
- children: [1, 0, 6].map((d) => /* @__PURE__ */ jsx("option", { value: d, children: sundayFirstName(d) }, d))
3793
- }
3794
- ) }),
3795
- /* @__PURE__ */ jsx(Field, { label: l.defaultView, htmlFor: `${uid}-view`, children: /* @__PURE__ */ jsx(
3796
- NativeSelect,
3797
- {
3798
- id: `${uid}-view`,
3799
- value: value.default_view,
3800
- onChange: (e) => set({ default_view: e.target.value }),
3801
- "data-testid": "calendar-settings-default-view",
3802
- children: VIEWS.map((v) => /* @__PURE__ */ jsx("option", { value: v, children: viewName(v, l) }, v))
3803
- }
3804
- ) }),
3805
- /* @__PURE__ */ jsx(Field, { label: l.defaultDuration, htmlFor: `${uid}-duration`, children: /* @__PURE__ */ jsx(
3806
- NativeSelect,
3807
- {
3808
- id: `${uid}-duration`,
3809
- value: value.default_duration_minutes,
3810
- onChange: (e) => set({ default_duration_minutes: Number(e.target.value) }),
3811
- "data-testid": "calendar-settings-duration",
3812
- children: Array.from(/* @__PURE__ */ new Set([...DURATIONS, value.default_duration_minutes])).sort((a, b) => a - b).map((m) => /* @__PURE__ */ jsx("option", { value: m, children: `${m} ${l.minutes}` }, m))
3813
- }
3814
- ) }),
3815
- writable.length > 0 && /* @__PURE__ */ jsx(Field, { label: l.defaultCalendar, htmlFor: `${uid}-calendar`, children: /* @__PURE__ */ jsxs(
3816
- NativeSelect,
3817
- {
3818
- id: `${uid}-calendar`,
3819
- value: value.default_calendar_id ?? "",
3820
- onChange: (e) => set({ default_calendar_id: e.target.value || null }),
3821
- "data-testid": "calendar-settings-default-calendar",
3822
- children: [
3823
- /* @__PURE__ */ jsx("option", { value: "", children: "\u2014" }),
3824
- writable.map((c) => /* @__PURE__ */ jsx("option", { value: c.id, children: c.name ?? c.id }, c.id))
3825
- ]
3826
- }
3827
- ) }),
3828
- /* @__PURE__ */ jsxs("fieldset", { className: "space-y-2", children: [
3829
- /* @__PURE__ */ jsx("legend", { className: "pb-1 text-sm font-medium", children: l.workingHours }),
3830
- [0, 1, 2, 3, 4, 5, 6].map((day) => {
3831
- const h = hoursFor(day);
3832
- return /* @__PURE__ */ jsxs(
3833
- "div",
3834
- {
3835
- className: "flex flex-wrap items-center gap-2 text-sm",
3836
- "data-testid": `calendar-settings-hours-${day}`,
3837
- children: [
3838
- /* @__PURE__ */ jsxs("label", { className: "flex w-36 items-center gap-2", children: [
3839
- /* @__PURE__ */ jsx(
3840
- "input",
3841
- {
3842
- type: "checkbox",
3843
- checked: !!h,
3844
- onChange: (e) => setHours(day, e.target.checked ? {} : null)
3845
- }
3846
- ),
3847
- mondayFirstName(day)
3848
- ] }),
3849
- h && /* @__PURE__ */ jsxs(Fragment, { children: [
3850
- /* @__PURE__ */ jsx(
3851
- Input,
3852
- {
3853
- type: "time",
3854
- className: "h-8 w-28",
3855
- "aria-label": `${mondayFirstName(day)} ${l.start}`,
3856
- value: h.start,
3857
- onChange: (e) => e.target.value && setHours(day, { start: e.target.value })
3858
- }
3859
- ),
3860
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2013" }),
3861
- /* @__PURE__ */ jsx(
3862
- Input,
3863
- {
3864
- type: "time",
3865
- className: "h-8 w-28",
3866
- "aria-label": `${mondayFirstName(day)} ${l.end}`,
3867
- value: h.end === "24:00" ? "23:59" : h.end,
3868
- onChange: (e) => e.target.value && setHours(day, { end: e.target.value })
3869
- }
3870
- )
3871
- ] })
3872
- ]
3873
- },
3874
- day
3875
- );
3876
- })
3877
- ] }),
3878
- /* @__PURE__ */ jsx("div", { className: "space-y-3", children: [
3879
- ["show_week_numbers", l.showWeekNumbers],
3880
- ["show_declined", l.showDeclined]
3881
- ].map(([key, label]) => /* @__PURE__ */ jsxs(
3882
- "label",
3883
- {
3884
- className: "flex items-center justify-between gap-4 text-sm sm:justify-start",
3885
- children: [
3886
- /* @__PURE__ */ jsx("span", { className: "sm:w-48", children: label }),
3887
- /* @__PURE__ */ jsx(
3888
- Switch,
3889
- {
3890
- checked: !!value[key],
3891
- onCheckedChange: (on) => set({ [key]: on }),
3892
- "aria-label": label,
3893
- "data-testid": `calendar-settings-${key}`
3894
- }
3895
- )
3896
- ]
3897
- },
3898
- key
3899
- )) }),
3900
- /* @__PURE__ */ jsx(Field, { label: l.autoAddInvitations, htmlFor: `${uid}-auto-add`, children: /* @__PURE__ */ jsxs(
3901
- NativeSelect,
3902
- {
3903
- id: `${uid}-auto-add`,
3904
- value: value.auto_add_invitations ?? "all",
3905
- onChange: (e) => set({
3906
- auto_add_invitations: e.target.value
3907
- }),
3908
- "data-testid": "calendar-settings-auto-add",
3909
- children: [
3910
- /* @__PURE__ */ jsx("option", { value: "all", children: l.autoAddAll }),
3911
- /* @__PURE__ */ jsx("option", { value: "known", children: l.autoAddKnown }),
3912
- /* @__PURE__ */ jsx("option", { value: "none", children: l.autoAddNone })
3913
- ]
3914
- }
3915
- ) }),
3916
- /* @__PURE__ */ jsxs("fieldset", { className: "space-y-1.5", children: [
3917
- /* @__PURE__ */ jsx("legend", { className: "pb-1 text-sm font-medium", children: l.overlays }),
3918
- [
3919
- ["facts", l.facts],
3920
- ["workflows", l.workflows],
3921
- ["tasks", l.tasks]
3922
- ].map(([o, label]) => /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-sm", children: [
3923
- /* @__PURE__ */ jsx(
3924
- "input",
3925
- {
3926
- type: "checkbox",
3927
- checked: overlays.includes(o),
3928
- onChange: () => toggleOverlay(o),
3929
- "data-testid": `calendar-settings-overlay-${o}`
3930
- }
3931
- ),
3932
- label
3933
- ] }, o))
3934
- ] }),
3935
- /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
3936
- /* @__PURE__ */ jsx("label", { htmlFor: `${uid}-decline`, className: "text-sm font-medium", children: l.declineMessage }),
3937
- /* @__PURE__ */ jsx(
3938
- Textarea,
3939
- {
3940
- id: `${uid}-decline`,
3941
- rows: 2,
3942
- maxLength: 1e3,
3943
- value: value.decline_message ?? "",
3944
- onChange: (e) => set({ decline_message: e.target.value || null })
3945
- }
3946
- )
3947
- ] }),
3948
- error && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", role: "alert", children: error }),
3949
- /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
3950
- onCancel && /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: onCancel, children: l.cancel }),
3951
- /* @__PURE__ */ jsx(Button, { type: "submit", disabled: !!saving, "data-testid": "calendar-settings-save", children: saving ? l.saving : l.save })
3952
- ] })
3953
- ]
3954
- }
3955
- );
3956
- }
3957
-
3958
- // src/calendar/print-layout.ts
3959
- function printRange(view, date, weekStart = 1, agendaDays = 30) {
3960
- if (view === "day") return { from: date, to: addDays(date, 1) };
3961
- if (view === "agenda") return { from: date, to: addDays(date, agendaDays) };
3962
- if (view === "week") {
3963
- const from2 = startOfWeek(date, weekStart);
3964
- return { from: from2, to: addDays(from2, 7) };
3965
- }
3966
- const first = `${date.slice(0, 8)}01`;
3967
- const from = startOfWeek(first, weekStart);
3968
- const nextMonth = addMonths(first, 1);
3969
- const lead = (dayOfWeekSun0(nextMonth) - weekStart + 7) % 7;
3970
- const to = lead === 0 ? nextMonth : addDays(nextMonth, 7 - lead);
3971
- return { from, to };
3972
- }
3973
- function eachDay(from, to) {
3974
- const out = [];
3975
- for (let d = from; d < to; d = addDays(d, 1)) out.push(d);
3976
- return out;
3977
- }
3978
- function eventDays(e, zone) {
3979
- if (e.all_day) {
3980
- const start = e.start.date ?? utcToDate(e.start_utc, "UTC");
3981
- const endExcl = e.end.date ?? addDays(start, 1);
3982
- return eachDay(start, endExcl > start ? endExcl : addDays(start, 1));
3983
- }
3984
- const startDay = utcToDate(e.start_utc, zone);
3985
- const lastInstant = new Date(
3986
- Math.max(new Date(e.start_utc).getTime(), new Date(e.end_utc).getTime() - 1)
3987
- );
3988
- const endDay = utcToDate(lastInstant, zone);
3989
- return eachDay(startDay, addDays(endDay, 1));
3990
- }
3991
- function timeLabel(e, day, zone) {
3992
- if (e.all_day) return "All day";
3993
- const fmt = new Intl.DateTimeFormat(void 0, {
3994
- timeZone: zone,
3995
- hour: "numeric",
3996
- minute: "2-digit"
3997
- });
3998
- const sDay = utcToDate(e.start_utc, zone);
3999
- const eDay = utcToDate(new Date(new Date(e.end_utc).getTime() - 1), zone);
4000
- const s = sDay === day ? fmt.format(new Date(e.start_utc)) : "\u2026";
4001
- const en = eDay === day ? fmt.format(new Date(e.end_utc)) : "\u2026";
4002
- return `${s} \u2013 ${en}`;
4003
- }
4004
- function eventsByDay(events, zone, from, to) {
4005
- const map = /* @__PURE__ */ new Map();
4006
- for (const e of events) {
4007
- for (const d of eventDays(e, zone)) {
4008
- if (d < from || d >= to) continue;
4009
- const list = map.get(d) ?? [];
4010
- list.push(e);
4011
- map.set(d, list);
4012
- }
4013
- }
4014
- for (const list of map.values())
4015
- list.sort(
4016
- (a, b) => Number(b.all_day) - Number(a.all_day) || new Date(a.start_utc).getTime() - new Date(b.start_utc).getTime() || a.title.localeCompare(b.title)
4017
- );
4018
- return map;
4019
- }
4020
- function heading(view, date, from, to) {
4021
- const last = addDays(to, -1);
4022
- if (view === "day")
4023
- return formatPlainDate(from, {
4024
- weekday: "long",
4025
- year: "numeric",
4026
- month: "long",
4027
- day: "numeric"
4028
- });
4029
- if (view === "month")
4030
- return formatPlainDate(`${date.slice(0, 7)}-15`, { year: "numeric", month: "long" });
4031
- const opts = { year: "numeric", month: "short", day: "numeric" };
4032
- return `${formatPlainDate(from, opts)} \u2013 ${formatPlainDate(last, opts)}`;
4033
- }
4034
- var STYLE = `
4035
- .arbi-calendar-print { color: #111; background: #fff; font: 12px/1.4 system-ui, -apple-system, 'Segoe UI', sans-serif; }
4036
- .arbi-calendar-print h1 { font-size: 18px; margin: 0 0 8px; }
4037
- .arbi-calendar-print .acp-zone { color: #555; font-size: 11px; margin-bottom: 12px; }
4038
- .arbi-calendar-print table { border-collapse: collapse; width: 100%; table-layout: fixed; }
4039
- .arbi-calendar-print th, .arbi-calendar-print td { border: 1px solid #bbb; padding: 4px; vertical-align: top; text-align: left; }
4040
- .arbi-calendar-print th { background: #f3f3f3; font-weight: 600; }
4041
- .arbi-calendar-print .acp-day { break-inside: avoid; margin-bottom: 10px; }
4042
- .arbi-calendar-print .acp-day h2 { font-size: 13px; margin: 0 0 4px; border-bottom: 1px solid #bbb; padding-bottom: 2px; }
4043
- .arbi-calendar-print .acp-ev { display: flex; gap: 6px; margin: 2px 0; break-inside: avoid; }
4044
- .arbi-calendar-print .acp-time { flex: 0 0 auto; min-width: 92px; color: #333; font-variant-numeric: tabular-nums; }
4045
- .arbi-calendar-print .acp-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 4px; vertical-align: middle; print-color-adjust: exact; -webkit-print-color-adjust: exact; }
4046
- .arbi-calendar-print .acp-meta { color: #555; font-size: 11px; white-space: pre-line; }
4047
- .arbi-calendar-print .acp-muted { color: #888; }
4048
- .arbi-calendar-print .acp-cancelled { text-decoration: line-through; color: #888; }
4049
- .arbi-calendar-print .acp-month td { height: 90px; }
4050
- .arbi-calendar-print .acp-month .acp-out { color: #aaa; }
4051
- .arbi-calendar-print .acp-cell-ev { font-size: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
4052
- @media screen { .arbi-calendar-print[data-screen-hidden='true'] { display: none; } }
4053
- @media print {
4054
- @page { margin: 12mm; }
4055
- body.arbi-printing-calendar > *:not(.arbi-calendar-print-host) { display: none !important; }
4056
- body.arbi-printing-calendar .arbi-calendar-print-host { display: block !important; }
4057
- body.arbi-printing-calendar .arbi-calendar-print { display: block !important; }
4058
- }
4059
- `;
4060
- function CalendarPrint({
4061
- events,
4062
- view,
4063
- date,
4064
- timeZone,
4065
- weekStart = 1,
4066
- agendaDays = 30,
4067
- title,
4068
- calendars,
4069
- screenHidden = false,
4070
- showDetails = true
4071
- }) {
4072
- const zone = safeZone(timeZone, detectTimeZone());
4073
- const { from, to } = printRange(view, date, weekStart, agendaDays);
4074
- const byDay = eventsByDay(events, zone, from, to);
4075
- const color = (e) => e.color || calendars?.find((c) => c.id === e.calendar_id)?.color || "#3b82f6";
4076
- const label = (e) => e.restricted && !e.title ? "Busy" : e.title || "(no title)";
4077
- const evLine = (e, day, details) => /* @__PURE__ */ jsxs("div", { className: "acp-ev", "data-testid": "calendar-print-event", children: [
4078
- /* @__PURE__ */ jsx("span", { className: "acp-time", children: timeLabel(e, day, zone) }),
4079
- /* @__PURE__ */ jsxs("span", { children: [
4080
- /* @__PURE__ */ jsx("span", { className: "acp-dot", style: { background: color(e) } }),
4081
- /* @__PURE__ */ jsx("span", { className: e.status === "cancelled" ? "acp-cancelled" : void 0, children: label(e) }),
4082
- details && e.location && /* @__PURE__ */ jsx("div", { className: "acp-meta", children: e.location }),
4083
- details && e.description && /* @__PURE__ */ jsx("div", { className: "acp-meta", children: e.description.slice(0, 500) })
4084
- ] })
4085
- ] }, `${e.instance_id}-${day}`);
4086
- let body;
4087
- if (view === "agenda" || view === "day") {
4088
- const days = view === "day" ? [from] : eachDay(from, to).filter((d) => byDay.has(d));
4089
- body = days.length ? days.map((d) => /* @__PURE__ */ jsxs("section", { className: "acp-day", children: [
4090
- /* @__PURE__ */ jsx("h2", { children: formatPlainDate(d, { weekday: "long", month: "long", day: "numeric" }) }),
4091
- (byDay.get(d) ?? []).map((e) => evLine(e, d, showDetails)),
4092
- !byDay.get(d)?.length && /* @__PURE__ */ jsx("div", { className: "acp-muted", children: "No events" })
4093
- ] }, d)) : /* @__PURE__ */ jsx("p", { className: "acp-muted", children: "No events" });
4094
- } else if (view === "week") {
4095
- const days = eachDay(from, to);
4096
- body = /* @__PURE__ */ jsxs("table", { children: [
4097
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: days.map((d) => /* @__PURE__ */ jsx("th", { children: formatPlainDate(d, { weekday: "short", day: "numeric", month: "short" }) }, d)) }) }),
4098
- /* @__PURE__ */ jsx("tbody", { children: /* @__PURE__ */ jsx("tr", { children: days.map((d) => /* @__PURE__ */ jsx("td", { style: { height: 480 }, children: (byDay.get(d) ?? []).map((e) => evLine(e, d, false)) }, d)) }) })
4099
- ] });
4100
- } else {
4101
- const days = eachDay(from, to);
4102
- const weeks = [];
4103
- for (let i = 0; i < days.length; i += 7) weeks.push(days.slice(i, i + 7));
4104
- const month = date.slice(0, 7);
4105
- body = /* @__PURE__ */ jsxs("table", { className: "acp-month", children: [
4106
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: weeks[0].map((d) => /* @__PURE__ */ jsx("th", { children: formatPlainDate(d, { weekday: "short" }) }, d)) }) }),
4107
- /* @__PURE__ */ jsx("tbody", { children: weeks.map((w) => /* @__PURE__ */ jsx("tr", { children: w.map((d) => /* @__PURE__ */ jsxs("td", { className: d.slice(0, 7) === month ? void 0 : "acp-out", children: [
4108
- /* @__PURE__ */ jsx("div", { children: Number(d.slice(8)) }),
4109
- (byDay.get(d) ?? []).slice(0, 6).map((e) => /* @__PURE__ */ jsxs(
4110
- "div",
4111
- {
4112
- className: "acp-cell-ev",
4113
- "data-testid": "calendar-print-event",
4114
- children: [
4115
- /* @__PURE__ */ jsx("span", { className: "acp-dot", style: { background: color(e) } }),
4116
- !e.all_day && `${timeLabel(e, d, zone).split(" \u2013 ")[0]} `,
4117
- label(e)
4118
- ]
4119
- },
4120
- `${e.instance_id}-${d}`
4121
- )),
4122
- (byDay.get(d)?.length ?? 0) > 6 && /* @__PURE__ */ jsxs("div", { className: "acp-cell-ev acp-muted", children: [
4123
- "+",
4124
- (byDay.get(d)?.length ?? 0) - 6,
4125
- " more"
4126
- ] })
4127
- ] }, d)) }, w[0])) })
4128
- ] });
4129
- }
4130
- return /* @__PURE__ */ jsxs(
4131
- "div",
4132
- {
4133
- className: "arbi-calendar-print",
4134
- "data-screen-hidden": screenHidden ? "true" : "false",
4135
- "data-testid": "calendar-print",
4136
- children: [
4137
- /* @__PURE__ */ jsx("style", { children: STYLE }),
4138
- /* @__PURE__ */ jsx("h1", { children: title ?? heading(view, date, from, to) }),
4139
- /* @__PURE__ */ jsxs("div", { className: "acp-zone", children: [
4140
- "Times in ",
4141
- zone.replace(/_/g, " ")
4142
- ] }),
4143
- body
4144
- ]
4145
- }
4146
- );
4147
- }
4148
- function printCalendar(props) {
4149
- const host = document.createElement("div");
4150
- host.className = "arbi-calendar-print-host";
4151
- host.style.display = "none";
4152
- document.body.appendChild(host);
4153
- let unmount = null;
4154
- if (props) {
4155
- const root = createRoot(host);
4156
- flushSync(() => root.render(/* @__PURE__ */ jsx(CalendarPrint, { ...props, screenHidden: false })));
4157
- unmount = () => root.unmount();
4158
- } else {
4159
- const existing = document.querySelector(".arbi-calendar-print");
4160
- if (!existing) {
4161
- host.remove();
4162
- return;
4163
- }
4164
- const clone = existing.cloneNode(true);
4165
- clone.setAttribute("data-screen-hidden", "false");
4166
- host.appendChild(clone);
4167
- }
4168
- document.body.classList.add("arbi-printing-calendar");
4169
- let done = false;
4170
- const cleanup = () => {
4171
- if (done) return;
4172
- done = true;
4173
- window.removeEventListener("afterprint", cleanup);
4174
- document.body.classList.remove("arbi-printing-calendar");
4175
- unmount?.();
4176
- host.remove();
4177
- };
4178
- window.addEventListener("afterprint", cleanup);
4179
- try {
4180
- window.print();
4181
- } finally {
4182
- if (!("onafterprint" in window)) setTimeout(cleanup, 1e3);
4183
- }
4184
- }
4185
-
4186
- // src/calendar/booking-slots.ts
4187
- function formatSlotTime(instant, zone, locale) {
4188
- return formatInZone(instant, zone, { hour: "numeric", minute: "2-digit" }, locale);
4189
- }
4190
- function formatDayLabel(date, locale) {
4191
- return formatPlainDate(date, { weekday: "long", month: "long", day: "numeric" }, locale);
4192
- }
4193
- function groupSlotsByDay(slots, zone, locale) {
4194
- const tz = safeZone(zone, detectTimeZone());
4195
- const byDay = /* @__PURE__ */ new Map();
4196
- const sorted = [...slots].sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
4197
- for (const start of sorted) {
4198
- const date = utcToDate(start, tz);
4199
- const list = byDay.get(date) ?? [];
4200
- list.push({ start, label: formatSlotTime(start, tz, locale) });
4201
- byDay.set(date, list);
4202
- }
4203
- return [...byDay.entries()].map(([date, list]) => ({
4204
- date,
4205
- label: formatDayLabel(date, locale),
4206
- slots: list
4207
- }));
4208
- }
4209
- function formatSlotRange(start, end, zone, locale) {
4210
- const tz = safeZone(zone, detectTimeZone());
4211
- const fmt = new Intl.DateTimeFormat(locale, {
4212
- timeZone: tz,
4213
- weekday: "long",
4214
- month: "long",
4215
- day: "numeric",
4216
- hour: "numeric",
4217
- minute: "2-digit"
4218
- });
4219
- const s = new Date(start);
4220
- const e = new Date(end);
4221
- const range = fmt.formatRange;
4222
- const text = range ? range.call(fmt, s, e) : `${fmt.format(s)} \u2013 ${formatSlotTime(end, tz, locale)}`;
4223
- return `${text} (${tz.replace(/_/g, " ")})`;
4224
- }
4225
- function zoneOptions(current) {
4226
- const zones = listTimeZones();
4227
- return zones.includes(current) ? zones : [current, ...zones];
4228
- }
4229
-
4230
- export { ARBI_WEBINAR_LABEL, CALENDAR_COLORS, CALENDAR_DEFAULT_LABELS, CALENDAR_NARROW_WIDTH, CalendarGrid, CalendarList, CalendarPrint, CalendarSettingsForm, DEFAULT_RECURRENCE_DESCRIBE_LABELS, DEFAULT_RECURRENCE_EDITOR_LABELS, DEFAULT_REMINDER_FORMAT_LABELS, EventDetails, EventEditor, MiniMonth, REMINDER_PRESETS, RRULE_WEEKDAYS, RecurrenceEditor, RoomFinder, SchedulingAssistant, ScopeDialog, TimeZonePicker, allDayInclusiveEnd, attendeeKey, buildRRule, contrastText, dayWindow, defaultRecurrenceForm, describeRecurrence, draftFromSlot, draftRangeUtc, effectiveCalendarView, eventDays, eventStateClasses, eventsByDay, formatDayLabel, formatEventWhen, formatReminder, formatSlotRange, formatSlotTime, formatZoneLabel, getTemporalTimeZonePlugin, groupSlotsByDay, hourTicks, isAllDayDraft, isWebinarConference, outsideIntervals, parseRRule, placeIntervals, printCalendar, printRange, reminderToMinutes, setDraftAllDay, setDraftEnd, setDraftStart, setPosOf, specDate, specTime, splitReminderMinutes, toBusinessHours, validateDraft, weekdayOf, workWeekHiddenDays, zoneOptions };
4231
5
  //# sourceMappingURL=index.js.map
4232
6
  //# sourceMappingURL=index.js.map