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>
A virtual location in an app scheme (msteams:, zoommtg:) is kept as a last
resort instead of hiding the meeting link; script schemes never reach the
href. A location only joins the meeting when the whole label is a service
name, so "Salle Teams" still opens the maps. Recognise Zoom's /wc/ join
path, and clear the copy-feedback timer on unmount.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Invitations - Teams above all - leave virtualLocations empty: LOCATION reads
"Microsoft Teams Meeting" and the join URL sits in the description between
help and dial-in links, so the popover showed no meeting link and "Copy
meeting link" was not offered. Find the join URL of known conferencing
services (Teams, Zoom, Meet, Webex, Jitsi, Whereby, GoTo) in the location
or description, unwrapping Outlook Safe Links. Nothing is written to the
event.
The location was plain text unless it was a URL. It is now always a link:
a URL opens as is, a location that only names the service joins the
meeting, anything else opens a Google Maps search (the Maps app on a
phone). A copy button sits next to it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From 0.16.21 Stalwart fails the whole CalendarEvent/set with
`forbidden` when the account may not send scheduling messages, so an
event with attendees was not saved at all and the calendar showed only
"Failed to save event".
Such a refusal now surfaces as SchedulingDeniedError, and the calendar
names the server's reason and offers to save the event without sending
the invitations.
Resolves the two conflicts in components/calendar/event-modal.tsx created
by #967 (RSVP controls), both keep-both:
- #967's canRsvp memo and #986's resolveContactName hook are both kept;
they are independent and were simply inserted at the same point.
- #967's RsvpBar component is kept, and StatusBadge takes #986's
signature ({ status, t }) since #986 drops the isOrganizer prop and
updates both call sites to match.
Since the freely scrolling views (#759) the day, week and month views are
positioned by their own focus rather than by the selected date, and a grid
click deliberately only marks a day that is already on screen. The mini
calendar shared that handler, so picking a day there highlighted it but
left the view where it was. Give it its own handler that jumps to the day
(keeping the mobile month-to-day switch), the same path the prev/next/today
controls and deep links use.
The calendar loads the principal's alias addresses so an alias-organised
event is recognised as the user's own. The fetch fired on mount, but after
a page load the JMAP client is restored asynchronously: the call failed
with "Not authenticated" in the console, and the once-only guard then
blocked the retry for the rest of the session, leaving the aliases empty.
Move the logic into useAccountPrincipalEmails, which waits for the client
and fetches once, and cover the sequence with a hook test.