Mail: list uses full width and reflows when the reader opens #1390

Closed
opened 2026-10-07 18:12:51 +00:00 by kayg · 13 comments
Owner

Owner report (2026-10-07)

"in general, mail items should be able to take maximum width and responsively wrap when email is opened to the side or bottom? basically having it centered like this flat, looks very weird"

Screenshot context: Mail, Morph layout, no message open, wide window. The list is a centered 42rem column (.mail-list-region { max-width: 42rem; margin-inline: auto } in apps/web/src/lib/mail/MailView.svelte) with large empty margins on both sides.

Decision

  • The Mail list fills the available width in every layout (Morph, columns, rows) with the normal page inset; no centered fixed-width column.
  • Rows reflow by the list's own width (container queries), not the window: when wide, one line per message (sender, subject + preview, date) with sender in a fixed-width column; when narrower (reader opened to the side, phone), the current two-line row. Opening the reader to the side or bottom reflows rows at once with no horizontal scroll and no clipped dates.
  • Long subjects and previews truncate with an ellipsis; the date stays right-aligned on the first line.
## Owner report (2026-10-07) "in general, mail items should be able to take maximum width and responsively wrap when email is opened to the side or bottom? basically having it centered like this flat, looks very weird" Screenshot context: Mail, Morph layout, no message open, wide window. The list is a centered 42rem column (`.mail-list-region { max-width: 42rem; margin-inline: auto }` in apps/web/src/lib/mail/MailView.svelte) with large empty margins on both sides. ## Decision - The Mail list fills the available width in every layout (Morph, columns, rows) with the normal page inset; no centered fixed-width column. - Rows reflow by the list's own width (container queries), not the window: when wide, one line per message (sender, subject + preview, date) with sender in a fixed-width column; when narrower (reader opened to the side, phone), the current two-line row. Opening the reader to the side or bottom reflows rows at once with no horizontal scroll and no clipped dates. - Long subjects and previews truncate with an ellipsis; the date stays right-aligned on the first line.
Author
Owner

Starting work on job/mailrows-1390 from base 0419170fe4 (branch job/mailrows-1390). I will handle #1389 and #1390 in separate commits and comments.

Starting work on job/mailrows-1390 from base 0419170fe456b58614befcb12d9c9d255118f1c0 (branch job/mailrows-1390). I will handle #1389 and #1390 in separate commits and comments.
Author
Owner

Local 10k-row baseline is captured before the list-width change (source ffe305d4f0): 10,002 loaded rows at 1440×900, p50 133.3 ms, p95 366.6 ms, 118/119 frames over 16.7 ms, server RSS 313,520→313,528 KiB, server CPU 270 ms. The host load average was 49.41 / 56.45 / 51.49 during the run, so these local timings are contaminated and will be reported with that limitation. The 10k fixture and scroll profile are local; the after run will use the same profile and report its load.

Local 10k-row baseline is captured before the list-width change (source ffe305d4f0070119841a83e77b12710f924d404): 10,002 loaded rows at 1440×900, p50 133.3 ms, p95 366.6 ms, 118/119 frames over 16.7 ms, server RSS 313,520→313,528 KiB, server CPU 270 ms. The host load average was 49.41 / 56.45 / 51.49 during the run, so these local timings are contaminated and will be reported with that limitation. The 10k fixture and scroll profile are local; the after run will use the same profile and report its load.
Author
Owner

Finding: the first post-merge bun run check exited 0 with 0 errors and a warning on the thread date selector. Svelte scoped CSS did not match the class passed into shared Pill. I changed the selector to target the rendered Pill DOM globally; the final check is running now.

Finding: the first post-merge bun run check exited 0 with 0 errors and a warning on the thread date selector. Svelte scoped CSS did not match the class passed into shared Pill. I changed the selector to target the rendered Pill DOM globally; the final check is running now.
Author
Owner

Finding: bun run build failed because the current LightningCSS minifier rejected inside as an invalid media query. I will use the 36rem query literal and retain the 14rem sender width as a shared role token. This keeps the CSS container reflow and avoids per-row resize measurement.

Finding: bun run build failed because the current LightningCSS minifier rejected inside as an invalid media query. I will use the 36rem query literal and retain the 14rem sender width as a shared role token. This keeps the CSS container reflow and avoids per-row resize measurement.
Author
Owner

Finding: bun run build failed because the current LightningCSS minifier rejected var(--mail-row-wide-breakpoint) inside @container as an invalid media query. I will use the 36rem query literal and retain the 14rem sender width as a shared role token. This keeps CSS container reflow and avoids per-row resize measurement.

Finding: bun run build failed because the current LightningCSS minifier rejected var(--mail-row-wide-breakpoint) inside @container as an invalid media query. I will use the 36rem query literal and retain the 14rem sender width as a shared role token. This keeps CSS container reflow and avoids per-row resize measurement.
Author
Owner

The production screenshot run exposed two harness assumptions. The Reload button recreated the build-update toast while the web bundle and shared server build differed; the row zoom also selected a virtualized row outside the viewport. I changed the capture helper to dismiss the toast with Escape and assert that no toast remains, and to scroll the target row into view before cropping. I am rerunning the Mail e2e before using any captures as proof.

The production screenshot run exposed two harness assumptions. The Reload button recreated the build-update toast while the web bundle and shared server build differed; the row zoom also selected a virtualized row outside the viewport. I changed the capture helper to dismiss the toast with Escape and assert that no toast remains, and to scroll the target row into view before cropping. I am rerunning the Mail e2e before using any captures as proof.
Author
Owner

The retry reached the 1440 px Columns row crop, then the shared screenshot helper failed while importing sharp (libstdc++.so.6 could not be loaded; the import also exited with SIGSEGV when the system library path was supplied). I changed this test to capture the row element directly with Playwright at CSS pixel scale. The row is scrolled into view first, so this remains a real production screenshot and needs no native image-processing dependency.

The retry reached the 1440 px Columns row crop, then the shared screenshot helper failed while importing sharp (`libstdc++.so.6` could not be loaded; the import also exited with SIGSEGV when the system library path was supplied). I changed this test to capture the row element directly with Playwright at CSS pixel scale. The row is scrolled into view first, so this remains a real production screenshot and needs no native image-processing dependency.
Author
Owner

The next screenshot check found that a route could preserve a virtual-list scroll offset even though Mail rows were mounted. The captured Columns empty state showed a blank list, so the image was not valid proof. The capture helper now resets the list to its first row and waits until a real row intersects the list viewport before it takes any Mail screenshot. I discarded the partial captures and am rerunning the suite.

The next screenshot check found that a route could preserve a virtual-list scroll offset even though Mail rows were mounted. The captured Columns empty state showed a blank list, so the image was not valid proof. The capture helper now resets the list to its first row and waits until a real row intersects the list viewport before it takes any Mail screenshot. I discarded the partial captures and am rerunning the suite.
Author
Owner

The row-viewport check showed that mounted virtual placeholders can still precede the real Mail response. That produced a blank capture even after resetting scroll. I tightened the capture precondition: wait for .mail-view[aria-busy=false], then require a populated row with a stable message ID inside the viewport. This rejects half-loaded screenshots instead of accepting them as evidence.

The row-viewport check showed that mounted virtual placeholders can still precede the real Mail response. That produced a blank capture even after resetting scroll. I tightened the capture precondition: wait for `.mail-view[aria-busy=false]`, then require a populated row with a stable message ID inside the viewport. This rejects half-loaded screenshots instead of accepting them as evidence.
Author
Owner

One later e2e attempt failed before screenshots after selecting fixture message cccccccc-cccc-4ccc-8ccc-ccccccccccc3: the row was selected, but the reader showed “This message could not be opened” and no body host appeared within 20 seconds. Earlier attempts with the same fixture progressed past this reader check. I added diagnostics that record only the Mail API status/body-presence metadata and redacted server warnings, then I will retry once to distinguish an API defect from shared-host interference.

One later e2e attempt failed before screenshots after selecting fixture message `cccccccc-cccc-4ccc-8ccc-ccccccccccc3`: the row was selected, but the reader showed “This message could not be opened” and no body host appeared within 20 seconds. Earlier attempts with the same fixture progressed past this reader check. I added diagnostics that record only the Mail API status/body-presence metadata and redacted server warnings, then I will retry once to distinguish an API defect from shared-host interference.
Author
Owner

Implemented #1390 on job/mailrows-1390.

Head SHA: 8078bface56724c49520e651b2cb4378d6b51d8e

Removed the 42rem centered list cap. Morph now uses the full page inset; Columns and Split use the full list pane. The list owns a CSS container query: narrow rows put sender/date on line one and subject/preview on line two; wide rows use sender, content, and date columns. The date stays visible, text ellipsizes, and the list does not gain horizontal scrolling. Resizing or opening a side/bottom reader changes the container width and reflows rows without a runtime observer or layout read. Virtualization keeps the existing row model.

The shared --mail-row-sender-column token defines the 14rem sender track. Existing shared Pill and IconLabel components remain the visible controls; no feature-local button or row treatment was added.

Files: apps/web/src/lib/mail/MailView.svelte, apps/web/src/lib/mail/MailView.svelte.test.ts, apps/web/e2e/mail-layouts.mjs, bench/mail-list-scroll-1390.mjs, packages/ui/src/tokens.css, docs/DESIGN.md, docs/perf/baseline.json, and exact perf contract hashes in contracts/perf/.

Gates

bun run check exited 0. Verbatim summary lines:

PASS production dependency licences: 759 locked package releases across apps/web, apps/docs and packages
perf-lint: PASS; 0 violations; 22544 scoped exceptions
User browser caches use userStorage; only documented device/public-link exceptions remain.
Glass alpha, blur and backdrop-filter roles use packages/ui/src/tokens.css.
Text sizes and UI shape values use shared role tokens.
Keyboard focus rings use the shared focus tokens.
UI transitions and animation options use shared motion tokens or documented exceptions.
svelte-check found 0 errors and 1 warning in 1 file

The one Svelte warning is the existing unused .ready selector in the unrelated Money route at apps/web/src/routes/money/[budget]/[month]/+page.svelte:407.

Focused Vitest output:

 Test Files  1 passed (1)
      Tests  2 passed (2)

Mail e2e output:

Mail layouts e2e passed; screenshots are in artifacts/mail-layouts

10k-message local scroll profile: 10,004 fixture messages, 10,002 loaded rows, 1440×900. Before: p50 133.3 ms, p95 366.6 ms, 26 long tasks (max 310 ms), server CPU 270 ms, RSS 313520→313528 KiB; load average 49.41/56.45/51.49. After: p50 100.0 ms, p95 233.3 ms, 11 long tasks (max 193 ms), server CPU 180 ms, RSS 324340→324340 KiB; load average 34.89/41.53/39.63. The host was heavily loaded for both runs and less loaded for the after run, so this is directional rather than a controlled comparison. Full results are in docs/perf/baseline.json. The reflow work is O(1) CSS style evaluation per rendered row; the list remains virtualized, with no per-row measurement.

Screenshot proof (macOS platform emulation, actual-size PNGs):

UX gaps closed: Layout responds to actual list-pane width, preserves a visible date, avoids horizontal scroll, and captures only after real Mail data is painted. Mouse and Enter reader activation show no focus ring; the row crops show sender, subject, preview, and date together. UX gaps left: None found in the requested layouts.

Decisions not stated in DESIGN: The query threshold is a literal 36rem. LightningCSS 1.33 rejected a custom property in an @container size condition during production build, so the threshold must remain literal. 36rem lets the maximum 620px Columns pane reach the wide layout. The sender track remains the shared 14rem token. DESIGN §45 records both values and the reflow contract.

For the merge round: Run cd apps/web && bun run test for the full Vitest suite, plus the full e2e and adversarial Mail matrices, to verify this branch combined with the merge-round build.

Implemented #1390 on `job/mailrows-1390`. **Head SHA:** `8078bface56724c49520e651b2cb4378d6b51d8e` Removed the 42rem centered list cap. Morph now uses the full page inset; Columns and Split use the full list pane. The list owns a CSS container query: narrow rows put sender/date on line one and subject/preview on line two; wide rows use sender, content, and date columns. The date stays visible, text ellipsizes, and the list does not gain horizontal scrolling. Resizing or opening a side/bottom reader changes the container width and reflows rows without a runtime observer or layout read. Virtualization keeps the existing row model. The shared `--mail-row-sender-column` token defines the 14rem sender track. Existing shared `Pill` and `IconLabel` components remain the visible controls; no feature-local button or row treatment was added. **Files:** `apps/web/src/lib/mail/MailView.svelte`, `apps/web/src/lib/mail/MailView.svelte.test.ts`, `apps/web/e2e/mail-layouts.mjs`, `bench/mail-list-scroll-1390.mjs`, `packages/ui/src/tokens.css`, `docs/DESIGN.md`, `docs/perf/baseline.json`, and exact perf contract hashes in `contracts/perf/`. **Gates** `bun run check` exited 0. Verbatim summary lines: ```text PASS production dependency licences: 759 locked package releases across apps/web, apps/docs and packages perf-lint: PASS; 0 violations; 22544 scoped exceptions User browser caches use userStorage; only documented device/public-link exceptions remain. Glass alpha, blur and backdrop-filter roles use packages/ui/src/tokens.css. Text sizes and UI shape values use shared role tokens. Keyboard focus rings use the shared focus tokens. UI transitions and animation options use shared motion tokens or documented exceptions. svelte-check found 0 errors and 1 warning in 1 file ``` The one Svelte warning is the existing unused `.ready` selector in the unrelated Money route at `apps/web/src/routes/money/[budget]/[month]/+page.svelte:407`. Focused Vitest output: ```text Test Files 1 passed (1) Tests 2 passed (2) ``` Mail e2e output: ```text Mail layouts e2e passed; screenshots are in artifacts/mail-layouts ``` **10k-message local scroll profile:** 10,004 fixture messages, 10,002 loaded rows, 1440×900. Before: p50 133.3 ms, p95 366.6 ms, 26 long tasks (max 310 ms), server CPU 270 ms, RSS 313520→313528 KiB; load average 49.41/56.45/51.49. After: p50 100.0 ms, p95 233.3 ms, 11 long tasks (max 193 ms), server CPU 180 ms, RSS 324340→324340 KiB; load average 34.89/41.53/39.63. The host was heavily loaded for both runs and less loaded for the after run, so this is directional rather than a controlled comparison. Full results are in `docs/perf/baseline.json`. The reflow work is O(1) CSS style evaluation per rendered row; the list remains virtualized, with no per-row measurement. **Screenshot proof (macOS platform emulation, actual-size PNGs):** - [morph-empty-1440-paper-light.png](https://git.kayg.org/attachments/cbe8b066-2b1b-4d39-bdcc-e07754bc9344) - [morph-empty-1920-paper-light.png](https://git.kayg.org/attachments/33fe2e1a-ae0d-43c7-985b-cec89d0c9d59) - [morph-empty-1440-tokyo-night-dark.png](https://git.kayg.org/attachments/679ca6c0-2afd-4e41-918a-df5a520beb28) - [morph-empty-1920-tokyo-night-dark.png](https://git.kayg.org/attachments/8f354dca-aeef-4f6f-a896-45e97b158974) - [morph-empty-820-paper-light.png](https://git.kayg.org/attachments/beb22b0e-83e7-4b85-8d58-fc14ed3052bf) - [morph-empty-820-tokyo-night-dark.png](https://git.kayg.org/attachments/f97b930b-6902-459e-b022-dd178f929ab1) - [morph-desktop-morph-paper-light.png](https://git.kayg.org/attachments/64e0b84c-5573-4f79-9233-ab1bdb8bb07a) - [morph-desktop-morph-tokyo-night-dark.png](https://git.kayg.org/attachments/efbc0c52-16d9-4cda-87b9-0106108375e3) - [columns-desktop-columns-paper-light.png](https://git.kayg.org/attachments/e3b597c2-a39b-4fa9-83ad-191f3e9f3702) - [columns-desktop-columns-tokyo-night-dark.png](https://git.kayg.org/attachments/9b3d5952-7275-49f7-bab1-a4bf2335216a) - [columns-narrow-desktop-columns-paper-light.png](https://git.kayg.org/attachments/2a2dc7b9-5d87-44e4-b9fd-d4ac88bbc89b) - [columns-narrow-desktop-columns-tokyo-night-dark.png](https://git.kayg.org/attachments/d697b445-0218-4591-a384-64429a0a3d42) - [split-desktop-split-paper-light.png](https://git.kayg.org/attachments/40ec946c-5c6e-49e2-aa26-22a779b920e1) - [split-desktop-split-tokyo-night-dark.png](https://git.kayg.org/attachments/ee713348-e239-435d-a530-deb9d102e2f7) - [split-tablet-split-paper-light.png](https://git.kayg.org/attachments/1b0d8f8d-8821-43cd-9435-6ef1db7623dc) - [split-tablet-split-tokyo-night-dark.png](https://git.kayg.org/attachments/fd82a348-4015-4945-a800-a4552a1434b7) - [columns-tablet-columns-paper-light.png](https://git.kayg.org/attachments/b2e053c8-6451-4646-a1e8-1ffb44d3a88b) - [columns-tablet-columns-tokyo-night-dark.png](https://git.kayg.org/attachments/65b5a2c8-663c-4c57-8076-7c42bede6628) - [mail-list-phone-paper-light.png](https://git.kayg.org/attachments/6699db2a-6706-43cb-83a1-8b71637fc2ac) - [mail-list-phone-tokyo-night-dark.png](https://git.kayg.org/attachments/3a3f8ed9-df05-4813-89fc-253b72ee8cdb) - [columns-phone-push-paper-light.png](https://git.kayg.org/attachments/14de9940-c04e-4b08-a1e4-62a46e708a00) - [columns-phone-push-tokyo-night-dark.png](https://git.kayg.org/attachments/c85ff21a-159a-46be-8d60-0b635a5ab667) - [columns-desktop-columns-row-zoom-paper-light.png](https://git.kayg.org/attachments/4cc20eac-081f-40ef-8032-ac438e7357ba) - [columns-desktop-columns-row-zoom-tokyo-night-dark.png](https://git.kayg.org/attachments/8d7f8e33-4671-4334-842b-29a3cbbd7db2) - [split-desktop-split-row-zoom-paper-light.png](https://git.kayg.org/attachments/58c44a26-ee51-432d-9699-ba6d1efd47fc) - [split-desktop-split-row-zoom-tokyo-night-dark.png](https://git.kayg.org/attachments/4068d8be-b098-49d8-b870-9328aefb1295) - [morph-desktop-morph-row-zoom-paper-light.png](https://git.kayg.org/attachments/fdde3256-18a5-4dfd-9464-4513f54336a8) - [morph-desktop-morph-row-zoom-tokyo-night-dark.png](https://git.kayg.org/attachments/4d93ee3b-009e-43f5-bf5f-adf6f9e3c2bd) - [columns-reader-after-click-paper-light.png](https://git.kayg.org/attachments/554fe770-a64f-4014-b6dc-2554436202c2) - [columns-reader-after-click-tokyo-night-dark.png](https://git.kayg.org/attachments/b3308ade-5fa6-4a3d-bc9c-17e4ed81c5d3) - [morph-reader-after-enter-paper-light.png](https://git.kayg.org/attachments/cb5c0cf0-d7c9-4649-bd16-932a639539c8) - [morph-reader-after-enter-tokyo-night-dark.png](https://git.kayg.org/attachments/848e8b45-657c-4456-bb8d-c3ead285af9c) **UX gaps closed:** Layout responds to actual list-pane width, preserves a visible date, avoids horizontal scroll, and captures only after real Mail data is painted. Mouse and Enter reader activation show no focus ring; the row crops show sender, subject, preview, and date together. **UX gaps left:** None found in the requested layouts. **Decisions not stated in DESIGN:** The query threshold is a literal `36rem`. LightningCSS 1.33 rejected a custom property in an `@container` size condition during production build, so the threshold must remain literal. `36rem` lets the maximum 620px Columns pane reach the wide layout. The sender track remains the shared 14rem token. DESIGN §45 records both values and the reflow contract. **For the merge round:** Run `cd apps/web && bun run test` for the full Vitest suite, plus the full e2e and adversarial Mail matrices, to verify this branch combined with the merge-round build.
Author
Owner

Verification update for #1390 (head remains 8078bface56724c49520e651b2cb4378d6b51d8e).

The full web test command also passed: cd apps/web && bun run test --maxWorkers=2 (exit code 0).

 Test Files  298 passed (298)
      Tests  2118 passed (2118)
   Start at  03:43:38
   Duration  488.68s (transform 28%, environment 26%, import 23%, tests 18%, setup 5%)

The full e2e and adversarial matrices remain merge-round checks per the shared verification policy.

Verification update for #1390 (head remains `8078bface56724c49520e651b2cb4378d6b51d8e`). The full web test command also passed: `cd apps/web && bun run test --maxWorkers=2` (exit code 0). ```text Test Files 298 passed (298) Tests 2118 passed (2118) Start at 03:43:38 Duration 488.68s (transform 28%, environment 26%, import 23%, tests 18%, setup 5%) ``` The full e2e and adversarial matrices remain merge-round checks per the shared verification policy.
Author
Owner

Deployed to calternal.cloud in merge round 35b (build 9dc65437e, live 2026-10-09 ~11:07 IST). Closing.

Deployed to calternal.cloud in merge round 35b (build 9dc65437e, live 2026-10-09 ~11:07 IST). Closing.
kayg closed this issue 2026-10-09 05:36:49 +00:00
Commenting is not possible because the repository is archived.
No labels
No milestone
No project
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal-v0#1390
No description provided.