Skip to content

Commit e970f91

Browse files
authored
fix(decrypt,addons): dark-mode email body + mobile decrypt overhaul (#212)
* fix: dark-mode email body legibility and cross-subdomain visited flag - 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). * fix(decrypt): theme-aware plain-text rendering, leave HTML emails alone 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. * fix(decrypt): mobile-friendly upload affordance, hide desktop-only banner 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. * fix(decrypt): single-screen list/reader flow on mobile 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. * fix(decrypt,addons): anchor mobile flex heights, drop left-border accents - 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. * revert: drop pg_visited cookie migration (#208) 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. * fix(decrypt): plain-text emails in monospace, define semibold token 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>. * refactor(styles): consolidate font-weight-semibold into font-weight-medium 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.
1 parent 1fcfa93 commit e970f91

10 files changed

Lines changed: 185 additions & 34 deletions

File tree

src/lib/components/Header.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@
116116
color: white;
117117
border-radius: var(--pg-border-radius-sm);
118118
text-decoration: none;
119-
font-weight: var(--pg-font-weight-semibold);
119+
font-weight: var(--pg-font-weight-medium);
120120
font-size: var(--pg-font-size-sm);
121121
transition: opacity 0.2s ease;
122122
white-space: nowrap;

src/lib/components/fallback/EmailView.svelte

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script>
22
import { run } from 'svelte/legacy'
3+
import { onMount } from 'svelte'
34
45
import * as email from './email'
56
import { emails, currSelected } from './../fallback/stores.js'
@@ -9,6 +10,17 @@
910
1011
let parsed = $state()
1112
let date = $state()
13+
let isDark = $state(false)
14+
15+
onMount(() => {
16+
const html = document.documentElement
17+
isDark = html.classList.contains('dark')
18+
const obs = new MutationObserver(() => {
19+
isDark = html.classList.contains('dark')
20+
})
21+
obs.observe(html, { attributes: true, attributeFilter: ['class'] })
22+
return () => obs.disconnect()
23+
})
1224
1325
run(() => {
1426
if ($currSelected >= 0) {
@@ -21,6 +33,33 @@
2133
}
2234
}
2335
})
36+
37+
function escapeHtml(s) {
38+
return s
39+
.replace(/&/g, '&amp;')
40+
.replace(/</g, '&lt;')
41+
.replace(/>/g, '&gt;')
42+
}
43+
44+
// HTML emails carry their own design — leave their styling untouched.
45+
// Plain-text emails have no styling of their own, so mirror the site
46+
// theme so the text stays legible when dark mode is toggled, and use
47+
// a monospace face — that's what real mail clients render text/plain
48+
// in, and ASCII tables / signatures / quoted replies rely on it.
49+
let bodyDoc = $derived.by(() => {
50+
if (!parsed) return ''
51+
if (parsed.html) {
52+
return `<!doctype html><html><body style="margin:1rem">${parsed.html}</body></html>`
53+
}
54+
const bg = isDark ? '#061b2d' : '#ffffff'
55+
const fg = isDark ? '#ffffff' : '#030e17'
56+
const scheme = isDark ? 'dark' : 'light'
57+
const text = escapeHtml(parsed.text ?? '')
58+
const bodyStyle = `margin:1rem;background:${bg};color:${fg};color-scheme:${scheme};font-size:13px;line-height:1.5`
59+
const preStyle =
60+
'margin:0;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace'
61+
return `<!doctype html><html><body style="${bodyStyle}"><pre style="${preStyle}">${text}</pre></body></html>`
62+
})
2463
</script>
2564
2665
{#if parsed}
@@ -69,11 +108,7 @@
69108
</div>
70109
71110
<div class="email-body">
72-
<iframe
73-
srcdoc={`<style>body{margin:1rem}</style>${parsed.html ?? parsed.text ?? ''}`}
74-
title="Mail message"
75-
sandbox=""
76-
></iframe>
111+
<iframe srcdoc={bodyDoc} title="Mail message" sandbox=""></iframe>
77112
</div>
78113
79114
{#if parsed.attachments && parsed.attachments.length > 0}

src/lib/components/fallback/ListView.svelte

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,23 @@
7575
gap: 0.15rem;
7676
padding: 0.75rem 1rem;
7777
border-bottom: 1px solid var(--pg-input-normal);
78-
border-left: 3px solid transparent;
7978
text-align: left;
80-
transition: background 0.15s ease;
79+
transition:
80+
background 0.15s ease,
81+
color 0.15s ease;
8182
8283
&:hover {
8384
background: var(--pg-soft-background);
8485
}
8586
8687
&.selected {
87-
background: var(--pg-general-background);
88-
border-left-color: var(--pg-primary);
88+
background: var(--pg-primary);
89+
color: white;
90+
91+
.email-sender,
92+
.email-date {
93+
color: rgba(255, 255, 255, 0.85);
94+
}
8995
}
9096
9197
&:focus-visible {

src/lib/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@
9898
"privacy": "Your emails are stored only in your browser. Clear them anytime in settings."
9999
},
100100
"drop": "Drop the \"postguard.encrypted\" attachment here",
101+
"upload": "Upload \"postguard.encrypted\"",
102+
"back": "Back to inbox",
101103
"search": "Search decrypted mails...",
102104
"decrypt": {
103105
"helper": "Decrypt this mail by showing who you are with <i class=\"yivi-web-logo\">Yivi</i>",

src/lib/locales/nl.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@
9898
"privacy": "Je e-mails worden alleen in je browser opgeslagen. Verwijder ze op elk moment via instellingen."
9999
},
100100
"drop": "Selecteer \"postguard.encrypted\" attachment hier",
101+
"upload": "Upload \"postguard.encrypted\"",
102+
"back": "Terug naar inbox",
101103
"search": "Zoek in ontsleutelde mails...",
102104
"decrypt": {
103105
"helper": "Ontsleutel deze email door te laten zien wie je bent met <i class=\"yivi-web-logo\">Yivi</i>",

src/routes/(app)/decrypt/+page.svelte

Lines changed: 116 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@
4747
currRight = RIGHTMODES.Decrypt
4848
}
4949
50+
function backToList() {
51+
// On mobile the list and reader are stacked single-screen flows;
52+
// resetting both selection and hashMode collapses currRight back to
53+
// Nothing via the effect above, which hides the reader panel.
54+
currSelected.set(-1)
55+
hashMode = false
56+
currRight = RIGHTMODES.Nothing
57+
}
58+
5059
function fromUrlSafeBase64(urlSafe) {
5160
let base64 = urlSafe.replace(/-/g, '+').replace(/_/g, '/')
5261
const pad = base64.length % 4
@@ -129,7 +138,10 @@
129138
<span>{$_('fallback.extensionPrompt')}</span>
130139
<a href={resolve('/addons/')}>{$_('fallback.extensionLink')}</a>
131140
</div>
132-
<div class="fallback-container">
141+
<div
142+
class="fallback-container"
143+
class:mobile-reading={currRight !== RIGHTMODES.Nothing}
144+
>
133145
<div class="left-panel">
134146
{#if !hashMode}
135147
<label class="upload-area">
@@ -138,11 +150,16 @@
138150
width="28px"
139151
aria-hidden="true"
140152
/>
141-
<span>{$_('fallback.drop')}</span>
153+
<span class="upload-text upload-text-desktop"
154+
>{$_('fallback.drop')}</span
155+
>
156+
<span class="upload-text upload-text-mobile"
157+
>{$_('fallback.upload')}</span
158+
>
142159
<input
143160
type="file"
144161
onchange={onFile}
145-
aria-label={$_('fallback.drop')}
162+
aria-label={$_('fallback.upload')}
146163
/>
147164
</label>
148165
{/if}
@@ -180,6 +197,12 @@
180197
</div>
181198
182199
<div class="right-panel">
200+
{#if currRight !== RIGHTMODES.Nothing}
201+
<button class="mobile-back" type="button" onclick={backToList}>
202+
<Icon icon="mdi:arrow-left" width="20px" />
203+
<span>{$_('fallback.back')}</span>
204+
</button>
205+
{/if}
183206
{#if currRight === RIGHTMODES.MailView}
184207
<EmailView />
185208
{:else if currRight === RIGHTMODES.Nothing}
@@ -242,7 +265,7 @@
242265
243266
a {
244267
color: var(--pg-primary);
245-
font-weight: var(--pg-font-weight-semibold);
268+
font-weight: var(--pg-font-weight-medium);
246269
text-decoration: none;
247270
248271
&:hover {
@@ -294,6 +317,14 @@
294317
}
295318
}
296319
320+
.upload-text-mobile {
321+
display: none;
322+
}
323+
324+
.mobile-back {
325+
display: none;
326+
}
327+
297328
.search-bar {
298329
display: flex;
299330
align-items: center;
@@ -377,7 +408,7 @@
377408
378409
h2 {
379410
font-size: var(--pg-font-size-lg);
380-
font-weight: var(--pg-font-weight-semibold);
411+
font-weight: var(--pg-font-weight-medium);
381412
margin: 0 0 0.75rem;
382413
}
383414
@@ -411,22 +442,96 @@
411442
412443
@media only screen and (max-width: 768px) {
413444
.fallback-page {
414-
height: auto;
415-
min-height: calc(100vh - 52px);
445+
height: calc(100vh - 52px);
446+
min-height: 0;
447+
padding: 0;
448+
}
449+
450+
.extension-banner {
451+
display: none;
452+
}
453+
454+
.upload-area {
455+
flex: 0 0 auto;
456+
flex-direction: row;
457+
padding: 0.75rem 1rem;
458+
margin: 0.75rem 1rem;
459+
border-style: solid;
460+
border-color: var(--pg-primary);
461+
background: var(--pg-primary);
462+
color: white;
463+
order: 3;
464+
465+
&:hover {
466+
color: white;
467+
opacity: 0.9;
468+
}
469+
}
470+
471+
.upload-text-desktop {
472+
display: none;
473+
}
474+
475+
.upload-text-mobile {
476+
display: inline;
477+
}
478+
479+
.search-bar {
480+
order: 1;
481+
padding-top: 0.75rem;
482+
flex: 0 0 auto;
483+
}
484+
485+
.email-list-area {
486+
order: 2;
487+
flex: 1 1 0;
488+
min-height: 0;
416489
}
417490
418491
.fallback-container {
419492
flex-direction: column;
420-
height: auto;
493+
height: 100%;
494+
gap: 0;
495+
flex: 1;
496+
min-height: 0;
421497
}
422498
423499
.left-panel {
424-
flex: none;
425-
max-height: 40vh;
500+
flex: 1 1 0;
501+
min-height: 0;
502+
max-height: none;
503+
border: none;
504+
border-radius: 0;
426505
}
427506
428507
.right-panel {
429-
min-height: 50vh;
508+
min-height: 0;
509+
border: none;
510+
border-radius: 0;
511+
flex: 1 1 0;
512+
}
513+
514+
// Single-screen flow: list view OR reader view, never both.
515+
.fallback-container.mobile-reading .left-panel {
516+
display: none;
517+
}
518+
519+
.fallback-container:not(.mobile-reading) .right-panel {
520+
display: none;
521+
}
522+
523+
.mobile-back {
524+
all: unset;
525+
cursor: pointer;
526+
display: flex;
527+
align-items: center;
528+
gap: 0.4rem;
529+
padding: 0.75rem 1rem;
530+
font-size: var(--pg-font-size-sm);
531+
font-weight: var(--pg-font-weight-medium);
532+
color: var(--pg-primary);
533+
border-bottom: 1px solid var(--pg-input-normal);
534+
flex-shrink: 0;
430535
}
431536
}
432537
</style>

src/routes/(marketing)/+layout.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@
120120
padding: 0.5rem 1rem;
121121
background: var(--pg-primary);
122122
color: var(--pg-on-primary);
123-
font-weight: var(--pg-font-weight-semibold);
123+
font-weight: var(--pg-font-weight-medium);
124124
text-decoration: none;
125125
z-index: 1000;
126126
transform: translateY(-200%);
@@ -156,7 +156,7 @@
156156
.footer-col {
157157
h4 {
158158
font-size: var(--pg-font-size-sm);
159-
font-weight: var(--pg-font-weight-semibold);
159+
font-weight: var(--pg-font-weight-medium);
160160
color: var(--pg-text);
161161
margin: 0 0 0.75rem;
162162
}
@@ -193,7 +193,7 @@
193193
a {
194194
color: var(--pg-text-secondary);
195195
text-decoration: none;
196-
font-weight: var(--pg-font-weight-semibold);
196+
font-weight: var(--pg-font-weight-medium);
197197
198198
&:hover {
199199
color: var(--pg-primary);

0 commit comments

Comments
 (0)