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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user