@aglyn/plugins-email 1.0.0-beta.143

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +58 -0
  4. package/src/index.d.ts +35 -0
  5. package/src/index.js +35 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/campaign-design-create-widget.d.ts +21 -0
  8. package/src/lib/components/campaign-design-create-widget.js +61 -0
  9. package/src/lib/components/campaign-design-create-widget.js.map +1 -0
  10. package/src/lib/components/campaign-sender-editor-widget.d.ts +18 -0
  11. package/src/lib/components/campaign-sender-editor-widget.js +39 -0
  12. package/src/lib/components/campaign-sender-editor-widget.js.map +1 -0
  13. package/src/lib/components/campaign-topic-options-widget.d.ts +19 -0
  14. package/src/lib/components/campaign-topic-options-widget.js +48 -0
  15. package/src/lib/components/campaign-topic-options-widget.js.map +1 -0
  16. package/src/lib/components/campaign-topic-select.d.ts +32 -0
  17. package/src/lib/components/campaign-topic-select.js +87 -0
  18. package/src/lib/components/campaign-topic-select.js.map +1 -0
  19. package/src/lib/components/dynamic-list-rule-fields.d.ts +173 -0
  20. package/src/lib/components/dynamic-list-rule-fields.js +1473 -0
  21. package/src/lib/components/dynamic-list-rule-fields.js.map +1 -0
  22. package/src/lib/components/email-blocks.d.ts +111 -0
  23. package/src/lib/components/email-blocks.js +875 -0
  24. package/src/lib/components/email-blocks.js.map +1 -0
  25. package/src/lib/components/email-design-preview.d.ts +62 -0
  26. package/src/lib/components/email-design-preview.js +174 -0
  27. package/src/lib/components/email-design-preview.js.map +1 -0
  28. package/src/lib/components/email-screens-card.d.ts +42 -0
  29. package/src/lib/components/email-screens-card.js +277 -0
  30. package/src/lib/components/email-screens-card.js.map +1 -0
  31. package/src/lib/components/email-template-detail.d.ts +48 -0
  32. package/src/lib/components/email-template-detail.js +681 -0
  33. package/src/lib/components/email-template-detail.js.map +1 -0
  34. package/src/lib/components/email-topic-detail.d.ts +32 -0
  35. package/src/lib/components/email-topic-detail.js +293 -0
  36. package/src/lib/components/email-topic-detail.js.map +1 -0
  37. package/src/lib/components/email-topics-card.d.ts +46 -0
  38. package/src/lib/components/email-topics-card.js +327 -0
  39. package/src/lib/components/email-topics-card.js.map +1 -0
  40. package/src/lib/components/email-zones.d.ts +28 -0
  41. package/src/lib/components/email-zones.js +20 -0
  42. package/src/lib/components/email-zones.js.map +1 -0
  43. package/src/lib/components/emails-console-page.d.ts +32 -0
  44. package/src/lib/components/emails-console-page.js +229 -0
  45. package/src/lib/components/emails-console-page.js.map +1 -0
  46. package/src/lib/components/emails-console-sections.d.ts +36 -0
  47. package/src/lib/components/emails-console-sections.js +108 -0
  48. package/src/lib/components/emails-console-sections.js.map +1 -0
  49. package/src/lib/components/list-detail-card.d.ts +47 -0
  50. package/src/lib/components/list-detail-card.js +273 -0
  51. package/src/lib/components/list-detail-card.js.map +1 -0
  52. package/src/lib/components/list-edit-card.d.ts +11 -0
  53. package/src/lib/components/list-edit-card.js +287 -0
  54. package/src/lib/components/list-edit-card.js.map +1 -0
  55. package/src/lib/components/list-import-drawer.d.ts +22 -0
  56. package/src/lib/components/list-import-drawer.js +662 -0
  57. package/src/lib/components/list-import-drawer.js.map +1 -0
  58. package/src/lib/components/list-members-panel.d.ts +94 -0
  59. package/src/lib/components/list-members-panel.js +686 -0
  60. package/src/lib/components/list-members-panel.js.map +1 -0
  61. package/src/lib/components/lists-card.d.ts +28 -0
  62. package/src/lib/components/lists-card.js +377 -0
  63. package/src/lib/components/lists-card.js.map +1 -0
  64. package/src/lib/components/sending-domain-detail.d.ts +26 -0
  65. package/src/lib/components/sending-domain-detail.js +496 -0
  66. package/src/lib/components/sending-domain-detail.js.map +1 -0
  67. package/src/lib/components/sending-domains-card.d.ts +33 -0
  68. package/src/lib/components/sending-domains-card.js +962 -0
  69. package/src/lib/components/sending-domains-card.js.map +1 -0
  70. package/src/lib/components/sending-sender-drawer.d.ts +94 -0
  71. package/src/lib/components/sending-sender-drawer.js +543 -0
  72. package/src/lib/components/sending-sender-drawer.js.map +1 -0
  73. package/src/lib/components/suppressions-card.d.ts +49 -0
  74. package/src/lib/components/suppressions-card.js +639 -0
  75. package/src/lib/components/suppressions-card.js.map +1 -0
  76. package/src/lib/components/use-org-email-topics.d.ts +79 -0
  77. package/src/lib/components/use-org-email-topics.js +111 -0
  78. package/src/lib/components/use-org-email-topics.js.map +1 -0
  79. package/src/lib/constants/bundle-common.d.ts +18 -0
  80. package/src/lib/constants/bundle-common.js +18 -0
  81. package/src/lib/constants/bundle-common.js.map +1 -0
  82. package/src/lib/hooks/use-org-company-options.d.ts +20 -0
  83. package/src/lib/hooks/use-org-company-options.js +138 -0
  84. package/src/lib/hooks/use-org-company-options.js.map +1 -0
  85. package/src/lib/hooks/use-org-contact-fields.d.ts +40 -0
  86. package/src/lib/hooks/use-org-contact-fields.js +91 -0
  87. package/src/lib/hooks/use-org-contact-fields.js.map +1 -0
  88. package/src/lib/hooks/use-org-contact-segments.d.ts +16 -0
  89. package/src/lib/hooks/use-org-contact-segments.js +55 -0
  90. package/src/lib/hooks/use-org-contact-segments.js.map +1 -0
  91. package/src/lib/hooks/use-org-crm-views.d.ts +8 -0
  92. package/src/lib/hooks/use-org-crm-views.js +74 -0
  93. package/src/lib/hooks/use-org-crm-views.js.map +1 -0
  94. package/src/lib/hooks/use-org-lists.d.ts +8 -0
  95. package/src/lib/hooks/use-org-lists.js +47 -0
  96. package/src/lib/hooks/use-org-lists.js.map +1 -0
  97. package/src/lib/model/email-design-document.d.ts +52 -0
  98. package/src/lib/model/email-design-document.js +62 -0
  99. package/src/lib/model/email-design-document.js.map +1 -0
  100. package/src/lib/model/index.d.ts +64 -0
  101. package/src/lib/model/index.js +71 -0
  102. package/src/lib/model/index.js.map +1 -0
  103. package/src/lib/model/sending-domain-status.d.ts +99 -0
  104. package/src/lib/model/sending-domain-status.js +196 -0
  105. package/src/lib/model/sending-domain-status.js.map +1 -0
  106. package/src/lib/model/template-provenance.d.ts +113 -0
  107. package/src/lib/model/template-provenance.js +107 -0
  108. package/src/lib/model/template-provenance.js.map +1 -0
  109. package/src/lib/model/template-report.d.ts +158 -0
  110. package/src/lib/model/template-report.js +249 -0
  111. package/src/lib/model/template-report.js.map +1 -0
  112. package/src/lib/plugin.d.ts +27 -0
  113. package/src/lib/plugin.js +163 -0
  114. package/src/lib/plugin.js.map +1 -0
  115. package/src/lib/server-console.d.ts +116 -0
  116. package/src/lib/server-console.js +422 -0
  117. package/src/lib/server-console.js.map +1 -0
  118. package/src/lib/server-email-drafts.d.ts +104 -0
  119. package/src/lib/server-email-drafts.js +381 -0
  120. package/src/lib/server-email-drafts.js.map +1 -0
  121. package/src/lib/server-list-gate.d.ts +183 -0
  122. package/src/lib/server-list-gate.js +365 -0
  123. package/src/lib/server-list-gate.js.map +1 -0
  124. package/src/lib/server-list-import.d.ts +199 -0
  125. package/src/lib/server-list-import.js +632 -0
  126. package/src/lib/server-list-import.js.map +1 -0
  127. package/src/lib/server-suppressions.d.ts +135 -0
  128. package/src/lib/server-suppressions.js +295 -0
  129. package/src/lib/server-suppressions.js.map +1 -0
  130. package/src/lib/server.d.ts +19 -0
  131. package/src/lib/server.js +834 -0
  132. package/src/lib/server.js.map +1 -0
  133. package/src/lib/site.d.ts +26 -0
  134. package/src/lib/site.js +81 -0
  135. package/src/lib/site.js.map +1 -0
  136. package/src/lib/unsubscribe-link.d.ts +311 -0
  137. package/src/lib/unsubscribe-link.js +398 -0
  138. package/src/lib/unsubscribe-link.js.map +1 -0
  139. package/src/lib/utils/create-email-screen.d.ts +59 -0
  140. package/src/lib/utils/create-email-screen.js +59 -0
  141. package/src/lib/utils/create-email-screen.js.map +1 -0
  142. package/src/lib/utils/generate-preset-id.d.ts +19 -0
  143. package/src/lib/utils/generate-preset-id.js +25 -0
  144. package/src/lib/utils/generate-preset-id.js.map +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-blocks.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n mdiCardTextOutline,\n mdiEmailOutline,\n mdiFormatText,\n mdiImageOutline,\n mdiMinus,\n mdiPackageVariantClosed,\n mdiArrowExpandVertical,\n mdiGestureTapButton,\n mdiCodeTags,\n} from '@aglyn/shared-data-mdi'\nimport { sanitizeAuthorHtml } from '@aglyn/aglyn/app-utils/author-html'\nimport { mergeSxProps } from '@aglyn/shared-ui-theme'\nimport Box from '@mui/material/Box'\nimport type { SxProps, Theme } from '@mui/material/styles'\nimport Typography from '@mui/material/Typography'\nimport { forwardRef } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { generatePresetId } from '../utils/generate-preset-id'\n\n/**\n * Email-safe blocks (AGL-346): the besigner canvas renders these web\n * previews; the render pipeline (email-render.ts in app-utils) converts\n * the same nodes to inline-styled table HTML for email clients. Merge\n * tokens like {{contact.firstName}} substitute at send time.\n *\n * Component ids are persisted in screen documents; never rename.\n *\n * Every block pulls `sx` out of its props and composes it AFTER its own\n * defaults with MUI array composition (AGL-587): the renderer's Leaf\n * passes the node-level sx (styles-panel output) through props.sx, and a\n * literal `sx={{...}}` after `{...rest}` used to silently discard it.\n */\n\ninterface EmailBlockStyleProps {\n /** Node-level styles merged by the renderer; wins over block defaults. */\n sx?: SxProps<Theme>\n}\n\n// ---------------------------------------------------------------- section\nexport const SECTION_ID: Aglyn.ComponentId = 'emailSection'\n\nexport interface EmailSectionProps extends EmailBlockStyleProps {\n backgroundColor?: string\n /** Inner padding in px (default 24). */\n padding?: number\n align?: 'left' | 'center' | 'right'\n children?: JSX.Children\n}\n\nexport const EmailSection = forwardRef<HTMLDivElement, EmailSectionProps>(\n (props, ref) => {\n const { backgroundColor, padding, align, children, sx, ...rest } = props\n return (\n <Box\n ref={ref}\n {...rest}\n sx={mergeSxProps(\n {\n maxWidth: 600,\n mx: 'auto',\n backgroundColor: backgroundColor || '#ffffff',\n p: `${padding ?? 24}px`,\n textAlign: align ?? 'left',\n },\n sx,\n )}\n >\n {children}\n </Box>\n )\n },\n)\nEmailSection.displayName = 'EmailSection'\n\nexport const emailSectionSchema: Aglyn.ComponentSchema<EmailSectionProps> = {\n $id: SECTION_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Email section',\n description: 'An email-safe band with its own background color and padding.',\n category: Aglyn.ComponentCategory.LAYOUT,\n icon: { path: mdiEmailOutline.path, sx: { color: '#f57c00' } },\n attributes: [\n {\n name: 'backgroundColor',\n label: 'Background color',\n description: 'Solid colors only — gradients are unreliable in email.',\n component: Aglyn.FieldComponentType.COLOR_PICKER,\n },\n {\n name: 'padding',\n label: 'Padding (px)',\n description: 'Inner spacing around the section content.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n type: 'number',\n },\n {\n name: 'align',\n label: 'Content alignment',\n description:\n 'How every block inside this section lines up. Each block can still ' +\n 'override it with its own alignment.',\n component: Aglyn.FieldComponentType.SELECT,\n options: [\n { value: 'left', label: 'Left' },\n { value: 'center', label: 'Center' },\n { value: 'right', label: 'Right' },\n ],\n },\n ],\n}\n\n// ------------------------------------------------------------------- text\nexport const TEXT_ID: Aglyn.ComponentId = 'emailText'\n\nexport interface EmailTextProps extends EmailBlockStyleProps {\n children?: string\n variant?: 'heading' | 'subheading' | 'body' | 'caption'\n color?: string\n align?: 'left' | 'center' | 'right'\n}\n\nconst TEXT_PRESETS: Record<\n NonNullable<EmailTextProps['variant']>,\n { fontSize: number; fontWeight: number; lineHeight: number }\n> = {\n heading: { fontSize: 28, fontWeight: 700, lineHeight: 1.25 },\n subheading: { fontSize: 20, fontWeight: 600, lineHeight: 1.3 },\n body: { fontSize: 15, fontWeight: 400, lineHeight: 1.55 },\n caption: { fontSize: 12, fontWeight: 400, lineHeight: 1.4 },\n}\n\nexport const EmailText = forwardRef<HTMLDivElement, EmailTextProps>(\n (props, ref) => {\n const { children, variant, color, align, sx, ...rest } = props\n const preset = TEXT_PRESETS[variant ?? 'body']\n return (\n <Typography\n ref={ref}\n component=\"div\"\n {...rest}\n sx={mergeSxProps(\n {\n fontSize: preset.fontSize,\n fontWeight: preset.fontWeight,\n lineHeight: preset.lineHeight,\n color: color || '#1a1a1a',\n textAlign: align ?? 'left',\n fontFamily: 'Helvetica, Arial, sans-serif',\n mb: 1,\n },\n sx,\n )}\n >\n {children ?? 'Email text'}\n </Typography>\n )\n },\n)\nEmailText.displayName = 'EmailText'\n\nexport const emailTextSchema: Aglyn.ComponentSchema<EmailTextProps> = {\n $id: TEXT_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Email text',\n description: 'A paragraph of email copy in one of the preset styles.',\n category: Aglyn.ComponentCategory.DATA_DISPLAY,\n icon: { path: mdiFormatText.path, sx: { color: '#f57c00' } },\n flags: { textEditable: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'children',\n label: 'Text',\n description:\n 'Supports merge tokens: {{contact.firstName}}, {{contact.email}}, ' +\n '{{org.name}}, {{unsubscribeUrl}}.',\n component: Aglyn.FieldComponentType.TEXTAREA,\n },\n {\n name: 'variant',\n label: 'Style',\n description:\n 'Size and weight of this text — Heading and Subheading also give it ' +\n 'the spacing above and below that a heading needs.',\n component: Aglyn.FieldComponentType.SELECT,\n options: [\n { value: 'heading', label: 'Heading' },\n { value: 'subheading', label: 'Subheading' },\n { value: 'body', label: 'Body' },\n { value: 'caption', label: 'Caption' },\n ],\n },\n {\n name: 'color',\n label: 'Color',\n description:\n 'Text color. Keep it dark on a light background: many clients ' +\n 'ignore dark mode overrides and show your color as-is.',\n component: Aglyn.FieldComponentType.COLOR_PICKER,\n },\n {\n name: 'align',\n label: 'Alignment',\n description: 'How this block of text lines up within the section.',\n component: Aglyn.FieldComponentType.SELECT,\n options: [\n { value: 'left', label: 'Left' },\n { value: 'center', label: 'Center' },\n { value: 'right', label: 'Right' },\n ],\n },\n ],\n}\n\n// --------------------------------------------------------------- richtext\nexport const RICHTEXT_ID: Aglyn.ComponentId = 'emailRichtext'\n\nexport interface EmailRichtextProps extends EmailBlockStyleProps {\n /** HTML fragment; sanitized with the custom-html policy (AGL-320). */\n html?: string\n}\n\nexport const EmailRichtext = forwardRef<HTMLDivElement, EmailRichtextProps>(\n (props, ref) => {\n const { html, sx, ...rest } = props\n if (!html?.trim()) {\n return (\n <Box\n ref={ref}\n {...rest}\n sx={mergeSxProps(\n {\n p: 2,\n border: '1px dashed',\n borderColor: 'divider',\n color: 'text.secondary',\n fontSize: 13,\n },\n sx,\n )}\n >\n {'Rich text — add HTML in the attributes panel'}\n </Box>\n )\n }\n return (\n <Box\n ref={ref}\n {...rest}\n sx={mergeSxProps(\n { fontFamily: 'Helvetica, Arial, sans-serif', fontSize: 15 },\n sx,\n )}\n // Sanitized on every render — same policy as the custom HTML block.\n // The `typeof window` guard this replaced existed only because that\n // policy was DOMPurify, which needs a DOM; it is now a pure function\n // (AGL-1901), so the body survives a server render instead of being\n // blanked — which for an email is the render that gets SENT.\n dangerouslySetInnerHTML={{ __html: sanitizeAuthorHtml(html) }}\n />\n )\n },\n)\nEmailRichtext.displayName = 'EmailRichtext'\n\nexport const emailRichtextSchema: Aglyn.ComponentSchema<EmailRichtextProps> = {\n $id: RICHTEXT_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Email rich text',\n description: 'An HTML fragment in an email, sanitised on render.',\n category: Aglyn.ComponentCategory.DATA_DISPLAY,\n icon: { path: mdiCardTextOutline.path, sx: { color: '#f57c00' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'html',\n label: 'HTML',\n description:\n 'Formatted content (p, b, i, a, ul/ol, h1-h4). Scripts and forms ' +\n 'are stripped; keep styling simple for email clients. Merge ' +\n 'tokens work here too.',\n component: Aglyn.FieldComponentType.TEXTAREA,\n },\n ],\n}\n\n// ------------------------------------------------------------------ image\nexport const IMAGE_ID: Aglyn.ComponentId = 'emailImage'\n\nexport interface EmailImageProps extends EmailBlockStyleProps {\n src?: string\n alt?: string\n /** Rendered width in px (max 600). */\n width?: number\n href?: string\n align?: 'left' | 'center' | 'right'\n}\n\nexport const EmailImage = forwardRef<HTMLDivElement, EmailImageProps>(\n (props, ref) => {\n const { src: storedSrc, alt, width, href, align, sx, ...rest } = props\n // A media-picker target, so it can hold a media reference (AGL-1215).\n // This is the CANVAS preview only — the sent HTML is built by\n // `email-render`, which needs an absolute URL and cannot resolve either\n // a reference or the relative CDN path it replaces.\n const src = Aglyn.resolveMediaSrc(storedSrc)\n return (\n <Box\n ref={ref}\n {...rest}\n sx={mergeSxProps({ textAlign: align ?? 'center', my: 1 }, sx)}\n >\n {src ? (\n <Box\n component=\"img\"\n src={src}\n alt={alt ?? ''}\n sx={{ maxWidth: '100%', width: width ? `${width}px` : '100%' }}\n />\n ) : (\n <Box\n sx={{\n height: 120,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n border: '1px dashed',\n borderColor: 'divider',\n color: 'text.secondary',\n fontSize: 13,\n }}\n >\n {'Image — pick from the media library'}\n </Box>\n )}\n </Box>\n )\n },\n)\nEmailImage.displayName = 'EmailImage'\n\nexport const emailImageSchema: Aglyn.ComponentSchema<EmailImageProps> = {\n $id: IMAGE_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Email image',\n description: 'An email-safe image, capped at 600px wide, optionally linked.',\n category: Aglyn.ComponentCategory.DATA_DISPLAY,\n icon: { path: mdiImageOutline.path, sx: { color: '#f57c00' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'src',\n label: 'Source URL',\n description: 'Use an absolute URL — email clients cannot resolve relative paths.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'alt',\n label: 'Alt text',\n // AGL-1896: the same \"Browse media\" default the site Image element\n // gets. It matters MORE here — most clients block images by default,\n // so for most recipients the alt text IS the block.\n description:\n 'Shown while images are blocked — most clients block by default. ' +\n 'Filled in from the media library when you pick a file that has ' +\n 'alt text.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'width',\n label: 'Width (px)',\n description:\n 'How wide the image renders. Blank fills the section — which is ' +\n 'usually right, since a phone is narrower than any number you pick.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n type: 'number',\n },\n {\n name: 'href',\n label: 'Link URL',\n description:\n 'Where the image goes when it is clicked. Blank leaves it as a ' +\n 'plain picture.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'align',\n label: 'Alignment',\n description: 'Where the image sits when it is narrower than the section.',\n component: Aglyn.FieldComponentType.SELECT,\n options: [\n { value: 'left', label: 'Left' },\n { value: 'center', label: 'Center' },\n { value: 'right', label: 'Right' },\n ],\n },\n ],\n}\n\n// ----------------------------------------------------------------- button\nexport const BUTTON_ID: Aglyn.ComponentId = 'emailButton'\n\nexport interface EmailButtonProps extends EmailBlockStyleProps {\n children?: string\n href?: string\n backgroundColor?: string\n color?: string\n align?: 'left' | 'center' | 'right'\n}\n\nexport const EmailButton = forwardRef<HTMLDivElement, EmailButtonProps>(\n (props, ref) => {\n const { children, href, backgroundColor, color, align, sx, ...rest } =\n props\n return (\n <Box\n ref={ref}\n {...rest}\n sx={mergeSxProps({ textAlign: align ?? 'center', my: 1.5 }, sx)}\n >\n <Box\n component=\"span\"\n sx={{\n display: 'inline-block',\n px: 3,\n py: 1.25,\n borderRadius: 1,\n backgroundColor: backgroundColor || '#1a73e8',\n color: color || '#ffffff',\n fontFamily: 'Helvetica, Arial, sans-serif',\n fontSize: 15,\n fontWeight: 600,\n }}\n >\n {children ?? 'Call to action'}\n </Box>\n </Box>\n )\n },\n)\nEmailButton.displayName = 'EmailButton'\n\nexport const emailButtonSchema: Aglyn.ComponentSchema<EmailButtonProps> = {\n $id: BUTTON_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Email button',\n description: 'A call-to-action button that survives every mail client.',\n category: Aglyn.ComponentCategory.INPUT,\n icon: { path: mdiGestureTapButton.path, sx: { color: '#f57c00' } },\n flags: { textEditable: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'children',\n label: 'Label',\n description:\n 'The words on the button. Say what happens next — \"See the report\" ' +\n 'reads as a destination where \"Click here\" does not.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'href',\n label: 'Link URL',\n description: 'Absolute URL (https://…). Merge tokens allowed.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'backgroundColor',\n label: 'Button color',\n description:\n 'The fill behind the label. Check it against the label color — a ' +\n 'button nobody can read is a link nobody clicks.',\n component: Aglyn.FieldComponentType.COLOR_PICKER,\n },\n {\n name: 'color',\n label: 'Label color',\n description: 'The color of the words on the button.',\n component: Aglyn.FieldComponentType.COLOR_PICKER,\n },\n {\n name: 'align',\n label: 'Alignment',\n description: 'Where the button sits across the width of the section.',\n component: Aglyn.FieldComponentType.SELECT,\n options: [\n { value: 'left', label: 'Left' },\n { value: 'center', label: 'Center' },\n { value: 'right', label: 'Right' },\n ],\n },\n ],\n}\n\n// ---------------------------------------------------------------- divider\nexport const DIVIDER_ID: Aglyn.ComponentId = 'emailDivider'\n\nexport interface EmailDividerProps extends EmailBlockStyleProps {\n color?: string\n}\n\nexport const EmailDivider = forwardRef<HTMLHRElement, EmailDividerProps>(\n (props, ref) => {\n const { color, sx, ...rest } = props\n return (\n <Box\n ref={ref}\n component=\"hr\"\n {...rest}\n sx={mergeSxProps(\n {\n border: 0,\n // Split shorthand so a palette token path from the theme-\n // reference color picker (AGL-588) resolves through the sx\n // system instead of dying inside a template string.\n borderTopStyle: 'solid',\n borderTopWidth: 1,\n borderTopColor: color || '#e0e0e0',\n my: 2,\n },\n sx,\n )}\n />\n )\n },\n)\nEmailDivider.displayName = 'EmailDivider'\n\nexport const emailDividerSchema: Aglyn.ComponentSchema<EmailDividerProps> = {\n $id: DIVIDER_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Email divider',\n description: 'A horizontal rule between email sections.',\n category: Aglyn.ComponentCategory.LAYOUT,\n icon: { path: mdiMinus.path, sx: { color: '#f57c00' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'color',\n label: 'Line color',\n description:\n 'The color of the rule. A light grey separates sections without ' +\n 'drawing the eye to the line itself.',\n component: Aglyn.FieldComponentType.COLOR_PICKER,\n },\n ],\n}\n\n// ----------------------------------------------------------------- spacer\nexport const SPACER_ID: Aglyn.ComponentId = 'emailSpacer'\n\nexport interface EmailSpacerProps extends EmailBlockStyleProps {\n /** Height in px (default 24). */\n height?: number\n}\n\nexport const EmailSpacer = forwardRef<HTMLDivElement, EmailSpacerProps>(\n (props, ref) => {\n const { height, sx, ...rest } = props\n return (\n <Box\n ref={ref}\n {...rest}\n sx={mergeSxProps({ height: height ?? 24 }, sx)}\n />\n )\n },\n)\nEmailSpacer.displayName = 'EmailSpacer'\n\nexport const emailSpacerSchema: Aglyn.ComponentSchema<EmailSpacerProps> = {\n $id: SPACER_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Email spacer',\n description: 'Fixed vertical whitespace between email blocks.',\n category: Aglyn.ComponentCategory.LAYOUT,\n icon: { path: mdiArrowExpandVertical.path, sx: { color: '#f57c00' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'height',\n label: 'Height (px)',\n description:\n 'How much empty vertical space this block adds. Email clients drop ' +\n 'CSS margins, so a spacer is how you get reliable breathing room.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n type: 'number',\n },\n ],\n}\n\n// ---------------------------------------------------------------- product\nexport const PRODUCT_ID: Aglyn.ComponentId = 'emailProduct'\n\nexport interface EmailProductProps extends EmailBlockStyleProps {\n /** Product id — resolved server-side at render (rename-safe). */\n productId?: string\n buttonLabel?: string\n}\n\nexport const EmailProduct = forwardRef<HTMLDivElement, EmailProductProps>(\n (props, ref) => {\n const { productId, buttonLabel, sx, ...rest } = props\n return (\n <Box\n ref={ref}\n {...rest}\n sx={mergeSxProps(\n {\n border: '1px solid',\n borderColor: 'divider',\n borderRadius: 1,\n p: 2,\n my: 1,\n textAlign: 'center',\n color: 'text.secondary',\n fontSize: 13,\n },\n sx,\n )}\n >\n {productId\n ? `Product card (${productId}) — name, price, image fill in at send`\n : 'Product card — pick a product in the attributes panel'}\n <Box sx={{ mt: 1, fontWeight: 600 }}>\n {buttonLabel ?? 'Shop now'}\n </Box>\n </Box>\n )\n },\n)\nEmailProduct.displayName = 'EmailProduct'\n\nexport const emailProductSchema: Aglyn.ComponentSchema<EmailProductProps> = {\n $id: PRODUCT_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Email product',\n description: 'A product card resolved at send time, so it never goes stale.',\n category: Aglyn.ComponentCategory.DATA_DISPLAY,\n icon: { path: mdiPackageVariantClosed.path, sx: { color: '#f57c00' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'productId',\n label: 'Product',\n description:\n 'Stored by id — the current name, price, and image render at send.',\n component: Aglyn.FieldComponentType.PRODUCT_SELECT,\n },\n {\n name: 'buttonLabel',\n label: 'Button label',\n description:\n 'The words on the card’s button. Blank uses the block’s default, ' +\n 'and the link always points at this product’s page.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n ],\n}\n\n// ------------------------------------------------------------ custom html\nexport const HTML_ID: Aglyn.ComponentId = 'emailHtml'\n\nexport interface EmailHtmlProps extends EmailBlockStyleProps {\n html?: string\n}\n\nexport const EmailHtml = forwardRef<HTMLDivElement, EmailHtmlProps>(\n (props, ref) => {\n const { html, sx, ...rest } = props\n return (\n <Box\n ref={ref}\n {...rest}\n sx={mergeSxProps({ fontFamily: 'Helvetica, Arial, sans-serif' }, sx)}\n // No `typeof window` guard any more (AGL-1901): the sanitizer needs\n // no DOM, so a server-rendered email keeps its custom HTML.\n dangerouslySetInnerHTML={{\n __html: html ? sanitizeAuthorHtml(html) : '',\n }}\n />\n )\n },\n)\nEmailHtml.displayName = 'EmailHtml'\n\nexport const emailHtmlSchema: Aglyn.ComponentSchema<EmailHtmlProps> = {\n $id: HTML_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Email custom HTML',\n description: 'Hand-written HTML for an email, used as-is.',\n category: Aglyn.ComponentCategory.DATA_DISPLAY,\n icon: { path: mdiCodeTags.path, sx: { color: '#f57c00' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'html',\n label: 'HTML',\n description:\n 'Raw table markup for advanced layouts. Sanitized with the ' +\n 'custom-html policy; the render pipeline passes it through as-is ' +\n 'after sanitization.',\n component: Aglyn.FieldComponentType.TEXTAREA,\n },\n ],\n}\n\nexport const emailPresets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(SECTION_ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Email section',\n pluginId: BUNDLE_ID,\n category: Aglyn.ComponentCategory.LAYOUT,\n icon: { path: mdiEmailOutline.path, sx: { color: '#f57c00' } },\n data: {\n $id: null,\n componentId: SECTION_ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n },\n {\n $id: generatePresetId(TEXT_ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Email text',\n pluginId: BUNDLE_ID,\n category: Aglyn.ComponentCategory.DATA_DISPLAY,\n icon: { path: mdiFormatText.path, sx: { color: '#f57c00' } },\n data: {\n $id: null,\n componentId: TEXT_ID,\n pluginId: BUNDLE_ID,\n props: { children: 'Hello {{contact.firstName}},', variant: 'body' },\n },\n },\n {\n $id: generatePresetId(RICHTEXT_ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Email rich text',\n pluginId: BUNDLE_ID,\n category: Aglyn.ComponentCategory.DATA_DISPLAY,\n icon: { path: mdiCardTextOutline.path, sx: { color: '#f57c00' } },\n data: {\n $id: null,\n componentId: RICHTEXT_ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n },\n {\n $id: generatePresetId(IMAGE_ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Email image',\n pluginId: BUNDLE_ID,\n category: Aglyn.ComponentCategory.DATA_DISPLAY,\n icon: { path: mdiImageOutline.path, sx: { color: '#f57c00' } },\n data: {\n $id: null,\n componentId: IMAGE_ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n },\n {\n $id: generatePresetId(BUTTON_ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Email button',\n pluginId: BUNDLE_ID,\n category: Aglyn.ComponentCategory.INPUT,\n icon: { path: mdiGestureTapButton.path, sx: { color: '#f57c00' } },\n data: {\n $id: null,\n componentId: BUTTON_ID,\n pluginId: BUNDLE_ID,\n props: { children: 'Shop now' },\n },\n },\n {\n $id: generatePresetId(DIVIDER_ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Email divider',\n pluginId: BUNDLE_ID,\n category: Aglyn.ComponentCategory.LAYOUT,\n icon: { path: mdiMinus.path, sx: { color: '#f57c00' } },\n data: {\n $id: null,\n componentId: DIVIDER_ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n },\n {\n $id: generatePresetId(SPACER_ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Email spacer',\n pluginId: BUNDLE_ID,\n category: Aglyn.ComponentCategory.LAYOUT,\n icon: { path: mdiArrowExpandVertical.path, sx: { color: '#f57c00' } },\n data: {\n $id: null,\n componentId: SPACER_ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n },\n {\n $id: generatePresetId(PRODUCT_ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Email product',\n pluginId: BUNDLE_ID,\n category: Aglyn.ComponentCategory.DATA_DISPLAY,\n icon: { path: mdiPackageVariantClosed.path, sx: { color: '#f57c00' } },\n data: {\n $id: null,\n componentId: PRODUCT_ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n },\n {\n $id: generatePresetId(HTML_ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Email custom HTML',\n pluginId: BUNDLE_ID,\n category: Aglyn.ComponentCategory.DATA_DISPLAY,\n icon: { path: mdiCodeTags.path, sx: { color: '#f57c00' } },\n data: {\n $id: null,\n componentId: HTML_ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n },\n]\n"],"names":["Aglyn","mdiCardTextOutline","mdiEmailOutline","mdiFormatText","mdiImageOutline","mdiMinus","mdiPackageVariantClosed","mdiArrowExpandVertical","mdiGestureTapButton","mdiCodeTags","sanitizeAuthorHtml","mergeSxProps","Box","Typography","forwardRef","BUNDLE_ID","generatePresetId","SECTION_ID","EmailSection","props","ref","backgroundColor","padding","align","children","sx","rest","maxWidth","mx","p","textAlign","displayName","emailSectionSchema","$id","pluginId","description","category","ComponentCategory","LAYOUT","icon","path","color","attributes","name","label","component","FieldComponentType","COLOR_PICKER","TEXT_FIELD","type","SELECT","options","value","TEXT_ID","TEXT_PRESETS","heading","fontSize","fontWeight","lineHeight","subheading","body","caption","EmailText","variant","preset","fontFamily","mb","emailTextSchema","DATA_DISPLAY","flags","textEditable","FEATURE_FLAG","ENABLED","TEXTAREA","RICHTEXT_ID","EmailRichtext","html","trim","border","borderColor","dangerouslySetInnerHTML","__html","emailRichtextSchema","selfClosing","IMAGE_ID","EmailImage","src","storedSrc","alt","width","href","resolveMediaSrc","my","height","display","alignItems","justifyContent","emailImageSchema","BUTTON_ID","EmailButton","px","py","borderRadius","emailButtonSchema","INPUT","DIVIDER_ID","EmailDivider","borderTopStyle","borderTopWidth","borderTopColor","emailDividerSchema","SPACER_ID","EmailSpacer","emailSpacerSchema","PRODUCT_ID","EmailProduct","productId","buttonLabel","mt","emailProductSchema","PRODUCT_SELECT","HTML_ID","EmailHtml","emailHtmlSchema","emailPresets","NodeType","PRESET","data","componentId"],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ export interface EmailDesignPreviewProps {
2
+ hostId: string;
3
+ /** The version document's raw `nodes` field, in any of its stored forms. */
4
+ nodes: unknown;
5
+ /**
6
+ * The plain-text body, for a message composed without a template.
7
+ *
8
+ * Such a message is not previewless — the send path synthesizes an HTML
9
+ * part for it through `renderTextEmailHtml`, and that part is what the
10
+ * inbox receives. Rendering the same function here shows the mailed copy
11
+ * rather than reporting that there is nothing to show. Ignored when there
12
+ * are nodes: a template supersedes it, exactly as it does at send time.
13
+ */
14
+ text?: string;
15
+ /** The version document has not arrived yet, as against not existing. */
16
+ loading?: boolean;
17
+ subject?: string;
18
+ preheader?: string;
19
+ /** What to say when there is nothing to draw. */
20
+ emptyMessage: string;
21
+ /**
22
+ * An extra line under the frame — for a surface where the rendering has a
23
+ * caveat of its own, such as a message sent before the template changed.
24
+ */
25
+ note?: string;
26
+ }
27
+ /**
28
+ * THE EMAIL AS AN INBOX RECEIVES IT, DRAWN SAFELY.
29
+ *
30
+ * ## The same renderer the send path calls
31
+ *
32
+ * `renderEmailHtml` is the function `campaign-send.ts` calls, given the same
33
+ * `rootId` and the same stored node map — so what is drawn here is the
34
+ * document that gets mailed, rather than a second rendering that agrees with
35
+ * it until it does not. The besigner is NOT that: it draws canvas nodes as
36
+ * React in a browser, which is a different pipeline with different output.
37
+ *
38
+ * Two inputs differ from a real send and both are said on screen. Merge
39
+ * tokens are left standing — no merge map is passed, and the renderer leaves
40
+ * unknown tokens visible for exactly this reason, so `{{contact.firstName}}`
41
+ * shows where personalization lands instead of a blank. Product blocks
42
+ * resolve against the catalog at send time and are not resolved here, because
43
+ * doing so would put a catalog read on every preview for a picture that can
44
+ * change between now and the send anyway.
45
+ *
46
+ * ## Sandboxed, because the HTML is tenant-authored
47
+ *
48
+ * The design is written by a site's own editors — or by a marketplace
49
+ * publisher, on an installed template — and may contain a custom-HTML block.
50
+ * It is rendered into an iframe with an EMPTY `sandbox` attribute, the
51
+ * maximally restrictive form: no scripts, no forms, no popups, no top-level
52
+ * navigation and, critically, no same-origin. The frame gets an opaque
53
+ * origin, so nothing inside it can reach the console's cookies, storage or
54
+ * DOM. `srcDoc` rather than a URL keeps the markup from ever being served
55
+ * from the console's own origin, where the sandbox attribute would be the
56
+ * only thing between tenant HTML and a live session.
57
+ */
58
+ export declare function EmailDesignPreview(props: EmailDesignPreviewProps): import("react").JSX.Element;
59
+ export declare namespace EmailDesignPreview {
60
+ var displayName: string;
61
+ }
62
+ export default EmailDesignPreview;
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
+ import { decodeStoredNodes } from "@aglyn/aglyn";
19
+ import { EMAIL_NODE_ROOT_ID, renderEmailHtml, renderTextEmailHtml } from "@aglyn/shared-util-email";
20
+ import { sanitizeAuthorHtml } from "@aglyn/aglyn/app-utils/author-html";
21
+ import { Box, Stack, Typography } from "@mui/material";
22
+ import { useEffect, useMemo, useState } from "react";
23
+ /**
24
+ * THE EMAIL AS AN INBOX RECEIVES IT, DRAWN SAFELY.
25
+ *
26
+ * ## The same renderer the send path calls
27
+ *
28
+ * `renderEmailHtml` is the function `campaign-send.ts` calls, given the same
29
+ * `rootId` and the same stored node map — so what is drawn here is the
30
+ * document that gets mailed, rather than a second rendering that agrees with
31
+ * it until it does not. The besigner is NOT that: it draws canvas nodes as
32
+ * React in a browser, which is a different pipeline with different output.
33
+ *
34
+ * Two inputs differ from a real send and both are said on screen. Merge
35
+ * tokens are left standing — no merge map is passed, and the renderer leaves
36
+ * unknown tokens visible for exactly this reason, so `{{contact.firstName}}`
37
+ * shows where personalization lands instead of a blank. Product blocks
38
+ * resolve against the catalog at send time and are not resolved here, because
39
+ * doing so would put a catalog read on every preview for a picture that can
40
+ * change between now and the send anyway.
41
+ *
42
+ * ## Sandboxed, because the HTML is tenant-authored
43
+ *
44
+ * The design is written by a site's own editors — or by a marketplace
45
+ * publisher, on an installed template — and may contain a custom-HTML block.
46
+ * It is rendered into an iframe with an EMPTY `sandbox` attribute, the
47
+ * maximally restrictive form: no scripts, no forms, no popups, no top-level
48
+ * navigation and, critically, no same-origin. The frame gets an opaque
49
+ * origin, so nothing inside it can reach the console's cookies, storage or
50
+ * DOM. `srcDoc` rather than a URL keeps the markup from ever being served
51
+ * from the console's own origin, where the sandbox attribute would be the
52
+ * only thing between tenant HTML and a live session.
53
+ */ export function EmailDesignPreview(props) {
54
+ const { hostId, nodes: rawNodes, text, loading, subject, preheader, emptyMessage, note } = props;
55
+ /*
56
+ * The origin the preview's images are fetched from.
57
+ *
58
+ * A picked image is stored as a `media:` reference that resolves to the
59
+ * site-RELATIVE CDN path, and the console mounts that route too — but a
60
+ * sandboxed `srcDoc` frame has an opaque origin, so a relative `src` inside
61
+ * it has nothing dependable to resolve against. Read after mount rather
62
+ * than during render because a server render has no `window`, and a value
63
+ * that differs between the two renders is a hydration mismatch on a string
64
+ * the size of a whole email.
65
+ */ const [origin, setOrigin] = useState('');
66
+ useEffect(()=>setOrigin(window.location.origin), []);
67
+ const nodes = useMemo(()=>{
68
+ var _decodeStoredNodes;
69
+ return (_decodeStoredNodes = decodeStoredNodes(rawNodes)) != null ? _decodeStoredNodes : {};
70
+ }, [
71
+ rawNodes
72
+ ]);
73
+ const productBlocks = useMemo(()=>Object.values(nodes).filter((node)=>(node == null ? void 0 : node.componentId) === 'emailProduct').length, [
74
+ nodes
75
+ ]);
76
+ const rendered = useMemo(()=>{
77
+ if (!Object.keys(nodes).length) {
78
+ /*
79
+ * A plain-text message renders through the send path's own text
80
+ * synthesizer, so the frame below draws what was actually mailed.
81
+ * `renderTextEmailHtml` escapes the body itself, which is why no
82
+ * sanitizer is threaded here — there is no author markup to strip.
83
+ */ const body = String(text != null ? text : '').trim();
84
+ if (!body) return null;
85
+ const html = renderTextEmailHtml(body, subject, preheader);
86
+ return html ? {
87
+ html,
88
+ text: body
89
+ } : null;
90
+ }
91
+ return renderEmailHtml({
92
+ /*
93
+ * The same policy the send path applies, so what is previewed here is
94
+ * what an inbox receives. The sandboxed frame below is a second wall,
95
+ * not this one's replacement — a preview that showed markup the mailed
96
+ * copy strips would be a preview of a different email.
97
+ */ sanitize: sanitizeAuthorHtml,
98
+ nodes,
99
+ // Besigner maps are rooted at '_@_'. Rendering one as the default
100
+ // 'root' finds no root and emits an empty 600px shell.
101
+ rootId: EMAIL_NODE_ROOT_ID,
102
+ subject,
103
+ preheader,
104
+ mediaOrigin: origin,
105
+ mediaHostId: hostId
106
+ });
107
+ }, [
108
+ nodes,
109
+ text,
110
+ subject,
111
+ preheader,
112
+ origin,
113
+ hostId
114
+ ]);
115
+ if (!rendered) {
116
+ return /*#__PURE__*/ _jsx(Typography, {
117
+ variant: "body2",
118
+ color: "text.secondary",
119
+ children: loading ? 'Loading this email…' : emptyMessage
120
+ });
121
+ }
122
+ return /*#__PURE__*/ _jsxs(Stack, {
123
+ spacing: 1,
124
+ children: [
125
+ /*#__PURE__*/ _jsx(Typography, {
126
+ variant: "body2",
127
+ children: subject ? `Subject: ${subject}` : 'This template carries no subject line of its own — each ' + 'email supplies one.'
128
+ }),
129
+ /*#__PURE__*/ _jsx(Box, {
130
+ component: "iframe",
131
+ title: "Email preview",
132
+ /*
133
+ * EMPTY sandbox — every restriction on, nothing allowed back. The
134
+ * markup below is authored outside this console and may contain a
135
+ * custom-HTML block, so it is rendered with no scripts, no forms, no
136
+ * navigation and an opaque origin: it cannot reach the console's
137
+ * session, storage or DOM even though it is drawn inside the console.
138
+ */ sandbox: "",
139
+ referrerPolicy: "no-referrer",
140
+ srcDoc: rendered.html,
141
+ sx: {
142
+ width: '100%',
143
+ // Fixed, with the frame's own scrollbar. A sandbox with no scripts
144
+ // is a frame that cannot measure or report its content height, and
145
+ // an iframe cannot size to its content on its own.
146
+ height: 640,
147
+ border: 1,
148
+ borderColor: 'divider',
149
+ borderRadius: 1,
150
+ bgcolor: 'common.white'
151
+ }
152
+ }),
153
+ /*#__PURE__*/ _jsx(Typography, {
154
+ variant: "caption",
155
+ color: "text.secondary",
156
+ children: 'Rendered by the same code that builds the mail. Merge tokens are ' + 'left standing — a real send fills them from each recipient.'
157
+ }),
158
+ productBlocks ? /*#__PURE__*/ _jsx(Typography, {
159
+ variant: "caption",
160
+ color: "text.secondary",
161
+ children: `${productBlocks} product ${productBlocks === 1 ? 'block is' : 'blocks are'} not drawn here — each resolves against the catalog when an email ` + 'sends, so what it shows depends on the product at that moment.'
162
+ }) : null,
163
+ note ? /*#__PURE__*/ _jsx(Typography, {
164
+ variant: "caption",
165
+ color: "text.secondary",
166
+ children: note
167
+ }) : null
168
+ ]
169
+ });
170
+ }
171
+ EmailDesignPreview.displayName = 'EmailDesignPreview';
172
+ export default EmailDesignPreview;
173
+
174
+ //# sourceMappingURL=email-design-preview.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/email-design-preview.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { decodeStoredNodes } from '@aglyn/aglyn'\nimport {\n EMAIL_NODE_ROOT_ID,\n renderEmailHtml,\n renderTextEmailHtml,\n} from '@aglyn/shared-util-email'\nimport { sanitizeAuthorHtml } from '@aglyn/aglyn/app-utils/author-html'\nimport { Box, Stack, Typography } from '@mui/material'\nimport { useEffect, useMemo, useState } from 'react'\n\nexport interface EmailDesignPreviewProps {\n hostId: string\n /** The version document's raw `nodes` field, in any of its stored forms. */\n nodes: unknown\n /**\n * The plain-text body, for a message composed without a template.\n *\n * Such a message is not previewless — the send path synthesizes an HTML\n * part for it through `renderTextEmailHtml`, and that part is what the\n * inbox receives. Rendering the same function here shows the mailed copy\n * rather than reporting that there is nothing to show. Ignored when there\n * are nodes: a template supersedes it, exactly as it does at send time.\n */\n text?: string\n /** The version document has not arrived yet, as against not existing. */\n loading?: boolean\n subject?: string\n preheader?: string\n /** What to say when there is nothing to draw. */\n emptyMessage: string\n /**\n * An extra line under the frame — for a surface where the rendering has a\n * caveat of its own, such as a message sent before the template changed.\n */\n note?: string\n}\n\n/**\n * THE EMAIL AS AN INBOX RECEIVES IT, DRAWN SAFELY.\n *\n * ## The same renderer the send path calls\n *\n * `renderEmailHtml` is the function `campaign-send.ts` calls, given the same\n * `rootId` and the same stored node map — so what is drawn here is the\n * document that gets mailed, rather than a second rendering that agrees with\n * it until it does not. The besigner is NOT that: it draws canvas nodes as\n * React in a browser, which is a different pipeline with different output.\n *\n * Two inputs differ from a real send and both are said on screen. Merge\n * tokens are left standing — no merge map is passed, and the renderer leaves\n * unknown tokens visible for exactly this reason, so `{{contact.firstName}}`\n * shows where personalization lands instead of a blank. Product blocks\n * resolve against the catalog at send time and are not resolved here, because\n * doing so would put a catalog read on every preview for a picture that can\n * change between now and the send anyway.\n *\n * ## Sandboxed, because the HTML is tenant-authored\n *\n * The design is written by a site's own editors — or by a marketplace\n * publisher, on an installed template — and may contain a custom-HTML block.\n * It is rendered into an iframe with an EMPTY `sandbox` attribute, the\n * maximally restrictive form: no scripts, no forms, no popups, no top-level\n * navigation and, critically, no same-origin. The frame gets an opaque\n * origin, so nothing inside it can reach the console's cookies, storage or\n * DOM. `srcDoc` rather than a URL keeps the markup from ever being served\n * from the console's own origin, where the sandbox attribute would be the\n * only thing between tenant HTML and a live session.\n */\nexport function EmailDesignPreview(props: EmailDesignPreviewProps) {\n const {\n hostId,\n nodes: rawNodes,\n text,\n loading,\n subject,\n preheader,\n emptyMessage,\n note,\n } = props\n\n /*\n * The origin the preview's images are fetched from.\n *\n * A picked image is stored as a `media:` reference that resolves to the\n * site-RELATIVE CDN path, and the console mounts that route too — but a\n * sandboxed `srcDoc` frame has an opaque origin, so a relative `src` inside\n * it has nothing dependable to resolve against. Read after mount rather\n * than during render because a server render has no `window`, and a value\n * that differs between the two renders is a hydration mismatch on a string\n * the size of a whole email.\n */\n const [origin, setOrigin] = useState('')\n useEffect(() => setOrigin(window.location.origin), [])\n\n const nodes = useMemo(\n () => decodeStoredNodes<Record<string, any>>(rawNodes) ?? {},\n [rawNodes],\n )\n const productBlocks = useMemo(\n () =>\n Object.values(nodes).filter(\n (node: any) => node?.componentId === 'emailProduct',\n ).length,\n [nodes],\n )\n const rendered = useMemo(() => {\n if (!Object.keys(nodes).length) {\n /*\n * A plain-text message renders through the send path's own text\n * synthesizer, so the frame below draws what was actually mailed.\n * `renderTextEmailHtml` escapes the body itself, which is why no\n * sanitizer is threaded here — there is no author markup to strip.\n */\n const body = String(text ?? '').trim()\n if (!body) return null\n const html = renderTextEmailHtml(body, subject, preheader)\n return html ? { html, text: body } : null\n }\n return renderEmailHtml({\n /*\n * The same policy the send path applies, so what is previewed here is\n * what an inbox receives. The sandboxed frame below is a second wall,\n * not this one's replacement — a preview that showed markup the mailed\n * copy strips would be a preview of a different email.\n */\n sanitize: sanitizeAuthorHtml,\n nodes,\n // Besigner maps are rooted at '_@_'. Rendering one as the default\n // 'root' finds no root and emits an empty 600px shell.\n rootId: EMAIL_NODE_ROOT_ID,\n subject,\n preheader,\n mediaOrigin: origin,\n mediaHostId: hostId,\n })\n }, [nodes, text, subject, preheader, origin, hostId])\n\n if (!rendered) {\n return (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {loading ? 'Loading this email…' : emptyMessage}\n </Typography>\n )\n }\n\n return (\n <Stack spacing={1}>\n <Typography variant=\"body2\">\n {subject\n ? `Subject: ${subject}`\n : 'This template carries no subject line of its own — each ' +\n 'email supplies one.'}\n </Typography>\n <Box\n component=\"iframe\"\n title=\"Email preview\"\n /*\n * EMPTY sandbox — every restriction on, nothing allowed back. The\n * markup below is authored outside this console and may contain a\n * custom-HTML block, so it is rendered with no scripts, no forms, no\n * navigation and an opaque origin: it cannot reach the console's\n * session, storage or DOM even though it is drawn inside the console.\n */\n sandbox=\"\"\n referrerPolicy=\"no-referrer\"\n srcDoc={rendered.html}\n sx={{\n width: '100%',\n // Fixed, with the frame's own scrollbar. A sandbox with no scripts\n // is a frame that cannot measure or report its content height, and\n // an iframe cannot size to its content on its own.\n height: 640,\n border: 1,\n borderColor: 'divider',\n borderRadius: 1,\n bgcolor: 'common.white',\n }}\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Rendered by the same code that builds the mail. Merge tokens are ' +\n 'left standing — a real send fills them from each recipient.'}\n </Typography>\n {productBlocks ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${productBlocks} product ${\n productBlocks === 1 ? 'block is' : 'blocks are'\n } not drawn here — each resolves against the catalog when an email ` +\n 'sends, so what it shows depends on the product at that moment.'}\n </Typography>\n ) : null}\n {note ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {note}\n </Typography>\n ) : null}\n </Stack>\n )\n}\nEmailDesignPreview.displayName = 'EmailDesignPreview'\n\nexport default EmailDesignPreview\n"],"names":["decodeStoredNodes","EMAIL_NODE_ROOT_ID","renderEmailHtml","renderTextEmailHtml","sanitizeAuthorHtml","Box","Stack","Typography","useEffect","useMemo","useState","EmailDesignPreview","props","hostId","nodes","rawNodes","text","loading","subject","preheader","emptyMessage","note","origin","setOrigin","window","location","productBlocks","Object","values","filter","node","componentId","length","rendered","keys","body","String","trim","html","sanitize","rootId","mediaOrigin","mediaHostId","variant","color","spacing","component","title","sandbox","referrerPolicy","srcDoc","sx","width","height","border","borderColor","borderRadius","bgcolor","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,iBAAiB,QAAQ,eAAc;AAChD,SACEC,kBAAkB,EAClBC,eAAe,EACfC,mBAAmB,QACd,2BAA0B;AACjC,SAASC,kBAAkB,QAAQ,qCAAoC;AACvE,SAASC,GAAG,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AACtD,SAASC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AA6BpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,SAASC,mBAAmBC,KAA8B;IAC/D,MAAM,EACJC,MAAM,EACNC,OAAOC,QAAQ,EACfC,IAAI,EACJC,OAAO,EACPC,OAAO,EACPC,SAAS,EACTC,YAAY,EACZC,IAAI,EACL,GAAGT;IAEJ;;;;;;;;;;GAUC,GACD,MAAM,CAACU,QAAQC,UAAU,GAAGb,SAAS;IACrCF,UAAU,IAAMe,UAAUC,OAAOC,QAAQ,CAACH,MAAM,GAAG,EAAE;IAErD,MAAMR,QAAQL,QACZ;YAAMT;gBAAAA,qBAAAA,kBAAuCe,qBAAvCf,qBAAoD,CAAC;OAC3D;QAACe;KAAS;IAEZ,MAAMW,gBAAgBjB,QACpB,IACEkB,OAAOC,MAAM,CAACd,OAAOe,MAAM,CACzB,CAACC,OAAcA,CAAAA,wBAAAA,KAAMC,WAAW,MAAK,gBACrCC,MAAM,EACV;QAAClB;KAAM;IAET,MAAMmB,WAAWxB,QAAQ;QACvB,IAAI,CAACkB,OAAOO,IAAI,CAACpB,OAAOkB,MAAM,EAAE;YAC9B;;;;;OAKC,GACD,MAAMG,OAAOC,OAAOpB,eAAAA,OAAQ,IAAIqB,IAAI;YACpC,IAAI,CAACF,MAAM,OAAO;YAClB,MAAMG,OAAOnC,oBAAoBgC,MAAMjB,SAASC;YAChD,OAAOmB,OAAO;gBAAEA;gBAAMtB,MAAMmB;YAAK,IAAI;QACvC;QACA,OAAOjC,gBAAgB;YACrB;;;;;OAKC,GACDqC,UAAUnC;YACVU;YACA,kEAAkE;YAClE,uDAAuD;YACvD0B,QAAQvC;YACRiB;YACAC;YACAsB,aAAanB;YACboB,aAAa7B;QACf;IACF,GAAG;QAACC;QAAOE;QAAME;QAASC;QAAWG;QAAQT;KAAO;IAEpD,IAAI,CAACoB,UAAU;QACb,qBACE,KAAC1B;YAAWoC,SAAQ;YAAQC,OAAM;sBAC/B3B,UAAU,wBAAwBG;;IAGzC;IAEA,qBACE,MAACd;QAAMuC,SAAS;;0BACd,KAACtC;gBAAWoC,SAAQ;0BACjBzB,UACG,CAAC,SAAS,EAAEA,SAAS,GACrB,6DACA;;0BAEN,KAACb;gBACCyC,WAAU;gBACVC,OAAM;gBACN;;;;;;SAMC,GACDC,SAAQ;gBACRC,gBAAe;gBACfC,QAAQjB,SAASK,IAAI;gBACrBa,IAAI;oBACFC,OAAO;oBACP,mEAAmE;oBACnE,mEAAmE;oBACnE,mDAAmD;oBACnDC,QAAQ;oBACRC,QAAQ;oBACRC,aAAa;oBACbC,cAAc;oBACdC,SAAS;gBACX;;0BAEF,KAAClD;gBAAWoC,SAAQ;gBAAUC,OAAM;0BACjC,sEACC;;YAEHlB,8BACC,KAACnB;gBAAWoC,SAAQ;gBAAUC,OAAM;0BACjC,GAAGlB,cAAc,SAAS,EACzBA,kBAAkB,IAAI,aAAa,aACpC,kEAAkE,CAAC,GAClE;iBAEF;YACHL,qBACC,KAACd;gBAAWoC,SAAQ;gBAAUC,OAAM;0BACjCvB;iBAED;;;AAGV;AACAV,mBAAmB+C,WAAW,GAAG;AAEjC,eAAe/C,mBAAkB"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * THE TEMPLATES: reusable besigner documents an email is built from.
3
+ *
4
+ * A template is a screen document with `kind: 'email'`, kept out of the main
5
+ * Screens list and opened in the besigner with only email-safe components on
6
+ * offer. It is not itself a message — a message is what a campaign sends, and
7
+ * one template can be behind many of them, which is why the row leads to the
8
+ * template's own page rather than straight into the editor.
9
+ *
10
+ * A template is not necessarily this org's. One installed from a marketplace
11
+ * listing appears here beside the locally authored ones, carries its
12
+ * publisher's provenance on the same document, and opens the same detail
13
+ * page; the chip beside its name is what distinguishes them.
14
+ *
15
+ * ## A table, on the surface's own row grammar
16
+ *
17
+ * The row opens the template, its name is ALSO a real link so it can be
18
+ * middle-clicked and copied, and editing and deleting live behind the shared
19
+ * overflow menu rather than as text buttons in the row — the same grammar the
20
+ * audiences table reads by. Delete in particular: it sat inline, one mis-click
21
+ * from the name beside it.
22
+ *
23
+ * ## Why the read is a CEILING with a probe
24
+ *
25
+ * `screens` holds every kind of screen the site has and the email ones are
26
+ * picked out here, so the cap cannot be a page: a page of the collection is
27
+ * not a page of this list. `collectionCeiling` orders on the document name,
28
+ * which is the one ordering that cannot drop a screen — `orderBy('displayName')`
29
+ * matches only documents that HAVE the field, so a screen created without a
30
+ * name would vanish rather than sort oddly. It reads one past the ceiling so
31
+ * the card can SAY when the site holds more than it drew, and the footer under
32
+ * the table pages the window the card already has.
33
+ */
34
+ export declare function EmailScreensCard(props: {
35
+ hostId: string;
36
+ /** The emails hub URL, which every template route hangs beneath. */
37
+ basePath: string;
38
+ }): import("react").JSX.Element;
39
+ export declare namespace EmailScreensCard {
40
+ var displayName: string;
41
+ }
42
+ export default EmailScreensCard;