@aglyn/plugins-email 1.0.0-beta.181 → 1.0.0-beta.183
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/package.json +17 -17
- package/src/lib/components/email-blocks.d.ts +20 -0
- package/src/lib/components/email-blocks.js +135 -2
- package/src/lib/components/email-blocks.js.map +1 -1
- package/src/lib/components/email-design-preview.d.ts +7 -0
- package/src/lib/components/email-design-preview.js +79 -3
- package/src/lib/components/email-design-preview.js.map +1 -1
- package/src/lib/server-email-drafts.d.ts +11 -4
- package/src/lib/server-email-drafts.js +23 -4
- package/src/lib/server-email-drafts.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-email",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.183",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,22 +25,22 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-ui-theme": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
39
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
40
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
41
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
42
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
43
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.183",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.183",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.183",
|
|
31
|
+
"@aglyn/shared-data-types": "1.0.0-beta.183",
|
|
32
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.183",
|
|
33
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.183",
|
|
34
|
+
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.183",
|
|
35
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.183",
|
|
36
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.183",
|
|
37
|
+
"@aglyn/shared-ui-theme": "1.0.0-beta.183",
|
|
38
|
+
"@aglyn/shared-util-email": "1.0.0-beta.183",
|
|
39
|
+
"@aglyn/shared-util-http": "1.0.0-beta.183",
|
|
40
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.183",
|
|
41
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.183",
|
|
42
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.183",
|
|
43
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.183",
|
|
44
44
|
"@swc/helpers": "0.5.23"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
@@ -107,5 +107,25 @@ export interface EmailHtmlProps extends EmailBlockStyleProps {
|
|
|
107
107
|
}
|
|
108
108
|
export declare const EmailHtml: import("react").ForwardRefExoticComponent<EmailHtmlProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
109
109
|
export declare const emailHtmlSchema: Aglyn.ComponentSchema<EmailHtmlProps>;
|
|
110
|
+
/**
|
|
111
|
+
* The two bands most emails a site sends begin and end with (AGL-3287).
|
|
112
|
+
*
|
|
113
|
+
* Each drops into any email like the blocks above, and the Components page
|
|
114
|
+
* starts a reusable Header or Footer email block from these same trees, by
|
|
115
|
+
* the ids core names in `REUSABLE_EMAIL_BLOCK_STARTER_PRESET_IDS` — one tree
|
|
116
|
+
* each, wherever it is started from.
|
|
117
|
+
*
|
|
118
|
+
* The words are placeholders to replace. No merge token names the company in
|
|
119
|
+
* both kinds of email — a campaign supplies `{{org.name}}`, and no token is
|
|
120
|
+
* supplied by every one of a site's own emails — while a token an email does
|
|
121
|
+
* not supply is blanked before sending, which would leave the header with no
|
|
122
|
+
* name at all.
|
|
123
|
+
*
|
|
124
|
+
* The footer carries no unsubscribe link: a campaign send appends its own,
|
|
125
|
+
* and a second one would do the same thing twice.
|
|
126
|
+
*/
|
|
127
|
+
export declare const EMAIL_HEADER_PRESET: Aglyn.PresetSchema;
|
|
128
|
+
/** See {@link EMAIL_HEADER_PRESET}. */
|
|
129
|
+
export declare const EMAIL_FOOTER_PRESET: Aglyn.PresetSchema;
|
|
110
130
|
export declare const emailPresets: Aglyn.PresetSchema[];
|
|
111
131
|
export {};
|
|
@@ -17,7 +17,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
17
17
|
* See the License for the specific language governing permissions and
|
|
18
18
|
* limitations under the License.
|
|
19
19
|
*/ import * as Aglyn from "@aglyn/aglyn";
|
|
20
|
-
import { mdiCardTextOutline, mdiEmailOutline, mdiFormatText, mdiImageOutline, mdiMinus, mdiPackageVariantClosed, mdiArrowExpandVertical, mdiGestureTapButton, mdiCodeTags } from "@aglyn/shared-data-mdi";
|
|
20
|
+
import { mdiCardTextOutline, mdiEmailOutline, mdiFormatText, mdiImageOutline, mdiMinus, mdiPackageVariantClosed, mdiPageLayoutFooter, mdiPageLayoutHeader, mdiArrowExpandVertical, mdiGestureTapButton, mdiCodeTags } from "@aglyn/shared-data-mdi";
|
|
21
21
|
import { sanitizeAuthorHtml } from "@aglyn/aglyn/app-utils/author-html";
|
|
22
22
|
import { mergeSxProps } from "@aglyn/shared-ui-theme";
|
|
23
23
|
import Box from "@mui/material/Box";
|
|
@@ -693,6 +693,137 @@ export const emailHtmlSchema = {
|
|
|
693
693
|
}
|
|
694
694
|
]
|
|
695
695
|
};
|
|
696
|
+
/**
|
|
697
|
+
* The two bands most emails a site sends begin and end with (AGL-3287).
|
|
698
|
+
*
|
|
699
|
+
* Each drops into any email like the blocks above, and the Components page
|
|
700
|
+
* starts a reusable Header or Footer email block from these same trees, by
|
|
701
|
+
* the ids core names in `REUSABLE_EMAIL_BLOCK_STARTER_PRESET_IDS` — one tree
|
|
702
|
+
* each, wherever it is started from.
|
|
703
|
+
*
|
|
704
|
+
* The words are placeholders to replace. No merge token names the company in
|
|
705
|
+
* both kinds of email — a campaign supplies `{{org.name}}`, and no token is
|
|
706
|
+
* supplied by every one of a site's own emails — while a token an email does
|
|
707
|
+
* not supply is blanked before sending, which would leave the header with no
|
|
708
|
+
* name at all.
|
|
709
|
+
*
|
|
710
|
+
* The footer carries no unsubscribe link: a campaign send appends its own,
|
|
711
|
+
* and a second one would do the same thing twice.
|
|
712
|
+
*/ export const EMAIL_HEADER_PRESET = {
|
|
713
|
+
$id: Aglyn.REUSABLE_EMAIL_BLOCK_STARTER_PRESET_IDS.header,
|
|
714
|
+
type: Aglyn.NodeType.PRESET,
|
|
715
|
+
displayName: 'Header',
|
|
716
|
+
description: 'Your logo above your company name, for the top of an email. Pick the ' + 'logo from your media library — until you do, the picture shows nothing ' + 'in a sent email.',
|
|
717
|
+
pluginId: BUNDLE_ID,
|
|
718
|
+
category: Aglyn.ComponentCategory.BLOCKS,
|
|
719
|
+
tags: [
|
|
720
|
+
'logo',
|
|
721
|
+
'masthead',
|
|
722
|
+
'top',
|
|
723
|
+
'brand'
|
|
724
|
+
],
|
|
725
|
+
icon: {
|
|
726
|
+
path: mdiPageLayoutHeader.path,
|
|
727
|
+
sx: {
|
|
728
|
+
color: '#f57c00'
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
data: {
|
|
732
|
+
$id: null,
|
|
733
|
+
componentId: SECTION_ID,
|
|
734
|
+
pluginId: BUNDLE_ID,
|
|
735
|
+
props: {
|
|
736
|
+
align: 'center',
|
|
737
|
+
padding: 24
|
|
738
|
+
},
|
|
739
|
+
nodes: [
|
|
740
|
+
{
|
|
741
|
+
$id: null,
|
|
742
|
+
componentId: IMAGE_ID,
|
|
743
|
+
pluginId: BUNDLE_ID,
|
|
744
|
+
props: {
|
|
745
|
+
alt: 'Your company logo',
|
|
746
|
+
width: 160,
|
|
747
|
+
align: 'center'
|
|
748
|
+
}
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
$id: null,
|
|
752
|
+
componentId: TEXT_ID,
|
|
753
|
+
pluginId: BUNDLE_ID,
|
|
754
|
+
props: {
|
|
755
|
+
children: 'Your company',
|
|
756
|
+
variant: 'subheading',
|
|
757
|
+
align: 'center'
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
]
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
/**
|
|
764
|
+
* The Footer's small print, a muted gray that still reads at 5:1 on white.
|
|
765
|
+
* A literal on purpose: it is written into the sent mail's own HTML, where no
|
|
766
|
+
* theme token resolves.
|
|
767
|
+
*/ const FOOTER_TEXT_COLOR = '#6b6b6b';
|
|
768
|
+
/** See {@link EMAIL_HEADER_PRESET}. */ export const EMAIL_FOOTER_PRESET = {
|
|
769
|
+
$id: Aglyn.REUSABLE_EMAIL_BLOCK_STARTER_PRESET_IDS.footer,
|
|
770
|
+
type: Aglyn.NodeType.PRESET,
|
|
771
|
+
displayName: 'Footer',
|
|
772
|
+
description: 'Your company name and address, and why the reader gets your emails, for ' + 'the bottom of an email. Campaign emails add the unsubscribe link for you.',
|
|
773
|
+
pluginId: BUNDLE_ID,
|
|
774
|
+
category: Aglyn.ComponentCategory.BLOCKS,
|
|
775
|
+
tags: [
|
|
776
|
+
'address',
|
|
777
|
+
'bottom',
|
|
778
|
+
'contact',
|
|
779
|
+
'legal'
|
|
780
|
+
],
|
|
781
|
+
icon: {
|
|
782
|
+
path: mdiPageLayoutFooter.path,
|
|
783
|
+
sx: {
|
|
784
|
+
color: '#f57c00'
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
data: {
|
|
788
|
+
$id: null,
|
|
789
|
+
componentId: SECTION_ID,
|
|
790
|
+
pluginId: BUNDLE_ID,
|
|
791
|
+
props: {
|
|
792
|
+
align: 'center',
|
|
793
|
+
padding: 24
|
|
794
|
+
},
|
|
795
|
+
nodes: [
|
|
796
|
+
{
|
|
797
|
+
$id: null,
|
|
798
|
+
componentId: DIVIDER_ID,
|
|
799
|
+
pluginId: BUNDLE_ID,
|
|
800
|
+
props: {}
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
$id: null,
|
|
804
|
+
componentId: TEXT_ID,
|
|
805
|
+
pluginId: BUNDLE_ID,
|
|
806
|
+
props: {
|
|
807
|
+
children: 'Your company · 123 Main St, City',
|
|
808
|
+
variant: 'caption',
|
|
809
|
+
align: 'center',
|
|
810
|
+
color: FOOTER_TEXT_COLOR
|
|
811
|
+
}
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
$id: null,
|
|
815
|
+
componentId: TEXT_ID,
|
|
816
|
+
pluginId: BUNDLE_ID,
|
|
817
|
+
props: {
|
|
818
|
+
children: "You're receiving this because you're a customer of Your company.",
|
|
819
|
+
variant: 'caption',
|
|
820
|
+
align: 'center',
|
|
821
|
+
color: FOOTER_TEXT_COLOR
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
]
|
|
825
|
+
}
|
|
826
|
+
};
|
|
696
827
|
export const emailPresets = [
|
|
697
828
|
{
|
|
698
829
|
$id: generatePresetId(SECTION_ID),
|
|
@@ -869,7 +1000,9 @@ export const emailPresets = [
|
|
|
869
1000
|
pluginId: BUNDLE_ID,
|
|
870
1001
|
props: {}
|
|
871
1002
|
}
|
|
872
|
-
}
|
|
1003
|
+
},
|
|
1004
|
+
EMAIL_HEADER_PRESET,
|
|
1005
|
+
EMAIL_FOOTER_PRESET
|
|
873
1006
|
];
|
|
874
1007
|
|
|
875
1008
|
//# sourceMappingURL=email-blocks.js.map
|
|
@@ -1 +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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"}
|
|
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 mdiPageLayoutFooter,\n mdiPageLayoutHeader,\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\n/**\n * The two bands most emails a site sends begin and end with (AGL-3287).\n *\n * Each drops into any email like the blocks above, and the Components page\n * starts a reusable Header or Footer email block from these same trees, by\n * the ids core names in `REUSABLE_EMAIL_BLOCK_STARTER_PRESET_IDS` — one tree\n * each, wherever it is started from.\n *\n * The words are placeholders to replace. No merge token names the company in\n * both kinds of email — a campaign supplies `{{org.name}}`, and no token is\n * supplied by every one of a site's own emails — while a token an email does\n * not supply is blanked before sending, which would leave the header with no\n * name at all.\n *\n * The footer carries no unsubscribe link: a campaign send appends its own,\n * and a second one would do the same thing twice.\n */\nexport const EMAIL_HEADER_PRESET: Aglyn.PresetSchema = {\n $id: Aglyn.REUSABLE_EMAIL_BLOCK_STARTER_PRESET_IDS.header,\n type: Aglyn.NodeType.PRESET,\n displayName: 'Header',\n description:\n 'Your logo above your company name, for the top of an email. Pick the ' +\n 'logo from your media library — until you do, the picture shows nothing ' +\n 'in a sent email.',\n pluginId: BUNDLE_ID,\n category: Aglyn.ComponentCategory.BLOCKS,\n tags: ['logo', 'masthead', 'top', 'brand'],\n icon: { path: mdiPageLayoutHeader.path, sx: { color: '#f57c00' } },\n data: {\n $id: null,\n componentId: SECTION_ID,\n pluginId: BUNDLE_ID,\n props: { align: 'center', padding: 24 },\n nodes: [\n {\n $id: null,\n componentId: IMAGE_ID,\n pluginId: BUNDLE_ID,\n props: { alt: 'Your company logo', width: 160, align: 'center' },\n },\n {\n $id: null,\n componentId: TEXT_ID,\n pluginId: BUNDLE_ID,\n props: {\n children: 'Your company',\n variant: 'subheading',\n align: 'center',\n },\n },\n ],\n },\n}\n\n/**\n * The Footer's small print, a muted gray that still reads at 5:1 on white.\n * A literal on purpose: it is written into the sent mail's own HTML, where no\n * theme token resolves.\n */\nconst FOOTER_TEXT_COLOR = '#6b6b6b'\n\n/** See {@link EMAIL_HEADER_PRESET}. */\nexport const EMAIL_FOOTER_PRESET: Aglyn.PresetSchema = {\n $id: Aglyn.REUSABLE_EMAIL_BLOCK_STARTER_PRESET_IDS.footer,\n type: Aglyn.NodeType.PRESET,\n displayName: 'Footer',\n description:\n 'Your company name and address, and why the reader gets your emails, for ' +\n 'the bottom of an email. Campaign emails add the unsubscribe link for you.',\n pluginId: BUNDLE_ID,\n category: Aglyn.ComponentCategory.BLOCKS,\n tags: ['address', 'bottom', 'contact', 'legal'],\n icon: { path: mdiPageLayoutFooter.path, sx: { color: '#f57c00' } },\n data: {\n $id: null,\n componentId: SECTION_ID,\n pluginId: BUNDLE_ID,\n props: { align: 'center', padding: 24 },\n nodes: [\n {\n $id: null,\n componentId: DIVIDER_ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n {\n $id: null,\n componentId: TEXT_ID,\n pluginId: BUNDLE_ID,\n props: {\n children: 'Your company · 123 Main St, City',\n variant: 'caption',\n align: 'center',\n color: FOOTER_TEXT_COLOR,\n },\n },\n {\n $id: null,\n componentId: TEXT_ID,\n pluginId: BUNDLE_ID,\n props: {\n children:\n \"You're receiving this because you're a customer of Your company.\",\n variant: 'caption',\n align: 'center',\n color: FOOTER_TEXT_COLOR,\n },\n },\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 EMAIL_HEADER_PRESET,\n EMAIL_FOOTER_PRESET,\n]\n"],"names":["Aglyn","mdiCardTextOutline","mdiEmailOutline","mdiFormatText","mdiImageOutline","mdiMinus","mdiPackageVariantClosed","mdiPageLayoutFooter","mdiPageLayoutHeader","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","EMAIL_HEADER_PRESET","REUSABLE_EMAIL_BLOCK_STARTER_PRESET_IDS","header","NodeType","PRESET","BLOCKS","tags","data","componentId","nodes","FOOTER_TEXT_COLOR","EMAIL_FOOTER_PRESET","footer","emailPresets"],"mappings":"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"}
|
|
@@ -43,6 +43,13 @@ export interface EmailDesignPreviewProps {
|
|
|
43
43
|
* doing so would put a catalog read on every preview for a picture that can
|
|
44
44
|
* change between now and the send anyway.
|
|
45
45
|
*
|
|
46
|
+
* The site's reusable blocks are NOT one of the differences (AGL-3287). A
|
|
47
|
+
* shared header or footer placed in the design is grafted from the site's
|
|
48
|
+
* published components before rendering, by the same composition the send
|
|
49
|
+
* runs, so the preview shows the header the inbox will get. Only the
|
|
50
|
+
* components the design places are read, in parallel, and a design that
|
|
51
|
+
* places none costs no read.
|
|
52
|
+
*
|
|
46
53
|
* ## Sandboxed, because the HTML is tenant-authored
|
|
47
54
|
*
|
|
48
55
|
* The design is written by a site's own editors — or by a marketplace
|
|
@@ -18,8 +18,22 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
18
18
|
import { decodeStoredNodes } from "@aglyn/aglyn";
|
|
19
19
|
import { EMAIL_NODE_ROOT_ID, renderEmailHtml, renderTextEmailHtml } from "@aglyn/shared-util-email";
|
|
20
20
|
import { sanitizeAuthorHtml } from "@aglyn/aglyn/app-utils/author-html";
|
|
21
|
+
import { collectReferencedComponentIds } from "@aglyn/aglyn/app-utils/compose-reusable-components";
|
|
22
|
+
import { composeReferencedComponents } from "@aglyn/aglyn/app-utils/load-referenced-components";
|
|
23
|
+
import { useFirestore } from "@aglyn/tenant-feature-instance";
|
|
21
24
|
import { Box, Stack, Typography } from "@mui/material";
|
|
25
|
+
import { doc, getDoc } from "firebase/firestore";
|
|
22
26
|
import { useEffect, useMemo, useState } from "react";
|
|
27
|
+
/**
|
|
28
|
+
* One of the site's component documents through the CLIENT SDK, as the
|
|
29
|
+
* on-demand loader reads it. `snapshot.data()` hands a compressed definition
|
|
30
|
+
* back as a Firestore `Bytes`, which `readStoredComponentTree` decodes.
|
|
31
|
+
*/ function clientComponentReader(firestore, hostId) {
|
|
32
|
+
return async (componentId)=>{
|
|
33
|
+
const snapshot = await getDoc(doc(firestore, 'hosts', hostId, 'components', componentId));
|
|
34
|
+
return snapshot.exists() ? snapshot.data() : null;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
23
37
|
/**
|
|
24
38
|
* THE EMAIL AS AN INBOX RECEIVES IT, DRAWN SAFELY.
|
|
25
39
|
*
|
|
@@ -39,6 +53,13 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
39
53
|
* doing so would put a catalog read on every preview for a picture that can
|
|
40
54
|
* change between now and the send anyway.
|
|
41
55
|
*
|
|
56
|
+
* The site's reusable blocks are NOT one of the differences (AGL-3287). A
|
|
57
|
+
* shared header or footer placed in the design is grafted from the site's
|
|
58
|
+
* published components before rendering, by the same composition the send
|
|
59
|
+
* runs, so the preview shows the header the inbox will get. Only the
|
|
60
|
+
* components the design places are read, in parallel, and a design that
|
|
61
|
+
* places none costs no read.
|
|
62
|
+
*
|
|
42
63
|
* ## Sandboxed, because the HTML is tenant-authored
|
|
43
64
|
*
|
|
44
65
|
* The design is written by a site's own editors — or by a marketplace
|
|
@@ -51,6 +72,7 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
51
72
|
* from the console's own origin, where the sandbox attribute would be the
|
|
52
73
|
* only thing between tenant HTML and a live session.
|
|
53
74
|
*/ export function EmailDesignPreview(props) {
|
|
75
|
+
var _ref;
|
|
54
76
|
const { hostId, nodes: rawNodes, text, loading, subject, preheader, emptyMessage, note } = props;
|
|
55
77
|
/*
|
|
56
78
|
* The origin the preview's images are fetched from.
|
|
@@ -64,16 +86,65 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
64
86
|
* the size of a whole email.
|
|
65
87
|
*/ const [origin, setOrigin] = useState('');
|
|
66
88
|
useEffect(()=>setOrigin(window.location.origin), []);
|
|
67
|
-
const
|
|
89
|
+
const firestore = useFirestore();
|
|
90
|
+
const stored = useMemo(()=>{
|
|
68
91
|
var _decodeStoredNodes;
|
|
69
92
|
return (_decodeStoredNodes = decodeStoredNodes(rawNodes)) != null ? _decodeStoredNodes : {};
|
|
70
93
|
}, [
|
|
71
94
|
rawNodes
|
|
72
95
|
]);
|
|
73
|
-
|
|
96
|
+
/*
|
|
97
|
+
* THE SITE'S REUSABLE BLOCKS, GRAFTED AS THE SEND GRAFTS THEM (AGL-3287).
|
|
98
|
+
*
|
|
99
|
+
* Tagged with the map it was composed FROM, so a stale answer — the design
|
|
100
|
+
* changed while its components were being read — is never drawn as the
|
|
101
|
+
* current one. Until the answer for this map arrives the frame shows the
|
|
102
|
+
* loading line rather than the email with its header missing, which would be
|
|
103
|
+
* a preview of a message nobody sends.
|
|
104
|
+
*
|
|
105
|
+
* A failed read draws the design without the blocks it could not load, and
|
|
106
|
+
* says so: an empty space presented as the mail is the one thing a preview
|
|
107
|
+
* must not do silently.
|
|
108
|
+
*/ const placesComponents = useMemo(()=>collectReferencedComponentIds(stored).size > 0, [
|
|
109
|
+
stored
|
|
110
|
+
]);
|
|
111
|
+
const [composition, setComposition] = useState(null);
|
|
112
|
+
/** The composition of THIS map for THIS site, once there is one. */ const composed = (composition == null ? void 0 : composition.source) === stored && composition.hostId === hostId ? composition : null;
|
|
113
|
+
useEffect(()=>{
|
|
114
|
+
// Composed already: a re-render asks for nothing more, so the reads are
|
|
115
|
+
// one per design shown rather than one per render.
|
|
116
|
+
if (!placesComponents || composed) return;
|
|
117
|
+
let active = true;
|
|
118
|
+
const settle = (nodes, failed)=>{
|
|
119
|
+
if (active) setComposition({
|
|
120
|
+
source: stored,
|
|
121
|
+
hostId,
|
|
122
|
+
nodes,
|
|
123
|
+
failed
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
composeReferencedComponents(stored, clientComponentReader(firestore, hostId)).then((nodes)=>settle(nodes, false)).catch((error)=>{
|
|
127
|
+
console.error(error);
|
|
128
|
+
settle(stored, true);
|
|
129
|
+
});
|
|
130
|
+
return ()=>{
|
|
131
|
+
active = false;
|
|
132
|
+
};
|
|
133
|
+
}, [
|
|
134
|
+
stored,
|
|
135
|
+
placesComponents,
|
|
136
|
+
composed,
|
|
137
|
+
firestore,
|
|
138
|
+
hostId
|
|
139
|
+
]);
|
|
140
|
+
const composing = placesComponents && !composed;
|
|
141
|
+
/** What is drawn: the design with its blocks grafted; `null` while grafting. */ const nodes = !placesComponents ? stored : (_ref = composed == null ? void 0 : composed.nodes) != null ? _ref : null;
|
|
142
|
+
const productBlocks = useMemo(()=>Object.values(nodes != null ? nodes : {}).filter((node)=>(node == null ? void 0 : node.componentId) === 'emailProduct').length, [
|
|
74
143
|
nodes
|
|
75
144
|
]);
|
|
76
145
|
const rendered = useMemo(()=>{
|
|
146
|
+
// Still reading the components this design places.
|
|
147
|
+
if (!nodes) return null;
|
|
77
148
|
if (!Object.keys(nodes).length) {
|
|
78
149
|
/*
|
|
79
150
|
* A plain-text message renders through the send path's own text
|
|
@@ -116,7 +187,7 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
116
187
|
return /*#__PURE__*/ _jsx(Typography, {
|
|
117
188
|
variant: "body2",
|
|
118
189
|
color: "text.secondary",
|
|
119
|
-
children: loading ? 'Loading this email…' : emptyMessage
|
|
190
|
+
children: loading || composing ? 'Loading this email…' : emptyMessage
|
|
120
191
|
});
|
|
121
192
|
}
|
|
122
193
|
return /*#__PURE__*/ _jsxs(Stack, {
|
|
@@ -160,6 +231,11 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
160
231
|
color: "text.secondary",
|
|
161
232
|
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
233
|
}) : null,
|
|
234
|
+
(composed == null ? void 0 : composed.failed) ? /*#__PURE__*/ _jsx(Typography, {
|
|
235
|
+
variant: "caption",
|
|
236
|
+
color: "warning.main",
|
|
237
|
+
children: 'A reusable block in this email could not be loaded, so it is ' + 'not drawn here.'
|
|
238
|
+
}) : null,
|
|
163
239
|
note ? /*#__PURE__*/ _jsx(Typography, {
|
|
164
240
|
variant: "caption",
|
|
165
241
|
color: "text.secondary",
|
|
@@ -1 +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"}
|
|
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 { collectReferencedComponentIds } from '@aglyn/aglyn/app-utils/compose-reusable-components'\nimport {\n composeReferencedComponents,\n type ReadComponentDocument,\n type StoredComponentDocument,\n} from '@aglyn/aglyn/app-utils/load-referenced-components'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { Box, Stack, Typography } from '@mui/material'\nimport { doc, getDoc, type Firestore } from 'firebase/firestore'\nimport { useEffect, useMemo, useState } from 'react'\n\n/**\n * One of the site's component documents through the CLIENT SDK, as the\n * on-demand loader reads it. `snapshot.data()` hands a compressed definition\n * back as a Firestore `Bytes`, which `readStoredComponentTree` decodes.\n */\nfunction clientComponentReader(\n firestore: Firestore,\n hostId: string,\n): ReadComponentDocument {\n return async (componentId) => {\n const snapshot = await getDoc(\n doc(firestore, 'hosts', hostId, 'components', componentId),\n )\n return snapshot.exists()\n ? (snapshot.data() as StoredComponentDocument)\n : null\n }\n}\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 * The site's reusable blocks are NOT one of the differences (AGL-3287). A\n * shared header or footer placed in the design is grafted from the site's\n * published components before rendering, by the same composition the send\n * runs, so the preview shows the header the inbox will get. Only the\n * components the design places are read, in parallel, and a design that\n * places none costs no read.\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 firestore = useFirestore()\n const stored = useMemo(\n () => decodeStoredNodes<Record<string, any>>(rawNodes) ?? {},\n [rawNodes],\n )\n /*\n * THE SITE'S REUSABLE BLOCKS, GRAFTED AS THE SEND GRAFTS THEM (AGL-3287).\n *\n * Tagged with the map it was composed FROM, so a stale answer — the design\n * changed while its components were being read — is never drawn as the\n * current one. Until the answer for this map arrives the frame shows the\n * loading line rather than the email with its header missing, which would be\n * a preview of a message nobody sends.\n *\n * A failed read draws the design without the blocks it could not load, and\n * says so: an empty space presented as the mail is the one thing a preview\n * must not do silently.\n */\n const placesComponents = useMemo(\n () => collectReferencedComponentIds(stored).size > 0,\n [stored],\n )\n const [composition, setComposition] = useState<{\n source: Record<string, any>\n hostId: string\n nodes: Record<string, any>\n failed: boolean\n } | null>(null)\n /** The composition of THIS map for THIS site, once there is one. */\n const composed =\n composition?.source === stored && composition.hostId === hostId\n ? composition\n : null\n useEffect(() => {\n // Composed already: a re-render asks for nothing more, so the reads are\n // one per design shown rather than one per render.\n if (!placesComponents || composed) return\n let active = true\n const settle = (nodes: Record<string, any>, failed: boolean) => {\n if (active) setComposition({ source: stored, hostId, nodes, failed })\n }\n composeReferencedComponents(\n stored,\n clientComponentReader(firestore, hostId),\n )\n .then((nodes) => settle(nodes, false))\n .catch((error) => {\n console.error(error)\n settle(stored, true)\n })\n return () => {\n active = false\n }\n }, [stored, placesComponents, composed, firestore, hostId])\n const composing = placesComponents && !composed\n /** What is drawn: the design with its blocks grafted; `null` while grafting. */\n const nodes: Record<string, any> | null = !placesComponents\n ? stored\n : (composed?.nodes ?? null)\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 // Still reading the components this design places.\n if (!nodes) return null\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 || composing ? '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 {composed?.failed ? (\n <Typography variant=\"caption\" color=\"warning.main\">\n {'A reusable block in this email could not be loaded, so it is ' +\n 'not drawn here.'}\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","collectReferencedComponentIds","composeReferencedComponents","useFirestore","Box","Stack","Typography","doc","getDoc","useEffect","useMemo","useState","clientComponentReader","firestore","hostId","componentId","snapshot","exists","data","EmailDesignPreview","props","nodes","rawNodes","text","loading","subject","preheader","emptyMessage","note","origin","setOrigin","window","location","stored","placesComponents","size","composition","setComposition","composed","source","active","settle","failed","then","catch","error","console","composing","productBlocks","Object","values","filter","node","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":"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"}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type PluginDraftCheck, type PluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts';
|
|
17
|
+
import { type PluginDraftCheck, type PluginDraftCheckContext, type PluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts';
|
|
18
18
|
/**
|
|
19
19
|
* AN EMAIL DESIGN ANOTHER PLUGIN ASKS FOR (AGL-2912).
|
|
20
20
|
*
|
|
@@ -81,13 +81,20 @@ export type EmailDesignContentRead = {
|
|
|
81
81
|
};
|
|
82
82
|
/** The content as the writer stores it, or every problem that stops it. */
|
|
83
83
|
export declare function readEmailDesignContent(content: Readonly<Record<string, unknown>>): EmailDesignContentRead;
|
|
84
|
+
/** A placement still standing once the site's components are grafted. */
|
|
85
|
+
export declare const EMAIL_DESIGN_UNRESOLVED_BLOCK_PROBLEM = "A reusable block names a component this site has not published, so it would send empty";
|
|
84
86
|
/**
|
|
85
87
|
* Whether content renders as an email: well-formed, every link one an inbox
|
|
86
88
|
* can open, and a message once rendered through the send path's own renderer.
|
|
89
|
+
*
|
|
90
|
+
* Judged COMPOSED (AGL-3287). The reusable blocks the design places — a shared
|
|
91
|
+
* header, a footer — are grafted from `context.components` first, as every send
|
|
92
|
+
* grafts them, so a link inside a footer is checked like any other and a
|
|
93
|
+
* header's text counts toward the message. A placement still standing after
|
|
94
|
+
* that names a component the caller could not supply, and would go out as
|
|
95
|
+
* nothing; it is refused rather than passed as a design that renders.
|
|
87
96
|
*/
|
|
88
|
-
export declare function checkEmailDesignContent(content: Readonly<Record<string, unknown>>, context:
|
|
89
|
-
hostId: string;
|
|
90
|
-
}): PluginDraftCheck;
|
|
97
|
+
export declare function checkEmailDesignContent(content: Readonly<Record<string, unknown>>, context: PluginDraftCheckContext): PluginDraftCheck;
|
|
91
98
|
export interface EmailDesignDraftWriterDeps {
|
|
92
99
|
/** The Admin SDK handle; specs hand in a double. */
|
|
93
100
|
firestore?: () => Firestore;
|
|
@@ -22,6 +22,7 @@ import { NON_PAGE_SCREEN_MAX_PER_HOST, nonPageScreenIds, SCREEN_KIND_EMAIL } fro
|
|
|
22
22
|
import { encodeStoredNodes } from "@aglyn/aglyn/app-utils/stored-nodes";
|
|
23
23
|
import { CANVAS_ROOT_ELEMENT_ID } from "@aglyn/aglyn/foundation/constants/canvas";
|
|
24
24
|
import { registerPluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
|
|
25
|
+
import { composeReusableComponentNodes, REUSABLE_INSTANCE_COMPONENT_ID } from "@aglyn/aglyn/app-utils/compose-reusable-components";
|
|
25
26
|
import { hasSafeLinkScheme } from "@aglyn/shared-util-http/safe-url-scheme";
|
|
26
27
|
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
27
28
|
import { emailPlainTextState, renderRecipientEmail } from "@aglyn/aglyn/app-utils/recipient-email-render";
|
|
@@ -155,16 +156,32 @@ function isRecord(value) {
|
|
|
155
156
|
}
|
|
156
157
|
return hasSafeLinkScheme(href) ? null : `${label} links to something that is not a web address`;
|
|
157
158
|
}
|
|
159
|
+
/** A placement still standing once the site's components are grafted. */ export const EMAIL_DESIGN_UNRESOLVED_BLOCK_PROBLEM = 'A reusable block names a component this site has not published, so it would send empty';
|
|
158
160
|
/**
|
|
159
161
|
* Whether content renders as an email: well-formed, every link one an inbox
|
|
160
162
|
* can open, and a message once rendered through the send path's own renderer.
|
|
163
|
+
*
|
|
164
|
+
* Judged COMPOSED (AGL-3287). The reusable blocks the design places — a shared
|
|
165
|
+
* header, a footer — are grafted from `context.components` first, as every send
|
|
166
|
+
* grafts them, so a link inside a footer is checked like any other and a
|
|
167
|
+
* header's text counts toward the message. A placement still standing after
|
|
168
|
+
* that names a component the caller could not supply, and would go out as
|
|
169
|
+
* nothing; it is refused rather than passed as a design that renders.
|
|
161
170
|
*/ export function checkEmailDesignContent(content, context) {
|
|
162
171
|
const read = readEmailDesignContent(content);
|
|
163
172
|
if (read.ok === false) return read;
|
|
164
|
-
const { value
|
|
173
|
+
const { value } = read;
|
|
174
|
+
// Property values and attribute overrides apply in the graft; style
|
|
175
|
+
// overrides land in `sx`, which the mail renderer never reads.
|
|
176
|
+
const nodes = composeReusableComponentNodes(value.nodes, context.components);
|
|
177
|
+
const reachable = nodes === value.nodes ? read.reachable : reachableIds(nodes).ids;
|
|
165
178
|
const problems = [];
|
|
166
179
|
for (const id of reachable){
|
|
167
|
-
const node =
|
|
180
|
+
const node = nodes[id];
|
|
181
|
+
if (node.componentId === REUSABLE_INSTANCE_COMPONENT_ID) {
|
|
182
|
+
problems.push(EMAIL_DESIGN_UNRESOLVED_BLOCK_PROBLEM);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
168
185
|
if (node.componentId === 'emailButton') {
|
|
169
186
|
var _node_props;
|
|
170
187
|
const problem = linkProblem('A button', (_node_props = node.props) == null ? void 0 : _node_props['href'], true);
|
|
@@ -182,7 +199,7 @@ function isRecord(value) {
|
|
|
182
199
|
content: {
|
|
183
200
|
mode: 'design',
|
|
184
201
|
template: {
|
|
185
|
-
nodes
|
|
202
|
+
nodes,
|
|
186
203
|
subject: value.subject,
|
|
187
204
|
preheader: value.preheader
|
|
188
205
|
}
|
|
@@ -205,7 +222,9 @@ function isRecord(value) {
|
|
|
205
222
|
facts: {
|
|
206
223
|
htmlBytes: new TextEncoder().encode(rendered.html).length,
|
|
207
224
|
messageText: rendered.messageText,
|
|
208
|
-
blocks
|
|
225
|
+
// The blocks the design itself holds, as stored — a placed component
|
|
226
|
+
// counts once, however many blocks it expands into.
|
|
227
|
+
blocks: read.reachable.size,
|
|
209
228
|
// No part is written by hand, so the text a campaign sends is the design's own.
|
|
210
229
|
plainText: emailPlainTextState({})
|
|
211
230
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/email/src/lib/server-email-drafts.ts"],"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 { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { uniqueDuplicateName } from '@aglyn/aglyn/app-utils/duplicate-resource'\nimport { nameSearchKey } from '@aglyn/aglyn/app-utils/name-search'\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport {\n NON_PAGE_SCREEN_MAX_PER_HOST,\n nonPageScreenIds,\n SCREEN_KIND_EMAIL,\n type BillableScreenSource,\n} from '@aglyn/aglyn/app-utils/screen-route'\nimport { encodeStoredNodes } from '@aglyn/aglyn/app-utils/stored-nodes'\nimport { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport {\n registerPluginResourceDraftWriter,\n type PluginDraftCheck,\n type PluginDraftRecord,\n type PluginDraftRefusal,\n type PluginDraftWrite,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { hasSafeLinkScheme } from '@aglyn/shared-util-http/safe-url-scheme'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n emailPlainTextState,\n renderRecipientEmail,\n} from '@aglyn/aglyn/app-utils/recipient-email-render'\nimport { emailDesignDocuments } from './model/email-design-document'\n\n/**\n * AN EMAIL DESIGN ANOTHER PLUGIN ASKS FOR (AGL-2912).\n *\n * The draft writer this plugin registers for the `emailDesign` resource on the\n * core's resource-drafts seam. A plugin that produces an email — a generator\n * working from a brief, an importer bringing templates over — asks for the\n * writer by name and gets this plugin's rules, never a copy of them:\n *\n * - THE DOCUMENTS are the pair `createEmailScreen` makes, built by the same\n * function (`emailDesignDocuments`): a `kind: 'email'` screen and its first\n * version. So the design lists on the Templates tab, opens in the besigner\n * and is offered by the campaign composer like one a person made. The\n * screen carries the subject and preheader a campaign built from it opens\n * on, and the other lines worth trying beside them.\n * - THE ROOM is the flat platform ceiling on non-page screens, met inside the\n * transaction with the arithmetic `/api/hosts/resources` and the duplicate\n * module use, and refused in their words.\n * - THE ROLE is the resources route's: a member who may write the site's\n * content.\n * - THE CHECK renders the design through `renderRecipientEmail`, the function\n * the send path and the composer's preview both call, so content that\n * passes is content that would render in both.\n *\n * Nothing is sent, scheduled or routed. A design is inert until a campaign\n * names it, and the writer touches the two new documents and nothing else.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The resource name this writer is registered under. */\nexport const EMAIL_DESIGN_DRAFT_RESOURCE = 'emailDesign'\n\n/** The id `plugins.config.json` registers this plugin under. */\nconst EMAIL_PLUGIN_ID = 'email'\n\n/** A design's name when the caller asks for none, as the composer's create names one. */\nexport const EMAIL_DESIGN_DEFAULT_NAME = 'Untitled email'\n\n/** The longest subject or preheader stored, the send route's header cap. */\nexport const EMAIL_DESIGN_HEADER_MAX_CHARS = 200\n\n/** The most alternative subject lines or preheaders a design keeps. */\nexport const EMAIL_DESIGN_MAX_VARIANTS = 10\n\n/** The resources route's refusal at the non-page ceiling. */\nexport const EMAIL_DESIGN_LIMIT_REFUSAL =\n 'This site is at its limit of ' +\n `${NON_PAGE_SCREEN_MAX_PER_HOST} email and template screens — ` +\n 'delete some to make room'\n\n/** The resources route's refusal for a member who may not write the site. */\nexport const EMAIL_DESIGN_ROLE_REFUSAL = 'Editing requires the editor role'\n\n/*\n * What the check renders against. No mail is sent and nothing is fetched: the\n * origin makes the renderer keep images and links it would drop without one,\n * and the recipient fills the merge tokens a real send would fill.\n */\nconst CHECK_SITE_BASE = 'https://example.com'\nconst CHECK_RECIPIENT = { email: 'reader@example.com', name: 'Sample Reader' }\n\n/** One node of a design, as far as the writer reads it. */\nexport interface EmailDesignNode {\n componentId?: unknown\n props?: Record<string, unknown>\n nodes?: unknown\n}\n\n/**\n * What a caller sends as `content`: the node map rooted at the canvas root,\n * the subject and preheader, and the alternatives to them.\n */\nexport interface EmailDesignDraftContent {\n nodes: Record<string, EmailDesignNode>\n subject: string\n preheader: string\n subjectVariants: string[]\n preheaderVariants: string[]\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** A header value on one line: controls and runs of space folded to one space. */\nfunction headerLine(value: unknown): string {\n return String(value ?? '').replace(/[\\p{Cc}\\s]+/gu, ' ').trim()\n}\n\n/** Alternatives as the design keeps them: on one line, unique, non-empty, bounded. */\nfunction variantsOf(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n const seen = new Set<string>()\n const lines: string[] = []\n for (const raw of value) {\n const line = headerLine(raw)\n const key = line.toLowerCase()\n if (!line || seen.has(key)) continue\n seen.add(key)\n lines.push(line)\n }\n return lines.slice(0, EMAIL_DESIGN_MAX_VARIANTS)\n}\n\n/** The ids reachable from the canvas root, or the structural problem that stops the walk. */\nfunction reachableIds(nodes: Record<string, EmailDesignNode>): { ids: Set<string>; problems: string[] } {\n const ids = new Set<string>()\n const problems = new Set<string>()\n const stack = [CANVAS_ROOT_ELEMENT_ID]\n while (stack.length) {\n const id = stack.pop() as string\n if (ids.has(id)) {\n problems.add('A block is placed in the design more than once')\n continue\n }\n const node = nodes[id]\n if (!isRecord(node)) {\n problems.add('The design lists a block it does not hold')\n continue\n }\n ids.add(id)\n for (const child of Array.isArray(node.nodes) ? node.nodes : []) {\n if (typeof child === 'string') stack.push(child)\n }\n }\n return { ids, problems: [...problems] }\n}\n\nexport type EmailDesignContentRead =\n | { ok: true; value: EmailDesignDraftContent; reachable: Set<string> }\n | { ok: false; problems: string[] }\n\n/** The content as the writer stores it, or every problem that stops it. */\nexport function readEmailDesignContent(content: Readonly<Record<string, unknown>>): EmailDesignContentRead {\n const nodes = content['nodes']\n if (!isRecord(nodes) || !Object.keys(nodes).length) {\n return { ok: false, problems: ['The design has no blocks'] }\n }\n const map = nodes as Record<string, EmailDesignNode>\n if (!isRecord(map[CANVAS_ROOT_ELEMENT_ID])) {\n return { ok: false, problems: ['The design has no root to render from'] }\n }\n const { ids, problems } = reachableIds(map)\n const stranded = Object.keys(map).filter((id) => !ids.has(id)).length\n if (stranded) {\n problems.push(\n `${stranded} ${stranded === 1 ? 'block is' : 'blocks are'} not placed in the design, so ${\n stranded === 1 ? 'it' : 'they'\n } would not be sent`,\n )\n }\n const value: EmailDesignDraftContent = {\n nodes: map,\n subject: headerLine(content['subject']),\n preheader: headerLine(content['preheader']),\n subjectVariants: variantsOf(content['subjectVariants']),\n preheaderVariants: variantsOf(content['preheaderVariants']),\n }\n const long = (lines: string[]) => lines.some((line) => line.length > EMAIL_DESIGN_HEADER_MAX_CHARS)\n if (long([value.subject, ...value.subjectVariants])) {\n problems.push(`A subject line is longer than ${EMAIL_DESIGN_HEADER_MAX_CHARS} characters`)\n }\n if (long([value.preheader, ...value.preheaderVariants])) {\n problems.push(`A preheader is longer than ${EMAIL_DESIGN_HEADER_MAX_CHARS} characters`)\n }\n return problems.length ? { ok: false, problems } : { ok: true, value, reachable: ids }\n}\n\n/** Why a link on a block would not open from an inbox, or `null`. */\nfunction linkProblem(label: string, raw: unknown, required: boolean): string | null {\n const written = String(raw ?? '').trim()\n if (/\\{\\{\\s*contact\\./.test(written)) {\n return `${label} puts the reader's own details in its link, which is never filled`\n }\n const href = written\n .replace(/\\{\\{\\s*site\\.url\\s*\\}\\}/g, CHECK_SITE_BASE)\n .replace(/\\{\\{\\s*unsubscribeUrl\\s*\\}\\}/g, `${CHECK_SITE_BASE}/preferences`)\n if (!href || href === '#') return required ? `${label} links nowhere` : null\n if (href.startsWith('/') && !href.startsWith('//')) {\n return `${label} links to a path with no site in front of it, which an inbox cannot open`\n }\n return hasSafeLinkScheme(href) ? null : `${label} links to something that is not a web address`\n}\n\n/**\n * Whether content renders as an email: well-formed, every link one an inbox\n * can open, and a message once rendered through the send path's own renderer.\n */\nexport function checkEmailDesignContent(\n content: Readonly<Record<string, unknown>>,\n context: { hostId: string },\n): PluginDraftCheck {\n const read = readEmailDesignContent(content)\n if (read.ok === false) return read\n const { value, reachable } = read\n const problems: string[] = []\n for (const id of reachable) {\n const node = value.nodes[id]\n if (node.componentId === 'emailButton') {\n const problem = linkProblem('A button', node.props?.['href'], true)\n if (problem) problems.push(problem)\n }\n if (node.componentId === 'emailImage') {\n const problem = linkProblem('An image', node.props?.['href'], false)\n if (problem) problems.push(problem)\n }\n }\n const rendered = renderRecipientEmail({\n subject: value.subject,\n preheader: value.preheader,\n content: {\n mode: 'design',\n template: { nodes: value.nodes, subject: value.subject, preheader: value.preheader },\n },\n recipient: CHECK_RECIPIENT,\n siteBase: CHECK_SITE_BASE,\n hostId: context.hostId,\n })\n if (!rendered.messageText.trim()) {\n problems.push('The design renders no message: it needs at least one block of text')\n }\n if (problems.length) return { ok: false, problems: [...new Set(problems)] }\n return {\n ok: true,\n facts: {\n htmlBytes: new TextEncoder().encode(rendered.html).length,\n messageText: rendered.messageText,\n blocks: reachable.size,\n // No part is written by hand, so the text a campaign sends is the design's own.\n plainText: emailPlainTextState({}),\n },\n }\n}\n\ninterface ScreenRow extends BillableScreenSource {\n name: string\n}\n\nasync function readScreenRows(\n hostRef: FirebaseFirestore.DocumentReference,\n read: (query: FirebaseFirestore.Query) => Promise<FirebaseFirestore.QuerySnapshot>,\n): Promise<ScreenRow[]> {\n const snapshot = await read(hostRef.collection('screens').select('displayName', 'kind', 'deletedAt'))\n return snapshot.docs.map((doc) => ({\n id: doc.id,\n kind: doc.get('kind'),\n deletedAt: doc.get('deletedAt'),\n name: String(doc.get('displayName') ?? ''),\n }))\n}\n\nfunction roleRefusal(host: FirebaseFirestore.DocumentSnapshot, uid: string): PluginDraftRefusal | null {\n const role = (host.get('memberRoles') ?? {})[uid]\n return hostRoleCanWrite(role) ? null : { status: 403, error: EMAIL_DESIGN_ROLE_REFUSAL }\n}\n\nfunction roomRefusal(rows: ScreenRow[], host: FirebaseFirestore.DocumentSnapshot): PluginDraftRefusal | null {\n const used = nonPageScreenIds(rows, host.get('screens') as never).size\n return used >= NON_PAGE_SCREEN_MAX_PER_HOST ? { status: 403, error: EMAIL_DESIGN_LIMIT_REFUSAL } : null\n}\n\nfunction strings(value: unknown): string[] {\n return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : []\n}\n\nfunction recordOf(screen: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n const versionId = screen.get('versionId')\n return {\n id: screen.id,\n name: String(screen.get('displayName') ?? ''),\n versionId: typeof versionId === 'string' && versionId ? versionId : null,\n facts: {\n subject: String(screen.get('emailSubject') ?? ''),\n preheader: String(screen.get('emailPreheader') ?? ''),\n subjectVariants: strings(screen.get('emailSubjectVariants')),\n preheaderVariants: strings(screen.get('emailPreheaderVariants')),\n },\n }\n}\n\n/** The subject, preheader and alternatives as the screen document stores them. */\nfunction copyFields(value: EmailDesignDraftContent): Record<string, unknown> {\n return {\n ...(value.subject ? { emailSubject: value.subject } : {}),\n ...(value.preheader ? { emailPreheader: value.preheader } : {}),\n ...(value.subjectVariants.length ? { emailSubjectVariants: value.subjectVariants } : {}),\n ...(value.preheaderVariants.length ? { emailPreheaderVariants: value.preheaderVariants } : {}),\n }\n}\n\nexport interface EmailDesignDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n /** Mints the first version's id. */\n mintId?: () => string\n}\n\nexport function createEmailDesignDraftWriter(\n deps: EmailDesignDraftWriterDeps = {},\n): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n const mintId = deps.mintId ?? createResourceUid\n return {\n refusal: async (context) => {\n const hostRef = firestore().collection('hosts').doc(context.hostId)\n const host = await hostRef.get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n return (\n roleRefusal(host, context.uid) ??\n roomRefusal(await readScreenRows(hostRef, (query) => query.get()), host)\n )\n },\n\n check: checkEmailDesignContent,\n\n read: async ({ hostId, id }) => {\n const screen = await firestore().collection('hosts').doc(hostId).collection('screens').doc(id).get()\n return screen.exists && screen.get('kind') === SCREEN_KIND_EMAIL ? recordOf(screen) : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n const read = readEmailDesignContent(request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n const packed = encodeStoredNodes(read.value.nodes)\n if (!packed) return { ok: false, status: 400, error: 'The design has no blocks' }\n const db = firestore()\n const hostRef = db.collection('hosts').doc(request.hostId)\n const screenRef = hostRef.collection('screens').doc(request.id)\n const versionId = mintId()\n return db.runTransaction(async (tx): Promise<PluginDraftWrite> => {\n // Every read before any write, which Firestore requires.\n const [host, existing, rows] = await Promise.all([\n tx.get(hostRef),\n tx.get(screenRef),\n readScreenRows(hostRef, (query) => tx.get(query)),\n ])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n if (existing.exists) {\n return existing.get('kind') === SCREEN_KIND_EMAIL\n ? { ok: true, replayed: true, ...recordOf(existing) }\n : { ok: false, status: 409, error: 'That id already names a screen that is not an email design' }\n }\n const refusal = roleRefusal(host, request.uid) ?? roomRefusal(rows, host)\n if (refusal) return { ok: false, ...refusal }\n const name = uniqueDuplicateName(\n headerLine(request.name) || EMAIL_DESIGN_DEFAULT_NAME,\n rows.filter((row) => row.deletedAt == null).map((row) => row.name),\n )\n const { screen, version } = emailDesignDocuments({\n screenId: request.id,\n versionId,\n displayName: name,\n nodes: read.value.nodes,\n })\n const stamps = { createdAt: request.now, updatedAt: request.now, createdBy: request.uid }\n const copy = copyFields(read.value)\n tx.create(screenRef, { ...screen, nameLower: nameSearchKey(name), ...copy, ...stamps })\n tx.create(screenRef.collection('versions').doc(versionId), {\n ...version,\n nodes: Buffer.from(packed),\n hostId: request.hostId,\n ...stamps,\n })\n return {\n ok: true,\n replayed: false,\n id: request.id,\n name,\n versionId,\n facts: {\n subject: read.value.subject,\n preheader: read.value.preheader,\n subjectVariants: read.value.subjectVariants,\n preheaderVariants: read.value.preheaderVariants,\n },\n }\n })\n },\n }\n}\n\nexport const emailDesignDraftWriter = createEmailDesignDraftWriter()\n\n/**\n * Registers the writer; the console surface calls it, since only the console\n * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.\n */\nexport function registerEmailDesignDraftWriter(): void {\n registerPluginResourceDraftWriter(EMAIL_DESIGN_DRAFT_RESOURCE, emailDesignDraftWriter, {\n pluginId: EMAIL_PLUGIN_ID,\n })\n}\n"],"names":["createResourceUid","uniqueDuplicateName","nameSearchKey","hostRoleCanWrite","NON_PAGE_SCREEN_MAX_PER_HOST","nonPageScreenIds","SCREEN_KIND_EMAIL","encodeStoredNodes","CANVAS_ROOT_ELEMENT_ID","registerPluginResourceDraftWriter","hasSafeLinkScheme","firebaseAdmin","emailPlainTextState","renderRecipientEmail","emailDesignDocuments","EMAIL_DESIGN_DRAFT_RESOURCE","EMAIL_PLUGIN_ID","EMAIL_DESIGN_DEFAULT_NAME","EMAIL_DESIGN_HEADER_MAX_CHARS","EMAIL_DESIGN_MAX_VARIANTS","EMAIL_DESIGN_LIMIT_REFUSAL","EMAIL_DESIGN_ROLE_REFUSAL","CHECK_SITE_BASE","CHECK_RECIPIENT","email","name","isRecord","value","Array","isArray","headerLine","String","replace","trim","variantsOf","seen","Set","lines","raw","line","key","toLowerCase","has","add","push","slice","reachableIds","nodes","ids","problems","stack","length","id","pop","node","child","readEmailDesignContent","content","Object","keys","ok","map","stranded","filter","subject","preheader","subjectVariants","preheaderVariants","long","some","reachable","linkProblem","label","required","written","test","href","startsWith","checkEmailDesignContent","context","read","componentId","problem","props","rendered","mode","template","recipient","siteBase","hostId","messageText","facts","htmlBytes","TextEncoder","encode","html","blocks","size","plainText","readScreenRows","hostRef","snapshot","collection","select","docs","doc","kind","get","deletedAt","roleRefusal","host","uid","role","status","error","roomRefusal","rows","used","strings","entry","recordOf","screen","versionId","copyFields","emailSubject","emailPreheader","emailSubjectVariants","emailPreheaderVariants","createEmailDesignDraftWriter","deps","firestore","app","mintId","refusal","exists","query","check","write","request","packed","db","screenRef","runTransaction","tx","existing","Promise","all","replayed","row","version","screenId","displayName","stamps","createdAt","now","updatedAt","createdBy","copy","create","nameLower","Buffer","from","emailDesignDraftWriter","registerEmailDesignDraftWriter","pluginId"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,iBAAiB,QAAQ,6CAA4C;AAC9E,SAASC,mBAAmB,QAAQ,4CAA2C;AAC/E,SAASC,aAAa,QAAQ,qCAAoC;AAClE,SAASC,gBAAgB,QAAQ,uCAAsC;AACvE,SACEC,4BAA4B,EAC5BC,gBAAgB,EAChBC,iBAAiB,QAEZ,sCAAqC;AAC5C,SAASC,iBAAiB,QAAQ,sCAAqC;AACvE,SAASC,sBAAsB,QAAQ,2CAA0C;AACjF,SACEC,iCAAiC,QAM5B,qDAAoD;AAC3D,SAASC,iBAAiB,QAAQ,0CAAyC;AAC3E,SAASC,aAAa,QAAQ,2BAA0B;AACxD,SACEC,mBAAmB,EACnBC,oBAAoB,QACf,gDAA+C;AACtD,SAASC,oBAAoB,QAAQ,mCAA+B;AA+BpE,uDAAuD,GACvD,OAAO,MAAMC,8BAA8B,cAAa;AAExD,8DAA8D,GAC9D,MAAMC,kBAAkB;AAExB,uFAAuF,GACvF,OAAO,MAAMC,4BAA4B,iBAAgB;AAEzD,0EAA0E,GAC1E,OAAO,MAAMC,gCAAgC,IAAG;AAEhD,qEAAqE,GACrE,OAAO,MAAMC,4BAA4B,GAAE;AAE3C,2DAA2D,GAC3D,OAAO,MAAMC,6BACX,kCACA,GAAGhB,6BAA6B,8BAA8B,CAAC,GAC/D,2BAA0B;AAE5B,2EAA2E,GAC3E,OAAO,MAAMiB,4BAA4B,mCAAkC;AAE3E;;;;CAIC,GACD,MAAMC,kBAAkB;AACxB,MAAMC,kBAAkB;IAAEC,OAAO;IAAsBC,MAAM;AAAgB;AAqB7E,SAASC,SAASC,KAAc;IAC9B,OAAO,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAACC,MAAMC,OAAO,CAACF;AACvE;AAEA,gFAAgF,GAChF,SAASG,WAAWH,KAAc;IAChC,OAAOI,OAAOJ,gBAAAA,QAAS,IAAIK,OAAO,CAAC,sDAAiB,KAAKC,IAAI;AAC/D;AAEA,oFAAoF,GACpF,SAASC,WAAWP,KAAc;IAChC,IAAI,CAACC,MAAMC,OAAO,CAACF,QAAQ,OAAO,EAAE;IACpC,MAAMQ,OAAO,IAAIC;IACjB,MAAMC,QAAkB,EAAE;IAC1B,KAAK,MAAMC,OAAOX,MAAO;QACvB,MAAMY,OAAOT,WAAWQ;QACxB,MAAME,MAAMD,KAAKE,WAAW;QAC5B,IAAI,CAACF,QAAQJ,KAAKO,GAAG,CAACF,MAAM;QAC5BL,KAAKQ,GAAG,CAACH;QACTH,MAAMO,IAAI,CAACL;IACb;IACA,OAAOF,MAAMQ,KAAK,CAAC,GAAG1B;AACxB;AAEA,2FAA2F,GAC3F,SAAS2B,aAAaC,KAAsC;IAC1D,MAAMC,MAAM,IAAIZ;IAChB,MAAMa,WAAW,IAAIb;IACrB,MAAMc,QAAQ;QAAC1C;KAAuB;IACtC,MAAO0C,MAAMC,MAAM,CAAE;QACnB,MAAMC,KAAKF,MAAMG,GAAG;QACpB,IAAIL,IAAIN,GAAG,CAACU,KAAK;YACfH,SAASN,GAAG,CAAC;YACb;QACF;QACA,MAAMW,OAAOP,KAAK,CAACK,GAAG;QACtB,IAAI,CAAC1B,SAAS4B,OAAO;YACnBL,SAASN,GAAG,CAAC;YACb;QACF;QACAK,IAAIL,GAAG,CAACS;QACR,KAAK,MAAMG,SAAS3B,MAAMC,OAAO,CAACyB,KAAKP,KAAK,IAAIO,KAAKP,KAAK,GAAG,EAAE,CAAE;YAC/D,IAAI,OAAOQ,UAAU,UAAUL,MAAMN,IAAI,CAACW;QAC5C;IACF;IACA,OAAO;QAAEP;QAAKC,UAAU;eAAIA;SAAS;IAAC;AACxC;AAMA,yEAAyE,GACzE,OAAO,SAASO,uBAAuBC,OAA0C;IAC/E,MAAMV,QAAQU,OAAO,CAAC,QAAQ;IAC9B,IAAI,CAAC/B,SAASqB,UAAU,CAACW,OAAOC,IAAI,CAACZ,OAAOI,MAAM,EAAE;QAClD,OAAO;YAAES,IAAI;YAAOX,UAAU;gBAAC;aAA2B;QAAC;IAC7D;IACA,MAAMY,MAAMd;IACZ,IAAI,CAACrB,SAASmC,GAAG,CAACrD,uBAAuB,GAAG;QAC1C,OAAO;YAAEoD,IAAI;YAAOX,UAAU;gBAAC;aAAwC;QAAC;IAC1E;IACA,MAAM,EAAED,GAAG,EAAEC,QAAQ,EAAE,GAAGH,aAAae;IACvC,MAAMC,WAAWJ,OAAOC,IAAI,CAACE,KAAKE,MAAM,CAAC,CAACX,KAAO,CAACJ,IAAIN,GAAG,CAACU,KAAKD,MAAM;IACrE,IAAIW,UAAU;QACZb,SAASL,IAAI,CACX,GAAGkB,SAAS,CAAC,EAAEA,aAAa,IAAI,aAAa,aAAa,8BAA8B,EACtFA,aAAa,IAAI,OAAO,OACzB,kBAAkB,CAAC;IAExB;IACA,MAAMnC,QAAiC;QACrCoB,OAAOc;QACPG,SAASlC,WAAW2B,OAAO,CAAC,UAAU;QACtCQ,WAAWnC,WAAW2B,OAAO,CAAC,YAAY;QAC1CS,iBAAiBhC,WAAWuB,OAAO,CAAC,kBAAkB;QACtDU,mBAAmBjC,WAAWuB,OAAO,CAAC,oBAAoB;IAC5D;IACA,MAAMW,OAAO,CAAC/B,QAAoBA,MAAMgC,IAAI,CAAC,CAAC9B,OAASA,KAAKY,MAAM,GAAGjC;IACrE,IAAIkD,KAAK;QAACzC,MAAMqC,OAAO;WAAKrC,MAAMuC,eAAe;KAAC,GAAG;QACnDjB,SAASL,IAAI,CAAC,CAAC,8BAA8B,EAAE1B,8BAA8B,WAAW,CAAC;IAC3F;IACA,IAAIkD,KAAK;QAACzC,MAAMsC,SAAS;WAAKtC,MAAMwC,iBAAiB;KAAC,GAAG;QACvDlB,SAASL,IAAI,CAAC,CAAC,2BAA2B,EAAE1B,8BAA8B,WAAW,CAAC;IACxF;IACA,OAAO+B,SAASE,MAAM,GAAG;QAAES,IAAI;QAAOX;IAAS,IAAI;QAAEW,IAAI;QAAMjC;QAAO2C,WAAWtB;IAAI;AACvF;AAEA,mEAAmE,GACnE,SAASuB,YAAYC,KAAa,EAAElC,GAAY,EAAEmC,QAAiB;IACjE,MAAMC,UAAU3C,OAAOO,cAAAA,MAAO,IAAIL,IAAI;IACtC,IAAI,mBAAmB0C,IAAI,CAACD,UAAU;QACpC,OAAO,GAAGF,MAAM,iEAAiE,CAAC;IACpF;IACA,MAAMI,OAAOF,QACV1C,OAAO,CAAC,4BAA4BV,iBACpCU,OAAO,CAAC,iCAAiC,GAAGV,gBAAgB,YAAY,CAAC;IAC5E,IAAI,CAACsD,QAAQA,SAAS,KAAK,OAAOH,WAAW,GAAGD,MAAM,cAAc,CAAC,GAAG;IACxE,IAAII,KAAKC,UAAU,CAAC,QAAQ,CAACD,KAAKC,UAAU,CAAC,OAAO;QAClD,OAAO,GAAGL,MAAM,wEAAwE,CAAC;IAC3F;IACA,OAAO9D,kBAAkBkE,QAAQ,OAAO,GAAGJ,MAAM,6CAA6C,CAAC;AACjG;AAEA;;;CAGC,GACD,OAAO,SAASM,wBACdrB,OAA0C,EAC1CsB,OAA2B;IAE3B,MAAMC,OAAOxB,uBAAuBC;IACpC,IAAIuB,KAAKpB,EAAE,KAAK,OAAO,OAAOoB;IAC9B,MAAM,EAAErD,KAAK,EAAE2C,SAAS,EAAE,GAAGU;IAC7B,MAAM/B,WAAqB,EAAE;IAC7B,KAAK,MAAMG,MAAMkB,UAAW;QAC1B,MAAMhB,OAAO3B,MAAMoB,KAAK,CAACK,GAAG;QAC5B,IAAIE,KAAK2B,WAAW,KAAK,eAAe;gBACE3B;YAAxC,MAAM4B,UAAUX,YAAY,aAAYjB,cAAAA,KAAK6B,KAAK,qBAAV7B,WAAY,CAAC,OAAO,EAAE;YAC9D,IAAI4B,SAASjC,SAASL,IAAI,CAACsC;QAC7B;QACA,IAAI5B,KAAK2B,WAAW,KAAK,cAAc;gBACG3B;YAAxC,MAAM4B,UAAUX,YAAY,aAAYjB,eAAAA,KAAK6B,KAAK,qBAAV7B,YAAY,CAAC,OAAO,EAAE;YAC9D,IAAI4B,SAASjC,SAASL,IAAI,CAACsC;QAC7B;IACF;IACA,MAAME,WAAWvE,qBAAqB;QACpCmD,SAASrC,MAAMqC,OAAO;QACtBC,WAAWtC,MAAMsC,SAAS;QAC1BR,SAAS;YACP4B,MAAM;YACNC,UAAU;gBAAEvC,OAAOpB,MAAMoB,KAAK;gBAAEiB,SAASrC,MAAMqC,OAAO;gBAAEC,WAAWtC,MAAMsC,SAAS;YAAC;QACrF;QACAsB,WAAWhE;QACXiE,UAAUlE;QACVmE,QAAQV,QAAQU,MAAM;IACxB;IACA,IAAI,CAACL,SAASM,WAAW,CAACzD,IAAI,IAAI;QAChCgB,SAASL,IAAI,CAAC;IAChB;IACA,IAAIK,SAASE,MAAM,EAAE,OAAO;QAAES,IAAI;QAAOX,UAAU;eAAI,IAAIb,IAAIa;SAAU;IAAC;IAC1E,OAAO;QACLW,IAAI;QACJ+B,OAAO;YACLC,WAAW,IAAIC,cAAcC,MAAM,CAACV,SAASW,IAAI,EAAE5C,MAAM;YACzDuC,aAAaN,SAASM,WAAW;YACjCM,QAAQ1B,UAAU2B,IAAI;YACtB,gFAAgF;YAChFC,WAAWtF,oBAAoB,CAAC;QAClC;IACF;AACF;AAMA,eAAeuF,eACbC,OAA4C,EAC5CpB,IAAkF;IAElF,MAAMqB,WAAW,MAAMrB,KAAKoB,QAAQE,UAAU,CAAC,WAAWC,MAAM,CAAC,eAAe,QAAQ;IACxF,OAAOF,SAASG,IAAI,CAAC3C,GAAG,CAAC,CAAC4C;YAIXA;eAJoB;YACjCrD,IAAIqD,IAAIrD,EAAE;YACVsD,MAAMD,IAAIE,GAAG,CAAC;YACdC,WAAWH,IAAIE,GAAG,CAAC;YACnBlF,MAAMM,QAAO0E,WAAAA,IAAIE,GAAG,CAAC,0BAARF,WAA0B;QACzC;;AACF;AAEA,SAASI,YAAYC,IAAwC,EAAEC,GAAW;QAC1DD;IAAd,MAAME,OAAO,EAACF,YAAAA,KAAKH,GAAG,CAAC,0BAATG,YAA2B,CAAC,EAAE,CAACC,IAAI;IACjD,OAAO5G,iBAAiB6G,QAAQ,OAAO;QAAEC,QAAQ;QAAKC,OAAO7F;IAA0B;AACzF;AAEA,SAAS8F,YAAYC,IAAiB,EAAEN,IAAwC;IAC9E,MAAMO,OAAOhH,iBAAiB+G,MAAMN,KAAKH,GAAG,CAAC,YAAqBV,IAAI;IACtE,OAAOoB,QAAQjH,+BAA+B;QAAE6G,QAAQ;QAAKC,OAAO9F;IAA2B,IAAI;AACrG;AAEA,SAASkG,QAAQ3F,KAAc;IAC7B,OAAOC,MAAMC,OAAO,CAACF,SAASA,MAAMoC,MAAM,CAAC,CAACwD,QAA2B,OAAOA,UAAU,YAAY,EAAE;AACxG;AAEA,SAASC,SAASC,MAA0C;QAI3CA,aAGKA,cACEA;IAPtB,MAAMC,YAAYD,OAAOd,GAAG,CAAC;IAC7B,OAAO;QACLvD,IAAIqE,OAAOrE,EAAE;QACb3B,MAAMM,QAAO0F,cAAAA,OAAOd,GAAG,CAAC,0BAAXc,cAA6B;QAC1CC,WAAW,OAAOA,cAAc,YAAYA,YAAYA,YAAY;QACpE/B,OAAO;YACL3B,SAASjC,QAAO0F,eAAAA,OAAOd,GAAG,CAAC,2BAAXc,eAA8B;YAC9CxD,WAAWlC,QAAO0F,eAAAA,OAAOd,GAAG,CAAC,6BAAXc,eAAgC;YAClDvD,iBAAiBoD,QAAQG,OAAOd,GAAG,CAAC;YACpCxC,mBAAmBmD,QAAQG,OAAOd,GAAG,CAAC;QACxC;IACF;AACF;AAEA,gFAAgF,GAChF,SAASgB,WAAWhG,KAA8B;IAChD,OAAO,aACDA,MAAMqC,OAAO,GAAG;QAAE4D,cAAcjG,MAAMqC,OAAO;IAAC,IAAI,CAAC,GACnDrC,MAAMsC,SAAS,GAAG;QAAE4D,gBAAgBlG,MAAMsC,SAAS;IAAC,IAAI,CAAC,GACzDtC,MAAMuC,eAAe,CAACf,MAAM,GAAG;QAAE2E,sBAAsBnG,MAAMuC,eAAe;IAAC,IAAI,CAAC,GAClFvC,MAAMwC,iBAAiB,CAAChB,MAAM,GAAG;QAAE4E,wBAAwBpG,MAAMwC,iBAAiB;IAAC,IAAI,CAAC;AAEhG;AASA,OAAO,SAAS6D,6BACdC,OAAmC,CAAC,CAAC;QAEnBA,iBACHA;IADf,MAAMC,aAAYD,kBAAAA,KAAKC,SAAS,YAAdD,kBAAmB,IAAMtH,cAAcwH,GAAG,GAAGD,SAAS;IACxE,MAAME,UAASH,eAAAA,KAAKG,MAAM,YAAXH,eAAejI;IAC9B,OAAO;QACLqI,SAAS,OAAOtD;gBAKZ8B;YAJF,MAAMT,UAAU8B,YAAY5B,UAAU,CAAC,SAASG,GAAG,CAAC1B,QAAQU,MAAM;YAClE,MAAMqB,OAAO,MAAMV,QAAQO,GAAG;YAC9B,IAAI,CAACG,KAAKwB,MAAM,EAAE,OAAO;gBAAErB,QAAQ;gBAAKC,OAAO;YAAe;YAC9D,QACEL,eAAAA,YAAYC,MAAM/B,QAAQgC,GAAG,aAA7BF,eACAM,YAAY,MAAMhB,eAAeC,SAAS,CAACmC,QAAUA,MAAM5B,GAAG,KAAKG;QAEvE;QAEA0B,OAAO1D;QAEPE,MAAM,OAAO,EAAES,MAAM,EAAErC,EAAE,EAAE;YACzB,MAAMqE,SAAS,MAAMS,YAAY5B,UAAU,CAAC,SAASG,GAAG,CAAChB,QAAQa,UAAU,CAAC,WAAWG,GAAG,CAACrD,IAAIuD,GAAG;YAClG,OAAOc,OAAOa,MAAM,IAAIb,OAAOd,GAAG,CAAC,YAAYrG,oBAAoBkH,SAASC,UAAU;QACxF;QAEAgB,OAAO,OAAOC;YACZ,MAAM1D,OAAOxB,uBAAuBkF,QAAQjF,OAAO;YACnD,IAAIuB,KAAKpB,EAAE,KAAK,OAAO,OAAO;gBAAEA,IAAI;gBAAOqD,QAAQ;gBAAKC,OAAOlC,KAAK/B,QAAQ,CAAC,EAAE;YAAC;YAChF,MAAM0F,SAASpI,kBAAkByE,KAAKrD,KAAK,CAACoB,KAAK;YACjD,IAAI,CAAC4F,QAAQ,OAAO;gBAAE/E,IAAI;gBAAOqD,QAAQ;gBAAKC,OAAO;YAA2B;YAChF,MAAM0B,KAAKV;YACX,MAAM9B,UAAUwC,GAAGtC,UAAU,CAAC,SAASG,GAAG,CAACiC,QAAQjD,MAAM;YACzD,MAAMoD,YAAYzC,QAAQE,UAAU,CAAC,WAAWG,GAAG,CAACiC,QAAQtF,EAAE;YAC9D,MAAMsE,YAAYU;YAClB,OAAOQ,GAAGE,cAAc,CAAC,OAAOC;oBAadlC;gBAZhB,yDAAyD;gBACzD,MAAM,CAACC,MAAMkC,UAAU5B,KAAK,GAAG,MAAM6B,QAAQC,GAAG,CAAC;oBAC/CH,GAAGpC,GAAG,CAACP;oBACP2C,GAAGpC,GAAG,CAACkC;oBACP1C,eAAeC,SAAS,CAACmC,QAAUQ,GAAGpC,GAAG,CAAC4B;iBAC3C;gBACD,IAAI,CAACzB,KAAKwB,MAAM,EAAE,OAAO;oBAAE1E,IAAI;oBAAOqD,QAAQ;oBAAKC,OAAO;gBAAe;gBACzE,IAAI8B,SAASV,MAAM,EAAE;oBACnB,OAAOU,SAASrC,GAAG,CAAC,YAAYrG,oBAC5B;wBAAEsD,IAAI;wBAAMuF,UAAU;uBAAS3B,SAASwB,aACxC;wBAAEpF,IAAI;wBAAOqD,QAAQ;wBAAKC,OAAO;oBAA6D;gBACpG;gBACA,MAAMmB,WAAUxB,eAAAA,YAAYC,MAAM4B,QAAQ3B,GAAG,aAA7BF,eAAkCM,YAAYC,MAAMN;gBACpE,IAAIuB,SAAS,OAAO;oBAAEzE,IAAI;mBAAUyE;gBACpC,MAAM5G,OAAOxB,oBACX6B,WAAW4G,QAAQjH,IAAI,KAAKR,2BAC5BmG,KAAKrD,MAAM,CAAC,CAACqF,MAAQA,IAAIxC,SAAS,IAAI,MAAM/C,GAAG,CAAC,CAACuF,MAAQA,IAAI3H,IAAI;gBAEnE,MAAM,EAAEgG,MAAM,EAAE4B,OAAO,EAAE,GAAGvI,qBAAqB;oBAC/CwI,UAAUZ,QAAQtF,EAAE;oBACpBsE;oBACA6B,aAAa9H;oBACbsB,OAAOiC,KAAKrD,KAAK,CAACoB,KAAK;gBACzB;gBACA,MAAMyG,SAAS;oBAAEC,WAAWf,QAAQgB,GAAG;oBAAEC,WAAWjB,QAAQgB,GAAG;oBAAEE,WAAWlB,QAAQ3B,GAAG;gBAAC;gBACxF,MAAM8C,OAAOlC,WAAW3C,KAAKrD,KAAK;gBAClCoH,GAAGe,MAAM,CAACjB,WAAW,aAAKpB;oBAAQsC,WAAW7J,cAAcuB;mBAAUoI,MAASL;gBAC9ET,GAAGe,MAAM,CAACjB,UAAUvC,UAAU,CAAC,YAAYG,GAAG,CAACiB,YAAY,aACtD2B;oBACHtG,OAAOiH,OAAOC,IAAI,CAACtB;oBACnBlD,QAAQiD,QAAQjD,MAAM;mBACnB+D;gBAEL,OAAO;oBACL5F,IAAI;oBACJuF,UAAU;oBACV/F,IAAIsF,QAAQtF,EAAE;oBACd3B;oBACAiG;oBACA/B,OAAO;wBACL3B,SAASgB,KAAKrD,KAAK,CAACqC,OAAO;wBAC3BC,WAAWe,KAAKrD,KAAK,CAACsC,SAAS;wBAC/BC,iBAAiBc,KAAKrD,KAAK,CAACuC,eAAe;wBAC3CC,mBAAmBa,KAAKrD,KAAK,CAACwC,iBAAiB;oBACjD;gBACF;YACF;QACF;IACF;AACF;AAEA,OAAO,MAAM+F,yBAAyBlC,+BAA8B;AAEpE;;;CAGC,GACD,OAAO,SAASmC;IACd1J,kCAAkCM,6BAA6BmJ,wBAAwB;QACrFE,UAAUpJ;IACZ;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/email/src/lib/server-email-drafts.ts"],"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 { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { uniqueDuplicateName } from '@aglyn/aglyn/app-utils/duplicate-resource'\nimport { nameSearchKey } from '@aglyn/aglyn/app-utils/name-search'\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport {\n NON_PAGE_SCREEN_MAX_PER_HOST,\n nonPageScreenIds,\n SCREEN_KIND_EMAIL,\n type BillableScreenSource,\n} from '@aglyn/aglyn/app-utils/screen-route'\nimport { encodeStoredNodes } from '@aglyn/aglyn/app-utils/stored-nodes'\nimport { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport {\n registerPluginResourceDraftWriter,\n type PluginDraftCheck,\n type PluginDraftCheckContext,\n type PluginDraftRecord,\n type PluginDraftRefusal,\n type PluginDraftWrite,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport {\n composeReusableComponentNodes,\n REUSABLE_INSTANCE_COMPONENT_ID,\n} from '@aglyn/aglyn/app-utils/compose-reusable-components'\nimport { hasSafeLinkScheme } from '@aglyn/shared-util-http/safe-url-scheme'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n emailPlainTextState,\n renderRecipientEmail,\n} from '@aglyn/aglyn/app-utils/recipient-email-render'\nimport { emailDesignDocuments } from './model/email-design-document'\n\n/**\n * AN EMAIL DESIGN ANOTHER PLUGIN ASKS FOR (AGL-2912).\n *\n * The draft writer this plugin registers for the `emailDesign` resource on the\n * core's resource-drafts seam. A plugin that produces an email — a generator\n * working from a brief, an importer bringing templates over — asks for the\n * writer by name and gets this plugin's rules, never a copy of them:\n *\n * - THE DOCUMENTS are the pair `createEmailScreen` makes, built by the same\n * function (`emailDesignDocuments`): a `kind: 'email'` screen and its first\n * version. So the design lists on the Templates tab, opens in the besigner\n * and is offered by the campaign composer like one a person made. The\n * screen carries the subject and preheader a campaign built from it opens\n * on, and the other lines worth trying beside them.\n * - THE ROOM is the flat platform ceiling on non-page screens, met inside the\n * transaction with the arithmetic `/api/hosts/resources` and the duplicate\n * module use, and refused in their words.\n * - THE ROLE is the resources route's: a member who may write the site's\n * content.\n * - THE CHECK renders the design through `renderRecipientEmail`, the function\n * the send path and the composer's preview both call, so content that\n * passes is content that would render in both.\n *\n * Nothing is sent, scheduled or routed. A design is inert until a campaign\n * names it, and the writer touches the two new documents and nothing else.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The resource name this writer is registered under. */\nexport const EMAIL_DESIGN_DRAFT_RESOURCE = 'emailDesign'\n\n/** The id `plugins.config.json` registers this plugin under. */\nconst EMAIL_PLUGIN_ID = 'email'\n\n/** A design's name when the caller asks for none, as the composer's create names one. */\nexport const EMAIL_DESIGN_DEFAULT_NAME = 'Untitled email'\n\n/** The longest subject or preheader stored, the send route's header cap. */\nexport const EMAIL_DESIGN_HEADER_MAX_CHARS = 200\n\n/** The most alternative subject lines or preheaders a design keeps. */\nexport const EMAIL_DESIGN_MAX_VARIANTS = 10\n\n/** The resources route's refusal at the non-page ceiling. */\nexport const EMAIL_DESIGN_LIMIT_REFUSAL =\n 'This site is at its limit of ' +\n `${NON_PAGE_SCREEN_MAX_PER_HOST} email and template screens — ` +\n 'delete some to make room'\n\n/** The resources route's refusal for a member who may not write the site. */\nexport const EMAIL_DESIGN_ROLE_REFUSAL = 'Editing requires the editor role'\n\n/*\n * What the check renders against. No mail is sent and nothing is fetched: the\n * origin makes the renderer keep images and links it would drop without one,\n * and the recipient fills the merge tokens a real send would fill.\n */\nconst CHECK_SITE_BASE = 'https://example.com'\nconst CHECK_RECIPIENT = { email: 'reader@example.com', name: 'Sample Reader' }\n\n/** One node of a design, as far as the writer reads it. */\nexport interface EmailDesignNode {\n componentId?: unknown\n props?: Record<string, unknown>\n nodes?: unknown\n}\n\n/**\n * What a caller sends as `content`: the node map rooted at the canvas root,\n * the subject and preheader, and the alternatives to them.\n */\nexport interface EmailDesignDraftContent {\n nodes: Record<string, EmailDesignNode>\n subject: string\n preheader: string\n subjectVariants: string[]\n preheaderVariants: string[]\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** A header value on one line: controls and runs of space folded to one space. */\nfunction headerLine(value: unknown): string {\n return String(value ?? '').replace(/[\\p{Cc}\\s]+/gu, ' ').trim()\n}\n\n/** Alternatives as the design keeps them: on one line, unique, non-empty, bounded. */\nfunction variantsOf(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n const seen = new Set<string>()\n const lines: string[] = []\n for (const raw of value) {\n const line = headerLine(raw)\n const key = line.toLowerCase()\n if (!line || seen.has(key)) continue\n seen.add(key)\n lines.push(line)\n }\n return lines.slice(0, EMAIL_DESIGN_MAX_VARIANTS)\n}\n\n/** The ids reachable from the canvas root, or the structural problem that stops the walk. */\nfunction reachableIds(nodes: Record<string, EmailDesignNode>): { ids: Set<string>; problems: string[] } {\n const ids = new Set<string>()\n const problems = new Set<string>()\n const stack = [CANVAS_ROOT_ELEMENT_ID]\n while (stack.length) {\n const id = stack.pop() as string\n if (ids.has(id)) {\n problems.add('A block is placed in the design more than once')\n continue\n }\n const node = nodes[id]\n if (!isRecord(node)) {\n problems.add('The design lists a block it does not hold')\n continue\n }\n ids.add(id)\n for (const child of Array.isArray(node.nodes) ? node.nodes : []) {\n if (typeof child === 'string') stack.push(child)\n }\n }\n return { ids, problems: [...problems] }\n}\n\nexport type EmailDesignContentRead =\n | { ok: true; value: EmailDesignDraftContent; reachable: Set<string> }\n | { ok: false; problems: string[] }\n\n/** The content as the writer stores it, or every problem that stops it. */\nexport function readEmailDesignContent(content: Readonly<Record<string, unknown>>): EmailDesignContentRead {\n const nodes = content['nodes']\n if (!isRecord(nodes) || !Object.keys(nodes).length) {\n return { ok: false, problems: ['The design has no blocks'] }\n }\n const map = nodes as Record<string, EmailDesignNode>\n if (!isRecord(map[CANVAS_ROOT_ELEMENT_ID])) {\n return { ok: false, problems: ['The design has no root to render from'] }\n }\n const { ids, problems } = reachableIds(map)\n const stranded = Object.keys(map).filter((id) => !ids.has(id)).length\n if (stranded) {\n problems.push(\n `${stranded} ${stranded === 1 ? 'block is' : 'blocks are'} not placed in the design, so ${\n stranded === 1 ? 'it' : 'they'\n } would not be sent`,\n )\n }\n const value: EmailDesignDraftContent = {\n nodes: map,\n subject: headerLine(content['subject']),\n preheader: headerLine(content['preheader']),\n subjectVariants: variantsOf(content['subjectVariants']),\n preheaderVariants: variantsOf(content['preheaderVariants']),\n }\n const long = (lines: string[]) => lines.some((line) => line.length > EMAIL_DESIGN_HEADER_MAX_CHARS)\n if (long([value.subject, ...value.subjectVariants])) {\n problems.push(`A subject line is longer than ${EMAIL_DESIGN_HEADER_MAX_CHARS} characters`)\n }\n if (long([value.preheader, ...value.preheaderVariants])) {\n problems.push(`A preheader is longer than ${EMAIL_DESIGN_HEADER_MAX_CHARS} characters`)\n }\n return problems.length ? { ok: false, problems } : { ok: true, value, reachable: ids }\n}\n\n/** Why a link on a block would not open from an inbox, or `null`. */\nfunction linkProblem(label: string, raw: unknown, required: boolean): string | null {\n const written = String(raw ?? '').trim()\n if (/\\{\\{\\s*contact\\./.test(written)) {\n return `${label} puts the reader's own details in its link, which is never filled`\n }\n const href = written\n .replace(/\\{\\{\\s*site\\.url\\s*\\}\\}/g, CHECK_SITE_BASE)\n .replace(/\\{\\{\\s*unsubscribeUrl\\s*\\}\\}/g, `${CHECK_SITE_BASE}/preferences`)\n if (!href || href === '#') return required ? `${label} links nowhere` : null\n if (href.startsWith('/') && !href.startsWith('//')) {\n return `${label} links to a path with no site in front of it, which an inbox cannot open`\n }\n return hasSafeLinkScheme(href) ? null : `${label} links to something that is not a web address`\n}\n\n/** A placement still standing once the site's components are grafted. */\nexport const EMAIL_DESIGN_UNRESOLVED_BLOCK_PROBLEM =\n 'A reusable block names a component this site has not published, so it would send empty'\n\n/**\n * Whether content renders as an email: well-formed, every link one an inbox\n * can open, and a message once rendered through the send path's own renderer.\n *\n * Judged COMPOSED (AGL-3287). The reusable blocks the design places — a shared\n * header, a footer — are grafted from `context.components` first, as every send\n * grafts them, so a link inside a footer is checked like any other and a\n * header's text counts toward the message. A placement still standing after\n * that names a component the caller could not supply, and would go out as\n * nothing; it is refused rather than passed as a design that renders.\n */\nexport function checkEmailDesignContent(\n content: Readonly<Record<string, unknown>>,\n context: PluginDraftCheckContext,\n): PluginDraftCheck {\n const read = readEmailDesignContent(content)\n if (read.ok === false) return read\n const { value } = read\n // Property values and attribute overrides apply in the graft; style\n // overrides land in `sx`, which the mail renderer never reads.\n const nodes = composeReusableComponentNodes(\n value.nodes as never,\n context.components as never,\n ) as unknown as Record<string, EmailDesignNode>\n const reachable =\n nodes === value.nodes ? read.reachable : reachableIds(nodes).ids\n const problems: string[] = []\n for (const id of reachable) {\n const node = nodes[id]\n if (node.componentId === REUSABLE_INSTANCE_COMPONENT_ID) {\n problems.push(EMAIL_DESIGN_UNRESOLVED_BLOCK_PROBLEM)\n continue\n }\n if (node.componentId === 'emailButton') {\n const problem = linkProblem('A button', node.props?.['href'], true)\n if (problem) problems.push(problem)\n }\n if (node.componentId === 'emailImage') {\n const problem = linkProblem('An image', node.props?.['href'], false)\n if (problem) problems.push(problem)\n }\n }\n const rendered = renderRecipientEmail({\n subject: value.subject,\n preheader: value.preheader,\n content: {\n mode: 'design',\n template: { nodes, subject: value.subject, preheader: value.preheader },\n },\n recipient: CHECK_RECIPIENT,\n siteBase: CHECK_SITE_BASE,\n hostId: context.hostId,\n })\n if (!rendered.messageText.trim()) {\n problems.push('The design renders no message: it needs at least one block of text')\n }\n if (problems.length) return { ok: false, problems: [...new Set(problems)] }\n return {\n ok: true,\n facts: {\n htmlBytes: new TextEncoder().encode(rendered.html).length,\n messageText: rendered.messageText,\n // The blocks the design itself holds, as stored — a placed component\n // counts once, however many blocks it expands into.\n blocks: read.reachable.size,\n // No part is written by hand, so the text a campaign sends is the design's own.\n plainText: emailPlainTextState({}),\n },\n }\n}\n\ninterface ScreenRow extends BillableScreenSource {\n name: string\n}\n\nasync function readScreenRows(\n hostRef: FirebaseFirestore.DocumentReference,\n read: (query: FirebaseFirestore.Query) => Promise<FirebaseFirestore.QuerySnapshot>,\n): Promise<ScreenRow[]> {\n const snapshot = await read(hostRef.collection('screens').select('displayName', 'kind', 'deletedAt'))\n return snapshot.docs.map((doc) => ({\n id: doc.id,\n kind: doc.get('kind'),\n deletedAt: doc.get('deletedAt'),\n name: String(doc.get('displayName') ?? ''),\n }))\n}\n\nfunction roleRefusal(host: FirebaseFirestore.DocumentSnapshot, uid: string): PluginDraftRefusal | null {\n const role = (host.get('memberRoles') ?? {})[uid]\n return hostRoleCanWrite(role) ? null : { status: 403, error: EMAIL_DESIGN_ROLE_REFUSAL }\n}\n\nfunction roomRefusal(rows: ScreenRow[], host: FirebaseFirestore.DocumentSnapshot): PluginDraftRefusal | null {\n const used = nonPageScreenIds(rows, host.get('screens') as never).size\n return used >= NON_PAGE_SCREEN_MAX_PER_HOST ? { status: 403, error: EMAIL_DESIGN_LIMIT_REFUSAL } : null\n}\n\nfunction strings(value: unknown): string[] {\n return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : []\n}\n\nfunction recordOf(screen: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n const versionId = screen.get('versionId')\n return {\n id: screen.id,\n name: String(screen.get('displayName') ?? ''),\n versionId: typeof versionId === 'string' && versionId ? versionId : null,\n facts: {\n subject: String(screen.get('emailSubject') ?? ''),\n preheader: String(screen.get('emailPreheader') ?? ''),\n subjectVariants: strings(screen.get('emailSubjectVariants')),\n preheaderVariants: strings(screen.get('emailPreheaderVariants')),\n },\n }\n}\n\n/** The subject, preheader and alternatives as the screen document stores them. */\nfunction copyFields(value: EmailDesignDraftContent): Record<string, unknown> {\n return {\n ...(value.subject ? { emailSubject: value.subject } : {}),\n ...(value.preheader ? { emailPreheader: value.preheader } : {}),\n ...(value.subjectVariants.length ? { emailSubjectVariants: value.subjectVariants } : {}),\n ...(value.preheaderVariants.length ? { emailPreheaderVariants: value.preheaderVariants } : {}),\n }\n}\n\nexport interface EmailDesignDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n /** Mints the first version's id. */\n mintId?: () => string\n}\n\nexport function createEmailDesignDraftWriter(\n deps: EmailDesignDraftWriterDeps = {},\n): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n const mintId = deps.mintId ?? createResourceUid\n return {\n refusal: async (context) => {\n const hostRef = firestore().collection('hosts').doc(context.hostId)\n const host = await hostRef.get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n return (\n roleRefusal(host, context.uid) ??\n roomRefusal(await readScreenRows(hostRef, (query) => query.get()), host)\n )\n },\n\n check: checkEmailDesignContent,\n\n read: async ({ hostId, id }) => {\n const screen = await firestore().collection('hosts').doc(hostId).collection('screens').doc(id).get()\n return screen.exists && screen.get('kind') === SCREEN_KIND_EMAIL ? recordOf(screen) : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n const read = readEmailDesignContent(request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n const packed = encodeStoredNodes(read.value.nodes)\n if (!packed) return { ok: false, status: 400, error: 'The design has no blocks' }\n const db = firestore()\n const hostRef = db.collection('hosts').doc(request.hostId)\n const screenRef = hostRef.collection('screens').doc(request.id)\n const versionId = mintId()\n return db.runTransaction(async (tx): Promise<PluginDraftWrite> => {\n // Every read before any write, which Firestore requires.\n const [host, existing, rows] = await Promise.all([\n tx.get(hostRef),\n tx.get(screenRef),\n readScreenRows(hostRef, (query) => tx.get(query)),\n ])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n if (existing.exists) {\n return existing.get('kind') === SCREEN_KIND_EMAIL\n ? { ok: true, replayed: true, ...recordOf(existing) }\n : { ok: false, status: 409, error: 'That id already names a screen that is not an email design' }\n }\n const refusal = roleRefusal(host, request.uid) ?? roomRefusal(rows, host)\n if (refusal) return { ok: false, ...refusal }\n const name = uniqueDuplicateName(\n headerLine(request.name) || EMAIL_DESIGN_DEFAULT_NAME,\n rows.filter((row) => row.deletedAt == null).map((row) => row.name),\n )\n const { screen, version } = emailDesignDocuments({\n screenId: request.id,\n versionId,\n displayName: name,\n nodes: read.value.nodes,\n })\n const stamps = { createdAt: request.now, updatedAt: request.now, createdBy: request.uid }\n const copy = copyFields(read.value)\n tx.create(screenRef, { ...screen, nameLower: nameSearchKey(name), ...copy, ...stamps })\n tx.create(screenRef.collection('versions').doc(versionId), {\n ...version,\n nodes: Buffer.from(packed),\n hostId: request.hostId,\n ...stamps,\n })\n return {\n ok: true,\n replayed: false,\n id: request.id,\n name,\n versionId,\n facts: {\n subject: read.value.subject,\n preheader: read.value.preheader,\n subjectVariants: read.value.subjectVariants,\n preheaderVariants: read.value.preheaderVariants,\n },\n }\n })\n },\n }\n}\n\nexport const emailDesignDraftWriter = createEmailDesignDraftWriter()\n\n/**\n * Registers the writer; the console surface calls it, since only the console\n * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.\n */\nexport function registerEmailDesignDraftWriter(): void {\n registerPluginResourceDraftWriter(EMAIL_DESIGN_DRAFT_RESOURCE, emailDesignDraftWriter, {\n pluginId: EMAIL_PLUGIN_ID,\n })\n}\n"],"names":["createResourceUid","uniqueDuplicateName","nameSearchKey","hostRoleCanWrite","NON_PAGE_SCREEN_MAX_PER_HOST","nonPageScreenIds","SCREEN_KIND_EMAIL","encodeStoredNodes","CANVAS_ROOT_ELEMENT_ID","registerPluginResourceDraftWriter","composeReusableComponentNodes","REUSABLE_INSTANCE_COMPONENT_ID","hasSafeLinkScheme","firebaseAdmin","emailPlainTextState","renderRecipientEmail","emailDesignDocuments","EMAIL_DESIGN_DRAFT_RESOURCE","EMAIL_PLUGIN_ID","EMAIL_DESIGN_DEFAULT_NAME","EMAIL_DESIGN_HEADER_MAX_CHARS","EMAIL_DESIGN_MAX_VARIANTS","EMAIL_DESIGN_LIMIT_REFUSAL","EMAIL_DESIGN_ROLE_REFUSAL","CHECK_SITE_BASE","CHECK_RECIPIENT","email","name","isRecord","value","Array","isArray","headerLine","String","replace","trim","variantsOf","seen","Set","lines","raw","line","key","toLowerCase","has","add","push","slice","reachableIds","nodes","ids","problems","stack","length","id","pop","node","child","readEmailDesignContent","content","Object","keys","ok","map","stranded","filter","subject","preheader","subjectVariants","preheaderVariants","long","some","reachable","linkProblem","label","required","written","test","href","startsWith","EMAIL_DESIGN_UNRESOLVED_BLOCK_PROBLEM","checkEmailDesignContent","context","read","components","componentId","problem","props","rendered","mode","template","recipient","siteBase","hostId","messageText","facts","htmlBytes","TextEncoder","encode","html","blocks","size","plainText","readScreenRows","hostRef","snapshot","collection","select","docs","doc","kind","get","deletedAt","roleRefusal","host","uid","role","status","error","roomRefusal","rows","used","strings","entry","recordOf","screen","versionId","copyFields","emailSubject","emailPreheader","emailSubjectVariants","emailPreheaderVariants","createEmailDesignDraftWriter","deps","firestore","app","mintId","refusal","exists","query","check","write","request","packed","db","screenRef","runTransaction","tx","existing","Promise","all","replayed","row","version","screenId","displayName","stamps","createdAt","now","updatedAt","createdBy","copy","create","nameLower","Buffer","from","emailDesignDraftWriter","registerEmailDesignDraftWriter","pluginId"],"mappings":"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"}
|