From 6c719b3327089537c825822fda23d2725c1b9894 Mon Sep 17 00:00:00 2001 From: Walid Mani Date: Mon, 12 Oct 2026 10:19:17 +0200 Subject: [PATCH] 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 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 13 ++ DESIGN.md | 7 +- assets/styles/app.css | 52 +++++- templates/_partials/_thread_row.html.twig | 122 +++++++++---- templates/mail/inbox.html.twig | 6 +- .../settings/_appearance_preview.html.twig | 2 +- tests/e2e/calendar.spec.ts | 5 +- tests/e2e/list-row-breakpoint.spec.ts | 165 ++++++++++++++++++ tests/e2e/live-counts.spec.ts | 5 +- 9 files changed, 330 insertions(+), 47 deletions(-) create mode 100644 tests/e2e/list-row-breakpoint.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0af5ae59..b92e923b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/DESIGN.md b/DESIGN.md index 6f009c7f..4f8c8272 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -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`, diff --git a/assets/styles/app.css b/assets/styles/app.css index e9a333d2..a422ce89 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -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; diff --git a/templates/_partials/_thread_row.html.twig b/templates/_partials/_thread_row.html.twig index 6769f9e8..55b15236 100644 --- a/templates/_partials/_thread_row.html.twig +++ b/templates/_partials/_thread_row.html.twig @@ -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"> - {# Content — one line when the list is wide, three when it is not #} -
+ {# Content — three shapes, chosen by the LIST's width: - {# Row 1 (stacked) / sender column (wide) #} -
+ 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`. #} +
+ + {# 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. #} +
{# 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. #} -
-
- +
+ {{ 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. #} - + {# Two lines: the whole second row, under the sender as + well as the subject. The grid's row gap is the air. #} + {# 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. {% endif %} - + {% include '_partials/_thread_labels.html.twig' with { item: item } %} - {# 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.
- @@ -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. #}
+ @rowwide:flex-row-reverse @rowwide:items-center @rowwide:justify-end @rowwide:gap-2 + @rowwide:self-center @rowwide:w-36"> -
+
{# 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. #} + group-hover:invisible group-has-[[aria-expanded=true]]:invisible"> {# The visibility classes go on a wrapper, never on the 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 %} -