Skip to content

fix(decrypt,addons): dark-mode email body + mobile decrypt overhaul#212

Merged
rubenhensen merged 8 commits into
mainfrom
fix/website-misc-fixes
May 12, 2026
Merged

fix(decrypt,addons): dark-mode email body + mobile decrypt overhaul#212
rubenhensen merged 8 commits into
mainfrom
fix/website-misc-fixes

Conversation

@rubenhensen

@rubenhensen rubenhensen commented May 12, 2026

Copy link
Copy Markdown
Contributor

Summary

A bundle of small site fixes on /decrypt and /addons.

  • Email body text is black text on dark background in darkmode. #159 — Dark-mode email body legibility. Plain-text emails now follow the host theme (background, text colour, color-scheme) and react live to the theme switcher via a MutationObserver on <html>'s class. HTML emails are rendered untouched so their own design isn't overridden — they carry their own self-contained styling.
  • Mobile /decrypt overhaul. The desktop-only "Install the Outlook/Thunderbird extension" banner and the "drop the postguard.encrypted attachment here" drag affordance both made no sense on phones. The mobile flow now mirrors a real inbox:
    • Inbox view (full screen): search bar + cog at the top, email list filling the middle, "Upload "postguard.encrypted"" primary button pinned at the bottom (via flex order, so the desktop layout is unchanged).
    • Reader view (full screen): tapping an email — or the upload button — swaps the panel to the reader with a "Back to inbox" button above the content. Back resets currSelected and hashMode so the existing effect collapses currRight to Nothing and restores the list.
  • Mobile flex heights. Anchored .fallback-page to calc(100vh - 52px) and added min-height: 0 / explicit flex: 1 1 0 along the panel/list chain so the email list can shrink instead of growing to its content and pushing the upload button below the fold.
  • Visual tells cleanup. Replaced two left-border accent strips that gave a templated/AI look:
    • Selected email list item is now a full primary-coloured row with white text (sender/date softened to rgba(255,255,255,0.85)), instead of the 3px primary stripe.
    • The Outlook callout on /addons is now a fully bordered amber card with deeper amber body text instead of the 4px left stripe.

Changes

  • src/lib/components/fallback/EmailView.svelte — theme-aware iframe rendering, dark-class observer, HTML escaping for plain-text bodies.
  • src/routes/(app)/decrypt/+page.svelte — mobile layout, single-screen list↔reader flow, "Back to inbox" button, anchored heights, mobile upload button styling.
  • src/lib/components/fallback/ListView.svelte — selected-item full-background style.
  • src/routes/(marketing)/addons/+page.svelte — Outlook callout box restyle.
  • src/lib/locales/{en,nl}.jsonfallback.upload and fallback.back strings.

Test plan

  • /decrypt in dark mode with a plain-text email: text legible, recolours live when the theme switcher toggles.
  • /decrypt in dark mode with an HTML email whose body hardcodes black on white: email keeps its own design untouched.
  • Mobile (<768px) /decrypt: extension banner gone; upload button pinned at the bottom; tapping an email opens the reader full-screen with a "Back to inbox" button; tapping back returns to the list.
  • Selected email in the inbox shows as a full primary-coloured row with white text (no left-border stripe).
  • /addons → Outlook tab: warning callout uses a full amber border, no left-border stripe.

Closes #159

- EmailView: render decrypted email in a forced light-themed iframe so
  emails that hardcode dark text on no background stay legible when the
  host page is in dark mode (#159).
- Marketing page: store the returning-visitor flag in a cookie scoped to
  the registrable domain so it carries across postguard.eu subdomains
  instead of being trapped per-origin in localStorage (#208).
Previously the iframe forced a light surface unconditionally. That fixed
the original dark-mode bug for HTML emails (which set their own dark
text on no background), but it also overrode the site theme for plain-
text emails — leaving them stuck in light mode forever.

- HTML emails: don't touch the styling. Email designs are self-contained.
- Plain-text emails: wrap the text in <pre> and inject body styles that
  follow the host theme (background, color, color-scheme), reactively
  switching when the user toggles the theme via a MutationObserver on
  the <html> class.
…nner

On mobile the "install Thunderbird/Outlook extension" banner is
irrelevant, and the dashed "drop the postguard.encrypted file here" zone
implies a drag-and-drop interaction that isn't available on touch.

- Hide the extension banner under 768px.
- Restyle the upload label as a solid primary button on mobile and swap
  its text to "Upload \"postguard.encrypted\"" so it reads as a tappable
  action rather than a drop target. The underlying <input type="file">
  already opens the native picker on tap.
- Add the new fallback.upload i18n key in EN and NL.
On a phone the side-by-side panels squeezed both lists and the email
view into ~40vh slices. Switch to a phone-style two-step flow instead.

- Inbox view: full-screen panel with search/cog at the top, email list
  filling the middle, and the upload button pinned to the bottom (via
  flex order — desktop layout is unchanged).
- Reader view: clicking an email or hitting Decrypt swaps to a
  full-screen right panel with a "Back to inbox" button up top.
- "Back" resets currSelected and hashMode so the effect collapses
  currRight back to Nothing, restoring the list view.
- Add fallback.back i18n key in EN and NL.
…ents

- decrypt mobile: anchor .fallback-page to the viewport height and add
  min-height:0 + explicit flex bases to the panel/list chain so the
  email list can shrink instead of growing to fit content and pushing
  the upload button below the fold.
- ListView: replace the 3px left-border selected accent with a solid
  primary-coloured background and white text — clearer affordance and
  drops the "AI-template" stripe look.
- Addons callout: drop the left-border stripe in favour of a full
  bordered, slightly muted amber card.
@rubenhensen rubenhensen changed the title fix: dark-mode email body + cross-subdomain pg_visited fix: dark-mode email body, cross-subdomain pg_visited, mobile /decrypt overhaul May 12, 2026
@rubenhensen

Copy link
Copy Markdown
Contributor Author

/dobby review

@dobby-coder

dobby-coder Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Wiggles ears excitedly! This agent will review the PR right away — checking out the code, reading the diff, and reporting back with findings!

@dobby-coder dobby-coder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Reviewed branch fix/website-misc-fixes locally. Verified:

  • npm ci --legacy-peer-deps && npm run build && npm run lint && npm run check — all green.
  • CI on the PR is green too.

The PR does what the body claims and the diffs are tight. A few notes, ordered by significance.

Nice work

  • XSS hardening of plain-text emails. Previously the iframe got ${parsed.html ?? parsed.text ?? ''}parsed.text was interpolated as HTML, so < characters in plain text rendered as broken tags. The new escapeHtml(parsed.text) fixes that. The sandbox="" attribute was already neutralising script execution, but this is the correct change either way.
  • Theme observer. MutationObserver on <html>.class is the right hook — matches how the theme toggle works elsewhere — and the cleanup on unmount is wired up correctly.
  • Mobile flex chain. The combination of height: calc(100vh - 52px) on .fallback-page plus min-height: 0 + flex: 1 1 0 down the panel/list chain is the textbook fix for "flex child grows past its container". Looks right.
  • backToList() ordering. Resetting currSelected and hashMode lets the existing reactive run collapse currRight to Nothing on its own; the explicit assignment is belt-and-braces and harmless.
  • i18n parity. Both en.json and nl.json get fallback.upload and fallback.back. No drift.

Worth a look

  1. registrableDomain() doesn't know about the Public Suffix List. For postguard.eu / postguard.nl the naive "last two parts" works. But on any multi-part public suffix (*.co.uk, *.github.io, *.vercel.app, *.pages.dev) the result equals the public suffix itself, and browsers will reject Domain=.co.uk outright — so no cookie gets set and the returning-visitor flag silently breaks on preview deploys. Probably fine for production today, but worth either a code comment acknowledging it or (longer term) a PSL-aware helper. Not blocking.

  2. hasVisited() prefix match is slightly loose. startsWith('pg_visited=') would also match a hypothetical pg_visited_v2=. Nothing in the repo today shares that prefix, so it's purely a hygiene point — splitting on = and comparing the name exactly would be tidier.

  3. --pg-font-weight-semibold (pre-existing, but you're adding a new caller). That custom property isn't defined in src/lib/global.scss, so font-weight: var(--pg-font-weight-semibold) falls back to normal (400). The new .mobile-back button at src/routes/(app)/decrypt/+page.svelte:531 is now the fifth caller of this undefined token. Not introduced by this PR, but adding --pg-font-weight-semibold: 600; to global.scss is a one-line, repo-wide win and fits the spirit of "visual tells cleanup".

  4. Plain-text email font. The new bodyStyle sets font-family: system-ui, -apple-system, sans-serif and the <pre> inherits it. Plain-text email bodies (ASCII tables, code blocks, formatted signatures) typically rely on monospace alignment. Most real mail clients render text/plain in a monospace face. Worth considering font-family: ui-monospace, SFMono-Regular, Menlo, monospace on the <pre> — purely a design call, not a correctness issue.

  5. HTML email srcdoc. ${parsed.html} is still concatenated directly into the wrapper. Given sandbox="", this is fine — scripts can't run and malformed markup is contained to the iframe. Just flagging it stays an implicit trust point: anything that loosens the sandbox later would re-open this.

Test plan coverage

The manual checklist in the PR body covers the right things. The one scenario I'd add: tap an email → "Back to inbox" → tap a different email — make sure the second email actually loads (i.e. currSelected = -1 between selections doesn't introduce a stale parsed flash). Looking at the reactive run, it should be fine, but worth eyeballing once.

Verdict

LGTM with the small notes above. None of them are blockers; (1) and (3) are the most worth picking up, and both are tiny.

Cookies cannot be shared across different registrable domains (e.g.
postguard.eu ↔ postguard.nl), so the cookie-scoped flag only solved
the subdomain case — which already worked well enough via the
existing localStorage approach for our deployed surfaces. Cross-TLD
sharing would require either consolidating to a single canonical
domain or a third-party tracker iframe, neither of which is in scope.
Revert to localStorage and close #208.
@rubenhensen rubenhensen changed the title fix: dark-mode email body, cross-subdomain pg_visited, mobile /decrypt overhaul fix(decrypt,addons): dark-mode email body + mobile decrypt overhaul May 12, 2026
@rubenhensen

Copy link
Copy Markdown
Contributor Author

Reverted the pg_visited cookie migration in dea6567 — cookies don't cross TLDs, so the change only solved the subdomain case and #208 has been closed instead. PR now covers just the #159 dark-mode fix and the mobile /decrypt overhaul. /dobby review

@dobby-coder

dobby-coder Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

the bot is on it! Re-reviewing PR #212 now that the cookie migration has been reverted — focusing on just the dark-mode fix (#159) and the mobile /decrypt overhaul.

@dobby-coder dobby-coder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed after dea6567. The cookie migration is fully gone — src/routes/(marketing)/+page.{svelte,js} are back on localStorage.getItem('pg_visited') / setItem('pg_visited', 'true'), no registrableDomain / hasVisited helpers remain, and the marketing routes are no longer touched by this PR at all.

That moots points (1) and (2) from the prior review (PSL handling, prefix match on pg_visited=). The remaining diff is exactly what the description says:

  • EmailView.svelteescapeHtml(parsed.text) + theme-aware <pre> wrapper + MutationObserver on <html> class, HTML emails left untouched.
  • decrypt/+page.svelte — mobile inbox/reader two-step flow, backToList() resets currSelected / hashMode, anchored calc(100vh - 52px) + min-height: 0 + flex: 1 1 0 chain.
  • ListView.svelte — selected row is now a full primary-coloured background.
  • addons/+page.svelte — Outlook callout converted to a fully bordered amber card.
  • en.json / nl.jsonfallback.upload + fallback.back added in parity.

CI green on dea6567 (Svelte Check, Lint, Build amd64/arm64, Conventional Commit). Prior local run of npm ci --legacy-peer-deps && npm run build && npm run lint && npm run check was green on the pre-revert tip and the revert only deletes code; nothing to re-verify.

The smaller non-blocking notes from before still apply if anyone wants to pick them up later:

  • --pg-font-weight-semibold is undefined in global.scss (new caller in .mobile-back); one-line fix.
  • <pre> for plain-text emails uses system-ui; monospace would match other mail clients but is a design call.
  • HTML srcdoc keeps an implicit trust point that the iframe stays sandbox="".

LGTM, approving.

Two notes from the dobby review.

- Define --pg-font-weight-semibold in global.scss. It was referenced in
  five places (Header, marketing root, marketing layout, blog post page,
  and the new .mobile-back button) but never declared, silently falling
  back to 400. Mapping it to 600 matches the existing --medium token.
- Switch the plain-text email <pre> to a monospace face. text/plain
  bodies routinely rely on column alignment (ASCII tables, quoted
  replies with leading '>', signatures), which broke under system-ui.
  Drop the font-family from <body> so it doesn't shadow the <pre>.
…edium

Both pointed at 600, so there were two tokens for one value. Drop the
just-added --pg-font-weight-semibold and migrate the existing five
callers (Header, marketing root + layout, blog post page, decrypt
.mobile-back) to --pg-font-weight-medium.
@rubenhensen rubenhensen merged commit e970f91 into main May 12, 2026
6 checks passed
@rubenhensen rubenhensen deleted the fix/website-misc-fixes branch May 12, 2026 10:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Email body text is black text on dark background in darkmode.

1 participant