fix: tighter folder tree, smaller mail list font, smooth resize via DOM ref

- MailFolderTree: reduce padding/gap ~25% (py-2→py-1, gap-2→gap-1.5)
- MailList: text-sm→text-xs for from and subject lines
- ResizablePanel: use DOM ref for width during drag instead of React state
  - No re-renders during drag, only single setWidth on mouseup
  - panelRef on outer div, contentRef on inner div
  - pointer-events:none on content during drag
  - contain:strict on content area
This commit is contained in:
Agent Zero
2026-07-17 23:49:19 +02:00
parent db433e81f1
commit d029892d27
3 changed files with 29 additions and 9 deletions
@@ -147,7 +147,7 @@ function FolderNode({
<button <button
onClick={() => onSelect(folder.id)} onClick={() => onSelect(folder.id)}
className={clsx( className={clsx(
'flex-1 flex items-center gap-2 px-2 py-2 md:py-1.5 rounded-md text-sm min-h-touch', 'flex-1 flex items-center gap-1.5 px-2 py-1 md:py-1 rounded-md text-sm min-h-touch',
'motion-safe:transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500', 'motion-safe:transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
isSelected isSelected
? 'bg-primary-50 text-primary-700 font-medium' ? 'bg-primary-50 text-primary-700 font-medium'
+2 -2
View File
@@ -184,12 +184,12 @@ export function MailList({
)} )}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className={clsx('text-sm truncate', !mail.is_seen ? 'text-secondary-900 font-semibold' : 'text-secondary-700')}> <span className={clsx('text-xs truncate', !mail.is_seen ? 'text-secondary-900 font-semibold' : 'text-secondary-700')}>
{decodeMimeHeader(mail.from_name) || mail.from_address} {decodeMimeHeader(mail.from_name) || mail.from_address}
</span> </span>
<span className="text-xs text-secondary-400 flex-shrink-0">{formatDate(mail.date)}</span> <span className="text-xs text-secondary-400 flex-shrink-0">{formatDate(mail.date)}</span>
</div> </div>
<p className={clsx('text-xs md:text-sm truncate mt-0.5', !mail.is_seen ? 'text-secondary-800' : 'text-secondary-600')}> <p className={clsx('text-xs truncate mt-0.5', !mail.is_seen ? 'text-secondary-800' : 'text-secondary-600')}>
{decodeMimeHeader(mail.subject) || t('mail.noSubject')} {decodeMimeHeader(mail.subject) || t('mail.noSubject')}
</p> </p>
{mail.labels && mail.labels.length > 0 && ( {mail.labels && mail.labels.length > 0 && (
+26 -6
View File
@@ -6,6 +6,10 @@
* positioned absolutely on the right edge (z-index above scrollbars). * positioned absolutely on the right edge (z-index above scrollbars).
* Scrollable content goes into an inner div so that the scrollbar never * Scrollable content goes into an inner div so that the scrollbar never
* overlaps the drag handle. * overlaps the drag handle.
*
* Performance: during drag, width is applied directly to the DOM via ref —
* no React re-render happens until mouseup. This prevents janky resizing
* when panel children (mail list, mail detail) are expensive to render.
*/ */
import React, { useState, useRef, useCallback, useEffect } from 'react'; import React, { useState, useRef, useCallback, useEffect } from 'react';
@@ -38,21 +42,25 @@ export function ResizablePanel({
...rest ...rest
}: ResizablePanelProps) { }: ResizablePanelProps) {
const [width, setWidth] = useState(initialWidth); const [width, setWidth] = useState(initialWidth);
const [isDragging, setIsDragging] = useState(false);
const isResizing = useRef(false); const isResizing = useRef(false);
const startX = useRef(0); const startX = useRef(0);
const startWidth = useRef(0); const startWidth = useRef(0);
const panelRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const handleMouseDown = useCallback( const handleMouseDown = useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
isResizing.current = true; isResizing.current = true;
setIsDragging(true);
startX.current = e.clientX; startX.current = e.clientX;
startWidth.current = width; startWidth.current = width;
document.body.style.cursor = 'col-resize'; document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none'; document.body.style.userSelect = 'none';
// Disable pointer events on content during drag to prevent reflows
if (contentRef.current) {
contentRef.current.style.pointerEvents = 'none';
}
}, },
[width], [width],
); );
@@ -62,15 +70,27 @@ export function ResizablePanel({
if (!isResizing.current) return; if (!isResizing.current) return;
const delta = e.clientX - startX.current; const delta = e.clientX - startX.current;
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth.current + delta)); const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth.current + delta));
setWidth(newWidth); // Apply width directly to DOM — no React re-render during drag
if (panelRef.current) {
panelRef.current.style.width = `${newWidth}px`;
}
}; };
const handleMouseUp = () => { const handleMouseUp = () => {
if (isResizing.current) { if (isResizing.current) {
isResizing.current = false; isResizing.current = false;
setIsDragging(false);
document.body.style.cursor = ''; document.body.style.cursor = '';
document.body.style.userSelect = ''; document.body.style.userSelect = '';
if (contentRef.current) {
contentRef.current.style.pointerEvents = '';
}
// Sync final width to React state (single re-render on mouseup)
if (panelRef.current) {
const finalWidth = panelRef.current.style.width;
if (finalWidth) {
setWidth(parseInt(finalWidth));
}
}
} }
}; };
@@ -85,6 +105,7 @@ export function ResizablePanel({
return ( return (
<div <div
ref={panelRef}
className={clsx( className={clsx(
resizable ? 'relative flex-shrink-0 flex flex-col' : 'relative flex-1 flex flex-col', resizable ? 'relative flex-shrink-0 flex flex-col' : 'relative flex-1 flex flex-col',
className, className,
@@ -94,11 +115,10 @@ export function ResizablePanel({
> >
{/* Inner scrollable content area — scrollbar stays inside, never overlaps handle */} {/* Inner scrollable content area — scrollbar stays inside, never overlaps handle */}
<div <div
ref={contentRef}
className="flex-1 overflow-y-auto overflow-x-hidden min-h-0" className="flex-1 overflow-y-auto overflow-x-hidden min-h-0"
style={{ style={{
contain: 'strict', contain: 'strict',
pointerEvents: isDragging ? 'none' : 'auto',
willChange: isDragging ? 'width' : undefined,
}} }}
> >
{children} {children}