@rapidmx/web-client 0.20.0 → 0.21.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.
- package/README.md +381 -381
- package/apps/admin/branding/index.tsx +39 -39
- package/apps/admin/data-requests/index.tsx +482 -482
- package/apps/admin/domains/[uid].tsx +166 -166
- package/apps/admin/escrow-scopes/[uid].tsx +350 -350
- package/apps/admin/index.tsx +129 -129
- package/apps/admin/mailboxes/[uid].tsx +271 -271
- package/apps/admin/mailboxes/new/index.tsx +30 -30
- package/apps/admin/plugins/index.tsx +15 -15
- package/apps/admin/retention-policy/index.tsx +39 -39
- package/apps/admin/signing-certificates/index.tsx +343 -343
- package/apps/escrow/audit-log/index.tsx +196 -196
- package/apps/escrow/matters/[uid].tsx +621 -621
- package/apps/shared/auth/adminAccess.ts +99 -99
- package/apps/shared/components/admin/layout/AdminShell.tsx +384 -384
- package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -238
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
- package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -194
- package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
- package/apps/shared/components/admin/settings/PluginsManager.tsx +2093 -2093
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
- package/apps/shared/components/admin/setup/SetupWizard.tsx +446 -446
- package/apps/shared/components/admin/usePagedList.tsx +129 -129
- package/apps/shared/components/calendar/EventModal.tsx +206 -206
- package/apps/shared/components/calendar/MonthView.tsx +185 -185
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
- package/apps/shared/components/calendar/SplitDayView.tsx +144 -144
- package/apps/shared/components/calendar/TimeGridView.tsx +246 -246
- package/apps/shared/components/calendar/allDay.ts +124 -124
- package/apps/shared/components/contacts/ContactForm.tsx +383 -383
- package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -103
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
- package/apps/shared/components/layout/AppShell.tsx +463 -461
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
- package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
- package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
- package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
- package/apps/shared/components/layout/UserMenu.tsx +439 -439
- package/apps/shared/components/mail/ConversationList.tsx +332 -332
- package/apps/shared/components/mail/ConversationThreadPane.tsx +613 -613
- package/apps/shared/components/mail/MailSelectionBar.tsx +240 -240
- package/apps/shared/components/mail/MenuButton.tsx +404 -404
- package/apps/shared/components/mail/MessageDetailPane.tsx +1757 -1757
- package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1705 -1681
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
- package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
- package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -258
- package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -79
- package/apps/shared/components/mail/reading/messageExport.ts +59 -59
- package/apps/shared/components/mail/reading/printMessage.ts +99 -99
- package/apps/shared/components/mail/reading/useMessageActions.ts +443 -443
- package/apps/shared/components/mail/verificationSeals.ts +125 -125
- package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
- package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
- package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
- package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
- package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
- package/apps/shared/keyboard/dispatch.ts +124 -124
- package/apps/shared/keyboard/format.ts +89 -89
- package/apps/shared/keyboard/keymap.ts +114 -114
- package/apps/shared/keyboard/registry.ts +65 -65
- package/apps/shared/keyboard/targets.ts +79 -79
- package/apps/shared/mail/folderOfType.ts +49 -49
- package/apps/shared/mail/folderTree.ts +143 -143
- package/apps/shared/mail/listAllPages.ts +39 -39
- package/apps/shared/mail/newMailNotifications.ts +171 -171
- package/apps/shared/mail/outbox/sendJob.ts +445 -445
- package/apps/shared/mail/outbox/sendOutcomes.ts +155 -155
- package/apps/shared/mail/reportNotices.ts +66 -66
- package/apps/shared/mail/senderBlocking.ts +141 -141
- package/apps/shared/mail/useMailConnection.ts +205 -205
- package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
- package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -60
- package/apps/shared/mail/useMarkMessageRead.ts +47 -47
- package/apps/shared/mail/useNewMailNotifications.ts +178 -178
- package/apps/shared/notifications/store.ts +560 -560
- package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
- package/apps/shared/search/localIndexBuilder.ts +481 -481
- package/apps/shared/signing/enrollmentStorage.ts +33 -33
- package/apps/shared/signing/enrollmentTracker.ts +385 -385
- package/apps/shared/signing/enrollmentView.ts +251 -251
- package/apps/shared/signing/useNow.ts +19 -19
- package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
- package/apps/shared/styles/app.css +396 -396
- package/apps/www/calendar/index.tsx +581 -581
- package/apps/www/contacts/[uid].tsx +112 -112
- package/apps/www/index.tsx +3012 -3012
- package/apps/www/messages/[uid].tsx +139 -139
- package/apps/www/settings/auto-reply/index.tsx +136 -136
- package/apps/www/settings/blocked-senders/index.tsx +303 -303
- package/apps/www/settings/encryption/index.tsx +1290 -1290
- package/apps/www/settings/filters/[uid].tsx +179 -179
- package/apps/www/settings/filters/index.tsx +105 -105
- package/apps/www/settings/filters/new/index.tsx +165 -165
- package/apps/www/settings/labels/index.tsx +207 -207
- package/apps/www/settings/privacy/index.tsx +495 -495
- package/apps/www/settings/profile/index.tsx +251 -251
- package/apps/www/settings/read-receipts/index.tsx +150 -150
- package/apps/www/settings/sharing/index.tsx +259 -259
- package/apps/www/settings/signatures/[uid].tsx +175 -175
- package/apps/www/settings/signatures/index.tsx +91 -91
- package/apps/www/settings/signatures/new/index.tsx +138 -138
- package/apps/www/tasks/index.tsx +654 -654
- package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -2
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
- package/dist/apps/shared/components/escrow/layout/EscrowShell.js +2 -2
- package/dist/apps/shared/components/layout/AppShell.js +4 -2
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +7 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +20 -1
- package/dist/apps/shared/components/mail/reading/printMessage.js +11 -11
- package/dist/apps/shared/styles/app.css +396 -396
- package/package.json +2 -2
|
@@ -1,144 +1,144 @@
|
|
|
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
|
-
import { EventAnchor, anchorOf } from "./EventShell.js";
|
|
10
|
-
|
|
11
|
-
const HOUR_HEIGHT_PX = 48;
|
|
12
|
-
const SLOT_MINUTES = 30;
|
|
13
|
-
const SLOTS_PER_DAY = (24 * 60) / SLOT_MINUTES;
|
|
14
|
-
const HOURS = Array.from({ length: 24 }, (_, h) => h);
|
|
15
|
-
|
|
16
|
-
export interface SplitDayColumn {
|
|
17
|
-
folderUid: string;
|
|
18
|
-
name: string;
|
|
19
|
-
color: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface SplitDayViewProps {
|
|
23
|
-
day: Date;
|
|
24
|
-
columns: SplitDayColumn[];
|
|
25
|
-
/** Already expanded for `day` — see `recurrence.ts`'s `expandAllOccurrences`. Not pre-filtered by
|
|
26
|
-
* calendar; this component splits them into columns itself by `occurrence.folderUid`. */
|
|
27
|
-
occurrences: CalendarOccurrence[];
|
|
28
|
-
onSelectEvent: (occurrence: CalendarOccurrence) => void;
|
|
29
|
-
/** Click on an empty slot in one calendar's column — the calendar it belongs to is `folderUid`; `anchor` is the slot. */
|
|
30
|
-
onSelectSlot: (start: Date, end: Date, folderUid: string, anchor: EventAnchor) => void;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Outlook's "Split" view: one column per checked calendar, all showing the same single day, so
|
|
35
|
-
* events across calendars can be compared side by side. A deliberately separate component from
|
|
36
|
-
* `TimeGridView` rather than a generalization of it — it shares that component's visual language
|
|
37
|
-
* (same hour rail, same slot height) but is genuinely simpler in one respect: no drag-to-move/resize.
|
|
38
|
-
* Dragging an event between calendar columns would mean reassigning its `folderUid`, a real feature
|
|
39
|
-
* `moveOccurrence`/`resolveDragAction` don't support today; rather than bolt that on, this view is
|
|
40
|
-
* click-only (select an event, or click a slot to create one already targeted at that column's
|
|
41
|
-
* calendar) — a practical scope cut, not an oversight.
|
|
42
|
-
*/
|
|
43
|
-
export default function SplitDayView({ day, columns, occurrences, onSelectEvent, onSelectSlot }: SplitDayViewProps) {
|
|
44
|
-
const dayOccurrences = occurrences.filter((occ) => occursOnDay(occ, day));
|
|
45
|
-
const dayStart = startOfDay(day);
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<div role="region" aria-label="Split view" data-calendar-scroller className="flex-1 flex flex-col min-h-0 overflow-y-auto">
|
|
49
|
-
<div className="flex border-b border-border shrink-0">
|
|
50
|
-
<div className="w-14 shrink-0" />
|
|
51
|
-
{columns.map((col) => (
|
|
52
|
-
<div key={col.folderUid} className="flex-1 min-w-0 py-1 flex items-center justify-center gap-1.5 border-l border-border">
|
|
53
|
-
<span aria-hidden="true" className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: col.color }} />
|
|
54
|
-
<span className="text-xs font-semibold truncate">{col.name}</span>
|
|
55
|
-
</div>
|
|
56
|
-
))}
|
|
57
|
-
</div>
|
|
58
|
-
<div className="flex flex-1">
|
|
59
|
-
<div className="w-14 shrink-0">
|
|
60
|
-
{HOURS.map((hour) => (
|
|
61
|
-
<div key={hour} style={{ height: HOUR_HEIGHT_PX }} className="text-[10px] text-text-muted text-right pr-1.5 -mt-1.5">
|
|
62
|
-
{hour === 0 ? "" : format(new Date(2000, 0, 1, hour), "ha")}
|
|
63
|
-
</div>
|
|
64
|
-
))}
|
|
65
|
-
</div>
|
|
66
|
-
{columns.map((col) => (
|
|
67
|
-
<SplitColumn
|
|
68
|
-
key={col.folderUid}
|
|
69
|
-
column={col}
|
|
70
|
-
dayStart={dayStart}
|
|
71
|
-
occurrences={dayOccurrences.filter((occ) => occ.folderUid === col.folderUid)}
|
|
72
|
-
onSelectEvent={onSelectEvent}
|
|
73
|
-
onSelectSlot={onSelectSlot}
|
|
74
|
-
/>
|
|
75
|
-
))}
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function SplitColumn({
|
|
82
|
-
column,
|
|
83
|
-
dayStart,
|
|
84
|
-
occurrences,
|
|
85
|
-
onSelectEvent,
|
|
86
|
-
onSelectSlot,
|
|
87
|
-
}: {
|
|
88
|
-
column: SplitDayColumn;
|
|
89
|
-
dayStart: Date;
|
|
90
|
-
occurrences: CalendarOccurrence[];
|
|
91
|
-
onSelectEvent: (occurrence: CalendarOccurrence) => void;
|
|
92
|
-
onSelectSlot: (start: Date, end: Date, folderUid: string, anchor: EventAnchor) => void;
|
|
93
|
-
}) {
|
|
94
|
-
return (
|
|
95
|
-
<div className="flex-1 min-w-0 relative border-l border-border">
|
|
96
|
-
{Array.from({ length: SLOTS_PER_DAY }, (_, i) => {
|
|
97
|
-
const slotStart = addMinutes(dayStart, i * SLOT_MINUTES);
|
|
98
|
-
return (
|
|
99
|
-
<button
|
|
100
|
-
key={i}
|
|
101
|
-
type="button"
|
|
102
|
-
onClick={(e) => onSelectSlot(slotStart, addMinutes(slotStart, SLOT_MINUTES), column.folderUid, anchorOf(e.currentTarget))}
|
|
103
|
-
style={{ height: HOUR_HEIGHT_PX / 2 }}
|
|
104
|
-
className="block w-full border-b border-border/50 text-left"
|
|
105
|
-
aria-label={`New event at ${format(slotStart, "h:mm a")} in ${column.name}`}
|
|
106
|
-
/>
|
|
107
|
-
);
|
|
108
|
-
})}
|
|
109
|
-
{occurrences.map((occurrence) => {
|
|
110
|
-
const start = new Date(occurrence.startDate);
|
|
111
|
-
const end = new Date(occurrence.endDate);
|
|
112
|
-
// Clipped to this day, so a multi-day event fills only the part of the day it covers. An
|
|
113
|
-
// all-day event is date-only (see `allDay.ts`), so it always spans the whole local day.
|
|
114
|
-
const dayEnd = addDays(dayStart, 1).getTime();
|
|
115
|
-
const visibleStart = occurrence.allDay ? dayStart.getTime() : Math.max(start.getTime(), dayStart.getTime());
|
|
116
|
-
const visibleEnd = occurrence.allDay ? dayEnd : Math.min(end.getTime(), dayEnd);
|
|
117
|
-
const top = ((visibleStart - dayStart.getTime()) / 60_000 / 60) * HOUR_HEIGHT_PX;
|
|
118
|
-
const height = Math.max(((visibleEnd - visibleStart) / 60_000 / 60) * HOUR_HEIGHT_PX, 16);
|
|
119
|
-
const isFree = occurrence.busyStatus === "free";
|
|
120
|
-
return (
|
|
121
|
-
<div
|
|
122
|
-
key={occurrence.occurrenceKey}
|
|
123
|
-
onClick={() => onSelectEvent(occurrence)}
|
|
124
|
-
style={{
|
|
125
|
-
position: "absolute",
|
|
126
|
-
top,
|
|
127
|
-
height,
|
|
128
|
-
left: 2,
|
|
129
|
-
right: 2,
|
|
130
|
-
...(isFree ? undefined : { backgroundColor: column.color, color: "#fff" }),
|
|
131
|
-
}}
|
|
132
|
-
className={[
|
|
133
|
-
"rounded-sm px-1.5 py-0.5 text-xs text-left overflow-hidden cursor-pointer",
|
|
134
|
-
isFree ? "bg-surface-alt text-text-muted" : "",
|
|
135
|
-
].join(" ")}
|
|
136
|
-
>
|
|
137
|
-
<div className="font-medium truncate">{occurrence.title}</div>
|
|
138
|
-
<div className="truncate">{format(start, "h:mma")}</div>
|
|
139
|
-
</div>
|
|
140
|
-
);
|
|
141
|
-
})}
|
|
142
|
-
</div>
|
|
143
|
-
);
|
|
144
|
-
}
|
|
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
|
+
import { EventAnchor, anchorOf } from "./EventShell.js";
|
|
10
|
+
|
|
11
|
+
const HOUR_HEIGHT_PX = 48;
|
|
12
|
+
const SLOT_MINUTES = 30;
|
|
13
|
+
const SLOTS_PER_DAY = (24 * 60) / SLOT_MINUTES;
|
|
14
|
+
const HOURS = Array.from({ length: 24 }, (_, h) => h);
|
|
15
|
+
|
|
16
|
+
export interface SplitDayColumn {
|
|
17
|
+
folderUid: string;
|
|
18
|
+
name: string;
|
|
19
|
+
color: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SplitDayViewProps {
|
|
23
|
+
day: Date;
|
|
24
|
+
columns: SplitDayColumn[];
|
|
25
|
+
/** Already expanded for `day` — see `recurrence.ts`'s `expandAllOccurrences`. Not pre-filtered by
|
|
26
|
+
* calendar; this component splits them into columns itself by `occurrence.folderUid`. */
|
|
27
|
+
occurrences: CalendarOccurrence[];
|
|
28
|
+
onSelectEvent: (occurrence: CalendarOccurrence) => void;
|
|
29
|
+
/** Click on an empty slot in one calendar's column — the calendar it belongs to is `folderUid`; `anchor` is the slot. */
|
|
30
|
+
onSelectSlot: (start: Date, end: Date, folderUid: string, anchor: EventAnchor) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Outlook's "Split" view: one column per checked calendar, all showing the same single day, so
|
|
35
|
+
* events across calendars can be compared side by side. A deliberately separate component from
|
|
36
|
+
* `TimeGridView` rather than a generalization of it — it shares that component's visual language
|
|
37
|
+
* (same hour rail, same slot height) but is genuinely simpler in one respect: no drag-to-move/resize.
|
|
38
|
+
* Dragging an event between calendar columns would mean reassigning its `folderUid`, a real feature
|
|
39
|
+
* `moveOccurrence`/`resolveDragAction` don't support today; rather than bolt that on, this view is
|
|
40
|
+
* click-only (select an event, or click a slot to create one already targeted at that column's
|
|
41
|
+
* calendar) — a practical scope cut, not an oversight.
|
|
42
|
+
*/
|
|
43
|
+
export default function SplitDayView({ day, columns, occurrences, onSelectEvent, onSelectSlot }: SplitDayViewProps) {
|
|
44
|
+
const dayOccurrences = occurrences.filter((occ) => occursOnDay(occ, day));
|
|
45
|
+
const dayStart = startOfDay(day);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div role="region" aria-label="Split view" data-calendar-scroller className="flex-1 flex flex-col min-h-0 overflow-y-auto">
|
|
49
|
+
<div className="flex border-b border-border shrink-0">
|
|
50
|
+
<div className="w-14 shrink-0" />
|
|
51
|
+
{columns.map((col) => (
|
|
52
|
+
<div key={col.folderUid} className="flex-1 min-w-0 py-1 flex items-center justify-center gap-1.5 border-l border-border">
|
|
53
|
+
<span aria-hidden="true" className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: col.color }} />
|
|
54
|
+
<span className="text-xs font-semibold truncate">{col.name}</span>
|
|
55
|
+
</div>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
<div className="flex flex-1">
|
|
59
|
+
<div className="w-14 shrink-0">
|
|
60
|
+
{HOURS.map((hour) => (
|
|
61
|
+
<div key={hour} style={{ height: HOUR_HEIGHT_PX }} className="text-[10px] text-text-muted text-right pr-1.5 -mt-1.5">
|
|
62
|
+
{hour === 0 ? "" : format(new Date(2000, 0, 1, hour), "ha")}
|
|
63
|
+
</div>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
{columns.map((col) => (
|
|
67
|
+
<SplitColumn
|
|
68
|
+
key={col.folderUid}
|
|
69
|
+
column={col}
|
|
70
|
+
dayStart={dayStart}
|
|
71
|
+
occurrences={dayOccurrences.filter((occ) => occ.folderUid === col.folderUid)}
|
|
72
|
+
onSelectEvent={onSelectEvent}
|
|
73
|
+
onSelectSlot={onSelectSlot}
|
|
74
|
+
/>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function SplitColumn({
|
|
82
|
+
column,
|
|
83
|
+
dayStart,
|
|
84
|
+
occurrences,
|
|
85
|
+
onSelectEvent,
|
|
86
|
+
onSelectSlot,
|
|
87
|
+
}: {
|
|
88
|
+
column: SplitDayColumn;
|
|
89
|
+
dayStart: Date;
|
|
90
|
+
occurrences: CalendarOccurrence[];
|
|
91
|
+
onSelectEvent: (occurrence: CalendarOccurrence) => void;
|
|
92
|
+
onSelectSlot: (start: Date, end: Date, folderUid: string, anchor: EventAnchor) => void;
|
|
93
|
+
}) {
|
|
94
|
+
return (
|
|
95
|
+
<div className="flex-1 min-w-0 relative border-l border-border">
|
|
96
|
+
{Array.from({ length: SLOTS_PER_DAY }, (_, i) => {
|
|
97
|
+
const slotStart = addMinutes(dayStart, i * SLOT_MINUTES);
|
|
98
|
+
return (
|
|
99
|
+
<button
|
|
100
|
+
key={i}
|
|
101
|
+
type="button"
|
|
102
|
+
onClick={(e) => onSelectSlot(slotStart, addMinutes(slotStart, SLOT_MINUTES), column.folderUid, anchorOf(e.currentTarget))}
|
|
103
|
+
style={{ height: HOUR_HEIGHT_PX / 2 }}
|
|
104
|
+
className="block w-full border-b border-border/50 text-left"
|
|
105
|
+
aria-label={`New event at ${format(slotStart, "h:mm a")} in ${column.name}`}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
{occurrences.map((occurrence) => {
|
|
110
|
+
const start = new Date(occurrence.startDate);
|
|
111
|
+
const end = new Date(occurrence.endDate);
|
|
112
|
+
// Clipped to this day, so a multi-day event fills only the part of the day it covers. An
|
|
113
|
+
// all-day event is date-only (see `allDay.ts`), so it always spans the whole local day.
|
|
114
|
+
const dayEnd = addDays(dayStart, 1).getTime();
|
|
115
|
+
const visibleStart = occurrence.allDay ? dayStart.getTime() : Math.max(start.getTime(), dayStart.getTime());
|
|
116
|
+
const visibleEnd = occurrence.allDay ? dayEnd : Math.min(end.getTime(), dayEnd);
|
|
117
|
+
const top = ((visibleStart - dayStart.getTime()) / 60_000 / 60) * HOUR_HEIGHT_PX;
|
|
118
|
+
const height = Math.max(((visibleEnd - visibleStart) / 60_000 / 60) * HOUR_HEIGHT_PX, 16);
|
|
119
|
+
const isFree = occurrence.busyStatus === "free";
|
|
120
|
+
return (
|
|
121
|
+
<div
|
|
122
|
+
key={occurrence.occurrenceKey}
|
|
123
|
+
onClick={() => onSelectEvent(occurrence)}
|
|
124
|
+
style={{
|
|
125
|
+
position: "absolute",
|
|
126
|
+
top,
|
|
127
|
+
height,
|
|
128
|
+
left: 2,
|
|
129
|
+
right: 2,
|
|
130
|
+
...(isFree ? undefined : { backgroundColor: column.color, color: "#fff" }),
|
|
131
|
+
}}
|
|
132
|
+
className={[
|
|
133
|
+
"rounded-sm px-1.5 py-0.5 text-xs text-left overflow-hidden cursor-pointer",
|
|
134
|
+
isFree ? "bg-surface-alt text-text-muted" : "",
|
|
135
|
+
].join(" ")}
|
|
136
|
+
>
|
|
137
|
+
<div className="font-medium truncate">{occurrence.title}</div>
|
|
138
|
+
<div className="truncate">{format(start, "h:mma")}</div>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
})}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|