@aglyn/shared-util-email 1.0.0-beta.203 → 1.0.0-beta.204
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 +3 -3
- package/src/index.d.ts +3 -0
- package/src/index.js +9 -0
- package/src/index.js.map +1 -1
- package/src/lib/email-design-chrome.d.ts +61 -0
- package/src/lib/email-design-chrome.js +99 -0
- package/src/lib/email-design-chrome.js.map +1 -0
- package/src/lib/email-linkify.d.ts +3 -1
- package/src/lib/email-linkify.js +2 -2
- package/src/lib/email-linkify.js.map +1 -1
- package/src/lib/email-palette.d.ts +137 -0
- package/src/lib/email-palette.js +756 -0
- package/src/lib/email-palette.js.map +1 -0
- package/src/lib/email-render.d.ts +25 -0
- package/src/lib/email-render.js +27 -9
- package/src/lib/email-render.js.map +1 -1
- package/src/lib/held-page.d.ts +117 -0
- package/src/lib/held-page.js +172 -0
- package/src/lib/held-page.js.map +1 -0
- package/src/lib/host-email-render.d.ts +34 -7
- package/src/lib/host-email-render.js +69 -25
- package/src/lib/host-email-render.js.map +1 -1
- package/src/lib/hosted-page-screen.js +8 -6
- package/src/lib/hosted-page-screen.js.map +1 -1
- package/src/lib/outbound-phishing-screen.d.ts +24 -0
- package/src/lib/outbound-phishing-screen.js +81 -2
- package/src/lib/outbound-phishing-screen.js.map +1 -1
- package/src/lib/risk-notice-catalog.d.ts +204 -0
- package/src/lib/risk-notice-catalog.js +1702 -0
- package/src/lib/risk-notice-catalog.js.map +1 -0
- package/src/lib/risk-notice-emails.d.ts +25 -0
- package/src/lib/risk-notice-emails.js +182 -0
- package/src/lib/risk-notice-emails.js.map +1 -0
- package/src/lib/sender-readiness.d.ts +155 -0
- package/src/lib/sender-readiness.js +432 -0
- package/src/lib/sender-readiness.js.map +1 -0
- package/src/lib/system-email-catalog.js +7 -1
- package/src/lib/system-email-catalog.js.map +1 -1
- package/src/lib/tenant-email-catalog.d.ts +7 -0
- package/src/lib/tenant-email-catalog.js +183 -13
- package/src/lib/tenant-email-catalog.js.map +1 -1
- package/src/lib/text-email-html.d.ts +26 -0
- package/src/lib/text-email-html.js +56 -1
- package/src/lib/text-email-html.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/shared-util-email",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.204",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
28
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.204",
|
|
29
|
+
"@aglyn/shared-util-http": "1.0.0-beta.204",
|
|
30
30
|
"@msgpack/msgpack": "^3.1.3",
|
|
31
31
|
"@swc/helpers": "0.5.23"
|
|
32
32
|
},
|
package/src/index.d.ts
CHANGED
|
@@ -40,6 +40,9 @@ export * from './lib/email-merge';
|
|
|
40
40
|
export * from './lib/mail-gateway';
|
|
41
41
|
export * from './lib/mail-bounce';
|
|
42
42
|
export * from './lib/email-deliverability';
|
|
43
|
+
export * from './lib/sender-readiness';
|
|
43
44
|
export * from './lib/outbound-phishing-screen';
|
|
44
45
|
export * from './lib/outbound-screen-gate';
|
|
45
46
|
export * from './lib/hosted-page-screen';
|
|
47
|
+
export * from './lib/email-palette';
|
|
48
|
+
export * from './lib/email-design-chrome';
|
package/src/index.js
CHANGED
|
@@ -52,6 +52,10 @@ export * from "./lib/email-merge.js";
|
|
|
52
52
|
export * from "./lib/mail-gateway.js";
|
|
53
53
|
export * from "./lib/mail-bounce.js";
|
|
54
54
|
export * from "./lib/email-deliverability.js";
|
|
55
|
+
// The sender's side of the same engine (AGL-3328): SPF, DKIM and DMARC for
|
|
56
|
+
// the domain mail leaves from, and whether they align. Pure as well; the
|
|
57
|
+
// lookups are `@aglyn/tenant-data-admin`'s.
|
|
58
|
+
export * from "./lib/sender-readiness.js";
|
|
55
59
|
// The outbound phishing screen a young workspace's campaigns and automated
|
|
56
60
|
// email pass (AGL-3356). Pure; the hold it leads to is `tenant-data-admin`'s.
|
|
57
61
|
export * from "./lib/outbound-phishing-screen.js";
|
|
@@ -61,5 +65,10 @@ export * from "./lib/outbound-screen-gate.js";
|
|
|
61
65
|
// The same screen read over a published page (AGL-3356): lookalike links and
|
|
62
66
|
// embeds, author-defined credential fields, a brand's call to action.
|
|
63
67
|
export * from "./lib/hosted-page-screen.js";
|
|
68
|
+
// The site's theme palette as plain data (AGL-3370), so a palette token an
|
|
69
|
+
// author picked resolves in mail. Built without MUI; `shared-ui-theme`'s
|
|
70
|
+
// parity spec holds it equal to the theme.
|
|
71
|
+
export * from "./lib/email-palette.js";
|
|
72
|
+
export * from "./lib/email-design-chrome.js";
|
|
64
73
|
|
|
65
74
|
//# sourceMappingURL=index.js.map
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/shared/util/email/src/index.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\nexport * from './lib/send-email'\nexport * from './lib/send-rate'\nexport * from './lib/sender-reputation'\nexport * from './lib/send-ceilings'\nexport * from './lib/sending-domain'\nexport * from './lib/tracking-host'\nexport * from './lib/sending-mailbox'\nexport * from './lib/host-sender'\nexport * from './lib/platform-sending-domain'\nexport * from './lib/email-health'\nexport * from './lib/system-email-catalog'\nexport * from './lib/tenant-email-catalog'\nexport * from './lib/host-email-render'\nexport * from './lib/email-render'\nexport * from './lib/text-email-html'\nexport * from './lib/marketing-send'\n// The List-Unsubscribe setting sequences and campaigns share (AGL-3307).\nexport * from './lib/list-unsubscribe'\nexport * from './lib/email-delivery-events'\n// The received-mail reader the CRM's capture route files from (AGL-2657).\n// Its neighbor `./lib/svix-signature` is deliberately NOT re-exported here:\n// it holds a `crypto` HMAC, and this barrel is reached from the browser\n// through `@aglyn/shared-ui-email-campaigns`, so a webhook imports it by its\n// own path — `@aglyn/shared-util-email/svix-signature`.\nexport * from './lib/received-email'\nexport * from './lib/email-revenue-window'\n// Exported so the console drift guard can pin it against @aglyn/aglyn.\nexport * from './lib/email-media-src'\n// Same arrangement, same reason (AGL-1223): a copy of `decodeStoredNodes`\n// this scope:shared lib may not import, pinned by `email-nodes-drift.spec.ts`.\nexport * from './lib/stored-email-nodes'\nexport * from './lib/email-merge'\n// The deliverability engine every send path asks (AGL-3328): the gateway an\n// MX names and its ledger, the bounce reader, and the address verdicts. Pure\n// — the DNS lookup and the store are injected by `@aglyn/tenant-data-admin`,\n// so nothing here reaches `node:dns` from the browser.\nexport * from './lib/mail-gateway'\nexport * from './lib/mail-bounce'\nexport * from './lib/email-deliverability'\n// The outbound phishing screen a young workspace's campaigns and automated\n// email pass (AGL-3356). Pure; the hold it leads to is `tenant-data-admin`'s.\nexport * from './lib/outbound-phishing-screen'\n// The send seam's gate for that screen — the store a hold writes is injected\n// by `@aglyn/tenant-data-admin`, like the governor's.\nexport * from './lib/outbound-screen-gate'\n// The same screen read over a published page (AGL-3356): lookalike links and\n// embeds, author-defined credential fields, a brand's call to action.\nexport * from './lib/hosted-page-screen'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,cAAc,sBAAkB;AAChC,cAAc,qBAAiB;AAC/B,cAAc,6BAAyB;AACvC,cAAc,yBAAqB;AACnC,cAAc,0BAAsB;AACpC,cAAc,yBAAqB;AACnC,cAAc,2BAAuB;AACrC,cAAc,uBAAmB;AACjC,cAAc,mCAA+B;AAC7C,cAAc,wBAAoB;AAClC,cAAc,gCAA4B;AAC1C,cAAc,gCAA4B;AAC1C,cAAc,6BAAyB;AACvC,cAAc,wBAAoB;AAClC,cAAc,2BAAuB;AACrC,cAAc,0BAAsB;AACpC,yEAAyE;AACzE,cAAc,4BAAwB;AACtC,cAAc,iCAA6B;AAC3C,0EAA0E;AAC1E,4EAA4E;AAC5E,wEAAwE;AACxE,6EAA6E;AAC7E,wDAAwD;AACxD,cAAc,0BAAsB;AACpC,cAAc,gCAA4B;AAC1C,uEAAuE;AACvE,cAAc,2BAAuB;AACrC,0EAA0E;AAC1E,+EAA+E;AAC/E,cAAc,8BAA0B;AACxC,cAAc,uBAAmB;AACjC,4EAA4E;AAC5E,6EAA6E;AAC7E,6EAA6E;AAC7E,uDAAuD;AACvD,cAAc,wBAAoB;AAClC,cAAc,uBAAmB;AACjC,cAAc,gCAA4B;AAC1C,2EAA2E;AAC3E,8EAA8E;AAC9E,cAAc,oCAAgC;AAC9C,6EAA6E;AAC7E,sDAAsD;AACtD,cAAc,gCAA4B;AAC1C,6EAA6E;AAC7E,sEAAsE;AACtE,cAAc,8BAA0B"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/shared/util/email/src/index.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\nexport * from './lib/send-email'\nexport * from './lib/send-rate'\nexport * from './lib/sender-reputation'\nexport * from './lib/send-ceilings'\nexport * from './lib/sending-domain'\nexport * from './lib/tracking-host'\nexport * from './lib/sending-mailbox'\nexport * from './lib/host-sender'\nexport * from './lib/platform-sending-domain'\nexport * from './lib/email-health'\nexport * from './lib/system-email-catalog'\nexport * from './lib/tenant-email-catalog'\nexport * from './lib/host-email-render'\nexport * from './lib/email-render'\nexport * from './lib/text-email-html'\nexport * from './lib/marketing-send'\n// The List-Unsubscribe setting sequences and campaigns share (AGL-3307).\nexport * from './lib/list-unsubscribe'\nexport * from './lib/email-delivery-events'\n// The received-mail reader the CRM's capture route files from (AGL-2657).\n// Its neighbor `./lib/svix-signature` is deliberately NOT re-exported here:\n// it holds a `crypto` HMAC, and this barrel is reached from the browser\n// through `@aglyn/shared-ui-email-campaigns`, so a webhook imports it by its\n// own path — `@aglyn/shared-util-email/svix-signature`.\nexport * from './lib/received-email'\nexport * from './lib/email-revenue-window'\n// Exported so the console drift guard can pin it against @aglyn/aglyn.\nexport * from './lib/email-media-src'\n// Same arrangement, same reason (AGL-1223): a copy of `decodeStoredNodes`\n// this scope:shared lib may not import, pinned by `email-nodes-drift.spec.ts`.\nexport * from './lib/stored-email-nodes'\nexport * from './lib/email-merge'\n// The deliverability engine every send path asks (AGL-3328): the gateway an\n// MX names and its ledger, the bounce reader, and the address verdicts. Pure\n// — the DNS lookup and the store are injected by `@aglyn/tenant-data-admin`,\n// so nothing here reaches `node:dns` from the browser.\nexport * from './lib/mail-gateway'\nexport * from './lib/mail-bounce'\nexport * from './lib/email-deliverability'\n// The sender's side of the same engine (AGL-3328): SPF, DKIM and DMARC for\n// the domain mail leaves from, and whether they align. Pure as well; the\n// lookups are `@aglyn/tenant-data-admin`'s.\nexport * from './lib/sender-readiness'\n// The outbound phishing screen a young workspace's campaigns and automated\n// email pass (AGL-3356). Pure; the hold it leads to is `tenant-data-admin`'s.\nexport * from './lib/outbound-phishing-screen'\n// The send seam's gate for that screen — the store a hold writes is injected\n// by `@aglyn/tenant-data-admin`, like the governor's.\nexport * from './lib/outbound-screen-gate'\n// The same screen read over a published page (AGL-3356): lookalike links and\n// embeds, author-defined credential fields, a brand's call to action.\nexport * from './lib/hosted-page-screen'\n// The site's theme palette as plain data (AGL-3370), so a palette token an\n// author picked resolves in mail. Built without MUI; `shared-ui-theme`'s\n// parity spec holds it equal to the theme.\nexport * from './lib/email-palette'\nexport * from './lib/email-design-chrome'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,cAAc,sBAAkB;AAChC,cAAc,qBAAiB;AAC/B,cAAc,6BAAyB;AACvC,cAAc,yBAAqB;AACnC,cAAc,0BAAsB;AACpC,cAAc,yBAAqB;AACnC,cAAc,2BAAuB;AACrC,cAAc,uBAAmB;AACjC,cAAc,mCAA+B;AAC7C,cAAc,wBAAoB;AAClC,cAAc,gCAA4B;AAC1C,cAAc,gCAA4B;AAC1C,cAAc,6BAAyB;AACvC,cAAc,wBAAoB;AAClC,cAAc,2BAAuB;AACrC,cAAc,0BAAsB;AACpC,yEAAyE;AACzE,cAAc,4BAAwB;AACtC,cAAc,iCAA6B;AAC3C,0EAA0E;AAC1E,4EAA4E;AAC5E,wEAAwE;AACxE,6EAA6E;AAC7E,wDAAwD;AACxD,cAAc,0BAAsB;AACpC,cAAc,gCAA4B;AAC1C,uEAAuE;AACvE,cAAc,2BAAuB;AACrC,0EAA0E;AAC1E,+EAA+E;AAC/E,cAAc,8BAA0B;AACxC,cAAc,uBAAmB;AACjC,4EAA4E;AAC5E,6EAA6E;AAC7E,6EAA6E;AAC7E,uDAAuD;AACvD,cAAc,wBAAoB;AAClC,cAAc,uBAAmB;AACjC,cAAc,gCAA4B;AAC1C,2EAA2E;AAC3E,yEAAyE;AACzE,4CAA4C;AAC5C,cAAc,4BAAwB;AACtC,2EAA2E;AAC3E,8EAA8E;AAC9E,cAAc,oCAAgC;AAC9C,6EAA6E;AAC7E,sDAAsD;AACtD,cAAc,gCAA4B;AAC1C,6EAA6E;AAC7E,sEAAsE;AACtE,cAAc,8BAA0B;AACxC,2EAA2E;AAC3E,yEAAyE;AACzE,2CAA2C;AAC3C,cAAc,yBAAqB;AACnC,cAAc,+BAA2B"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { EmailChrome } from './email-render';
|
|
18
|
+
/**
|
|
19
|
+
* WHICH PARTS OF THE SENDER'S HEADER AND FOOTER A DESIGNED EMAIL WEARS
|
|
20
|
+
* (AGL-3372).
|
|
21
|
+
*
|
|
22
|
+
* A design goes out inside the sender's chrome like the built-in copy does,
|
|
23
|
+
* except where the design already draws that part itself: an email that
|
|
24
|
+
* opens with its own logo band gets no second one above it, and one that
|
|
25
|
+
* closes with its own address and "why you get this" gets no second footer.
|
|
26
|
+
*
|
|
27
|
+
* Three ways a design says it has drawn a part:
|
|
28
|
+
*
|
|
29
|
+
* 1. A section marked with {@link EMAIL_ROLE_PROP}: the Besigner's Header and
|
|
30
|
+
* Footer presets carry the mark, and so does every reusable email block
|
|
31
|
+
* started from them, which reaches this function already grafted in.
|
|
32
|
+
* 2. A section in exactly the shape those presets had before they carried
|
|
33
|
+
* the mark — a logo over a subheading, or a divider over two captions —
|
|
34
|
+
* so a design made from them earlier is read the same way.
|
|
35
|
+
* 3. A placed reusable email block that says nothing about itself. Its tree
|
|
36
|
+
* is the author's own and could be either band, so the design is treated
|
|
37
|
+
* as drawing both: no chrome, which is where every such design stood
|
|
38
|
+
* before this function existed. Never two headers.
|
|
39
|
+
*/
|
|
40
|
+
/** The section prop a Header or Footer band carries. */
|
|
41
|
+
export declare const EMAIL_ROLE_PROP = "emailRole";
|
|
42
|
+
export type EmailRole = 'header' | 'footer';
|
|
43
|
+
/**
|
|
44
|
+
* The parts of a design's own chrome it draws, read from the tree as it will
|
|
45
|
+
* render (reusable blocks grafted in).
|
|
46
|
+
*/
|
|
47
|
+
export declare function designChromeRoles(composed: Readonly<Record<string, unknown>> | null | undefined): Set<EmailRole>;
|
|
48
|
+
/**
|
|
49
|
+
* The chrome a designed email is drawn inside: the sender's, less the parts
|
|
50
|
+
* the design draws itself. `undefined` when nothing of it is left.
|
|
51
|
+
*
|
|
52
|
+
* @param stored The design as saved, its reusable blocks still placements.
|
|
53
|
+
* Pass `null` when those placements will draw nothing in this send.
|
|
54
|
+
* @param composed The tree as it will render.
|
|
55
|
+
* @param chrome The sender's full chrome.
|
|
56
|
+
*/
|
|
57
|
+
export declare function chromeForDesign(input: {
|
|
58
|
+
stored: Readonly<Record<string, unknown>> | null | undefined;
|
|
59
|
+
composed: Readonly<Record<string, unknown>> | null | undefined;
|
|
60
|
+
chrome: EmailChrome | undefined;
|
|
61
|
+
}): EmailChrome | undefined;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
/**
|
|
18
|
+
* WHICH PARTS OF THE SENDER'S HEADER AND FOOTER A DESIGNED EMAIL WEARS
|
|
19
|
+
* (AGL-3372).
|
|
20
|
+
*
|
|
21
|
+
* A design goes out inside the sender's chrome like the built-in copy does,
|
|
22
|
+
* except where the design already draws that part itself: an email that
|
|
23
|
+
* opens with its own logo band gets no second one above it, and one that
|
|
24
|
+
* closes with its own address and "why you get this" gets no second footer.
|
|
25
|
+
*
|
|
26
|
+
* Three ways a design says it has drawn a part:
|
|
27
|
+
*
|
|
28
|
+
* 1. A section marked with {@link EMAIL_ROLE_PROP}: the Besigner's Header and
|
|
29
|
+
* Footer presets carry the mark, and so does every reusable email block
|
|
30
|
+
* started from them, which reaches this function already grafted in.
|
|
31
|
+
* 2. A section in exactly the shape those presets had before they carried
|
|
32
|
+
* the mark — a logo over a subheading, or a divider over two captions —
|
|
33
|
+
* so a design made from them earlier is read the same way.
|
|
34
|
+
* 3. A placed reusable email block that says nothing about itself. Its tree
|
|
35
|
+
* is the author's own and could be either band, so the design is treated
|
|
36
|
+
* as drawing both: no chrome, which is where every such design stood
|
|
37
|
+
* before this function existed. Never two headers.
|
|
38
|
+
*/ /** The section prop a Header or Footer band carries. */ export const EMAIL_ROLE_PROP = 'emailRole';
|
|
39
|
+
/** The component id a placed reusable block has in a stored design. */ const REUSABLE_INSTANCE = 'reusableInstance';
|
|
40
|
+
const asNode = (value)=>value && typeof value === 'object' ? value : undefined;
|
|
41
|
+
const childrenOf = (map, node)=>(Array.isArray(node.nodes) ? node.nodes : []).map((id)=>asNode(map[String(id)])).filter((child)=>Boolean(child));
|
|
42
|
+
/** The unmarked Header preset: a logo over one subheading. */ function isLegacyHeaderBand(map, node) {
|
|
43
|
+
var _kids__props;
|
|
44
|
+
const kids = childrenOf(map, node);
|
|
45
|
+
return kids.length === 2 && kids[0].componentId === 'emailImage' && kids[1].componentId === 'emailText' && ((_kids__props = kids[1].props) == null ? void 0 : _kids__props['variant']) === 'subheading';
|
|
46
|
+
}
|
|
47
|
+
/** The unmarked Footer preset: a divider over two captions. */ function isLegacyFooterBand(map, node) {
|
|
48
|
+
const kids = childrenOf(map, node);
|
|
49
|
+
return kids.length === 3 && kids[0].componentId === 'emailDivider' && kids.slice(1).every((kid)=>{
|
|
50
|
+
var _kid_props;
|
|
51
|
+
return kid.componentId === 'emailText' && ((_kid_props = kid.props) == null ? void 0 : _kid_props['variant']) === 'caption';
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The parts of a design's own chrome it draws, read from the tree as it will
|
|
56
|
+
* render (reusable blocks grafted in).
|
|
57
|
+
*/ export function designChromeRoles(composed) {
|
|
58
|
+
const roles = new Set();
|
|
59
|
+
for (const value of Object.values(composed != null ? composed : {})){
|
|
60
|
+
var _node_props;
|
|
61
|
+
const node = asNode(value);
|
|
62
|
+
if (!node || node.componentId !== 'emailSection') continue;
|
|
63
|
+
const role = (_node_props = node.props) == null ? void 0 : _node_props[EMAIL_ROLE_PROP];
|
|
64
|
+
if (role === 'header' || role === 'footer') roles.add(role);
|
|
65
|
+
else if (isLegacyHeaderBand(composed != null ? composed : {}, node)) roles.add('header');
|
|
66
|
+
else if (isLegacyFooterBand(composed != null ? composed : {}, node)) roles.add('footer');
|
|
67
|
+
}
|
|
68
|
+
return roles;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The chrome a designed email is drawn inside: the sender's, less the parts
|
|
72
|
+
* the design draws itself. `undefined` when nothing of it is left.
|
|
73
|
+
*
|
|
74
|
+
* @param stored The design as saved, its reusable blocks still placements.
|
|
75
|
+
* Pass `null` when those placements will draw nothing in this send.
|
|
76
|
+
* @param composed The tree as it will render.
|
|
77
|
+
* @param chrome The sender's full chrome.
|
|
78
|
+
*/ export function chromeForDesign(input) {
|
|
79
|
+
var _input_stored;
|
|
80
|
+
const { chrome } = input;
|
|
81
|
+
if (!chrome) return undefined;
|
|
82
|
+
const roles = designChromeRoles(input.composed);
|
|
83
|
+
const placesBlocks = Object.values((_input_stored = input.stored) != null ? _input_stored : {}).some((value)=>{
|
|
84
|
+
var _asNode;
|
|
85
|
+
return ((_asNode = asNode(value)) == null ? void 0 : _asNode.componentId) === REUSABLE_INSTANCE;
|
|
86
|
+
});
|
|
87
|
+
// An unmarked reusable block may be either band: never risk two headers.
|
|
88
|
+
if (placesBlocks && !roles.size) return undefined;
|
|
89
|
+
const header = roles.has('header') ? undefined : chrome.header;
|
|
90
|
+
const footer = roles.has('footer') ? undefined : chrome.footer;
|
|
91
|
+
if (!header && !footer) return undefined;
|
|
92
|
+
return _extends({}, header ? {
|
|
93
|
+
header
|
|
94
|
+
} : {}, footer ? {
|
|
95
|
+
footer
|
|
96
|
+
} : {});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
//# sourceMappingURL=email-design-chrome.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/email-design-chrome.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 type { EmailChrome } from './email-render'\n\n/**\n * WHICH PARTS OF THE SENDER'S HEADER AND FOOTER A DESIGNED EMAIL WEARS\n * (AGL-3372).\n *\n * A design goes out inside the sender's chrome like the built-in copy does,\n * except where the design already draws that part itself: an email that\n * opens with its own logo band gets no second one above it, and one that\n * closes with its own address and \"why you get this\" gets no second footer.\n *\n * Three ways a design says it has drawn a part:\n *\n * 1. A section marked with {@link EMAIL_ROLE_PROP}: the Besigner's Header and\n * Footer presets carry the mark, and so does every reusable email block\n * started from them, which reaches this function already grafted in.\n * 2. A section in exactly the shape those presets had before they carried\n * the mark — a logo over a subheading, or a divider over two captions —\n * so a design made from them earlier is read the same way.\n * 3. A placed reusable email block that says nothing about itself. Its tree\n * is the author's own and could be either band, so the design is treated\n * as drawing both: no chrome, which is where every such design stood\n * before this function existed. Never two headers.\n */\n\n/** The section prop a Header or Footer band carries. */\nexport const EMAIL_ROLE_PROP = 'emailRole'\n\nexport type EmailRole = 'header' | 'footer'\n\n/** The component id a placed reusable block has in a stored design. */\nconst REUSABLE_INSTANCE = 'reusableInstance'\n\ninterface DesignNode {\n componentId?: unknown\n props?: Record<string, unknown>\n nodes?: unknown\n}\n\nconst asNode = (value: unknown): DesignNode | undefined =>\n value && typeof value === 'object' ? (value as DesignNode) : undefined\n\nconst childrenOf = (\n map: Readonly<Record<string, unknown>>,\n node: DesignNode,\n): DesignNode[] =>\n (Array.isArray(node.nodes) ? node.nodes : [])\n .map((id) => asNode(map[String(id)]))\n .filter((child): child is DesignNode => Boolean(child))\n\n/** The unmarked Header preset: a logo over one subheading. */\nfunction isLegacyHeaderBand(map: Readonly<Record<string, unknown>>, node: DesignNode) {\n const kids = childrenOf(map, node)\n return (\n kids.length === 2 &&\n kids[0].componentId === 'emailImage' &&\n kids[1].componentId === 'emailText' &&\n kids[1].props?.['variant'] === 'subheading'\n )\n}\n\n/** The unmarked Footer preset: a divider over two captions. */\nfunction isLegacyFooterBand(map: Readonly<Record<string, unknown>>, node: DesignNode) {\n const kids = childrenOf(map, node)\n return (\n kids.length === 3 &&\n kids[0].componentId === 'emailDivider' &&\n kids.slice(1).every(\n (kid) => kid.componentId === 'emailText' && kid.props?.['variant'] === 'caption',\n )\n )\n}\n\n/**\n * The parts of a design's own chrome it draws, read from the tree as it will\n * render (reusable blocks grafted in).\n */\nexport function designChromeRoles(\n composed: Readonly<Record<string, unknown>> | null | undefined,\n): Set<EmailRole> {\n const roles = new Set<EmailRole>()\n for (const value of Object.values(composed ?? {})) {\n const node = asNode(value)\n if (!node || node.componentId !== 'emailSection') continue\n const role = node.props?.[EMAIL_ROLE_PROP]\n if (role === 'header' || role === 'footer') roles.add(role)\n else if (isLegacyHeaderBand(composed ?? {}, node)) roles.add('header')\n else if (isLegacyFooterBand(composed ?? {}, node)) roles.add('footer')\n }\n return roles\n}\n\n/**\n * The chrome a designed email is drawn inside: the sender's, less the parts\n * the design draws itself. `undefined` when nothing of it is left.\n *\n * @param stored The design as saved, its reusable blocks still placements.\n * Pass `null` when those placements will draw nothing in this send.\n * @param composed The tree as it will render.\n * @param chrome The sender's full chrome.\n */\nexport function chromeForDesign(input: {\n stored: Readonly<Record<string, unknown>> | null | undefined\n composed: Readonly<Record<string, unknown>> | null | undefined\n chrome: EmailChrome | undefined\n}): EmailChrome | undefined {\n const { chrome } = input\n if (!chrome) return undefined\n const roles = designChromeRoles(input.composed)\n const placesBlocks = Object.values(input.stored ?? {}).some(\n (value) => asNode(value)?.componentId === REUSABLE_INSTANCE,\n )\n // An unmarked reusable block may be either band: never risk two headers.\n if (placesBlocks && !roles.size) return undefined\n const header = roles.has('header') ? undefined : chrome.header\n const footer = roles.has('footer') ? undefined : chrome.footer\n if (!header && !footer) return undefined\n return { ...(header ? { header } : {}), ...(footer ? { footer } : {}) }\n}\n"],"names":["EMAIL_ROLE_PROP","REUSABLE_INSTANCE","asNode","value","undefined","childrenOf","map","node","Array","isArray","nodes","id","String","filter","child","Boolean","isLegacyHeaderBand","kids","length","componentId","props","isLegacyFooterBand","slice","every","kid","designChromeRoles","composed","roles","Set","Object","values","role","add","chromeForDesign","input","chrome","placesBlocks","stored","some","size","header","has","footer"],"mappings":"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"}
|
|
@@ -21,4 +21,6 @@
|
|
|
21
21
|
* end in one and a wrapping "(see https://…/a_(b))" is the rarer case.
|
|
22
22
|
* Anything the count says is unbalanced belongs to the prose.
|
|
23
23
|
*/
|
|
24
|
-
export declare function linkifyEscapedText(escaped: string
|
|
24
|
+
export declare function linkifyEscapedText(escaped: string,
|
|
25
|
+
/** The link color: the theme's accent text shade, when the sender has a theme. */
|
|
26
|
+
color?: string): string;
|
package/src/lib/email-linkify.js
CHANGED
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
* Balanced closing parens and brackets are kept, because a URL can genuinely
|
|
54
54
|
* end in one and a wrapping "(see https://…/a_(b))" is the rarer case.
|
|
55
55
|
* Anything the count says is unbalanced belongs to the prose.
|
|
56
|
-
*/ export function linkifyEscapedText(escaped) {
|
|
56
|
+
*/ export function linkifyEscapedText(escaped, /** The link color: the theme's accent text shade, when the sender has a theme. */ color = '#1a73e8') {
|
|
57
57
|
return escaped.replace(BARE_URL, (match)=>{
|
|
58
58
|
let url = match.replace(URL_TRAILING_PUNCTUATION, '');
|
|
59
59
|
// `)` and `]` alike: a token's value dropped into "[{{brand.supportUrl}}]"
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
}
|
|
67
67
|
if (!url) return match;
|
|
68
68
|
const trailer = match.slice(url.length);
|
|
69
|
-
return `<a href="${url}" target="_blank" ` + `style="color
|
|
69
|
+
return `<a href="${url}" target="_blank" ` + `style="color:${color};text-decoration:underline;word-break:break-word;">` + `${url}</a>${trailer}`;
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/email-linkify.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\n/**\n * Bare URLs in escaped text, made into links (AGL-3367).\n *\n * Shared by the two places a sender's plain words become HTML: the fallback\n * HTML part `sendEmail` synthesizes for a text-only message, and the\n * renderer's `emailText` block. The second is where a platform email's\n * composed body lands once it wears the system chrome, so without this a\n * digest that used to arrive with clickable links in its fallback part would\n * arrive branded and dead.\n */\n\n/**\n * Trailing characters stripped from a matched URL.\n *\n * A sentence that ends \"…see {@link https://example.com/billing}.\" puts the\n * period inside the match, because a period is a legal URL character and the\n * regex cannot tell prose from path. Closing brackets are handled separately\n * below, since a URL may legitimately end in one.\n *\n * `;` is deliberately absent: by the time this runs the text is already\n * escaped, so a query string reads `?a=1&b=2` and trimming `;` would cut\n * an entity in half and corrupt the link.\n */\nconst URL_TRAILING_PUNCTUATION = /[.,!?'\"]+$/\n\n/**\n * Bare absolute URLs in already-escaped text.\n *\n * Matched AFTER escaping, not before, so the href and the visible label are\n * the same string and neither can reintroduce markup: `&` inside a query\n * string is `&` by then, which is what an href attribute is supposed to\n * carry and parses back to `&` in the client.\n *\n * `http`/`https` only. Every URL our system copy emits is absolute — the\n * senders share one `consoleOrigin()` precisely because a mail client has no\n * page to resolve a relative path against — and matching bare `www.` or\n * addresses would turn ordinary prose into links nobody wrote.\n */\nconst BARE_URL = /https?:\\/\\/[^\\s<>\"]+/g\n\n/**\n * Links the bare URLs in one escaped line.\n *\n * Balanced closing parens and brackets are kept, because a URL can genuinely\n * end in one and a wrapping \"(see https://…/a_(b))\" is the rarer case.\n * Anything the count says is unbalanced belongs to the prose.\n */\nexport function linkifyEscapedText(escaped: string): string {\n return escaped.replace(BARE_URL, (match) => {\n let url = match.replace(URL_TRAILING_PUNCTUATION, '')\n // `)` and `]` alike: a token's value dropped into \"[{{brand.supportUrl}}]\"\n // closes on a bracket the URL never opened.\n for (;;) {\n const close = url.slice(-1)\n const open = close === ')' ? '(' : close === ']' ? '[' : ''\n if (!open || url.split(close).length <= url.split(open).length) break\n url = url.slice(0, -1)\n }\n if (!url) return match\n const trailer = match.slice(url.length)\n return (\n `<a href=\"${url}\" target=\"_blank\" ` +\n `style=\"color
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/email-linkify.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\n/**\n * Bare URLs in escaped text, made into links (AGL-3367).\n *\n * Shared by the two places a sender's plain words become HTML: the fallback\n * HTML part `sendEmail` synthesizes for a text-only message, and the\n * renderer's `emailText` block. The second is where a platform email's\n * composed body lands once it wears the system chrome, so without this a\n * digest that used to arrive with clickable links in its fallback part would\n * arrive branded and dead.\n */\n\n/**\n * Trailing characters stripped from a matched URL.\n *\n * A sentence that ends \"…see {@link https://example.com/billing}.\" puts the\n * period inside the match, because a period is a legal URL character and the\n * regex cannot tell prose from path. Closing brackets are handled separately\n * below, since a URL may legitimately end in one.\n *\n * `;` is deliberately absent: by the time this runs the text is already\n * escaped, so a query string reads `?a=1&b=2` and trimming `;` would cut\n * an entity in half and corrupt the link.\n */\nconst URL_TRAILING_PUNCTUATION = /[.,!?'\"]+$/\n\n/**\n * Bare absolute URLs in already-escaped text.\n *\n * Matched AFTER escaping, not before, so the href and the visible label are\n * the same string and neither can reintroduce markup: `&` inside a query\n * string is `&` by then, which is what an href attribute is supposed to\n * carry and parses back to `&` in the client.\n *\n * `http`/`https` only. Every URL our system copy emits is absolute — the\n * senders share one `consoleOrigin()` precisely because a mail client has no\n * page to resolve a relative path against — and matching bare `www.` or\n * addresses would turn ordinary prose into links nobody wrote.\n */\nconst BARE_URL = /https?:\\/\\/[^\\s<>\"]+/g\n\n/**\n * Links the bare URLs in one escaped line.\n *\n * Balanced closing parens and brackets are kept, because a URL can genuinely\n * end in one and a wrapping \"(see https://…/a_(b))\" is the rarer case.\n * Anything the count says is unbalanced belongs to the prose.\n */\nexport function linkifyEscapedText(\n escaped: string,\n /** The link color: the theme's accent text shade, when the sender has a theme. */\n color = '#1a73e8',\n): string {\n return escaped.replace(BARE_URL, (match) => {\n let url = match.replace(URL_TRAILING_PUNCTUATION, '')\n // `)` and `]` alike: a token's value dropped into \"[{{brand.supportUrl}}]\"\n // closes on a bracket the URL never opened.\n for (;;) {\n const close = url.slice(-1)\n const open = close === ')' ? '(' : close === ']' ? '[' : ''\n if (!open || url.split(close).length <= url.split(open).length) break\n url = url.slice(0, -1)\n }\n if (!url) return match\n const trailer = match.slice(url.length)\n return (\n `<a href=\"${url}\" target=\"_blank\" ` +\n `style=\"color:${color};text-decoration:underline;word-break:break-word;\">` +\n `${url}</a>${trailer}`\n )\n })\n}\n\n"],"names":["URL_TRAILING_PUNCTUATION","BARE_URL","linkifyEscapedText","escaped","color","replace","match","url","close","slice","open","split","length","trailer"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;CASC,GAED;;;;;;;;;;;CAWC,GACD,MAAMA,2BAA2B;AAEjC;;;;;;;;;;;;CAYC,GACD,MAAMC,WAAW;AAEjB;;;;;;CAMC,GACD,OAAO,SAASC,mBACdC,OAAe,EACf,gFAAgF,GAChFC,QAAQ,SAAS;IAEjB,OAAOD,QAAQE,OAAO,CAACJ,UAAU,CAACK;QAChC,IAAIC,MAAMD,MAAMD,OAAO,CAACL,0BAA0B;QAClD,2EAA2E;QAC3E,4CAA4C;QAC5C,OAAS;YACP,MAAMQ,QAAQD,IAAIE,KAAK,CAAC,CAAC;YACzB,MAAMC,OAAOF,UAAU,MAAM,MAAMA,UAAU,MAAM,MAAM;YACzD,IAAI,CAACE,QAAQH,IAAII,KAAK,CAACH,OAAOI,MAAM,IAAIL,IAAII,KAAK,CAACD,MAAME,MAAM,EAAE;YAChEL,MAAMA,IAAIE,KAAK,CAAC,GAAG,CAAC;QACtB;QACA,IAAI,CAACF,KAAK,OAAOD;QACjB,MAAMO,UAAUP,MAAMG,KAAK,CAACF,IAAIK,MAAM;QACtC,OACE,CAAC,SAAS,EAAEL,IAAI,kBAAkB,CAAC,GACnC,CAAC,aAAa,EAAEH,MAAM,mDAAmD,CAAC,GAC1E,GAAGG,IAAI,IAAI,EAAEM,SAAS;IAE1B;AACF"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The palette an email is painted with (AGL-3370).
|
|
19
|
+
*
|
|
20
|
+
* A color an author picks in the Besigner is stored as a palette TOKEN PATH
|
|
21
|
+
* (`primary.main`, `grey.600`), which MUI resolves on a web page and a mail
|
|
22
|
+
* client never will. So an email needs the same palette the site renders
|
|
23
|
+
* with, as plain data, before it writes a single `style` attribute.
|
|
24
|
+
*
|
|
25
|
+
* That palette is built by `@aglyn/shared-ui-theme` — MUI's `createTheme`,
|
|
26
|
+
* then `createResponsiveTheme`'s shade and accessibility passes — and this
|
|
27
|
+
* lib may not import it: a `type:util` package cannot stand on a `type:ui`
|
|
28
|
+
* one, and a server rendering mail has no business loading MUI to do it. What
|
|
29
|
+
* follows is that derivation ported, light scheme only, with the INPUT colors
|
|
30
|
+
* of the two bases copied and everything else computed the way MUI computes
|
|
31
|
+
* it. `email-palette-parity.spec.ts` in `shared-ui-theme` builds both and
|
|
32
|
+
* holds them equal, so the copy cannot drift without a red.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* A light-scheme MUI palette as plain data: `primary`, `secondary`,
|
|
36
|
+
* `tertiary`, `surface`, `error`, `warning`, `info`, `success` (each
|
|
37
|
+
* `{ main, light, dark, contrastText }`), `background`, `text`, `tint`,
|
|
38
|
+
* `divider`, `inputOutline`, `grey` and `common`.
|
|
39
|
+
*/
|
|
40
|
+
export type EmailPalette = Readonly<Record<string, unknown>>;
|
|
41
|
+
/** One authored palette color: a `main`, and any shade the author also set. */
|
|
42
|
+
export interface EmailPaletteColorInput {
|
|
43
|
+
main?: string;
|
|
44
|
+
light?: string;
|
|
45
|
+
dark?: string;
|
|
46
|
+
contrastText?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The shape of `HostThemeSchemeColors` (`@aglyn/shared-data-types`), spelled
|
|
50
|
+
* out so this lib needs no dependency on the types package for one shape. An
|
|
51
|
+
* interface has no index signature, so without this a site's typed colors
|
|
52
|
+
* would not be assignable to a plain record.
|
|
53
|
+
*/
|
|
54
|
+
export interface EmailPaletteSchemeColors {
|
|
55
|
+
primary?: EmailPaletteColorInput;
|
|
56
|
+
secondary?: EmailPaletteColorInput;
|
|
57
|
+
tertiary?: EmailPaletteColorInput;
|
|
58
|
+
surface?: EmailPaletteColorInput;
|
|
59
|
+
error?: EmailPaletteColorInput;
|
|
60
|
+
warning?: EmailPaletteColorInput;
|
|
61
|
+
info?: EmailPaletteColorInput;
|
|
62
|
+
success?: EmailPaletteColorInput;
|
|
63
|
+
background?: {
|
|
64
|
+
default?: string;
|
|
65
|
+
paper?: string;
|
|
66
|
+
};
|
|
67
|
+
text?: {
|
|
68
|
+
primary?: string;
|
|
69
|
+
secondary?: string;
|
|
70
|
+
disabled?: string;
|
|
71
|
+
};
|
|
72
|
+
tint?: {
|
|
73
|
+
primary?: string;
|
|
74
|
+
secondary?: string;
|
|
75
|
+
tertiary?: string;
|
|
76
|
+
};
|
|
77
|
+
divider?: string;
|
|
78
|
+
}
|
|
79
|
+
export interface EmailPaletteInput {
|
|
80
|
+
/**
|
|
81
|
+
* The base a site or sender layers onto: `'platform'` is the console brand
|
|
82
|
+
* (`consoleOptions`), `'tenant'` the neutral default a customer site wears
|
|
83
|
+
* (`tenantOptions`). {@link emailPaletteBaseForHost} picks it for a site.
|
|
84
|
+
*/
|
|
85
|
+
base: 'platform' | 'tenant';
|
|
86
|
+
/**
|
|
87
|
+
* A site's authored light-scheme colors — `host.theme.colorSchemes.light`.
|
|
88
|
+
* Stored data, so every leaf is checked as it is read; a plain record is
|
|
89
|
+
* accepted as readily as the typed shape.
|
|
90
|
+
*/
|
|
91
|
+
colors?: EmailPaletteSchemeColors | Readonly<Record<string, unknown>> | null;
|
|
92
|
+
/**
|
|
93
|
+
* A white-label org's brand color. It becomes `primary.main` and the whole
|
|
94
|
+
* `primary` record is re-derived from it, exactly as a theme given
|
|
95
|
+
* `palette.primary = { main }` would. Ignored unless it parses as a color.
|
|
96
|
+
*/
|
|
97
|
+
primaryColor?: string | null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The fully derived light palette a site or sender renders with: the
|
|
101
|
+
* chosen base, the site's authored colors layered one level deep (a color's
|
|
102
|
+
* whole record replaced, so MUI re-derives its shades from the new `main`),
|
|
103
|
+
* then a white-label `primaryColor` over that.
|
|
104
|
+
*
|
|
105
|
+
* Equal, for every token the Besigner's color picker offers, to the palette
|
|
106
|
+
* of `createResponsiveTheme` over `mergeThemeOptions(siteBaseOptions(…))`
|
|
107
|
+
* and the host theme's light `hostThemeToThemeOptions`.
|
|
108
|
+
*
|
|
109
|
+
* Where the site's colors cannot be derived at all — a `main` MUI cannot
|
|
110
|
+
* parse, on which `createTheme` itself throws — the email falls back to the
|
|
111
|
+
* base rather than failing the send: there is no rendered page to agree with.
|
|
112
|
+
*/
|
|
113
|
+
export declare function buildEmailPalette(input: EmailPaletteInput): EmailPalette;
|
|
114
|
+
/** The platform's own palette — the console brand, light scheme. */
|
|
115
|
+
export declare const PLATFORM_EMAIL_PALETTE: EmailPalette;
|
|
116
|
+
/**
|
|
117
|
+
* Resolves a stored color prop for an email's markup.
|
|
118
|
+
*
|
|
119
|
+
* - A palette token path (`primary.main`, `grey.600`, `divider`) resolves
|
|
120
|
+
* against `palette` to an opaque hex — or to nothing without a palette.
|
|
121
|
+
* - A literal CSS color (hex, `rgb()`/`rgba()`/`hsl()`/`hsla()`,
|
|
122
|
+
* `transparent`, a named color) passes through as written.
|
|
123
|
+
* - Anything else — an unknown token, garbage, an empty value, and anything
|
|
124
|
+
* that could step outside a `style` attribute — is `undefined`, so the
|
|
125
|
+
* caller uses its own default.
|
|
126
|
+
*/
|
|
127
|
+
export declare function resolveEmailColor(value: unknown, palette: EmailPalette | undefined): string | undefined;
|
|
128
|
+
/**
|
|
129
|
+
* Which base a site's mail layers onto: `'platform'` for the operator's own
|
|
130
|
+
* hosts, `'tenant'` for every customer site — the answer
|
|
131
|
+
* `wearsPlatformBrand(hostBrandKey(host))` gives the page itself, so a site's
|
|
132
|
+
* email and its published page start from the same palette.
|
|
133
|
+
*/
|
|
134
|
+
export declare function emailPaletteBaseForHost(host: {
|
|
135
|
+
cname?: string | null;
|
|
136
|
+
subdomain?: string | null;
|
|
137
|
+
} | null | undefined): 'platform' | 'tenant';
|