The day header in the calendar month view was rendered outside the vertical scroll container, causing each header column to divide the full container width (100%) while the day grid cells below divided the remaining width excluding the vertical scrollbar. On platforms with classic scrollbars, this caused an escalating misalignment between the header columns and the day cells.
Move the day header inside the scroll container as a sticky row (sticky top-0 z-50 bg-background), matching the pattern established in the week and day views, and account for the header height in row minimum height, scrollToFocus, and sampleVisibleMonth.
The sidebar showed the same people icon for any calendar with a share,
labelled "Share calendar", and the settings badge only gave a count, so
there was no way to tell from either who could see the calendar. A share
granted to a service account read like a share with other people.
Both now carry a hint listing the recipients ("Shared with
bot@example.com (Booking robot)"), resolved from the principals the
server lists. Until every recipient is known the hint falls back to the
count. Principals are fetched only when a calendar on screen is shared.
The per-client principal cache of the attendee availability check moves
to lib/jmap/principal-cache.ts so both use it. It no longer keeps an
empty or failed answer: the next caller asks again instead of seeing
no principals for the rest of the session.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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
Each day is now one row: the date on the left (a large day number,
today in a filled circle, with the short weekday and month and a
Today or Tomorrow label) and its events beside it. Every event shows
its time range in a column of its own, then the calendar dot and the
title, with location and calendar on one muted line below. The grey
day header bars are gone.
On wide screens a month row keeps its height instead of growing for
the busiest day. A day with more entries than fit shows the first ones
and a "+N more" link that opens the day view, and clicking a date
number opens that day too. Today keeps the filled circle and the
selected day is tinted instead of looking the same. Phones still grow
the row to fit and open a day by tapping the cell.
On wide screens a click on an event now opens its card next to it
instead of the side panel. The card stays until it is closed and has
edit, delete, more and close buttons at the top, a square in the
event's colour beside the title, and a line naming the calendar.
More opens the same menu as a right-click (duplicate, export, copy).
Delete asks for confirmation inside the card, and edit goes straight
to the form. Hover previews still work as before while no card is
open. Phones keep opening the full editor.
Clicking or dragging over empty time in the week or day view used to
open the full event panel. On wide screens it now draws the new event
at that spot as a "(No title)" block and opens a small editor beside
it with a title field, the picked time, a calendar picker, Save and
"More options". More options carries the title, time and calendar
over into the full editor. Enter saves, Escape or a click elsewhere
discards. Phones keep opening the full editor.
The placeholder for an event being created is now drawn like a saved
event, in the calendar's colour, instead of a dashed outline.
The week and day headers now show a small weekday above a large date,
with today's date in a filled circle and its weekday in the primary
colour. Clicking a date in the week header opens that day in the day
view. The all-day row moved below the dates, where it lines up with
the columns it belongs to.
The current time line is thicker with a larger dot at the start of
today's column, and events shorter than 45 minutes show their start
time next to the title instead of hiding it.
The calendar list in the sidebar now works like Google Calendar's:
- "My calendars" and a new "Other calendars" section (shared
calendars, still grouped by owner) fold away, and stay folded
- each calendar has a checkbox filled in its colour with a check mark
- hovering a calendar shows an options button that opens the same
menu as a right-click
In the mini month, today is the filled circle and the selected day is
tinted, so the two no longer look alike.
The previous commit copied Google Calendar's look along with its
layout. The bar and the sidebar now use the webmail's regular buttons,
sizes and title style again; only the arrangement stays: menu button,
Today and arrows with the title, the view menu on the right and the
create button at the top of the sidebar.
The bar now spans the sidebar and the grid. It starts with a menu
button that hides or shows the sidebar (remembered across visits),
then a pill Today button, the arrows and a large period title. On
the right a view menu lists Day, Week, Month and Agenda (and Tasks)
with their shortcut keys, and import and subscribe moved into a
"more actions" menu instead of a button of their own.
The Create button moved to the top of the sidebar. While the sidebar
is hidden or an overlay, a compact create button sits in the bar.
The hook ran before the modal entered its saving state, so Save and
Ctrl+Enter stayed live while a plugin handler was busy. Each extra press
created another event with a fresh uid and sent another invitation.
- mark the modal as saving before the hook runs
- pass uid and attendees to the conflict-check view too
- don't send a null meeting link when creating an event
Plugins rendering in calendar-event-actions could never see the event's
uid or participants, so a meeting plugin minted a fresh room on every
click and could never propagate attendee or time changes; and of the
declared calendar hooks, none ever fired — the form-save bus existed but
had no emit site anywhere.
- calendar-event-actions eventData gains uid (undefined for new drafts)
and attendees (email list), so plugins can key meetings off the event
and fan out to participants.
- onCalendarEventFormSave now fires in handleSave as a transform:
handlers receive the accumulated patch plus the form view and may
return a patch (currently { virtualLocation }) merged into the saved
event — a meeting plugin refreshes its link in the same save that
sends scheduling messages, so attendees always get the current URL.
- Both buses are typed; contract tests cover the patch flow.
Testing: vitest plugin-hooks suite (41 passing), tsc clean, eslint clean
on touched files.
Calendar, contacts, files and settings still drew the left navigation
rail under Mountain View, so moving away from mail switched to the
default frame. They now show the Mountain View top bar too, without the
mail search and the menu button, which only make sense in mail.
Only the mail shell updated document.title, so every other surface sat at the
static value the root layout renders: the tab could not tell you where you
were, and on a multi-account setup it could not tell you whose mailbox you
were in either.
Each surface now sets "<screen> - <account> - <app>" through the shared
useDocumentTitle hook. Labels reuse the sidebar navigation translations, so
the tab and the rail never disagree about what a screen is called - no new
translation keys.
Refs #1159
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The expand/collapse toggle referenced calendar.events.show_more and
show_less, which were missing from every locale (translations test failed),
and passed a defaultMessage option next-intl does not support. Add both keys
to all 27 locales, reusing each locale's existing email_composer.show_less
wording, and drop the unsupported option.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The all-day/tasks area in the week view took its height from
(allDayRowCount + taskRowCount) across every loaded day. Since free
scrolling loads 37 to 180 days, one crowded day anywhere in that range
blew the header up to 250+ px for every week and pushed the time grid down;
tasks were also stacked below the global all-day row count, leaving a gap
on days without all-day events.
- Stack each day's tasks directly below that day's all-day events.
- Size the area from the days in view only: track the first visible day by
date (so columns prepended while loading do not shift it) plus the number
of visible columns (7 on desktop, by width on mobile).
- Cap the area at 3 rows with an expand/collapse toggle when a visible day
has more entries.
Tests cover per-day task stacking, the 3-row cap and toggle, an empty week
next to a crowded one, and the area growing when scrolling into it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>