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:
2026-08-03 20:53:35 +02:00
co-authored by Claude Opus 4.8
parent 1d1a621ce2
commit 0a478a6b26
16 changed files with 574 additions and 75 deletions
+15 -2
View File
@@ -1,10 +1,13 @@
import { useQuery } from "@tanstack/react-query";
import { useVirtualizer } from "@tanstack/react-virtual";
import { useRef } from "react";
import { useT } from "../../i18n";
import { api } from "../../lib/api";
import { formatClock, getDateLocale } from "../../lib/timeFormat";
import type { Address, ThreadSummary } from "../../lib/types";
import { Icon } from "../Icon";
import { LabelChips } from "../mail/LabelChips";
function timeLabel(iso: string): string {
const d = new Date(iso);
@@ -86,6 +89,15 @@ export function MessageList({
// every row shows a checkbox (selection mode) until all are cleared.
const checkMode = checkedIds.size > 0;
// Aggregated labels per thread (written by the reading pane) — shown before the subject.
const threadIds = threads.map((th) => th.id);
const labelsQuery = useQuery({
queryKey: ["thread-labels", threadIds],
queryFn: () => api.threadLabelsBatch(threadIds),
enabled: threadIds.length > 0,
});
const labelMap = labelsQuery.data ?? {};
const virtualizer = useVirtualizer({
count: threads.length,
getScrollElement: () => parentRef.current,
@@ -208,11 +220,12 @@ export function MessageList({
{/* Subject + snippet (extra left margin -> 3x the gap to the sender). */}
<div className="ml-6 flex min-w-0 flex-1 flex-col justify-center">
<div
className={`truncate text-[14px] ${
className={`flex min-w-0 items-center gap-1.5 text-[14px] ${
unread ? "font-black text-app-fg" : "text-app-fg"
}`}
>
{thread.subject}
<LabelChips labels={labelMap[thread.id] ?? []} />
<span className="truncate">{thread.subject}</span>
</div>
<div className="truncate text-xs text-app-faint">{thread.snippet}</div>
</div>