A window snapped to half the screen (640-800 px) or a 1366x768 laptop
at 150 % (911 px) got the phone and tablet layout: bottom tab bar,
folders in a drawer, Settings as a phone list with a Back arrow. The
layout switched at the 64rem (1024 px) breakpoint.
Now the phone layout starts below 600 px. From 600 to 1024 px the app
is a compact desktop: app rail at the side, folder sidebar folded to
its icons (the existing collapsed state, opened per session with its
toggle), Settings with its two panes and a narrower page list. Below
840 px the split layout opens a message in the list's place, as the
focused layout does. This holds in the browser and the desktop app,
in the Pro shell (which now runs from 600 px) and under Mountain View.
- `hooks/use-media-query.ts`: classifyWidth, PHONE_BELOW 600,
FULL_DESKTOP 1024; the tablet class (`isTablet`) is gone, the store
has `isCompact` instead. Plugins still see "tablet" for it.
- `app/globals.css`: a `desk:` breakpoint at 37.5rem for the layout
classes, and a `tap:` variant for 44px rows on phones and touch
screens below 1024 px (was every window below 1024 px).
- Touch screens below 1024 px keep swiping list rows (was phones only).
- The boot skeletons, boot script, mail app, sidebar, reader, admin
shell and calendar, contacts and files follow the new widths.
Tests changed with the behaviour: the tablet cases in pro-gate,
mail-app-mv-composer-move, mail-app-compose-handoff,
mail-app-thread-expand-mark-read, mobile-tour-end and
thread-list-item-columns now run on a phone or a compact desktop, as
those widths no longer exist as a tablet. mail-app-short-window-tab-bar
is replaced by mail-app-compact-layout: a phone on its side is a
compact desktop with the rail at the side, so the short-window tab bar
rule is gone. Class and media-query assertions follow desk:/tap: and
37.5rem; the 32px-text test uses 1100 px, since 1440 px at 32px is now
a compact desktop.
The desktop app and an installed PWA had a row across the whole window
with the title and the window controls. That row is gone. The rail,
sidebar, list and reader run to the top of the window in their own
colours, and the controls sit in the top row of whatever is rightmost.
- `lib/window-controls-overlay.ts` finds and marks those rows before
paint, and they reserve the controls' width through
`--titlebar-right-inset`
- the rows and the rail are the drag area, everything clickable is
`no-drag`, and an invisible band lets a page with no row still move
- settings and the admin shell get a row only under the overlay, Pro's
tab strip and Mountain View's bar match the controls' height
- Alt floats the menu over the top left, in `window-menu-bar.tsx`
- skeletons carry the same marks, so nothing shifts when the app loads
An installed app had no title bar of its own, no way to save attachments
except a zip, and "pop out" stayed inside the page. A new window was a
second copy of the app that had to restore the session itself, and on
start-up that evicts any login made without "Remember me" from the list
every window shares.
- Title bar: `display_override` with `window-controls-overlay`, one
`--titlebar-inset` variable at the page roots and skeletons, and a drag
strip with the window title. 0 without an overlay, so nothing moves.
- Save all to a folder: `showDirectoryPicker`, asking the device policy
before the picker and before each file, with the zip as the fallback.
- Pop-out routes `/popout/message/<id>` and `/popout/compose`, on offer in
an installed app or when the desktop bridge has windows. Hidden in Lite.
- A pop-out borrows the main window's session and never evicts or starts a
token timer. Every renewal now takes a Web Lock, so two windows can't
send the same refresh token, which Stalwart 1.0 may treat as a replay.
- A draft moves to its window under the same id. A closing window hands
unsaved text to the main window to save, since its own request wouldn't
survive the close.
- Read state, drafts, theme and language follow across windows, and
`html[data-desktop]` gates the app-like behaviour in `app/desktop.css`.
Review of the calendar store found writes that went missing, went out
wrong, or appeared twice.
- Writes with different scheduling flags stay separate and go out in
order. Merging them collapsed the two steps of an occurrence change, so
attendees who had answered got no mail, and "don't notify" went out to
everyone with a later RSVP.
- An edit made after a create's lost response is sent as an update. An
import of a uid the server has updates what differs.
- `createCalendarEvent`, `batchCreateCalendarEvents` and
`createCalendarTask` choose a uid before the first send, so a lost
answer online doesn't duplicate.
- The client resolves stand-in ids before the network, which covers an
open dialog once the server is back.
- A stale whole `recurrenceOverrides` map no longer erases overrides.
- A 4xx refuses that write and the queue goes on. `serverFail`,
`rateLimit` or an answer with no verdict retry instead of deleting.
- "This and all following" is refused offline with a clear message.
- A full list only drops what the server says it doesn't have. Sync adopts
a stand-in by uid. Reminder keys survive a stand-in becoming real.
Accounts the session no longer lists are forgotten.
Changed tests: the one asserting a sticky scheduling flag is gone, it
locked in the bug. Reminder keys now use the account and uid. Two flush
assertions drop `sent: 1`, because sync reads a lost write back first. The
two-account id test uses a listed account, as unlisted ones are removed.
The installed app only handled mailto: and webcal: launches. The launch
consumer dropped files, and nothing could hand a File to the composer or
the import dialogs, because the pending mailto is JSON in sessionStorage.
- manifest: `shortcuts` named in the browser's language, `file_handlers`
for .eml, .ics and .vcf, and `share_target`, all with the base path and
per-domain branding. Lite's manifest is unchanged.
- `lib/launch-handoff.ts`: the one place files and links wait, in memory
and in the Cache API, so they survive a sign-in that leaves the page.
The desktop bridge can call the same functions, since launchQueue never
fires in Electron. Sign-out clears it.
- launch consumer: routes files by type, and takes a shortcut to an open
window, which focus-existing would otherwise only focus.
- .eml opens in a new host for the preview, also before sign-in, with the
strictest device policy deciding about saving. .ics and .vcf open their
import dialogs through a new `initialFile` prop. A .ics needs the server,
so offline the dialog says so and carries on once back online.
- composer: `initialFiles` goes through `addFiles`, so plugin vetoes and
size limits still apply, also from a Pro compose tab.
- `sw.js` takes the share POST before the offline check and keeps the files
for the page. Without a worker, `/protocol/share` passes text and links
on and the page says the files are lost. `protocol` joins the app
sections and the handler pages join the shell, so a mailto: launch
offline no longer gets a network error.
Browsers without launchQueue or a service worker behave as before.
- The button and the panel of Calendar, Contacts, Files and Settings
show only when the plugin's shouldShow says yes, so a plugin switched
off for these views (admin, user or no service) leaves no button that
opens an empty panel, and no extra rows in Contacts and Settings.
- Files: while the details are open the panel steps aside there only;
the other views keep it as the user left it. With both stored open the
details fold when the page loads.
- Resizing stops where the view keeps its 480 px, so a drag or the arrow
keys no longer fold the panel away (and the handle keeps the focus).
- Settings and Contacts render the same element with or without the
panel, so a form keeps what was typed when the panel starts docking.
- Contacts on a phone: the panel button sits over an open contact too.
- The panel buttons of the reader and the views have one name, "Plugin
panel", with aria-pressed, and name the plugin's panel shortcut in
their title; the open panels are named landmarks.
The reader tests look the button up by its new name and pressed state
(viewer-toolbar-over-side-panel, reader-plugin-surfaces,
viewer-plugin-panel-width), since "Show/Hide plugin panel" is gone.
A frame slot `calendar-task-view` draws the task view below Bulwark's
toolbar in place of its list (props: the toolbar's filter, the selected
calendars); `ui.openCalendar({ view })` opens a calendar view and
`ui.openTask(id)` a task in Bulwark's editor.