@rapidmx/web-client 0.14.0 → 0.15.1
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/apps/admin/domains/[uid].tsx +3 -2
- package/apps/shared/appearance/AppearanceForm.tsx +9 -1
- package/apps/shared/calendar/bookingPlugin.ts +100 -0
- package/apps/shared/calendar/calendarLiveUpdates.ts +27 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +37 -5
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +3 -1
- package/apps/shared/components/admin/settings/PluginsManager.tsx +1545 -1491
- 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 +640 -0
- package/apps/shared/components/calendar/EventExpandedForm.tsx +210 -0
- package/apps/shared/components/calendar/EventFormParts.tsx +622 -0
- package/apps/shared/components/calendar/EventModal.tsx +126 -862
- 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/LazyDescriptionEditor.tsx +70 -0
- package/apps/shared/components/calendar/MonthView.tsx +15 -3
- 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 +10 -5
- package/apps/shared/components/calendar/RequestChangeForm.tsx +271 -0
- package/apps/shared/components/calendar/SplitDayView.tsx +6 -5
- package/apps/shared/components/calendar/TaskQuickForm.tsx +289 -0
- package/apps/shared/components/calendar/TimeGridView.tsx +8 -6
- 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 +183 -0
- package/apps/shared/components/calendar/findTimeGrid.ts +90 -0
- package/apps/shared/components/calendar/swipeNavigation.ts +14 -0
- package/apps/shared/components/layout/UserMenu.tsx +80 -28
- package/apps/shared/components/mail/ConversationList.tsx +100 -48
- package/apps/shared/components/mail/ConversationThreadPane.tsx +490 -486
- package/apps/shared/components/mail/InviteCard.tsx +387 -0
- package/apps/shared/components/mail/MessageDetailPane.tsx +24 -2
- package/apps/shared/components/mail/SwipeRow.tsx +120 -0
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +7 -3
- 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/MailShell.tsx +522 -490
- package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
- 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/mail/newMailNotifications.ts +171 -183
- package/apps/shared/mail/useNewMailNotifications.ts +178 -178
- package/apps/shared/notifications/preferences.ts +36 -0
- package/apps/shared/notifications/store.ts +560 -551
- package/apps/www/_routes.ts +1 -0
- package/apps/www/calendar/index.tsx +139 -37
- package/apps/www/index.tsx +273 -126
- package/apps/www/messages/[uid].tsx +31 -4
- package/apps/www/settings/profile/index.tsx +248 -0
- package/dist/apps/admin/domains/[uid].js +4 -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/settings/MailboxCreateForm.js +3 -1
- 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 +549 -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 +69 -0
- package/dist/apps/shared/components/calendar/EventFormParts.js +202 -0
- package/dist/apps/shared/components/calendar/EventModal.d.ts +26 -13
- package/dist/apps/shared/components/calendar/EventModal.js +57 -455
- 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/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 +138 -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 +67 -0
- package/dist/apps/shared/components/calendar/eventFormat.js +163 -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/swipeNavigation.d.ts +9 -0
- package/dist/apps/shared/components/calendar/swipeNavigation.js +8 -0
- package/dist/apps/shared/components/layout/UserMenu.d.ts +10 -8
- package/dist/apps/shared/components/layout/UserMenu.js +70 -23
- 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/InviteCard.d.ts +20 -0
- package/dist/apps/shared/components/mail/InviteCard.js +150 -0
- 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 +8 -3
- 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/MailShell.d.ts +5 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +14 -3
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
- 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/newMailNotifications.d.ts +3 -8
- package/dist/apps/shared/mail/newMailNotifications.js +3 -12
- 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/www/_routes.js +1 -0
- package/dist/apps/www/calendar/index.js +76 -6
- package/dist/apps/www/index.js +140 -39
- 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 +3 -3
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { HTMLAttributes, ReactNode, useContext, useState } from "react";
|
|
6
|
+
import { HiOutlineBars2, HiOutlineCalendarDays, HiOutlineClock, HiOutlineXMark } from "react-icons/hi2";
|
|
7
|
+
import { describeRecurrence } from "@rapidmx/react-shared/calendar/recurrence.js";
|
|
8
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
9
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
10
|
+
import { EventShellContext } from "./EventShell.js";
|
|
11
|
+
import { CalendarField, DateTimeControls, DescriptionRow, GuestsRow, IconRow, LocationRow, VideoConferencingRow } from "./EventFormParts.js";
|
|
12
|
+
import { EventFormController } from "./eventForm.js";
|
|
13
|
+
import { BUSY_STATUS_LABEL, VISIBILITY_LABEL, describeReminder, formatFormWhen, reminderOf } from "./eventFormat.js";
|
|
14
|
+
|
|
15
|
+
export interface EventQuickFormProps {
|
|
16
|
+
c: EventFormController;
|
|
17
|
+
/** "More options": the same form, larger. */
|
|
18
|
+
onExpand: () => void;
|
|
19
|
+
/** A new event's tab strip (Event, Task, Appointment schedule), drawn under the header, and the attributes that make the rows below it
|
|
20
|
+
* the selected tab's panel. */
|
|
21
|
+
tabs?: ReactNode;
|
|
22
|
+
tabPanelProps?: HTMLAttributes<HTMLDivElement>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The quick-create popover: a title, when (a click opens the date, time, zone and repeat controls in place), guests, video conferencing,
|
|
27
|
+
* a location, an "Add description" row (which opens the rich-text box in place) and which calendar - with the busy status, visibility and reminder it will have
|
|
28
|
+
* written under it - then "More options" and Save. Its
|
|
29
|
+
* values are `EventEditor`'s, shared with the full form.
|
|
30
|
+
*/
|
|
31
|
+
export default function EventQuickForm({ c, onExpand, tabs, tabPanelProps }: EventQuickFormProps) {
|
|
32
|
+
const { values } = c;
|
|
33
|
+
const { dragHandleProps } = useContext(EventShellContext);
|
|
34
|
+
const [timeOpen, setTimeOpen] = useState(false);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<form onSubmit={c.onSubmit} className="flex flex-col">
|
|
38
|
+
<div className="flex items-center justify-between px-2 pt-2">
|
|
39
|
+
{dragHandleProps ? (
|
|
40
|
+
<div {...dragHandleProps} aria-hidden="true" className="flex-1 h-7 flex items-center cursor-move text-text-muted select-none touch-none">
|
|
41
|
+
<HiOutlineBars2 size={20} />
|
|
42
|
+
</div>
|
|
43
|
+
) : (
|
|
44
|
+
<div className="flex-1" />
|
|
45
|
+
)}
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
aria-label="Close"
|
|
49
|
+
onClick={c.onCancel}
|
|
50
|
+
className="w-8 h-8 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text"
|
|
51
|
+
>
|
|
52
|
+
<HiOutlineXMark size={20} aria-hidden="true" />
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{tabs}
|
|
57
|
+
|
|
58
|
+
<div {...tabPanelProps} className="flex flex-col gap-3 px-5 pb-3">
|
|
59
|
+
{c.error && <Alert>{c.error}</Alert>}
|
|
60
|
+
|
|
61
|
+
<div className="pl-8">
|
|
62
|
+
<input
|
|
63
|
+
type="text"
|
|
64
|
+
aria-label="Title"
|
|
65
|
+
placeholder="Add title"
|
|
66
|
+
data-autofocus
|
|
67
|
+
className="w-full text-xl bg-transparent text-text border-0 border-b-2 border-primary/50 focus:border-primary focus:outline-none pb-1 placeholder:text-text-muted"
|
|
68
|
+
value={values.title}
|
|
69
|
+
onChange={(e) => c.update({ title: e.target.value })}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<IconRow icon={<HiOutlineClock size={20} />}>
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
aria-expanded={timeOpen}
|
|
77
|
+
onClick={() => setTimeOpen((open) => !open)}
|
|
78
|
+
className="w-full text-left rounded-md hover:bg-surface-alt -mx-1 px-1 py-0.5"
|
|
79
|
+
>
|
|
80
|
+
<span className="block text-sm">{formatFormWhen(values.start, values.end, values.allDay)}</span>
|
|
81
|
+
<span className="block text-xs text-text-muted">
|
|
82
|
+
{values.formZone.replace(/_/g, " ")} •{" "}
|
|
83
|
+
{values.recurrenceRule ? `Repeats ${describeRecurrence(values.recurrenceRule)}` : "Does not repeat"}
|
|
84
|
+
</span>
|
|
85
|
+
</button>
|
|
86
|
+
{timeOpen && (
|
|
87
|
+
<div className="mt-2">
|
|
88
|
+
<DateTimeControls c={c} />
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</IconRow>
|
|
92
|
+
|
|
93
|
+
<GuestsRow c={c} />
|
|
94
|
+
<VideoConferencingRow c={c} />
|
|
95
|
+
<LocationRow c={c} />
|
|
96
|
+
<DescriptionRow c={c} collapsible />
|
|
97
|
+
|
|
98
|
+
<IconRow icon={<HiOutlineCalendarDays size={20} />}>
|
|
99
|
+
<CalendarField c={c} />
|
|
100
|
+
<p className="text-xs text-text-muted mt-0.5">
|
|
101
|
+
{BUSY_STATUS_LABEL[values.busyStatus]} • {VISIBILITY_LABEL[values.visibility]} • {describeReminder(reminderOf(values.reminderMinutes))}
|
|
102
|
+
</p>
|
|
103
|
+
</IconRow>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div className="flex items-center justify-end gap-2 px-4 py-3">
|
|
107
|
+
<Button type="button" variant="text" className="!w-auto" onClick={onExpand}>
|
|
108
|
+
More options
|
|
109
|
+
</Button>
|
|
110
|
+
<Button type="submit" loading={c.saving} disabled={c.saving} className="!w-auto">
|
|
111
|
+
Save
|
|
112
|
+
</Button>
|
|
113
|
+
</div>
|
|
114
|
+
</form>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { createContext, ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
6
|
+
import { createPortal } from "react-dom";
|
|
7
|
+
import { OverlayDepthContext, useOverlayDialog } from "@rapidmx/react-shared/components/overlays/overlayStack.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Where a new event's quick-create popover hangs: the rectangle (viewport coordinates) of what was clicked - a time slot, a day cell, the
|
|
11
|
+
* "New event" button. `placement` says on which side it opens: beside the rectangle (`"side"`, the default: to its right, or to its left
|
|
12
|
+
* when there is no room) or under it (`"below"`).
|
|
13
|
+
*/
|
|
14
|
+
export interface EventAnchor {
|
|
15
|
+
left: number;
|
|
16
|
+
top: number;
|
|
17
|
+
right: number;
|
|
18
|
+
bottom: number;
|
|
19
|
+
placement?: "side" | "below";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The anchor for a clicked element (see `EventAnchor`). */
|
|
23
|
+
export function anchorOf(element: Element, placement: EventAnchor["placement"] = "side"): EventAnchor {
|
|
24
|
+
const { left, top, right, bottom } = element.getBoundingClientRect();
|
|
25
|
+
return { left, top, right, bottom, placement };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** How the shell is drawn: a popover next to `anchor`, a bottom sheet (a phone's quick create), or a centered card with a backdrop. */
|
|
29
|
+
export type EventShellVariant = "popover" | "sheet" | "card";
|
|
30
|
+
|
|
31
|
+
const VIEWPORT_MARGIN = 8;
|
|
32
|
+
const ANCHOR_GAP = 8;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Where a `width` x `height` popover goes for `anchor`, kept wholly inside the viewport (its margin on every side). With no anchor - the
|
|
36
|
+
* keyboard shortcut - it is centered, near the top.
|
|
37
|
+
*/
|
|
38
|
+
export function placePopover(
|
|
39
|
+
anchor: EventAnchor | undefined,
|
|
40
|
+
size: { width: number; height: number },
|
|
41
|
+
viewport: { width: number; height: number },
|
|
42
|
+
): { left: number; top: number } {
|
|
43
|
+
let left: number;
|
|
44
|
+
let top: number;
|
|
45
|
+
if (!anchor) {
|
|
46
|
+
left = (viewport.width - size.width) / 2;
|
|
47
|
+
top = viewport.height * 0.1;
|
|
48
|
+
} else if (anchor.placement === "below") {
|
|
49
|
+
left = anchor.left;
|
|
50
|
+
top = anchor.bottom + ANCHOR_GAP;
|
|
51
|
+
} else {
|
|
52
|
+
left = anchor.right + ANCHOR_GAP;
|
|
53
|
+
if (left + size.width > viewport.width - VIEWPORT_MARGIN) {
|
|
54
|
+
left = anchor.left - ANCHOR_GAP - size.width;
|
|
55
|
+
}
|
|
56
|
+
top = anchor.top;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
left: Math.max(VIEWPORT_MARGIN, Math.min(left, viewport.width - size.width - VIEWPORT_MARGIN)),
|
|
60
|
+
top: Math.max(VIEWPORT_MARGIN, Math.min(top, viewport.height - size.height - VIEWPORT_MARGIN)),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** What a form inside a popover uses to make its grip drag the popover. */
|
|
65
|
+
export interface EventShellContextValue {
|
|
66
|
+
dragHandleProps: { onPointerDown: (event: React.PointerEvent) => void } | undefined;
|
|
67
|
+
}
|
|
68
|
+
export const EventShellContext = createContext<EventShellContextValue>({ dragHandleProps: undefined });
|
|
69
|
+
|
|
70
|
+
export interface EventShellProps {
|
|
71
|
+
variant: EventShellVariant;
|
|
72
|
+
/** The dialog's accessible name. */
|
|
73
|
+
label: string;
|
|
74
|
+
/** Escape, the backdrop (unless `onBackdropPress` says otherwise) and the close buttons' shared way out. */
|
|
75
|
+
onClose: () => void;
|
|
76
|
+
/** What a press on the backdrop does, where it must not just close: a half-typed quick event is not thrown away by a stray click. */
|
|
77
|
+
onBackdropPress?: () => void;
|
|
78
|
+
anchor?: EventAnchor;
|
|
79
|
+
/** Names the face being shown (details, quick create, full form): when it changes, focus moves to the new face's `data-autofocus` element,
|
|
80
|
+
* or to the dialog itself when it has none. */
|
|
81
|
+
focusKey: string;
|
|
82
|
+
/** The widest the popover or card gets, in px. */
|
|
83
|
+
width: number;
|
|
84
|
+
children: ReactNode;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The frame every face of the event dialog shares (the read-only details, the quick-create popover, the full form), so that switching from
|
|
89
|
+
* one to another keeps the focus handling and - the point of drawing them all through one element tree - keeps a form's state when the quick
|
|
90
|
+
* popover grows into the full card. The tree is deliberately the same for every variant (backdrop, dialog, children): only classes and
|
|
91
|
+
* styles change, so React never remounts what is inside.
|
|
92
|
+
*
|
|
93
|
+
* Escape and the Tab focus trap come from the shared overlay stack (`useOverlayDialog`), as for `Modal`. A popover is placed beside its
|
|
94
|
+
* anchor (`placePopover()`), re-placed when its own height changes, and can be dragged by a grip that takes `EventShellContext`'s handle.
|
|
95
|
+
*/
|
|
96
|
+
export default function EventShell({ variant, label, onClose, onBackdropPress, anchor, focusKey, width, children }: EventShellProps) {
|
|
97
|
+
const dialogRef = useRef<HTMLDivElement>(null);
|
|
98
|
+
const depth = useOverlayDialog(true, dialogRef, onClose);
|
|
99
|
+
// How far the grip has dragged the popover from where it was placed.
|
|
100
|
+
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
|
101
|
+
const offsetRef = useRef(offset);
|
|
102
|
+
offsetRef.current = offset;
|
|
103
|
+
const dragStart = useRef<{ x: number; y: number; from: { x: number; y: number } } | null>(null);
|
|
104
|
+
|
|
105
|
+
const place = useCallback(() => {
|
|
106
|
+
const dialog = dialogRef.current;
|
|
107
|
+
// A resize reported after the dialog has gone (the popover closes as soon as its event is saved) has nothing left to place.
|
|
108
|
+
if (!dialog) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (variant !== "popover") {
|
|
112
|
+
dialog.style.left = "";
|
|
113
|
+
dialog.style.top = "";
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const size = { width: dialog.offsetWidth, height: dialog.offsetHeight };
|
|
117
|
+
const viewport = { width: window.innerWidth, height: window.innerHeight };
|
|
118
|
+
const base = placePopover(anchor, size, viewport);
|
|
119
|
+
const left = Math.max(VIEWPORT_MARGIN, Math.min(base.left + offsetRef.current.x, viewport.width - size.width - VIEWPORT_MARGIN));
|
|
120
|
+
const top = Math.max(VIEWPORT_MARGIN, Math.min(base.top + offsetRef.current.y, viewport.height - size.height - VIEWPORT_MARGIN));
|
|
121
|
+
dialog.style.left = `${Math.round(left)}px`;
|
|
122
|
+
dialog.style.top = `${Math.round(top)}px`;
|
|
123
|
+
}, [anchor, variant]);
|
|
124
|
+
|
|
125
|
+
useLayoutEffect(() => {
|
|
126
|
+
place();
|
|
127
|
+
}, [place, offset]);
|
|
128
|
+
|
|
129
|
+
// Declared after `useOverlayDialog()`'s own effect, which focuses the dialog itself: the face's first field wins over that.
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
(dialogRef.current!.querySelector<HTMLElement>("[data-autofocus]") ?? dialogRef.current!).focus();
|
|
132
|
+
}, [focusKey]);
|
|
133
|
+
|
|
134
|
+
// The popover grows and shrinks with its rows (the time controls expanding, a guest added): keep it inside the window.
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (variant !== "popover" || typeof ResizeObserver === "undefined") {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const observer = new ResizeObserver(() => place());
|
|
140
|
+
observer.observe(dialogRef.current!);
|
|
141
|
+
return () => observer.disconnect();
|
|
142
|
+
}, [place, variant]);
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
function handleMove(event: PointerEvent) {
|
|
146
|
+
const start = dragStart.current;
|
|
147
|
+
if (start) {
|
|
148
|
+
setOffset({ x: start.from.x + event.clientX - start.x, y: start.from.y + event.clientY - start.y });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function handleUp() {
|
|
152
|
+
dragStart.current = null;
|
|
153
|
+
}
|
|
154
|
+
window.addEventListener("pointermove", handleMove);
|
|
155
|
+
window.addEventListener("pointerup", handleUp);
|
|
156
|
+
return () => {
|
|
157
|
+
window.removeEventListener("pointermove", handleMove);
|
|
158
|
+
window.removeEventListener("pointerup", handleUp);
|
|
159
|
+
};
|
|
160
|
+
}, []);
|
|
161
|
+
|
|
162
|
+
const context: EventShellContextValue = {
|
|
163
|
+
dragHandleProps:
|
|
164
|
+
variant === "popover"
|
|
165
|
+
? {
|
|
166
|
+
onPointerDown: (event) => {
|
|
167
|
+
dragStart.current = { x: event.clientX, y: event.clientY, from: offsetRef.current };
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
: undefined,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const backdropClass =
|
|
174
|
+
variant === "card"
|
|
175
|
+
? "fixed inset-0 bg-black/50 flex items-center justify-center p-3 md:p-5 z-[1000]"
|
|
176
|
+
: variant === "sheet"
|
|
177
|
+
? "fixed inset-0 bg-black/50 flex items-end z-[1000]"
|
|
178
|
+
: "fixed inset-0 z-[1000]";
|
|
179
|
+
const dialogClass =
|
|
180
|
+
variant === "card"
|
|
181
|
+
? "w-full max-h-[calc(100vh-1.5rem)] md:max-h-[calc(100vh-2.5rem)] overflow-y-auto bg-surface border border-border rounded-xl shadow-modal focus:outline-none"
|
|
182
|
+
: variant === "sheet"
|
|
183
|
+
? "w-full max-h-[90vh] overflow-y-auto bg-surface border-t border-border rounded-t-2xl shadow-modal focus:outline-none pb-[env(safe-area-inset-bottom)]"
|
|
184
|
+
: "fixed max-h-[calc(100vh-1rem)] overflow-y-auto bg-surface border border-border rounded-xl shadow-modal focus:outline-none";
|
|
185
|
+
const dialogStyle: React.CSSProperties =
|
|
186
|
+
variant === "popover" ? { width, maxWidth: `calc(100vw - ${VIEWPORT_MARGIN * 2}px)` } : variant === "card" ? { maxWidth: width } : {};
|
|
187
|
+
|
|
188
|
+
return createPortal(
|
|
189
|
+
<div
|
|
190
|
+
className={backdropClass}
|
|
191
|
+
data-event-shell={variant}
|
|
192
|
+
onMouseDown={(event) => event.target === event.currentTarget && (onBackdropPress ?? onClose)()}
|
|
193
|
+
>
|
|
194
|
+
<div ref={dialogRef} role="dialog" aria-modal="true" aria-label={label} tabIndex={-1} className={dialogClass} style={dialogStyle}>
|
|
195
|
+
<OverlayDepthContext.Provider value={depth}>
|
|
196
|
+
<EventShellContext.Provider value={context}>{children}</EventShellContext.Provider>
|
|
197
|
+
</OverlayDepthContext.Provider>
|
|
198
|
+
</div>
|
|
199
|
+
</div>,
|
|
200
|
+
document.body,
|
|
201
|
+
);
|
|
202
|
+
}
|