From 0ddfc8c658c5bd77eab882176f3c1f1363714d8c Mon Sep 17 00:00:00 2001 From: Lukas Remis Date: Wed, 30 Sep 2026 21:23:10 +0200 Subject: [PATCH] Calendar panel: a picked mini-calendar day showed nowhere; no new-tab link on a phone In Mail's side panel the agenda already lists the whole month and the mini calendar never marked a selection, so clicking a day in the current month changed nothing on screen. The day is now highlighted and the agenda scrolls to it (TODAY scrolls back); a day in another month flips both to that month. On a phone the panel drops the "Open calendar in new tab" footer: the host passes newTabLink: false to the embedded calendar. Co-Authored-By: Claude Opus 5.5 --- frontend/src/components/mail/RightPanel.tsx | 11 +++++-- .../webmail_plugin_calendar/ui/page.js | 33 ++++++++++++++++--- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/mail/RightPanel.tsx b/frontend/src/components/mail/RightPanel.tsx index 31efcbc..82467b9 100644 --- a/frontend/src/components/mail/RightPanel.tsx +++ b/frontend/src/components/mail/RightPanel.tsx @@ -12,6 +12,7 @@ import { useT } from "../../i18n"; import { api, apiFetch } from "../../lib/api"; import type { Address, AttachmentOut, MessageEnvelope, ThreadDetail } from "../../lib/types"; import { AvatarImage } from "../SenderAvatar"; +import { useIsMobile } from "../../lib/useIsMobile"; import { loadBundle } from "../../plugins/bundleLoader"; import { usePluginContext } from "../../plugins/context"; import { isPreviewable } from "../thread/previewKind"; @@ -123,6 +124,8 @@ const CAL_BUNDLE = "/api/plugins/calendar/ui/page.js"; function PanelCalendar({ invite }: { invite: InviteSignal | null }) { const ref = useRef(null); const ctx = usePluginContext("calendar"); + // A phone has no use for "Open calendar in new tab". + const mobile = useIsMobile(); // Remount when a new "Invite all" fires so the form opens with those guests. const inviteN = invite?.n ?? 0; useEffect(() => { @@ -131,11 +134,15 @@ function PanelCalendar({ invite }: { invite: InviteSignal | null }) { loadBundle(CAL_BUNDLE).then((mount) => { if (!alive || !ref.current) return; ref.current.innerHTML = ""; - cleanup = mount(ref.current, ctx, { embed: "panel", newEventGuests: invite?.guests }); + cleanup = mount(ref.current, ctx, { + embed: "panel", + newEventGuests: invite?.guests, + newTabLink: !mobile, + }); }); return () => { alive = false; if (typeof cleanup === "function") cleanup(); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ctx, inviteN]); + }, [ctx, inviteN, mobile]); return
; } diff --git a/plugins/webmail_plugin_calendar/webmail_plugin_calendar/ui/page.js b/plugins/webmail_plugin_calendar/webmail_plugin_calendar/ui/page.js index bebe860..be01ead 100644 --- a/plugins/webmail_plugin_calendar/webmail_plugin_calendar/ui/page.js +++ b/plugins/webmail_plugin_calendar/webmail_plugin_calendar/ui/page.js @@ -286,6 +286,7 @@ const CSS = ` .cal-mini-grid button:hover{background:rgb(var(--app-hover))} .cal-mini-grid button.out{color:rgb(var(--app-faint))} .cal-mini-grid button.today{background:#3b82f6;color:#fff} +.cal-mini-grid button.sel{background:rgb(var(--app-active));font-weight:600} /* month */ .cal-month{display:grid;grid-template-columns:repeat(7,1fr);grid-template-rows:auto;grid-auto-rows:minmax(0,1fr);height:100%} .cal-month .dow{border-bottom:1px solid rgb(var(--app-border));border-right:1px solid rgb(var(--app-border));padding:4px 8px;font-size:13px;font-weight:600;text-transform:uppercase;color:rgb(var(--app-faint))} @@ -443,6 +444,9 @@ export default function mount(el, ctx, props) { // any change here propagates to both places. const embed = !!(props && props.embed === "panel"); const newEventGuests = (props && props.newEventGuests) || null; + // Embed: the host can drop the "Open calendar in new tab" footer (Mail on a + // phone, where a new tab is no use). + const newTabLink = !(props && props.newTabLink === false); const state = { embed, @@ -466,6 +470,7 @@ export default function mount(el, ctx, props) { panelMenuOpen: false, // embed: "..." view-options menu open panelMini: "month", // embed mini calendar: month | week panelView: "agenda", // embed agenda: day | 3days | agenda + scrollToAnchor: false, // embed: bring the anchor day to the top of the agenda once me: "", quota: "", jitsi: "", // custom Jitsi server (blank => https://meet.jit.si/) @@ -1047,6 +1052,13 @@ export default function mount(el, ctx, props) { ${state.confirm ? confirmHtml() : ""}
`; wire(); + if (state.scrollToAnchor) { + state.scrollToAnchor = false; + const a = state.anchor; + const row = el.querySelector(`[data-agday="${a.getFullYear()}-${pad2(a.getMonth() + 1)}-${pad2(a.getDate())}"]`); + const body = el.querySelector(".cal-embed-body"); + if (row && body) body.scrollTop += row.getBoundingClientRect().top - body.getBoundingClientRect().top; + } if (state.eventPopover) positionPopover(); if (state.search && !state.eventPopover && !state.composing && !state.confirm) focusSearch(); } @@ -1087,6 +1099,7 @@ export default function mount(el, ctx, props) { } function panelFoot() { + if (!newTabLink) return ""; return `
`; } @@ -1186,7 +1199,10 @@ export default function mount(el, ctx, props) { let cells = ""; for (let i = 0; i < cellCount; i++) { const d = addDays(gs, i); - const cls = [!week && d.getMonth() !== m.getMonth() ? "out" : "", sameDay(d, new Date()) ? "today" : ""].join(" "); + const today = sameDay(d, new Date()); + // Embed: the agenda has no other sign of which day was picked. + const sel = state.embed && !today && sameDay(d, state.anchor); + const cls = [!week && d.getMonth() !== m.getMonth() ? "out" : "", today ? "today" : "", sel ? "sel" : ""].join(" "); cells += ``; } return `
@@ -1466,7 +1482,7 @@ export default function mount(el, ctx, props) { const d = addDays(start, i); if (!short && d.getMonth() !== start.getMonth()) break; const dayEvs = evs.filter((ev) => sameDay(ev._s, d)).sort((a, b) => a._s - b._s); - html += `
${DAYNAMES[(d.getDay() + 6) % 7].toUpperCase()}${d.getDate()}
${ + html += `
${DAYNAMES[(d.getDay() + 6) % 7].toUpperCase()}${d.getDate()}
${ dayEvs.length ? dayEvs.map((ev) => `
${ev.all_day ? "" : hhmm(ev._s) + " "}${esc(ev.summary || "(no title)")}
`).join("") : `No events` }
`; } @@ -1530,7 +1546,7 @@ export default function mount(el, ctx, props) { act("panel", () => { state.rightOpen = !state.rightOpen; render(); }); act("miniprev", () => { state.mini = new Date(state.mini.getFullYear(), state.mini.getMonth() - 1, 1); render(); }); act("mininext", () => { state.mini = new Date(state.mini.getFullYear(), state.mini.getMonth() + 1, 1); render(); }); - act("mini-today", () => { state.anchor = startOfDay(new Date()); state.mini = startOfMonth(state.anchor); reload(); }); + act("mini-today", () => { state.anchor = startOfDay(new Date()); state.mini = startOfMonth(state.anchor); state.scrollToAnchor = state.embed; reload(); }); // --- left panel: collapse, theme, resize (like Mail) --- act("left-toggle", () => { state.leftCollapsed = !state.leftCollapsed; @@ -1591,7 +1607,16 @@ export default function mount(el, ctx, props) { }); on("[data-view]", "click", (e) => { state.view = e.currentTarget.getAttribute("data-view"); reload(); }); - on("[data-mini]", "click", (e) => { state.anchor = startOfDay(new Date(e.currentTarget.getAttribute("data-mini"))); if (state.view === "month") state.view = "day"; reload(); }); + on("[data-mini]", "click", (e) => { + state.anchor = startOfDay(new Date(e.currentTarget.getAttribute("data-mini"))); + if (state.embed) { + // The panel's agenda already shows the whole month, so without this a + // picked day changed nothing on screen. + state.mini = startOfMonth(state.anchor); + state.scrollToAnchor = true; + } else if (state.view === "month") state.view = "day"; + reload(); + }); on("[data-cal]", "change", (e) => { const id = e.currentTarget.getAttribute("data-cal"); if (e.currentTarget.checked) state.hidden.delete(id); else state.hidden.add(id);