feat: contain the signature editing surface so a stored inline position:fixed covers its own box instead of a teammate's dashboard, since the visual editor mounts that markup with innerHTML and forcing the source view for every inline style would take the visual editor away from almost every real signature, and stop the editor's table rule overriding a template's own width attribute, which edited a 600px design at full container width and discarded its column widths

This commit is contained in:
Matthew Meszaros
2026-09-09 08:33:46 -07:00
parent 035865e5c5
commit cc50f1d026
2 changed files with 19 additions and 2 deletions
+5 -1
View File
@@ -9,6 +9,10 @@
// document, an event handler) forces the source view. The visual surface
// works by assigning innerHTML, and a signature is org data a teammate
// wrote, so an <img onerror> there would run in someone else's dashboard.
// Inline styles are allowed through, because almost every real signature
// has them, so the surface is paint-contained instead: that makes it the
// containing block for a positioned descendant, and a signature saying
// position:fixed can no longer lay itself over a teammate's dashboard.
// - the preview renders in the inbox's sandboxed frame, never in the
// dashboard DOM, for the same reason.
@@ -359,7 +363,7 @@ export default function EmailEditor({
commitHtml(adopted);
setCode(true);
}}
className="min-h-[120px] px-3 py-2.5 text-[13px] text-slate-800 outline-none prose prose-sm max-w-none"
className="email-signature-surface min-h-[120px] px-3 py-2.5 text-[13px] text-slate-800 outline-none prose prose-sm max-w-none"
/>
)}
</div>
+14 -1
View File
@@ -459,7 +459,12 @@ svg.loading circle {
leave the dashboard, and a mail client would ignore them anyway. A cell with
its own inline border or background wins, so a real design still previews as
itself while it is being edited. */
.tiptap-body table { border-collapse: collapse; margin: 0.4em 0; width: 100%; table-layout: fixed; }
.tiptap-body table { border-collapse: collapse; margin: 0.4em 0; max-width: 100%; }
/* Only a table that sizes itself nowhere else. A presentational width= loses to
any stylesheet rule, so forcing one here edited a 600px design at full
container width and table-layout:fixed threw away its column widths. An
inline style beats this rule on its own. */
.tiptap-body table:not([width]) { width: 100%; }
.tiptap-body td, .tiptap-body th { border: 1px dashed #e2e8f0; padding: 0.4em 0.55em; vertical-align: top; position: relative; }
.tiptap-body th { background: #f8fafc; font-weight: 600; text-align: left; }
.tiptap-body td > p:last-child, .tiptap-body th > p:last-child { margin-bottom: 0; }
@@ -468,6 +473,14 @@ svg.loading circle {
background: rgba(2, 132, 199, 0.08); /* sky-600 */
}
/* The signature is other people's markup mounted in the dashboard's own DOM,
because a contentEditable is how the visual editor works. Paint containment
makes this box the containing block for anything positioned inside it and
clips to it, so a signature carrying position:fixed covers itself rather
than a teammate's screen. Markup that could execute never reaches here at
all; EmailEditor forces the source view for that. */
.email-signature-surface { contain: layout paint; }
/* Merge-tag chips shown inside the composer editor. Only the node-view button
carries .tpl-var; the resolved `<span data-var>` the preview renders is left
unstyled on purpose so it reads as the plain value it will send as. */