@rapidmx/web-client 0.15.1 → 0.17.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 -373
- package/apps/admin/branding/index.tsx +39 -39
- package/apps/admin/data-requests/index.tsx +482 -480
- package/apps/admin/distribution-lists/new/index.tsx +3 -1
- package/apps/admin/domains/[uid].tsx +166 -164
- package/apps/admin/domains/new/index.tsx +3 -1
- package/apps/admin/escrow-scopes/[uid].tsx +350 -348
- package/apps/admin/escrow-scopes/new/index.tsx +3 -1
- package/apps/admin/index.tsx +129 -124
- package/apps/admin/mailboxes/[uid].tsx +271 -224
- package/apps/admin/mailboxes/new/index.tsx +30 -28
- 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/admin/transport-rules/new/index.tsx +3 -1
- package/apps/escrow/audit-log/index.tsx +196 -196
- package/apps/escrow/matters/[uid].tsx +621 -621
- package/apps/escrow/matters/new/index.tsx +3 -1
- package/apps/shared/auth/adminAccess.ts +99 -99
- package/apps/shared/components/admin/layout/AdminShell.tsx +381 -378
- package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -0
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
- package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -0
- 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/MailboxCreateForm.tsx +50 -3
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
- package/apps/shared/components/admin/settings/PluginsManager.tsx +1545 -1545
- 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 -444
- package/apps/shared/components/admin/usePagedList.tsx +129 -129
- package/apps/shared/components/calendar/EventEditor.tsx +5 -10
- package/apps/shared/components/calendar/EventFormParts.tsx +16 -47
- package/apps/shared/components/calendar/EventModal.tsx +206 -206
- package/apps/shared/components/calendar/GuestInput.tsx +100 -0
- package/apps/shared/components/calendar/MonthView.tsx +185 -185
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
- package/apps/shared/components/calendar/RequestChangeForm.tsx +23 -54
- 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/calendar/eventForm.ts +2 -2
- package/apps/shared/components/calendar/eventFormat.ts +49 -22
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
- package/apps/shared/components/contacts/ContactForm.tsx +383 -383
- package/apps/shared/components/contacts/ContactsSidebar.tsx +1 -1
- package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -98
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +218 -216
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
- package/apps/shared/components/layout/AppShell.tsx +461 -480
- package/apps/shared/components/layout/FloatingActionButton.tsx +31 -0
- 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 -424
- package/apps/shared/components/mail/ConversationList.tsx +323 -323
- package/apps/shared/components/mail/ConversationThreadPane.tsx +613 -491
- package/apps/shared/components/mail/EmptyFolderBar.tsx +41 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +240 -227
- package/apps/shared/components/mail/MenuButton.tsx +31 -11
- package/apps/shared/components/mail/MessageDetailPane.tsx +1757 -1559
- package/apps/shared/components/mail/PermanentDeleteDialog.tsx +47 -0
- 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 +1681 -1677
- package/apps/shared/components/mail/compose/RecipientInput.tsx +54 -11
- 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/layout/FolderBadgeChip.tsx +21 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +161 -134
- package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
- package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -0
- package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -0
- package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -0
- package/apps/shared/components/mail/reading/messageExport.ts +59 -0
- package/apps/shared/components/mail/reading/printMessage.ts +99 -0
- package/apps/shared/components/mail/reading/useMessageActions.ts +443 -0
- 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/components/settings/layout/SettingsShell.tsx +257 -254
- package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
- package/apps/shared/components/tasks/layout/TasksShell.tsx +219 -217
- 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/folderCounts.ts +17 -0
- package/apps/shared/mail/folderOfType.ts +49 -0
- 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/outgoingReplies.ts +192 -0
- package/apps/shared/mail/outbox/sendJob.ts +19 -1
- package/apps/shared/mail/outbox/sendOutcomes.ts +13 -4
- package/apps/shared/mail/permanentDelete.ts +160 -0
- package/apps/shared/mail/primaryMailbox.ts +71 -0
- package/apps/shared/mail/reportNotices.ts +66 -0
- package/apps/shared/mail/senderBlocking.ts +141 -0
- package/apps/shared/mail/useCollapsedSections.ts +146 -0
- package/apps/shared/mail/useMailConnection.ts +205 -203
- package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
- package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -0
- package/apps/shared/mail/useMarkMessageRead.ts +47 -47
- package/apps/shared/mail/useNewMailNotifications.ts +178 -178
- package/apps/shared/mail/usePermanentDelete.tsx +160 -0
- package/apps/shared/navigation/frameContext.tsx +1 -1
- package/apps/shared/navigation/index.ts +43 -0
- package/apps/shared/navigation/pageTitle.ts +19 -0
- package/apps/shared/notifications/store.ts +560 -560
- package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
- package/apps/shared/search/crossMailboxSearch.ts +169 -0
- 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 -386
- package/apps/www/_shell.tsx +82 -0
- package/apps/www/calendar/index.tsx +581 -571
- package/apps/www/contacts/[uid].tsx +112 -109
- package/apps/www/contacts/index.tsx +14 -3
- package/apps/www/index.tsx +2962 -2479
- package/apps/www/messages/[uid].tsx +139 -135
- package/apps/www/settings/appearance/index.tsx +5 -2
- package/apps/www/settings/auto-reply/index.tsx +136 -133
- package/apps/www/settings/blocked-senders/index.tsx +303 -0
- package/apps/www/settings/encryption/index.tsx +1290 -1287
- package/apps/www/settings/filters/[uid].tsx +179 -176
- package/apps/www/settings/filters/index.tsx +105 -102
- package/apps/www/settings/filters/new/index.tsx +165 -140
- package/apps/www/settings/labels/index.tsx +207 -204
- package/apps/www/settings/privacy/index.tsx +495 -492
- package/apps/www/settings/profile/index.tsx +251 -248
- package/apps/www/settings/read-receipts/index.tsx +150 -147
- package/apps/www/settings/sharing/index.tsx +259 -256
- package/apps/www/settings/signatures/[uid].tsx +175 -172
- package/apps/www/settings/signatures/index.tsx +91 -88
- package/apps/www/settings/signatures/new/index.tsx +138 -135
- package/apps/www/tasks/index.tsx +654 -649
- package/dist/apps/admin/data-requests/index.js +1 -1
- package/dist/apps/admin/distribution-lists/new/index.js +3 -1
- package/dist/apps/admin/domains/[uid].js +3 -1
- package/dist/apps/admin/domains/new/index.js +3 -1
- package/dist/apps/admin/escrow-scopes/[uid].js +3 -1
- package/dist/apps/admin/escrow-scopes/new/index.js +3 -1
- package/dist/apps/admin/index.js +5 -2
- package/dist/apps/admin/mailboxes/[uid].js +15 -3
- package/dist/apps/admin/mailboxes/new/index.js +3 -1
- package/dist/apps/admin/transport-rules/new/index.js +3 -1
- package/dist/apps/escrow/matters/new/index.js +3 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +6 -3
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +35 -0
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +115 -0
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.d.ts +16 -0
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +92 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +25 -3
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +3 -1
- package/dist/apps/shared/components/calendar/EventEditor.js +5 -10
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -2
- package/dist/apps/shared/components/calendar/EventFormParts.js +5 -16
- package/dist/apps/shared/components/calendar/GuestInput.d.ts +45 -0
- package/dist/apps/shared/components/calendar/GuestInput.js +30 -0
- package/dist/apps/shared/components/calendar/RequestChangeForm.js +11 -22
- package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -2
- package/dist/apps/shared/components/calendar/eventFormat.d.ts +21 -8
- package/dist/apps/shared/components/calendar/eventFormat.js +50 -23
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +5 -4
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +1 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -2
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +7 -5
- package/dist/apps/shared/components/layout/AppShell.d.ts +8 -11
- package/dist/apps/shared/components/layout/AppShell.js +24 -33
- package/dist/apps/shared/components/layout/FloatingActionButton.d.ts +13 -0
- package/dist/apps/shared/components/layout/FloatingActionButton.js +9 -0
- package/dist/apps/shared/components/layout/UserMenu.d.ts +4 -1
- package/dist/apps/shared/components/layout/UserMenu.js +6 -5
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
- package/dist/apps/shared/components/mail/EmptyFolderBar.d.ts +18 -0
- package/dist/apps/shared/components/mail/EmptyFolderBar.js +10 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +14 -4
- package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -13
- package/dist/apps/shared/components/mail/MenuButton.d.ts +9 -2
- package/dist/apps/shared/components/mail/MenuButton.js +15 -11
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +5 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +147 -7
- package/dist/apps/shared/components/mail/PermanentDeleteDialog.d.ts +19 -0
- package/dist/apps/shared/components/mail/PermanentDeleteDialog.js +11 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +8 -4
- package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +21 -1
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +18 -10
- package/dist/apps/shared/components/mail/layout/FolderBadgeChip.d.ts +6 -0
- package/dist/apps/shared/components/mail/layout/FolderBadgeChip.js +9 -0
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +16 -7
- package/dist/apps/shared/components/mail/layout/MailShell.js +78 -65
- package/dist/apps/shared/components/mail/layout/SidebarSection.d.ts +23 -0
- package/dist/apps/shared/components/mail/layout/SidebarSection.js +16 -0
- package/dist/apps/shared/components/mail/reading/MessageMoreMenu.d.ts +56 -0
- package/dist/apps/shared/components/mail/reading/MessageMoreMenu.js +153 -0
- package/dist/apps/shared/components/mail/reading/MessageSourceDialog.d.ts +27 -0
- package/dist/apps/shared/components/mail/reading/MessageSourceDialog.js +27 -0
- package/dist/apps/shared/components/mail/reading/PendingMessageCard.d.ts +14 -0
- package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
- package/dist/apps/shared/components/mail/reading/messageExport.d.ts +19 -0
- package/dist/apps/shared/components/mail/reading/messageExport.js +51 -0
- package/dist/apps/shared/components/mail/reading/printMessage.d.ts +30 -0
- package/dist/apps/shared/components/mail/reading/printMessage.js +70 -0
- package/dist/apps/shared/components/mail/reading/useMessageActions.d.ts +57 -0
- package/dist/apps/shared/components/mail/reading/useMessageActions.js +366 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +10 -7
- package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +7 -5
- package/dist/apps/shared/keyboard/GlobalShortcuts.js +1 -1
- package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
- package/dist/apps/shared/mail/folderCounts.js +12 -0
- package/dist/apps/shared/mail/folderOfType.d.ts +13 -0
- package/dist/apps/shared/mail/folderOfType.js +40 -0
- package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +80 -0
- package/dist/apps/shared/mail/outbox/outgoingReplies.js +131 -0
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +5 -1
- package/dist/apps/shared/mail/outbox/sendJob.js +14 -1
- package/dist/apps/shared/mail/outbox/sendOutcomes.js +13 -4
- package/dist/apps/shared/mail/permanentDelete.d.ts +62 -0
- package/dist/apps/shared/mail/permanentDelete.js +127 -0
- package/dist/apps/shared/mail/primaryMailbox.d.ts +29 -0
- package/dist/apps/shared/mail/primaryMailbox.js +61 -0
- package/dist/apps/shared/mail/reportNotices.d.ts +21 -0
- package/dist/apps/shared/mail/reportNotices.js +47 -0
- package/dist/apps/shared/mail/senderBlocking.d.ts +26 -0
- package/dist/apps/shared/mail/senderBlocking.js +92 -0
- package/dist/apps/shared/mail/useCollapsedSections.d.ts +32 -0
- package/dist/apps/shared/mail/useCollapsedSections.js +110 -0
- package/dist/apps/shared/mail/useMailConnection.js +3 -1
- package/dist/apps/shared/mail/useMailboxUpdateAccess.d.ts +11 -0
- package/dist/apps/shared/mail/useMailboxUpdateAccess.js +52 -0
- package/dist/apps/shared/mail/usePermanentDelete.d.ts +35 -0
- package/dist/apps/shared/mail/usePermanentDelete.js +96 -0
- package/dist/apps/shared/navigation/frameContext.d.ts +1 -1
- package/dist/apps/shared/navigation/index.d.ts +16 -0
- package/dist/apps/shared/navigation/index.js +34 -0
- package/dist/apps/shared/navigation/pageTitle.d.ts +11 -0
- package/dist/apps/shared/navigation/pageTitle.js +12 -0
- package/dist/apps/shared/search/crossMailboxSearch.d.ts +72 -0
- package/dist/apps/shared/search/crossMailboxSearch.js +136 -0
- package/dist/apps/shared/styles/app.css +396 -386
- package/dist/apps/www/_shell.d.ts +26 -0
- package/dist/apps/www/_shell.js +31 -0
- package/dist/apps/www/calendar/index.d.ts +7 -2
- package/dist/apps/www/calendar/index.js +7 -4
- package/dist/apps/www/contacts/[uid].d.ts +14 -3
- package/dist/apps/www/contacts/[uid].js +5 -3
- package/dist/apps/www/contacts/index.d.ts +7 -2
- package/dist/apps/www/contacts/index.js +8 -4
- package/dist/apps/www/index.d.ts +7 -2
- package/dist/apps/www/index.js +542 -207
- package/dist/apps/www/messages/[uid].d.ts +14 -3
- package/dist/apps/www/messages/[uid].js +7 -5
- package/dist/apps/www/settings/appearance/index.d.ts +8 -2
- package/dist/apps/www/settings/appearance/index.js +4 -2
- package/dist/apps/www/settings/auto-reply/index.d.ts +7 -2
- package/dist/apps/www/settings/auto-reply/index.js +4 -2
- package/dist/apps/www/settings/blocked-senders/index.d.ts +9 -0
- package/dist/apps/www/settings/blocked-senders/index.js +134 -0
- package/dist/apps/www/settings/encryption/index.d.ts +7 -2
- package/dist/apps/www/settings/encryption/index.js +4 -2
- package/dist/apps/www/settings/filters/[uid].d.ts +7 -2
- package/dist/apps/www/settings/filters/[uid].js +4 -2
- package/dist/apps/www/settings/filters/index.d.ts +7 -2
- package/dist/apps/www/settings/filters/index.js +4 -2
- package/dist/apps/www/settings/filters/new/index.d.ts +7 -2
- package/dist/apps/www/settings/filters/new/index.js +28 -5
- package/dist/apps/www/settings/labels/index.d.ts +7 -2
- package/dist/apps/www/settings/labels/index.js +4 -2
- package/dist/apps/www/settings/privacy/index.d.ts +7 -2
- package/dist/apps/www/settings/privacy/index.js +4 -2
- package/dist/apps/www/settings/profile/index.d.ts +7 -2
- package/dist/apps/www/settings/profile/index.js +4 -2
- package/dist/apps/www/settings/read-receipts/index.d.ts +7 -2
- package/dist/apps/www/settings/read-receipts/index.js +4 -2
- package/dist/apps/www/settings/sharing/index.d.ts +7 -2
- package/dist/apps/www/settings/sharing/index.js +4 -2
- package/dist/apps/www/settings/signatures/[uid].d.ts +7 -2
- package/dist/apps/www/settings/signatures/[uid].js +4 -2
- package/dist/apps/www/settings/signatures/index.d.ts +7 -2
- package/dist/apps/www/settings/signatures/index.js +4 -2
- package/dist/apps/www/settings/signatures/new/index.d.ts +7 -2
- package/dist/apps/www/settings/signatures/new/index.js +5 -3
- package/dist/apps/www/tasks/index.d.ts +7 -2
- package/dist/apps/www/tasks/index.js +5 -3
- package/package.json +4 -2
- package/apps/shared/navigation/AppRouter.tsx +0 -300
- package/apps/shared/navigation/idle.ts +0 -45
- package/apps/shared/navigation/routerContext.tsx +0 -83
- package/apps/shared/navigation/routes.ts +0 -72
- package/apps/www/_routedPage.tsx +0 -24
- package/apps/www/_routes.ts +0 -37
- package/dist/apps/shared/navigation/AppRouter.d.ts +0 -52
- package/dist/apps/shared/navigation/AppRouter.js +0 -242
- package/dist/apps/shared/navigation/idle.d.ts +0 -14
- package/dist/apps/shared/navigation/idle.js +0 -43
- package/dist/apps/shared/navigation/routerContext.d.ts +0 -37
- package/dist/apps/shared/navigation/routerContext.js +0 -56
- package/dist/apps/shared/navigation/routes.d.ts +0 -32
- package/dist/apps/shared/navigation/routes.js +0 -37
- package/dist/apps/www/_routedPage.d.ts +0 -12
- package/dist/apps/www/_routedPage.js +0 -19
- package/dist/apps/www/_routes.d.ts +0 -11
- package/dist/apps/www/_routes.js +0 -31
|
@@ -1,246 +1,246 @@
|
|
|
1
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
-
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import React from "react";
|
|
6
|
-
import { useDraggable, useDroppable } from "@dnd-kit/core";
|
|
7
|
-
import { addDays, addMinutes, format, isToday, startOfDay } from "date-fns";
|
|
8
|
-
import { dayDropId, eventDragId, resizeDragId, slotDropId } from "@rapidmx/react-shared/calendar/calendarDragIds.js";
|
|
9
|
-
import { CalendarOccurrence } from "@rapidmx/react-shared/calendar/recurrence.js";
|
|
10
|
-
import { occursOnDay, startsOnDay } from "./allDay.js";
|
|
11
|
-
import { EventAnchor, anchorOf } from "./EventShell.js";
|
|
12
|
-
|
|
13
|
-
const HOUR_HEIGHT_PX = 48;
|
|
14
|
-
const SLOT_MINUTES = 30;
|
|
15
|
-
const SLOTS_PER_DAY = (24 * 60) / SLOT_MINUTES;
|
|
16
|
-
const HOURS = Array.from({ length: 24 }, (_, h) => h);
|
|
17
|
-
|
|
18
|
-
export interface TimeGridViewProps {
|
|
19
|
-
/** One day for the Day view, seven for the Week view, five for the Work Week view. */
|
|
20
|
-
days: Date[];
|
|
21
|
-
/** Already expanded for the visible range — see `recurrence.ts`'s `expandAllOccurrences`. */
|
|
22
|
-
occurrences: CalendarOccurrence[];
|
|
23
|
-
/** Each occurrence's own calendar color, keyed by `folderUid` — see `calendarColors.ts`. */
|
|
24
|
-
folderColors: Record<string, string>;
|
|
25
|
-
onSelectEvent: (occurrence: CalendarOccurrence) => void;
|
|
26
|
-
/** Click on an empty slot — start/end default to a 30-minute block there, for the "New event" flow. `anchor` is the slot, for the
|
|
27
|
-
* quick-create popover to open beside. */
|
|
28
|
-
onSelectSlot: (start: Date, end: Date, anchor: EventAnchor) => void;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** An hourly time grid (Week, Work Week, or Day view — they share all their rendering/drag logic and
|
|
32
|
-
* differ only in how many day columns are shown), with a day-header row and all-day events in a
|
|
33
|
-
* header strip above it. */
|
|
34
|
-
export default function TimeGridView({ days, occurrences, folderColors, onSelectEvent, onSelectSlot }: TimeGridViewProps) {
|
|
35
|
-
const allDayEvents = occurrences.filter((occ) => occ.allDay);
|
|
36
|
-
const timedEvents = occurrences.filter((occ) => !occ.allDay);
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<div data-calendar-scroller className="flex-1 flex flex-col min-h-0 overflow-y-auto">
|
|
40
|
-
<div className="flex border-b border-border shrink-0">
|
|
41
|
-
<div className="w-14 shrink-0" />
|
|
42
|
-
{days.map((day) => (
|
|
43
|
-
<div
|
|
44
|
-
key={day.toISOString()}
|
|
45
|
-
className={["flex-1 min-w-0 py-1 text-center text-xs font-semibold border-l border-border", isToday(day) ? "text-primary-dark" : ""].join(" ")}
|
|
46
|
-
>
|
|
47
|
-
{format(day, "EEE d")}
|
|
48
|
-
</div>
|
|
49
|
-
))}
|
|
50
|
-
</div>
|
|
51
|
-
{allDayEvents.length > 0 && (
|
|
52
|
-
<div className="flex border-b border-border shrink-0">
|
|
53
|
-
<div className="w-14 shrink-0" />
|
|
54
|
-
{days.map((day) => (
|
|
55
|
-
<div key={day.toISOString()} className="flex-1 min-w-0 p-1 flex flex-col gap-0.5 border-l border-border">
|
|
56
|
-
{allDayEvents
|
|
57
|
-
.filter((occ) => occursOnDay(occ, day))
|
|
58
|
-
.map((occ) => (
|
|
59
|
-
<button
|
|
60
|
-
key={occ.occurrenceKey}
|
|
61
|
-
type="button"
|
|
62
|
-
onClick={() => onSelectEvent(occ)}
|
|
63
|
-
style={{ backgroundColor: folderColors[occ.folderUid], color: "#fff" }}
|
|
64
|
-
className="text-xs text-left truncate rounded-sm px-1.5 py-0.5"
|
|
65
|
-
>
|
|
66
|
-
{occ.title}
|
|
67
|
-
</button>
|
|
68
|
-
))}
|
|
69
|
-
</div>
|
|
70
|
-
))}
|
|
71
|
-
</div>
|
|
72
|
-
)}
|
|
73
|
-
<div className="flex flex-1">
|
|
74
|
-
<div className="w-14 shrink-0">
|
|
75
|
-
{HOURS.map((hour) => (
|
|
76
|
-
<div key={hour} style={{ height: HOUR_HEIGHT_PX }} className="text-[10px] text-text-muted text-right pr-1.5 -mt-1.5">
|
|
77
|
-
{hour === 0 ? "" : format(new Date(2000, 0, 1, hour), "ha")}
|
|
78
|
-
</div>
|
|
79
|
-
))}
|
|
80
|
-
</div>
|
|
81
|
-
{days.map((day) => (
|
|
82
|
-
<DayColumn
|
|
83
|
-
key={day.toISOString()}
|
|
84
|
-
day={day}
|
|
85
|
-
occurrences={timedEvents.filter((occ) => occursOnDay(occ, day))}
|
|
86
|
-
folderColors={folderColors}
|
|
87
|
-
onSelectEvent={onSelectEvent}
|
|
88
|
-
onSelectSlot={onSelectSlot}
|
|
89
|
-
/>
|
|
90
|
-
))}
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
interface DayColumnProps {
|
|
97
|
-
day: Date;
|
|
98
|
-
occurrences: CalendarOccurrence[];
|
|
99
|
-
folderColors: Record<string, string>;
|
|
100
|
-
onSelectEvent: (occurrence: CalendarOccurrence) => void;
|
|
101
|
-
onSelectSlot: (start: Date, end: Date, anchor: EventAnchor) => void;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function DayColumn({ day, occurrences, folderColors, onSelectEvent, onSelectSlot }: DayColumnProps) {
|
|
105
|
-
const dayStart = startOfDay(day);
|
|
106
|
-
|
|
107
|
-
return (
|
|
108
|
-
<div className={["flex-1 min-w-0 relative border-l border-border", isToday(day) ? "bg-primary/5" : ""].join(" ")}>
|
|
109
|
-
{Array.from({ length: SLOTS_PER_DAY }, (_, i) => {
|
|
110
|
-
const slotStart = addMinutes(dayStart, i * SLOT_MINUTES);
|
|
111
|
-
return <TimeSlot key={i} start={slotStart} onSelectSlot={onSelectSlot} />;
|
|
112
|
-
})}
|
|
113
|
-
{occurrences.map((occurrence) =>
|
|
114
|
-
startsOnDay(occurrence, day) ? (
|
|
115
|
-
<EventBlock
|
|
116
|
-
key={occurrence.occurrenceKey}
|
|
117
|
-
occurrence={occurrence}
|
|
118
|
-
color={folderColors[occurrence.folderUid]}
|
|
119
|
-
dayStart={dayStart}
|
|
120
|
-
onSelect={onSelectEvent}
|
|
121
|
-
/>
|
|
122
|
-
) : (
|
|
123
|
-
<ContinuationBlock
|
|
124
|
-
key={occurrence.occurrenceKey}
|
|
125
|
-
occurrence={occurrence}
|
|
126
|
-
color={folderColors[occurrence.folderUid]}
|
|
127
|
-
dayStart={dayStart}
|
|
128
|
-
onSelect={onSelectEvent}
|
|
129
|
-
/>
|
|
130
|
-
),
|
|
131
|
-
)}
|
|
132
|
-
</div>
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function TimeSlot({ start, onSelectSlot }: { start: Date; onSelectSlot: (start: Date, end: Date, anchor: EventAnchor) => void }) {
|
|
137
|
-
const { setNodeRef, isOver } = useDroppable({ id: slotDropId(start) });
|
|
138
|
-
return (
|
|
139
|
-
<button
|
|
140
|
-
ref={setNodeRef}
|
|
141
|
-
type="button"
|
|
142
|
-
onClick={(e) => onSelectSlot(start, addMinutes(start, SLOT_MINUTES), anchorOf(e.currentTarget))}
|
|
143
|
-
style={{ height: HOUR_HEIGHT_PX / 2 }}
|
|
144
|
-
className={["block w-full border-b border-border/50 text-left", isOver ? "bg-primary/10" : ""].join(" ")}
|
|
145
|
-
aria-label={`New event at ${format(start, "h:mm a, MMM d")}`}
|
|
146
|
-
/>
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** A block's position within its day column. A multi-day event is drawn in every day column it
|
|
151
|
-
* overlaps, clipped to that day. */
|
|
152
|
-
function blockGeometry(occurrence: CalendarOccurrence, dayStart: Date): { top: number; height: number } {
|
|
153
|
-
const visibleStart = Math.max(new Date(occurrence.startDate).getTime(), dayStart.getTime());
|
|
154
|
-
const visibleEnd = Math.min(new Date(occurrence.endDate).getTime(), addDays(dayStart, 1).getTime());
|
|
155
|
-
return {
|
|
156
|
-
top: ((visibleStart - dayStart.getTime()) / 60_000 / 60) * HOUR_HEIGHT_PX,
|
|
157
|
-
height: Math.max(((visibleEnd - visibleStart) / 60_000 / 60) * HOUR_HEIGHT_PX, 16),
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** A multi-day event's block in a day column after its first: clickable, but not draggable/resizable
|
|
162
|
-
* (its drag ids are already taken by the start day's block). */
|
|
163
|
-
function ContinuationBlock({
|
|
164
|
-
occurrence,
|
|
165
|
-
color,
|
|
166
|
-
dayStart,
|
|
167
|
-
onSelect,
|
|
168
|
-
}: {
|
|
169
|
-
occurrence: CalendarOccurrence;
|
|
170
|
-
color: string;
|
|
171
|
-
dayStart: Date;
|
|
172
|
-
onSelect: (occurrence: CalendarOccurrence) => void;
|
|
173
|
-
}) {
|
|
174
|
-
const isFree = occurrence.busyStatus === "free";
|
|
175
|
-
const { top, height } = blockGeometry(occurrence, dayStart);
|
|
176
|
-
return (
|
|
177
|
-
<div
|
|
178
|
-
onClick={() => onSelect(occurrence)}
|
|
179
|
-
style={{ position: "absolute", top, height, left: 2, right: 2, ...(isFree ? undefined : { backgroundColor: color, color: "#fff" }) }}
|
|
180
|
-
className={["rounded-sm px-1.5 py-0.5 text-xs text-left overflow-hidden cursor-pointer", isFree ? "bg-surface-alt text-text-muted" : ""].join(" ")}
|
|
181
|
-
>
|
|
182
|
-
<div className="font-medium truncate">{occurrence.title}</div>
|
|
183
|
-
</div>
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function EventBlock({
|
|
188
|
-
occurrence,
|
|
189
|
-
color,
|
|
190
|
-
dayStart,
|
|
191
|
-
onSelect,
|
|
192
|
-
}: {
|
|
193
|
-
occurrence: CalendarOccurrence;
|
|
194
|
-
/** This occurrence's calendar color — see `calendarColors.ts`. Only applied for a "busy" block; a
|
|
195
|
-
* "free" one keeps Outlook's own muted/outline treatment regardless of which calendar it's on. */
|
|
196
|
-
color: string;
|
|
197
|
-
dayStart: Date;
|
|
198
|
-
onSelect: (occurrence: CalendarOccurrence) => void;
|
|
199
|
-
}) {
|
|
200
|
-
const { setNodeRef, listeners, attributes, transform, isDragging } = useDraggable({ id: eventDragId(occurrence) });
|
|
201
|
-
const { setNodeRef: setResizeRef, listeners: resizeListeners, attributes: resizeAttributes } = useDraggable({
|
|
202
|
-
id: resizeDragId(occurrence),
|
|
203
|
-
});
|
|
204
|
-
const isFree = occurrence.busyStatus === "free";
|
|
205
|
-
|
|
206
|
-
const start = new Date(occurrence.startDate);
|
|
207
|
-
const { top, height } = blockGeometry(occurrence, dayStart);
|
|
208
|
-
// The resize handle's own drag position has no live pixel preview (it only applies its ns-resize
|
|
209
|
-
// affordance) — the actual new end time is computed from whichever slot it's dropped onto (see
|
|
210
|
-
// `resolveDragAction`'s "resize" case), snapped to the grid rather than following the pointer
|
|
211
|
-
// continuously. Simpler, and avoids a visual preview that could disagree with the snapped result.
|
|
212
|
-
|
|
213
|
-
return (
|
|
214
|
-
<div
|
|
215
|
-
ref={setNodeRef}
|
|
216
|
-
onClick={() => onSelect(occurrence)}
|
|
217
|
-
style={{
|
|
218
|
-
position: "absolute",
|
|
219
|
-
top,
|
|
220
|
-
height,
|
|
221
|
-
left: 2,
|
|
222
|
-
right: 2,
|
|
223
|
-
transform: transform ? `translate(${transform.x}px, ${transform.y}px)` : undefined,
|
|
224
|
-
zIndex: isDragging ? 10 : undefined,
|
|
225
|
-
...(isFree ? undefined : { backgroundColor: color, color: "#fff" }),
|
|
226
|
-
}}
|
|
227
|
-
className={[
|
|
228
|
-
"rounded-sm px-1.5 py-0.5 text-xs text-left overflow-hidden cursor-pointer",
|
|
229
|
-
isFree ? "bg-surface-alt text-text-muted" : "",
|
|
230
|
-
isDragging ? "opacity-50" : "",
|
|
231
|
-
].join(" ")}
|
|
232
|
-
{...listeners}
|
|
233
|
-
{...attributes}
|
|
234
|
-
>
|
|
235
|
-
<div className="font-medium truncate">{occurrence.title}</div>
|
|
236
|
-
<div className="truncate">{format(start, "h:mma")}</div>
|
|
237
|
-
<div
|
|
238
|
-
ref={setResizeRef}
|
|
239
|
-
className="absolute bottom-0 left-0 right-0 h-1.5 cursor-ns-resize"
|
|
240
|
-
aria-label={`Resize "${occurrence.title}"`}
|
|
241
|
-
{...resizeListeners}
|
|
242
|
-
{...resizeAttributes}
|
|
243
|
-
/>
|
|
244
|
-
</div>
|
|
245
|
-
);
|
|
246
|
-
}
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { useDraggable, useDroppable } from "@dnd-kit/core";
|
|
7
|
+
import { addDays, addMinutes, format, isToday, startOfDay } from "date-fns";
|
|
8
|
+
import { dayDropId, eventDragId, resizeDragId, slotDropId } from "@rapidmx/react-shared/calendar/calendarDragIds.js";
|
|
9
|
+
import { CalendarOccurrence } from "@rapidmx/react-shared/calendar/recurrence.js";
|
|
10
|
+
import { occursOnDay, startsOnDay } from "./allDay.js";
|
|
11
|
+
import { EventAnchor, anchorOf } from "./EventShell.js";
|
|
12
|
+
|
|
13
|
+
const HOUR_HEIGHT_PX = 48;
|
|
14
|
+
const SLOT_MINUTES = 30;
|
|
15
|
+
const SLOTS_PER_DAY = (24 * 60) / SLOT_MINUTES;
|
|
16
|
+
const HOURS = Array.from({ length: 24 }, (_, h) => h);
|
|
17
|
+
|
|
18
|
+
export interface TimeGridViewProps {
|
|
19
|
+
/** One day for the Day view, seven for the Week view, five for the Work Week view. */
|
|
20
|
+
days: Date[];
|
|
21
|
+
/** Already expanded for the visible range — see `recurrence.ts`'s `expandAllOccurrences`. */
|
|
22
|
+
occurrences: CalendarOccurrence[];
|
|
23
|
+
/** Each occurrence's own calendar color, keyed by `folderUid` — see `calendarColors.ts`. */
|
|
24
|
+
folderColors: Record<string, string>;
|
|
25
|
+
onSelectEvent: (occurrence: CalendarOccurrence) => void;
|
|
26
|
+
/** Click on an empty slot — start/end default to a 30-minute block there, for the "New event" flow. `anchor` is the slot, for the
|
|
27
|
+
* quick-create popover to open beside. */
|
|
28
|
+
onSelectSlot: (start: Date, end: Date, anchor: EventAnchor) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** An hourly time grid (Week, Work Week, or Day view — they share all their rendering/drag logic and
|
|
32
|
+
* differ only in how many day columns are shown), with a day-header row and all-day events in a
|
|
33
|
+
* header strip above it. */
|
|
34
|
+
export default function TimeGridView({ days, occurrences, folderColors, onSelectEvent, onSelectSlot }: TimeGridViewProps) {
|
|
35
|
+
const allDayEvents = occurrences.filter((occ) => occ.allDay);
|
|
36
|
+
const timedEvents = occurrences.filter((occ) => !occ.allDay);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div data-calendar-scroller className="flex-1 flex flex-col min-h-0 overflow-y-auto">
|
|
40
|
+
<div className="flex border-b border-border shrink-0">
|
|
41
|
+
<div className="w-14 shrink-0" />
|
|
42
|
+
{days.map((day) => (
|
|
43
|
+
<div
|
|
44
|
+
key={day.toISOString()}
|
|
45
|
+
className={["flex-1 min-w-0 py-1 text-center text-xs font-semibold border-l border-border", isToday(day) ? "text-primary-dark" : ""].join(" ")}
|
|
46
|
+
>
|
|
47
|
+
{format(day, "EEE d")}
|
|
48
|
+
</div>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
{allDayEvents.length > 0 && (
|
|
52
|
+
<div className="flex border-b border-border shrink-0">
|
|
53
|
+
<div className="w-14 shrink-0" />
|
|
54
|
+
{days.map((day) => (
|
|
55
|
+
<div key={day.toISOString()} className="flex-1 min-w-0 p-1 flex flex-col gap-0.5 border-l border-border">
|
|
56
|
+
{allDayEvents
|
|
57
|
+
.filter((occ) => occursOnDay(occ, day))
|
|
58
|
+
.map((occ) => (
|
|
59
|
+
<button
|
|
60
|
+
key={occ.occurrenceKey}
|
|
61
|
+
type="button"
|
|
62
|
+
onClick={() => onSelectEvent(occ)}
|
|
63
|
+
style={{ backgroundColor: folderColors[occ.folderUid], color: "#fff" }}
|
|
64
|
+
className="text-xs text-left truncate rounded-sm px-1.5 py-0.5"
|
|
65
|
+
>
|
|
66
|
+
{occ.title}
|
|
67
|
+
</button>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
<div className="flex flex-1">
|
|
74
|
+
<div className="w-14 shrink-0">
|
|
75
|
+
{HOURS.map((hour) => (
|
|
76
|
+
<div key={hour} style={{ height: HOUR_HEIGHT_PX }} className="text-[10px] text-text-muted text-right pr-1.5 -mt-1.5">
|
|
77
|
+
{hour === 0 ? "" : format(new Date(2000, 0, 1, hour), "ha")}
|
|
78
|
+
</div>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
{days.map((day) => (
|
|
82
|
+
<DayColumn
|
|
83
|
+
key={day.toISOString()}
|
|
84
|
+
day={day}
|
|
85
|
+
occurrences={timedEvents.filter((occ) => occursOnDay(occ, day))}
|
|
86
|
+
folderColors={folderColors}
|
|
87
|
+
onSelectEvent={onSelectEvent}
|
|
88
|
+
onSelectSlot={onSelectSlot}
|
|
89
|
+
/>
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface DayColumnProps {
|
|
97
|
+
day: Date;
|
|
98
|
+
occurrences: CalendarOccurrence[];
|
|
99
|
+
folderColors: Record<string, string>;
|
|
100
|
+
onSelectEvent: (occurrence: CalendarOccurrence) => void;
|
|
101
|
+
onSelectSlot: (start: Date, end: Date, anchor: EventAnchor) => void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function DayColumn({ day, occurrences, folderColors, onSelectEvent, onSelectSlot }: DayColumnProps) {
|
|
105
|
+
const dayStart = startOfDay(day);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className={["flex-1 min-w-0 relative border-l border-border", isToday(day) ? "bg-primary/5" : ""].join(" ")}>
|
|
109
|
+
{Array.from({ length: SLOTS_PER_DAY }, (_, i) => {
|
|
110
|
+
const slotStart = addMinutes(dayStart, i * SLOT_MINUTES);
|
|
111
|
+
return <TimeSlot key={i} start={slotStart} onSelectSlot={onSelectSlot} />;
|
|
112
|
+
})}
|
|
113
|
+
{occurrences.map((occurrence) =>
|
|
114
|
+
startsOnDay(occurrence, day) ? (
|
|
115
|
+
<EventBlock
|
|
116
|
+
key={occurrence.occurrenceKey}
|
|
117
|
+
occurrence={occurrence}
|
|
118
|
+
color={folderColors[occurrence.folderUid]}
|
|
119
|
+
dayStart={dayStart}
|
|
120
|
+
onSelect={onSelectEvent}
|
|
121
|
+
/>
|
|
122
|
+
) : (
|
|
123
|
+
<ContinuationBlock
|
|
124
|
+
key={occurrence.occurrenceKey}
|
|
125
|
+
occurrence={occurrence}
|
|
126
|
+
color={folderColors[occurrence.folderUid]}
|
|
127
|
+
dayStart={dayStart}
|
|
128
|
+
onSelect={onSelectEvent}
|
|
129
|
+
/>
|
|
130
|
+
),
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function TimeSlot({ start, onSelectSlot }: { start: Date; onSelectSlot: (start: Date, end: Date, anchor: EventAnchor) => void }) {
|
|
137
|
+
const { setNodeRef, isOver } = useDroppable({ id: slotDropId(start) });
|
|
138
|
+
return (
|
|
139
|
+
<button
|
|
140
|
+
ref={setNodeRef}
|
|
141
|
+
type="button"
|
|
142
|
+
onClick={(e) => onSelectSlot(start, addMinutes(start, SLOT_MINUTES), anchorOf(e.currentTarget))}
|
|
143
|
+
style={{ height: HOUR_HEIGHT_PX / 2 }}
|
|
144
|
+
className={["block w-full border-b border-border/50 text-left", isOver ? "bg-primary/10" : ""].join(" ")}
|
|
145
|
+
aria-label={`New event at ${format(start, "h:mm a, MMM d")}`}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A block's position within its day column. A multi-day event is drawn in every day column it
|
|
151
|
+
* overlaps, clipped to that day. */
|
|
152
|
+
function blockGeometry(occurrence: CalendarOccurrence, dayStart: Date): { top: number; height: number } {
|
|
153
|
+
const visibleStart = Math.max(new Date(occurrence.startDate).getTime(), dayStart.getTime());
|
|
154
|
+
const visibleEnd = Math.min(new Date(occurrence.endDate).getTime(), addDays(dayStart, 1).getTime());
|
|
155
|
+
return {
|
|
156
|
+
top: ((visibleStart - dayStart.getTime()) / 60_000 / 60) * HOUR_HEIGHT_PX,
|
|
157
|
+
height: Math.max(((visibleEnd - visibleStart) / 60_000 / 60) * HOUR_HEIGHT_PX, 16),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** A multi-day event's block in a day column after its first: clickable, but not draggable/resizable
|
|
162
|
+
* (its drag ids are already taken by the start day's block). */
|
|
163
|
+
function ContinuationBlock({
|
|
164
|
+
occurrence,
|
|
165
|
+
color,
|
|
166
|
+
dayStart,
|
|
167
|
+
onSelect,
|
|
168
|
+
}: {
|
|
169
|
+
occurrence: CalendarOccurrence;
|
|
170
|
+
color: string;
|
|
171
|
+
dayStart: Date;
|
|
172
|
+
onSelect: (occurrence: CalendarOccurrence) => void;
|
|
173
|
+
}) {
|
|
174
|
+
const isFree = occurrence.busyStatus === "free";
|
|
175
|
+
const { top, height } = blockGeometry(occurrence, dayStart);
|
|
176
|
+
return (
|
|
177
|
+
<div
|
|
178
|
+
onClick={() => onSelect(occurrence)}
|
|
179
|
+
style={{ position: "absolute", top, height, left: 2, right: 2, ...(isFree ? undefined : { backgroundColor: color, color: "#fff" }) }}
|
|
180
|
+
className={["rounded-sm px-1.5 py-0.5 text-xs text-left overflow-hidden cursor-pointer", isFree ? "bg-surface-alt text-text-muted" : ""].join(" ")}
|
|
181
|
+
>
|
|
182
|
+
<div className="font-medium truncate">{occurrence.title}</div>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function EventBlock({
|
|
188
|
+
occurrence,
|
|
189
|
+
color,
|
|
190
|
+
dayStart,
|
|
191
|
+
onSelect,
|
|
192
|
+
}: {
|
|
193
|
+
occurrence: CalendarOccurrence;
|
|
194
|
+
/** This occurrence's calendar color — see `calendarColors.ts`. Only applied for a "busy" block; a
|
|
195
|
+
* "free" one keeps Outlook's own muted/outline treatment regardless of which calendar it's on. */
|
|
196
|
+
color: string;
|
|
197
|
+
dayStart: Date;
|
|
198
|
+
onSelect: (occurrence: CalendarOccurrence) => void;
|
|
199
|
+
}) {
|
|
200
|
+
const { setNodeRef, listeners, attributes, transform, isDragging } = useDraggable({ id: eventDragId(occurrence) });
|
|
201
|
+
const { setNodeRef: setResizeRef, listeners: resizeListeners, attributes: resizeAttributes } = useDraggable({
|
|
202
|
+
id: resizeDragId(occurrence),
|
|
203
|
+
});
|
|
204
|
+
const isFree = occurrence.busyStatus === "free";
|
|
205
|
+
|
|
206
|
+
const start = new Date(occurrence.startDate);
|
|
207
|
+
const { top, height } = blockGeometry(occurrence, dayStart);
|
|
208
|
+
// The resize handle's own drag position has no live pixel preview (it only applies its ns-resize
|
|
209
|
+
// affordance) — the actual new end time is computed from whichever slot it's dropped onto (see
|
|
210
|
+
// `resolveDragAction`'s "resize" case), snapped to the grid rather than following the pointer
|
|
211
|
+
// continuously. Simpler, and avoids a visual preview that could disagree with the snapped result.
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<div
|
|
215
|
+
ref={setNodeRef}
|
|
216
|
+
onClick={() => onSelect(occurrence)}
|
|
217
|
+
style={{
|
|
218
|
+
position: "absolute",
|
|
219
|
+
top,
|
|
220
|
+
height,
|
|
221
|
+
left: 2,
|
|
222
|
+
right: 2,
|
|
223
|
+
transform: transform ? `translate(${transform.x}px, ${transform.y}px)` : undefined,
|
|
224
|
+
zIndex: isDragging ? 10 : undefined,
|
|
225
|
+
...(isFree ? undefined : { backgroundColor: color, color: "#fff" }),
|
|
226
|
+
}}
|
|
227
|
+
className={[
|
|
228
|
+
"rounded-sm px-1.5 py-0.5 text-xs text-left overflow-hidden cursor-pointer",
|
|
229
|
+
isFree ? "bg-surface-alt text-text-muted" : "",
|
|
230
|
+
isDragging ? "opacity-50" : "",
|
|
231
|
+
].join(" ")}
|
|
232
|
+
{...listeners}
|
|
233
|
+
{...attributes}
|
|
234
|
+
>
|
|
235
|
+
<div className="font-medium truncate">{occurrence.title}</div>
|
|
236
|
+
<div className="truncate">{format(start, "h:mma")}</div>
|
|
237
|
+
<div
|
|
238
|
+
ref={setResizeRef}
|
|
239
|
+
className="absolute bottom-0 left-0 right-0 h-1.5 cursor-ns-resize"
|
|
240
|
+
aria-label={`Resize "${occurrence.title}"`}
|
|
241
|
+
{...resizeListeners}
|
|
242
|
+
{...resizeAttributes}
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|