@rapidmx/web-client 0.14.1 → 0.16.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 +373 -373
- package/apps/admin/branding/index.tsx +39 -39
- package/apps/admin/data-requests/index.tsx +482 -480
- package/apps/admin/domains/[uid].tsx +164 -163
- package/apps/admin/escrow-scopes/[uid].tsx +348 -348
- package/apps/admin/index.tsx +127 -124
- package/apps/admin/mailboxes/[uid].tsx +269 -224
- package/apps/admin/mailboxes/new/index.tsx +28 -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/escrow/audit-log/index.tsx +196 -196
- package/apps/escrow/matters/[uid].tsx +621 -621
- package/apps/shared/appearance/AppearanceForm.tsx +9 -1
- package/apps/shared/auth/adminAccess.ts +99 -99
- package/apps/shared/calendar/bookingPlugin.ts +100 -0
- package/apps/shared/calendar/calendarLiveUpdates.ts +27 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +378 -346
- 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 +53 -4
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
- package/apps/shared/components/admin/settings/PluginsManager.tsx +64 -10
- 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 +444 -444
- package/apps/shared/components/admin/usePagedList.tsx +129 -129
- package/apps/shared/components/calendar/AppointmentQuickForm.tsx +327 -0
- package/apps/shared/components/calendar/DescriptionEditor.tsx +274 -0
- package/apps/shared/components/calendar/EventDescriptionView.tsx +70 -0
- package/apps/shared/components/calendar/EventDetails.tsx +401 -0
- package/apps/shared/components/calendar/EventEditor.tsx +635 -0
- package/apps/shared/components/calendar/EventExpandedForm.tsx +210 -0
- package/apps/shared/components/calendar/EventFormParts.tsx +591 -0
- package/apps/shared/components/calendar/EventModal.tsx +206 -946
- package/apps/shared/components/calendar/EventQuickForm.tsx +116 -0
- package/apps/shared/components/calendar/EventShell.tsx +202 -0
- package/apps/shared/components/calendar/FindATime.tsx +379 -0
- package/apps/shared/components/calendar/GuestInput.tsx +100 -0
- package/apps/shared/components/calendar/LazyDescriptionEditor.tsx +70 -0
- package/apps/shared/components/calendar/MonthView.tsx +185 -173
- package/apps/shared/components/calendar/QuickCreateFaces.tsx +87 -0
- package/apps/shared/components/calendar/QuickCreateTabs.tsx +81 -0
- package/apps/shared/components/calendar/QuickFaceFrame.tsx +81 -0
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -222
- package/apps/shared/components/calendar/RequestChangeForm.tsx +240 -0
- package/apps/shared/components/calendar/SplitDayView.tsx +144 -143
- package/apps/shared/components/calendar/TaskQuickForm.tsx +289 -0
- package/apps/shared/components/calendar/TimeGridView.tsx +246 -244
- package/apps/shared/components/calendar/allDay.ts +124 -124
- package/apps/shared/components/calendar/eventDialogFields.ts +86 -0
- package/apps/shared/components/calendar/eventForm.ts +108 -0
- package/apps/shared/components/calendar/eventFormat.ts +210 -0
- package/apps/shared/components/calendar/findTimeGrid.ts +90 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
- package/apps/shared/components/calendar/swipeNavigation.ts +14 -0
- 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 +217 -216
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
- package/apps/shared/components/layout/AppShell.tsx +480 -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 -372
- package/apps/shared/components/mail/ConversationList.tsx +323 -271
- package/apps/shared/components/mail/ConversationThreadPane.tsx +130 -5
- package/apps/shared/components/mail/InviteCard.tsx +387 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +236 -227
- package/apps/shared/components/mail/MessageDetailPane.tsx +1559 -1537
- package/apps/shared/components/mail/SwipeRow.tsx +120 -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 -1673
- 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/invite/InviteRowChip.tsx +104 -0
- package/apps/shared/components/mail/invite/InviteRsvpPopover.tsx +271 -0
- package/apps/shared/components/mail/invite/InviteTimeline.tsx +200 -0
- package/apps/shared/components/mail/invite/ProposeTimeForm.tsx +109 -0
- package/apps/shared/components/mail/invite/inviteFormat.ts +110 -0
- package/apps/shared/components/mail/invite/inviteStore.ts +114 -0
- package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +546 -490
- package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
- package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -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 +255 -253
- package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
- package/apps/shared/components/tasks/layout/TasksShell.tsx +218 -217
- package/apps/shared/gestures/useEnterSlide.ts +54 -0
- package/apps/shared/gestures/useSwipe.ts +114 -0
- package/apps/shared/gestures/useSwipeSlide.ts +113 -0
- package/apps/shared/gestures/useWheelPaging.ts +107 -0
- 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/folderTree.ts +143 -143
- package/apps/shared/mail/listAllPages.ts +39 -39
- package/apps/shared/mail/newMailNotifications.ts +3 -15
- 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/primaryMailbox.ts +71 -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/useMarkMessageRead.ts +47 -47
- package/apps/shared/mail/useNewMailNotifications.ts +2 -2
- package/apps/shared/navigation/AppRouter.tsx +300 -300
- package/apps/shared/navigation/routerContext.tsx +83 -83
- package/apps/shared/notifications/preferences.ts +36 -0
- package/apps/shared/notifications/store.ts +9 -0
- 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/_routes.ts +1 -0
- package/apps/www/calendar/index.tsx +578 -469
- package/apps/www/contacts/[uid].tsx +109 -109
- package/apps/www/contacts/index.tsx +6 -0
- package/apps/www/index.tsx +2847 -2332
- package/apps/www/messages/[uid].tsx +135 -108
- package/apps/www/settings/auto-reply/index.tsx +133 -133
- package/apps/www/settings/encryption/index.tsx +1287 -1287
- package/apps/www/settings/filters/[uid].tsx +176 -176
- package/apps/www/settings/filters/index.tsx +102 -102
- package/apps/www/settings/filters/new/index.tsx +140 -140
- package/apps/www/settings/labels/index.tsx +204 -204
- package/apps/www/settings/privacy/index.tsx +492 -492
- package/apps/www/settings/profile/index.tsx +248 -0
- package/apps/www/settings/read-receipts/index.tsx +147 -147
- package/apps/www/settings/sharing/index.tsx +256 -256
- package/apps/www/settings/signatures/[uid].tsx +172 -172
- package/apps/www/settings/signatures/index.tsx +88 -88
- package/apps/www/settings/signatures/new/index.tsx +135 -135
- package/apps/www/tasks/index.tsx +649 -649
- package/dist/apps/admin/data-requests/index.js +1 -1
- package/dist/apps/admin/domains/[uid].js +4 -2
- package/dist/apps/admin/index.js +2 -1
- package/dist/apps/admin/mailboxes/[uid].js +12 -2
- package/dist/apps/shared/appearance/AppearanceForm.js +9 -1
- package/dist/apps/shared/calendar/bookingPlugin.d.ts +50 -0
- package/dist/apps/shared/calendar/bookingPlugin.js +57 -0
- package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -0
- package/dist/apps/shared/calendar/calendarLiveUpdates.js +23 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +7 -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 +28 -4
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +40 -6
- package/dist/apps/shared/components/calendar/AppointmentQuickForm.d.ts +47 -0
- package/dist/apps/shared/components/calendar/AppointmentQuickForm.js +135 -0
- package/dist/apps/shared/components/calendar/DescriptionEditor.d.ts +26 -0
- package/dist/apps/shared/components/calendar/DescriptionEditor.js +192 -0
- package/dist/apps/shared/components/calendar/EventDescriptionView.d.ts +14 -0
- package/dist/apps/shared/components/calendar/EventDescriptionView.js +39 -0
- package/dist/apps/shared/components/calendar/EventDetails.d.ts +33 -0
- package/dist/apps/shared/components/calendar/EventDetails.js +137 -0
- package/dist/apps/shared/components/calendar/EventEditor.d.ts +59 -0
- package/dist/apps/shared/components/calendar/EventEditor.js +544 -0
- package/dist/apps/shared/components/calendar/EventExpandedForm.d.ts +14 -0
- package/dist/apps/shared/components/calendar/EventExpandedForm.js +44 -0
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +70 -0
- package/dist/apps/shared/components/calendar/EventFormParts.js +191 -0
- package/dist/apps/shared/components/calendar/EventModal.d.ts +26 -13
- package/dist/apps/shared/components/calendar/EventModal.js +57 -459
- package/dist/apps/shared/components/calendar/EventQuickForm.d.ts +18 -0
- package/dist/apps/shared/components/calendar/EventQuickForm.js +25 -0
- package/dist/apps/shared/components/calendar/EventShell.d.ts +64 -0
- package/dist/apps/shared/components/calendar/EventShell.js +134 -0
- package/dist/apps/shared/components/calendar/FindATime.d.ts +18 -0
- package/dist/apps/shared/components/calendar/FindATime.js +190 -0
- 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/LazyDescriptionEditor.d.ts +8 -0
- package/dist/apps/shared/components/calendar/LazyDescriptionEditor.js +44 -0
- package/dist/apps/shared/components/calendar/MonthView.d.ts +5 -1
- package/dist/apps/shared/components/calendar/MonthView.js +11 -5
- package/dist/apps/shared/components/calendar/QuickCreateFaces.d.ts +30 -0
- package/dist/apps/shared/components/calendar/QuickCreateFaces.js +44 -0
- package/dist/apps/shared/components/calendar/QuickCreateTabs.d.ts +24 -0
- package/dist/apps/shared/components/calendar/QuickCreateTabs.js +46 -0
- package/dist/apps/shared/components/calendar/QuickFaceFrame.d.ts +25 -0
- package/dist/apps/shared/components/calendar/QuickFaceFrame.js +19 -0
- package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +2 -2
- package/dist/apps/shared/components/calendar/RequestChangeForm.d.ts +36 -0
- package/dist/apps/shared/components/calendar/RequestChangeForm.js +127 -0
- package/dist/apps/shared/components/calendar/SplitDayView.d.ts +3 -2
- package/dist/apps/shared/components/calendar/SplitDayView.js +3 -2
- package/dist/apps/shared/components/calendar/TaskQuickForm.d.ts +40 -0
- package/dist/apps/shared/components/calendar/TaskQuickForm.js +101 -0
- package/dist/apps/shared/components/calendar/TimeGridView.d.ts +4 -2
- package/dist/apps/shared/components/calendar/TimeGridView.js +3 -2
- package/dist/apps/shared/components/calendar/eventDialogFields.d.ts +28 -0
- package/dist/apps/shared/components/calendar/eventDialogFields.js +59 -0
- package/dist/apps/shared/components/calendar/eventForm.d.ts +98 -0
- package/dist/apps/shared/components/calendar/eventForm.js +1 -0
- package/dist/apps/shared/components/calendar/eventFormat.d.ts +80 -0
- package/dist/apps/shared/components/calendar/eventFormat.js +190 -0
- package/dist/apps/shared/components/calendar/findTimeGrid.d.ts +30 -0
- package/dist/apps/shared/components/calendar/findTimeGrid.js +83 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +3 -2
- package/dist/apps/shared/components/calendar/swipeNavigation.d.ts +9 -0
- package/dist/apps/shared/components/calendar/swipeNavigation.js +8 -0
- 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 +4 -3
- 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 +14 -9
- package/dist/apps/shared/components/layout/UserMenu.js +74 -26
- package/dist/apps/shared/components/mail/ConversationList.d.ts +12 -1
- package/dist/apps/shared/components/mail/ConversationList.js +21 -4
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +4 -1
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
- package/dist/apps/shared/components/mail/InviteCard.d.ts +20 -0
- package/dist/apps/shared/components/mail/InviteCard.js +150 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +6 -1
- package/dist/apps/shared/components/mail/MailSelectionBar.js +3 -3
- package/dist/apps/shared/components/mail/MessageDetailPane.js +13 -2
- package/dist/apps/shared/components/mail/SwipeRow.d.ts +22 -0
- package/dist/apps/shared/components/mail/SwipeRow.js +71 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +16 -7
- 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/invite/InviteRowChip.d.ts +23 -0
- package/dist/apps/shared/components/mail/invite/InviteRowChip.js +46 -0
- package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.d.ts +20 -0
- package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.js +141 -0
- package/dist/apps/shared/components/mail/invite/InviteTimeline.d.ts +27 -0
- package/dist/apps/shared/components/mail/invite/InviteTimeline.js +120 -0
- package/dist/apps/shared/components/mail/invite/ProposeTimeForm.d.ts +17 -0
- package/dist/apps/shared/components/mail/invite/ProposeTimeForm.js +45 -0
- package/dist/apps/shared/components/mail/invite/inviteFormat.d.ts +28 -0
- package/dist/apps/shared/components/mail/invite/inviteFormat.js +94 -0
- package/dist/apps/shared/components/mail/invite/inviteStore.d.ts +28 -0
- package/dist/apps/shared/components/mail/invite/inviteStore.js +95 -0
- 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 +10 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +76 -46
- 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/PendingMessageCard.d.ts +14 -0
- package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +7 -5
- package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +4 -3
- package/dist/apps/shared/gestures/useEnterSlide.d.ts +14 -0
- package/dist/apps/shared/gestures/useEnterSlide.js +47 -0
- package/dist/apps/shared/gestures/useSwipe.d.ts +45 -0
- package/dist/apps/shared/gestures/useSwipe.js +90 -0
- package/dist/apps/shared/gestures/useSwipeSlide.d.ts +40 -0
- package/dist/apps/shared/gestures/useSwipeSlide.js +95 -0
- package/dist/apps/shared/gestures/useWheelPaging.d.ts +32 -0
- package/dist/apps/shared/gestures/useWheelPaging.js +85 -0
- package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
- package/dist/apps/shared/mail/folderCounts.js +12 -0
- package/dist/apps/shared/mail/newMailNotifications.d.ts +3 -8
- package/dist/apps/shared/mail/newMailNotifications.js +3 -12
- 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/primaryMailbox.d.ts +29 -0
- package/dist/apps/shared/mail/primaryMailbox.js +61 -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/useNewMailNotifications.js +3 -2
- package/dist/apps/shared/notifications/preferences.d.ts +13 -0
- package/dist/apps/shared/notifications/preferences.js +36 -0
- package/dist/apps/shared/notifications/store.d.ts +1 -0
- package/dist/apps/shared/notifications/store.js +7 -23
- 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/_routes.js +1 -0
- package/dist/apps/www/calendar/index.js +78 -7
- package/dist/apps/www/contacts/index.js +3 -1
- package/dist/apps/www/index.js +548 -210
- package/dist/apps/www/messages/[uid].js +13 -5
- package/dist/apps/www/settings/profile/index.d.ts +4 -0
- package/dist/apps/www/settings/profile/index.js +124 -0
- package/package.json +2 -2
|
@@ -4,11 +4,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
7
|
-
import { HiOutlineBars3 } from "react-icons/hi2";
|
|
7
|
+
import { HiOutlineBars3, HiOutlinePencilSquare } from "react-icons/hi2";
|
|
8
8
|
import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
|
|
9
9
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
10
10
|
import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
11
11
|
import AppShell from "../../layout/AppShell.js";
|
|
12
|
+
import FloatingActionButton from "../../layout/FloatingActionButton.js";
|
|
12
13
|
import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
|
|
13
14
|
import KeyEnrollmentGate from "../../layout/KeyEnrollmentGate.js";
|
|
14
15
|
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
@@ -17,7 +18,11 @@ import LocalIndexLifecycle from "../../../search/LocalIndexLifecycle.js";
|
|
|
17
18
|
import { NO_LIVE_UPDATES } from "../../../mail/useMailLiveUpdates.js";
|
|
18
19
|
import { MAILBOX_LIST_LIMIT, MailConnectionContext, useMailConnection } from "../../../mail/useMailConnection.js";
|
|
19
20
|
import { folderRows } from "../../../mail/folderTree.js";
|
|
20
|
-
import {
|
|
21
|
+
import { primaryMailboxUid } from "../../../mail/primaryMailbox.js";
|
|
22
|
+
import { badgeFor, countOfFolder, inboxUnreadTotal, unreadBadgeTotal, } from "../../../mail/folderCounts.js";
|
|
23
|
+
import { ALL_MAILBOXES_SECTION, useSidebarSections } from "../../../mail/useCollapsedSections.js";
|
|
24
|
+
import FolderBadgeChip from "./FolderBadgeChip.js";
|
|
25
|
+
import SidebarSection from "./SidebarSection.js";
|
|
21
26
|
import { useUnreadTitle } from "../../../mail/useUnreadTitle.js";
|
|
22
27
|
import { pendingCountFor, usePendingSends } from "../../../mail/outbox/pendingSends.js";
|
|
23
28
|
import OutboxBadge from "../OutboxBadge.js";
|
|
@@ -42,6 +47,7 @@ const MailShellContext = createContext({
|
|
|
42
47
|
noteFolderUids: () => undefined,
|
|
43
48
|
live: NO_LIVE_UPDATES,
|
|
44
49
|
trackMessageChange: () => NO_TRACKER,
|
|
50
|
+
mobileSearchSlot: null,
|
|
45
51
|
});
|
|
46
52
|
/** Reads the mailbox/folder a page is currently showing, as resolved by the enclosing `MailShell`. */
|
|
47
53
|
export function useMailShell() {
|
|
@@ -65,13 +71,6 @@ function aggregateBadge(mailboxFolders, type, counts) {
|
|
|
65
71
|
}, { unread: 0, total: 0 });
|
|
66
72
|
return badgeFor(type, sum);
|
|
67
73
|
}
|
|
68
|
-
/** A folder's badge: an accent pill with the unread count, or - for the folders that show how many they hold - plain muted text. */
|
|
69
|
-
function FolderBadgeChip({ badge }) {
|
|
70
|
-
return (_jsxs("span", { className: [
|
|
71
|
-
"text-xs rounded-pill py-0.5 px-1.5",
|
|
72
|
-
badge.kind === "unread" ? "font-bold bg-primary/15 text-primary-dark" : "font-medium text-text-muted",
|
|
73
|
-
].join(" "), children: [_jsx("span", { "aria-hidden": "true", children: badge.value }), _jsxs("span", { className: "sr-only", children: [" ", badgeLabel(badge)] })] }));
|
|
74
|
-
}
|
|
75
74
|
export { MAILBOX_LIST_LIMIT };
|
|
76
75
|
/**
|
|
77
76
|
* A separate component (not inlined into `MailShell`'s own render) so `useCompose()` resolves against
|
|
@@ -91,6 +90,14 @@ function ComposeButton({ mailboxUid }) {
|
|
|
91
90
|
const env = useKeyEnvironment();
|
|
92
91
|
return (_jsx("button", { type: "button", title: withHint("Compose", SHORTCUTS.mail.create, env), "aria-keyshortcuts": ariaKeyShortcuts(SHORTCUTS.mail.create, env), onClick: () => openCompose({ mailboxUid }), onPointerEnter: prefetchComposeWindow, onFocus: prefetchComposeWindow, className: "block text-center w-full py-2.5 px-4 rounded-sm font-semibold text-sm bg-primary text-white hover:bg-primary-dark", children: "Compose" }));
|
|
93
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* A small round Compose button floating at the bottom right on a phone, just above the bottom tab bar (which is `h-14`, fixed), for the
|
|
95
|
+
* layout where the sidebar's Compose button is behind the folders drawer. Opens the same window, for the same mailbox, as that button.
|
|
96
|
+
*/
|
|
97
|
+
function MobileComposeButton({ mailboxUid }) {
|
|
98
|
+
const { openCompose } = useCompose();
|
|
99
|
+
return (_jsx(FloatingActionButton, { label: "New message", icon: HiOutlinePencilSquare, onClick: () => openCompose({ mailboxUid }), onPointerEnter: prefetchComposeWindow, onFocus: prefetchComposeWindow }));
|
|
100
|
+
}
|
|
94
101
|
/**
|
|
95
102
|
* Mail's keyboard shortcuts that don't belong to a list or a message: "New message". A component of its own for the reason `ComposeButton`
|
|
96
103
|
* is - `useCompose()` only resolves below `ComposeProvider`, i.e. inside the `AppShell` this shell renders - and mounted once, whatever number
|
|
@@ -116,6 +123,11 @@ function MailShortcuts({ mailboxUid }) {
|
|
|
116
123
|
* behavior change from before this, and a multi-mailbox user sees every mailbox's folders (and the new
|
|
117
124
|
* aggregate section) simultaneously, matching the shared-mailbox feature's own "display as a separate set
|
|
118
125
|
* of folders" requirement.
|
|
126
|
+
*
|
|
127
|
+
* An address that names nothing opens "All Mailboxes > Inbox" when there is more than one mailbox (that is when the section exists), and
|
|
128
|
+
* the one mailbox's own Inbox otherwise; a `?mailboxUid=`, `?folderUid=` or `?aggregate=` always wins. Nothing is selected until the address
|
|
129
|
+
* has been read. What needs a single mailbox while an all-mailboxes view is open (Compose, the new-message shortcut, key unlocking) uses the
|
|
130
|
+
* primary mailbox.
|
|
119
131
|
*/
|
|
120
132
|
export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }) {
|
|
121
133
|
// The mailboxes, their folders and the live connection come from the persistent app frame when there is one (`AppChrome` owns them, so
|
|
@@ -131,6 +143,8 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
|
|
|
131
143
|
const [requestedFolderUid, setRequestedFolderUid] = useState(null);
|
|
132
144
|
const [requestedAggregateType, setRequestedAggregateType] = useState(null);
|
|
133
145
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
146
|
+
// Whether the URL has been read yet (see below): until then the selection is not known, and the sidebar sections have nothing to open for.
|
|
147
|
+
const [selectionRead, setSelectionRead] = useState(false);
|
|
134
148
|
// The selection lives in the URL, and the router (`AppRouter`) changes the URL without a page load when a folder link is
|
|
135
149
|
// clicked - so it is read from the router's location, which updates, rather than once from `window.location`. Still read
|
|
136
150
|
// in an effect, never during the first render, so the server render and the hydrating render agree.
|
|
@@ -140,36 +154,51 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
|
|
|
140
154
|
setRequestedMailboxUid(params.get("mailboxUid"));
|
|
141
155
|
setRequestedFolderUid(params.get("folderUid"));
|
|
142
156
|
setRequestedAggregateType(params.get("aggregate"));
|
|
157
|
+
setSelectionRead(true);
|
|
143
158
|
}, [search]);
|
|
144
159
|
// Choosing a folder (or mailbox) changes the URL without a page load now, so the drawer that held the choice - which used to go
|
|
145
160
|
// with the page - is closed here.
|
|
146
161
|
useEffect(() => {
|
|
147
162
|
setDrawerOpen(false);
|
|
148
163
|
}, [search]);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
164
|
+
// What the address asks for. Nothing is selected until it has been read (the first render, before the effect above): choosing a
|
|
165
|
+
// default then would list a folder - the most expensive being every mailbox's - only to throw it away for the one the address names.
|
|
166
|
+
const requestedAggregate = selectionRead && isAggregateFolderType(requestedAggregateType) ? requestedAggregateType : undefined;
|
|
167
|
+
const requestedMailbox = selectionRead && requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined;
|
|
168
|
+
// The mailbox the caller has not chosen: their own, never a shared one that merely sorts first. It is what compose, the new-message
|
|
169
|
+
// shortcut and everything else that needs a single mailbox use while an all-mailboxes view (which has none) is open.
|
|
170
|
+
const defaultMailboxUid = primaryMailboxUid(mailboxes, userUid);
|
|
171
|
+
// With several mailboxes the address that names nothing opens "All Mailboxes > Inbox", the merged Inbox; with one there is no such
|
|
172
|
+
// section, so it opens that mailbox's own Inbox. A mailbox, a folder or an all-mailboxes view in the address wins over either.
|
|
173
|
+
const aggregateByDefault = selectionRead && !requestedAggregate && !requestedMailbox && requestedFolderUid === null && mailboxes.length > 1;
|
|
174
|
+
const aggregateFolderType = requestedAggregate ?? (aggregateByDefault ? "inbox" : undefined);
|
|
175
|
+
const mailboxUid = aggregateFolderType || !selectionRead ? undefined : (requestedMailbox ?? defaultMailboxUid);
|
|
156
176
|
const selectedMailboxFolders = mailboxFolders.find((mf) => mf.mailbox.uid === mailboxUid)?.folders ?? [];
|
|
157
177
|
const folderUid = aggregateFolderType
|
|
158
178
|
? undefined
|
|
159
179
|
: (requestedFolderUid && selectedMailboxFolders.some((f) => f.uid === requestedFolderUid) ? requestedFolderUid : undefined) ??
|
|
160
180
|
selectedMailboxFolders.find((f) => f.type === "inbox")?.uid;
|
|
161
181
|
// The mailbox `KeyEnrollmentGate`/`LocalIndexLifecycle`/`ComposeButton` treat as "the" mailbox when
|
|
162
|
-
// there's no single selected one to use (aggregate mode) - the caller's own
|
|
163
|
-
// have one, else whichever accessible mailbox happens to be first. See MailShell's own doc comment on
|
|
182
|
+
// there's no single selected one to use (aggregate mode) - the caller's primary mailbox (`primaryMailbox()`). See MailShell's own doc comment on
|
|
164
183
|
// this being an accepted limitation: an aggregate-view message from a *different*, not-yet-visited
|
|
165
184
|
// mailbox may still need that mailbox's own folder view opened directly to unlock/decrypt it.
|
|
166
|
-
const defaultMailboxUid = mailboxes.find((mb) => mb.ownerUserUid === userUid)?.uid ?? mailboxes[0]?.uid;
|
|
167
185
|
const activeMailboxUid = mailboxUid ?? defaultMailboxUid;
|
|
186
|
+
// Which sidebar sections are open: "All mailboxes" and the primary mailbox unless the reader chose otherwise. Any of them can be collapsed;
|
|
187
|
+
// a change of the open folder into another section opens that one (see `useSidebarSections()`); the first selection, once the address has been read
|
|
188
|
+
// and the mailboxes are here, is where a load lands, not a navigation. A lone mailbox has nothing to collapse, so its section is never a toggle.
|
|
189
|
+
const sections = useSidebarSections({
|
|
190
|
+
userUid,
|
|
191
|
+
primaryUid: defaultMailboxUid,
|
|
192
|
+
activeId: aggregateFolderType ? ALL_MAILBOXES_SECTION : mailboxUid,
|
|
193
|
+
});
|
|
194
|
+
const collapsible = mailboxes.length > 1;
|
|
168
195
|
const counts = folderCounts.counts;
|
|
169
196
|
const trackMessageChange = folderCounts.track;
|
|
170
197
|
// `(3) Acme: Mail` in the tab strip while there is unread mail in an Inbox - kept by the frame when there is one.
|
|
171
198
|
useUnreadTitle(inboxUnreadTotal(mailboxFolders, counts), { enabled: !hosted });
|
|
172
|
-
|
|
199
|
+
// A state rather than a ref: the page reads it during its render, so it has to re-render once the row it names is in the document.
|
|
200
|
+
const [mobileSearchSlot, setMobileSearchSlot] = useState(null);
|
|
201
|
+
const contextValue = useMemo(() => ({ mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange, mobileSearchSlot }), [mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange, mobileSearchSlot]);
|
|
173
202
|
// A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else for a
|
|
174
203
|
// mailbox-less caller to do here yet, so the icon rail/header/folder tree don't render at all. Checks
|
|
175
204
|
// `mailboxes.length` directly (not `!mailboxUid`) since `mailboxUid` is legitimately undefined in
|
|
@@ -190,32 +219,33 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
|
|
|
190
219
|
// A function, not a plain JSX constant — it's rendered twice (desktop `<aside>` + mobile
|
|
191
220
|
// `Drawer`), possibly *simultaneously* mounted (the aside is only CSS-hidden below `md`, not
|
|
192
221
|
// unmounted).
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
222
|
+
// `idPrefix` keeps each section's folder list's id (which its heading's `aria-controls` names) unique between the two.
|
|
223
|
+
const sidebarContent = (idPrefix) => (_jsxs(_Fragment, { children: [_jsx("div", { className: "p-3", children: _jsx(ComposeButton, { mailboxUid: defaultMailboxUid }) }), foldersLoading ? (_jsx("div", { className: "flex-1 overflow-y-auto px-3 pb-3", children: _jsx(SkeletonList, { count: 6, className: "pt-1" }) })) : (_jsxs("nav", { className: "flex-1 overflow-y-auto px-3 pb-3 flex flex-col gap-3", children: [mailboxes.length > 1 && (_jsx(SidebarSection, { domId: `${idPrefix}-mail-section-${ALL_MAILBOXES_SECTION}`, label: "All Mailboxes", collapsible: collapsible, expanded: sections.isExpanded(ALL_MAILBOXES_SECTION), onToggle: () => sections.toggle(ALL_MAILBOXES_SECTION), unread: unreadBadgeTotal(mailboxFolders.flatMap((mf) => mf.folders), counts), children: AGGREGATE_FOLDER_TYPES.map((type) => {
|
|
224
|
+
const badge = aggregateBadge(mailboxFolders, type, counts);
|
|
225
|
+
return (_jsxs("a", { href: `/?aggregate=${encodeURIComponent(type)}`, className: [
|
|
226
|
+
"flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
|
|
227
|
+
aggregateFolderType === type
|
|
228
|
+
? "bg-primary/10 text-primary-dark font-semibold"
|
|
229
|
+
: "text-text hover:bg-surface-alt",
|
|
230
|
+
].join(" "), children: [_jsx("span", { className: badge?.kind === "unread" ? "font-semibold" : undefined, children: FOLDER_LABELS[type] }), badge && _jsx(FolderBadgeChip, { badge: badge })] }, type));
|
|
231
|
+
}) })), mailboxFolders.map(({ mailbox, folders, error: mailboxError }) => (_jsx(SidebarSection, { domId: `${idPrefix}-mail-section-${mailbox.uid}`, label: _jsxs(_Fragment, { children: [mailbox.displayName, mailbox.ownerUserUid ? "" : " (shared)"] }), collapsible: collapsible, expanded: sections.isExpanded(mailbox.uid), onToggle: () => sections.toggle(mailbox.uid), unread: unreadBadgeTotal(folders, counts), notice: mailboxError && (_jsx("div", { className: "px-2.5 pb-1", children: _jsx(Alert, { children: mailboxError }) })), children: folderRows(folders, pendingCountFor(mailbox.uid, pendingSends)).map((row) => {
|
|
232
|
+
if (row.kind === "placeholder") {
|
|
233
|
+
// A folder the server has not made (or told us about) yet - the Outbox and Sent Items are made with the first message
|
|
234
|
+
// sent - shown the moment a message is on its way. Nothing to open; the real folder replaces it when it arrives.
|
|
235
|
+
const sending = pendingCountFor(mailbox.uid, pendingSends);
|
|
236
|
+
return (_jsxs("div", { "data-folder-placeholder": row.type, className: "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5 text-text-muted", children: [_jsx("span", { children: FOLDER_LABELS[row.type] }), row.type === "outbox" && _jsx(OutboxBadge, { total: sending, pendingHere: sending })] }, `placeholder-${row.type}`));
|
|
237
|
+
}
|
|
238
|
+
const folder = row.folder;
|
|
239
|
+
const badge = badgeFor(folder.type, countOfFolder(folder, counts));
|
|
240
|
+
return (_jsxs("a", { href: `/?mailboxUid=${encodeURIComponent(mailbox.uid)}&folderUid=${encodeURIComponent(folder.uid)}`, className: [
|
|
241
|
+
"flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
|
|
242
|
+
folder.uid === folderUid
|
|
243
|
+
? "bg-primary/10 text-primary-dark font-semibold"
|
|
244
|
+
: "text-text hover:bg-surface-alt",
|
|
245
|
+
].join(" "), children: [_jsx("span", { className: badge?.kind === "unread" ? "font-semibold" : undefined, children: FOLDER_LABELS[folder.type] ?? folder.name }), badge &&
|
|
246
|
+
(folder.type === "outbox" ? (_jsx(OutboxBadge, { total: badge.value, status: outboxStatuses[folder.uid], pendingHere: pendingCountFor(mailbox.uid, pendingSends) })) : (_jsx(FolderBadgeChip, { badge: badge })))] }, folder.uid));
|
|
247
|
+
}) }, mailbox.uid)))] }))] }));
|
|
248
|
+
inner = (_jsxs(_Fragment, { children: [_jsx(MailShortcuts, { mailboxUid: defaultMailboxUid }), _jsx("aside", { className: "hidden md:flex w-64 shrink-0 bg-surface border-r border-border flex-col", children: sidebarContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Folders", fullScreen: true, children: _jsx("div", { className: "flex flex-col", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 overflow-y-auto", children: [_jsxs("div", { className: "md:hidden flex items-center gap-2 p-3", children: [_jsx("button", { type: "button", className: "shrink-0 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open folders", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("div", { ref: setMobileSearchSlot, className: "flex-1 min-w-0" })] }), _jsx(MobileComposeButton, { mailboxUid: defaultMailboxUid }), _jsx(MailShellContext.Provider, { value: contextValue, children: children })] })] }));
|
|
219
249
|
}
|
|
220
250
|
// Wraps AppShell unconditionally, at a stable tree position regardless of whether mailboxUid has
|
|
221
251
|
// resolved yet - KeyEnrollmentGate itself passes `children` through untouched until a real
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
export interface SidebarSectionProps {
|
|
3
|
+
/** Unique per rendering of the sidebar (it is on screen twice on a phone: beside the list and in the drawer): names the folder list the heading controls. */
|
|
4
|
+
domId: string;
|
|
5
|
+
/** The heading's text. */
|
|
6
|
+
label: ReactNode;
|
|
7
|
+
/** Whether the heading is a toggle at all. Not for a lone mailbox, whose section has nothing to be hidden for. */
|
|
8
|
+
collapsible: boolean;
|
|
9
|
+
expanded: boolean;
|
|
10
|
+
onToggle: () => void;
|
|
11
|
+
/** The unread mail in the section's folders that carry an unread count (see `unreadBadgeTotal()`), shown on the heading while it is collapsed (and only when there is some) so new mail in a hidden section is still noticed. */
|
|
12
|
+
unread?: number;
|
|
13
|
+
/** Shown between the heading and the folders, collapsed or not: a failure to load the section's folders is not something to hide. */
|
|
14
|
+
notice?: ReactNode;
|
|
15
|
+
/** The section's folder rows. */
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* One section of Mail's sidebar ("All mailboxes", or one mailbox): a heading over a list of folders. When `collapsible` the heading is a
|
|
20
|
+
* disclosure button (`aria-expanded`, `aria-controls` the list, a chevron that turns) and the list is `hidden` while it is collapsed -
|
|
21
|
+
* kept in the document, so the button's `aria-controls` always names something and nothing is fetched or rebuilt on opening it.
|
|
22
|
+
*/
|
|
23
|
+
export default function SidebarSection({ domId, label, collapsible, expanded, onToggle, unread, notice, children }: SidebarSectionProps): React.JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { HiChevronDown } from "react-icons/hi2";
|
|
3
|
+
import FolderBadgeChip from "./FolderBadgeChip.js";
|
|
4
|
+
/**
|
|
5
|
+
* One section of Mail's sidebar ("All mailboxes", or one mailbox): a heading over a list of folders. When `collapsible` the heading is a
|
|
6
|
+
* disclosure button (`aria-expanded`, `aria-controls` the list, a chevron that turns) and the list is `hidden` while it is collapsed -
|
|
7
|
+
* kept in the document, so the button's `aria-controls` always names something and nothing is fetched or rebuilt on opening it.
|
|
8
|
+
*/
|
|
9
|
+
export default function SidebarSection({ domId, label, collapsible, expanded, onToggle, unread = 0, notice, children }) {
|
|
10
|
+
const shown = !collapsible || expanded;
|
|
11
|
+
return (_jsxs("div", { children: [collapsible ? (_jsx("h2", { className: "mb-1", children: _jsxs("button", { type: "button", "aria-expanded": expanded, "aria-controls": domId, onClick: onToggle, className: [
|
|
12
|
+
"flex w-full items-center gap-1 rounded-sm px-2.5 py-0.5 text-left text-xs font-bold uppercase tracking-wide text-text-muted",
|
|
13
|
+
"focus-visible:outline-2 focus-visible:outline-primary",
|
|
14
|
+
"hover:bg-surface-alt hover:text-text",
|
|
15
|
+
].join(" "), children: [_jsx(HiChevronDown, { size: 14, "aria-hidden": "true", className: ["shrink-0 transition-transform", expanded ? "" : "-rotate-90"].join(" ") }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: label }), !expanded && unread > 0 && _jsx(FolderBadgeChip, { badge: { kind: "unread", value: unread } })] }) })) : (_jsx("div", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5 truncate", children: label })), notice, _jsx("div", { id: domId, hidden: !shown, className: shown ? "flex flex-col gap-0.5" : "hidden", children: children })] }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { OutgoingReply } from "../../../mail/outbox/outgoingReplies.js";
|
|
3
|
+
/**
|
|
4
|
+
* A reply or forward that has just been sent, drawn where its message will be once the server has filed it: the sender, who it went to, its body as
|
|
5
|
+
* it was composed, and one line saying where it is - "Sending...", "Sent", or, when it did not go, "Not sent: <why>" with the same ways out the
|
|
6
|
+
* pop-up offers (Retry, Open draft ...). It reads like the message's own card, so the real message replaces it without the thread rearranging.
|
|
7
|
+
*
|
|
8
|
+
* The body goes through the reading pane's own sanitizing frame (`MessageBody`), exactly as a received message's does.
|
|
9
|
+
*/
|
|
10
|
+
export default function PendingMessageCard({ reply, headerRef, }: {
|
|
11
|
+
reply: OutgoingReply;
|
|
12
|
+
/** Registered so the pane can put the focus here when the message first appears. Focusable, but not in the tab order. */
|
|
13
|
+
headerRef: (node: HTMLHeadingElement | null) => void;
|
|
14
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
3
|
+
import MailAddress, { RecipientLine } from "../MailAddress.js";
|
|
4
|
+
import MessageBody from "./MessageBody.js";
|
|
5
|
+
import { BODY_FONT_STYLE, CardShell, SenderAvatar } from "./MessageCard.js";
|
|
6
|
+
/**
|
|
7
|
+
* A reply or forward that has just been sent, drawn where its message will be once the server has filed it: the sender, who it went to, its body as
|
|
8
|
+
* it was composed, and one line saying where it is - "Sending...", "Sent", or, when it did not go, "Not sent: <why>" with the same ways out the
|
|
9
|
+
* pop-up offers (Retry, Open draft ...). It reads like the message's own card, so the real message replaces it without the thread rearranging.
|
|
10
|
+
*
|
|
11
|
+
* The body goes through the reading pane's own sanitizing frame (`MessageBody`), exactly as a received message's does.
|
|
12
|
+
*/
|
|
13
|
+
export default function PendingMessageCard({ reply, headerRef, }) {
|
|
14
|
+
const failed = reply.state === "failed";
|
|
15
|
+
return (_jsxs(CardShell, { children: [_jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [_jsx(SenderAvatar, { from: reply.sender }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-baseline justify-between gap-x-2 text-sm", children: [_jsx("h2", { ref: headerRef, tabIndex: -1, style: BODY_FONT_STYLE, className: "flex-1 min-w-[13rem] font-semibold outline-none focus-visible:outline-2 focus-visible:outline-primary", children: _jsx(MailAddress, { recipient: reply.sender }) }), _jsxs("div", { role: failed ? "alert" : "status", "data-testid": "pending-message-status", "data-state": reply.state, className: ["flex items-center gap-1.5 text-xs shrink-0", failed ? "font-semibold text-danger" : "text-text-muted"].join(" "), children: [reply.state === "sending" && _jsx("span", { "aria-hidden": "true", className: "rr-sending-dot h-1.5 w-1.5 shrink-0 rounded-full bg-primary" }), _jsx("span", { children: failed ? `Not sent: ${reply.failure}` : reply.state === "sending" ? "Sending…" : "Sent" })] }, failed ? "failed" : "progress")] }), _jsx(RecipientLine, { label: "To", recipients: reply.to }), _jsx(RecipientLine, { label: "Cc", recipients: reply.cc }), _jsx(RecipientLine, { label: "Bcc", recipients: reply.bcc })] })] }), failed && (_jsx("div", { className: "flex flex-wrap gap-2 px-4 pb-3", children: reply.actions.map((action) => (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: action.onClick, children: action.label }, action.label))) })), _jsx("div", { className: "px-4 pb-4", children: _jsx(MessageBody, { messageUid: reply.uid, messageVersion: 0, title: `Message: ${reply.subject}`, content: { kind: "html", html: reply.html } }) })] }));
|
|
16
|
+
}
|
|
@@ -13,9 +13,11 @@ import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedbac
|
|
|
13
13
|
import AppShell from "../../layout/AppShell.js";
|
|
14
14
|
import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
|
|
15
15
|
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
16
|
+
import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
|
|
16
17
|
import { mergePluginNavItems } from "../../../plugins/pluginNav.js";
|
|
17
18
|
/** Every settings section with its own sidebar entry. */
|
|
18
19
|
export const SETTINGS_SECTIONS = [
|
|
20
|
+
{ id: "profile", href: "/settings/profile", label: "Profile" },
|
|
19
21
|
{ id: "appearance", href: "/settings/appearance", label: "Appearance" },
|
|
20
22
|
{ id: "auto-reply", href: "/settings/auto-reply", label: "Automatic Replies" },
|
|
21
23
|
{ id: "filters", href: "/settings/filters", label: "Mail Filters" },
|
|
@@ -72,7 +74,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
72
74
|
}
|
|
73
75
|
listMailboxes({ limit: 100 })
|
|
74
76
|
.then((result) => {
|
|
75
|
-
setMailboxes(result);
|
|
77
|
+
setMailboxes(orderMailboxes(result, userUid));
|
|
76
78
|
setStatus("ready");
|
|
77
79
|
})
|
|
78
80
|
.catch((err) => {
|
|
@@ -81,7 +83,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
81
83
|
});
|
|
82
84
|
}, [userUid]);
|
|
83
85
|
const mailboxUid = (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
|
|
84
|
-
mailboxes
|
|
86
|
+
primaryMailboxUid(mailboxes, userUid);
|
|
85
87
|
const contextValue = useMemo(() => ({ mailboxUid, mailboxes }), [mailboxUid, mailboxes]);
|
|
86
88
|
// A full-screen takeover, not nested inside the rest of the app's chrome — same as ContactsShell/
|
|
87
89
|
// TasksShell, there's nothing else for a mailbox-less caller to configure here yet.
|
|
@@ -109,8 +111,8 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
109
111
|
}, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), _jsx("nav", { "aria-label": "Settings sections", className: "flex flex-col gap-0.5", children: sections.map((section) => (_jsx("a", {
|
|
110
112
|
// `inner` (this whole branch) is only ever computed once `status === "ready"`
|
|
111
113
|
// and `mailboxUid` has already resolved truthy — a falsy `mailboxUid` returns
|
|
112
|
-
// `<MailboxProvisioning />` above instead (
|
|
113
|
-
//
|
|
114
|
+
// `<MailboxProvisioning />` above instead (which is what the `!` below records,
|
|
115
|
+
// `primaryMailboxUid()` being honest that it may find none) — so no fallback is
|
|
114
116
|
// needed here, matching this plan's established "dead guard the UI
|
|
115
117
|
// structurally can't trigger" removal precedent (e.g.
|
|
116
118
|
// `DomainDetailContent.handleVerify`).
|
|
@@ -118,7 +120,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
118
120
|
"block px-2.5 py-1.5 rounded-sm text-sm",
|
|
119
121
|
section.id === active ? "bg-primary/10 text-primary-dark font-medium" : "text-text hover:bg-surface-alt",
|
|
120
122
|
].join(" "), children: section.label }, section.id))) })] }));
|
|
121
|
-
inner = (_jsxs(_Fragment, { children: [_jsx("aside", { className: "hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Settings", children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [_jsx("button", { type: "button", className: "md:hidden m-3 self-start w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open settings menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx(SettingsShellContext.Provider, { value: contextValue, children: children })] })] }));
|
|
123
|
+
inner = (_jsxs(_Fragment, { children: [_jsx("aside", { className: "hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Settings", fullScreen: true, children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [_jsx("button", { type: "button", className: "md:hidden m-3 self-start w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open settings menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx(SettingsShellContext.Provider, { value: contextValue, children: children })] })] }));
|
|
122
124
|
}
|
|
123
125
|
return (_jsx(AppShell, { active: "settings", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner }));
|
|
124
126
|
}
|
|
@@ -79,5 +79,5 @@ export default function TasksSidebar({ mailboxUid, tasks, userUid, active, onSel
|
|
|
79
79
|
// `Drawer` (see `ContactsSidebar` for the identical pattern and the duplicate-id reasoning behind
|
|
80
80
|
// the `idPrefix`).
|
|
81
81
|
const navContent = (idPrefix) => (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx(NavItem, { label: "My Day", count: myDayCount, active: active.type === "myDay", onClick: () => handleSelect({ type: "myDay" }) }), _jsx(NavItem, { label: "Important", count: importantCount, active: active.type === "important", onClick: () => handleSelect({ type: "important" }) }), _jsx(NavItem, { label: "Planned", count: plannedCount, active: active.type === "planned", onClick: () => handleSelect({ type: "planned" }) }), _jsx(NavItem, { label: "Assigned to me", count: assignedToMeCount, active: active.type === "assignedToMe", onClick: () => handleSelect({ type: "assignedToMe" }) }), _jsx(NavItem, { label: "Flagged email", active: active.type === "flagged", onClick: () => handleSelect({ type: "flagged" }) })] }), _jsx(NavItem, { label: "Tasks", count: tasks.length, active: active.type === "all", onClick: () => handleSelect({ type: "all" }) }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center justify-between px-2.5 mb-1", children: [_jsx("h2", { className: "text-xs font-bold uppercase tracking-wide text-text-muted", children: "Lists" }), _jsx("button", { type: "button", onClick: () => setAddingList(true), "aria-label": "New list", className: "text-xs font-bold text-primary-dark hover:underline", children: "+" })] }), listsError && _jsx("p", { className: "px-2.5 text-xs text-danger", children: listsError }), _jsx("div", { className: "flex flex-col gap-0.5", children: lists.map((list) => (_jsx(NavItem, { label: list.name, count: listCounts.get(list.uid) ?? 0, active: active.type === "list" && active.uid === list.uid, onClick: () => handleSelect({ type: "list", uid: list.uid, name: list.name }) }, list.uid))) }), addingList && (_jsxs("form", { onSubmit: handleAddList, className: "flex items-center gap-1 px-2.5 mt-1", children: [_jsx("input", { type: "text", autoFocus: true, "aria-label": "New list name", id: `${idPrefix}-new-list-name`, value: newListName, onChange: (e) => setNewListName(e.target.value), className: "flex-1 text-xs py-1 px-1.5 border border-border rounded-sm bg-surface" }), _jsx("button", { type: "submit", className: "text-xs font-semibold text-primary-dark", children: "Add" })] }))] })] }));
|
|
82
|
-
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "lg:hidden m-3 mb-0 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open tasks menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("nav", { "aria-label": "Tasks", className: "hidden lg:flex w-56 shrink-0 border-r border-border flex-col gap-4 p-3 overflow-y-auto", children: navContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Tasks", children: _jsx("div", { className: "flex flex-col gap-4", children: navContent("mobile") }) })] }));
|
|
82
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "lg:hidden m-3 mb-0 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open tasks menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("nav", { "aria-label": "Tasks", className: "hidden lg:flex w-56 shrink-0 border-r border-border flex-col gap-4 p-3 overflow-y-auto", children: navContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Tasks", fullScreen: true, children: _jsx("div", { className: "flex flex-col gap-4", children: navContent("mobile") }) })] }));
|
|
83
83
|
}
|
|
@@ -13,6 +13,7 @@ import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedbac
|
|
|
13
13
|
import AppShell from "../../layout/AppShell.js";
|
|
14
14
|
import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
|
|
15
15
|
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
16
|
+
import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
|
|
16
17
|
const TasksShellContext = createContext({ mailboxes: [] });
|
|
17
18
|
/** Reads the mailbox/folder Tasks is currently showing, as resolved by the enclosing `TasksShell`. */
|
|
18
19
|
export function useTasksShell() {
|
|
@@ -51,7 +52,7 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
|
|
|
51
52
|
}
|
|
52
53
|
listMailboxes({ limit: 100 })
|
|
53
54
|
.then((result) => {
|
|
54
|
-
setMailboxes(result);
|
|
55
|
+
setMailboxes(orderMailboxes(result, userUid));
|
|
55
56
|
setStatus("ready");
|
|
56
57
|
})
|
|
57
58
|
.catch((err) => {
|
|
@@ -60,7 +61,7 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
|
|
|
60
61
|
});
|
|
61
62
|
}, [userUid]);
|
|
62
63
|
const mailboxUid = (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
|
|
63
|
-
mailboxes
|
|
64
|
+
primaryMailboxUid(mailboxes, userUid);
|
|
64
65
|
useEffect(() => {
|
|
65
66
|
if (!mailboxUid) {
|
|
66
67
|
setFolders([]);
|
|
@@ -98,7 +99,7 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
|
|
|
98
99
|
}, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), folderError && _jsx(Alert, { children: folderError })] }));
|
|
99
100
|
// With a single mailbox and no error this column would be empty: no 224 px of nothing beside the tasks' own menu.
|
|
100
101
|
const hasSidebar = mailboxes.length > 1 || !!folderError;
|
|
101
|
-
inner = (_jsxs(_Fragment, { children: [hasSidebar && (_jsx("aside", { className: "hidden lg:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") })), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Mailbox", children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [hasSidebar && (_jsx("button", { type: "button", className: "lg:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open mailbox switcher", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) })), _jsx(TasksShellContext.Provider, { value: contextValue, children: children })] })] }));
|
|
102
|
+
inner = (_jsxs(_Fragment, { children: [hasSidebar && (_jsx("aside", { className: "hidden lg:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") })), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Mailbox", fullScreen: true, children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [hasSidebar && (_jsx("button", { type: "button", className: "lg:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open mailbox switcher", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) })), _jsx(TasksShellContext.Provider, { value: contextValue, children: children })] })] }));
|
|
102
103
|
}
|
|
103
104
|
return (_jsx(AppShell, { active: "tasks", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner }));
|
|
104
105
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type CSSProperties } from "react";
|
|
2
|
+
/** How long the new contents take to settle. Short, so a run of steps (a wheel kept turning) never waits on the last one. */
|
|
3
|
+
export declare const ENTER_SLIDE_MS = 140;
|
|
4
|
+
/**
|
|
5
|
+
* The look of a page that has just been swapped for the next or previous one by a vertical step (the wheel): the new contents start
|
|
6
|
+
* a little way off in the direction the step went - below for `1` (the next), above for `-1` - and faded, and settle into place.
|
|
7
|
+
* The old contents are not animated out: the swap is immediate, so a run of steps never queues, and a step in the middle of a
|
|
8
|
+
* settle just starts the next one from its start. Put `style` on the element that holds the contents and call `enter()` as the
|
|
9
|
+
* contents change. Nothing moves for someone who asked for less motion.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useEnterSlide(): {
|
|
12
|
+
style: CSSProperties;
|
|
13
|
+
enter: (direction: 1 | -1) => void;
|
|
14
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { useEffect, useRef, useState } from "react";
|
|
6
|
+
/** How long the new contents take to settle. Short, so a run of steps (a wheel kept turning) never waits on the last one. */
|
|
7
|
+
export const ENTER_SLIDE_MS = 140;
|
|
8
|
+
/** How far off, in pixels, and how faded the new contents start. */
|
|
9
|
+
const ENTER_OFFSET_PX = 56;
|
|
10
|
+
const ENTER_START_OPACITY = 0.35;
|
|
11
|
+
/** The pause that lets the browser draw the contents at their starting place before they move. */
|
|
12
|
+
const ENTER_SETTLE_MS = 20;
|
|
13
|
+
/** Whether the user asked their device for less motion. */
|
|
14
|
+
function prefersReducedMotion() {
|
|
15
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The look of a page that has just been swapped for the next or previous one by a vertical step (the wheel): the new contents start
|
|
19
|
+
* a little way off in the direction the step went - below for `1` (the next), above for `-1` - and faded, and settle into place.
|
|
20
|
+
* The old contents are not animated out: the swap is immediate, so a run of steps never queues, and a step in the middle of a
|
|
21
|
+
* settle just starts the next one from its start. Put `style` on the element that holds the contents and call `enter()` as the
|
|
22
|
+
* contents change. Nothing moves for someone who asked for less motion.
|
|
23
|
+
*/
|
|
24
|
+
export function useEnterSlide() {
|
|
25
|
+
const [offset, setOffset] = useState(0);
|
|
26
|
+
const [animated, setAnimated] = useState(false);
|
|
27
|
+
const timer = useRef(undefined);
|
|
28
|
+
useEffect(() => () => window.clearTimeout(timer.current), []);
|
|
29
|
+
function enter(direction) {
|
|
30
|
+
if (prefersReducedMotion()) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
window.clearTimeout(timer.current);
|
|
34
|
+
setAnimated(false);
|
|
35
|
+
setOffset(direction * ENTER_OFFSET_PX);
|
|
36
|
+
timer.current = window.setTimeout(() => {
|
|
37
|
+
setAnimated(true);
|
|
38
|
+
setOffset(0);
|
|
39
|
+
}, ENTER_SETTLE_MS);
|
|
40
|
+
}
|
|
41
|
+
const style = {
|
|
42
|
+
transform: offset === 0 ? undefined : `translate3d(0, ${offset}px, 0)`,
|
|
43
|
+
opacity: offset === 0 ? undefined : ENTER_START_OPACITY,
|
|
44
|
+
transition: animated ? `transform ${ENTER_SLIDE_MS}ms ease-out, opacity ${ENTER_SLIDE_MS}ms ease-out` : "none",
|
|
45
|
+
};
|
|
46
|
+
return { style, enter };
|
|
47
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type TouchEvent } from "react";
|
|
2
|
+
/** Which way the finger moved: `"left"` is right to left. */
|
|
3
|
+
export type SwipeDirection = "left" | "right";
|
|
4
|
+
export interface UseSwipeOptions {
|
|
5
|
+
/** Nothing is tracked while this is `false` (a desktop pointer, select mode). Default `true`. */
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
/** The directions that mean something; a drag the other way doesn't move at all. Default both. */
|
|
8
|
+
directions?: readonly SwipeDirection[];
|
|
9
|
+
/** How far the finger has to travel to commit, in px. Default `MIN_SWIPE_DISTANCE`, or a third of the element's width when that is more. */
|
|
10
|
+
distance?: number;
|
|
11
|
+
/** Called as the finger moves, with the horizontal offset (the direction filter applied), and with `0` once it lifts. */
|
|
12
|
+
onDrag?: (offset: number) => void;
|
|
13
|
+
/** Called once when a swipe commits. `onDrag(0)` follows. */
|
|
14
|
+
onSwipe: (direction: SwipeDirection) => void;
|
|
15
|
+
}
|
|
16
|
+
/** The least distance that commits a swipe, and how far the finger must move before its direction is decided. */
|
|
17
|
+
export declare const MIN_SWIPE_DISTANCE = 80;
|
|
18
|
+
export declare const SWIPE_SLOP = 10;
|
|
19
|
+
/** A quicker flick than this (px per ms) commits after `FLING_MIN_DISTANCE` alone. */
|
|
20
|
+
export declare const FLING_VELOCITY = 0.5;
|
|
21
|
+
export declare const FLING_MIN_DISTANCE = 40;
|
|
22
|
+
/**
|
|
23
|
+
* A horizontal swipe by one finger, for the touch screens the app's phone layout is for: mouse and pen are left alone.
|
|
24
|
+
*
|
|
25
|
+
* Spread the handlers on the element and give it `touch-action: pan-y` (`style`), so the browser keeps scrolling vertically and leaves
|
|
26
|
+
* the horizontal movement to this. A gesture that starts out mostly vertical is a scroll and is never a swipe, however far it later
|
|
27
|
+
* strays sideways; one that starts out horizontal is a swipe, and stays one.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useSwipe({ enabled, directions, distance, onDrag, onSwipe }: UseSwipeOptions): {
|
|
30
|
+
handlers: {
|
|
31
|
+
onTouchStart: (event: TouchEvent<HTMLElement>) => void;
|
|
32
|
+
onTouchMove: (event: TouchEvent<HTMLElement>) => void;
|
|
33
|
+
onTouchEnd: () => void;
|
|
34
|
+
onTouchCancel: () => void;
|
|
35
|
+
} | {
|
|
36
|
+
onTouchStart?: undefined;
|
|
37
|
+
onTouchMove?: undefined;
|
|
38
|
+
onTouchEnd?: undefined;
|
|
39
|
+
onTouchCancel?: undefined;
|
|
40
|
+
};
|
|
41
|
+
/** Leaves vertical panning to the browser. */
|
|
42
|
+
style: {
|
|
43
|
+
readonly touchAction: "pan-y";
|
|
44
|
+
} | undefined;
|
|
45
|
+
};
|