Forced theme: light panels nested inside dark painted cards survive theming #310

Open
opened 2026-09-09 02:26:36 +00:00 by jcoffey-dev · 0 comments
Owner

Follow-up to #290 / #294, from a real-mail test by @taisau:
https://github.com/Coffey-Labs/ihasmail/issues/290#issuecomment-5593496568

#294 works for the case it was built around — a white wrapper around a
coloured CTA neutralises correctly, and that is pinned by the
keeps a coloured button and drops the white sheet around it test. This is a
different shape of failure and it is still open.

What happens

A dark-design campaign renders with beige cards inside it, which reads to the
user as "the theme did not apply". In the reported specimen 14 of 21 light
panels survive.

The specimen has no bgcolor attributes and layout-only <style> blocks; all
colour is inline:

  • <body style="background-color: #e7e5e2"> — lum ≈ 0.79, neutralised correctly
  • 600px content cards at background-color: #2b2b2b — lum ≈ 0.02, marked data-ihm-keep
  • content tables at background-color: #e7e5e2 nested inside those dark cards

Why

It follows directly from the current code rather than being an edge case.

markKeptSurfaces() in web/src/lib/html.ts marks any element whose declared
background falls below LIGHT_SURFACE_LUMINANCE (0.5). There is no area cap and
no second look at what is inside, so a full-width layout card is marked exactly
like a button.

EMAIL_BASE_CSS then exempts the marked element and its whole subtree:

.ihm-email-root.forced *:not([data-ihm-keep]):not([data-ihm-keep] *) {
  color: inherit !important;
  background-color: transparent !important;
}

The [data-ihm-keep] * half is right for its intended job — a button label must
stay legible on its paint — but it cannot tell "text inside a small painted
surface" from "a whole light sheet sitting inside a large painted one".

Approach

Apply the same luminance test to descendants of kept surfaces: walk kept
subtrees and stop exempting any descendant that itself declares a background at
or above the threshold. A button label declares no background of its own so it
stays exempt; a nested light sheet declares one, so it is caught. When such a
descendant is neutralised its own subtree should stop being exempt too, or the
boundary just moves down a level.

The two alternatives @taisau raised were considered and are not being taken:

  • Dropping the [data-ihm-keep] * half reintroduces exactly what #294
    fixed. The common pattern is <td bgcolor="#1155CC" style="color:#fff"> with a
    nested <a>; neutralising the descendant puts the unreadable white-on-white
    label back.
  • An area threshold is a magic number that misfires both ways — a legitimate
    full-width hero banner is large, and a small dark panel with a light chip in it
    is small.

Done when

  • A light panel nested in a dark painted card is neutralised
  • A coloured button keeps its label colour (existing test stays green)
  • Both shapes covered in web/src/lib/__tests__/html.test.ts
  • Confirmed against the reporter's specimen, with the white-wrapper template as
    the regression

Rebuilt from: GH Archive, session transcript.

Follow-up to #290 / #294, from a real-mail test by @taisau: https://github.com/Coffey-Labs/ihasmail/issues/290#issuecomment-5593496568 #294 works for the case it was built around — a white wrapper around a coloured CTA neutralises correctly, and that is pinned by the `keeps a coloured button and drops the white sheet around it` test. This is a different shape of failure and it is still open. ## What happens A dark-design campaign renders with beige cards inside it, which reads to the user as "the theme did not apply". In the reported specimen 14 of 21 light panels survive. The specimen has no `bgcolor` attributes and layout-only `<style>` blocks; all colour is inline: - `<body style="background-color: #e7e5e2">` — lum ≈ 0.79, neutralised correctly - 600px content cards at `background-color: #2b2b2b` — lum ≈ 0.02, marked `data-ihm-keep` - content tables at `background-color: #e7e5e2` **nested inside** those dark cards ## Why It follows directly from the current code rather than being an edge case. `markKeptSurfaces()` in `web/src/lib/html.ts` marks *any* element whose declared background falls below `LIGHT_SURFACE_LUMINANCE` (0.5). There is no area cap and no second look at what is inside, so a full-width layout card is marked exactly like a button. `EMAIL_BASE_CSS` then exempts the marked element **and its whole subtree**: ```css .ihm-email-root.forced *:not([data-ihm-keep]):not([data-ihm-keep] *) { color: inherit !important; background-color: transparent !important; } ``` The `[data-ihm-keep] *` half is right for its intended job — a button label must stay legible on its paint — but it cannot tell "text inside a small painted surface" from "a whole light sheet sitting inside a large painted one". ## Approach Apply the same luminance test to descendants of kept surfaces: walk kept subtrees and stop exempting any descendant that itself declares a background at or above the threshold. A button label declares no background of its own so it stays exempt; a nested light sheet declares one, so it is caught. When such a descendant is neutralised its own subtree should stop being exempt too, or the boundary just moves down a level. The two alternatives @taisau raised were considered and are not being taken: - **Dropping the `[data-ihm-keep] *` half** reintroduces exactly what #294 fixed. The common pattern is `<td bgcolor="#1155CC" style="color:#fff">` with a nested `<a>`; neutralising the descendant puts the unreadable white-on-white label back. - **An area threshold** is a magic number that misfires both ways — a legitimate full-width hero banner is large, and a small dark panel with a light chip in it is small. ## Done when - A light panel nested in a dark painted card is neutralised - A coloured button keeps its label colour (existing test stays green) - Both shapes covered in `web/src/lib/__tests__/html.test.ts` - Confirmed against the reporter's specimen, with the white-wrapper template as the regression <sub>Rebuilt from: GH Archive, session transcript.</sub>
This repo is archived. You cannot comment on issues.