From f94f4fc605905816a1fb64916f3ef122dfe6f657 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:46:37 +0200 Subject: [PATCH] feat(calendar): match the calendar layout to the rest of the app The calendar bar spanned both the sidebar and the grid, and the sidebar opened with a full-width primary "Create event" button. Neither exists in mail or contacts, so the calendar looked like a different app. - the sidebar runs full height with a title row and a "+" button, like the contacts sidebar - the view bar sits over the grid only and uses the density header padding, so its border lines up with the sidebar header - calendar rows and section headers follow the contacts sidebar: full width 32px rows, chevron on the left --- components/calendar/calendar-app.tsx | 26 +++++------- .../calendar/calendar-sidebar-header.tsx | 39 ++++++++++++++++++ .../calendar/calendar-sidebar-panel.tsx | 40 ++++++++++--------- components/calendar/calendar-toolbar.tsx | 2 +- 4 files changed, 72 insertions(+), 35 deletions(-) create mode 100644 components/calendar/calendar-sidebar-header.tsx diff --git a/components/calendar/calendar-app.tsx b/components/calendar/calendar-app.tsx index e7f982bab..8edb59548 100644 --- a/components/calendar/calendar-app.tsx +++ b/components/calendar/calendar-app.tsx @@ -75,6 +75,7 @@ import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { useConfirmDialog } from "@/hooks/use-confirm-dialog"; import { SchedulingDeniedError } from "@/lib/jmap/scheduling-error"; import { CreateCalendarModal } from "@/components/calendar/create-calendar-modal"; +import { CalendarSidebarHeader } from "@/components/calendar/calendar-sidebar-header"; import { getUserParticipantId, collectUserCalendarAddresses } from "@/lib/calendar-participants"; import { generateBirthdayEvents, createBirthdayCalendar, BIRTHDAY_CALENDAR_ID } from "@/lib/birthday-calendar"; import { sharedCalendarColorKey, pickUnusedCalendarColor } from "@/lib/shared-calendar-colors"; @@ -1774,15 +1775,13 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = {!inlineApp && (
- {/* Desktop: the bar spans the sidebar and the grid, as in Google Calendar. */} - {!isMobile && calendarToolbar}
{/* Sidebar - in-flow when desktop pane, overlay when narrow */} {(isNarrow || !sidebarCollapsed) && ( <>
- {!isMobile && ( - - )} + { setNarrowSidebarOpen(false); openCreateModal(); }} + tourTarget={!isNarrow} + /> +
+
+
1} /> +
{!isNarrow && ( - {isMobile && calendarToolbar} + {calendarToolbar} {/* isolate keeps the views' sticky headers (z-50) below the toolbar's import dropdown, which overlaps this area (#1049). */} diff --git a/components/calendar/calendar-sidebar-header.tsx b/components/calendar/calendar-sidebar-header.tsx new file mode 100644 index 000000000..79272d9aa --- /dev/null +++ b/components/calendar/calendar-sidebar-header.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { Button } from "@/components/ui/button"; +import { Plus } from "@/components/icons"; + +interface CalendarSidebarHeaderProps { + onCreateEvent: () => void; + /** Mark the button for the guided tour; off while the toolbar's own "+" is the visible one. */ + tourTarget?: boolean; +} + +/** + * Title row at the top of the calendar sidebar, the same shape as the + * contacts sidebar header: the app name on the left and "+" on the right. + */ +export function CalendarSidebarHeader({ onCreateEvent, tourTarget = true }: CalendarSidebarHeaderProps) { + const t = useTranslations("calendar"); + + return ( +
+ {t("title")} + +
+ ); +} diff --git a/components/calendar/calendar-sidebar-panel.tsx b/components/calendar/calendar-sidebar-panel.tsx index 37b6f6abc..e6b798f45 100644 --- a/components/calendar/calendar-sidebar-panel.tsx +++ b/components/calendar/calendar-sidebar-panel.tsx @@ -255,10 +255,11 @@ export function CalendarSidebarPanel({ data-account={cal.accountName ?? ''} data-visible={isVisible} className={cn( - "flex items-center gap-2 w-full px-1.5 py-1 rounded-md text-sm transition-colors duration-150", + "flex items-center gap-2 w-full ps-5 pe-3 text-sm text-foreground/80 transition-colors duration-150", "hover:bg-muted", - hasMenu && "pe-7" + hasMenu && "pe-9" )} + style={{ paddingBlock: 'var(--density-sidebar-py, 4px)', minHeight: '32px' }} > {/* A checkbox filled in the calendar colour, empty when hidden. */} openContextMenu(e, cal)} className={cn( - "absolute end-1 top-1/2 -translate-y-1/2 p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-accent transition-opacity", + "absolute end-2 top-1/2 -translate-y-1/2 p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-accent transition-opacity", "opacity-0 group-hover/cal:opacity-100 focus-visible:opacity-100", contextMenu.isOpen && contextMenu.data?.id === cal.id && "opacity-100" )} @@ -424,19 +425,19 @@ export function CalendarSidebarPanel({ const renderSectionHeader = (key: string, label: string, onAdd?: () => void) => { const expanded = !collapsedAccountGroups.has(key); return ( -
+
{onAdd && ( {expanded && ( -
+
{owned.length > 0 && (
-
+
{t('my_calendars')}
-
+
{owned.map(renderCalendarItem)}
)} {sharedGroups.map((sg) => (
-
+
{sg.label}
-
+
{sg.calendars.map(renderCalendarItem)}
@@ -560,15 +562,15 @@ export function CalendarSidebarPanel({ )} {sharedAccountGroups.length > 0 && ( -
+
{renderSectionHeader('__others__', t('other_calendars'))} {!collapsedAccountGroups.has('__others__') && sharedAccountGroups.map((group) => (
-
+
{group.accountName}
-
+
{group.calendars.map(renderCalendarItem)}
diff --git a/components/calendar/calendar-toolbar.tsx b/components/calendar/calendar-toolbar.tsx index 197a4ebc7..99eba0c23 100644 --- a/components/calendar/calendar-toolbar.tsx +++ b/components/calendar/calendar-toolbar.tsx @@ -332,7 +332,7 @@ export function CalendarToolbar({ const currentViewLabel = t(`views.${viewMode}`); return ( -
+
{onMenuClick && (