diff --git a/components/calendar/calendar-app.tsx b/components/calendar/calendar-app.tsx index 84380754d..d2e4f57ad 100644 --- a/components/calendar/calendar-app.tsx +++ b/components/calendar/calendar-app.tsx @@ -189,11 +189,17 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = const [pendingScopeAction, setPendingScopeAction] = useState(null); const [detailEvent, setDetailEvent] = useState(null); const [detailAnchorRect, setDetailAnchorRect] = useState(null); + // A clicked event's card stays until closed; a hover preview goes when + // the pointer leaves. Held in a ref too for the hover timers. + const [detailPinned, setDetailPinned] = useState(false); + const detailPinnedRef = useRef(false); + useEffect(() => { detailPinnedRef.current = detailPinned; }, [detailPinned]); const [pendingPreview, setPendingPreview] = useState(null); // Quick create (#calendar redesign): an empty slot clicked in the week or // day view opens a small editor beside a placeholder block. const [quickDraft, setQuickDraft] = useState(null); const [defaultModalTitle, setDefaultModalTitle] = useState(); + const [modalStartsInEdit, setModalStartsInEdit] = useState(false); const [showTaskModal, setShowTaskModal] = useState(false); const [editTask, setEditTask] = useState(null); const [mobileReturnToMonth, setMobileReturnToMonth] = useState(false); @@ -618,7 +624,8 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = setShowEventModal(true); }, [selectedDate, setSelectedDate]); - const openEditModal = useCallback((event: CalendarEvent) => { + const openEditModal = useCallback((event: CalendarEvent, startInForm = false) => { + setModalStartsInEdit(startInForm); setEditEvent(event); setDefaultModalDate(undefined); setShowTaskModal(false); @@ -675,15 +682,27 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = const closeDetail = useCallback(() => { if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; } + detailPinnedRef.current = false; + setDetailPinned(false); setDetailEvent(null); setDetailAnchorRect(null); }, []); - const handleSelectEvent = useCallback((event: CalendarEvent, _anchorRect: DOMRect) => { - // Click opens the sidebar for viewing/editing - closeDetail(); - openEditModal(event); - }, [closeDetail, openEditModal]); + const handleSelectEvent = useCallback((event: CalendarEvent, anchorRect: DOMRect) => { + // Phones open the full editor; wide screens show the event's card next + // to it, with edit and delete at the top (as in Google Calendar). + if (isMobile) { + closeDetail(); + openEditModal(event); + return; + } + if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; } + setQuickDraft(null); + detailPinnedRef.current = true; + setDetailPinned(true); + setDetailEvent(event); + setDetailAnchorRect(anchorRect); + }, [isMobile, closeDetail, openEditModal]); const { contextMenu: eventContextMenu, @@ -714,6 +733,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = const handleHoverEvent = useCallback((event: CalendarEvent, anchorRect: DOMRect) => { if (isMobile) return; + if (detailPinnedRef.current) return; if (calendarHoverPreview === 'off') return; if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; } // Don't show hover popover if the sidebar is already open for this event @@ -732,7 +752,9 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = const handleHoverLeave = useCallback(() => { if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; } + if (detailPinnedRef.current) return; hoverTimerRef.current = setTimeout(() => { + if (detailPinnedRef.current) return; setDetailEvent(null); setDetailAnchorRect(null); }, 300); @@ -822,7 +844,8 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = if (detailEvent) { const ev = detailEvent; closeDetail(); - openEditModal(ev); + // The pencil on the event card goes straight to the form. + openEditModal(ev, true); } }, [detailEvent, closeDetail, openEditModal]); @@ -983,8 +1006,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = setEditEvent(null); setShowTaskModal(false); setEditTask(null); - setDetailEvent(null); - setDetailAnchorRect(null); + closeDetail(); setQuickDraft((prev) => ({ start, end: finalEnd, @@ -993,7 +1015,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = ? prev.calendarId : preferred?.id ?? "", })); - }, [isMobile, openCreateModal, writableCalendars]); + }, [isMobile, openCreateModal, writableCalendars, closeDetail]); const closeQuickCreate = useCallback(() => setQuickDraft(null), []); @@ -1883,13 +1905,14 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = {!isMobile && showEventModal && (
handleContextMenuEvent(e, detailEvent)} onClose={closeDetail} onSaveNote={handleSaveNoteFromDetail} onRsvp={handleRsvpFromDetail} @@ -2032,13 +2056,14 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = {showEventModal && isMobile && ( void; onDelete: () => void; onDuplicate: () => void; + /** Opens the event's full action menu (export, copy, ...) at the click. */ + onMore?: (e: React.MouseEvent) => void; onClose: () => void; onSaveNote: (note: string) => void; onRsvp?: (status: CalendarParticipant["participationStatus"]) => void; @@ -44,6 +46,7 @@ interface EventDetailPopoverProps { } const POPOVER_WIDTH = 360; +const HEADER_BUTTON = "p-1.5 rounded-md hover:bg-muted transition-colors duration-150 text-muted-foreground hover:text-foreground"; const POPOVER_GAP = 8; const VIEWPORT_MARGIN = 12; const MAX_HEIGHT = 480; @@ -118,6 +121,7 @@ export function EventDetailPopover({ onEdit, onDelete, onDuplicate, + onMore, onClose, onSaveNote, onRsvp, @@ -324,49 +328,99 @@ export function EventDetailPopover({ visibility: position ? "visible" : "hidden", }} > - {/* Color accent bar */} -
- - {/* Header */} -
-
-
- -

- {event.title || t("events.no_title")} -

-
- {calendar && ( -

- {calendar.name} - {event.status === "tentative" && ( - - {t("detail.tentative")} - - )} - {event.status === "cancelled" && ( - - {t("detail.cancelled")} - - )} -

- )} -
+ {/* Actions sit at the top, the event's colour square beside its title. */} +
+ {canEditBody && ( + + )} + {canEditBody && ( + + )} +
+ {showDeleteConfirm && ( +
+ + {t("detail.delete_confirm")} + + + +
+ )} + +
+
+ {/* Content */}
)} + {/* Calendar */} + {calendar && ( +
+ + {calendar.name} +
+ )} + {/* Description */} {event.description && (
@@ -634,63 +696,6 @@ export function EventDetailPopover({
)} - {/* Action Bar */} -
- {showDeleteConfirm ? ( -
- - {t("form.delete_confirm")} - - - -
- ) : ( - <> - {canEditBody && ( - - )} - -
- {canEditBody && ( - - )} - - )} -
); diff --git a/components/calendar/event-modal.tsx b/components/calendar/event-modal.tsx index f15e2fae8..f4a2bd85e 100644 --- a/components/calendar/event-modal.tsx +++ b/components/calendar/event-modal.tsx @@ -49,6 +49,8 @@ interface EventModalProps { defaultDate?: Date; defaultEndDate?: Date; defaultAllDay?: boolean; + /** Open an existing event straight in the form instead of its summary. */ + startInEditMode?: boolean; /** Title to start a new event with (from the quick editor's "More options"). */ defaultTitle?: string; defaultCalendarId?: string; @@ -195,6 +197,7 @@ export function EventModal({ defaultEndDate, defaultAllDay, defaultTitle, + startInEditMode, defaultCalendarId, onSave, onDelete, @@ -220,7 +223,7 @@ export function EventModal({ : "fixed inset-0 z-50 flex flex-col bg-background"; const isEdit = !!event; const formatEventDate = useFormatEventDate(); - const [mode, setMode] = useState<"view" | "edit">(isEdit ? "view" : "edit"); + const [mode, setMode] = useState<"view" | "edit">(isEdit && !startInEditMode ? "view" : "edit"); const userIsOrganizer = useMemo(() => { if (!event) return true;