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
+48
View File
@@ -76,6 +76,7 @@ import {
PlayCircle,
PenSquare,
CalendarClock,
CalendarPlus,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "@/i18n/navigation";
@@ -88,6 +89,8 @@ import { useDeviceDetection } from "@/hooks/use-media-query";
import { useAuthStore } from "@/stores/auth-store";
import { useAccountStore } from "@/stores/account-store";
import { useEmailStore } from "@/stores/email-store";
import { useCalendarStore } from "@/stores/calendar-store";
import { usePolicyStore } from "@/stores/policy-store";
import { useThemeStore } from "@/stores/theme-store";
import { EmailIdentityBadge } from "./email-identity-badge";
import { UnsubscribeBanner } from "./unsubscribe-banner";
@@ -703,6 +706,9 @@ export function EmailViewer({
const isScheduled = email?.isScheduled === true;
const canCancelScheduled = isScheduled && email?.scheduledUndoStatus === 'pending';
const calendarEnabled = usePolicyStore((s) => s.isFeatureEnabled('calendarEnabled'));
const createAppointmentVisible = !isScheduled && !isDraft && calendarEnabled && !!email;
// Tablet list visibility
const { isTablet, isMobile } = useDeviceDetection();
@@ -1029,6 +1035,34 @@ export function EmailViewer({
const { isMobile: isMobileDevice } = useDeviceDetection();
const router = useRouter();
const handleCreateAppointment = useCallback(() => {
if (!email) return;
const subject = email.subject ? `Re: ${email.subject}` : "";
const body = email.htmlBody?.[0]?.partId
? email.bodyValues?.[email.htmlBody[0].partId]?.value || ""
: "";
const participants: { name?: string; email: string }[] = [];
const seen = new Set<string>();
const addParticipant = (p?: { name?: string; email?: string }) => {
if (!p?.email) return;
const normalized = p.email.toLowerCase();
if (!seen.has(normalized)) {
seen.add(normalized);
participants.push({ name: p.name, email: p.email });
}
};
if (email.from) email.from.forEach(addParticipant);
if (email.to) email.to.forEach(addParticipant);
if (email.cc) email.cc.forEach(addParticipant);
useCalendarStore.getState().setNewEventPrefill({
title: subject,
description: body,
participants,
date: email.receivedAt,
});
router.push('/calendar');
}, [email, router]);
const handleViewContactSidebar = (contact: ContactCard | null, recipientEmail: string) => {
if (isMobileDevice) {
// No room for a sidebar on mobile - send the user to the contacts page
@@ -2909,6 +2943,20 @@ export function EmailViewer({
<Forward className="w-4 h-4" />
{showToolbarLabels && <span className="hidden sm:inline text-sm">{t('forward')}</span>}
</Button>
{createAppointmentVisible && (
<Button
variant="ghost"
size="sm"
onClick={handleCreateAppointment}
data-overflow-item
data-overflow-priority="3.5"
className="hidden sm:flex sm:flex-row sm:h-8 sm:gap-1.5 sm:py-0"
title={t('create_appointment')}
>
<CalendarPlus className="w-4 h-4" />
{showToolbarLabels && <span className="hidden sm:inline text-sm">{t('create_appointment')}</span>}
</Button>
)}
</>)}
<PluginSlot name="toolbar-actions" />
</div>