feat(mail): resizable columns, account in folder tree, mail settings in CRM settings

- Add ResizablePanel component (components/ui/ResizablePanel.tsx) with
  drag-to-resize via mouse events, no external dependencies
- Replace fixed-width columns in Mail.tsx with ResizablePanel for all
  three panes (folder tree, mail list, mail detail)
- Remove account-select toolbar item from Mail.tsx; account selection
  is now integrated into the folder tree
- Restructure MailFolderTree to show mail accounts as top-level nodes
  with folders nested underneath, built from flat list via parent_id
- Add expand/collapse chevrons for accounts and folders with children
- Load folders for ALL accounts (not just selected one) so multi-account
  tree works
- Derive selectedAccountId from selected folder instead of separate
  account selector
- Add Mail tab to CRM Settings page (Settings.tsx) with 📧 icon
- Add /settings/mail route pointing to MailSettingsPage
- Update settings link in mail toolbar to point to /settings/mail
- Update no-accounts empty state link to /settings/mail
This commit is contained in:
Agent Zero
2026-07-15 13:37:22 +02:00
parent 5693fe1c3a
commit 0409a08002
5 changed files with 447 additions and 170 deletions
@@ -0,0 +1,96 @@
/**
* ResizablePanel — a flex panel with drag-to-resize handle.
* No external dependencies; uses React + mouse events only.
*/
import React, { useState, useRef, useCallback, useEffect } from 'react';
import clsx from 'clsx';
export interface ResizablePanelProps {
/** Initial width in pixels (only used when resizable=true) */
initialWidth?: number;
/** Minimum width in pixels */
minWidth?: number;
/** Maximum width in pixels */
maxWidth?: number;
/** Panel content */
children: React.ReactNode;
/** Extra className for the panel container */
className?: string;
/** When false, panel becomes flex-1 with no drag handle */
resizable?: boolean;
/** testid forwarded to the container div */
'data-testid'?: string;
}
export function ResizablePanel({
initialWidth = 224,
minWidth = 150,
maxWidth = 600,
children,
className,
resizable = true,
...rest
}: ResizablePanelProps) {
const [width, setWidth] = useState(initialWidth);
const isResizing = useRef(false);
const startX = useRef(0);
const startWidth = useRef(0);
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
isResizing.current = true;
startX.current = e.clientX;
startWidth.current = width;
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
},
[width],
);
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!isResizing.current) return;
const delta = e.clientX - startX.current;
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth.current + delta));
setWidth(newWidth);
};
const handleMouseUp = () => {
if (isResizing.current) {
isResizing.current = false;
document.body.style.cursor = '';
document.body.style.userSelect = '';
}
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [minWidth, maxWidth]);
return (
<div
className={clsx(resizable ? 'relative flex-shrink-0' : 'relative flex-1', className)}
style={resizable ? { width: `${width}px` } : undefined}
data-testid={rest['data-testid']}
>
{children}
{resizable && (
<div
className="absolute top-0 right-0 h-full w-1 cursor-col-resize bg-transparent hover:bg-primary-300 active:bg-primary-400 transition-colors z-10"
onMouseDown={handleMouseDown}
data-testid="resize-handle"
role="separator"
aria-orientation="vertical"
/>
)}
</div>
);
}