{# Email template editor — requires /web/static/dist/mail_editor.js (npm run build:editor). One template serves both edit and display modes. Display (readonly) mode simply hides the help line, tab strip, toolbar, Write/Source mounts and the hidden form input, and the Preview panel is always visible. The Alpine component (pvHtmlEditor) starts on tab='preview' when readonly and auto-renders on init, so the detail page lands on the live record- aware preview without any extra clicks. #}

Write — TipTap. HTML source — same code editor as Code fields (VSCode theme, Ctrl+Space). Switch language to Jinja for template syntax highlighting. Use Insert variable or type {% raw %}{{ object. }}{% endraw %} in source.

{# Tab strip + variable picker. Only rendered in edit mode — display mode is implicitly Preview-only. #}
{# Tab buttons: @mousedown.prevent keeps the editor selection intact so a click on "HTML source" or "Preview" doesn't trigger a ProseMirror blur transaction that races against syncAll(). #}
{# Variable picker is only useful while editing HTML source — inserting {{ … }} into TipTap turns it into stylable text that's easy to mangle. Hide it on Write / Preview. #}
Loading fields…
Select a model above first.
No matches.
{# Write tab — TipTap. Display mode skips this entirely. Toolbar styled as an MS Word-shaped ribbon: labeled groups separated by hairlines, with related actions consolidated into dropdown triggers (Styles ▾, List ▾, Align ▾, Insert ▾) so the row stays scannable. Every interactive element uses @mousedown.prevent so the editor never loses focus on click — a blur fires a ProseMirror selection transaction that races our chain().focus().run() and aborts with "Applying a mismatched transaction". #}
{% include 'widgets/partials/html_editor_toolbar.html' %}
{# Source tab — same CodeMirror stack as the Code field widget. #}
{# Preview panel — always visible in display mode; tab-gated in edit mode. The record picker is purely model-driven so the same UI works in both modes (the component carries the stored subject in cfg so the POST payload is correct even with no form input around). #}
Searching…
No matches.

{# Rendered output: subject line + sanitized HTML body. When the live preview hasn't run yet, fall back to the raw client-side preview so the tab is never empty. #}
{# Hidden mirror of `this.html` for form submission. Display mode also lives inside the form but autosave is disabled there and there's no Save button, so the input is inert. #}