fix(list): align the unread dot with the first line, not the row midpoint

The dot is absolutely positioned with `top-1/2`, so it centres on the whole
row. In the default multi-line layout (sender / subject / preview) that puts
it a full line below the checkbox and the avatar it reads as a column with —
visibly out of line, and worse the taller the row.

Anchor it on the first line instead: top padding + half an avatar, per
density. Same fix for the messages of an expanded thread.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FTRyrFCR2N9qkpKK37GgWb
This commit is contained in:
lucletoffe
2026-07-31 12:45:55 +02:00
co-authored by Claude Opus 5
parent fc7fec44a8
commit 7d868caae1
2 changed files with 30 additions and 8 deletions
+7 -2
View File
@@ -137,9 +137,14 @@ export function ThreadEmailItem({
</button>
)}
{/* Unread indicator */}
{/* Unread indicator — anchored on the first line (top padding + half a
small avatar), not on the row's midpoint, so it stays in line with
the checkbox and avatar when the row wraps to a second line. */}
{isUnread && (
<div className="absolute start-7 top-1/2 -translate-y-1/2">
<div
className="absolute start-7 -translate-y-1/2"
style={{ top: `calc(var(--density-item-py) + ${density === 'extra-compact' ? '0.625rem' : '1rem'})` }}
>
<Circle className="w-1.5 h-1.5 fill-unread text-unread" />
</div>
)}
+23 -6
View File
@@ -20,6 +20,27 @@ import { ThreadEmailItem } from "./thread-email-item";
import { EmailHoverActions } from "./email-hover-actions";
import { useTranslations } from "next-intl";
/**
* Unread bullet in the row gutter.
*
* It is absolutely positioned so it never widens the row, which means it has
* to be told where the row's *first* line sits: centring it on the row (the
* old `top-1/2`) drifts down as soon as the row grows a second and third line,
* leaving it visibly out of line with the checkbox and the avatar it reads as
* a column with. Anchor = top padding + half an avatar.
*/
function UnreadDot({ density, compactAvatar }: { density: string; compactAvatar: boolean }) {
const halfFirstLine = density === 'extra-compact' ? '0.625rem' : compactAvatar ? '1rem' : '1.25rem';
return (
<div
className="absolute start-0.5 -translate-y-1/2"
style={{ top: `calc(var(--density-item-py) + ${halfFirstLine})` }}
>
<Circle className="w-2 h-2 fill-unread text-unread" />
</div>
);
}
/**
* Small chip showing the originating folder of a message, rendered in the
* aggregate "All …" views (All Mail / unified / cross-account) where rows come
@@ -244,9 +265,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
)}
{isUnread && (
<div className="absolute start-0.5 top-1/2 -translate-y-1/2">
<Circle className="w-2 h-2 fill-unread text-unread" />
</div>
<UnreadDot density={density} compactAvatar={isFocusedMailLayout} />
)}
{density !== 'extra-compact' && (
@@ -683,9 +702,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
)}
{hasUnread && (
<div className="absolute start-0.5 top-1/2 -translate-y-1/2">
<Circle className="w-2 h-2 fill-unread text-unread" />
</div>
<UnreadDot density={density} compactAvatar={isFocusedMailLayout} />
)}
{density !== 'extra-compact' && (