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 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import { useT } from "../../i18n";
|
|||||||
import { api, apiFetch } from "../../lib/api";
|
import { api, apiFetch } from "../../lib/api";
|
||||||
import type { Address, AttachmentOut, MessageEnvelope, ThreadDetail } from "../../lib/types";
|
import type { Address, AttachmentOut, MessageEnvelope, ThreadDetail } from "../../lib/types";
|
||||||
import { AvatarImage } from "../SenderAvatar";
|
import { AvatarImage } from "../SenderAvatar";
|
||||||
|
import { useIsMobile } from "../../lib/useIsMobile";
|
||||||
import { loadBundle } from "../../plugins/bundleLoader";
|
import { loadBundle } from "../../plugins/bundleLoader";
|
||||||
import { usePluginContext } from "../../plugins/context";
|
import { usePluginContext } from "../../plugins/context";
|
||||||
import { isPreviewable } from "../thread/previewKind";
|
import { isPreviewable } from "../thread/previewKind";
|
||||||
@@ -123,6 +124,8 @@ const CAL_BUNDLE = "/api/plugins/calendar/ui/page.js";
|
|||||||
function PanelCalendar({ invite }: { invite: InviteSignal | null }) {
|
function PanelCalendar({ invite }: { invite: InviteSignal | null }) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const ctx = usePluginContext("calendar");
|
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.
|
// Remount when a new "Invite all" fires so the form opens with those guests.
|
||||||
const inviteN = invite?.n ?? 0;
|
const inviteN = invite?.n ?? 0;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -131,11 +134,15 @@ function PanelCalendar({ invite }: { invite: InviteSignal | null }) {
|
|||||||
loadBundle(CAL_BUNDLE).then((mount) => {
|
loadBundle(CAL_BUNDLE).then((mount) => {
|
||||||
if (!alive || !ref.current) return;
|
if (!alive || !ref.current) return;
|
||||||
ref.current.innerHTML = "";
|
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(); };
|
return () => { alive = false; if (typeof cleanup === "function") cleanup(); };
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [ctx, inviteN]);
|
}, [ctx, inviteN, mobile]);
|
||||||
return <div ref={ref} className="h-full" />;
|
return <div ref={ref} className="h-full" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ const CSS = `
|
|||||||
.cal-mini-grid button:hover{background:rgb(var(--app-hover))}
|
.cal-mini-grid button:hover{background:rgb(var(--app-hover))}
|
||||||
.cal-mini-grid button.out{color:rgb(var(--app-faint))}
|
.cal-mini-grid button.out{color:rgb(var(--app-faint))}
|
||||||
.cal-mini-grid button.today{background:#3b82f6;color:#fff}
|
.cal-mini-grid button.today{background:#3b82f6;color:#fff}
|
||||||
|
.cal-mini-grid button.sel{background:rgb(var(--app-active));font-weight:600}
|
||||||
/* month */
|
/* 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{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))}
|
.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.
|
// any change here propagates to both places.
|
||||||
const embed = !!(props && props.embed === "panel");
|
const embed = !!(props && props.embed === "panel");
|
||||||
const newEventGuests = (props && props.newEventGuests) || null;
|
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 = {
|
const state = {
|
||||||
embed,
|
embed,
|
||||||
@@ -466,6 +470,7 @@ export default function mount(el, ctx, props) {
|
|||||||
panelMenuOpen: false, // embed: "..." view-options menu open
|
panelMenuOpen: false, // embed: "..." view-options menu open
|
||||||
panelMini: "month", // embed mini calendar: month | week
|
panelMini: "month", // embed mini calendar: month | week
|
||||||
panelView: "agenda", // embed agenda: day | 3days | agenda
|
panelView: "agenda", // embed agenda: day | 3days | agenda
|
||||||
|
scrollToAnchor: false, // embed: bring the anchor day to the top of the agenda once
|
||||||
me: "",
|
me: "",
|
||||||
quota: "",
|
quota: "",
|
||||||
jitsi: "", // custom Jitsi server (blank => https://meet.jit.si/)
|
jitsi: "", // custom Jitsi server (blank => https://meet.jit.si/)
|
||||||
@@ -1047,6 +1052,13 @@ export default function mount(el, ctx, props) {
|
|||||||
${state.confirm ? confirmHtml() : ""}
|
${state.confirm ? confirmHtml() : ""}
|
||||||
</div>`;
|
</div>`;
|
||||||
wire();
|
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.eventPopover) positionPopover();
|
||||||
if (state.search && !state.eventPopover && !state.composing && !state.confirm) focusSearch();
|
if (state.search && !state.eventPopover && !state.composing && !state.confirm) focusSearch();
|
||||||
}
|
}
|
||||||
@@ -1087,6 +1099,7 @@ export default function mount(el, ctx, props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function panelFoot() {
|
function panelFoot() {
|
||||||
|
if (!newTabLink) return "";
|
||||||
return `<div class="cal-panelfoot"><button data-act="open-newtab">Open calendar in new tab <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4h6v6M20 4l-9 9M18 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4"/></svg></button></div>`;
|
return `<div class="cal-panelfoot"><button data-act="open-newtab">Open calendar in new tab <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4h6v6M20 4l-9 9M18 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4"/></svg></button></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1186,7 +1199,10 @@ export default function mount(el, ctx, props) {
|
|||||||
let cells = "";
|
let cells = "";
|
||||||
for (let i = 0; i < cellCount; i++) {
|
for (let i = 0; i < cellCount; i++) {
|
||||||
const d = addDays(gs, 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 += `<button class="${cls}" data-mini="${iso(d)}">${d.getDate()}</button>`;
|
cells += `<button class="${cls}" data-mini="${iso(d)}">${d.getDate()}</button>`;
|
||||||
}
|
}
|
||||||
return `<div class="cal-mini">
|
return `<div class="cal-mini">
|
||||||
@@ -1466,7 +1482,7 @@ export default function mount(el, ctx, props) {
|
|||||||
const d = addDays(start, i);
|
const d = addDays(start, i);
|
||||||
if (!short && d.getMonth() !== start.getMonth()) break;
|
if (!short && d.getMonth() !== start.getMonth()) break;
|
||||||
const dayEvs = evs.filter((ev) => sameDay(ev._s, d)).sort((a, b) => a._s - b._s);
|
const dayEvs = evs.filter((ev) => sameDay(ev._s, d)).sort((a, b) => a._s - b._s);
|
||||||
html += `<div class="day"><div class="dt">${DAYNAMES[(d.getDay() + 6) % 7].toUpperCase()}<b>${d.getDate()}</b></div><div style="flex:1">${
|
html += `<div class="day" data-agday="${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}"><div class="dt">${DAYNAMES[(d.getDay() + 6) % 7].toUpperCase()}<b>${d.getDate()}</b></div><div style="flex:1">${
|
||||||
dayEvs.length ? dayEvs.map((ev) => `<div class="cal-ev" data-ev="${ev.id}" data-start="${esc(ev.start)}" style="border-color:${esc(ev.color)}">${ev.all_day ? "" : hhmm(ev._s) + " "}${esc(ev.summary || "(no title)")}</div>`).join("") : `<span class="none">No events</span>`
|
dayEvs.length ? dayEvs.map((ev) => `<div class="cal-ev" data-ev="${ev.id}" data-start="${esc(ev.start)}" style="border-color:${esc(ev.color)}">${ev.all_day ? "" : hhmm(ev._s) + " "}${esc(ev.summary || "(no title)")}</div>`).join("") : `<span class="none">No events</span>`
|
||||||
}</div></div>`;
|
}</div></div>`;
|
||||||
}
|
}
|
||||||
@@ -1530,7 +1546,7 @@ export default function mount(el, ctx, props) {
|
|||||||
act("panel", () => { state.rightOpen = !state.rightOpen; render(); });
|
act("panel", () => { state.rightOpen = !state.rightOpen; render(); });
|
||||||
act("miniprev", () => { state.mini = new Date(state.mini.getFullYear(), state.mini.getMonth() - 1, 1); 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("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) ---
|
// --- left panel: collapse, theme, resize (like Mail) ---
|
||||||
act("left-toggle", () => {
|
act("left-toggle", () => {
|
||||||
state.leftCollapsed = !state.leftCollapsed;
|
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-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) => {
|
on("[data-cal]", "change", (e) => {
|
||||||
const id = e.currentTarget.getAttribute("data-cal");
|
const id = e.currentTarget.getAttribute("data-cal");
|
||||||
if (e.currentTarget.checked) state.hidden.delete(id); else state.hidden.add(id);
|
if (e.currentTarget.checked) state.hidden.delete(id); else state.hidden.add(id);
|
||||||
|
|||||||
Reference in New Issue
Block a user