Mail right panel: Calendar / People / Attachments (shared calendar forms)
New slide-out right panel in Mail (hidden by default, toggle in the reading pane, resizable, persisted). Three tabs: - Calendar: mounts the calendar plugin bundle in a new embed:"panel" mode, so the agenda, mini-month, event popover, event form and search results are the calendar app's OWN code — future changes propagate to both. Events click → real popover; New event / Edit / "Invite all" open the shared event form in the panel; search shows the calendar's results panel; All-calendars filter + open-in-new-tab. - People: conversation senders / contact books (All/Recent/VIPs/Autosaved) drilling into a contact detail (Call/Message/Compose/Invite, related mail, find-all); "Compose to all" / "Invite all to new event". - Attachments: every attachment in the thread, grouped by sender, with download and clickable previews (reuses the mail AttachmentPreview overlay for supported types). Calendar bundle: mount(el, ctx, props) reads embed/newEventGuests; renderEmbed() + panelBar/panelFoot; embed-aware agenda (TODAY); form/popover repositioned for the narrow panel; guest deep-link accepts comma-separated emails. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U72FZiF1W1pb8XxyWBTREA
This commit is contained in:
@@ -12,6 +12,7 @@ import { ThemeToggle } from "../components/ThemeToggle";
|
||||
import { EmptyIllustration } from "../components/EmptyIllustration";
|
||||
import { BackfillBanner } from "../components/BackfillBanner";
|
||||
import { ThreadView } from "../components/thread/ThreadView";
|
||||
import { RightPanel, type PanelTab } from "../components/mail/RightPanel";
|
||||
import { useT } from "../i18n";
|
||||
import type { MessageKey } from "../i18n/en";
|
||||
import { api } from "../lib/api";
|
||||
@@ -93,12 +94,21 @@ export function MailLayout() {
|
||||
const [listW, setListW] = useState(() => Number(localStorage.getItem("panel.list")) || 384);
|
||||
// Collapsed sidebar shows icons only (persisted).
|
||||
const [collapsed, setCollapsed] = useState(() => localStorage.getItem("panel.collapsed") === "1");
|
||||
// Right info panel (Calendar / People / Attachments) — hidden by default.
|
||||
const [rightOpen, setRightOpen] = useState(() => localStorage.getItem("panel.right") === "1");
|
||||
const [rightTab, setRightTab] = useState<PanelTab>(() => (localStorage.getItem("panel.rightTab") as PanelTab) || "calendar");
|
||||
const [rightW, setRightW] = useState(() => Number(localStorage.getItem("panel.rightW")) || 340);
|
||||
const clampSidebar = (w: number) => Math.min(Math.max(w, 180), 420);
|
||||
// The list can be dragged up to 75% of the window width.
|
||||
const clampList = (w: number) => Math.min(Math.max(w, 260), window.innerWidth * 0.75);
|
||||
const clampRight = (w: number) => Math.min(Math.max(w, 280), 560);
|
||||
useEffect(() => localStorage.setItem("panel.sidebar", String(sidebarW)), [sidebarW]);
|
||||
useEffect(() => localStorage.setItem("panel.list", String(listW)), [listW]);
|
||||
useEffect(() => localStorage.setItem("panel.collapsed", collapsed ? "1" : "0"), [collapsed]);
|
||||
useEffect(() => localStorage.setItem("panel.right", rightOpen ? "1" : "0"), [rightOpen]);
|
||||
useEffect(() => localStorage.setItem("panel.rightTab", rightTab), [rightTab]);
|
||||
useEffect(() => localStorage.setItem("panel.rightW", String(rightW)), [rightW]);
|
||||
const openPanel = (tab: PanelTab) => { setRightTab(tab); setRightOpen(true); };
|
||||
useEffect(() => localStorage.setItem("mail.listOrder", listOrder), [listOrder]);
|
||||
|
||||
const isDrafts = view === "folder:drafts";
|
||||
@@ -930,6 +940,18 @@ export function MailLayout() {
|
||||
<span className="ml-auto text-[13px] text-app-faint">
|
||||
{t("mail.messageCount", { count: detailQuery.data.messages.length })}
|
||||
</span>
|
||||
<button
|
||||
className={`rounded p-1.5 hover:bg-app-hover hover:text-app-fg ${rightOpen ? "text-app-fg" : "text-app-muted"}`}
|
||||
title={t("panel.toggle")}
|
||||
aria-label={t("panel.toggle")}
|
||||
aria-pressed={rightOpen}
|
||||
onClick={() => (rightOpen ? setRightOpen(false) : openPanel(rightTab))}
|
||||
>
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||
<rect x="3" y="4" width="18" height="16" rx="2" />
|
||||
<path d="M15 4v16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<ThreadView
|
||||
@@ -944,6 +966,19 @@ export function MailLayout() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-6 text-app-faint">
|
||||
{!rightOpen && (
|
||||
<button
|
||||
className="absolute right-3 top-3 rounded p-1.5 text-app-muted hover:bg-app-hover hover:text-app-fg"
|
||||
title={t("panel.toggle")}
|
||||
aria-label={t("panel.toggle")}
|
||||
onClick={() => openPanel(rightTab)}
|
||||
>
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||
<rect x="3" y="4" width="18" height="16" rx="2" />
|
||||
<path d="M15 4v16" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
<EmptyIllustration className="w-56 max-w-[55%]" />
|
||||
<span className="text-3xl">{t("mail.selectConversation")}</span>
|
||||
</div>
|
||||
@@ -967,6 +1002,14 @@ export function MailLayout() {
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
{rightOpen && (
|
||||
<>
|
||||
<Resizer onDelta={(dx) => setRightW((w) => clampRight(w - dx))} />
|
||||
<div style={{ width: rightW }} className="shrink-0 border-l border-app-border">
|
||||
<RightPanel tab={rightTab} onTab={setRightTab} onClose={() => setRightOpen(false)} thread={detailQuery.data} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user