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:
@@ -23,6 +23,13 @@
|
|||||||
--font-size-base: 16px;
|
--font-size-base: 16px;
|
||||||
--list-item-height: 48px;
|
--list-item-height: 48px;
|
||||||
--transition-duration: 0.2s;
|
--transition-duration: 0.2s;
|
||||||
|
|
||||||
|
/* Density spacing variables (defaults = regular) */
|
||||||
|
--density-item-py: 12px;
|
||||||
|
--density-item-gap: 12px;
|
||||||
|
--density-header-py: 12px;
|
||||||
|
--density-card-p: 16px;
|
||||||
|
--density-sidebar-py: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
|
|||||||
@@ -146,7 +146,8 @@ export function CalendarAgendaView({
|
|||||||
onClick={(e) => onSelectEvent(ev, e.currentTarget.getBoundingClientRect())}
|
onClick={(e) => onSelectEvent(ev, e.currentTarget.getBoundingClientRect())}
|
||||||
onMouseEnter={(e) => onHoverEvent?.(ev, e.currentTarget.getBoundingClientRect())}
|
onMouseEnter={(e) => onHoverEvent?.(ev, e.currentTarget.getBoundingClientRect())}
|
||||||
onMouseLeave={() => onHoverLeave?.()}
|
onMouseLeave={() => onHoverLeave?.()}
|
||||||
className="w-full flex items-start gap-3 px-4 py-3 hover:bg-muted/50 transition-colors text-left"
|
className="w-full flex items-start px-4 hover:bg-muted/50 transition-colors text-left"
|
||||||
|
style={{ gap: 'var(--density-item-gap)', paddingBlock: 'var(--density-item-py)' }}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center pt-0.5 min-w-[60px]">
|
<div className="flex flex-col items-center pt-0.5 min-w-[60px]">
|
||||||
{ev.showWithoutTime ? (
|
{ev.showWithoutTime ? (
|
||||||
|
|||||||
@@ -69,10 +69,11 @@ export function ContactGroupList({
|
|||||||
key={group.id}
|
key={group.id}
|
||||||
onClick={() => onSelectGroup(group.id)}
|
onClick={() => onSelectGroup(group.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full flex items-center gap-3 px-4 py-3 text-left transition-colors",
|
"w-full flex items-center px-4 text-left transition-colors",
|
||||||
"hover:bg-muted",
|
"hover:bg-muted",
|
||||||
group.id === selectedGroupId && "bg-accent text-accent-foreground"
|
group.id === selectedGroupId && "bg-accent text-accent-foreground"
|
||||||
)}
|
)}
|
||||||
|
style={{ gap: 'var(--density-item-gap)', paddingBlock: 'var(--density-item-py)' }}
|
||||||
>
|
>
|
||||||
<div className="w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
|
<div className="w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||||
<Users className="w-4 h-4 text-primary" />
|
<Users className="w-4 h-4 text-primary" />
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ export function ContactListItem({ contact, isSelected, onClick }: ContactListIte
|
|||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full flex items-center gap-3 px-4 py-3 text-left transition-colors",
|
"w-full flex items-center px-4 text-left transition-colors",
|
||||||
"hover:bg-muted",
|
"hover:bg-muted",
|
||||||
isSelected && "bg-accent text-accent-foreground"
|
isSelected && "bg-accent text-accent-foreground"
|
||||||
)}
|
)}
|
||||||
|
style={{ gap: 'var(--density-item-gap)', paddingBlock: 'var(--density-item-py)' }}
|
||||||
>
|
>
|
||||||
<Avatar name={name} email={email} size="sm" />
|
<Avatar name={name} email={email} size="sm" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
|
|||||||
@@ -204,7 +204,8 @@ export function ContactList({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onToggleSelection(contact.id);
|
onToggleSelection(contact.id);
|
||||||
}}
|
}}
|
||||||
className="pl-4 pr-1 py-3 flex-shrink-0"
|
className="pl-4 pr-1 flex-shrink-0"
|
||||||
|
style={{ paddingBlock: 'var(--density-item-py)' }}
|
||||||
>
|
>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"w-4 h-4 rounded border flex items-center justify-center transition-colors",
|
"w-4 h-4 rounded border flex items-center justify-center transition-colors",
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export function EmailListItem({ email, selected, onClick, onContextMenu }: Email
|
|||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
style={{ minHeight: 'var(--list-item-height)' }}
|
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 */}
|
{/* Checkbox - only visible when in selection mode */}
|
||||||
{selectedEmailIds.size > 0 && (
|
{selectedEmailIds.size > 0 && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -96,13 +96,13 @@ export function EmailList({
|
|||||||
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const parentRef = useRef<HTMLDivElement>(null);
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
const listDensity = useSettingsStore((state) => state.listDensity);
|
const density = useSettingsStore((state) => state.density);
|
||||||
const showPreview = useSettingsStore((state) => state.showPreview);
|
const showPreview = useSettingsStore((state) => state.showPreview);
|
||||||
|
|
||||||
const estimateSize = useCallback(() => {
|
const estimateSize = useCallback(() => {
|
||||||
const base = { compact: 72, regular: 88, comfortable: 104 }[listDensity];
|
const base = { compact: 60, regular: 84, comfortable: 104 }[density];
|
||||||
return showPreview ? base + 40 : base;
|
return showPreview ? base + 36 : base;
|
||||||
}, [listDensity, showPreview]);
|
}, [density, showPreview]);
|
||||||
|
|
||||||
const virtualizer = useVirtualizer({
|
const virtualizer = useVirtualizer({
|
||||||
count: threadGroups.length,
|
count: threadGroups.length,
|
||||||
@@ -217,7 +217,7 @@ export function EmailList({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
virtualizer.measure();
|
virtualizer.measure();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [listDensity, showPreview]);
|
}, [density, showPreview]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-col min-h-0", className)}>
|
<div className={cn("flex flex-col min-h-0", className)}>
|
||||||
|
|||||||
@@ -1309,7 +1309,7 @@ export function EmailViewer({
|
|||||||
"bg-background border-b border-border",
|
"bg-background border-b border-border",
|
||||||
toolbarPosition === 'below-subject' && "max-lg:sticky max-lg:top-0 max-lg:z-10"
|
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">
|
<div className="flex items-start justify-between gap-2 lg:gap-4">
|
||||||
{/* Back button (for below-subject mode on tablet) */}
|
{/* Back button (for below-subject mode on tablet) */}
|
||||||
{toolbarPosition === 'below-subject' && isTablet && !tabletListVisible && onBack && (
|
{toolbarPosition === 'below-subject' && isTablet && !tabletListVisible && onBack && (
|
||||||
@@ -1644,8 +1644,8 @@ export function EmailViewer({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* === SENDER INFO (Desktop) === */}
|
{/* === SENDER INFO (Desktop) === */}
|
||||||
<div className="hidden lg:block bg-background border-b border-border px-6 py-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 gap-4">
|
<div className="flex items-start" style={{ gap: 'var(--density-item-gap)' }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => sender?.email && handleViewContactSidebar(null, sender.email)}
|
onClick={() => sender?.email && handleViewContactSidebar(null, sender.email)}
|
||||||
className="cursor-pointer group flex-shrink-0"
|
className="cursor-pointer group flex-shrink-0"
|
||||||
@@ -2175,8 +2175,8 @@ export function EmailViewer({
|
|||||||
{/* Email Content Area */}
|
{/* Email Content Area */}
|
||||||
<div className="flex-1 overflow-auto bg-muted/30">
|
<div className="flex-1 overflow-auto bg-muted/30">
|
||||||
{/* Mobile/Tablet Sender Info - scrolls with content */}
|
{/* Mobile/Tablet Sender Info - scrolls with content */}
|
||||||
<div className="lg:hidden bg-background border-b border-border px-4 py-3">
|
<div className="lg:hidden bg-background border-b border-border px-4" style={{ paddingBlock: 'var(--density-header-py)' }}>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start" style={{ gap: 'var(--density-item-gap)' }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => sender?.email && handleViewContactSidebar(null, sender.email)}
|
onClick={() => sender?.email && handleViewContactSidebar(null, sender.email)}
|
||||||
className="cursor-pointer group flex-shrink-0"
|
className="cursor-pointer group flex-shrink-0"
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export function ThreadConversationView({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full bg-background">
|
<div className="flex flex-col h-full bg-background">
|
||||||
{/* Header */}
|
{/* 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
|
<button
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="p-2 -ml-2 rounded-full hover:bg-muted transition-colors"
|
className="p-2 -ml-2 rounded-full hover:bg-muted transition-colors"
|
||||||
@@ -158,7 +158,7 @@ export function ThreadConversationView({
|
|||||||
|
|
||||||
{/* Email Cards */}
|
{/* Email Cards */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<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) => {
|
{emails.map((email, index) => {
|
||||||
const senderEmail = email.from?.[0]?.email?.toLowerCase();
|
const senderEmail = email.from?.[0]?.email?.toLowerCase();
|
||||||
const senderIsTrusted = senderEmail ? isSenderTrusted(senderEmail) : false;
|
const senderIsTrusted = senderEmail ? isSenderTrusted(senderEmail) : false;
|
||||||
@@ -423,9 +423,10 @@ function EmailCard({
|
|||||||
<button
|
<button
|
||||||
onClick={onToggleExpanded}
|
onClick={onToggleExpanded}
|
||||||
className={cn(
|
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"
|
!isExpanded && "hover:bg-muted/50"
|
||||||
)}
|
)}
|
||||||
|
style={{ gap: 'var(--density-item-gap)', padding: 'var(--density-card-p)' }}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
name={sender?.name}
|
name={sender?.name}
|
||||||
@@ -503,7 +504,7 @@ function EmailCard({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Email Body */}
|
{/* Email Body */}
|
||||||
<div className="px-4 py-4">
|
<div style={{ padding: 'var(--density-card-p)' }}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"prose prose-sm max-w-none dark:prose-invert",
|
"prose prose-sm max-w-none dark:prose-invert",
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function ThreadEmailItem({
|
|||||||
{...dragHandlers}
|
{...dragHandlers}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative cursor-pointer transition-all duration-150",
|
"relative cursor-pointer transition-all duration-150",
|
||||||
"pl-12 pr-4 py-2.5",
|
"pl-12 pr-4",
|
||||||
"border-l-2 border-l-transparent",
|
"border-l-2 border-l-transparent",
|
||||||
selected
|
selected
|
||||||
? "bg-blue-200 dark:bg-blue-900/50 border-l-primary"
|
? "bg-blue-200 dark:bg-blue-900/50 border-l-primary"
|
||||||
@@ -73,6 +73,7 @@ export function ThreadEmailItem({
|
|||||||
)}
|
)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
|
style={{ paddingBlock: 'var(--density-item-py)' }}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
{/* Checkbox - only visible when in selection mode */}
|
{/* Checkbox - only visible when in selection mode */}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
|||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
style={{ minHeight: 'var(--list-item-height)' }}
|
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 */}
|
{/* Checkbox - only visible when in selection mode */}
|
||||||
{selectedEmailIds.size > 0 && (
|
{selectedEmailIds.size > 0 && (
|
||||||
<button
|
<button
|
||||||
@@ -324,7 +324,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
|||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
style={{ minHeight: 'var(--list-item-height)' }}
|
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 */}
|
{/* Checkbox for thread selection - only visible when in selection mode */}
|
||||||
{selectedEmailIds.size > 0 && (
|
{selectedEmailIds.size > 0 && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ export function NavigationRail({
|
|||||||
"relative flex items-center gap-2.5 rounded-md transition-colors duration-150",
|
"relative flex items-center gap-2.5 rounded-md transition-colors duration-150",
|
||||||
collapsed
|
collapsed
|
||||||
? "justify-center w-10 h-10"
|
? "justify-center w-10 h-10"
|
||||||
: "px-2.5 py-1.5 text-sm",
|
: "px-2.5 text-sm",
|
||||||
"max-lg:min-h-[44px]",
|
"max-lg:min-h-[44px]",
|
||||||
isActive
|
isActive
|
||||||
? "bg-primary/10 text-primary font-medium"
|
? "bg-primary/10 text-primary font-medium"
|
||||||
@@ -237,6 +237,7 @@ export function NavigationRail({
|
|||||||
)}
|
)}
|
||||||
aria-current={isActive ? "page" : undefined}
|
aria-current={isActive ? "page" : undefined}
|
||||||
title={collapsed ? t(item.labelKey) : undefined}
|
title={collapsed ? t(item.labelKey) : undefined}
|
||||||
|
style={collapsed ? undefined : { paddingBlock: 'var(--density-sidebar-py)' }}
|
||||||
>
|
>
|
||||||
<Icon className={cn("w-[18px] h-[18px] flex-shrink-0", isActive && "text-primary")} />
|
<Icon className={cn("w-[18px] h-[18px] flex-shrink-0", isActive && "text-primary")} />
|
||||||
{!collapsed && <span className="truncate">{t(item.labelKey)}</span>}
|
{!collapsed && <span className="truncate">{t(item.labelKey)}</span>}
|
||||||
|
|||||||
@@ -125,8 +125,9 @@ function MailboxTreeItem({
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
{...(globalDragging ? dropHandlers : {})}
|
{...(globalDragging ? dropHandlers : {})}
|
||||||
|
style={{ paddingBlock: 'var(--density-sidebar-py)' }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group w-full flex items-center py-1 lg:py-1 max-lg:py-3 max-lg:min-h-[44px] text-sm transition-all duration-200",
|
"group w-full flex items-center max-lg:min-h-[44px] text-sm transition-all duration-200",
|
||||||
isCollapsed ? "justify-center px-1" : "px-2",
|
isCollapsed ? "justify-center px-1" : "px-2",
|
||||||
isVirtualNode
|
isVirtualNode
|
||||||
? "text-muted-foreground"
|
? "text-muted-foreground"
|
||||||
@@ -163,13 +164,14 @@ function MailboxTreeItem({
|
|||||||
onClick={() => !isVirtualNode && onMailboxSelect?.(node.id)}
|
onClick={() => !isVirtualNode && onMailboxSelect?.(node.id)}
|
||||||
disabled={isVirtualNode}
|
disabled={isVirtualNode}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center py-1 lg:py-1 max-lg:py-2 px-1 rounded",
|
"flex items-center px-1 rounded",
|
||||||
"transition-colors duration-150",
|
"transition-colors duration-150",
|
||||||
isCollapsed ? "justify-center" : "flex-1 text-left",
|
isCollapsed ? "justify-center" : "flex-1 text-left",
|
||||||
isVirtualNode && "cursor-default select-none"
|
isVirtualNode && "cursor-default select-none"
|
||||||
)}
|
)}
|
||||||
style={isCollapsed ? undefined : {
|
style={{
|
||||||
paddingLeft: hasChildren ? '4px' : `${indentPixels + 24}px`
|
paddingBlock: 'var(--density-sidebar-py)',
|
||||||
|
...(isCollapsed ? {} : { paddingLeft: hasChildren ? '4px' : `${indentPixels + 24}px` })
|
||||||
}}
|
}}
|
||||||
title={isCollapsed ? node.name : undefined}
|
title={isCollapsed ? node.name : undefined}
|
||||||
>
|
>
|
||||||
@@ -258,8 +260,9 @@ function TagItem({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...(globalDragging ? dropHandlers : {})}
|
{...(globalDragging ? dropHandlers : {})}
|
||||||
|
style={{ paddingBlock: 'var(--density-sidebar-py)' }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group w-full flex items-center py-1 lg:py-1 max-lg:py-3 max-lg:min-h-[44px] text-sm transition-all duration-200",
|
"group w-full flex items-center max-lg:min-h-[44px] text-sm transition-all duration-200",
|
||||||
isCollapsed ? "justify-center px-1" : "px-2",
|
isCollapsed ? "justify-center px-1" : "px-2",
|
||||||
isSelected
|
isSelected
|
||||||
? "bg-accent text-accent-foreground"
|
? "bg-accent text-accent-foreground"
|
||||||
@@ -270,10 +273,10 @@ function TagItem({
|
|||||||
<button
|
<button
|
||||||
onClick={() => onTagSelect?.(isSelected ? null : kw.id)}
|
onClick={() => onTagSelect?.(isSelected ? null : kw.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center py-1 lg:py-1 max-lg:py-2 px-1 rounded transition-colors duration-150",
|
"flex items-center px-1 rounded transition-colors duration-150",
|
||||||
isCollapsed ? "justify-center" : "flex-1 text-left"
|
isCollapsed ? "justify-center" : "flex-1 text-left"
|
||||||
)}
|
)}
|
||||||
style={isCollapsed ? undefined : { paddingLeft: '40px' }}
|
style={{ paddingBlock: 'var(--density-sidebar-py)', ...(isCollapsed ? {} : { paddingLeft: '40px' }) }}
|
||||||
title={isCollapsed ? kw.label : undefined}
|
title={isCollapsed ? kw.label : undefined}
|
||||||
>
|
>
|
||||||
<span className={cn("w-3 h-3 rounded-full flex-shrink-0", palette?.dot || "bg-gray-400", !isCollapsed && "mr-2")} />
|
<span className={cn("w-3 h-3 rounded-full flex-shrink-0", palette?.dot || "bg-gray-400", !isCollapsed && "mr-2")} />
|
||||||
@@ -489,8 +492,9 @@ export function Sidebar({
|
|||||||
{emailKeywords.length > 0 && (
|
{emailKeywords.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
|
style={{ paddingBlock: 'var(--density-sidebar-py)' }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group w-full flex items-center py-1 lg:py-1 max-lg:py-3 max-lg:min-h-[44px] text-sm transition-all duration-200 font-medium",
|
"group w-full flex items-center max-lg:min-h-[44px] text-sm transition-all duration-200 font-medium",
|
||||||
isCollapsed ? "justify-center px-1" : "px-2",
|
isCollapsed ? "justify-center px-1" : "px-2",
|
||||||
"text-foreground hover:bg-muted"
|
"text-foreground hover:bg-muted"
|
||||||
)}
|
)}
|
||||||
@@ -528,11 +532,11 @@ export function Sidebar({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center py-1 lg:py-1 max-lg:py-2 px-1 rounded",
|
"flex items-center px-1 rounded",
|
||||||
"transition-colors duration-150",
|
"transition-colors duration-150",
|
||||||
isCollapsed ? "justify-center" : "flex-1 text-left"
|
isCollapsed ? "justify-center" : "flex-1 text-left"
|
||||||
)}
|
)}
|
||||||
style={isCollapsed ? undefined : { paddingLeft: '4px' }}
|
style={{ paddingBlock: 'var(--density-sidebar-py)', ...(isCollapsed ? {} : { paddingLeft: '4px' }) }}
|
||||||
title={isCollapsed ? t("tags") : undefined}
|
title={isCollapsed ? t("tags") : undefined}
|
||||||
>
|
>
|
||||||
<Tag className={cn(
|
<Tag className={cn(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { SettingsSection, SettingItem, RadioGroup, ToggleSwitch } from './settin
|
|||||||
export function AppearanceSettings() {
|
export function AppearanceSettings() {
|
||||||
const t = useTranslations('settings.appearance');
|
const t = useTranslations('settings.appearance');
|
||||||
const { theme, setTheme } = useThemeStore();
|
const { theme, setTheme } = useThemeStore();
|
||||||
const { fontSize, listDensity, animationsEnabled, toolbarPosition, updateSetting } = useSettingsStore();
|
const { fontSize, density, animationsEnabled, toolbarPosition, updateSetting } = useSettingsStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsSection title={t('title')} description={t('description')}>
|
<SettingsSection title={t('title')} description={t('description')}>
|
||||||
@@ -44,12 +44,12 @@ export function AppearanceSettings() {
|
|||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
{/* List Density */}
|
{/* Density */}
|
||||||
<SettingItem label={t('list_density.label')} description={t('list_density.description')}>
|
<SettingItem label={t('list_density.label')} description={t('list_density.description')}>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={listDensity}
|
value={density}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
updateSetting('listDensity', value as 'compact' | 'regular' | 'comfortable')
|
updateSetting('density', value as 'compact' | 'regular' | 'comfortable')
|
||||||
}
|
}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'compact', label: t('list_density.compact') },
|
{ value: 'compact', label: t('list_density.compact') },
|
||||||
|
|||||||
@@ -510,8 +510,8 @@
|
|||||||
"large": "Groß"
|
"large": "Groß"
|
||||||
},
|
},
|
||||||
"list_density": {
|
"list_density": {
|
||||||
"label": "Listendichte",
|
"label": "Dichte",
|
||||||
"description": "Abstand in E-Mail-Listen steuern",
|
"description": "Abstände in der Benutzeroberfläche steuern",
|
||||||
"compact": "Kompakt",
|
"compact": "Kompakt",
|
||||||
"regular": "Normal",
|
"regular": "Normal",
|
||||||
"comfortable": "Komfortabel"
|
"comfortable": "Komfortabel"
|
||||||
|
|||||||
@@ -510,8 +510,8 @@
|
|||||||
"large": "Large"
|
"large": "Large"
|
||||||
},
|
},
|
||||||
"list_density": {
|
"list_density": {
|
||||||
"label": "List Density",
|
"label": "Density",
|
||||||
"description": "Control spacing in email lists",
|
"description": "Control spacing and padding across the UI",
|
||||||
"compact": "Compact",
|
"compact": "Compact",
|
||||||
"regular": "Regular",
|
"regular": "Regular",
|
||||||
"comfortable": "Comfortable"
|
"comfortable": "Comfortable"
|
||||||
|
|||||||
@@ -510,8 +510,8 @@
|
|||||||
"large": "Grande"
|
"large": "Grande"
|
||||||
},
|
},
|
||||||
"list_density": {
|
"list_density": {
|
||||||
"label": "Densidad de Lista",
|
"label": "Densidad",
|
||||||
"description": "Controle el espaciado en las listas de correo",
|
"description": "Controle el espaciado en toda la interfaz",
|
||||||
"compact": "Compacto",
|
"compact": "Compacto",
|
||||||
"regular": "Regular",
|
"regular": "Regular",
|
||||||
"comfortable": "Cómodo"
|
"comfortable": "Cómodo"
|
||||||
|
|||||||
@@ -510,8 +510,8 @@
|
|||||||
"large": "Grande"
|
"large": "Grande"
|
||||||
},
|
},
|
||||||
"list_density": {
|
"list_density": {
|
||||||
"label": "Densité de la liste",
|
"label": "Densité",
|
||||||
"description": "Contrôlez l'espacement dans les listes d'emails",
|
"description": "Contrôlez l'espacement dans toute l'interface",
|
||||||
"compact": "Compacte",
|
"compact": "Compacte",
|
||||||
"regular": "Normale",
|
"regular": "Normale",
|
||||||
"comfortable": "Confortable"
|
"comfortable": "Confortable"
|
||||||
|
|||||||
@@ -510,8 +510,8 @@
|
|||||||
"large": "Grande"
|
"large": "Grande"
|
||||||
},
|
},
|
||||||
"list_density": {
|
"list_density": {
|
||||||
"label": "Densità elenco",
|
"label": "Densità",
|
||||||
"description": "Controlla la spaziatura negli elenchi email",
|
"description": "Controlla la spaziatura in tutta l'interfaccia",
|
||||||
"compact": "Compatto",
|
"compact": "Compatto",
|
||||||
"regular": "Normale",
|
"regular": "Normale",
|
||||||
"comfortable": "Comodo"
|
"comfortable": "Comodo"
|
||||||
|
|||||||
@@ -510,8 +510,8 @@
|
|||||||
"large": "大"
|
"large": "大"
|
||||||
},
|
},
|
||||||
"list_density": {
|
"list_density": {
|
||||||
"label": "リストの密度",
|
"label": "密度",
|
||||||
"description": "メールリストの間隔を調整",
|
"description": "UI全体の間隔を調整",
|
||||||
"compact": "コンパクト",
|
"compact": "コンパクト",
|
||||||
"regular": "標準",
|
"regular": "標準",
|
||||||
"comfortable": "ゆったり"
|
"comfortable": "ゆったり"
|
||||||
|
|||||||
@@ -510,8 +510,8 @@
|
|||||||
"large": "Groot"
|
"large": "Groot"
|
||||||
},
|
},
|
||||||
"list_density": {
|
"list_density": {
|
||||||
"label": "Lijstdichtheid",
|
"label": "Dichtheid",
|
||||||
"description": "Regeleer de ruimte in e-maillijsten",
|
"description": "Regeleer de ruimte in de gehele interface",
|
||||||
"compact": "Compact",
|
"compact": "Compact",
|
||||||
"regular": "Normaal",
|
"regular": "Normaal",
|
||||||
"comfortable": "Comfortabel"
|
"comfortable": "Comfortabel"
|
||||||
|
|||||||
@@ -510,8 +510,8 @@
|
|||||||
"large": "Grande"
|
"large": "Grande"
|
||||||
},
|
},
|
||||||
"list_density": {
|
"list_density": {
|
||||||
"label": "Densidade da Lista",
|
"label": "Densidade",
|
||||||
"description": "Controle o espaçamento nas listas de e-mail",
|
"description": "Controle o espaçamento em toda a interface",
|
||||||
"compact": "Compacto",
|
"compact": "Compacto",
|
||||||
"regular": "Regular",
|
"regular": "Regular",
|
||||||
"comfortable": "Confortável"
|
"comfortable": "Confortável"
|
||||||
|
|||||||
+62
-17
@@ -19,7 +19,9 @@ let isLoadingFromServer = false;
|
|||||||
const SYNC_DEBOUNCE_MS = 2000;
|
const SYNC_DEBOUNCE_MS = 2000;
|
||||||
|
|
||||||
export type FontSize = 'small' | 'medium' | 'large';
|
export type FontSize = 'small' | 'medium' | 'large';
|
||||||
export type ListDensity = 'compact' | 'regular' | 'comfortable';
|
export type Density = 'compact' | 'regular' | 'comfortable';
|
||||||
|
/** @deprecated Use Density instead */
|
||||||
|
export type ListDensity = Density;
|
||||||
export type DeleteAction = 'trash' | 'permanent';
|
export type DeleteAction = 'trash' | 'permanent';
|
||||||
export type ReplyMode = 'reply' | 'replyAll';
|
export type ReplyMode = 'reply' | 'replyAll';
|
||||||
export type DateFormat = 'regional' | 'iso' | 'custom';
|
export type DateFormat = 'regional' | 'iso' | 'custom';
|
||||||
@@ -64,7 +66,7 @@ export const DEFAULT_KEYWORDS: KeywordDefinition[] = [
|
|||||||
interface SettingsState {
|
interface SettingsState {
|
||||||
// Appearance
|
// Appearance
|
||||||
fontSize: FontSize;
|
fontSize: FontSize;
|
||||||
listDensity: ListDensity;
|
density: Density;
|
||||||
animationsEnabled: boolean;
|
animationsEnabled: boolean;
|
||||||
|
|
||||||
// Language & Region
|
// Language & Region
|
||||||
@@ -141,7 +143,7 @@ interface SettingsState {
|
|||||||
const DEFAULT_SETTINGS = {
|
const DEFAULT_SETTINGS = {
|
||||||
// Appearance
|
// Appearance
|
||||||
fontSize: 'medium' as FontSize,
|
fontSize: 'medium' as FontSize,
|
||||||
listDensity: 'regular' as ListDensity,
|
density: 'regular' as Density,
|
||||||
animationsEnabled: true,
|
animationsEnabled: true,
|
||||||
|
|
||||||
// Language & Region
|
// Language & Region
|
||||||
@@ -199,9 +201,9 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
applyFontSize(value as FontSize);
|
applyFontSize(value as FontSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply list density to document root
|
// Apply density to document root
|
||||||
if (key === 'listDensity') {
|
if (key === 'density') {
|
||||||
applyListDensity(value as ListDensity);
|
applyDensity(value as Density);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply animations to document root
|
// Apply animations to document root
|
||||||
@@ -213,7 +215,7 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
resetToDefaults: () => {
|
resetToDefaults: () => {
|
||||||
set(DEFAULT_SETTINGS);
|
set(DEFAULT_SETTINGS);
|
||||||
applyFontSize(DEFAULT_SETTINGS.fontSize);
|
applyFontSize(DEFAULT_SETTINGS.fontSize);
|
||||||
applyListDensity(DEFAULT_SETTINGS.listDensity);
|
applyDensity(DEFAULT_SETTINGS.density);
|
||||||
applyAnimations(DEFAULT_SETTINGS.animationsEnabled);
|
applyAnimations(DEFAULT_SETTINGS.animationsEnabled);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -221,7 +223,7 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
const state = get();
|
const state = get();
|
||||||
const settings = {
|
const settings = {
|
||||||
fontSize: state.fontSize,
|
fontSize: state.fontSize,
|
||||||
listDensity: state.listDensity,
|
density: state.density,
|
||||||
animationsEnabled: state.animationsEnabled,
|
animationsEnabled: state.animationsEnabled,
|
||||||
dateFormat: state.dateFormat,
|
dateFormat: state.dateFormat,
|
||||||
timeFormat: state.timeFormat,
|
timeFormat: state.timeFormat,
|
||||||
@@ -269,7 +271,7 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
|
|
||||||
// Apply visual settings
|
// Apply visual settings
|
||||||
applyFontSize(get().fontSize);
|
applyFontSize(get().fontSize);
|
||||||
applyListDensity(get().listDensity);
|
applyDensity(get().density);
|
||||||
applyAnimations(get().animationsEnabled);
|
applyAnimations(get().animationsEnabled);
|
||||||
|
|
||||||
// Apply cross-store settings
|
// Apply cross-store settings
|
||||||
@@ -395,7 +397,24 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'settings-storage',
|
name: 'settings-storage',
|
||||||
version: 1,
|
version: 2,
|
||||||
|
migrate: (persisted, version) => {
|
||||||
|
const state = persisted as Record<string, unknown>;
|
||||||
|
if (version < 2 && state.listDensity) {
|
||||||
|
state.density = state.listDensity;
|
||||||
|
delete state.listDensity;
|
||||||
|
}
|
||||||
|
return state as unknown as SettingsState;
|
||||||
|
},
|
||||||
|
onRehydrateStorage: () => {
|
||||||
|
return (state) => {
|
||||||
|
if (state) {
|
||||||
|
applyFontSize(state.fontSize);
|
||||||
|
applyDensity(state.density);
|
||||||
|
applyAnimations(state.animationsEnabled);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -413,16 +432,42 @@ function applyFontSize(size: FontSize) {
|
|||||||
root.style.setProperty('--font-size-base', sizeMap[size]);
|
root.style.setProperty('--font-size-base', sizeMap[size]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyListDensity(density: ListDensity) {
|
function applyDensity(density: Density) {
|
||||||
if (typeof document === 'undefined') return;
|
if (typeof document === 'undefined') return;
|
||||||
|
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
const densityMap = {
|
|
||||||
compact: '32px',
|
const densityValues = {
|
||||||
regular: '48px',
|
compact: {
|
||||||
comfortable: '64px',
|
'--list-item-height': 'auto',
|
||||||
|
'--density-item-py': '4px',
|
||||||
|
'--density-item-gap': '8px',
|
||||||
|
'--density-header-py': '6px',
|
||||||
|
'--density-card-p': '10px',
|
||||||
|
'--density-sidebar-py': '1px',
|
||||||
|
},
|
||||||
|
regular: {
|
||||||
|
'--list-item-height': '48px',
|
||||||
|
'--density-item-py': '12px',
|
||||||
|
'--density-item-gap': '12px',
|
||||||
|
'--density-header-py': '12px',
|
||||||
|
'--density-card-p': '16px',
|
||||||
|
'--density-sidebar-py': '4px',
|
||||||
|
},
|
||||||
|
comfortable: {
|
||||||
|
'--list-item-height': '64px',
|
||||||
|
'--density-item-py': '16px',
|
||||||
|
'--density-item-gap': '16px',
|
||||||
|
'--density-header-py': '16px',
|
||||||
|
'--density-card-p': '20px',
|
||||||
|
'--density-sidebar-py': '6px',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
root.style.setProperty('--list-item-height', densityMap[density]);
|
|
||||||
|
const values = densityValues[density];
|
||||||
|
for (const [prop, val] of Object.entries(values)) {
|
||||||
|
root.style.setProperty(prop, val);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyAnimations(enabled: boolean) {
|
function applyAnimations(enabled: boolean) {
|
||||||
@@ -440,7 +485,7 @@ function applyAnimations(enabled: boolean) {
|
|||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const store = useSettingsStore.getState();
|
const store = useSettingsStore.getState();
|
||||||
applyFontSize(store.fontSize);
|
applyFontSize(store.fontSize);
|
||||||
applyListDensity(store.listDensity);
|
applyDensity(store.density);
|
||||||
applyAnimations(store.animationsEnabled);
|
applyAnimations(store.animationsEnabled);
|
||||||
|
|
||||||
// Shared sync function used by all store subscribers
|
// Shared sync function used by all store subscribers
|
||||||
|
|||||||
Reference in New Issue
Block a user