From cc50f1d026f6525940d2e8615aae8e76a0dfa145 Mon Sep 17 00:00:00 2001 From: Matthew Meszaros Date: Wed, 9 Sep 2026 08:33:46 -0700 Subject: [PATCH] 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 --- web/src/components/app/EmailEditor.tsx | 6 +++++- web/src/global.css | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/web/src/components/app/EmailEditor.tsx b/web/src/components/app/EmailEditor.tsx index 7a4b8cee..1efd403f 100644 --- a/web/src/components/app/EmailEditor.tsx +++ b/web/src/components/app/EmailEditor.tsx @@ -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 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" /> )} diff --git a/web/src/global.css b/web/src/global.css index aab72ffc..844e0dce 100644 --- a/web/src/global.css +++ b/web/src/global.css @@ -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 `` the preview renders is left unstyled on purpose so it reads as the plain value it will send as. */