@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.
- package/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +58 -0
- package/src/index.d.ts +35 -0
- package/src/index.js +35 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/campaign-design-create-widget.d.ts +21 -0
- package/src/lib/components/campaign-design-create-widget.js +61 -0
- package/src/lib/components/campaign-design-create-widget.js.map +1 -0
- package/src/lib/components/campaign-sender-editor-widget.d.ts +18 -0
- package/src/lib/components/campaign-sender-editor-widget.js +39 -0
- package/src/lib/components/campaign-sender-editor-widget.js.map +1 -0
- package/src/lib/components/campaign-topic-options-widget.d.ts +19 -0
- package/src/lib/components/campaign-topic-options-widget.js +48 -0
- package/src/lib/components/campaign-topic-options-widget.js.map +1 -0
- package/src/lib/components/campaign-topic-select.d.ts +32 -0
- package/src/lib/components/campaign-topic-select.js +87 -0
- package/src/lib/components/campaign-topic-select.js.map +1 -0
- package/src/lib/components/dynamic-list-rule-fields.d.ts +173 -0
- package/src/lib/components/dynamic-list-rule-fields.js +1473 -0
- package/src/lib/components/dynamic-list-rule-fields.js.map +1 -0
- package/src/lib/components/email-blocks.d.ts +111 -0
- package/src/lib/components/email-blocks.js +875 -0
- package/src/lib/components/email-blocks.js.map +1 -0
- package/src/lib/components/email-design-preview.d.ts +62 -0
- package/src/lib/components/email-design-preview.js +174 -0
- package/src/lib/components/email-design-preview.js.map +1 -0
- package/src/lib/components/email-screens-card.d.ts +42 -0
- package/src/lib/components/email-screens-card.js +277 -0
- package/src/lib/components/email-screens-card.js.map +1 -0
- package/src/lib/components/email-template-detail.d.ts +48 -0
- package/src/lib/components/email-template-detail.js +681 -0
- package/src/lib/components/email-template-detail.js.map +1 -0
- package/src/lib/components/email-topic-detail.d.ts +32 -0
- package/src/lib/components/email-topic-detail.js +293 -0
- package/src/lib/components/email-topic-detail.js.map +1 -0
- package/src/lib/components/email-topics-card.d.ts +46 -0
- package/src/lib/components/email-topics-card.js +327 -0
- package/src/lib/components/email-topics-card.js.map +1 -0
- package/src/lib/components/email-zones.d.ts +28 -0
- package/src/lib/components/email-zones.js +20 -0
- package/src/lib/components/email-zones.js.map +1 -0
- package/src/lib/components/emails-console-page.d.ts +32 -0
- package/src/lib/components/emails-console-page.js +229 -0
- package/src/lib/components/emails-console-page.js.map +1 -0
- package/src/lib/components/emails-console-sections.d.ts +36 -0
- package/src/lib/components/emails-console-sections.js +108 -0
- package/src/lib/components/emails-console-sections.js.map +1 -0
- package/src/lib/components/list-detail-card.d.ts +47 -0
- package/src/lib/components/list-detail-card.js +273 -0
- package/src/lib/components/list-detail-card.js.map +1 -0
- package/src/lib/components/list-edit-card.d.ts +11 -0
- package/src/lib/components/list-edit-card.js +287 -0
- package/src/lib/components/list-edit-card.js.map +1 -0
- package/src/lib/components/list-import-drawer.d.ts +22 -0
- package/src/lib/components/list-import-drawer.js +662 -0
- package/src/lib/components/list-import-drawer.js.map +1 -0
- package/src/lib/components/list-members-panel.d.ts +94 -0
- package/src/lib/components/list-members-panel.js +686 -0
- package/src/lib/components/list-members-panel.js.map +1 -0
- package/src/lib/components/lists-card.d.ts +28 -0
- package/src/lib/components/lists-card.js +377 -0
- package/src/lib/components/lists-card.js.map +1 -0
- package/src/lib/components/sending-domain-detail.d.ts +26 -0
- package/src/lib/components/sending-domain-detail.js +496 -0
- package/src/lib/components/sending-domain-detail.js.map +1 -0
- package/src/lib/components/sending-domains-card.d.ts +33 -0
- package/src/lib/components/sending-domains-card.js +962 -0
- package/src/lib/components/sending-domains-card.js.map +1 -0
- package/src/lib/components/sending-sender-drawer.d.ts +94 -0
- package/src/lib/components/sending-sender-drawer.js +543 -0
- package/src/lib/components/sending-sender-drawer.js.map +1 -0
- package/src/lib/components/suppressions-card.d.ts +49 -0
- package/src/lib/components/suppressions-card.js +639 -0
- package/src/lib/components/suppressions-card.js.map +1 -0
- package/src/lib/components/use-org-email-topics.d.ts +79 -0
- package/src/lib/components/use-org-email-topics.js +111 -0
- package/src/lib/components/use-org-email-topics.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +18 -0
- package/src/lib/constants/bundle-common.js +18 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/hooks/use-org-company-options.d.ts +20 -0
- package/src/lib/hooks/use-org-company-options.js +138 -0
- package/src/lib/hooks/use-org-company-options.js.map +1 -0
- package/src/lib/hooks/use-org-contact-fields.d.ts +40 -0
- package/src/lib/hooks/use-org-contact-fields.js +91 -0
- package/src/lib/hooks/use-org-contact-fields.js.map +1 -0
- package/src/lib/hooks/use-org-contact-segments.d.ts +16 -0
- package/src/lib/hooks/use-org-contact-segments.js +55 -0
- package/src/lib/hooks/use-org-contact-segments.js.map +1 -0
- package/src/lib/hooks/use-org-crm-views.d.ts +8 -0
- package/src/lib/hooks/use-org-crm-views.js +74 -0
- package/src/lib/hooks/use-org-crm-views.js.map +1 -0
- package/src/lib/hooks/use-org-lists.d.ts +8 -0
- package/src/lib/hooks/use-org-lists.js +47 -0
- package/src/lib/hooks/use-org-lists.js.map +1 -0
- package/src/lib/model/email-design-document.d.ts +52 -0
- package/src/lib/model/email-design-document.js +62 -0
- package/src/lib/model/email-design-document.js.map +1 -0
- package/src/lib/model/index.d.ts +64 -0
- package/src/lib/model/index.js +71 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/sending-domain-status.d.ts +99 -0
- package/src/lib/model/sending-domain-status.js +196 -0
- package/src/lib/model/sending-domain-status.js.map +1 -0
- package/src/lib/model/template-provenance.d.ts +113 -0
- package/src/lib/model/template-provenance.js +107 -0
- package/src/lib/model/template-provenance.js.map +1 -0
- package/src/lib/model/template-report.d.ts +158 -0
- package/src/lib/model/template-report.js +249 -0
- package/src/lib/model/template-report.js.map +1 -0
- package/src/lib/plugin.d.ts +27 -0
- package/src/lib/plugin.js +163 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server-console.d.ts +116 -0
- package/src/lib/server-console.js +422 -0
- package/src/lib/server-console.js.map +1 -0
- package/src/lib/server-email-drafts.d.ts +104 -0
- package/src/lib/server-email-drafts.js +381 -0
- package/src/lib/server-email-drafts.js.map +1 -0
- package/src/lib/server-list-gate.d.ts +183 -0
- package/src/lib/server-list-gate.js +365 -0
- package/src/lib/server-list-gate.js.map +1 -0
- package/src/lib/server-list-import.d.ts +199 -0
- package/src/lib/server-list-import.js +632 -0
- package/src/lib/server-list-import.js.map +1 -0
- package/src/lib/server-suppressions.d.ts +135 -0
- package/src/lib/server-suppressions.js +295 -0
- package/src/lib/server-suppressions.js.map +1 -0
- package/src/lib/server.d.ts +19 -0
- package/src/lib/server.js +834 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/site.d.ts +26 -0
- package/src/lib/site.js +81 -0
- package/src/lib/site.js.map +1 -0
- package/src/lib/unsubscribe-link.d.ts +311 -0
- package/src/lib/unsubscribe-link.js +398 -0
- package/src/lib/unsubscribe-link.js.map +1 -0
- package/src/lib/utils/create-email-screen.d.ts +59 -0
- package/src/lib/utils/create-email-screen.js +59 -0
- package/src/lib/utils/create-email-screen.js.map +1 -0
- package/src/lib/utils/generate-preset-id.d.ts +19 -0
- package/src/lib/utils/generate-preset-id.js +25 -0
- 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;
|