@rapidmx/web-client 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/apps/admin/audit-log/index.tsx +35 -8
  2. package/apps/admin/data-requests/index.tsx +480 -485
  3. package/apps/admin/distribution-lists/[uid].tsx +19 -1
  4. package/apps/admin/escrow-scopes/[uid].tsx +348 -237
  5. package/apps/admin/escrow-scopes/new/index.tsx +8 -2
  6. package/apps/admin/index.tsx +19 -1
  7. package/apps/admin/ingest-queue/index.tsx +44 -5
  8. package/apps/admin/mailboxes/[uid].tsx +211 -184
  9. package/apps/admin/quarantine/index.tsx +46 -14
  10. package/apps/admin/transport-rules/[uid].tsx +30 -1
  11. package/apps/admin/transport-rules/_transportRuleConfig.tsx +38 -2
  12. package/apps/admin/transport-rules/index.tsx +24 -6
  13. package/apps/admin/transport-rules/new/index.tsx +30 -1
  14. package/apps/escrow/_layout.tsx +2 -3
  15. package/apps/escrow/audit-log/index.tsx +196 -168
  16. package/apps/escrow/matters/[uid].tsx +621 -521
  17. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +22 -1
  18. package/apps/shared/components/admin/layout/AdminShell.tsx +3 -1
  19. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -0
  20. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  21. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +55 -11
  22. package/apps/shared/components/admin/settings/BrandingForm.tsx +15 -4
  23. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +18 -7
  24. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +10 -2
  25. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +6 -0
  26. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +45 -12
  27. package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -595
  28. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -98
  29. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +49 -13
  30. package/apps/shared/components/admin/setup/SetupWizard.tsx +150 -34
  31. package/apps/shared/components/admin/signOut.ts +79 -0
  32. package/apps/shared/components/admin/usePagedList.tsx +129 -0
  33. package/apps/shared/components/calendar/EventModal.tsx +717 -549
  34. package/apps/shared/components/calendar/MonthView.tsx +173 -139
  35. package/apps/shared/components/calendar/RecurrenceEditor.tsx +222 -180
  36. package/apps/shared/components/calendar/SplitDayView.tsx +143 -137
  37. package/apps/shared/components/calendar/TimeGridView.tsx +244 -198
  38. package/apps/shared/components/calendar/allDay.ts +124 -0
  39. package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
  40. package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
  41. package/apps/shared/components/contacts/ContactForm.tsx +377 -348
  42. package/apps/shared/components/contacts/ContactsSidebar.tsx +6 -2
  43. package/apps/shared/components/contacts/KeyChangeReview.tsx +192 -0
  44. package/apps/shared/components/contacts/contactKeys.ts +78 -0
  45. package/apps/shared/components/escrow/layout/EscrowShell.tsx +148 -134
  46. package/apps/shared/components/layout/AppShell.tsx +87 -15
  47. package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
  48. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +389 -298
  49. package/apps/shared/components/layout/MailboxProvisioning.tsx +19 -3
  50. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +350 -0
  51. package/apps/shared/components/layout/UnlockPromptProvider.tsx +257 -128
  52. package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
  53. package/apps/shared/components/mail/MessageDetailPane.tsx +1318 -630
  54. package/apps/shared/components/mail/compose/ComposeContext.tsx +20 -15
  55. package/apps/shared/components/mail/compose/ComposeWindow.tsx +854 -132
  56. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
  57. package/apps/shared/components/mail/layout/MailShell.tsx +12 -2
  58. package/apps/shared/components/mail/pinnedSigners.ts +124 -0
  59. package/apps/shared/components/mail/verificationSeals.ts +125 -0
  60. package/apps/shared/components/mail/writableMailboxes.ts +101 -0
  61. package/apps/shared/components/rules/RuleBuilder.tsx +311 -276
  62. package/apps/shared/mail/listAllPages.ts +39 -0
  63. package/apps/shared/search/LocalIndexLifecycle.tsx +64 -55
  64. package/apps/shared/search/localIndexBuilder.ts +481 -179
  65. package/apps/shared/search/localIndexRpcClient.ts +228 -28
  66. package/apps/shared/search/localIndexSchema.ts +15 -6
  67. package/apps/shared/search/localIndexStorage.ts +98 -0
  68. package/apps/shared/search/localIndexVFS.ts +32 -1
  69. package/apps/shared/search/localIndexWorker.ts +648 -186
  70. package/apps/www/calendar/index.tsx +438 -417
  71. package/apps/www/contacts/[uid].tsx +21 -2
  72. package/apps/www/contacts/index.tsx +116 -12
  73. package/apps/www/index.tsx +1314 -1041
  74. package/apps/www/messages/[uid].tsx +18 -6
  75. package/apps/www/settings/auto-reply/index.tsx +134 -129
  76. package/apps/www/settings/booking-types/[uid].tsx +306 -300
  77. package/apps/www/settings/encryption/index.tsx +739 -216
  78. package/apps/www/settings/filters/[uid].tsx +171 -155
  79. package/apps/www/settings/filters/new/index.tsx +8 -1
  80. package/apps/www/settings/focused-inbox/index.tsx +150 -148
  81. package/apps/www/settings/privacy/index.tsx +494 -436
  82. package/apps/www/settings/read-receipts/index.tsx +148 -125
  83. package/apps/www/settings/sharing/index.tsx +17 -14
  84. package/apps/www/tasks/index.tsx +638 -579
  85. package/dist/apps/admin/audit-log/index.js +38 -8
  86. package/dist/apps/admin/data-requests/index.js +52 -72
  87. package/dist/apps/admin/distribution-lists/[uid].js +1 -1
  88. package/dist/apps/admin/escrow-scopes/[uid].js +91 -12
  89. package/dist/apps/admin/escrow-scopes/new/index.js +8 -3
  90. package/dist/apps/admin/index.js +4 -1
  91. package/dist/apps/admin/ingest-queue/index.js +24 -6
  92. package/dist/apps/admin/mailboxes/[uid].js +20 -4
  93. package/dist/apps/admin/quarantine/index.js +20 -11
  94. package/dist/apps/admin/transport-rules/[uid].js +18 -2
  95. package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
  96. package/dist/apps/admin/transport-rules/index.js +22 -6
  97. package/dist/apps/admin/transport-rules/new/index.js +18 -2
  98. package/dist/apps/escrow/_layout.js +3 -3
  99. package/dist/apps/escrow/audit-log/index.js +26 -1
  100. package/dist/apps/escrow/matters/[uid].js +77 -42
  101. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
  102. package/dist/apps/shared/components/admin/layout/AdminShell.js +3 -1
  103. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -0
  104. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  105. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
  106. package/dist/apps/shared/components/admin/settings/BrandingForm.js +15 -4
  107. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +13 -7
  108. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +7 -2
  109. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +6 -0
  110. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +34 -12
  111. package/dist/apps/shared/components/admin/settings/PluginsManager.js +390 -78
  112. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +40 -11
  113. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +29 -13
  114. package/dist/apps/shared/components/admin/setup/SetupWizard.js +87 -21
  115. package/dist/apps/shared/components/admin/signOut.js +75 -0
  116. package/dist/apps/shared/components/admin/usePagedList.js +99 -0
  117. package/dist/apps/shared/components/calendar/EventModal.js +142 -21
  118. package/dist/apps/shared/components/calendar/MonthView.js +12 -5
  119. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
  120. package/dist/apps/shared/components/calendar/SplitDayView.js +10 -4
  121. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
  122. package/dist/apps/shared/components/calendar/allDay.js +111 -0
  123. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
  124. package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
  125. package/dist/apps/shared/components/contacts/ContactForm.js +32 -10
  126. package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
  127. package/dist/apps/shared/components/contacts/KeyChangeReview.js +63 -0
  128. package/dist/apps/shared/components/contacts/contactKeys.js +64 -0
  129. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +24 -5
  130. package/dist/apps/shared/components/layout/AppShell.js +83 -14
  131. package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
  132. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +49 -12
  133. package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
  134. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
  135. package/dist/apps/shared/components/layout/UnlockPromptProvider.js +73 -14
  136. package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
  137. package/dist/apps/shared/components/mail/MessageDetailPane.js +441 -36
  138. package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
  139. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +699 -128
  140. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
  141. package/dist/apps/shared/components/mail/layout/MailShell.js +12 -3
  142. package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
  143. package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
  144. package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
  145. package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
  146. package/dist/apps/shared/mail/listAllPages.js +25 -0
  147. package/dist/apps/shared/search/LocalIndexLifecycle.js +58 -54
  148. package/dist/apps/shared/search/localIndexBuilder.js +292 -37
  149. package/dist/apps/shared/search/localIndexRpcClient.js +188 -25
  150. package/dist/apps/shared/search/localIndexSchema.js +12 -6
  151. package/dist/apps/shared/search/localIndexStorage.js +86 -0
  152. package/dist/apps/shared/search/localIndexVFS.js +68 -40
  153. package/dist/apps/shared/search/localIndexWorker.js +519 -173
  154. package/dist/apps/www/calendar/index.js +28 -10
  155. package/dist/apps/www/contacts/[uid].js +13 -2
  156. package/dist/apps/www/contacts/index.js +80 -10
  157. package/dist/apps/www/index.js +339 -108
  158. package/dist/apps/www/messages/[uid].js +18 -6
  159. package/dist/apps/www/settings/auto-reply/index.js +9 -4
  160. package/dist/apps/www/settings/booking-types/[uid].js +17 -12
  161. package/dist/apps/www/settings/encryption/index.js +514 -97
  162. package/dist/apps/www/settings/filters/[uid].js +22 -7
  163. package/dist/apps/www/settings/filters/new/index.js +8 -2
  164. package/dist/apps/www/settings/focused-inbox/index.js +3 -1
  165. package/dist/apps/www/settings/privacy/index.js +57 -22
  166. package/dist/apps/www/settings/read-receipts/index.js +10 -3
  167. package/dist/apps/www/settings/sharing/index.js +7 -11
  168. package/dist/apps/www/tasks/index.js +50 -10
  169. package/package.json +2 -2
@@ -1,137 +1,143 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import React from "react";
6
- import { addMinutes, format, isSameDay, startOfDay } from "date-fns";
7
- import { CalendarOccurrence } from "@rapidmx/react-shared/calendar/recurrence.js";
8
-
9
- const HOUR_HEIGHT_PX = 48;
10
- const SLOT_MINUTES = 30;
11
- const SLOTS_PER_DAY = (24 * 60) / SLOT_MINUTES;
12
- const HOURS = Array.from({ length: 24 }, (_, h) => h);
13
-
14
- export interface SplitDayColumn {
15
- folderUid: string;
16
- name: string;
17
- color: string;
18
- }
19
-
20
- export interface SplitDayViewProps {
21
- day: Date;
22
- columns: SplitDayColumn[];
23
- /** Already expanded for `day` — see `recurrence.ts`'s `expandAllOccurrences`. Not pre-filtered by
24
- * calendar; this component splits them into columns itself by `occurrence.folderUid`. */
25
- occurrences: CalendarOccurrence[];
26
- onSelectEvent: (occurrence: CalendarOccurrence) => void;
27
- /** Click on an empty slot in one calendar's column — the calendar it belongs to is `folderUid`. */
28
- onSelectSlot: (start: Date, end: Date, folderUid: string) => void;
29
- }
30
-
31
- /**
32
- * Outlook's "Split" view: one column per checked calendar, all showing the same single day, so
33
- * events across calendars can be compared side by side. A deliberately separate component from
34
- * `TimeGridView` rather than a generalization of it — it shares that component's visual language
35
- * (same hour rail, same slot height) but is genuinely simpler in one respect: no drag-to-move/resize.
36
- * Dragging an event between calendar columns would mean reassigning its `folderUid`, a real feature
37
- * `moveOccurrence`/`resolveDragAction` don't support today; rather than bolt that on, this view is
38
- * click-only (select an event, or click a slot to create one already targeted at that column's
39
- * calendar) — a practical scope cut, not an oversight.
40
- */
41
- export default function SplitDayView({ day, columns, occurrences, onSelectEvent, onSelectSlot }: SplitDayViewProps) {
42
- const dayOccurrences = occurrences.filter((occ) => isSameDay(new Date(occ.startDate), day));
43
- const dayStart = startOfDay(day);
44
-
45
- return (
46
- <div role="region" aria-label="Split view" className="flex-1 flex flex-col min-h-0 overflow-y-auto">
47
- <div className="flex border-b border-border shrink-0">
48
- <div className="w-14 shrink-0" />
49
- {columns.map((col) => (
50
- <div key={col.folderUid} className="flex-1 min-w-0 py-1 flex items-center justify-center gap-1.5 border-l border-border">
51
- <span aria-hidden="true" className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: col.color }} />
52
- <span className="text-xs font-semibold truncate">{col.name}</span>
53
- </div>
54
- ))}
55
- </div>
56
- <div className="flex flex-1">
57
- <div className="w-14 shrink-0">
58
- {HOURS.map((hour) => (
59
- <div key={hour} style={{ height: HOUR_HEIGHT_PX }} className="text-[10px] text-text-muted text-right pr-1.5 -mt-1.5">
60
- {hour === 0 ? "" : format(new Date(2000, 0, 1, hour), "ha")}
61
- </div>
62
- ))}
63
- </div>
64
- {columns.map((col) => (
65
- <SplitColumn
66
- key={col.folderUid}
67
- column={col}
68
- dayStart={dayStart}
69
- occurrences={dayOccurrences.filter((occ) => occ.folderUid === col.folderUid)}
70
- onSelectEvent={onSelectEvent}
71
- onSelectSlot={onSelectSlot}
72
- />
73
- ))}
74
- </div>
75
- </div>
76
- );
77
- }
78
-
79
- function SplitColumn({
80
- column,
81
- dayStart,
82
- occurrences,
83
- onSelectEvent,
84
- onSelectSlot,
85
- }: {
86
- column: SplitDayColumn;
87
- dayStart: Date;
88
- occurrences: CalendarOccurrence[];
89
- onSelectEvent: (occurrence: CalendarOccurrence) => void;
90
- onSelectSlot: (start: Date, end: Date, folderUid: string) => void;
91
- }) {
92
- return (
93
- <div className="flex-1 min-w-0 relative border-l border-border">
94
- {Array.from({ length: SLOTS_PER_DAY }, (_, i) => {
95
- const slotStart = addMinutes(dayStart, i * SLOT_MINUTES);
96
- return (
97
- <button
98
- key={i}
99
- type="button"
100
- onClick={() => onSelectSlot(slotStart, addMinutes(slotStart, SLOT_MINUTES), column.folderUid)}
101
- style={{ height: HOUR_HEIGHT_PX / 2 }}
102
- className="block w-full border-b border-border/50 text-left"
103
- aria-label={`New event at ${format(slotStart, "h:mm a")} in ${column.name}`}
104
- />
105
- );
106
- })}
107
- {occurrences.map((occurrence) => {
108
- const start = new Date(occurrence.startDate);
109
- const end = new Date(occurrence.endDate);
110
- const top = ((start.getTime() - dayStart.getTime()) / 60_000 / 60) * HOUR_HEIGHT_PX;
111
- const height = Math.max(((end.getTime() - start.getTime()) / 60_000 / 60) * HOUR_HEIGHT_PX, 16);
112
- const isFree = occurrence.busyStatus === "free";
113
- return (
114
- <div
115
- key={occurrence.occurrenceKey}
116
- onClick={() => onSelectEvent(occurrence)}
117
- style={{
118
- position: "absolute",
119
- top,
120
- height,
121
- left: 2,
122
- right: 2,
123
- ...(isFree ? undefined : { backgroundColor: column.color, color: "#fff" }),
124
- }}
125
- className={[
126
- "rounded-sm px-1.5 py-0.5 text-xs text-left overflow-hidden cursor-pointer",
127
- isFree ? "bg-surface-alt text-text-muted" : "",
128
- ].join(" ")}
129
- >
130
- <div className="font-medium truncate">{occurrence.title}</div>
131
- <div className="truncate">{format(start, "h:mma")}</div>
132
- </div>
133
- );
134
- })}
135
- </div>
136
- );
137
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React from "react";
6
+ import { addDays, addMinutes, format, startOfDay } from "date-fns";
7
+ import { CalendarOccurrence } from "@rapidmx/react-shared/calendar/recurrence.js";
8
+ import { occursOnDay } from "./allDay.js";
9
+
10
+ const HOUR_HEIGHT_PX = 48;
11
+ const SLOT_MINUTES = 30;
12
+ const SLOTS_PER_DAY = (24 * 60) / SLOT_MINUTES;
13
+ const HOURS = Array.from({ length: 24 }, (_, h) => h);
14
+
15
+ export interface SplitDayColumn {
16
+ folderUid: string;
17
+ name: string;
18
+ color: string;
19
+ }
20
+
21
+ export interface SplitDayViewProps {
22
+ day: Date;
23
+ columns: SplitDayColumn[];
24
+ /** Already expanded for `day` — see `recurrence.ts`'s `expandAllOccurrences`. Not pre-filtered by
25
+ * calendar; this component splits them into columns itself by `occurrence.folderUid`. */
26
+ occurrences: CalendarOccurrence[];
27
+ onSelectEvent: (occurrence: CalendarOccurrence) => void;
28
+ /** Click on an empty slot in one calendar's column — the calendar it belongs to is `folderUid`. */
29
+ onSelectSlot: (start: Date, end: Date, folderUid: string) => void;
30
+ }
31
+
32
+ /**
33
+ * Outlook's "Split" view: one column per checked calendar, all showing the same single day, so
34
+ * events across calendars can be compared side by side. A deliberately separate component from
35
+ * `TimeGridView` rather than a generalization of it — it shares that component's visual language
36
+ * (same hour rail, same slot height) but is genuinely simpler in one respect: no drag-to-move/resize.
37
+ * Dragging an event between calendar columns would mean reassigning its `folderUid`, a real feature
38
+ * `moveOccurrence`/`resolveDragAction` don't support today; rather than bolt that on, this view is
39
+ * click-only (select an event, or click a slot to create one already targeted at that column's
40
+ * calendar) — a practical scope cut, not an oversight.
41
+ */
42
+ export default function SplitDayView({ day, columns, occurrences, onSelectEvent, onSelectSlot }: SplitDayViewProps) {
43
+ const dayOccurrences = occurrences.filter((occ) => occursOnDay(occ, day));
44
+ const dayStart = startOfDay(day);
45
+
46
+ return (
47
+ <div role="region" aria-label="Split view" className="flex-1 flex flex-col min-h-0 overflow-y-auto">
48
+ <div className="flex border-b border-border shrink-0">
49
+ <div className="w-14 shrink-0" />
50
+ {columns.map((col) => (
51
+ <div key={col.folderUid} className="flex-1 min-w-0 py-1 flex items-center justify-center gap-1.5 border-l border-border">
52
+ <span aria-hidden="true" className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: col.color }} />
53
+ <span className="text-xs font-semibold truncate">{col.name}</span>
54
+ </div>
55
+ ))}
56
+ </div>
57
+ <div className="flex flex-1">
58
+ <div className="w-14 shrink-0">
59
+ {HOURS.map((hour) => (
60
+ <div key={hour} style={{ height: HOUR_HEIGHT_PX }} className="text-[10px] text-text-muted text-right pr-1.5 -mt-1.5">
61
+ {hour === 0 ? "" : format(new Date(2000, 0, 1, hour), "ha")}
62
+ </div>
63
+ ))}
64
+ </div>
65
+ {columns.map((col) => (
66
+ <SplitColumn
67
+ key={col.folderUid}
68
+ column={col}
69
+ dayStart={dayStart}
70
+ occurrences={dayOccurrences.filter((occ) => occ.folderUid === col.folderUid)}
71
+ onSelectEvent={onSelectEvent}
72
+ onSelectSlot={onSelectSlot}
73
+ />
74
+ ))}
75
+ </div>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ function SplitColumn({
81
+ column,
82
+ dayStart,
83
+ occurrences,
84
+ onSelectEvent,
85
+ onSelectSlot,
86
+ }: {
87
+ column: SplitDayColumn;
88
+ dayStart: Date;
89
+ occurrences: CalendarOccurrence[];
90
+ onSelectEvent: (occurrence: CalendarOccurrence) => void;
91
+ onSelectSlot: (start: Date, end: Date, folderUid: string) => void;
92
+ }) {
93
+ return (
94
+ <div className="flex-1 min-w-0 relative border-l border-border">
95
+ {Array.from({ length: SLOTS_PER_DAY }, (_, i) => {
96
+ const slotStart = addMinutes(dayStart, i * SLOT_MINUTES);
97
+ return (
98
+ <button
99
+ key={i}
100
+ type="button"
101
+ onClick={() => onSelectSlot(slotStart, addMinutes(slotStart, SLOT_MINUTES), column.folderUid)}
102
+ style={{ height: HOUR_HEIGHT_PX / 2 }}
103
+ className="block w-full border-b border-border/50 text-left"
104
+ aria-label={`New event at ${format(slotStart, "h:mm a")} in ${column.name}`}
105
+ />
106
+ );
107
+ })}
108
+ {occurrences.map((occurrence) => {
109
+ const start = new Date(occurrence.startDate);
110
+ const end = new Date(occurrence.endDate);
111
+ // Clipped to this day, so a multi-day event fills only the part of the day it covers. An
112
+ // all-day event is date-only (see `allDay.ts`), so it always spans the whole local day.
113
+ const dayEnd = addDays(dayStart, 1).getTime();
114
+ const visibleStart = occurrence.allDay ? dayStart.getTime() : Math.max(start.getTime(), dayStart.getTime());
115
+ const visibleEnd = occurrence.allDay ? dayEnd : Math.min(end.getTime(), dayEnd);
116
+ const top = ((visibleStart - dayStart.getTime()) / 60_000 / 60) * HOUR_HEIGHT_PX;
117
+ const height = Math.max(((visibleEnd - visibleStart) / 60_000 / 60) * HOUR_HEIGHT_PX, 16);
118
+ const isFree = occurrence.busyStatus === "free";
119
+ return (
120
+ <div
121
+ key={occurrence.occurrenceKey}
122
+ onClick={() => onSelectEvent(occurrence)}
123
+ style={{
124
+ position: "absolute",
125
+ top,
126
+ height,
127
+ left: 2,
128
+ right: 2,
129
+ ...(isFree ? undefined : { backgroundColor: column.color, color: "#fff" }),
130
+ }}
131
+ className={[
132
+ "rounded-sm px-1.5 py-0.5 text-xs text-left overflow-hidden cursor-pointer",
133
+ isFree ? "bg-surface-alt text-text-muted" : "",
134
+ ].join(" ")}
135
+ >
136
+ <div className="font-medium truncate">{occurrence.title}</div>
137
+ <div className="truncate">{format(start, "h:mma")}</div>
138
+ </div>
139
+ );
140
+ })}
141
+ </div>
142
+ );
143
+ }