mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
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:
9 files changed
+330
-47
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user