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. */