Files
SRCmail/components/layout/resize-handle.tsx
T
Linus Rath cf02f587de feat: resizable columns, nav rail overhaul, multi-select & drag-drop, UI polish
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)
2026-03-09 16:46:25 +01:00

80 lines
2.3 KiB
TypeScript

"use client";
import { useCallback, useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
interface ResizeHandleProps {
onResize: (delta: number) => void;
onResizeEnd?: () => void;
onDoubleClick?: () => void;
className?: string;
}
const KEYBOARD_STEP = 10;
export function ResizeHandle({ onResize, onResizeEnd, onDoubleClick, className }: ResizeHandleProps) {
const isDragging = useRef(false);
const lastX = useRef(0);
const handleMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault();
isDragging.current = true;
lastX.current = e.clientX;
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
}, []);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
let delta = 0;
if (e.key === "ArrowLeft") delta = -KEYBOARD_STEP;
else if (e.key === "ArrowRight") delta = KEYBOARD_STEP;
else return;
e.preventDefault();
onResize(delta);
onResizeEnd?.();
}, [onResize, onResizeEnd]);
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging.current) return;
const delta = e.clientX - lastX.current;
lastX.current = e.clientX;
onResize(delta);
};
const handleMouseUp = () => {
if (!isDragging.current) return;
isDragging.current = false;
document.body.style.cursor = "";
document.body.style.userSelect = "";
onResizeEnd?.();
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [onResize, onResizeEnd]);
return (
<div
role="separator"
aria-orientation="vertical"
aria-label="Resize"
tabIndex={0}
onMouseDown={handleMouseDown}
onKeyDown={handleKeyDown}
onDoubleClick={onDoubleClick}
className={cn(
"w-1 flex-shrink-0 cursor-col-resize hover:bg-primary/30 active:bg-primary/50 transition-colors relative group",
"focus-visible:outline-none focus-visible:bg-primary/40 focus-visible:ring-2 focus-visible:ring-primary/50",
className
)}
>
<div className="absolute inset-y-0 -left-1 -right-1" />
</div>
);
}