import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Loading, ErrorState } from '../../../components/PageState.jsx' import { api } from '../../../api/client.js' import { getEmailBlock, listEmailBlocks, newEmailBlock } from '../../../emailBlocks/index.js' // Admin → Engagement → Templates (ENGAGEMENT.md §4.6.2, Phase 5b). // // Phase 5a moved every subject and body out of `mailer.js` into rows. This is the // screen that lets someone change one, and its whole shape follows from a single // fact about email: // // **the server renders the mail, so the server renders the preview.** // // There is no React renderer for an `email.*` block anywhere in this client. The // preview is HTML the server produced with the same call the send path uses, // dropped into a sandboxed iframe. That costs a round trip per edit — debounced // below — and buys the only property that matters on a screen like this: what is // on screen is what will arrive, not a second implementation's opinion of it. // // **The sandbox is a security boundary, not a nicety.** The preview is // operator-authored HTML. It renders with `sandbox` and no `allow-scripts`, from // `srcdoc` (an opaque origin), so it can neither run script nor reach this page's // cookies even if someone stores markup that gets past `sanitizeHtml`. The // attributes are asserted in `client/test/emailTemplates.test.js` for the same // reason the server's checks are asserted: this is the kind of attribute someone // removes while debugging and does not put back. // // What the operator can do here is deliberately bounded (settled with the org // lead at the start of the phase): // // • **A shipped default is edited in place.** `protected` blocks deletion and // nothing else; saving sets `customized = 1`, which is what stops the next // seed bump from taking the edit back. // • **Duplicate is the only way to a new template**, so every template on a // deployment descends from one that renders. const DANGER = { color: '#d98b84', borderColor: '#5b2020' } // Three widths, because a mail body has to survive all of them and the failures // are different: 640 is a desktop client's reading pane, 360 is a phone, and the // plain-text part is what a text-only client and every screen reader gets. const WIDTHS = [ ['desktop', 'Desktop', 640], ['mobile', 'Mobile', 360], ] /** Short, human label for a template's channel. */ const CHANNEL_LABEL = { email: 'Email', inapp: 'On the site', push: 'Push' } // ── The preview frame ────────────────────────────────────────────────────── /** * The rendered HTML, in a sandboxed frame. * * `dark` applies a CSS inversion to the FRAME, not to the mail: it approximates * what Apple Mail and Outlook do to a light-only message, which is the failure * §4.6.2 asks this control to expose ("a light-only template renders as unreadable * dark-on-dark in about a third of inboxes"). It is an approximation and says so * on screen — the alternative, rendering a second dark palette server-side, would * be a preview of a mail this system does not send. */ function PreviewFrame({ html, width, dark }) { return (