diff --git a/assets/styles/app.css b/assets/styles/app.css index 4f8934e1..33adccaa 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -45,6 +45,15 @@ --app-bg: linear-gradient(to bottom right, #eff6ff, #f1f5f9, #e0e7ff); --scrim-alpha: 0; + /* The sheet a message body is rendered on. Deliberately the same warm + paper in every theme: an email's HTML was authored against a light + background — senders set dark text and assume white behind it — so + rendering it on a near-black surface is both harsh to read and a + misrepresentation of what was sent. Themes may override it; none + currently need to. */ + --rgb-sheet: 247 245 239; + --rgb-sheet-ink: 35 34 32; + /* user-tunable — overwritten inline on by AppearanceRenderer */ --rgb-main: var(--rgb-surface); @@ -382,6 +391,47 @@ 0 4px 6px -4px rgb(var(--rgb-shadow) / var(--shadow-alpha)); } +@utility mail-sheet { + /* Re-points the whole palette, not just the background. + Everything inside here — dividers, muted labels, inset blocks, hover + states — is written against the theme's tokens, and on a dark theme + those are pale-on-dark: a border at white/0.09 is invisible on cream and + a bg-sunken of white/0.07 lightens where it should darken. Setting the + channels rather than restyling each descendant means every existing + utility keeps working and nothing new has to know about the sheet. */ + --rgb-surface: 247 245 239; + --rgb-main: 247 245 239; + --rgb-ink: 35 34 32; + --rgb-ink-soft: 82 78 72; + --rgb-ink-muted: 107 102 94; + --rgb-ink-faint: 150 144 134; + --rgb-line: 35 34 32; + --rgb-border: 35 34 32; + --rgb-raised: 35 34 32; + --rgb-sunken: 35 34 32; + --rgb-field: 35 34 32; + --rgb-field-border: 35 34 32; + --rgb-inverse: 35 34 32; + --rgb-inverse-ink: 247 245 239; + + --main-alpha: 1; + --line-alpha: 0.09; + --border-alpha: 0.10; + --raised-alpha: 0.04; + --sunken-alpha: 0.05; + --hover-alpha: 0.045; + --field-alpha: 0.03; + --field-border-alpha: 0.15; + --shadow-alpha: 0.06; + + background-color: rgb(var(--rgb-surface)); + color: rgb(var(--rgb-ink)); + + /* Plenty of mail leaves its links to the client, and an accent tuned for + the surrounding theme is not necessarily readable on paper. */ + & a:not([class*="text-"]):not([class*="bg-"]) { color: #1d4ed8; } +} + @utility app-bg { position: relative; background-image: var(--app-bg); diff --git a/templates/_partials/_thread_row.html.twig b/templates/_partials/_thread_row.html.twig index 7315a20b..bb0f29ce 100644 --- a/templates/_partials/_thread_row.html.twig +++ b/templates/_partials/_thread_row.html.twig @@ -134,20 +134,9 @@ the conversation it answers. aria-hidden="true"> - {# Star — wide layout only; the stacked one lives in the meta column - on the right, beside the time. #} - - {# Content — one line when the list is wide, three when it is not #} -
+
{# Row 1 (stacked) / sender column (wide) #}
@@ -175,82 +164,43 @@ the conversation it answers.
- {# Meta column — stacked layout only. - Mirrors the reference app: the time sits top-right and the row's - three states hang under it. The star is duplicated rather than moved - because the wide layout keeps it on the left, and only one of the two - is ever rendered — display:none takes the other out of the - accessibility tree, so a screen reader still finds exactly one. That - is safe here because starring POSTs and the server re-renders the - whole row; neither button holds state of its own. #} -
- - {% if date %} - {% if date|date('Y-m-d') == 'now'|date('Y-m-d') %} - {{ date|date('g:i a') }} - {% elseif date|date('Y') == 'now'|date('Y') %} - {{ date|date('M j') }} - {% else %} - {{ date|date('M j Y') }} + {# Meta column — one column at every width. + Date on top, then the star and the paperclip, then the unread dot. + The star is a control and sits inboard of the paperclip, which is + only a fact about the row; both are on the trailing edge where the + eye already is after reading the time. + + 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 absolutely positioned over the whole column, which is why the + star could not live here before. #} + {# The meta area takes the row's own shape. A stacked row is three + lines tall and a vertical stack sits beside it; a wide row is one + line, where the same stack has to spread out and leaves the date + floating over a lone star with air between them. So: column when + narrow, single line when wide. #} +
+ +
+ + {% if date %} + {% if date|date('Y-m-d') == 'now'|date('Y-m-d') %} + {{ date|date('g:i a') }} + {% elseif date|date('Y') == 'now'|date('Y') %} + {{ date|date('M j') }} + {% else %} + {{ date|date('M j Y') }} + {% endif %} {% endif %} - {% endif %} - - - {# Attachment and star share a line, star outermost — it is the one - you reach for, so it sits where the thumb already is, and the - paperclip is a fact about the row rather than a control. #} -
- {% if hasAttachment %} - - - {{ 'thread_row.has_attachment'|trans }} - - {% endif %} - - -
- - {# Unread, as a dot, on its own line. The bold text says it too, but - a dot is what you count down a column without reading anything. #} - {% if unread %} - - {{ 'thread_row.unread'|trans|default('Unread') }} - {% endif %} -
- - {# Date + hover actions slot — desktop only #} -
diff --git a/templates/mail/_message_content.html.twig b/templates/mail/_message_content.html.twig index 0ea3ea5d..d5e537b9 100644 --- a/templates/mail/_message_content.html.twig +++ b/templates/mail/_message_content.html.twig @@ -39,11 +39,11 @@
{% if message.bodyHtmlSafe %} -
+
{{ message.bodyHtmlSafe|raw }}
{% else %} -
+
{{ message.bodyText }}
{% endif %} diff --git a/templates/mail/_thread_message.html.twig b/templates/mail/_thread_message.html.twig index 3c136568..0bfc5843 100644 --- a/templates/mail/_thread_message.html.twig +++ b/templates/mail/_thread_message.html.twig @@ -120,11 +120,11 @@ Variables: {# ── Body — full pane width ─────────────────────────────────────────── #}
{% if message.bodyHtmlSafe %} -
+
{{ message.bodyHtmlSafe|raw }}
{% else %} -
{{ message.bodyText }}
+
{{ message.bodyText }}
{% endif %} {% if message.hasAttachments %} diff --git a/tests/e2e/calendar.spec.ts b/tests/e2e/calendar.spec.ts index 26c13960..de033d8a 100644 --- a/tests/e2e/calendar.spec.ts +++ b/tests/e2e/calendar.spec.ts @@ -259,15 +259,13 @@ test.describe("calendar pane", () => { await page.setViewportSize({ width: 1440, height: 900 }); await ensurePaneClosed(page); - const inlineDate = page.locator('[data-row-date="inline"]').first(); - const stackedDate = page.locator('[data-row-date="stacked"]').first(); + // The content block is what changes shape: a column when the list is + // narrow, a row when it is wide. Read on flex-direction rather than on + // visibility — the row's parts sit in zero-height positioning contexts + // that Playwright calls hidden either way. + const layout = page.locator("[data-row-layout]").first(); - // On display rather than visibility: the inline date column is laid out - // as a zero-height positioning context for an absolutely placed span, - // so Playwright calls it hidden either way. Which layout the container - // query picked is the actual subject here. - await expect(inlineDate).toHaveCSS("display", "flex"); - await expect(stackedDate).toHaveCSS("display", "none"); + await expect(layout).toHaveCSS("flex-direction", "row"); await page.locator("[data-calendar-toggle]").click(); await expect(page.locator("turbo-frame#calendar-pane-frame")).toBeVisible(); @@ -280,8 +278,7 @@ test.describe("calendar pane", () => { await page.mouse.move(20, box.y + box.height / 2, { steps: 12 }); await page.mouse.up(); - await expect(stackedDate).toHaveCSS("display", "flex"); - await expect(inlineDate).toHaveCSS("display", "none"); + await expect(layout).toHaveCSS("flex-direction", "column"); }); /**