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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user