feat: P2.2 Create Appointment from Email + P2.4 Calendar Dashlet + P2.12 Action Wheel + P2.14 Share Files

- P2.2: 'Create Appointment' button in email viewer → pre-fills event modal
  with subject, body, participants, date. calendar-store newEventPrefill state.
- P2.4: MiniCalendarDashlet in sidebar bottom — month grid with event dots,
  day click navigates to calendar. Collapsible, respect firstDayOfWeek.
- P2.12: Custom radial menu (components/ui/radial-menu.tsx) — circular SVG
  menu with keyboard nav, animations. Wired into email-list, contact-list,
  file-browser, calendar-month-view right-click handlers.
- P2.14: 'Send as Attachment' button in file browser — opens compose tab
  with selected files pre-attached via Pro tab store.
This commit is contained in:
Bernd Rodler
2026-08-07 13:15:04 +02:00
parent 67f61f18d0
commit 83e29b3ef1
14 changed files with 933 additions and 42 deletions
+69 -3
View File
@@ -1,13 +1,14 @@
"use client";
import { useMemo, useState } from "react";
import { useMemo, useState, useCallback } from "react";
import { useTranslations, useLocale } from "next-intl";
import { Search, BookUser, Trash2, Users, Download, X, UserPlus, CheckSquare, Square, Filter, Mail, Phone, Image as ImageIcon, RotateCcw, Menu } from "lucide-react";
import { Search, BookUser, Trash2, Users, Download, X, UserPlus, CheckSquare, Square, Filter, Mail, Phone, Image as ImageIcon, RotateCcw, Menu, Pencil } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { ContactListItem } from "./contact-list-item";
import { ContactContextMenu } from "./contact-context-menu";
import { useContextMenu } from "@/hooks/use-context-menu";
import { RadialMenu, type RadialMenuItem } from "@/components/ui/radial-menu";
import { cn } from "@/lib/utils";
import type { AnniversaryDate, ContactCard } from "@/lib/jmap/types";
import { getContactDisplayName, getContactPhotoUri } from "@/stores/contact-store";
@@ -142,6 +143,63 @@ export function ContactList({
const density = useSettingsStore((state) => state.density);
const groupByLetter = useSettingsStore((state) => state.groupContactsByLetter);
const { contextMenu, openContextMenu, closeContextMenu, menuRef } = useContextMenu<ContactCard>();
// Radial menu state
const [radialMenuOpen, setRadialMenuOpen] = useState(false);
const [radialMenuPos, setRadialMenuPos] = useState({ x: 0, y: 0 });
const [radialMenuContact, setRadialMenuContact] = useState<ContactCard | null>(null);
const openRadialMenu = useCallback((e: React.MouseEvent, contact: ContactCard) => {
e.preventDefault();
setRadialMenuPos({ x: e.clientX, y: e.clientY });
setRadialMenuContact(contact);
setRadialMenuOpen(true);
}, []);
const closeRadialMenu = useCallback(() => {
setRadialMenuOpen(false);
}, []);
const radialMenuItems = useMemo<RadialMenuItem[]>(() => {
if (!radialMenuContact) return [];
const c = radialMenuContact;
const items: RadialMenuItem[] = [];
items.push({
id: "edit",
icon: <Pencil className="w-5 h-5" />,
label: t("edit"),
onClick: () => { onEditContact(c.id); },
});
items.push({
id: "delete",
icon: <Trash2 className="w-5 h-5" />,
label: t("delete"),
onClick: () => { onDeleteContact(c); },
destructive: true,
});
if (c.emails && Object.keys(c.emails).length > 0) {
const contactEmails = c.emails;
items.push({
id: "send-email",
icon: <Mail className="w-5 h-5" />,
label: t("send_email"),
onClick: () => {
const values = Object.values(contactEmails);
if (values[0]?.address) {
window.location.href = `mailto:${values[0].address}`;
}
},
});
}
items.push({
id: "export",
icon: <Download className="w-5 h-5" />,
label: t("export"),
onClick: () => { onBulkExport(); },
});
return items;
}, [radialMenuContact, t, onEditContact, onDeleteContact, onBulkExport]);
const [filtersOpen, setFiltersOpen] = useState(false);
const [filters, setFilters] = useState<ListFilters>(EMPTY_FILTERS);
const activeFilters = countActiveFilters(filters);
@@ -571,7 +629,7 @@ export function ContactList({
e.stopPropagation();
onToggleSelection(contact.id);
}}
onContextMenu={(e, c) => openContextMenu(e, c)}
onContextMenu={(e, c) => { openContextMenu(e, c); openRadialMenu(e, c); }}
/>
);
return groupByLetter ? (
@@ -592,6 +650,14 @@ export function ContactList({
)}
</div>
{/* Radial Action Menu */}
<RadialMenu
items={radialMenuItems}
isOpen={radialMenuOpen}
position={radialMenuPos}
onClose={closeRadialMenu}
/>
{contextMenu.data && (
<ContactContextMenu
contact={contextMenu.data}