Mail (phone): side panel as a drawer sliding in from the right
The reading view's toolbar gains the side-panel toggle on a phone too. It opens Calendar/People/Attachments as a right-hand drawer over a dimmed backdrop, closed by its X, the backdrop or Escape, and put away when the conversation changes. It keeps its own open state so the desktop's remembered panel never opens on a phone, and it mounts on first use only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -162,6 +162,16 @@ export function MailLayout() {
|
|||||||
useEffect(() => localStorage.setItem("panel.rightTab", rightTab), [rightTab]);
|
useEffect(() => localStorage.setItem("panel.rightTab", rightTab), [rightTab]);
|
||||||
useEffect(() => localStorage.setItem("panel.rightW", String(rightW)), [rightW]);
|
useEffect(() => localStorage.setItem("panel.rightW", String(rightW)), [rightW]);
|
||||||
const openPanel = (tab: PanelTab) => { setRightTab(tab); setRightOpen(true); };
|
const openPanel = (tab: PanelTab) => { setRightTab(tab); setRightOpen(true); };
|
||||||
|
// Phone: the same panel as a drawer sliding in from the right. Its own state,
|
||||||
|
// so the desktop's remembered "open" never springs it onto a phone screen.
|
||||||
|
// Mounted on first use only (the Calendar tab loads a whole plugin bundle),
|
||||||
|
// then kept so it can slide out again.
|
||||||
|
const [sheetOpen, setSheetOpen] = useState(false);
|
||||||
|
const [sheetUsed, setSheetUsed] = useState(false);
|
||||||
|
const openSheet = () => {
|
||||||
|
setSheetUsed(true);
|
||||||
|
setSheetOpen(true);
|
||||||
|
};
|
||||||
useEffect(() => localStorage.setItem("mail.listOrder", listOrder), [listOrder]);
|
useEffect(() => localStorage.setItem("mail.listOrder", listOrder), [listOrder]);
|
||||||
|
|
||||||
const isDrafts = view === "folder:drafts";
|
const isDrafts = view === "folder:drafts";
|
||||||
@@ -309,6 +319,14 @@ export function MailLayout() {
|
|||||||
window.addEventListener("keydown", onKey);
|
window.addEventListener("keydown", onKey);
|
||||||
return () => window.removeEventListener("keydown", onKey);
|
return () => window.removeEventListener("keydown", onKey);
|
||||||
}, [drawerOpen]);
|
}, [drawerOpen]);
|
||||||
|
// The side-panel drawer belongs to the conversation it was opened over.
|
||||||
|
useEffect(() => setSheetOpen(false), [view, mobile, threadId]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sheetOpen) return;
|
||||||
|
const onKey = (e: KeyboardEvent) => e.key === "Escape" && setSheetOpen(false);
|
||||||
|
window.addEventListener("keydown", onKey);
|
||||||
|
return () => window.removeEventListener("keydown", onKey);
|
||||||
|
}, [sheetOpen]);
|
||||||
|
|
||||||
// Auto-mark an opened thread read after a short dwell (4s), not instantly — and
|
// Auto-mark an opened thread read after a short dwell (4s), not instantly — and
|
||||||
// cancel it if you leave before then, so a quick glance doesn't mark it read.
|
// cancel it if you leave before then, so a quick glance doesn't mark it read.
|
||||||
@@ -1230,7 +1248,20 @@ export function MailLayout() {
|
|||||||
<span className={`ml-auto text-[13px] text-app-faint ${mobile ? "hidden" : ""}`}>
|
<span className={`ml-auto text-[13px] text-app-faint ${mobile ? "hidden" : ""}`}>
|
||||||
{t("mail.messageCount", { count: detailQuery.data.messages.length })}
|
{t("mail.messageCount", { count: detailQuery.data.messages.length })}
|
||||||
</span>
|
</span>
|
||||||
{!mobile && (
|
{mobile ? (
|
||||||
|
<button
|
||||||
|
className="ml-auto rounded p-1.5 text-app-fg hover:bg-app-hover"
|
||||||
|
title={t("panel.toggle")}
|
||||||
|
aria-label={t("panel.toggle")}
|
||||||
|
aria-expanded={sheetOpen}
|
||||||
|
onClick={openSheet}
|
||||||
|
>
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||||
|
<rect x="3" y="4" width="18" height="16" rx="2" />
|
||||||
|
<path d="M15 4v16" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
<button
|
<button
|
||||||
className={`rounded p-1.5 hover:bg-app-hover hover:text-app-fg ${rightOpen ? "text-app-fg" : "text-app-muted"}`}
|
className={`rounded p-1.5 hover:bg-app-hover hover:text-app-fg ${rightOpen ? "text-app-fg" : "text-app-muted"}`}
|
||||||
title={t("panel.toggle")}
|
title={t("panel.toggle")}
|
||||||
@@ -1276,6 +1307,25 @@ export function MailLayout() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
{mobile && sheetUsed && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
onClick={() => setSheetOpen(false)}
|
||||||
|
className={`fixed inset-0 z-40 bg-black/40 transition-opacity duration-200 ${
|
||||||
|
sheetOpen ? "opacity-100" : "pointer-events-none opacity-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-hidden={!sheetOpen}
|
||||||
|
className={`fixed inset-y-0 right-0 z-50 w-[88vw] max-w-[420px] border-l border-app-border pb-[env(safe-area-inset-bottom)] pt-[env(safe-area-inset-top)] shadow-2xl transition-transform duration-200 ease-out bg-app-surface ${
|
||||||
|
sheetOpen ? "translate-x-0" : "pointer-events-none translate-x-full"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<RightPanel tab={rightTab} onTab={setRightTab} onClose={() => setSheetOpen(false)} thread={detailQuery.data} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{rightOpen && !mobile && (
|
{rightOpen && !mobile && (
|
||||||
<>
|
<>
|
||||||
<Resizer onDelta={(dx) => setRightW((w) => clampRight(w - dx))} />
|
<Resizer onDelta={(dx) => setRightW((w) => clampRight(w - dx))} />
|
||||||
|
|||||||
Reference in New Issue
Block a user