Give the mail row two lines between a phone's three and a wide list's one (#62)

A row went to one line once the list was 36rem wide, which with the reading pane open left the subject about 120px. One line now starts at 75rem. Below that the row is two lines, sender and subject then the preview across the whole row, and under 36rem the three it always was.

The hover actions show on any pointer that can hover, not only in one-line rows. A scheduled draft keeps its snippet beside the badge from 36rem. With the two-line preview setting on, a one-line row no longer wraps its preview.

Co-authored-by: Walid Mani <Flouss@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-12 10:19:17 +02:00
1 parent f19818fa50
commit 6c719b3327
9 files changed
+330 -47

No files matched your search

+13
View File
@@ -8,6 +8,19 @@ The published image tags: `latest` follows the most recent release below,
## Unreleased
**The mail list is readable beside an open message.** A row went to a single line once the list was
about 575px wide, which left the subject around 120px and its preview wrapped next to it ("Facture
pour Co…") whenever the reading pane was open, or on a large phone. A row now goes to one line only
once the list is 75rem (about 1200px) wide. Below that it is two lines — sender and subject, then
the preview across the whole row — and on a list narrower than about 575px, a phone, the three it
always was. Not less, because a one-line row in a 65rem list (the list beside the reading pane on a
2560px screen) still cut real subjects after about 30 characters. Archive, delete, snooze and the
other row actions appear on hover in two- and three-line rows too, instead of only in one-line rows.
A two-line row has no unread dot; like a one-line row it says unread with a bold sender and subject.
Also fixed: with the two-line preview setting on, a one-line row wrapped its preview anyway. No
setting; no migration.
[#62](https://github.com/karatektus/pl_mail/pull/62), by @Flouss.
**Each inbox tab shows how many conversations in it are unread.** The number is the sidebar's
Inbox badge split by category, so the tabs add up to it, and it appears on every tab, the one you
are on included, and updates in place on sync. Before, an unread conversation in Promotions made
+4 -3
View File
@@ -582,8 +582,8 @@ at the right. It has three positions (mail, split, calendar), and the split need
- **Below 768px** the sidebar becomes a drawer, compose goes full-screen, and search collapses to an
icon that opens a full-width row under the top bar.
- **Container queries, not viewport queries,** for anything inside a pane. A mail row switches
between stacked and single-line on the *list's* width (`@xl`, 36rem), because a docked calendar can
make the list narrower than a phone.
between three lines, two and one on the *list's* width (`@xl`, 36rem, and `@rowwide`, 75rem),
because a docked calendar can make the list narrower than a phone.
- **Nothing scrolls the page sideways.** Panes scroll their own overflow. The shell uses `dvh` and
honours safe-area insets.
- **Design at German length, at 320px wide, and at text scale 1.25.**
@@ -705,7 +705,8 @@ The row is the product. Keep it quiet.
- A **36px avatar disc** that doubles as the checkbox. It turns into an accent disc with a tick on
hover or when checked.
- **Sender, subject, snippet, chips, time.** In a wide list they share one line, subject and snippet
together. In a narrow one they stack, and the snippet clamps to the user's 0–2 lines.
together. In a middling one sender and subject share the first line and the snippet takes the
second. In a narrow one they stack. Below one line the snippet clamps to the user's 0–2 lines.
- **The row** is `flex items-center gap-3 pl-3 pr-4 list-row-y`. Its padding comes from the density
variable, never `py-*`.
- **Unread:** sender and subject `font-semibold text-ink`, plus a faint white wash (`row-unread`,
+47 -5
View File
@@ -5078,6 +5078,36 @@ label:has(> input.sr-only) {
preview in the settings pane is styled by these same rules rather than by a
copy of them that could drift. */
/*
Where a list row stops being three stacked lines and becomes one.
This was Tailwind's stock `@xl` (36rem), and it was too early: one line has to
carry the avatar, a 10rem sender column and a 9rem date/actions column before
the subject and preview get a single pixel, so a list with the reading pane
open (about 45rem) left them ~260px between them — "Facture pour Co…" and a
preview wrapped to two cramped lines. Stacked gives them the full width, and
it is already the layout a phone gets. One line pays off only when there is
room for it. 58rem was the first guess and still too early for real mail: a
list of about 65rem (a 2560px window with the reading pane open) cut a
subject like "Votre Colis N°8373 Nécessite Une Vérification" after ~30
characters and the preview after ~35. Measured by sweeping the list width
with long French and English subjects: at 75rem both the subject and a
usable preview fit; wider only adds preview. `@rowwide:` in
_thread_row.html.twig is this token, and so is the `@container` rule below,
which reads it through `theme()` rather than repeating the number.
Below it the row is not three lines everywhere. From `@xl` (36rem) up to this
token it is two — sender and subject on the first, the preview on the second
— because three lines in a list 60rem wide spent a line on a sender name
with most of the row empty beside it. That band is `@xl:@max-rowwide:` in the
row template and needs no rule here: the preview clamp below treats it like
the three-line row, which is what it should do, since the preview has a
line of its own in both.
*/
@theme {
--container-rowwide: 75rem;
}
/* The account corner: the coloured triangle in a row's lower-left. */
[data-account-corner] {
display: var(--list-corner-display);
@@ -5134,8 +5164,13 @@ label:has(> input.sr-only) {
arrangement the wide layout exists to avoid. So the wide branch takes the
same on/off switch without the clamp.
`@container`, matching the row's own `@xl` (Tailwind's 36rem container
width, written out by hand because a plain rule cannot use the variant): the
`@container`, matching the row's own `@rowwide` (the 75rem container width
declared by --container-rowwide in the "Mail list display" block above). A
plain rule cannot use the variant, and a custom property means nothing inside
a query, so the number comes through `theme()`, which Tailwind resolves at
build time. It was written out by hand once, with a comment asking for the
two to be changed together, which is the kind of promise that gets kept
until it does not: the
row switches on the LIST's width, not the viewport's — the list is narrower
than a phone whenever the calendar pane is docked wide.
*/
@@ -5145,7 +5180,9 @@ label:has(> input.sr-only) {
badge saying when it goes has already eaten the line, and what is left of
the snippet at 393px is two characters and an ellipsis. These rules are
unlayered and would otherwise beat that `.hidden` and put the fragment back.
Wide, where the row shows both, there is nothing to exempt.
From `@xl` up, where the row shows both, the element still carries
`.hidden`, so it stays out of this rule and its one line is Tailwind's
`truncate`.
*/
[data-row-snippet]:not(.hidden) {
display: var(--list-preview-display);
@@ -5155,8 +5192,13 @@ label:has(> input.sr-only) {
overflow: hidden;
}
@container (min-width: 36rem) {
[data-row-snippet] {
@container (min-width: theme(--container-rowwide)) {
/* `:not(.hidden)` for SPECIFICITY, not for the exemption above: a container
query adds none, so a bare `[data-row-snippet]` (0,1,0) lost to the
stacked rule's `[data-row-snippet]:not(.hidden)` (0,2,0) and the
"wide is always one line" override never applied — with the two-line
setting on, a one-line row wrapped its preview anyway. */
[data-row-snippet]:not(.hidden) {
display: var(--list-preview-display-wide);
-webkit-line-clamp: 1;
white-space: nowrap;
+89 -33
View File
@@ -361,7 +361,7 @@ be a worse regression than the one this feature fixes.
move, not a restyle. What it buys is that both are now custom
properties, which is what lets the density differ per surface and the
unread tint scale with the emphasis setting. #}
class="pointer-events-auto flex items-center @xl:items-center gap-3 pl-3 pr-4 list-row-y cursor-pointer transition-colors
class="pointer-events-auto flex items-center @rowwide:items-center gap-3 pl-3 pr-4 list-row-y cursor-pointer transition-colors
{{ unread ? 'row-unread' : 'bg-transparent' }}
hover:bg-white/60 dark:hover:bg-white/[0.06]"
>
@@ -404,12 +404,31 @@ be a worse regression than the one this feature fixes.
aria-hidden="true"><i class="fa-solid fa-check text-xs"></i></span>
</label>
{# Content — one line when the list is wide, three when it is not #}
<div data-row-layout
class="flex-1 min-w-0 flex flex-col @xl:flex-row @xl:items-center @xl:gap-4">
{# Content — three shapes, chosen by the LIST's width:
{# Row 1 (stacked) / sender column (wide) #}
<div class="flex items-center justify-between gap-2 @xl:justify-start @xl:w-40 @xl:flex-shrink-0">
below `@xl` (36rem) three lines: sender, subject, preview
`@xl` up to `@rowwide` two lines: sender and subject, then preview
`@rowwide` (75rem) up one line
The middle one is a grid, because the sender and the subject are not
siblings: the subject lives two wrappers down, beside the preview. So
both wrappers go `display: contents` in that band and their children
become cells of this grid — sender and subject on the first row, the
preview spanning the second. The sender column is the 10rem the
one-line layout gives it, so subjects line up down the list.
`@xl:@max-rowwide:` is a range, not a step: nothing here has to be
undone again at `@rowwide`. #}
<div data-row-layout
class="flex-1 min-w-0 flex flex-col
@xl:@max-rowwide:grid @xl:@max-rowwide:grid-cols-[10rem_minmax(0,1fr)] @xl:@max-rowwide:gap-x-4 @xl:@max-rowwide:gap-y-1
@rowwide:flex-row @rowwide:items-center @rowwide:gap-4">
{# Row 1 (three lines) / first cell of row 1 (two lines) / sender
column (one line). `min-w-0` is for the middle one: a grid item
will not shrink under its content without it, and a long sender
would push the column past its 10rem. #}
<div data-row-sender class="min-w-0 flex items-center justify-between gap-2 @rowwide:justify-start @rowwide:w-40 @rowwide:flex-shrink-0">
{# Sender and its marker travel together in one group, so the
badge sits to the LEFT of the name at both container widths.
Hanging it off the outer flex instead would have put it at
@@ -500,9 +519,9 @@ be a worse regression than the one this feature fixes.
that same position wedges them between a subject and a preview
that are already competing for one line, so they drop to a line
of their own underneath. #}
<div class="min-w-0 flex-1 flex flex-col">
<div class="min-w-0 flex flex-col @xl:flex-row @xl:items-baseline @xl:gap-2">
<span class="truncate text-sm mt-1 @xl:mt-0 @xl:flex-shrink-0 @xl:max-w-[45%]
<div class="min-w-0 flex-1 flex flex-col @xl:@max-rowwide:contents">
<div class="min-w-0 flex flex-col @xl:@max-rowwide:contents @rowwide:flex-row @rowwide:items-baseline @rowwide:gap-2">
<span data-row-subject class="truncate text-sm mt-1 @xl:@max-rowwide:mt-0 @rowwide:mt-0 @rowwide:flex-shrink-0 @rowwide:max-w-[45%]
{{ unread ? 'font-semibold text-ink' : 'font-normal text-ink-soft' }}">
{{ subjectLine }}
{% if count > 1 %}
@@ -513,7 +532,9 @@ be a worse regression than the one this feature fixes.
{# A little air above the preview when stacked. It separates
the subject from the snippet, and the meta column mirrors
the same margin so the dot stays level with it. #}
<span class="min-w-0 flex-1 flex items-center gap-1.5 text-sm mt-1.5 @xl:mt-0">
{# Two lines: the whole second row, under the sender as
well as the subject. The grid's row gap is the air. #}
<span class="min-w-0 flex-1 flex items-center gap-1.5 text-sm mt-1.5 @xl:@max-rowwide:mt-0 @xl:@max-rowwide:col-span-2 @rowwide:mt-0">
{# A draft with a send time on it. Without this the
Drafts list is the one place a scheduled message
lives and the only place that does not say so — the
@@ -589,15 +610,23 @@ be a worse regression than the one this feature fixes.
</button>
</span>
{% endif %}
<span class="@xl:hidden contents">
<span class="@rowwide:hidden contents">
{% include '_partials/_thread_labels.html.twig' with { item: item } %}
</span>
{# A stacked row has one line for the preview, and a
scheduled draft has already spent most of it saying
when it goes — what is left of the snippet at 393px
is two characters and an ellipsis, which is noise
where the badge is information. Wide, both fit and
both are shown. #}
{# A three-line row has one short line for the preview,
and a scheduled draft has already spent most of it
saying when it goes — what is left of the snippet at
393px is two characters and an ellipsis, which is
noise where the badge is information. From `@xl` up
both fit and both are shown: the two-line row gives
the preview the whole width, and the one-line row
always had the room.
`@xl`, and not `@rowwide` with the rest of the
one-line layout: when the one-line breakpoint moved
to 75rem this went with it, and every Drafts list
between 36rem and 75rem lost the snippet beside the
badge that it had been showing. #}
{# `@xl:inline-block`, not `@xl:block`, and this is the
second time this file has paid for it: Tailwind emits
`.hidden` AFTER `.block`, so `hidden @xl:block` is
@@ -605,7 +634,7 @@ be a worse regression than the one this feature fixes.
not outrank it. `.inline-block` is emitted after
`.hidden` and wins, and a flex item blockifies it
anyway, so `truncate` still truncates. The pairing
further down this file (`hidden @xl:flex`) works for
further down this file (`hidden @rowwide:flex`) works for
the same reason in the other direction. #}
{# `data-row-snippet` carries the preview-lines setting;
`truncate` stays for the one-line default, and
@@ -615,7 +644,7 @@ be a worse regression than the one this feature fixes.
</span>
</div>
<div class="hidden @xl:flex items-center mt-1">
<div class="hidden @rowwide:flex items-center mt-1">
{% include '_partials/_thread_labels.html.twig' with { item: item } %}
</div>
</div>
@@ -626,7 +655,7 @@ be a worse regression than the one this feature fixes.
lines beside it. Wide the row is one line, so this is too.
The hover actions still swap for the date, but only on the date's
own row and only where there is a hover to speak of. They used to be
own row and only for a pointer that can hover. They used to be
absolutely positioned over the whole column, which is why nothing
else could live here. #}
{# The meta area takes the row's own shape. A stacked row is three
@@ -638,21 +667,28 @@ be a worse regression than the one this feature fixes.
content's height and distributes, so the time sits on the sender's
line, the icons on the subject's and the dot on the preview's —
packing from the top instead left the third item hanging below the
last line of text. Wide, the row is one line and so is this. #}
last line of text. Wide, the row is one line and so is this.
The two-line row between them needs nothing of its own here. It is
still the column: the unread dot is gone at that width (see the dot
below), which leaves the time and the star, and the grid's row gap is
the star's own `mt-1`, so the two sit level with the two lines of
text. #}
<div data-row-meta
class="flex flex-shrink-0 ml-2
flex-col items-end self-start
@xl:flex-row-reverse @xl:items-center @xl:justify-end @xl:gap-2
@xl:self-center @xl:w-36">
@rowwide:flex-row-reverse @rowwide:items-center @rowwide:justify-end @rowwide:gap-2
@rowwide:self-center @rowwide:w-36">
<div class="relative flex items-center justify-end w-full h-5 @xl:h-6 @xl:w-auto @xl:flex-1">
<div class="relative flex items-center justify-end w-full h-5 @rowwide:h-6 @rowwide:w-auto @rowwide:flex-1">
{# The paperclip rides with the date: wide, it sits just inboard
of it and vanishes with it when the hover actions take the
row over. Leaving it outside meant it stayed put underneath
them. Stacked there is no hover, so it simply sits there. #}
them. Stacked it is not here at all — see the copy beside
the star. #}
<span class="absolute inset-0 flex items-center justify-end gap-1.5
text-xs text-ink-faint whitespace-nowrap
@xl:group-hover:invisible @xl:group-has-[[aria-expanded=true]]:invisible">
group-hover:invisible group-has-[[aria-expanded=true]]:invisible">
{# The visibility classes go on a wrapper, never on the <i>
itself: Font Awesome's own .fa-solid rule sets display,
and it lands after Tailwind's utilities, so `hidden` on
@@ -660,7 +696,7 @@ be a worse regression than the one this feature fixes.
Every other icon in this file is wrapped for the same
reason. #}
{% if hasAttachment %}
<span class="hidden @xl:inline" aria-hidden="true">
<span class="hidden @rowwide:inline" aria-hidden="true">
<i class="fa-solid fa-paperclip text-[11px]"></i>
</span>
{% endif %}
@@ -692,8 +728,25 @@ be a worse regression than the one this feature fixes.
{# Shown on hover — and for as long as the spam menu hanging off
one of these buttons is open. That menu is a descendant, so
without the second rule a pointer that strays off the row on
its way to the menu hides the menu it was heading for. #}
<div class="hidden @xl:flex absolute inset-0 items-center justify-end invisible group-hover:visible has-[[aria-expanded=true]]:visible pointer-events-auto gap-1">
its way to the menu hides the menu it was heading for.
Below one line as well, since the breakpoint moved to 75rem:
left at `hidden @rowwide:flex`, archive, delete and snooze
would have gone from every list narrower than that, which is
most of them. `[@media(hover:hover)]` rather than a width,
because what the strip needs is a pointer that can hover — it
is only ever shown by `group-hover`, so on a touch screen
it would be markup nothing can reach.
Two things differ there, and both are because the strip is
wider than the time it stands in for. `right-0` without a
left edge, so it grows leftwards out of the time's cell instead
of being squeezed into it; and `bg-surface`, because growing
leftwards puts it over the end of the sender or subject,
which would otherwise show through between the buttons. One
line, the column is as wide as the strip (`w-36`) and there
is nothing underneath to cover. #}
<div class="hidden [@media(hover:hover)]:flex @rowwide:flex absolute inset-y-0 right-0 items-center justify-end invisible group-hover:visible has-[[aria-expanded=true]]:visible pointer-events-auto gap-1 rounded-lg bg-surface @rowwide:bg-transparent">
{% if false == draft and placement == 'normal' %}
<button type="button" data-action="click->mail--message-row#archive"
data-mail--message-row-archive-url-param="{{ path('app_status_archive', {'type': type, 'id': rowId}) }}"
@@ -829,13 +882,13 @@ be a worse regression than the one this feature fixes.
{# The star, on the row's trailing edge where the pointer already
is. The paperclip lives with the date above — it is a fact about
the row, not a control, and it belongs to the same line. #}
<div class="flex items-center gap-2 h-5 mt-1 @xl:mt-0 @xl:order-first">
<div class="flex items-center gap-2 h-5 mt-1 @rowwide:mt-0 @rowwide:order-first">
{# Stacked, the paperclip belongs with the star: there is no
hover for it to vanish under, and a three-line row reads
better with its two marks together than with one stranded
beside the time. The wide copy lives with the date. #}
{% if hasAttachment %}
<span class="@xl:hidden text-ink-faint" title="{{ 'thread_row.has_attachment'|trans }}">
<span class="@rowwide:hidden text-ink-faint" title="{{ 'thread_row.has_attachment'|trans }}">
<i class="fa-solid fa-paperclip text-xs" aria-hidden="true"></i>
</span>
<span class="sr-only">{{ 'thread_row.has_attachment'|trans }}</span>
@@ -856,8 +909,11 @@ be a worse regression than the one this feature fixes.
</button>
</div>
{# Stacked only — a wide row already says "unread" twice over, with
a bold sender and a bold subject.
{# Three lines only — a wide row already says "unread" twice over,
with a bold sender and a bold subject, and a two-line row puts
those same two side by side on its first line. It also has no
third line for the dot to sit on: the meta column is the time
over the star there, level with the two rows of text.
Boxed to the star's width rather than right-aligned on its own:
the dot is 8px and the star glyph is 14, so aligning their right
+4 -2
View File
@@ -130,8 +130,10 @@
phone-sized card, and `min-w-0` on each tab let them all shrink until
the labels ran under the next tab's icon. So a tab keeps its own width
(`shrink-0`) and the row overflows sideways — inside the nav, never the
page. Padding tightens below the list's `@xl`, the same breakpoint the
rows use, because the card can be narrow on a wide viewport too.
page. Padding tightens below the list's `@xl` (36rem), which is
also where the rows go from three lines to two — one line comes
later, at `@rowwide` — because the card can be narrow on a wide
viewport too.
`pb-px -mb-px` on the nav is what the tabs' own `-mb-px` used to do
against the wrapper's hairline: an overflow container clips whatever
@@ -16,7 +16,7 @@ arrangement of the same parts, not a drawing of them — a drawing would be free
to be wrong.
`@container` on the list: the snippet's two-line clamp is a container query
matching the real list's `@xl`, and this pane is narrow, so the preview shows
matching the real list's `@rowwide`, and this pane is narrow, so the preview shows
the stacked behaviour — which is the one the second line exists for.
#}
{# Sticky within its own column rather than across the top of the panel: the
+4 -1
View File
@@ -964,7 +964,10 @@ test.describe("calendar pane", () => {
* rendering one-line rows, truncated to nothing.
*/
test("stacks the mail rows once the pane has taken enough width", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
// 1600, not 1440: one line starts at a 75rem list now, and a 1440px
// window gives one just short of that, so the first assertion below
// found a row that was never one line to begin with.
await page.setViewportSize({ width: 1600, height: 900 });
await ensurePaneClosed(page);
// The content block is what changes shape: a column when the list is
+165
View File
@@ -0,0 +1,165 @@
import { test, expect, type Page } from "./support/test";
import { INBOX_SUBJECTS, mailRow, seed } from "./support/config";
/**
* Where a list row stops being three stacked lines and becomes one.
*
* The row used to go to one line at Tailwind's stock 36rem, which is a list
* about 575px wide: with the reading pane open, or on a big phone, the avatar,
* the sender column and the date column took ~450px of it and the subject was
* left ~120px — "Facture pour Co…" — with a preview wrapped into two cramped
* lines beside it. The stacked layout gives both the full width and is the one a
* phone already gets, so one line now starts at 75rem (`--container-rowwide`
* in app.css; the `@rowwide:` variant in _thread_row.html.twig). 58rem was the
* first value and was still too early: a 2560px window with the reading pane
* open gives a list of ~65rem, where real subjects were cut after ~30 chars.
*
* Under 75rem the row is not one shape either: three lines below 36rem, and
* from there up two — sender and subject sharing the first line, the preview on
* the second. "Stacked" in the tests below means the property both of those
* have and the one-line row does not: the preview is under the subject.
*
* Measured on the LIST's width, not the viewport's: the row is a container
* query on #message-list. The viewports below are chosen so the list lands on
* each side of the line (sidebar and gutters take ~250px).
*/
const SUBJECT = INBOX_SUBJECTS.star;
const geometry = async (page: Page) => {
const row = mailRow(page, SUBJECT);
// The row's own hooks, the same family as `data-row-snippet`, which the
// preview-lines setting already hangs on. None of the three is anything a
// role or a name could find — they are spans of text in one list item —
// and a utility class like `truncate` says how a thing is styled, not
// what it is.
const sender = row.locator("[data-row-sender]");
const subject = row.locator("[data-row-subject]");
const snippet = row.locator("[data-row-snippet]");
return {
list: (await page.locator("#message-list").boundingBox())!,
row: (await row.boundingBox())!,
sender: (await sender.boundingBox())!,
subject: (await subject.boundingBox())!,
snippet: (await snippet.boundingBox())!,
};
};
const openInbox = async (page: Page, width: number) => {
await page.setViewportSize({ width, height: 900 });
await page.goto("/mail/inbox");
await expect(mailRow(page, SUBJECT)).toBeVisible();
};
test.describe("list row layout around the one-line breakpoint", () => {
test.beforeAll(() => {
seed("seed-mail");
});
test("a list in the middle band keeps the preview under the subject and gives the subject the width", async ({ page }) => {
// List ≈ 850px: wider than the old 36rem line, narrower than 75rem.
await openInbox(page, 1100);
const g = await geometry(page);
// Presence companion: the list really is in the band under test.
expect(g.list.width).toBeGreaterThan(36 * 16);
expect(g.list.width).toBeLessThan(75 * 16);
// Stacked: the preview sits BELOW the subject, not beside it …
expect(g.snippet.y).toBeGreaterThan(g.subject.y + g.subject.height - 2);
// … and the subject is not squeezed into a corner of the row.
expect(g.subject.width).toBeGreaterThan(g.row.width * 0.5);
});
test("between 36rem and 75rem the sender shares the first line with the subject", async ({ page }) => {
// The in-between shape: two lines, not three. Sender and subject on the
// first, the preview alone on the second and starting back under the
// sender, so it has the row's whole width.
await openInbox(page, 1100);
const g = await geometry(page);
expect(g.list.width).toBeGreaterThan(36 * 16);
expect(g.list.width).toBeLessThan(75 * 16);
expect(Math.abs(g.sender.y - g.subject.y)).toBeLessThan(4);
expect(g.subject.x).toBeGreaterThanOrEqual(g.sender.x + g.sender.width - 2);
expect(g.snippet.y).toBeGreaterThan(g.subject.y + g.subject.height - 2);
expect(g.snippet.x).toBeLessThan(g.subject.x);
});
test("a list under 36rem keeps all three lines", async ({ page }) => {
// A phone. Presence companion for the test above: the two-line shape is
// a band, not simply what every list under 75rem now gets.
await openInbox(page, 480);
const g = await geometry(page);
expect(g.list.width).toBeLessThan(36 * 16);
expect(g.subject.y).toBeGreaterThan(g.sender.y + g.sender.height - 2);
expect(g.snippet.y).toBeGreaterThan(g.subject.y + g.subject.height - 2);
});
test("a list past the first 58rem guess still keeps the preview under the subject", async ({ page }) => {
// List ≈ 1050px (~65rem): the width a 2560px window with the reading
// pane open gives. One line here cut real subjects after ~30
// characters, which is why 58rem was raised to 75rem.
await openInbox(page, 1300);
const g = await geometry(page);
// Presence companion: the list is above the OLD line, so a regression
// to 58rem is what this fails on.
expect(g.list.width).toBeGreaterThan(58 * 16);
expect(g.list.width).toBeLessThan(75 * 16);
expect(g.snippet.y).toBeGreaterThan(g.subject.y + g.subject.height - 2);
});
test("a wide list keeps subject and preview on one line", async ({ page }) => {
// List ≈ 1350px.
await openInbox(page, 1600);
const g = await geometry(page);
expect(g.list.width).toBeGreaterThan(75 * 16);
// Same line: the preview starts to the right of the subject's end and
// within a line's height of its top.
expect(g.snippet.x).toBeGreaterThanOrEqual(g.subject.x + g.subject.width - 2);
expect(Math.abs(g.snippet.y - g.subject.y)).toBeLessThan(8);
});
test("the hover actions are reachable below the one-line breakpoint", async ({ page }) => {
// They were `hidden @xl:flex`, so moving the breakpoint without moving
// them would have taken archive, delete and snooze away from every list
// that is now stacked on a pointer device.
await openInbox(page, 1100);
const row = mailRow(page, SUBJECT);
await row.hover();
await expect(row.getByRole("button", { name: "Archive" })).toBeVisible();
await expect(row.getByRole("button", { name: "Delete" })).toBeVisible();
});
test("the two-line preview setting stays out of the one-line layout", async ({ page }) => {
await openInbox(page, 1600);
// What AppearanceRenderer writes on <html> for previewLines = 2, set
// directly so the spec does not depend on the saved setting.
await page.evaluate(() => {
const root = document.documentElement.style;
root.setProperty("--list-preview-display", "-webkit-box");
root.setProperty("--list-preview-lines", "2");
root.setProperty("--list-preview-wrap", "normal");
});
const snippet = mailRow(page, SUBJECT).locator("[data-row-snippet]");
await expect(snippet).toHaveCSS("white-space", "nowrap");
await expect(snippet).toHaveCSS("-webkit-line-clamp", "1");
// Presence companion: the same setting DOES take effect when stacked,
// so the assertion above is not true of an element the setting never
// reached.
await page.setViewportSize({ width: 1100, height: 900 });
await expect(snippet).toHaveCSS("white-space", "normal");
await expect(snippet).toHaveCSS("-webkit-line-clamp", "2");
});
});
+3 -2
View File
@@ -199,8 +199,9 @@ test.describe("counters after reading one mail", () => {
* would pass the read case alone.
*/
test("the row's own mark-read button moves the badge, and back again", async ({ page }) => {
// The row action strip is `hidden @xl:flex` — a container query on the
// list pane — so it does not exist at all in a narrow window.
// Wide, so the row is one line and the action strip sits where this
// test has always clicked it. Narrower lists show it too, on hover —
// list-row-breakpoint.spec.ts holds that.
await page.setViewportSize({ width: 1600, height: 900 });
await page.goto("/mail/inbox");
await expect(mailRow(page, INBOX_SUBJECTS.read)).toBeVisible();