feat: add extra-compact density option and fix font size scaling
- Add 'extra-compact' density level that hides avatars and preview text, showing only sender and subject for maximum information density - Add visual density preview in appearance settings showing a 3-row email list mockup that reflects each density option - Fix global font size setting only changing line height by moving font-size from body to :root so rem-based Tailwind classes scale - Apply extra-compact behavior to email list items, thread list items, thread email items, and thread conversation view (EmailCard) - Update email list virtualizer size estimates for extra-compact - Add extra-compact translations for all 8 locales
This commit is contained in:
@@ -24,6 +24,7 @@ export function EmailListItem({ email, selected, onClick, onContextMenu }: Email
|
||||
const t = useTranslations('email_viewer');
|
||||
const { selectedEmailIds, toggleEmailSelection, selectRangeEmails, selectedMailbox, clearSelection } = useEmailStore();
|
||||
const showPreview = useSettingsStore((state) => state.showPreview);
|
||||
const density = useSettingsStore((state) => state.density);
|
||||
const emailKeywords = useSettingsStore((state) => state.emailKeywords);
|
||||
const { identities } = useAuthStore();
|
||||
const isChecked = selectedEmailIds.has(email.id);
|
||||
@@ -116,12 +117,14 @@ export function EmailListItem({ email, selected, onClick, onContextMenu }: Email
|
||||
)}
|
||||
|
||||
{/* Avatar */}
|
||||
<Avatar
|
||||
name={sender?.name}
|
||||
email={sender?.email}
|
||||
size="md"
|
||||
className="flex-shrink-0 shadow-sm"
|
||||
/>
|
||||
{density !== 'extra-compact' && (
|
||||
<Avatar
|
||||
name={sender?.name}
|
||||
email={sender?.email}
|
||||
size="md"
|
||||
className="flex-shrink-0 shadow-sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -183,7 +186,7 @@ export function EmailListItem({ email, selected, onClick, onContextMenu }: Email
|
||||
</div>
|
||||
|
||||
{/* Third Line: Preview (controlled by showPreview setting) */}
|
||||
{showPreview && (
|
||||
{showPreview && density !== 'extra-compact' && (
|
||||
<p className={cn(
|
||||
"text-sm leading-relaxed line-clamp-2",
|
||||
isUnread
|
||||
|
||||
@@ -100,8 +100,8 @@ export function EmailList({
|
||||
const showPreview = useSettingsStore((state) => state.showPreview);
|
||||
|
||||
const estimateSize = useCallback(() => {
|
||||
const base = { compact: 60, regular: 84, comfortable: 104 }[density];
|
||||
return showPreview ? base + 36 : base;
|
||||
const base = { 'extra-compact': 32, compact: 60, regular: 84, comfortable: 104 }[density];
|
||||
return (showPreview && density !== 'extra-compact') ? base + 36 : base;
|
||||
}, [density, showPreview]);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
|
||||
@@ -221,6 +221,7 @@ function EmailCard({
|
||||
}: EmailCardProps) {
|
||||
const t = useTranslations();
|
||||
const resolvedTheme = useThemeStore((state) => state.resolvedTheme);
|
||||
const density = useSettingsStore((state) => state.density);
|
||||
const sender = email.from?.[0];
|
||||
const isUnread = !email.keywords?.$seen;
|
||||
const isStarred = email.keywords?.$flagged;
|
||||
@@ -428,12 +429,14 @@ function EmailCard({
|
||||
)}
|
||||
style={{ gap: 'var(--density-item-gap)', padding: 'var(--density-card-p)' }}
|
||||
>
|
||||
<Avatar
|
||||
name={sender?.name}
|
||||
email={sender?.email}
|
||||
size="md"
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
{density !== 'extra-compact' && (
|
||||
<Avatar
|
||||
name={sender?.name}
|
||||
email={sender?.email}
|
||||
size="md"
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-0.5">
|
||||
<span className={cn(
|
||||
@@ -452,7 +455,7 @@ function EmailCard({
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{formatDate(email.receivedAt)}
|
||||
</div>
|
||||
{!isExpanded && (
|
||||
{!isExpanded && density !== 'extra-compact' && (
|
||||
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
|
||||
{email.preview || "No preview available"}
|
||||
</p>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Avatar } from "@/components/ui/avatar";
|
||||
import { Paperclip, Star, Circle, CheckSquare, Square } from "lucide-react";
|
||||
import { useEmailDrag } from "@/hooks/use-email-drag";
|
||||
import { useEmailStore } from "@/stores/email-store";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
|
||||
interface ThreadEmailItemProps {
|
||||
email: Email;
|
||||
@@ -27,6 +28,7 @@ export function ThreadEmailItem({
|
||||
const isStarred = email.keywords?.$flagged;
|
||||
const sender = email.from?.[0];
|
||||
const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection } = useEmailStore();
|
||||
const density = useSettingsStore((state) => state.density);
|
||||
const isChecked = selectedEmailIds.has(email.id);
|
||||
|
||||
const { dragHandlers, isDragging } = useEmailDrag({
|
||||
@@ -104,12 +106,14 @@ export function ThreadEmailItem({
|
||||
)}
|
||||
|
||||
{/* Small Avatar */}
|
||||
<Avatar
|
||||
name={sender?.name}
|
||||
email={sender?.email}
|
||||
size="sm"
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
{density !== 'extra-compact' && (
|
||||
<Avatar
|
||||
name={sender?.name}
|
||||
email={sender?.email}
|
||||
size="sm"
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
|
||||
@@ -42,6 +42,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
||||
const sender = email.from?.[0];
|
||||
const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection } = useEmailStore();
|
||||
const emailKeywords = useSettingsStore((state) => state.emailKeywords);
|
||||
const density = useSettingsStore((state) => state.density);
|
||||
const isChecked = selectedEmailIds.has(email.id);
|
||||
|
||||
// Resolve color and keyword definition from keyword definitions if not passed directly
|
||||
@@ -128,12 +129,14 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Avatar
|
||||
name={sender?.name}
|
||||
email={sender?.email}
|
||||
size="md"
|
||||
className="flex-shrink-0 shadow-sm"
|
||||
/>
|
||||
{density !== 'extra-compact' && (
|
||||
<Avatar
|
||||
name={sender?.name}
|
||||
email={sender?.email}
|
||||
size="md"
|
||||
className="flex-shrink-0 shadow-sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2 mb-1">
|
||||
@@ -185,7 +188,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
||||
{email.subject || "(no subject)"}
|
||||
</div>
|
||||
|
||||
{showPreview && (
|
||||
{showPreview && density !== 'extra-compact' && (
|
||||
<p className={cn(
|
||||
"text-sm leading-relaxed line-clamp-2",
|
||||
isUnread
|
||||
@@ -216,6 +219,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
||||
}, ref) {
|
||||
const t = useTranslations('threads');
|
||||
const showPreview = useSettingsStore((state) => state.showPreview);
|
||||
const density = useSettingsStore((state) => state.density);
|
||||
const isMobile = useUIStore((state) => state.isMobile);
|
||||
const { latestEmail, participantNames, hasUnread, hasStarred, hasAttachment, emailCount } = thread;
|
||||
|
||||
@@ -377,12 +381,14 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Avatar
|
||||
name={latestEmail.from?.[0]?.name}
|
||||
email={latestEmail.from?.[0]?.email}
|
||||
size="md"
|
||||
className="flex-shrink-0 shadow-sm"
|
||||
/>
|
||||
{density !== 'extra-compact' && (
|
||||
<Avatar
|
||||
name={latestEmail.from?.[0]?.name}
|
||||
email={latestEmail.from?.[0]?.email}
|
||||
size="md"
|
||||
className="flex-shrink-0 shadow-sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2 mb-1">
|
||||
@@ -446,7 +452,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
||||
{latestEmail.subject || "(no subject)"}
|
||||
</div>
|
||||
|
||||
{showPreview && (
|
||||
{showPreview && density !== 'extra-compact' && (
|
||||
<p className={cn(
|
||||
"text-sm leading-relaxed line-clamp-2",
|
||||
hasUnread
|
||||
|
||||
Reference in New Issue
Block a user