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)
This commit is contained in:
Linus Rath
2026-03-09 16:46:25 +01:00
parent 58b9b70871
commit cf02f587de
44 changed files with 2181 additions and 314 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ export function ConfirmDialog({
const resolvedCancelText = cancelText || t("cancel");
return (
<div className="fixed inset-0 bg-black/50 backdrop-blur-[2px] flex items-center justify-center z-[60] p-4 animate-in fade-in duration-150">
<div className="fixed inset-0 bg-black/50 backdrop-blur-[1px] flex items-center justify-center z-[60] p-4 animate-in fade-in duration-150">
<div
ref={dialogRef}
role="alertdialog"
+4 -4
View File
@@ -74,8 +74,8 @@ export function ContextMenuItem({
role="menuitem"
disabled={disabled}
className={cn(
"w-full px-3 py-2 text-sm text-left flex items-center gap-2",
"transition-colors duration-100",
"w-full px-3 py-1.5 text-sm text-left flex items-center gap-2",
"transition-colors duration-150",
"focus:outline-none focus:bg-muted",
disabled && "opacity-50 cursor-not-allowed",
!disabled && "hover:bg-muted cursor-pointer",
@@ -157,8 +157,8 @@ export function ContextMenuSubMenu({
>
<div
className={cn(
"w-full px-3 py-2 text-sm flex items-center gap-2",
"transition-colors duration-100 cursor-pointer",
"w-full px-3 py-1.5 text-sm flex items-center gap-2",
"transition-colors duration-150 cursor-pointer",
"hover:bg-muted"
)}
role="menuitem"
+2 -2
View File
@@ -69,10 +69,10 @@ export function WelcomeBanner() {
</div>
<button
onClick={dismiss}
className="flex-shrink-0 p-1 rounded hover:bg-muted transition-colors"
className="flex-shrink-0 p-1.5 rounded-md hover:bg-muted transition-colors duration-150 text-muted-foreground hover:text-foreground"
aria-label={t("dismiss")}
>
<X className="w-4 h-4 text-muted-foreground" />
<X className="w-4 h-4" />
</button>
</div>
<div className="mt-3 flex justify-end">