Fix preview rendering and inline text editing in email builder#5
Open
yonardr wants to merge 6 commits into
Open
Fix preview rendering and inline text editing in email builder#5yonardr wants to merge 6 commits into
yonardr wants to merge 6 commits into
Conversation
690c1d2 to
180a813
Compare
180a813 to
21a3d15
Compare
added 2 commits
June 9, 2026 11:40
The shared wrapper's `@media (prefers-color-scheme: dark)` block made preview text light-grey on the forced-white block background under a dark OS theme. Strip that block from preview output only (JS previews before `srcdoc`; server-rendered template preview via `stripDarkModeCSS`). Sent emails are unaffected.
Inline editing was blocked for elements with a non-text child, so text
sharing a `<p>` with an icon or link wasn't editable.
- `replaceTextVars` now rewrites placeholders only in text content, so
`href="{{.Var}}"` is no longer corrupted (neutralized to `#` via
`replaceHrefVars`).
- `<img>`/`<a>` no longer block editing: surrounding text is wrapped in
editable spans, link text is edited separately. Listener wiring moved
to `bindEditable`.
21a3d15 to
31eb13c
Compare
There was a problem hiding this comment.
Pull request overview
This PR targets two UX issues in the Sendry email builder preview/editor: dark-mode CSS leaking into iframe previews (making forced-light previews unreadable) and inline text editing being blocked when text is adjacent to non-text children like icons/links.
Changes:
- Strip
@media (prefers-color-scheme: dark)CSS from preview output (client-side preview iframes and server-rendered template preview). - Update builder placeholder rewriting to avoid modifying variables inside HTML tags/attributes, and neutralize
href="{{.Var}}"placeholders. - Adjust inline-edit enabling to allow editing text nodes in mixed-content elements by wrapping text fragments in editable spans and refactoring edit binding logic.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| internal/web/views/template_view.html | Strips dark-mode media block from rendered template preview before setting iframe.srcdoc. |
| internal/web/views/block_view.html | Strips dark-mode media block from block preview iframe HTML. |
| internal/web/views/block_form.html | Strips dark-mode media block from block form live preview iframe HTML. |
| internal/web/static/js/builder.js | Updates variable placeholder rewriting and expands inline-edit support for mixed-content nodes (icon/link + text). |
| internal/web/handlers/templates.go | Adds server-side stripDarkModeCSS for template preview output. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
indexOf(original) hit the first match, so editing a repeated short fragment rewrote the wrong one. Record each fragment's source offset at wrap time and apply edits by offset; fall back to indexOf if it drifts.
919633a to
5e1958c
Compare
Comment on lines
+552
to
559
| edits.forEach(function(e) { | ||
| var pos = e.idx + offset; | ||
| newSourceHTML = newSourceHTML.slice(0, pos) + e.current + newSourceHTML.slice(pos + e.original.length); | ||
| offset += e.current.length - e.original.length; | ||
| e.node.dataset.originalText = e.current; | ||
| if (e.node.dataset.sourceIndex !== undefined) e.node.dataset.sourceIndex = String(e.idx); | ||
| changes++; | ||
| }); |
Comment on lines
+485
to
+503
| if (mixed) { | ||
| var textNodes = []; | ||
| node.childNodes.forEach(function(c) { | ||
| if (c.nodeType === 3 && c.nodeValue && c.nodeValue.trim()) textNodes.push(c); | ||
| }); | ||
| textNodes.forEach(function(tn) { | ||
| var raw = tn.nodeValue; | ||
| var trimmed = raw.trim(); | ||
| if (!trimmed) return; | ||
| var at = locate(trimmed); | ||
| if (at === -1) return; | ||
| var span = document.createElement('span'); | ||
| span.dataset.inlineText = '1'; | ||
| span.textContent = raw; | ||
| tn.parentNode.replaceChild(span, tn); | ||
| bindEditable(span, trimmed, item, previewEl, at); | ||
| }); | ||
| return; | ||
| } |
Comment on lines
+378
to
+380
| function replaceHrefVars(html) { | ||
| return html.replace(/(href=")(\{\{\.\w+\}\})(")/gi, '$1#$3'); | ||
| } |
| if (!r.ok) return r.text().then(function(t) { throw new Error(t); }); | ||
| return r.text(); | ||
| }).then(function(rendered) { | ||
| rendered = rendered.replace(/@media\s*\(prefers-color-scheme:\s*dark\)\s*\{[\s\S]*?\}\s*\}/g, ''); |
| if (!r.ok) return r.text().then(function(t) { throw new Error(t); }); | ||
| return r.text(); | ||
| }).then(function(html) { | ||
| html = html.replace(/@media\s*\(prefers-color-scheme:\s*dark\)\s*\{[\s\S]*?\}\s*\}/g, ''); |
| if (!r.ok) return r.text().then(function(t) { throw new Error(t); }); | ||
| return r.text(); | ||
| }).then(function(html) { | ||
| html = html.replace(/@media\s*\(prefers-color-scheme:\s*dark\)\s*\{[\s\S]*?\}\s*\}/g, ''); |
Comment on lines
+488
to
+492
| var darkModeBlockRe = regexp.MustCompile(`(?s)@media\s*\(prefers-color-scheme:\s*dark\)\s*\{.*?\}\s*\}`) | ||
|
|
||
| func stripDarkModeCSS(html string) string { | ||
| return darkModeBlockRe.ReplaceAllString(html, "") | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two independent fixes in the email builder (sendry-web). Atomic commits.
1. Preview showed grey text in dark mode
The shared email wrapper carries a
@media (prefers-color-scheme: dark)block. Previews render it in an iframe, so under a dark OS theme the text became light-grey on the forced-white block background — unreadable.Fix: strip the dark-mode block from preview output only (JS previews before
srcdoc; server-rendered template preview viastripDarkModeCSS). Sent emails keep dark mode.2. Text next to an icon/link was not editable
Inline editing was disabled for any element with a non-text child, so status text sharing a
<p>with an icon (Оплачен <img>) or a link wasn't editable.Fix:
replaceTextVarsnow rewrites placeholders only in text content (no longer breakshref="{{.Var}}", which is neutralized to#viareplaceHrefVars);<img>/<a>no longer block editing — surrounding text is wrapped in editable spans, link text is edited separately.