fix: make density setting functional across entire UI

Rename "List Density" to "Density" and apply spacing changes globally
instead of only to email list item heights.

- Replace hardcoded padding/gap values with CSS custom properties
  (--density-item-py, --density-item-gap, --density-header-py,
  --density-card-p, --density-sidebar-py) set via JS on :root
- Apply density-responsive spacing to email list items, thread views,
  email viewer, sidebar, navigation rail, contacts, and calendar
- Use inline styles instead of Tailwind arbitrary value classes to
  avoid Turbopack compilation hangs
- Rename listDensity → density in store, types, and components
- Add persist migration (v1 → v2) and onRehydrateStorage callback
- Update all 8 locale files with broadened labels/descriptions
This commit is contained in:
Linus Rath
2026-03-14 18:35:43 +01:00
parent 86673a7b45
commit 838de8e5da
23 changed files with 133 additions and 70 deletions
+1 -1
View File
@@ -87,7 +87,7 @@ export function EmailListItem({ email, selected, onClick, onContextMenu }: Email
onContextMenu={handleContextMenu}
style={{ minHeight: 'var(--list-item-height)' }}
>
<div className="flex items-start gap-3 px-4 py-3">
<div className="flex items-start px-4" style={{ gap: 'var(--density-item-gap)', paddingBlock: 'var(--density-item-py)' }}>
{/* Checkbox - only visible when in selection mode */}
{selectedEmailIds.size > 0 && (
<button
+5 -5
View File
@@ -96,13 +96,13 @@ export function EmailList({
const [isProcessing, setIsProcessing] = useState(false);
const parentRef = useRef<HTMLDivElement>(null);
const listDensity = useSettingsStore((state) => state.listDensity);
const density = useSettingsStore((state) => state.density);
const showPreview = useSettingsStore((state) => state.showPreview);
const estimateSize = useCallback(() => {
const base = { compact: 72, regular: 88, comfortable: 104 }[listDensity];
return showPreview ? base + 40 : base;
}, [listDensity, showPreview]);
const base = { compact: 60, regular: 84, comfortable: 104 }[density];
return showPreview ? base + 36 : base;
}, [density, showPreview]);
const virtualizer = useVirtualizer({
count: threadGroups.length,
@@ -217,7 +217,7 @@ export function EmailList({
useEffect(() => {
virtualizer.measure();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [listDensity, showPreview]);
}, [density, showPreview]);
return (
<div className={cn("flex flex-col min-h-0", className)}>
+5 -5
View File
@@ -1309,7 +1309,7 @@ export function EmailViewer({
"bg-background border-b border-border",
toolbarPosition === 'below-subject' && "max-lg:sticky max-lg:top-0 max-lg:z-10"
)}>
<div className="px-4 lg:px-6 py-3 lg:py-4">
<div className="px-4 lg:px-6" style={{ paddingBlock: 'var(--density-header-py)' }}>
<div className="flex items-start justify-between gap-2 lg:gap-4">
{/* Back button (for below-subject mode on tablet) */}
{toolbarPosition === 'below-subject' && isTablet && !tabletListVisible && onBack && (
@@ -1644,8 +1644,8 @@ export function EmailViewer({
)}
{/* === SENDER INFO (Desktop) === */}
<div className="hidden lg:block bg-background border-b border-border px-6 py-4">
<div className="flex items-start gap-4">
<div className="hidden lg:block bg-background border-b border-border px-6" style={{ paddingBlock: 'var(--density-header-py)' }}>
<div className="flex items-start" style={{ gap: 'var(--density-item-gap)' }}>
<button
onClick={() => sender?.email && handleViewContactSidebar(null, sender.email)}
className="cursor-pointer group flex-shrink-0"
@@ -2175,8 +2175,8 @@ export function EmailViewer({
{/* Email Content Area */}
<div className="flex-1 overflow-auto bg-muted/30">
{/* Mobile/Tablet Sender Info - scrolls with content */}
<div className="lg:hidden bg-background border-b border-border px-4 py-3">
<div className="flex items-start gap-3">
<div className="lg:hidden bg-background border-b border-border px-4" style={{ paddingBlock: 'var(--density-header-py)' }}>
<div className="flex items-start" style={{ gap: 'var(--density-item-gap)' }}>
<button
onClick={() => sender?.email && handleViewContactSidebar(null, sender.email)}
className="cursor-pointer group flex-shrink-0"
@@ -139,7 +139,7 @@ export function ThreadConversationView({
return (
<div className="flex flex-col h-full bg-background">
{/* Header */}
<div className="flex items-center gap-3 px-4 py-3 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 sticky top-0 z-10">
<div className="flex items-center px-4 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 sticky top-0 z-10" style={{ gap: 'var(--density-item-gap)', paddingBlock: 'var(--density-header-py)' }}>
<button
onClick={onBack}
className="p-2 -ml-2 rounded-full hover:bg-muted transition-colors"
@@ -158,7 +158,7 @@ export function ThreadConversationView({
{/* Email Cards */}
<div className="flex-1 overflow-y-auto">
<div className="p-4 space-y-3">
<div className="space-y-3" style={{ padding: 'var(--density-card-p)' }}>
{emails.map((email, index) => {
const senderEmail = email.from?.[0]?.email?.toLowerCase();
const senderIsTrusted = senderEmail ? isSenderTrusted(senderEmail) : false;
@@ -423,9 +423,10 @@ function EmailCard({
<button
onClick={onToggleExpanded}
className={cn(
"w-full flex items-start gap-3 p-4 text-left transition-colors",
"w-full flex items-start text-left transition-colors",
!isExpanded && "hover:bg-muted/50"
)}
style={{ gap: 'var(--density-item-gap)', padding: 'var(--density-card-p)' }}
>
<Avatar
name={sender?.name}
@@ -503,7 +504,7 @@ function EmailCard({
)}
{/* Email Body */}
<div className="px-4 py-4">
<div style={{ padding: 'var(--density-card-p)' }}>
<div
className={cn(
"prose prose-sm max-w-none dark:prose-invert",
+2 -1
View File
@@ -61,7 +61,7 @@ export function ThreadEmailItem({
{...dragHandlers}
className={cn(
"relative cursor-pointer transition-all duration-150",
"pl-12 pr-4 py-2.5",
"pl-12 pr-4",
"border-l-2 border-l-transparent",
selected
? "bg-blue-200 dark:bg-blue-900/50 border-l-primary"
@@ -73,6 +73,7 @@ export function ThreadEmailItem({
)}
onClick={handleClick}
onContextMenu={handleContextMenu}
style={{ paddingBlock: 'var(--density-item-py)' }}
>
<div className="flex items-start gap-3">
{/* Checkbox - only visible when in selection mode */}
+2 -2
View File
@@ -101,7 +101,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
onContextMenu={handleContextMenu}
style={{ minHeight: 'var(--list-item-height)' }}
>
<div className="flex items-start gap-3 px-3 py-3">
<div className="flex items-start px-3" style={{ gap: 'var(--density-item-gap)', paddingBlock: 'var(--density-item-py)' }}>
{/* Checkbox - only visible when in selection mode */}
{selectedEmailIds.size > 0 && (
<button
@@ -324,7 +324,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
onContextMenu={handleContextMenu}
style={{ minHeight: 'var(--list-item-height)' }}
>
<div className="flex items-start gap-3 px-3 py-3">
<div className="flex items-start px-3" style={{ gap: 'var(--density-item-gap)', paddingBlock: 'var(--density-item-py)' }}>
{/* Checkbox for thread selection - only visible when in selection mode */}
{selectedEmailIds.size > 0 && (
<button