mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 16:16:49 +00:00
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`.
49 lines
1.7 KiB
TypeScript
49 lines
1.7 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { applyTitlebarInset, watchTitlebarArea, type TitlebarArea } from "@/lib/window-controls-overlay";
|
|
|
|
/**
|
|
* The strip above the app in a window without a native title bar: the part
|
|
* the user drags the window by, with the window's title in small text. The
|
|
* native window controls sit next to it, outside the area it is given. The
|
|
* page roots leave this row empty through --titlebar-inset.
|
|
*
|
|
* Renders nothing, and changes nothing on the page, without an overlay. It
|
|
* repeats the title for sighted users only, so it stays out of the
|
|
* accessibility tree and out of the tab order.
|
|
*/
|
|
export function WindowTitleBar() {
|
|
const [area, setArea] = useState<TitlebarArea | null>(null);
|
|
const [title, setTitle] = useState("");
|
|
|
|
useEffect(() => watchTitlebarArea((next) => {
|
|
applyTitlebarInset(next);
|
|
setArea(next);
|
|
}), []);
|
|
|
|
const visible = area !== null;
|
|
useEffect(() => {
|
|
if (!visible) return;
|
|
const read = () => setTitle(document.title);
|
|
read();
|
|
// Each surface sets its own title, and Next puts <title> back on navigation.
|
|
const observer = new MutationObserver(read);
|
|
observer.observe(document.head, { childList: true, subtree: true, characterData: true });
|
|
return () => observer.disconnect();
|
|
}, [visible]);
|
|
|
|
if (!area) return null;
|
|
|
|
return (
|
|
<div
|
|
aria-hidden="true"
|
|
data-window-title-bar=""
|
|
className="window-title-bar fixed z-[100002] flex items-center bg-background ps-3 pe-3 text-xs text-muted-foreground select-none"
|
|
style={{ top: area.y, left: area.x, width: area.width, height: area.height }}
|
|
>
|
|
<span className="truncate">{title}</span>
|
|
</div>
|
|
);
|
|
}
|