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
|
## 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
|
**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
|
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
|
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
|
- **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.
|
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
|
- **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
|
between three lines, two and one on the *list's* width (`@xl`, 36rem, and `@rowwide`, 75rem),
|
||||||
make the list narrower than a phone.
|
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
|
- **Nothing scrolls the page sideways.** Panes scroll their own overflow. The shell uses `dvh` and
|
||||||
honours safe-area insets.
|
honours safe-area insets.
|
||||||
- **Design at German length, at 320px wide, and at text scale 1.25.**
|
- **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
|
- A **36px avatar disc** that doubles as the checkbox. It turns into an accent disc with a tick on
|
||||||
hover or when checked.
|
hover or when checked.
|
||||||
- **Sender, subject, snippet, chips, time.** In a wide list they share one line, subject and snippet
|
- **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
|
- **The row** is `flex items-center gap-3 pl-3 pr-4 list-row-y`. Its padding comes from the density
|
||||||
variable, never `py-*`.
|
variable, never `py-*`.
|
||||||
- **Unread:** sender and subject `font-semibold text-ink`, plus a faint white wash (`row-unread`,
|
- **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
|
preview in the settings pane is styled by these same rules rather than by a
|
||||||
copy of them that could drift. */
|
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. */
|
/* The account corner: the coloured triangle in a row's lower-left. */
|
||||||
[data-account-corner] {
|
[data-account-corner] {
|
||||||
display: var(--list-corner-display);
|
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
|
arrangement the wide layout exists to avoid. So the wide branch takes the
|
||||||
same on/off switch without the clamp.
|
same on/off switch without the clamp.
|
||||||
|
|
||||||
`@container`, matching the row's own `@xl` (Tailwind's 36rem container
|
`@container`, matching the row's own `@rowwide` (the 75rem container width
|
||||||
width, written out by hand because a plain rule cannot use the variant): the
|
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
|
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.
|
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
|
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
|
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.
|
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) {
|
[data-row-snippet]:not(.hidden) {
|
||||||
display: var(--list-preview-display);
|
display: var(--list-preview-display);
|
||||||
@@ -5155,8 +5192,13 @@ label:has(> input.sr-only) {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@container (min-width: 36rem) {
|
@container (min-width: theme(--container-rowwide)) {
|
||||||
[data-row-snippet] {
|
/* `: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);
|
display: var(--list-preview-display-wide);
|
||||||
-webkit-line-clamp: 1;
|
-webkit-line-clamp: 1;
|
||||||
white-space: nowrap;
|
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
|
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
|
properties, which is what lets the density differ per surface and the
|
||||||
unread tint scale with the emphasis setting. #}
|
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' }}
|
{{ unread ? 'row-unread' : 'bg-transparent' }}
|
||||||
hover:bg-white/60 dark:hover:bg-white/[0.06]"
|
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>
|
aria-hidden="true"><i class="fa-solid fa-check text-xs"></i></span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{# Content — one line when the list is wide, three when it is not #}
|
{# Content — three shapes, chosen by the LIST's width:
|
||||||
<div data-row-layout
|
|
||||||
class="flex-1 min-w-0 flex flex-col @xl:flex-row @xl:items-center @xl:gap-4">
|
|
||||||
|
|
||||||
{# Row 1 (stacked) / sender column (wide) #}
|
below `@xl` (36rem) three lines: sender, subject, preview
|
||||||
<div class="flex items-center justify-between gap-2 @xl:justify-start @xl:w-40 @xl:flex-shrink-0">
|
`@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
|
{# Sender and its marker travel together in one group, so the
|
||||||
badge sits to the LEFT of the name at both container widths.
|
badge sits to the LEFT of the name at both container widths.
|
||||||
Hanging it off the outer flex instead would have put it at
|
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 same position wedges them between a subject and a preview
|
||||||
that are already competing for one line, so they drop to a line
|
that are already competing for one line, so they drop to a line
|
||||||
of their own underneath. #}
|
of their own underneath. #}
|
||||||
<div class="min-w-0 flex-1 flex flex-col">
|
<div class="min-w-0 flex-1 flex flex-col @xl:@max-rowwide:contents">
|
||||||
<div class="min-w-0 flex flex-col @xl:flex-row @xl:items-baseline @xl:gap-2">
|
<div class="min-w-0 flex flex-col @xl:@max-rowwide:contents @rowwide:flex-row @rowwide:items-baseline @rowwide:gap-2">
|
||||||
<span class="truncate text-sm mt-1 @xl:mt-0 @xl:flex-shrink-0 @xl:max-w-[45%]
|
<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' }}">
|
{{ unread ? 'font-semibold text-ink' : 'font-normal text-ink-soft' }}">
|
||||||
{{ subjectLine }}
|
{{ subjectLine }}
|
||||||
{% if count > 1 %}
|
{% 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
|
{# A little air above the preview when stacked. It separates
|
||||||
the subject from the snippet, and the meta column mirrors
|
the subject from the snippet, and the meta column mirrors
|
||||||
the same margin so the dot stays level with it. #}
|
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
|
{# A draft with a send time on it. Without this the
|
||||||
Drafts list is the one place a scheduled message
|
Drafts list is the one place a scheduled message
|
||||||
lives and the only place that does not say so — the
|
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>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="@xl:hidden contents">
|
<span class="@rowwide:hidden contents">
|
||||||
{% include '_partials/_thread_labels.html.twig' with { item: item } %}
|
{% include '_partials/_thread_labels.html.twig' with { item: item } %}
|
||||||
</span>
|
</span>
|
||||||
{# A stacked row has one line for the preview, and a
|
{# A three-line row has one short line for the preview,
|
||||||
scheduled draft has already spent most of it saying
|
and a scheduled draft has already spent most of it
|
||||||
when it goes — what is left of the snippet at 393px
|
saying when it goes — what is left of the snippet at
|
||||||
is two characters and an ellipsis, which is noise
|
393px is two characters and an ellipsis, which is
|
||||||
where the badge is information. Wide, both fit and
|
noise where the badge is information. From `@xl` up
|
||||||
both are shown. #}
|
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
|
{# `@xl:inline-block`, not `@xl:block`, and this is the
|
||||||
second time this file has paid for it: Tailwind emits
|
second time this file has paid for it: Tailwind emits
|
||||||
`.hidden` AFTER `.block`, so `hidden @xl:block` is
|
`.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
|
not outrank it. `.inline-block` is emitted after
|
||||||
`.hidden` and wins, and a flex item blockifies it
|
`.hidden` and wins, and a flex item blockifies it
|
||||||
anyway, so `truncate` still truncates. The pairing
|
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. #}
|
the same reason in the other direction. #}
|
||||||
{# `data-row-snippet` carries the preview-lines setting;
|
{# `data-row-snippet` carries the preview-lines setting;
|
||||||
`truncate` stays for the one-line default, and
|
`truncate` stays for the one-line default, and
|
||||||
@@ -615,7 +644,7 @@ be a worse regression than the one this feature fixes.
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</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 } %}
|
{% include '_partials/_thread_labels.html.twig' with { item: item } %}
|
||||||
</div>
|
</div>
|
||||||
</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.
|
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
|
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
|
absolutely positioned over the whole column, which is why nothing
|
||||||
else could live here. #}
|
else could live here. #}
|
||||||
{# The meta area takes the row's own shape. A stacked row is three
|
{# 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
|
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 —
|
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
|
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
|
<div data-row-meta
|
||||||
class="flex flex-shrink-0 ml-2
|
class="flex flex-shrink-0 ml-2
|
||||||
flex-col items-end self-start
|
flex-col items-end self-start
|
||||||
@xl:flex-row-reverse @xl:items-center @xl:justify-end @xl:gap-2
|
@rowwide:flex-row-reverse @rowwide:items-center @rowwide:justify-end @rowwide:gap-2
|
||||||
@xl:self-center @xl:w-36">
|
@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
|
{# The paperclip rides with the date: wide, it sits just inboard
|
||||||
of it and vanishes with it when the hover actions take the
|
of it and vanishes with it when the hover actions take the
|
||||||
row over. Leaving it outside meant it stayed put underneath
|
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
|
<span class="absolute inset-0 flex items-center justify-end gap-1.5
|
||||||
text-xs text-ink-faint whitespace-nowrap
|
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>
|
{# The visibility classes go on a wrapper, never on the <i>
|
||||||
itself: Font Awesome's own .fa-solid rule sets display,
|
itself: Font Awesome's own .fa-solid rule sets display,
|
||||||
and it lands after Tailwind's utilities, so `hidden` on
|
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
|
Every other icon in this file is wrapped for the same
|
||||||
reason. #}
|
reason. #}
|
||||||
{% if hasAttachment %}
|
{% 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>
|
<i class="fa-solid fa-paperclip text-[11px]"></i>
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% 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
|
{# 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
|
one of these buttons is open. That menu is a descendant, so
|
||||||
without the second rule a pointer that strays off the row on
|
without the second rule a pointer that strays off the row on
|
||||||
its way to the menu hides the menu it was heading for. #}
|
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">
|
|
||||||
|
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' %}
|
{% if false == draft and placement == 'normal' %}
|
||||||
<button type="button" data-action="click->mail--message-row#archive"
|
<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}) }}"
|
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
|
{# 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
|
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. #}
|
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
|
{# Stacked, the paperclip belongs with the star: there is no
|
||||||
hover for it to vanish under, and a three-line row reads
|
hover for it to vanish under, and a three-line row reads
|
||||||
better with its two marks together than with one stranded
|
better with its two marks together than with one stranded
|
||||||
beside the time. The wide copy lives with the date. #}
|
beside the time. The wide copy lives with the date. #}
|
||||||
{% if hasAttachment %}
|
{% 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>
|
<i class="fa-solid fa-paperclip text-xs" aria-hidden="true"></i>
|
||||||
</span>
|
</span>
|
||||||
<span class="sr-only">{{ 'thread_row.has_attachment'|trans }}</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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Stacked only — a wide row already says "unread" twice over, with
|
{# Three lines only — a wide row already says "unread" twice over,
|
||||||
a bold sender and a bold subject.
|
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:
|
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
|
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
|
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
|
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
|
(`shrink-0`) and the row overflows sideways — inside the nav, never the
|
||||||
page. Padding tightens below the list's `@xl`, the same breakpoint the
|
page. Padding tightens below the list's `@xl` (36rem), which is
|
||||||
rows use, because the card can be narrow on a wide viewport too.
|
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
|
`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
|
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.
|
to be wrong.
|
||||||
|
|
||||||
`@container` on the list: the snippet's two-line clamp is a container query
|
`@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.
|
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
|
{# 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.
|
* rendering one-line rows, truncated to nothing.
|
||||||
*/
|
*/
|
||||||
test("stacks the mail rows once the pane has taken enough width", async ({ page }) => {
|
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);
|
await ensurePaneClosed(page);
|
||||||
|
|
||||||
// The content block is what changes shape: a column when the list is
|
// 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.
|
* would pass the read case alone.
|
||||||
*/
|
*/
|
||||||
test("the row's own mark-read button moves the badge, and back again", async ({ page }) => {
|
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
|
// Wide, so the row is one line and the action strip sits where this
|
||||||
// list pane — so it does not exist at all in a narrow window.
|
// 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.setViewportSize({ width: 1600, height: 900 });
|
||||||
await page.goto("/mail/inbox");
|
await page.goto("/mail/inbox");
|
||||||
await expect(mailRow(page, INBOX_SUBJECTS.read)).toBeVisible();
|
await expect(mailRow(page, INBOX_SUBJECTS.read)).toBeVisible();
|
||||||
|
|||||||
Reference in new issue
Block a user