Labels in Mail UI (chips + Actions + sidebar), plain-text Lato, calendar-panel polish
Labels:
- Moved labelling out of the "+label" message slot into Mail: label chips on the
thread subject bar (removable ✕) and before list subjects, and a Labels submenu
in the per-message Actions menu (multi-select checkboxes + "Create new…").
Per-message storage kept; the reading pane writes a thread-union aggregate so the
list (no message ids) can show chips.
- Sidebar LABELS section rewritten: aligned monocolor tag rows, collapsible
(collapsed by default), inline "+" create popover, hover "−" delete with a confirm
popover, and click-to-filter the mail list by label.
- Backend: batch message/thread label reads, thread aggregate, create + delete
(delete unhooks from all mail), and GET /labels/{name}/threads. Core GET /threads
gained an `ids` param to power the label view. Removed the plugin-pages list from
the Mail sidebar.
Plain-text mail now renders in Lato (self-hosted at /fonts, @font-face'd into the
sandboxed message iframe) instead of the monospace <pre> default.
Calendar right-panel (embed mode): restored the "…" view-options menu, pinned the
toolbar + mini-calendar (only the agenda scrolls), plain "All calendars" button
(inline resets), and the event popover now closes on any outside click.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U72FZiF1W1pb8XxyWBTREA
This commit is contained in:
@@ -249,13 +249,18 @@ const CSS = `
|
||||
.cal-panelfoot button{display:inline-flex;align-items:center;gap:6px;background:none;border:0;color:rgb(var(--app-muted));cursor:pointer;font:inherit;font-size:13px}
|
||||
.cal-panelfoot button:hover{color:rgb(var(--app-fg))}
|
||||
.cal-panelcal{position:relative}
|
||||
.cal-panelmenu{position:relative}
|
||||
.cal-panelcalbtn{display:flex;align-items:center;gap:6px;background:none;border:0;outline:none;color:rgb(var(--app-fg));cursor:pointer;font:inherit;font-size:13px;padding:6px 4px}
|
||||
.cal-panelcalbtn:hover{color:rgb(var(--app-muted))}
|
||||
.cal-ddsec{padding:5px 10px 2px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:rgb(var(--app-faint))}
|
||||
.cal-ddsep{border-top:1px solid rgb(var(--app-border));margin:4px 0}
|
||||
.cal-embed .cal-searchbox{margin:0}
|
||||
.cal-embed .cal-form{right:8px;width:auto;top:52px;max-height:calc(100% - 64px)}
|
||||
.cal-embed .cal-pop{position:fixed}
|
||||
.cal-view{flex:1;min-height:0;overflow:auto;position:relative}
|
||||
.cal-btn{border:0;border-radius:6px;padding:6px 10px;cursor:pointer;font:inherit;background:rgb(var(--app-hover));color:rgb(var(--app-fg))}
|
||||
.cal-btn-primary{background:#3b82f6;color:#fff;flex:1}
|
||||
.cal-iconbtn{border:0;background:none;color:rgb(var(--app-muted));cursor:pointer;padding:6px;border-radius:4px;display:inline-flex}
|
||||
.cal-iconbtn{border:0;background:none;color:rgb(var(--app-muted));cursor:pointer;padding:6px;border-radius:4px;display:inline-flex;outline:none}
|
||||
.cal-iconbtn:hover{background:rgb(var(--app-hover));color:rgb(var(--app-fg))}
|
||||
.cal-searchbox{flex:1;max-width:520px;margin:0 auto;display:flex;align-items:center;gap:8px;padding:0 10px;border-radius:8px;border:1px solid rgb(var(--app-border));background:rgb(var(--app-surface))}
|
||||
.cal-searchbox .ic{color:rgb(var(--app-faint));flex:0 0 auto}
|
||||
@@ -385,12 +390,16 @@ const CSS = `
|
||||
`;
|
||||
|
||||
export default function mount(el, ctx, props) {
|
||||
if (!document.getElementById("cal-styles")) {
|
||||
const style = document.createElement("style");
|
||||
style.id = "cal-styles";
|
||||
style.textContent = CSS;
|
||||
document.head.appendChild(style);
|
||||
// Always (re)write the stylesheet so a newer bundle version's CSS wins — the
|
||||
// singleton element would otherwise keep a stale copy from an earlier mount,
|
||||
// leaving new classes (e.g. .cal-panelcalbtn) unstyled = default button frame.
|
||||
let calStyle = document.getElementById("cal-styles");
|
||||
if (!calStyle) {
|
||||
calStyle = document.createElement("style");
|
||||
calStyle.id = "cal-styles";
|
||||
document.head.appendChild(calStyle);
|
||||
}
|
||||
calStyle.textContent = CSS;
|
||||
|
||||
// Embed mode: the SAME app runs compactly inside Mail's right panel — reusing the
|
||||
// agenda, mini-month, event popover, event form and search results verbatim, so
|
||||
@@ -417,6 +426,9 @@ export default function mount(el, ctx, props) {
|
||||
search: null, // null | { q, from:Date, to:Date, results:[], loading:bool }
|
||||
searchOpen: false, // embed: search box revealed
|
||||
panelCalOpen: false, // embed: "All calendars" dropdown open
|
||||
panelMenuOpen: false, // embed: "..." view-options menu open
|
||||
panelMini: "month", // embed mini calendar: month | week
|
||||
panelView: "agenda", // embed agenda: day | 3days | agenda
|
||||
me: "",
|
||||
quota: "",
|
||||
jitsi: "", // custom Jitsi server (blank => https://meet.jit.si/)
|
||||
@@ -878,8 +890,9 @@ export default function mount(el, ctx, props) {
|
||||
function renderEmbed() {
|
||||
el.innerHTML = `<div class="cal-app cal-embed">
|
||||
${panelBar()}
|
||||
${state.search ? "" : `<div class="cal-embed-mini">${miniMonth()}</div>`}
|
||||
<div class="cal-embed-body">
|
||||
${state.search ? searchPanelHtml() : `<div class="cal-embed-mini">${miniMonth()}</div>${agenda()}`}
|
||||
${state.search ? searchPanelHtml() : agenda()}
|
||||
</div>
|
||||
${panelFoot()}
|
||||
${state.composing ? formHtml() : ""}
|
||||
@@ -895,12 +908,28 @@ export default function mount(el, ctx, props) {
|
||||
const cur = state.hidden.size && state.calendars.length - state.hidden.size === 1
|
||||
? state.calendars.find((c) => !state.hidden.has(c.id))
|
||||
: null;
|
||||
// Inline resets guarantee no default-button frame/background/focus-ring even if
|
||||
// an older injected stylesheet is missing these classes.
|
||||
const plain = "border:0;background:none;outline:none;box-shadow:none";
|
||||
return `<div class="cal-panelbar">
|
||||
<button class="cal-iconbtn" data-act="new" title="New event"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg></button>
|
||||
<button class="cal-iconbtn${state.searchOpen ? " on" : ""}" data-act="panel-search" title="Search"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg></button>
|
||||
<button class="cal-iconbtn" style="${plain}" data-act="new" title="New event"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg></button>
|
||||
<button class="cal-iconbtn${state.searchOpen ? " on" : ""}" style="${plain}" data-act="panel-search" title="Search"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg></button>
|
||||
<div class="cal-dd cal-panelmenu">
|
||||
<button class="cal-iconbtn" style="${plain}" data-act="panel-menu" title="Options"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></button>
|
||||
${state.panelMenuOpen ? `<div class="cal-ddpop" style="left:0;min-width:170px">
|
||||
<div class="cal-ddsec">Mini calendar</div>
|
||||
<button data-panelmini="week">${state.panelMini === "week" ? "✓ " : " "}Week</button>
|
||||
<button data-panelmini="month">${state.panelMini === "month" ? "✓ " : " "}Month</button>
|
||||
<div class="cal-ddsep"></div>
|
||||
<div class="cal-ddsec">View</div>
|
||||
<button data-panelview="day">${state.panelView === "day" ? "✓ " : " "}Day</button>
|
||||
<button data-panelview="3days">${state.panelView === "3days" ? "✓ " : " "}3 days</button>
|
||||
<button data-panelview="agenda">${state.panelView === "agenda" ? "✓ " : " "}Agenda</button>
|
||||
</div>` : ""}
|
||||
</div>
|
||||
<div style="flex:1"></div>
|
||||
<div class="cal-dd cal-panelcal">
|
||||
<button class="cal-iconbtn" data-act="panelcal" style="width:auto;gap:6px;padding:6px 8px;font-size:13px"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>${cur ? esc(cur.name) : "All calendars"} <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M4 6l4 4 4-4"/></svg></button>
|
||||
<button class="cal-panelcalbtn" style="display:flex;align-items:center;gap:6px;${plain};color:rgb(var(--app-fg));cursor:pointer;font:inherit;font-size:13px;padding:6px 4px" data-act="panelcal"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>${cur ? esc(cur.name) : "All calendars"} <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M4 6l4 4 4-4"/></svg></button>
|
||||
${state.panelCalOpen ? `<div class="cal-ddpop" style="right:0;left:auto">
|
||||
<button data-panelcal="all">${!cur ? "✓ " : ""}All calendars</button>
|
||||
${state.calendars.map((c) => `<button data-panelcal="${c.id}">${cur && cur.id === c.id ? "✓ " : ""}<span style="display:inline-block;width:9px;height:9px;border-radius:2px;background:${esc(c.color)};margin-right:4px"></span>${esc(c.name)}</button>`).join("")}
|
||||
@@ -1003,11 +1032,14 @@ export default function mount(el, ctx, props) {
|
||||
|
||||
function miniMonth() {
|
||||
const m = state.mini;
|
||||
const gs = startOfWeek(startOfMonth(m));
|
||||
// Embed "Mini calendar" option: Week shows just the anchor's week row.
|
||||
const week = state.embed && state.panelMini === "week";
|
||||
const gs = week ? startOfWeek(state.anchor) : startOfWeek(startOfMonth(m));
|
||||
const cellCount = week ? 7 : 42;
|
||||
let cells = "";
|
||||
for (let i = 0; i < 42; i++) {
|
||||
for (let i = 0; i < cellCount; i++) {
|
||||
const d = addDays(gs, i);
|
||||
const cls = [d.getMonth() !== m.getMonth() ? "out" : "", sameDay(d, new Date()) ? "today" : ""].join(" ");
|
||||
const cls = [!week && d.getMonth() !== m.getMonth() ? "out" : "", sameDay(d, new Date()) ? "today" : ""].join(" ");
|
||||
cells += `<button class="${cls}" data-mini="${iso(d)}">${d.getDate()}</button>`;
|
||||
}
|
||||
return `<div class="cal-mini">
|
||||
@@ -1123,7 +1155,7 @@ export default function mount(el, ctx, props) {
|
||||
const when = ev.all_day ? "All day" : `${hhmm(ev._s)} – ${hhmm(ev._e)} ${tzAbbr(ev.tz)} (${durLabel})`;
|
||||
const left = Math.min(rect.right + 8, window.innerWidth - 302);
|
||||
const top = Math.min(Math.max(rect.top, 8), window.innerHeight - 230);
|
||||
return `<div class="cal-popoverlay" style="position:absolute;inset:0;z-index:59" data-act="pop-close"></div>
|
||||
return `<div class="cal-popoverlay" style="position:fixed;inset:0;z-index:59" data-act="pop-close"></div>
|
||||
<div class="cal-pop" style="left:${left}px;top:${top}px">
|
||||
<div class="body">
|
||||
<div class="cap">${esc(cal ? cal.name : "")}</div>
|
||||
@@ -1258,15 +1290,18 @@ export default function mount(el, ctx, props) {
|
||||
}
|
||||
|
||||
function agenda() {
|
||||
const start = startOfMonth(state.anchor);
|
||||
// Embed "View" option: Agenda = the month; Day/3 days = a short run from the anchor.
|
||||
const short = state.embed && state.panelView !== "agenda";
|
||||
const count = short ? (state.panelView === "day" ? 1 : 3) : 30;
|
||||
const start = short ? startOfDay(state.anchor) : startOfMonth(state.anchor);
|
||||
const evs = visibleEvents();
|
||||
const rightAction = state.embed
|
||||
? `<button class="cal-agenda-today" data-act="mini-today">TODAY</button>`
|
||||
: `<button class="cal-iconbtn" data-act="panel">✕</button>`;
|
||||
let html = `<div class="cal-mainhead"><span class="cal-title">${MONTHS[start.getMonth()]} <span>${start.getFullYear()}</span></span><div style="flex:1"></div>${rightAction}</div><div class="cal-agenda">`;
|
||||
for (let i = 0; i < 30; i++) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const d = addDays(start, i);
|
||||
if (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);
|
||||
html += `<div class="day"><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>`
|
||||
@@ -1292,8 +1327,11 @@ export default function mount(el, ctx, props) {
|
||||
state.panelCalOpen = false; render();
|
||||
});
|
||||
act("open-newtab", () => window.open("/p/calendar", "_blank", "noopener"));
|
||||
if (state.panelCalOpen) {
|
||||
setTimeout(() => { const close = () => { state.panelCalOpen = false; document.removeEventListener("click", close); render(); }; document.addEventListener("click", close); }, 0);
|
||||
act("panel-menu", (e) => { e.stopPropagation(); state.panelMenuOpen = !state.panelMenuOpen; render(); });
|
||||
on("[data-panelmini]", "click", (e) => { e.stopPropagation(); state.panelMini = e.currentTarget.getAttribute("data-panelmini"); state.panelMenuOpen = false; render(); });
|
||||
on("[data-panelview]", "click", (e) => { e.stopPropagation(); state.panelView = e.currentTarget.getAttribute("data-panelview"); state.panelMenuOpen = false; render(); });
|
||||
if (state.panelCalOpen || state.panelMenuOpen) {
|
||||
setTimeout(() => { const close = () => { state.panelCalOpen = false; state.panelMenuOpen = false; document.removeEventListener("click", close); render(); }; document.addEventListener("click", close); }, 0);
|
||||
}
|
||||
// --- search ---
|
||||
on(".cal-search", "input", (e) => {
|
||||
|
||||
Reference in New Issue
Block a user