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.
The formatting toolbar sticks to the top of the composer while the body
scrolls. Its background was bg-muted/30 with a backdrop blur, and in a
browser that skips backdrop-filter the body text showed sharp under the
buttons. The toolbar now uses the colour bg-muted/30 gives over the
page, made opaque: it looks the same at rest and hides the text when
pinned.
The browser reads document.title back with its whitespace stripped and
collapsed, so a title with two spaces in a row, a tab or a space at the
edge never reads back as written. useDocumentTitle put its title back on
every <head> mutation whenever the two differed, and its own write was
such a mutation: an endless loop that froze the tab at full CPU, for
example on opening a message whose subject has two spaces in a row. It
now also accepts what the browser read back after its own write.
Switching folders kept the previous folder's mail under a spinner until
the new page arrived, and a failed load emptied the folder.
The store now remembers the first page of each plain folder it showed
(in memory, 30 folders, keyed by login, shared owner, folder, category
filter and sort). Opening a remembered folder paints those rows straight
away and fetches in the background; the fresh page replaces them, and a
failure leaves them up with the error. What happens to the list on screen
- a message read, archived, deleted - is remembered with it.
Opening a message puts its subject in the browser-tab title. The title does
not stay in the tab strip: browsers keep it in history and in the
session-restore list, and screen shares show it. For mailboxes that hold
other people's correspondence that is a leak nobody audits.
A new feature gate, tabTitleSubjectEnabled ("Subject in Tab Title" under
Admin -> Policy), is on by default, so nothing changes for existing
installs. With it off, the title names the folder instead, so the tab still
says where you are. It is an admin gate rather than a user setting: whose
mail the boxes hold is a deployment decision.
The gate counts as off until the policy has loaded, so a subject never
slips into history while the setting is still unknown.
Refs #1159
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 tab title was "<context> - <app>", so two mailboxes open side by side
read the same in the tab strip and in the browser's session-restore list.
It is now "<context> - <account> - <app>", the order Gmail and Outlook Web
use: browsers cut a long title from the end, so the part that changes comes
first and the app name last. There is no setting for it; the account is what
tells the tabs apart.
The formatting, and the mail view's choice of context (composer, then
message, then mailbox), live in lib/tab-title.ts with tests. The mail view
sets its title through useDocumentTitle(), which other surfaces can use too.
Next renders the root layout's metadata <title> again on every client-side
navigation, after the surface has mounted; the hook puts its title back when
that happens. Only the surface that set the title last does so, so two
titles never fight.
Refs #1159
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tailwind v4's preflight no longer gives buttons a pointer cursor, so most
buttons, menu items, tabs, switches and selects showed the arrow. A base-layer
rule in globals.css gives them the pointer again and gives disabled ones
not-allowed. Cursor utilities on components still take precedence.
On a branded deployment (APP_NAME, APP_LOGO_*) three places after sign-in
still showed Bulwark or Stalwart (#1160):
- the TOTP issuer was 'Stalwart', so the authenticator app listed the
mail server instead of the service; it is now the configured app name,
- the About card used the Bulwark wordmark and title; with APP_NAME set
it shows the configured name and logo (light/dark as configured), and
keeps the Bulwark mark otherwise,
- the Files settings preview thumbnail used the bundled logo; it now uses
the configured one when there is one.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>