Resizable Columns - Add ResizeHandle component with mouse drag, keyboard (Arrow keys), and double-click to reset to default width - Add sidebarWidth/emailListWidth to ui-store with clamping + persistence - Wire resize handles between sidebar/email-list panels on desktop Navigation Rail Overhaul - Move StorageQuotaCircle, push-status, sign-out from Sidebar to NavigationRail - Interactive SVG ring with popover breakdown (used/free/total) - Sidebar collapse state lifted to ui-store - Show total email count per mailbox alongside unread badge Email Multi-Selection & Drag-and-Drop - Ctrl+Click (toggle) and Shift+Click (range) on all list items - Add selectRangeEmails and lastSelectedEmailId to email-store - Enable drag-and-drop on thread items and thread headers - useEmailDrag accepts optional threadEmails for full-thread drag Email Viewer Layout - Remove card wrapper for cleaner full-width reading - Always render HTML body when available - Adjust skeleton loader to match flat layout Modal & UI Polish - Standardise backdrops, close buttons, padding, border-radius, transitions - Migrate template-string classNames to cn() in settings - Unify focus-ring token to ring-ring on form controls i18n - Add storage_used/free/total keys to all 8 locales Dev Mock JMAP Server (new, gated by DEV_MOCK_JMAP=true) - Session, Mailbox/Email/Thread/Identity CRUD, back-references, upload - GET /download with Content-Disposition, GET /eventsource SSE Tests (46 new) - ui-store (13), email-selection (10), resize-handle (9), mock-server (14)
58 lines
1.7 KiB
TypeScript
58 lines
1.7 KiB
TypeScript
"use client";
|
|
|
|
import { useTranslations } from "next-intl";
|
|
import { cn } from "@/lib/utils";
|
|
import type { Calendar } from "@/lib/jmap/types";
|
|
|
|
interface CalendarSidebarPanelProps {
|
|
calendars: Calendar[];
|
|
selectedCalendarIds: string[];
|
|
onToggleVisibility: (id: string) => void;
|
|
}
|
|
|
|
export function CalendarSidebarPanel({
|
|
calendars,
|
|
selectedCalendarIds,
|
|
onToggleVisibility,
|
|
}: CalendarSidebarPanelProps) {
|
|
const t = useTranslations("calendar");
|
|
|
|
if (calendars.length === 0) return null;
|
|
|
|
return (
|
|
<div className="mt-4">
|
|
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2 px-1">
|
|
{t("my_calendars")}
|
|
</h3>
|
|
<div className="space-y-0.5">
|
|
{calendars.map((cal) => {
|
|
const isVisible = selectedCalendarIds.includes(cal.id);
|
|
const color = cal.color || "#3b82f6";
|
|
|
|
return (
|
|
<button
|
|
key={cal.id}
|
|
onClick={() => onToggleVisibility(cal.id)}
|
|
className={cn(
|
|
"flex items-center gap-2 w-full px-1.5 py-1 rounded-md text-sm transition-colors duration-150",
|
|
"hover:bg-muted"
|
|
)}
|
|
>
|
|
<span
|
|
className={cn(
|
|
"w-3 h-3 rounded-sm border-2 flex-shrink-0 transition-colors",
|
|
isVisible ? "border-transparent" : "border-muted-foreground/40 bg-transparent"
|
|
)}
|
|
style={isVisible ? { backgroundColor: color, borderColor: color } : undefined}
|
|
/>
|
|
<span className={cn("truncate", !isVisible && "text-muted-foreground")}>
|
|
{cal.name}
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|