MAIL: three reading layouts (Columns, Split, Morph), chosen from the view-mode capsule; fast message switching #640

Open
opened 2026-10-01 15:53:57 +00:00 by kayg · 12 comments
Owner

Owner request (2026-10-01): Mail reading layouts, all very fast

Mail now syncs (Fastmail, 99+ in the Inbox). Today, opening a message replaces the list with a full-page reader. The owner wants three layouts:

  1. Columns: sidebar | message list (the open folder) | message reader. The reference is the owner's own earlier app, mailternal (git.kayg.org/kayg/mailternal): reuse its proven pane behaviour and widths where they fit the calternal design system.
  2. Split: sidebar | (message list on top / reader below), with a draggable divider between the two rows (resizable-panel rule: invisible hit zone, persisted per User, double-click resets, keyboard separator).
  3. Morph: the message opens from the Tab Bar morph: the capsule expands into a large reading card over the list, sized like the other morph surfaces (shortcuts card, #583); closing folds it back; ↑/↓ moves to the previous/next message inside the card.
    Rules:
  • Choose the layout from the top row's view-mode capsule (#582), saved per User; it is a deep-linkable state (§33).
  • Default: Columns on wide screens; at narrow widths Columns/Split fall back to Morph; phones always use a full-screen push (as now) or the presentation sheet.
  • All panes resize per the resizable-panel rule; no right info sidebar (the reader is content, not chrome).
  • Keyboard: ↑/↓ move the selection and the open message follows in Columns/Split (no reload flicker); Return opens in Morph; Escape closes Morph.
  • Speed: opening the next message ≤ 50 ms warm (prefetch the next/previous message bodies and the sanitized HTML); switching folder ≤ 100 ms warm with cached list rows; the list virtualised for 50k messages; measured with the #549 harness on the HDD emulation.
  • Also: the reader's "Links in this message" block dumps raw tracking URLs. Show it collapsed by default ("12 links"), with the URLs shortened to their domain.
    Tests: e2e per layout (select message → reader shows it; ↑/↓; resize persists; Morph open/close/↑↓); a perf profile per path. Screenshots at 1440/820/390 in both themes.
    Do not name other products in code, commits or issues (mailternal is our own app).
## Owner request (2026-10-01): Mail reading layouts, all very fast Mail now syncs (Fastmail, 99+ in the Inbox). Today, opening a message replaces the list with a full-page reader. The owner wants three layouts: 1. **Columns:** sidebar | message list (the open folder) | message reader. The reference is the owner's own earlier app, mailternal (`git.kayg.org/kayg/mailternal`): reuse its proven pane behaviour and widths where they fit the calternal design system. 2. **Split:** sidebar | (message list on top / reader below), with a draggable divider between the two rows (resizable-panel rule: invisible hit zone, persisted per User, double-click resets, keyboard `separator`). 3. **Morph:** the message opens from the **Tab Bar morph**: the capsule expands into a large reading card over the list, sized like the other morph surfaces (shortcuts card, #583); closing folds it back; ↑/↓ moves to the previous/next message inside the card. **Rules:** - Choose the layout from the top row's **view-mode capsule** (#582), saved per User; it is a deep-linkable state (§33). - **Default:** Columns on wide screens; at narrow widths Columns/Split fall back to Morph; phones always use a full-screen push (as now) or the presentation sheet. - All panes resize per the resizable-panel rule; no right info sidebar (the reader is content, not chrome). - Keyboard: ↑/↓ move the selection and the open message follows in Columns/Split (no reload flicker); Return opens in Morph; Escape closes Morph. - **Speed:** opening the next message ≤ 50 ms warm (prefetch the next/previous message bodies and the sanitized HTML); switching folder ≤ 100 ms warm with cached list rows; the list virtualised for 50k messages; measured with the #549 harness on the HDD emulation. - Also: the reader's "Links in this message" block dumps raw tracking URLs. Show it collapsed by default ("12 links"), with the URLs shortened to their domain. **Tests:** e2e per layout (select message → reader shows it; ↑/↓; resize persists; Morph open/close/↑↓); a perf profile per path. Screenshots at 1440/820/390 in both themes. Do not name other products in code, commits or issues (mailternal is our own app).
Author
Owner

Started #640 on branch job/maillayouts from base 687ff703136e71e89f8dfba139e93cd0788b25c1. I am first integrating the shared blaze harness and implementing the Mail reader's warm-step path from #641, then I will add the reading layouts and profile the paths.

Started #640 on branch `job/maillayouts` from base `687ff703136e71e89f8dfba139e93cd0788b25c1`. I am first integrating the shared blaze harness and implementing the Mail reader's warm-step path from #641, then I will add the reading layouts and profile the paths.
Author
Owner

Finding: MailView.svelte resets detail, list rows, and loading state on every route change, and each load refetches accounts, folders, and preferences. The message list fetches 50 messages at a time and renders every accumulated row. These behaviors prevent warm arrow traversal and do not virtualize large folders; the implementation will preserve a scoped list snapshot and bound detail cache across selection changes.

Finding: MailView.svelte resets detail, list rows, and loading state on every route change, and each load refetches accounts, folders, and preferences. The message list fetches 50 messages at a time and renders every accumulated row. These behaviors prevent warm arrow traversal and do not virtualize large folders; the implementation will preserve a scoped list snapshot and bound detail cache across selection changes.
Author
Owner

The production-build browser run exposed a startup race in the reader. The detail request could finish before /api/v1/mail/preferences; the default after_one_second rule then posted to /api/v1/mail/messages/<id>/read-state for the seeded Mail message. The fixture account has no provider, so the existing route returned HTTP 500. The reader now waits for the server-backed preference before scheduling read marking, and applies it to the open message when it arrives. The rerun completed all 10 responsive/theme captures with no browser errors.

The production-build browser run exposed a startup race in the reader. The detail request could finish before `/api/v1/mail/preferences`; the default `after_one_second` rule then posted to `/api/v1/mail/messages/<id>/read-state` for the seeded Mail message. The fixture account has no provider, so the existing route returned HTTP 500. The reader now waits for the server-backed preference before scheduling read marking, and applies it to the open message when it arrives. The rerun completed all 10 responsive/theme captures with no browser errors.
Author
Owner

Completed

Built Columns, Split, and Morph reading layouts for Mail. The view-mode capsule is saved per User and included in the deep link. Columns and Split keep the reader in a pane; Morph expands from the Tab Bar and supports Escape and arrow navigation. The list stays virtualized, and the reader reuses one sandboxed iframe.

The reader now keeps a bounded 256-entry / 64 MiB body cache, a four-worker prefetch queue, the visible window and 96 rows in the travel direction. The first 240 loaded rows are warmed without blocking list input. The Links block starts collapsed and displays safe link domains.

The Split divider persists per User and supports pointer drag, keyboard separator controls, snap-to-collapse, and double-click reset. The Mail e2e covers selection/navigation, resize persistence/reset, all layouts, and phone push.

Before / after blaze summary

State Columns mismatch / incomplete frames per pass Browser and load
Before body-window and repeat fixes 114 / 128 average (8 passes) Chromium + WebKit; 1-minute load average 27–35
After, Chromium 0 / 0 in Columns, Split, and Morph 4 passes per desktop layout; load average 22–34
After, WebKit 74 / 74 average in Columns; 186.5 / 186.5 in Split; 23.8 / 23.8 in Morph load average 22–33; frame intervals were hundreds of ms

Final profiles used the shared #641 harness with 10,000 messages and 250 loaded rows. Warm paint p50/p95 below uses only steps that produced a complete paint; the high p95 tails and WebKit mismatches are load-contaminated. The perf VM returned No route to host; these are local runs without HDD emulation. The 50 ms target is not verified.

Path Browser / viewport Warm paint p50 / p95 ms Warm mismatch / incomplete Highest warm p95 frame ms
Columns Chromium · 1440 38.8 / 22,692.3 0 / 0 400
Columns WebKit · 1440 77 / 30,585 94 / 94 588
Split Chromium · 1440 32.7 / 18,308.1 0 / 0 250
Split WebKit · 1440 54 / 20,381 498 / 498 350
Morph Chromium · 1440 26.3 / 17,656.9 0 / 0 300
Morph WebKit · 1440 56 / 24,305 47 / 47 501
Morph fallback Chromium · 820 6,790.9 / 88,560.3 0 / 0 516.8
Morph fallback WebKit · 820 65 / 24,788 51 / 51 535

Visual evidence

View Light Dark
Columns · 1440 Columns at 1440, light Columns at 1440, dark
Split · 1440 Split at 1440, light Split at 1440, dark
Morph · 1440 Morph at 1440, light Morph at 1440, dark
Morph fallback · 820 Morph fallback at 820, light Morph fallback at 820, dark
Phone push · 390 Phone push at 390, light Phone push at 390, dark

Decisions

  • Columns is the wide-screen default at 1200 px and above. Morph is the fallback from 768 through 1199 px. Phones at 767 px and below use the full-screen push reader.
  • The initial Columns width and Split list height are 360 px. The User can resize and persist both.
  • The body cache is capped at 256 entries and 64 MiB. Prefetch covers 96 rows in the travel direction; the warm window covers 240 rows.

Head: 68bd0b4999e4c21dc613f6d9b995b5d319653012.

Gates:

svelte-check found 0 errors and 0 warnings
Test Files  150 passed (150)
      Tests  1015 passed (1015)
Mail layouts e2e passed; screenshots are in /home/kayg/Developer/calternal-wt/maillayouts/artifacts/mail-layouts
✓ built in 58.72s
Wrote site to "build"
✔ done

Known gap: local load prevented a clean verification of the 50 ms target and WebKit repeat completion. No Rust route or contract changed.

## Completed Built Columns, Split, and Morph reading layouts for Mail. The view-mode capsule is saved per User and included in the deep link. Columns and Split keep the reader in a pane; Morph expands from the Tab Bar and supports Escape and arrow navigation. The list stays virtualized, and the reader reuses one sandboxed iframe. The reader now keeps a bounded 256-entry / 64 MiB body cache, a four-worker prefetch queue, the visible window and 96 rows in the travel direction. The first 240 loaded rows are warmed without blocking list input. The Links block starts collapsed and displays safe link domains. The Split divider persists per User and supports pointer drag, keyboard separator controls, snap-to-collapse, and double-click reset. The Mail e2e covers selection/navigation, resize persistence/reset, all layouts, and phone push. ## Before / after blaze summary | State | Columns mismatch / incomplete frames per pass | Browser and load | | --- | ---: | --- | | Before body-window and repeat fixes | 114 / 128 average (8 passes) | Chromium + WebKit; 1-minute load average 27–35 | | After, Chromium | 0 / 0 in Columns, Split, and Morph | 4 passes per desktop layout; load average 22–34 | | After, WebKit | 74 / 74 average in Columns; 186.5 / 186.5 in Split; 23.8 / 23.8 in Morph | load average 22–33; frame intervals were hundreds of ms | Final profiles used the shared #641 harness with 10,000 messages and 250 loaded rows. Warm paint p50/p95 below uses only steps that produced a complete paint; the high p95 tails and WebKit mismatches are load-contaminated. The perf VM returned `No route to host`; these are local runs without HDD emulation. The 50 ms target is not verified. | Path | Browser / viewport | Warm paint p50 / p95 ms | Warm mismatch / incomplete | Highest warm p95 frame ms | | --- | --- | ---: | ---: | ---: | | Columns | Chromium · 1440 | 38.8 / 22,692.3 | 0 / 0 | 400 | | Columns | WebKit · 1440 | 77 / 30,585 | 94 / 94 | 588 | | Split | Chromium · 1440 | 32.7 / 18,308.1 | 0 / 0 | 250 | | Split | WebKit · 1440 | 54 / 20,381 | 498 / 498 | 350 | | Morph | Chromium · 1440 | 26.3 / 17,656.9 | 0 / 0 | 300 | | Morph | WebKit · 1440 | 56 / 24,305 | 47 / 47 | 501 | | Morph fallback | Chromium · 820 | 6,790.9 / 88,560.3 | 0 / 0 | 516.8 | | Morph fallback | WebKit · 820 | 65 / 24,788 | 51 / 51 | 535 | ## Visual evidence | View | Light | Dark | | --- | --- | --- | | Columns · 1440 | ![Columns at 1440, light](https://git.kayg.org/attachments/4eb25338-acfb-484e-90ef-88ca9a355bdb) | ![Columns at 1440, dark](https://git.kayg.org/attachments/55e6234c-d522-4e51-a4e1-02a7fb037218) | | Split · 1440 | ![Split at 1440, light](https://git.kayg.org/attachments/32d9ea9a-58f0-4b38-a83b-e4d7dbd3373b) | ![Split at 1440, dark](https://git.kayg.org/attachments/ef58836a-18b6-4a15-89d1-462d0d8ee443) | | Morph · 1440 | ![Morph at 1440, light](https://git.kayg.org/attachments/4eefee6d-6ab4-45c4-b026-688154b5df8c) | ![Morph at 1440, dark](https://git.kayg.org/attachments/3e354a4f-5e7c-4274-9482-2882d6d8bad4) | | Morph fallback · 820 | ![Morph fallback at 820, light](https://git.kayg.org/attachments/d8eeac4c-3209-45c0-b434-6dcbd5e1715a) | ![Morph fallback at 820, dark](https://git.kayg.org/attachments/0c763a1a-aea9-4c73-8b45-36428edf214c) | | Phone push · 390 | ![Phone push at 390, light](https://git.kayg.org/attachments/cba0afc9-8116-4587-bf69-119b01391ac9) | ![Phone push at 390, dark](https://git.kayg.org/attachments/5843805b-06fe-4a44-ab4a-3c8492e03f04) | ## Decisions - Columns is the wide-screen default at 1200 px and above. Morph is the fallback from 768 through 1199 px. Phones at 767 px and below use the full-screen push reader. - The initial Columns width and Split list height are 360 px. The User can resize and persist both. - The body cache is capped at 256 entries and 64 MiB. Prefetch covers 96 rows in the travel direction; the warm window covers 240 rows. Head: `68bd0b4999e4c21dc613f6d9b995b5d319653012`. Gates: ```text svelte-check found 0 errors and 0 warnings Test Files 150 passed (150) Tests 1015 passed (1015) Mail layouts e2e passed; screenshots are in /home/kayg/Developer/calternal-wt/maillayouts/artifacts/mail-layouts ✓ built in 58.72s Wrote site to "build" ✔ done ``` Known gap: local load prevented a clean verification of the 50 ms target and WebKit repeat completion. No Rust route or contract changed.
Author
Owner

Final audit follow-up: several touched modules still lack the required concise leading module documentation, or retain only a source-attribution comment. The audit found this in apps/web/src/lib/mail/MailView.svelte, apps/web/src/lib/files/FilesSidebar.svelte, apps/web/src/lib/notifications/InboxPanel.svelte, packages/ui/src/components/pageChrome.ts, and some changed e2e/test modules and ported shared components. I reached the four-hour job limit before updating those comments, so this remains a known gap for review.

Final audit follow-up: several touched modules still lack the required concise leading module documentation, or retain only a source-attribution comment. The audit found this in `apps/web/src/lib/mail/MailView.svelte`, `apps/web/src/lib/files/FilesSidebar.svelte`, `apps/web/src/lib/notifications/InboxPanel.svelte`, `packages/ui/src/components/pageChrome.ts`, and some changed e2e/test modules and ported shared components. I reached the four-hour job limit before updating those comments, so this remains a known gap for review.
Author
Owner

Starting round 2 fixes on job/maillayouts, based on origin/dev at 687ff703136e71e89f8dfba139e93cd0788b25c1. Current branch head: 68bd0b4999e4c21dc613f6d9b995b5d319653012. I am addressing the reader frame sizing/banner flow, the Tab Bar morph card, mailbox title, WebKit frame timing, and module docs; I will capture the requested responsive/theme screenshots and report the gates and WebKit results here and on #641.

Starting round 2 fixes on `job/maillayouts`, based on `origin/dev` at `687ff703136e71e89f8dfba139e93cd0788b25c1`. Current branch head: `68bd0b4999e4c21dc613f6d9b995b5d319653012`. I am addressing the reader frame sizing/banner flow, the Tab Bar morph card, mailbox title, WebKit frame timing, and module docs; I will capture the requested responsive/theme screenshots and report the gates and WebKit results here and on #641.
Author
Owner

Round 2 finding: MailReaderContent.svelte capped every body iframe at 200dvh. frame.ts then measured body.scrollHeight, which cannot shrink below the prior iframe viewport after a long-to-short message switch. This explains the clipped 10-screen body and the empty space left by the reused frame. The fix measures the isolated wrapper's intrinsic box and observes it for image/font size changes; the blocked-image notice now stays in normal flow before the frame.

Round 2 finding: `MailReaderContent.svelte` capped every body iframe at `200dvh`. `frame.ts` then measured `body.scrollHeight`, which cannot shrink below the prior iframe viewport after a long-to-short message switch. This explains the clipped 10-screen body and the empty space left by the reused frame. The fix measures the isolated wrapper's intrinsic box and observes it for image/font size changes; the blocked-image notice now stays in normal flow before the frame.
Author
Owner

Finding from the image-heavy reader check: the consent endpoint returned 204, but the reader stayed on the cached blocked HTML (Remote images are blocked, 0 image requests routed, no <img> nodes). changeRemoteContent() fetched through mailDetailCache without invalidating the pre-consent body. I now evict the message detail and any stale prefetch after consent, then fetch the newly sanitized body. The fixture and E2E verify consented image decode and iframe growth.

Finding from the image-heavy reader check: the consent endpoint returned 204, but the reader stayed on the cached blocked HTML (`Remote images are blocked`, 0 image requests routed, no `<img>` nodes). `changeRemoteContent()` fetched through `mailDetailCache` without invalidating the pre-consent body. I now evict the message detail and any stale prefetch after consent, then fetch the newly sanitized body. The fixture and E2E verify consented image decode and iframe growth.
Author
Owner

Round 2 is complete. Head: fce14c9a4.

Built

  • The sandboxed message body now determines the outer frame height. Live body, font and image size changes use the existing frame ResizeObserver; the reader has one scroll area and the remote-image notice stays above the frame.
  • The Morph reader grows from the shared Tab Bar capsule in place, stays to the right of the sidebar, fits short messages, and scrolls within the viewport for long messages. It has no heading bar and reuses the shared pills and ProgressiveBlur.
  • The page title stays on the mailbox. The message subject and sender stay in the reader.
  • Fast Arrow-key repeats update the reader on every selection and write the stable thread URL on key release. This keeps WebKit route state from lagging behind the selected row.
  • Added coverage for short, ten-screen and image-heavy messages, content shrink/growth, title, link updates, sidebar bounds, and all screenshot sizes and themes. Added opt-in virtual-range tracing to the #641 blaze runner.
  • The final docs diff is Mail §45 only. CLAUDE.md matches origin/dev; keyboard-motion rule text is untouched by this job.

Files

  • apps/web/src/lib/mail/MailView.svelte, MailReaderContent.svelte, MailMorphCard.svelte, frame.ts
  • apps/web/src/routes/layout.css
  • packages/ui/src/components/TabBar.svelte, pageChrome.ts
  • apps/web/e2e/mail-layouts.mjs, tests/adversarial/mail_screenshot_fixture.py
  • bench/blaze.mjs, docs/DESIGN.md

Verification

bun run check:

User browser caches use userStorage; only documented device/public-link exceptions remain.
Text sizes and UI shape values use shared role tokens.
UI transitions and animation options use shared motion tokens or documented exceptions.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/maillayouts/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

bun run test:

Test Files  150 passed (150)
     Tests  1015 passed (1015)
   Duration  101.18s (transform 50%, environment 21%, import 14%, tests 11%, setup 5%)

cargo fmt --check: exit code 0, no output. No Rust crate or route changed, so per-crate cargo clippy and cargo test were not applicable. Production build and test:e2e:mail-layouts passed. The production E2E passed all 18 screenshot cases and the image-heavy frame growth checks. The Columns, Split and Morph blaze profiles report 0 mismatches and 0 incomplete frames for Chromium and WebKit; #641 has the per-engine timing and baseline table. Timing is local to a loaded shared host.

Screenshots

390 px

820 px

1440 px

Decisions

  • Keep the selected reader content live during Arrow-key repeat, then replace the stable thread URL once on key release. This was recorded in DESIGN §45 after the WebKit trace showed per-key shallow route updates lagging behind selection.
  • Use the existing frame observer for live content sizing and an initial synchronous fit only after a new sandbox document loads.
  • Keep Morph in the Tab Bar content column, cap it at 62rem by 76dvh, and retain a 12px gap from the sidebar and viewport edge.

Known gaps

  • The local blaze host was loaded (one-minute load average ranged from 13.6 to 33.1 across profiles), so paint p95 values are not a clean performance baseline. The comparisons are recorded on #641.
  • The runner sends 400 Down and 400 Up inputs while 250 rows are loaded; unpainted counts include inputs past the currently painted selection. WebKit does not expose Long Tasks to this runner.
  • The visual reviewer should review the attached captures. No screenshot files are committed.
Round 2 is complete. Head: `fce14c9a4`. ## Built - The sandboxed message body now determines the outer frame height. Live body, font and image size changes use the existing frame `ResizeObserver`; the reader has one scroll area and the remote-image notice stays above the frame. - The Morph reader grows from the shared Tab Bar capsule in place, stays to the right of the sidebar, fits short messages, and scrolls within the viewport for long messages. It has no heading bar and reuses the shared pills and ProgressiveBlur. - The page title stays on the mailbox. The message subject and sender stay in the reader. - Fast Arrow-key repeats update the reader on every selection and write the stable thread URL on key release. This keeps WebKit route state from lagging behind the selected row. - Added coverage for short, ten-screen and image-heavy messages, content shrink/growth, title, link updates, sidebar bounds, and all screenshot sizes and themes. Added opt-in virtual-range tracing to the #641 blaze runner. - The final docs diff is Mail §45 only. `CLAUDE.md` matches `origin/dev`; keyboard-motion rule text is untouched by this job. ## Files - `apps/web/src/lib/mail/MailView.svelte`, `MailReaderContent.svelte`, `MailMorphCard.svelte`, `frame.ts` - `apps/web/src/routes/layout.css` - `packages/ui/src/components/TabBar.svelte`, `pageChrome.ts` - `apps/web/e2e/mail-layouts.mjs`, `tests/adversarial/mail_screenshot_fixture.py` - `bench/blaze.mjs`, `docs/DESIGN.md` ## Verification `bun run check`: ```text User browser caches use userStorage; only documented device/public-link exceptions remain. Text sizes and UI shape values use shared role tokens. UI transitions and animation options use shared motion tokens or documented exceptions. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/maillayouts/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` `bun run test`: ```text Test Files 150 passed (150) Tests 1015 passed (1015) Duration 101.18s (transform 50%, environment 21%, import 14%, tests 11%, setup 5%) ``` `cargo fmt --check`: exit code 0, no output. No Rust crate or route changed, so per-crate `cargo clippy` and `cargo test` were not applicable. Production build and `test:e2e:mail-layouts` passed. The production E2E passed all 18 screenshot cases and the image-heavy frame growth checks. The Columns, Split and Morph blaze profiles report 0 mismatches and 0 incomplete frames for Chromium and WebKit; #641 has the per-engine timing and baseline table. Timing is local to a loaded shared host. ## Screenshots ### 390 px - [columns phone push paper light](https://git.kayg.org/attachments/781f0446-cdf3-45eb-824e-fc770ae4d7b3) - [columns phone push tokyo night dark](https://git.kayg.org/attachments/5552aaba-382a-4caf-bd62-eaeaa2b06f4b) - [morph phone push paper light](https://git.kayg.org/attachments/5cd4e6de-c541-4a41-aa7d-a0989a628095) - [morph phone push tokyo night dark](https://git.kayg.org/attachments/e2621875-844a-4c1a-bc79-0a096a3355f3) - [split phone push paper light](https://git.kayg.org/attachments/86ba48e6-77fc-4d44-90e5-108ed113b62e) - [split phone push tokyo night dark](https://git.kayg.org/attachments/86075acd-1fe8-42e2-85c9-79dbaf8de68c) ### 820 px - [columns tablet morph paper light](https://git.kayg.org/attachments/c835369e-27d4-4f7d-83b2-da582c47b4c0) - [columns tablet morph tokyo night dark](https://git.kayg.org/attachments/25a408a0-67a9-4784-a5c9-0686ad9efd76) - [morph tablet morph paper light](https://git.kayg.org/attachments/cd74f625-89d8-4e6a-9c8f-d386d1fca1ae) - [morph tablet morph tokyo night dark](https://git.kayg.org/attachments/da66e1ec-b9c8-4c1c-bba1-304d73681143) - [split tablet morph paper light](https://git.kayg.org/attachments/7f7aefb5-6a25-46e6-bb00-0d2e2a5c85f5) - [split tablet morph tokyo night dark](https://git.kayg.org/attachments/6cdd269e-5c8b-455f-bee7-f23e646a6abf) ### 1440 px - [columns desktop columns paper light](https://git.kayg.org/attachments/c52d865f-b1c5-478b-842f-f95c50e530bf) - [columns desktop columns tokyo night dark](https://git.kayg.org/attachments/3935192b-99de-4fcd-af00-c466832cd8dd) - [morph desktop morph paper light](https://git.kayg.org/attachments/c57d5fe9-4062-4bac-b244-3fd266bb6dbe) - [morph desktop morph tokyo night dark](https://git.kayg.org/attachments/e502031c-b200-4baf-bc52-a11b58ebc771) - [split desktop split paper light](https://git.kayg.org/attachments/f2767bc3-2e3e-448a-8fb4-9f6118337f65) - [split desktop split tokyo night dark](https://git.kayg.org/attachments/6268dfb0-b67a-4f16-9fcd-f632527790f6) ## Decisions - Keep the selected reader content live during Arrow-key repeat, then replace the stable thread URL once on key release. This was recorded in DESIGN §45 after the WebKit trace showed per-key shallow route updates lagging behind selection. - Use the existing frame observer for live content sizing and an initial synchronous fit only after a new sandbox document loads. - Keep Morph in the Tab Bar content column, cap it at 62rem by 76dvh, and retain a 12px gap from the sidebar and viewport edge. ## Known gaps - The local blaze host was loaded (one-minute load average ranged from 13.6 to 33.1 across profiles), so paint p95 values are not a clean performance baseline. The comparisons are recorded on #641. - The runner sends 400 Down and 400 Up inputs while 250 rows are loaded; `unpainted` counts include inputs past the currently painted selection. WebKit does not expose Long Tasks to this runner. - The visual reviewer should review the attached captures. No screenshot files are committed.
Author
Owner

Starting round 3 on branch job/maillayouts, based on origin/dev at 687ff703136e71e89f8dfba139e93cd0788b25c1. Current head: fce14c9a41bc06bf6771b6092db8994b7069c00f. I am fixing Morph title stacking above its ProgressiveBlur and restoring the Mail reader block gap below the remote-images notice, then I will capture the production screenshots and run the requested gates.

Starting round 3 on branch `job/maillayouts`, based on `origin/dev` at `687ff703136e71e89f8dfba139e93cd0788b25c1`. Current head: `fce14c9a41bc06bf6771b6092db8994b7069c00f`. I am fixing Morph title stacking above its ProgressiveBlur and restoring the Mail reader block gap below the remote-images notice, then I will capture the production screenshots and run the requested gates.
Author
Owner

Finding: the Morph heading has no positioned stacking layer, so the absolute ProgressiveBlur paints over its subject and sender content; the navigation controls already use position: relative; z-index: 1 and stay crisp. Columns and Split render the reader without the grid gap that Morph applies, so the remote-image Card and the next body frame touch. The shared UI tokens do not define a general content-block gap. I will raise the Morph heading above the blur and add a shared --content-block-gap token at the existing 1rem reader spacing, then use it in the body stack across layouts.

Finding: the Morph heading has no positioned stacking layer, so the absolute ProgressiveBlur paints over its subject and sender content; the navigation controls already use `position: relative; z-index: 1` and stay crisp. Columns and Split render the reader without the grid gap that Morph applies, so the remote-image Card and the next body frame touch. The shared UI tokens do not define a general content-block gap. I will raise the Morph heading above the blur and add a shared `--content-block-gap` token at the existing 1rem reader spacing, then use it in the body stack across layouts.
Author
Owner

Round 3 complete

Built

  • The Morph subject and sender now sit above the ProgressiveBlur layer. Message content still scrolls beneath the floating title.
  • The reader uses a shared --content-block-gap of 1rem between the remote-images notice and message body in every layout.
  • Captured and attached the full 18-image production set at 390, 820 and 1440 px in Paper light and Tokyo Night dark.

Files

  • apps/web/src/lib/mail/MailMorphCard.svelte
  • apps/web/src/lib/mail/MailReaderContent.svelte
  • packages/ui/src/tokens.css

Screenshots

390 px

820 px

1440 px

Gates

cargo fmt --check: exit code 0; stdout was empty.

bun run check:

$ node scripts/check-user-storage.mjs && node scripts/check-type-tokens.mjs && node scripts/check-motion-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
User browser caches use userStorage; only documented device/public-link exceptions remain.
Text sizes and UI shape values use shared role tokens.
UI transitions and animation options use shared motion tokens or documented exceptions.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/maillayouts/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

bun run test result lines:

 Test Files  150 passed (150)
      Tests  1015 passed (1015)
   Start at  02:35:48
   Duration  102.26s (transform 49%, environment 21%, import 15%, tests 10%, setup 4%)

bun run build:

✓ built in 38.65s
Run npm run preview to preview your production build locally.
> Using @sveltejs/adapter-static
  Wrote site to "build"
  ✔ done

Production Mail e2e:

Mail layouts e2e passed; screenshots are in /home/kayg/Developer/calternal-wt/maillayouts/artifacts/mail-layouts

No Rust crate or API route changed, so per-crate clippy/test and the API adversarial round did not apply.

Decisions

  • DESIGN §§34 and 45 set the blur and reader behavior but do not define a general content-block gap role. I promoted the existing 1rem reader rhythm to --content-block-gap and use it for the notice/body stack in all layouts.
  • The Morph title layer uses z-index: 1, matching the controls, so the sticky blur can sample scrolling content without covering the subject or sender.

Known gaps

  • The inherited 50 ms warm-message target remains unverified; the earlier perf VM run could not reach the host. This round changed only CSS paint order and spacing.
  • Vitest printed existing jsdom CSS/scrollTo() warnings; all 1,015 tests passed.

Head: f9f360e68f4e9ca106ddd7fea24d1f4363881a2b.

## Round 3 complete ### Built - The Morph subject and sender now sit above the ProgressiveBlur layer. Message content still scrolls beneath the floating title. - The reader uses a shared `--content-block-gap` of `1rem` between the remote-images notice and message body in every layout. - Captured and attached the full 18-image production set at 390, 820 and 1440 px in Paper light and Tokyo Night dark. ### Files - `apps/web/src/lib/mail/MailMorphCard.svelte` - `apps/web/src/lib/mail/MailReaderContent.svelte` - `packages/ui/src/tokens.css` ### Screenshots **390 px** - Columns: [Paper light](https://git.kayg.org/attachments/a87e8f7a-5ef9-44ab-8f63-dd00a6725577) · [Tokyo Night dark](https://git.kayg.org/attachments/f0969be1-95e1-4f13-9158-342953c35f2a) - Split: [Paper light](https://git.kayg.org/attachments/029a651b-7c24-456a-9453-57cd0398c20b) · [Tokyo Night dark](https://git.kayg.org/attachments/aa5f27c4-2ea1-4ae7-a269-def853eb6765) - Morph: [Paper light](https://git.kayg.org/attachments/e28181d2-bfc1-4683-a5b2-1e4ce6c2b3ed) · [Tokyo Night dark](https://git.kayg.org/attachments/a978ad55-71b2-4d89-acd9-162d0924fc9f) **820 px** - Columns: [Paper light](https://git.kayg.org/attachments/44382f68-d9d3-4326-b379-a199e9d80c93) · [Tokyo Night dark](https://git.kayg.org/attachments/81cc05d7-3e5e-45ae-b43e-34343c68f883) - Split: [Paper light](https://git.kayg.org/attachments/a2574b44-686f-447d-9b4a-880b266534c3) · [Tokyo Night dark](https://git.kayg.org/attachments/c89c6224-9a28-4d75-87ce-bde79764c477) - Morph: [Paper light](https://git.kayg.org/attachments/c187a3d1-649f-4172-a710-ffa77b830b7e) · [Tokyo Night dark](https://git.kayg.org/attachments/2065a614-c064-450f-9d91-bfaeebbce4e5) **1440 px** - Columns: [Paper light](https://git.kayg.org/attachments/d4a77cb5-9508-4d37-96d5-67e080b23bfe) · [Tokyo Night dark](https://git.kayg.org/attachments/a2173265-372a-4c14-9610-5fb9e2d2a366) - Split: [Paper light](https://git.kayg.org/attachments/0b510a62-417a-4e0e-8b2e-b665264a6fd4) · [Tokyo Night dark](https://git.kayg.org/attachments/fcd499a3-b66d-4027-808b-8ce937133db8) - Morph: [Paper light](https://git.kayg.org/attachments/4ddf3519-06e4-457b-b232-c1b166158d7e) · [Tokyo Night dark](https://git.kayg.org/attachments/0ed38f61-5172-410c-852b-7f92594933bb) ### Gates `cargo fmt --check`: exit code 0; stdout was empty. `bun run check`: ```text $ node scripts/check-user-storage.mjs && node scripts/check-type-tokens.mjs && node scripts/check-motion-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json User browser caches use userStorage; only documented device/public-link exceptions remain. Text sizes and UI shape values use shared role tokens. UI transitions and animation options use shared motion tokens or documented exceptions. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/maillayouts/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` `bun run test` result lines: ```text Test Files 150 passed (150) Tests 1015 passed (1015) Start at 02:35:48 Duration 102.26s (transform 49%, environment 21%, import 15%, tests 10%, setup 4%) ``` `bun run build`: ```text ✓ built in 38.65s Run npm run preview to preview your production build locally. > Using @sveltejs/adapter-static Wrote site to "build" ✔ done ``` Production Mail e2e: ```text Mail layouts e2e passed; screenshots are in /home/kayg/Developer/calternal-wt/maillayouts/artifacts/mail-layouts ``` No Rust crate or API route changed, so per-crate clippy/test and the API adversarial round did not apply. ### Decisions - DESIGN §§34 and 45 set the blur and reader behavior but do not define a general content-block gap role. I promoted the existing 1rem reader rhythm to `--content-block-gap` and use it for the notice/body stack in all layouts. - The Morph title layer uses `z-index: 1`, matching the controls, so the sticky blur can sample scrolling content without covering the subject or sender. ### Known gaps - The inherited 50 ms warm-message target remains unverified; the earlier perf VM run could not reach the host. This round changed only CSS paint order and spacing. - Vitest printed existing jsdom CSS/`scrollTo()` warnings; all 1,015 tests passed. Head: `f9f360e68f4e9ca106ddd7fea24d1f4363881a2b`.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
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#640
No description provided.