@samva/markup 0.6.0 → 0.8.0
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/AGENTS.md +94 -47
- package/README.md +1 -1
- package/dist/brand-DcoVkh_S.d.ts +88 -0
- package/dist/{catalog-60eaYC7b.d.ts → catalog-CKuOooB8.d.ts} +13 -15
- package/dist/{checks-DBweSIPN.js → checks-D73Xizbw.js} +414 -2
- package/dist/checks.js +1 -1
- package/dist/classes.d.ts +2 -3
- package/dist/compile-sms.d.ts +1 -1
- package/dist/compile-whatsapp.d.ts +1 -1
- package/dist/compiler.d.ts +62 -45
- package/dist/compiler.js +206 -863
- package/dist/components.d.ts +2 -2
- package/dist/{corpus-BjYMqOoY.d.ts → corpus-CPPfgAHN.d.ts} +14 -14
- package/dist/{css-COcFOT1y.d.ts → css-CS2jLID6.d.ts} +28 -2
- package/dist/email/assets.d.ts +40 -0
- package/dist/email/assets.js +63 -0
- package/dist/email/brand-footer.d.ts +34 -0
- package/dist/email/brand-footer.js +62 -0
- package/dist/email/brand.d.ts +2 -0
- package/dist/email/brand.js +148 -0
- package/dist/email/html.d.ts +2 -42
- package/dist/email/html.js +11 -0
- package/dist/email/render.d.ts +12 -5
- package/dist/email/render.js +2 -5
- package/dist/email/selection.d.ts +2 -91
- package/dist/email/stylesheets.d.ts +1 -1
- package/dist/email/stylesheets.js +3 -0
- package/dist/emit.js +2 -2
- package/dist/html-Bl9BAdqc.d.ts +42 -0
- package/dist/{ops-2aiiYO7m.js → ops-BvwqHl7V.js} +2 -2
- package/dist/ops.js +1 -1
- package/dist/{parser-CQcASEyy.d.ts → parser-t29cHLVL.d.ts} +2 -2
- package/dist/parser.d.ts +1 -1
- package/dist/parser.js +1 -2
- package/dist/registry-Dcp6QEK_.d.ts +73 -0
- package/dist/registry.d.ts +2 -42
- package/dist/{render-aaJBnsXK.js → render-vAv_4r9v.js} +174 -17
- package/dist/selection-CCArx9lU.d.ts +91 -0
- package/dist/{showcase-DGZAWn4Z.d.ts → showcase-egkusQBf.d.ts} +15 -56
- package/dist/{presets → starters}/category-mapping.d.ts +5 -5
- package/dist/{presets → starters}/category-mapping.js +5 -5
- package/dist/starters/corpus.d.ts +2 -0
- package/dist/{presets → starters}/corpus.js +15 -15
- package/dist/starters/library.d.ts +18 -0
- package/dist/starters/library.js +692 -0
- package/dist/{presets → starters}/public-gallery.d.ts +47 -47
- package/dist/{presets → starters}/public-gallery.js +92 -92
- package/dist/{presets → starters}/public-handoff.d.ts +14 -14
- package/dist/{presets → starters}/public-handoff.js +40 -40
- package/dist/{presets → starters}/public-preview-artifacts.d.ts +4 -4
- package/dist/{presets → starters}/public-preview-artifacts.js +15 -15
- package/dist/{presets → starters}/public-preview.d.ts +1 -1
- package/dist/{presets → starters}/public-preview.js +3 -3
- package/dist/starters/showcase.d.ts +2 -0
- package/dist/starters/showcase.js +335 -0
- package/dist/starters/sources.d.ts +45 -0
- package/dist/{presets → starters}/sources.js +1951 -272
- package/dist/starters/theme.d.ts +45 -0
- package/dist/starters/theme.js +77 -0
- package/dist/starters.d.ts +2 -0
- package/dist/{presets.js → starters.js} +4 -11
- package/dist/tailwind-BYq7Lo6u.js +727 -0
- package/dist/theme-css-hFqcPSar.js +587 -0
- package/dist/theme-css.d.ts +56 -15
- package/dist/theme-css.js +2 -509
- package/dist/tsx-source-instrumentation.d.ts +9 -0
- package/dist/tsx-source-instrumentation.js +20 -2
- package/dist/tsx-tracer.js +3 -3
- package/dist/unsubscribe-link-CoesnQDU.js +26 -0
- package/package.json +56 -44
- package/samva-brand.d.ts +14 -0
- package/dist/footer.d.ts +0 -66
- package/dist/footer.js +0 -93
- package/dist/presets/corpus.d.ts +0 -2
- package/dist/presets/showcase.d.ts +0 -2
- package/dist/presets/showcase.js +0 -627
- package/dist/presets/sources.d.ts +0 -38
- package/dist/presets.d.ts +0 -2
- package/dist/theme-6ApjjiBo.d.ts +0 -116
- package/dist/theme.d.ts +0 -2
- package/dist/theme.js +0 -118
- package/dist/tokens.d.ts +0 -6
- package/dist/tokens.js +0 -417
package/dist/components.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { f as ElementNode } from "./ast-BrDtApwS.js";
|
|
2
|
-
import { i as EMAIL_TAG_SPECS, l as WHATSAPP_TAG_SPECS, o as TagSpec, t as AttributeSpec } from "./parser-
|
|
2
|
+
import { i as EMAIL_TAG_SPECS, l as WHATSAPP_TAG_SPECS, o as TagSpec, t as AttributeSpec } from "./parser-t29cHLVL.js";
|
|
3
3
|
import { r as PredicateRef, s as VariableRef } from "./variables-J_L3qxRa.js";
|
|
4
4
|
import { s as SmlChildren } from "./jsx-runtime-I-1nCtl2.js";
|
|
5
5
|
//#region src/components.d.ts
|
|
@@ -244,7 +244,7 @@ declare const WhatsApp: (props: PropsOf<{
|
|
|
244
244
|
readonly category: {
|
|
245
245
|
readonly type: "string";
|
|
246
246
|
readonly required: true;
|
|
247
|
-
readonly values: readonly ["marketing", "utility", "authentication"
|
|
247
|
+
readonly values: readonly ["marketing", "utility", "authentication"];
|
|
248
248
|
};
|
|
249
249
|
};
|
|
250
250
|
readonly content: "whatsapp";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as OwnedShowcaseId, t as BrandRecreationShowcaseId } from "./showcase-
|
|
1
|
+
import { n as OwnedShowcaseId, t as BrandRecreationShowcaseId } from "./showcase-egkusQBf.js";
|
|
2
2
|
import { Result } from "better-result";
|
|
3
|
-
//#region
|
|
3
|
+
//#region starters/corpus.d.ts
|
|
4
4
|
type TemplateCorpusKind = "owned-editable-sml" | "brand-recreation-sml" | "inspiration-reference";
|
|
5
5
|
declare const templateFacetOptions: {
|
|
6
6
|
readonly jobType: readonly ["account-access", "commerce", "product-communication", "lifecycle", "publishing-campaigns"];
|
|
@@ -32,8 +32,8 @@ interface CorpusTakedown {
|
|
|
32
32
|
readonly contact: string;
|
|
33
33
|
readonly procedureUrl: string;
|
|
34
34
|
}
|
|
35
|
-
interface
|
|
36
|
-
readonly type: "
|
|
35
|
+
interface OwnedEditableStarter {
|
|
36
|
+
readonly type: "OwnedEditableStarter";
|
|
37
37
|
readonly corpusKind: "owned-editable-sml";
|
|
38
38
|
readonly provenance: {
|
|
39
39
|
readonly owner: "Samva";
|
|
@@ -45,8 +45,8 @@ interface OwnedEditablePreset {
|
|
|
45
45
|
readonly retention: CorpusRetention;
|
|
46
46
|
readonly takedown: CorpusTakedown;
|
|
47
47
|
}
|
|
48
|
-
interface
|
|
49
|
-
readonly type: "
|
|
48
|
+
interface BrandRecreationStarter {
|
|
49
|
+
readonly type: "BrandRecreationStarter";
|
|
50
50
|
readonly corpusKind: "brand-recreation-sml";
|
|
51
51
|
readonly provenance: {
|
|
52
52
|
readonly owner: "Samva";
|
|
@@ -64,13 +64,13 @@ interface BrandRecreationPreset {
|
|
|
64
64
|
readonly takedown: CorpusTakedown;
|
|
65
65
|
}
|
|
66
66
|
/** Clean-room recreations of a named brand's own email, authored as editable Samva projects. */
|
|
67
|
-
declare const brandRecreationProvenance: Record<BrandRecreationShowcaseId,
|
|
67
|
+
declare const brandRecreationProvenance: Record<BrandRecreationShowcaseId, BrandRecreationStarter["provenance"]>;
|
|
68
68
|
/**
|
|
69
|
-
* Owned
|
|
70
|
-
* Every other owned
|
|
69
|
+
* Owned starters authored as complete showcase projects with their own stylesheet and assets.
|
|
70
|
+
* Every other owned starter is the single module `packages/markup/starters/<id>.tsx`.
|
|
71
71
|
*/
|
|
72
|
-
declare const
|
|
73
|
-
declare const
|
|
72
|
+
declare const ownedShowcaseStarterSources: Record<OwnedShowcaseId, string>;
|
|
73
|
+
declare const ownedStarterSourcePath: (id: string) => string;
|
|
74
74
|
interface InspirationReference {
|
|
75
75
|
readonly type: "InspirationReference";
|
|
76
76
|
readonly corpusKind: "inspiration-reference";
|
|
@@ -85,7 +85,7 @@ interface InspirationReference {
|
|
|
85
85
|
readonly retention: CorpusRetention;
|
|
86
86
|
readonly takedown: CorpusTakedown;
|
|
87
87
|
}
|
|
88
|
-
type TemplateCorpusRecord =
|
|
88
|
+
type TemplateCorpusRecord = OwnedEditableStarter | BrandRecreationStarter | InspirationReference;
|
|
89
89
|
interface PublicCorpusRegistration {
|
|
90
90
|
readonly id: string;
|
|
91
91
|
readonly corpus: TemplateCorpusRecord;
|
|
@@ -102,5 +102,5 @@ type TemplateFacetQuery = Partial<TemplateFacetSet>;
|
|
|
102
102
|
/** Strict intersection: every selected value in every selected dimension must match. */
|
|
103
103
|
declare const matchesTemplateFacets: (facets: TemplateFacetSet, query: TemplateFacetQuery) => boolean;
|
|
104
104
|
//#endregion
|
|
105
|
-
export { PublicCorpusRegistrationIssue as a, TemplateFacetQuery as c, matchesTemplateFacets as d,
|
|
106
|
-
//# sourceMappingURL=corpus-
|
|
105
|
+
export { PublicCorpusRegistrationIssue as a, TemplateFacetQuery as c, matchesTemplateFacets as d, ownedShowcaseStarterSources as f, templateFacetOptions as h, PublicCorpusRegistration as i, TemplateFacetSet as l, registerPublicCorpusEntry as m, InspirationReference as n, TemplateCorpusKind as o, ownedStarterSourcePath as p, OwnedEditableStarter as r, TemplateCorpusRecord as s, BrandRecreationStarter as t, brandRecreationProvenance as u };
|
|
106
|
+
//# sourceMappingURL=corpus-CPPfgAHN.d.ts.map
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { r as EmailDiagnostic } from "./diagnostics-BMYtBCQk.js";
|
|
2
2
|
import { m as EmailNode } from "./elements-BzjjnoCS.js";
|
|
3
3
|
import "css-tree";
|
|
4
|
+
//#region src/email/font-stacks.d.ts
|
|
5
|
+
/** One `@font-face` rule, its `src` already resolved to served URLs. */
|
|
6
|
+
interface FontFaceRule {
|
|
7
|
+
/** The family the rule declares, unquoted. */
|
|
8
|
+
readonly family: string;
|
|
9
|
+
/** The rule as it goes into the email's `<head>`. */
|
|
10
|
+
readonly css: string;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/email/fonts.d.ts
|
|
14
|
+
/**
|
|
15
|
+
* Maps a relative `src` URL, as authored, to the URL its project file is served
|
|
16
|
+
* from, or `undefined` when the project has no such file. The stylesheet's host
|
|
17
|
+
* owns this: it knows which file the stylesheet came from and the asset base.
|
|
18
|
+
*/
|
|
19
|
+
type FontFileResolver = (reference: string) => string | undefined;
|
|
20
|
+
//#endregion
|
|
4
21
|
//#region src/email/selectors.d.ts
|
|
5
22
|
type EmailElementNode = Extract<EmailNode, {
|
|
6
23
|
type: "Element";
|
|
@@ -65,8 +82,10 @@ interface CssRule {
|
|
|
65
82
|
}
|
|
66
83
|
interface Stylesheet {
|
|
67
84
|
readonly rules: readonly CssRule[];
|
|
68
|
-
/** `@
|
|
85
|
+
/** `@page`, `@keyframes` and other at-rules kept verbatim in `<head>`. */
|
|
69
86
|
readonly headAtRules: readonly string[];
|
|
87
|
+
/** Validated `@font-face` rules, `src` resolved; the render hides them from classic Outlook. */
|
|
88
|
+
readonly fontFaces: readonly FontFaceRule[];
|
|
70
89
|
/** Unconditional custom properties, for resolving `var()` before inlining. */
|
|
71
90
|
readonly variables: Readonly<Record<string, string>>;
|
|
72
91
|
readonly diagnostics: readonly EmailDiagnostic[];
|
|
@@ -77,7 +96,14 @@ declare const compileSelector: (selector: string) => CompiledSelector | undefine
|
|
|
77
96
|
declare const parseStylesheet: (source: string, options?: {
|
|
78
97
|
readonly origin?: string;
|
|
79
98
|
readonly startOrder?: number;
|
|
99
|
+
/** Resolves a relative `@font-face` src to the URL its project file is served from. */
|
|
100
|
+
readonly resolveFontFile?: FontFileResolver | undefined;
|
|
101
|
+
/**
|
|
102
|
+
* The sheet belongs to the project theme, whose `@import`s the Tailwind
|
|
103
|
+
* compile resolves; they are layering, not something to report here.
|
|
104
|
+
*/
|
|
105
|
+
readonly themeImports?: boolean | undefined;
|
|
80
106
|
}) => Stylesheet;
|
|
81
107
|
//#endregion
|
|
82
108
|
export { compileSelector as a, CompiledSelector as c, matchesSelector as d, Stylesheet as i, Compound as l, CssRule as n, parseStylesheet as o, Specificity as r, Ancestry as s, CssDeclaration as t, classListOf as u };
|
|
83
|
-
//# sourceMappingURL=css-
|
|
109
|
+
//# sourceMappingURL=css-CS2jLID6.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { r as EmailDiagnostic } from "../diagnostics-BMYtBCQk.js";
|
|
2
|
+
//#region src/email/assets.d.ts
|
|
3
|
+
/** Files a template may import, and what they are served as. */
|
|
4
|
+
declare const ASSET_CONTENT_TYPES: Readonly<Record<string, string>>;
|
|
5
|
+
interface AssetEntry {
|
|
6
|
+
/** Project-relative path the template imported. */
|
|
7
|
+
readonly path: string;
|
|
8
|
+
/** SHA-256 of the exact bytes, lowercase hex. */
|
|
9
|
+
readonly digest: string;
|
|
10
|
+
readonly bytes: number;
|
|
11
|
+
readonly contentType: string;
|
|
12
|
+
/** `<digest><ext>` — the name the asset has under any base. */
|
|
13
|
+
readonly fileName: string;
|
|
14
|
+
}
|
|
15
|
+
interface AssetManifest {
|
|
16
|
+
/** Where these assets are served from in this build. */
|
|
17
|
+
readonly base: string;
|
|
18
|
+
readonly assets: readonly AssetEntry[];
|
|
19
|
+
}
|
|
20
|
+
/** True when the compiler turns this import into a content-addressed asset. */
|
|
21
|
+
declare const isAssetPath: (path: string) => boolean;
|
|
22
|
+
/** Address one file by its content. */
|
|
23
|
+
declare const assetEntry: (path: string, bytes: Uint8Array) => Promise<AssetEntry>;
|
|
24
|
+
/**
|
|
25
|
+
* The URL an asset has under a base. The base is a build input: publication
|
|
26
|
+
* passes its hosted origin, and a local export passes whatever it will serve
|
|
27
|
+
* the exported files from.
|
|
28
|
+
*/
|
|
29
|
+
declare const assetUrl: (entry: AssetEntry, base: string) => string;
|
|
30
|
+
/** Record, but never fetch, a URL the template points at. */
|
|
31
|
+
declare const isExternalAsset: (reference: string) => boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Reject an asset base that would produce URLs a client will not load. The
|
|
34
|
+
* relative forms are the ones a page on disk resolves against its own
|
|
35
|
+
* directory, which is what a local export writes.
|
|
36
|
+
*/
|
|
37
|
+
declare const checkAssetBase: (base: string) => readonly EmailDiagnostic[];
|
|
38
|
+
//#endregion
|
|
39
|
+
export { ASSET_CONTENT_TYPES, AssetEntry, AssetManifest, assetEntry, assetUrl, checkAssetBase, isAssetPath, isExternalAsset };
|
|
40
|
+
//# sourceMappingURL=assets.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//#region src/email/assets.ts
|
|
2
|
+
/** Files a template may import, and what they are served as. */
|
|
3
|
+
const ASSET_CONTENT_TYPES = {
|
|
4
|
+
".png": "image/png",
|
|
5
|
+
".jpg": "image/jpeg",
|
|
6
|
+
".jpeg": "image/jpeg",
|
|
7
|
+
".gif": "image/gif",
|
|
8
|
+
".webp": "image/webp",
|
|
9
|
+
".avif": "image/avif",
|
|
10
|
+
".svg": "image/svg+xml",
|
|
11
|
+
".ico": "image/x-icon",
|
|
12
|
+
".woff": "font/woff",
|
|
13
|
+
".woff2": "font/woff2",
|
|
14
|
+
".ttf": "font/ttf",
|
|
15
|
+
".otf": "font/otf",
|
|
16
|
+
".pdf": "application/pdf",
|
|
17
|
+
".txt": "text/plain"
|
|
18
|
+
};
|
|
19
|
+
const extensionOf = (path) => {
|
|
20
|
+
const dot = path.lastIndexOf(".");
|
|
21
|
+
return dot < 0 ? "" : path.slice(dot).toLowerCase();
|
|
22
|
+
};
|
|
23
|
+
/** True when the compiler turns this import into a content-addressed asset. */
|
|
24
|
+
const isAssetPath = (path) => Object.hasOwn(ASSET_CONTENT_TYPES, extensionOf(path));
|
|
25
|
+
const hex = (buffer) => Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
26
|
+
/** Address one file by its content. */
|
|
27
|
+
const assetEntry = async (path, bytes) => {
|
|
28
|
+
const extension = extensionOf(path);
|
|
29
|
+
const view = new Uint8Array(bytes.byteLength);
|
|
30
|
+
view.set(bytes);
|
|
31
|
+
const digest = hex(await crypto.subtle.digest("SHA-256", view));
|
|
32
|
+
return {
|
|
33
|
+
path,
|
|
34
|
+
digest,
|
|
35
|
+
bytes: bytes.byteLength,
|
|
36
|
+
contentType: ASSET_CONTENT_TYPES[extension] ?? "application/octet-stream",
|
|
37
|
+
fileName: `${digest}${extension}`
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The URL an asset has under a base. The base is a build input: publication
|
|
42
|
+
* passes its hosted origin, and a local export passes whatever it will serve
|
|
43
|
+
* the exported files from.
|
|
44
|
+
*/
|
|
45
|
+
const assetUrl = (entry, base) => `${base.replace(/\/+$/, "")}/${entry.fileName}`;
|
|
46
|
+
const EXTERNAL_IMAGE = /^https:\/\//i;
|
|
47
|
+
/** Record, but never fetch, a URL the template points at. */
|
|
48
|
+
const isExternalAsset = (reference) => EXTERNAL_IMAGE.test(reference);
|
|
49
|
+
/**
|
|
50
|
+
* Reject an asset base that would produce URLs a client will not load. The
|
|
51
|
+
* relative forms are the ones a page on disk resolves against its own
|
|
52
|
+
* directory, which is what a local export writes.
|
|
53
|
+
*/
|
|
54
|
+
const checkAssetBase = (base) => /^(https:\/\/|\.{1,2}\/|\/)/i.test(base) ? [] : [{
|
|
55
|
+
code: "invalid-asset-base",
|
|
56
|
+
severity: "error",
|
|
57
|
+
message: `Asset base ${JSON.stringify(base)} must be an https URL, an absolute path, or a relative path for a local export.`,
|
|
58
|
+
origins: []
|
|
59
|
+
}];
|
|
60
|
+
//#endregion
|
|
61
|
+
export { ASSET_CONTENT_TYPES, assetEntry, assetUrl, checkAssetBase, isAssetPath, isExternalAsset };
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=assets.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { t as EmailContent } from "../template-DWftqh4x.js";
|
|
2
|
+
import { RenderedEmail } from "./render.js";
|
|
3
|
+
import { a as EmailBrand, n as BrandComponents } from "../brand-DcoVkh_S.js";
|
|
4
|
+
//#region src/email/brand-footer.d.ts
|
|
5
|
+
/** A footer block for the send path to append, and its plain-text tail. */
|
|
6
|
+
interface RenderedBrandFooter {
|
|
7
|
+
readonly html: string;
|
|
8
|
+
readonly text: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Render `BrandFooter` against a brand on its own, for the send path to
|
|
12
|
+
* append to a message whose template renders no footer of its own. It
|
|
13
|
+
* compiles the same component, styled by the same brand theme, a template
|
|
14
|
+
* would get from `samva:brand`. Without `unsubscribe` the unsubscribe row is
|
|
15
|
+
* left out.
|
|
16
|
+
*/
|
|
17
|
+
declare const renderBrandFooter: (brand: EmailBrand, options: {
|
|
18
|
+
readonly unsubscribe: boolean;
|
|
19
|
+
}) => Promise<RenderedBrandFooter>;
|
|
20
|
+
/**
|
|
21
|
+
* Render content against a brand as a project whose `theme.css` is
|
|
22
|
+
* `@import "samva:brand";` renders it: Tailwind compiles the brand theme for
|
|
23
|
+
* the classes the content names, `BrandLogo` and `BrandFooter` are bound to the
|
|
24
|
+
* brand, and the render inlines the result. A brand preview renders through
|
|
25
|
+
* this, so it shows what a template importing the brand compiles to.
|
|
26
|
+
*/
|
|
27
|
+
declare const renderBrandSample: (brand: EmailBrand, sample: {
|
|
28
|
+
/** Every class the content names; nothing is discovered from source here. */
|
|
29
|
+
readonly classes: readonly string[];
|
|
30
|
+
readonly render: (components: BrandComponents) => EmailContent;
|
|
31
|
+
}) => Promise<RenderedEmail>;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { RenderedBrandFooter, renderBrandFooter, renderBrandSample };
|
|
34
|
+
//# sourceMappingURL=brand-footer.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { a as renderEmailContent, o as applyStylesheet, r as compileEmail } from "../render-vAv_4r9v.js";
|
|
2
|
+
import { EmailCompileError } from "./diagnostics.js";
|
|
3
|
+
import { BRAND_COMPONENT_CLASSES, brandComponents, withoutUnsubscribeRow } from "./brand.js";
|
|
4
|
+
import { c as parseBrandCss, i as brandTailwindCss } from "../theme-css-hFqcPSar.js";
|
|
5
|
+
import { t as compileTailwind, u as parseStylesheet } from "../tailwind-BYq7Lo6u.js";
|
|
6
|
+
//#region src/email/brand-footer.ts
|
|
7
|
+
/** The brand theme as Tailwind compiles it; a theme outside the grammar is a compile refusal. */
|
|
8
|
+
const brandTheme = (brand) => {
|
|
9
|
+
const parsed = parseBrandCss(brand.css);
|
|
10
|
+
if (parsed.isErr()) throw new EmailCompileError({ diagnostics: parsed.error.map((finding) => ({
|
|
11
|
+
code: finding.code,
|
|
12
|
+
severity: "error",
|
|
13
|
+
message: `samva:brand: ${finding.message}`,
|
|
14
|
+
origins: []
|
|
15
|
+
})) });
|
|
16
|
+
return brandTailwindCss(parsed.value);
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Render `BrandFooter` against a brand on its own, for the send path to
|
|
20
|
+
* append to a message whose template renders no footer of its own. It
|
|
21
|
+
* compiles the same component, styled by the same brand theme, a template
|
|
22
|
+
* would get from `samva:brand`. Without `unsubscribe` the unsubscribe row is
|
|
23
|
+
* left out.
|
|
24
|
+
*/
|
|
25
|
+
const renderBrandFooter = async (brand, options) => {
|
|
26
|
+
const tailwind = await compileTailwind(BRAND_COMPONENT_CLASSES, { css: brandTheme(brand) });
|
|
27
|
+
const node = brandComponents(brand).BrandFooter({});
|
|
28
|
+
if (node === null) return {
|
|
29
|
+
html: "",
|
|
30
|
+
text: ""
|
|
31
|
+
};
|
|
32
|
+
const styled = applyStylesheet(node, parseStylesheet(tailwind.css, { origin: "samva:brand" }));
|
|
33
|
+
const compiled = compileEmail(styled.tree);
|
|
34
|
+
const rendered = options.unsubscribe ? compiled : withoutUnsubscribeRow({
|
|
35
|
+
html: compiled.html,
|
|
36
|
+
text: compiled.text
|
|
37
|
+
});
|
|
38
|
+
const text = (rendered.text ?? "").trim();
|
|
39
|
+
return {
|
|
40
|
+
html: rendered.html ?? "",
|
|
41
|
+
text: text === "" ? "" : `\n\n---\n${text}`
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Render content against a brand as a project whose `theme.css` is
|
|
46
|
+
* `@import "samva:brand";` renders it: Tailwind compiles the brand theme for
|
|
47
|
+
* the classes the content names, `BrandLogo` and `BrandFooter` are bound to the
|
|
48
|
+
* brand, and the render inlines the result. A brand preview renders through
|
|
49
|
+
* this, so it shows what a template importing the brand compiles to.
|
|
50
|
+
*/
|
|
51
|
+
const renderBrandSample = async (brand, sample) => {
|
|
52
|
+
const tailwind = await compileTailwind([...BRAND_COMPONENT_CLASSES, ...sample.classes], { css: "@import \"samva:brand\";" }, {
|
|
53
|
+
stylesheets: /* @__PURE__ */ new Map(),
|
|
54
|
+
brandCss: brandTheme(brand)
|
|
55
|
+
});
|
|
56
|
+
const components = brandComponents(brand);
|
|
57
|
+
return renderEmailContent(parseStylesheet(tailwind.css, { origin: "samva:brand" }), () => sample.render(components));
|
|
58
|
+
};
|
|
59
|
+
//#endregion
|
|
60
|
+
export { renderBrandFooter, renderBrandSample };
|
|
61
|
+
|
|
62
|
+
//# sourceMappingURL=brand-footer.js.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as EmailBrand, c as EmailBrandFooter, d as RenderedFooterMarkers, f as UNSUBSCRIBE_ROW_ATTRIBUTE, g as UNSUBSCRIBE_URL_PLACEHOLDER, h as withoutUnsubscribeRow, i as BrandLogoProps, l as EmailBrandSocialLink, m as renderedFooterMarkers, n as BrandComponents, o as EmailBrandAddress, p as brandComponents, r as BrandFooterProps, s as EmailBrandAssets, t as BRAND_COMPONENT_CLASSES, u as FOOTER_ATTRIBUTE } from "../brand-DcoVkh_S.js";
|
|
2
|
+
export { BRAND_COMPONENT_CLASSES, BrandComponents, BrandFooterProps, BrandLogoProps, EmailBrand, EmailBrandAddress, EmailBrandAssets, EmailBrandFooter, EmailBrandSocialLink, FOOTER_ATTRIBUTE, RenderedFooterMarkers, UNSUBSCRIBE_ROW_ATTRIBUTE, UNSUBSCRIBE_URL_PLACEHOLDER, brandComponents, renderedFooterMarkers, withoutUnsubscribeRow };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Fragment, jsx } from "./jsx-runtime.js";
|
|
2
|
+
import { i as UNSUBSCRIBE_URL_PLACEHOLDER, r as UNSUBSCRIBE_LINK, t as BRAND_FOOTER_MARKER } from "../unsubscribe-link-CoesnQDU.js";
|
|
3
|
+
//#region src/email/brand.ts
|
|
4
|
+
/** Marks the block the send path recognizes as the template's own footer. */
|
|
5
|
+
const FOOTER_ATTRIBUTE = "data-samva-footer";
|
|
6
|
+
/** Marks the footer row that carries the unsubscribe link. */
|
|
7
|
+
const UNSUBSCRIBE_ROW_ATTRIBUTE = "data-samva-unsubscribe";
|
|
8
|
+
/** Every class the brand components use, compiled for any project that imports them. */
|
|
9
|
+
const BRAND_COMPONENT_CLASSES = [
|
|
10
|
+
"dark:hidden!",
|
|
11
|
+
"hidden",
|
|
12
|
+
"dark:block!",
|
|
13
|
+
"text-xs",
|
|
14
|
+
"text-gray-500",
|
|
15
|
+
"text-center",
|
|
16
|
+
"font-semibold",
|
|
17
|
+
"underline"
|
|
18
|
+
];
|
|
19
|
+
const addressLines = (address) => [
|
|
20
|
+
address.street1,
|
|
21
|
+
address.street2,
|
|
22
|
+
[
|
|
23
|
+
address.city,
|
|
24
|
+
address.region,
|
|
25
|
+
address.postalCode
|
|
26
|
+
].filter((part) => part.trim() !== "").join(", "),
|
|
27
|
+
address.country
|
|
28
|
+
].filter((line) => line !== void 0 && line.trim() !== "");
|
|
29
|
+
const classes = (...names) => {
|
|
30
|
+
const joined = names.filter((name) => name !== void 0 && name !== "").join(" ");
|
|
31
|
+
return joined === "" ? void 0 : joined;
|
|
32
|
+
};
|
|
33
|
+
const row = (children, props = {}) => jsx("tr", {
|
|
34
|
+
...props,
|
|
35
|
+
children: jsx("td", {
|
|
36
|
+
style: { padding: "0 0 8px 0" },
|
|
37
|
+
children
|
|
38
|
+
})
|
|
39
|
+
});
|
|
40
|
+
/** Bind `BrandLogo` and `BrandFooter` to one brand. */
|
|
41
|
+
const brandComponents = (brand) => {
|
|
42
|
+
const assets = brand.assets ?? {};
|
|
43
|
+
const footer = brand.footer ?? {};
|
|
44
|
+
const BrandLogo = ({ width = 120, alt, align, className }) => {
|
|
45
|
+
if (assets.logoUrl === void 0) return null;
|
|
46
|
+
const label = alt ?? footer.companyName ?? "";
|
|
47
|
+
const image = (src, imageClass, imageAlt) => jsx("img", {
|
|
48
|
+
src,
|
|
49
|
+
alt: imageAlt,
|
|
50
|
+
width,
|
|
51
|
+
border: 0,
|
|
52
|
+
...align === void 0 ? {} : { align },
|
|
53
|
+
className: classes(imageClass, className),
|
|
54
|
+
style: {
|
|
55
|
+
display: "block",
|
|
56
|
+
outline: "none",
|
|
57
|
+
border: "none",
|
|
58
|
+
height: "auto"
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
if (assets.logoDarkUrl === void 0) return image(assets.logoUrl, void 0, label);
|
|
62
|
+
return jsx(Fragment, { children: [image(assets.logoUrl, "dark:hidden!", label), jsx("div", {
|
|
63
|
+
className: "hidden dark:block!",
|
|
64
|
+
style: {
|
|
65
|
+
msoHide: "all",
|
|
66
|
+
maxHeight: 0,
|
|
67
|
+
overflow: "hidden"
|
|
68
|
+
},
|
|
69
|
+
children: image(assets.logoDarkUrl, void 0, "")
|
|
70
|
+
})] });
|
|
71
|
+
};
|
|
72
|
+
const BrandFooter = ({ unsubscribeLabel = "Unsubscribe", className }) => {
|
|
73
|
+
const rows = [];
|
|
74
|
+
if (footer.companyName !== void 0 && footer.companyName.trim() !== "") rows.push(row(jsx("span", {
|
|
75
|
+
className: "font-semibold",
|
|
76
|
+
children: footer.companyName
|
|
77
|
+
})));
|
|
78
|
+
if (footer.address !== void 0) {
|
|
79
|
+
const lines = addressLines(footer.address);
|
|
80
|
+
if (lines.length > 0) rows.push(row(lines.flatMap((line, index) => index === 0 ? [line] : [jsx("br", {}), line])));
|
|
81
|
+
}
|
|
82
|
+
const links = footer.socialLinks ?? [];
|
|
83
|
+
if (links.length > 0) rows.push(row(links.flatMap((link, index) => [...index === 0 ? [] : [" · "], jsx("a", {
|
|
84
|
+
href: link.url,
|
|
85
|
+
className: "text-gray-500 underline",
|
|
86
|
+
children: link.label
|
|
87
|
+
})])));
|
|
88
|
+
rows.push(row(unsubscribeLink(unsubscribeLabel), {
|
|
89
|
+
[UNSUBSCRIBE_ROW_ATTRIBUTE]: "",
|
|
90
|
+
[BRAND_FOOTER_MARKER]: true
|
|
91
|
+
}));
|
|
92
|
+
return jsx("table", {
|
|
93
|
+
role: "presentation",
|
|
94
|
+
width: "100%",
|
|
95
|
+
border: 0,
|
|
96
|
+
cellpadding: 0,
|
|
97
|
+
cellspacing: 0,
|
|
98
|
+
[FOOTER_ATTRIBUTE]: "",
|
|
99
|
+
[BRAND_FOOTER_MARKER]: true,
|
|
100
|
+
children: jsx("tbody", { children: jsx("tr", { children: jsx("td", {
|
|
101
|
+
align: "center",
|
|
102
|
+
className: classes("text-xs text-gray-500 text-center", className),
|
|
103
|
+
style: { padding: "32px 16px" },
|
|
104
|
+
children: jsx("table", {
|
|
105
|
+
role: "presentation",
|
|
106
|
+
border: 0,
|
|
107
|
+
cellpadding: 0,
|
|
108
|
+
cellspacing: 0,
|
|
109
|
+
children: jsx("tbody", { children: rows })
|
|
110
|
+
})
|
|
111
|
+
}) }) })
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
BrandLogo,
|
|
116
|
+
BrandFooter
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* The unsubscribe link. Its href is the placeholder, which no author-written
|
|
121
|
+
* link may carry: the serializer accepts it only on this node, and marks it so
|
|
122
|
+
* click tracking never rewrites it.
|
|
123
|
+
*/
|
|
124
|
+
const unsubscribeLink = (label) => jsx("a", {
|
|
125
|
+
href: UNSUBSCRIBE_URL_PLACEHOLDER,
|
|
126
|
+
className: "text-gray-500 underline",
|
|
127
|
+
[UNSUBSCRIBE_LINK]: true,
|
|
128
|
+
children: label
|
|
129
|
+
});
|
|
130
|
+
const UNSUBSCRIBE_ROW = new RegExp(`<tr ${UNSUBSCRIBE_ROW_ATTRIBUTE}=""[^>]*>[\\s\\S]*?</tr>`, "g");
|
|
131
|
+
const UNSUBSCRIBE_HREF = `href="${UNSUBSCRIBE_URL_PLACEHOLDER}"`;
|
|
132
|
+
/** Read the footer markers from rendered HTML. */
|
|
133
|
+
const renderedFooterMarkers = (html) => ({
|
|
134
|
+
footer: html.includes(`${FOOTER_ATTRIBUTE}=""`),
|
|
135
|
+
unsubscribeLink: html.includes(UNSUBSCRIBE_HREF)
|
|
136
|
+
});
|
|
137
|
+
/**
|
|
138
|
+
* Drop the unsubscribe row from a rendered message, for a send that resolved
|
|
139
|
+
* no unsubscribe group: there is no link to put there, so the row goes.
|
|
140
|
+
*/
|
|
141
|
+
const withoutUnsubscribeRow = (rendered) => ({
|
|
142
|
+
html: rendered.html?.replace(UNSUBSCRIBE_ROW, ""),
|
|
143
|
+
text: rendered.text?.split("\n").filter((line) => !line.includes(UNSUBSCRIBE_URL_PLACEHOLDER)).join("\n")
|
|
144
|
+
});
|
|
145
|
+
//#endregion
|
|
146
|
+
export { BRAND_COMPONENT_CLASSES, FOOTER_ATTRIBUTE, UNSUBSCRIBE_ROW_ATTRIBUTE, UNSUBSCRIBE_URL_PLACEHOLDER, brandComponents, renderedFooterMarkers, withoutUnsubscribeRow };
|
|
147
|
+
|
|
148
|
+
//# sourceMappingURL=brand.js.map
|
package/dist/email/html.d.ts
CHANGED
|
@@ -1,42 +1,2 @@
|
|
|
1
|
-
import { r as
|
|
2
|
-
|
|
3
|
-
import { EmittedPosition } from "./selection.js";
|
|
4
|
-
//#region src/email/html.d.ts
|
|
5
|
-
/** Escape a value for element content. */
|
|
6
|
-
declare const escapeText: (value: string) => string;
|
|
7
|
-
/** Escape a value for a double-quoted attribute. */
|
|
8
|
-
declare const escapeAttribute: (value: string) => string;
|
|
9
|
-
/** camelCase to kebab-case; already-kebab names pass through unchanged. */
|
|
10
|
-
declare const kebab: (property: string) => string;
|
|
11
|
-
/** A bare number is pixels for every property that takes a length. */
|
|
12
|
-
declare const declarationValue: (property: string, value: string | number) => string;
|
|
13
|
-
interface EmailHtmlOutput {
|
|
14
|
-
readonly html: string;
|
|
15
|
-
readonly diagnostics: readonly EmailDiagnostic[];
|
|
16
|
-
/**
|
|
17
|
-
* Each emitted element's offset range and origins. A compatibility finding
|
|
18
|
-
* carries a position in the emitted document, and this is what turns that
|
|
19
|
-
* back into the authoring element the author can edit.
|
|
20
|
-
*/
|
|
21
|
-
readonly positions: readonly EmittedPosition[];
|
|
22
|
-
}
|
|
23
|
-
/** How this document will be consumed. Delivery is the default; export is opt-in. */
|
|
24
|
-
interface SerializeEmailOptions {
|
|
25
|
-
/**
|
|
26
|
-
* Accept relative references. A local export serves assets from a relative
|
|
27
|
-
* base beside the file; a delivered message has no directory to resolve one
|
|
28
|
-
* against, so this stays off for anything that will be sent.
|
|
29
|
-
*/
|
|
30
|
-
readonly relativeUrls?: boolean | undefined;
|
|
31
|
-
/**
|
|
32
|
-
* Stamp every element with its instance path, so an editor preview can
|
|
33
|
-
* resolve a click in the rendered document to exactly one rendered element.
|
|
34
|
-
* A delivered message never carries it.
|
|
35
|
-
*/
|
|
36
|
-
readonly instancePaths?: boolean | undefined;
|
|
37
|
-
}
|
|
38
|
-
/** Lower the derived tree to email HTML, reporting every unsupported construct with its origin. */
|
|
39
|
-
declare const serializeEmailHtml: (node: EmailNode, options?: SerializeEmailOptions) => EmailHtmlOutput;
|
|
40
|
-
//#endregion
|
|
41
|
-
export { EmailHtmlOutput, SerializeEmailOptions, declarationValue, escapeAttribute, escapeText, kebab, serializeEmailHtml };
|
|
42
|
-
//# sourceMappingURL=html.d.ts.map
|
|
1
|
+
import { a as escapeText, i as escapeAttribute, n as SerializeEmailOptions, o as kebab, r as declarationValue, s as serializeEmailHtml, t as EmailHtmlOutput } from "../html-Bl9BAdqc.js";
|
|
2
|
+
export { EmailHtmlOutput, SerializeEmailOptions, declarationValue, escapeAttribute, escapeText, kebab, serializeEmailHtml };
|
package/dist/email/html.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { n as isObject } from "../guards-eV1qLbf2.js";
|
|
2
|
+
import { i as UNSUBSCRIBE_URL_PLACEHOLDER, n as RESERVED_FOOTER_ATTRIBUTES, r as UNSUBSCRIBE_LINK, t as BRAND_FOOTER_MARKER } from "../unsubscribe-link-CoesnQDU.js";
|
|
2
3
|
import { EMAIL_ELEMENTS, EMAIL_GLOBAL_ATTRIBUTES, emailElement } from "./elements.js";
|
|
3
4
|
import { INSTANCE_PATH_ATTRIBUTE } from "./selection.js";
|
|
4
5
|
//#region src/email/html.ts
|
|
@@ -117,9 +118,15 @@ const attributes = (sink, tag, spec, props, origins) => {
|
|
|
117
118
|
const emitted = [];
|
|
118
119
|
const present = /* @__PURE__ */ new Set();
|
|
119
120
|
let classes;
|
|
121
|
+
const unsubscribeLink = tag === "a" && props[UNSUBSCRIBE_LINK] === true;
|
|
120
122
|
for (const [key, value] of Object.entries(props)) {
|
|
121
123
|
if (key === "key") continue;
|
|
122
124
|
const name = key === "className" ? "class" : key;
|
|
125
|
+
if (unsubscribeLink && name === "href" && value === "%samva:unsubscribe-url%") {
|
|
126
|
+
present.add(name);
|
|
127
|
+
emitted.push(` href="${UNSUBSCRIBE_URL_PLACEHOLDER}" ses:no-track`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
123
130
|
if (name === "class") {
|
|
124
131
|
if (value === void 0 || value === null) continue;
|
|
125
132
|
if (typeof value !== "string") {
|
|
@@ -148,6 +155,10 @@ const attributes = (sink, tag, spec, props, origins) => {
|
|
|
148
155
|
report(sink, "event-handler", `<${tag} ${name}> is an event handler; email clients strip scripting and Samva refuses it.`, origins);
|
|
149
156
|
continue;
|
|
150
157
|
}
|
|
158
|
+
if (RESERVED_FOOTER_ATTRIBUTES.has(name) && props[BRAND_FOOTER_MARKER] !== true) {
|
|
159
|
+
report(sink, "reserved-attribute", `<${tag} ${name}> is written only by BrandFooter from samva:brand; render BrandFooter instead of marking an element as the footer.`, origins);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
151
162
|
if (/^(data|aria)-[a-z0-9-]+$/.test(name)) {
|
|
152
163
|
const text = attributeValue(sink, tag, name, { type: "string" }, value, origins);
|
|
153
164
|
if (text !== void 0) emitted.push(` ${name}="${escapeAttribute(text)}"`);
|
package/dist/email/render.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { r as EmailDiagnostic } from "../diagnostics-BMYtBCQk.js";
|
|
2
2
|
import { m as EmailNode } from "../elements-BzjjnoCS.js";
|
|
3
|
-
import { EmittedPosition } from "
|
|
4
|
-
import { SerializeEmailOptions } from "
|
|
5
|
-
import { r as TemplateDefinition } from "../template-DWftqh4x.js";
|
|
6
|
-
import { i as Stylesheet } from "../css-
|
|
3
|
+
import { n as EmittedPosition } from "../selection-CCArx9lU.js";
|
|
4
|
+
import { n as SerializeEmailOptions } from "../html-Bl9BAdqc.js";
|
|
5
|
+
import { r as TemplateDefinition, t as EmailContent } from "../template-DWftqh4x.js";
|
|
6
|
+
import { i as Stylesheet } from "../css-CS2jLID6.js";
|
|
7
7
|
//#region src/email/render.d.ts
|
|
8
8
|
/** Classic Outlook lays a document out correctly only under a transitional doctype. */
|
|
9
9
|
declare const EMAIL_DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
|
|
@@ -38,6 +38,13 @@ declare const renderEmail: <Input>(template: TemplateDefinition<Input>, data: un
|
|
|
38
38
|
* loaded into the same process cannot restyle an entry that is rendered again.
|
|
39
39
|
*/
|
|
40
40
|
declare const bindStylesheets: (stylesheet?: Stylesheet | undefined) => typeof renderEmail;
|
|
41
|
+
/**
|
|
42
|
+
* Render authored content against a stylesheet: inline what a client honors,
|
|
43
|
+
* keep the rest for `<head>`, and lower to HTML and text. `renderEmail` runs a
|
|
44
|
+
* validated template through it; a host that composes content without a
|
|
45
|
+
* template definition calls it directly.
|
|
46
|
+
*/
|
|
47
|
+
declare const renderEmailContent: (stylesheet: Stylesheet | undefined, render: () => EmailContent, options?: SerializeEmailOptions) => RenderedEmail;
|
|
41
48
|
//#endregion
|
|
42
|
-
export { EMAIL_DOCTYPE, EmailCompileOutput, RenderedEmail, bindStylesheets, compileEmail, renderEmail };
|
|
49
|
+
export { EMAIL_DOCTYPE, EmailCompileOutput, RenderedEmail, bindStylesheets, compileEmail, renderEmail, renderEmailContent };
|
|
43
50
|
//# sourceMappingURL=render.d.ts.map
|
package/dist/email/render.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { i as renderEmail, n as bindStylesheets, r as compileEmail, t as EMAIL_DOCTYPE } from "../render-
|
|
2
|
-
import "./html.js";
|
|
1
|
+
import { a as renderEmailContent, i as renderEmail, n as bindStylesheets, r as compileEmail, t as EMAIL_DOCTYPE } from "../render-vAv_4r9v.js";
|
|
3
2
|
import "./diagnostics.js";
|
|
4
|
-
|
|
5
|
-
import "./text.js";
|
|
6
|
-
export { EMAIL_DOCTYPE, bindStylesheets, compileEmail, renderEmail };
|
|
3
|
+
export { EMAIL_DOCTYPE, bindStylesheets, compileEmail, renderEmail, renderEmailContent };
|