From dfc6659e4fcb52694f1b6b52661c7fa84990e7f8 Mon Sep 17 00:00:00 2001 From: David Longworth Date: Mon, 1 Jun 2026 13:49:34 +0100 Subject: [PATCH] Add email docs and stacks-email templates --- .prettierignore | 2 +- package-lock.json | 1843 +++++++++++++++-- packages/stacks-docs/README.md | 7 + packages/stacks-docs/_redirects | 3 - packages/stacks-docs/package.json | 1 + packages/stacks-docs/src/app.css | 14 +- .../src/components/EmailOptionsTable.svelte | 88 + .../src/components/StacksEmailEmbed.svelte | 401 ++++ .../docs/public/email/components/button.md | 27 + .../src/docs/public/email/components/cards.md | 26 + .../email/components/component-button.svg | 1 + .../email/components/component-cards.svg | 1 + .../email/components/component-dividers.svg | 1 + .../email/components/component-footer.svg | 1 + .../email/components/component-graphic.svg | 1 + .../email/components/component-header.svg | 1 + .../email/components/component-headline.svg | 8 + .../email/components/component-link.svg | 1 + .../email/components/component-preview.svg | 1 + .../email/components/component-spacers.svg | 1 + .../email/components/component-subtitle.svg | 1 + .../email/components/component-text.svg | 1 + .../email/components/component-title.svg | 1 + .../docs/public/email/components/dividers.md | 26 + .../docs/public/email/components/footer.md | 33 + .../docs/public/email/components/graphic.md | 27 + .../docs/public/email/components/header.md | 35 + .../docs/public/email/components/headline.md | 25 + .../docs/public/email/components/preview.md | 26 + .../docs/public/email/components/spacers.md | 23 + .../docs/public/email/components/subtitle.md | 26 + .../src/docs/public/email/components/text.md | 24 + .../src/docs/public/email/components/title.md | 23 + .../src/docs/public/email/overview.md | 279 +++ .../templates/email-template-newsletter.png | Bin 0 -> 42877 bytes .../templates/email-template-promotional.png | Bin 0 -> 34016 bytes .../email-template-transactional.png | Bin 0 -> 50358 bytes .../docs/public/email/templates/newsletter.md | 128 ++ .../public/email/templates/promotional.md | 128 ++ .../public/email/templates/transactional.md | 95 + .../[[section]]/[subsection]/+page.svelte | 14 +- .../src/routes/api/email/catalog/+server.ts | 6 + .../src/routes/api/email/compile/+server.ts | 66 + packages/stacks-docs/src/structure.yaml | 69 +- packages/stacks-docs/static/email | 1 + .../static/images/heros/email-authoring.svg | 6 + .../static/images/heros/email-components.svg | 36 + .../static/images/heros/email-overview.svg | 14 + .../static/images/heros/email-types.svg | 1 + .../stacks-docs/static/social/instagram.png | Bin 0 -> 906 bytes .../stacks-docs/static/social/linkedin.png | Bin 0 -> 607 bytes .../stacks-docs/static/social/threads.png | Bin 0 -> 1111 bytes packages/stacks-docs/static/social/x.png | Bin 0 -> 904 bytes .../stacks-docs/static/social/youtube.png | Bin 0 -> 573 bytes .../static/stack-overflow-business-logo.png | Bin 0 -> 6616 bytes .../static/stack-overflow-logo-off-white.png | Bin 0 -> 4461 bytes .../static/stack-overflow-logo.png | Bin 0 -> 5012 bytes packages/stacks-docs/svelte.config.js | 134 +- packages/stacks-email/README.md | 96 + packages/stacks-email/components/button.ts | 150 ++ packages/stacks-email/components/footer.ts | 425 ++++ packages/stacks-email/components/graphic.ts | 220 ++ packages/stacks-email/components/header.ts | 196 ++ packages/stacks-email/components/headline.ts | 192 ++ packages/stacks-email/components/index.ts | 11 + packages/stacks-email/components/preview.ts | 85 + packages/stacks-email/components/section.ts | 51 + packages/stacks-email/components/spacer.ts | 39 + packages/stacks-email/components/spacers.ts | 76 + packages/stacks-email/components/text.ts | 210 ++ packages/stacks-email/components/title.ts | 143 ++ packages/stacks-email/eslint.config.js | 29 + packages/stacks-email/mjml-config.ts | 246 +++ packages/stacks-email/mjml-json.ts | 19 + packages/stacks-email/package.json | 46 + packages/stacks-email/registry.ts | 35 + packages/stacks-email/src/app.css | 1 + packages/stacks-email/src/app.d.ts | 12 + packages/stacks-email/src/app.html | 11 + .../src/components/TemplateSidebar.svelte | 59 + .../src/lib/highlight/highlight.ts | 29 + .../stacks-email/src/lib/pipeline/compile.ts | 165 ++ .../stacks-email/src/lib/pipeline/template.ts | 92 + .../src/lib/pipeline/transform.ts | 12 + .../stacks-email/src/lib/public/catalog.ts | 15 + .../stacks-email/src/lib/public/compile.ts | 63 + .../stacks-email/src/lib/public/components.ts | 165 ++ packages/stacks-email/src/lib/public/index.ts | 53 + .../stacks-email/src/lib/public/templates.ts | 291 +++ .../stacks-email/src/lib/public/validation.ts | 38 + .../stacks-email/src/routes/+layout.svelte | 7 + packages/stacks-email/src/routes/+layout.ts | 1 + .../stacks-email/src/routes/+page.server.ts | 7 + packages/stacks-email/src/routes/+page.svelte | 22 + .../src/routes/api/compile/+server.ts | 231 +++ .../src/routes/emails/[slug]/+page.server.ts | 73 + .../src/routes/emails/[slug]/+page.svelte | 167 ++ .../stacks-email/src/types/json2mjml.d.ts | 4 + .../stacks-email/src/types/stacks-icons.d.ts | 1 + .../static/email/hero/1200x630.png | Bin 0 -> 24010 bytes .../static/email/social/instagram.png | Bin 0 -> 906 bytes .../static/email/social/linkedin.png | Bin 0 -> 607 bytes .../static/email/social/threads.png | Bin 0 -> 1111 bytes .../stacks-email/static/email/social/x.png | Bin 0 -> 904 bytes .../static/email/social/youtube.png | Bin 0 -> 573 bytes .../static/email/spots/SpotLock.png | Bin 0 -> 2398 bytes .../email/stack-overflow-business-logo.png | Bin 0 -> 6616 bytes .../email/stack-overflow-logo-off-white.png | Bin 0 -> 4461 bytes .../static/email/stack-overflow-logo.png | Bin 0 -> 5012 bytes packages/stacks-email/svelte.config.js | 34 + .../stacks-email/templates/transactional.ts | 138 ++ packages/stacks-email/tokens.ts | 161 ++ packages/stacks-email/tsconfig.json | 14 + packages/stacks-email/types.ts | 86 + packages/stacks-email/variants.ts | 44 + packages/stacks-email/vite.config.ts | 6 + 116 files changed, 7709 insertions(+), 239 deletions(-) create mode 100644 packages/stacks-docs/src/components/EmailOptionsTable.svelte create mode 100644 packages/stacks-docs/src/components/StacksEmailEmbed.svelte create mode 100644 packages/stacks-docs/src/docs/public/email/components/button.md create mode 100644 packages/stacks-docs/src/docs/public/email/components/cards.md create mode 100644 packages/stacks-docs/src/docs/public/email/components/component-button.svg create mode 100644 packages/stacks-docs/src/docs/public/email/components/component-cards.svg create mode 100644 packages/stacks-docs/src/docs/public/email/components/component-dividers.svg create mode 100644 packages/stacks-docs/src/docs/public/email/components/component-footer.svg create mode 100644 packages/stacks-docs/src/docs/public/email/components/component-graphic.svg create mode 100644 packages/stacks-docs/src/docs/public/email/components/component-header.svg create mode 100644 packages/stacks-docs/src/docs/public/email/components/component-headline.svg create mode 100644 packages/stacks-docs/src/docs/public/email/components/component-link.svg create mode 100644 packages/stacks-docs/src/docs/public/email/components/component-preview.svg create mode 100644 packages/stacks-docs/src/docs/public/email/components/component-spacers.svg create mode 100644 packages/stacks-docs/src/docs/public/email/components/component-subtitle.svg create mode 100644 packages/stacks-docs/src/docs/public/email/components/component-text.svg create mode 100644 packages/stacks-docs/src/docs/public/email/components/component-title.svg create mode 100644 packages/stacks-docs/src/docs/public/email/components/dividers.md create mode 100644 packages/stacks-docs/src/docs/public/email/components/footer.md create mode 100644 packages/stacks-docs/src/docs/public/email/components/graphic.md create mode 100644 packages/stacks-docs/src/docs/public/email/components/header.md create mode 100644 packages/stacks-docs/src/docs/public/email/components/headline.md create mode 100644 packages/stacks-docs/src/docs/public/email/components/preview.md create mode 100644 packages/stacks-docs/src/docs/public/email/components/spacers.md create mode 100644 packages/stacks-docs/src/docs/public/email/components/subtitle.md create mode 100644 packages/stacks-docs/src/docs/public/email/components/text.md create mode 100644 packages/stacks-docs/src/docs/public/email/components/title.md create mode 100644 packages/stacks-docs/src/docs/public/email/overview.md create mode 100644 packages/stacks-docs/src/docs/public/email/templates/email-template-newsletter.png create mode 100644 packages/stacks-docs/src/docs/public/email/templates/email-template-promotional.png create mode 100644 packages/stacks-docs/src/docs/public/email/templates/email-template-transactional.png create mode 100644 packages/stacks-docs/src/docs/public/email/templates/newsletter.md create mode 100644 packages/stacks-docs/src/docs/public/email/templates/promotional.md create mode 100644 packages/stacks-docs/src/docs/public/email/templates/transactional.md create mode 100644 packages/stacks-docs/src/routes/api/email/catalog/+server.ts create mode 100644 packages/stacks-docs/src/routes/api/email/compile/+server.ts create mode 120000 packages/stacks-docs/static/email create mode 100644 packages/stacks-docs/static/images/heros/email-authoring.svg create mode 100644 packages/stacks-docs/static/images/heros/email-components.svg create mode 100644 packages/stacks-docs/static/images/heros/email-overview.svg create mode 100644 packages/stacks-docs/static/images/heros/email-types.svg create mode 100644 packages/stacks-docs/static/social/instagram.png create mode 100644 packages/stacks-docs/static/social/linkedin.png create mode 100644 packages/stacks-docs/static/social/threads.png create mode 100644 packages/stacks-docs/static/social/x.png create mode 100644 packages/stacks-docs/static/social/youtube.png create mode 100644 packages/stacks-docs/static/stack-overflow-business-logo.png create mode 100644 packages/stacks-docs/static/stack-overflow-logo-off-white.png create mode 100644 packages/stacks-docs/static/stack-overflow-logo.png create mode 100644 packages/stacks-email/README.md create mode 100644 packages/stacks-email/components/button.ts create mode 100644 packages/stacks-email/components/footer.ts create mode 100644 packages/stacks-email/components/graphic.ts create mode 100644 packages/stacks-email/components/header.ts create mode 100644 packages/stacks-email/components/headline.ts create mode 100644 packages/stacks-email/components/index.ts create mode 100644 packages/stacks-email/components/preview.ts create mode 100644 packages/stacks-email/components/section.ts create mode 100644 packages/stacks-email/components/spacer.ts create mode 100644 packages/stacks-email/components/spacers.ts create mode 100644 packages/stacks-email/components/text.ts create mode 100644 packages/stacks-email/components/title.ts create mode 100644 packages/stacks-email/eslint.config.js create mode 100644 packages/stacks-email/mjml-config.ts create mode 100644 packages/stacks-email/mjml-json.ts create mode 100644 packages/stacks-email/package.json create mode 100644 packages/stacks-email/registry.ts create mode 100644 packages/stacks-email/src/app.css create mode 100644 packages/stacks-email/src/app.d.ts create mode 100644 packages/stacks-email/src/app.html create mode 100644 packages/stacks-email/src/components/TemplateSidebar.svelte create mode 100644 packages/stacks-email/src/lib/highlight/highlight.ts create mode 100644 packages/stacks-email/src/lib/pipeline/compile.ts create mode 100644 packages/stacks-email/src/lib/pipeline/template.ts create mode 100644 packages/stacks-email/src/lib/pipeline/transform.ts create mode 100644 packages/stacks-email/src/lib/public/catalog.ts create mode 100644 packages/stacks-email/src/lib/public/compile.ts create mode 100644 packages/stacks-email/src/lib/public/components.ts create mode 100644 packages/stacks-email/src/lib/public/index.ts create mode 100644 packages/stacks-email/src/lib/public/templates.ts create mode 100644 packages/stacks-email/src/lib/public/validation.ts create mode 100644 packages/stacks-email/src/routes/+layout.svelte create mode 100644 packages/stacks-email/src/routes/+layout.ts create mode 100644 packages/stacks-email/src/routes/+page.server.ts create mode 100644 packages/stacks-email/src/routes/+page.svelte create mode 100644 packages/stacks-email/src/routes/api/compile/+server.ts create mode 100644 packages/stacks-email/src/routes/emails/[slug]/+page.server.ts create mode 100644 packages/stacks-email/src/routes/emails/[slug]/+page.svelte create mode 100644 packages/stacks-email/src/types/json2mjml.d.ts create mode 100644 packages/stacks-email/src/types/stacks-icons.d.ts create mode 100644 packages/stacks-email/static/email/hero/1200x630.png create mode 100644 packages/stacks-email/static/email/social/instagram.png create mode 100644 packages/stacks-email/static/email/social/linkedin.png create mode 100644 packages/stacks-email/static/email/social/threads.png create mode 100644 packages/stacks-email/static/email/social/x.png create mode 100644 packages/stacks-email/static/email/social/youtube.png create mode 100644 packages/stacks-email/static/email/spots/SpotLock.png create mode 100644 packages/stacks-email/static/email/stack-overflow-business-logo.png create mode 100644 packages/stacks-email/static/email/stack-overflow-logo-off-white.png create mode 100644 packages/stacks-email/static/email/stack-overflow-logo.png create mode 100644 packages/stacks-email/svelte.config.js create mode 100644 packages/stacks-email/templates/transactional.ts create mode 100644 packages/stacks-email/tokens.ts create mode 100644 packages/stacks-email/tsconfig.json create mode 100644 packages/stacks-email/types.ts create mode 100644 packages/stacks-email/variants.ts create mode 100644 packages/stacks-email/vite.config.ts diff --git a/.prettierignore b/.prettierignore index 9b4a518cde..8504d59126 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,6 @@ .vscode/ dist/ -**/.svelte-kit +.svelte-kit/ # TODO: prettier breaks less files containing namespaced mixins with whitespaces # BEFORE PRETTIER: #stacks-internals #responsify('.w25', { width: 25% !important; }); # AFTER PRETTIER: #stacks-internals #responsify('.w25', { width: 25% !important; });; diff --git a/package-lock.json b/package-lock.json index 484b147a88..dcc58034f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -131,7 +131,6 @@ }, "node_modules/@babel/runtime": { "version": "7.28.4", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1403,6 +1402,50 @@ } } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "license": "MIT", @@ -1428,7 +1471,7 @@ }, "node_modules/@jridgewell/source-map": { "version": "0.3.11", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1667,6 +1710,12 @@ "node": ">= 8" } }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "license": "MIT" + }, "node_modules/@open-wc/dedupe-mixin": { "version": "2.0.1", "dev": true, @@ -2355,6 +2404,16 @@ "win32" ] }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.2.9", "dev": true, @@ -2508,6 +2567,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2521,6 +2581,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2532,6 +2593,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2545,6 +2607,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2558,6 +2621,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2571,6 +2635,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2584,6 +2649,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2597,6 +2663,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2610,6 +2677,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2623,6 +2691,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2636,6 +2705,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2649,6 +2719,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2662,6 +2733,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2675,6 +2747,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2688,6 +2761,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2701,6 +2775,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2714,6 +2789,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2727,6 +2803,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2740,6 +2817,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2753,6 +2831,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2766,6 +2845,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2779,6 +2859,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2878,6 +2959,10 @@ "version": "6.7.0", "license": "MIT" }, + "node_modules/@stackoverflow/stacks-email": { + "resolved": "packages/stacks-email", + "link": true + }, "node_modules/@stackoverflow/stacks-icons": { "version": "7.0.0-beta.24", "resolved": "https://registry.npmjs.org/@stackoverflow/stacks-icons/-/stacks-icons-7.0.0-beta.24.tgz", @@ -3184,6 +3269,16 @@ "acorn": "^8.9.0" } }, + "node_modules/@sveltejs/adapter-auto": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-6.1.1.tgz", + "integrity": "sha512-cBNt4jgH4KuaNO5gRSB2CZKkGtz+OCZ8lPjRQGjhvVUD4akotnj2weUia6imLl2v07K3IgsQRyM36909miSwoQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, "node_modules/@sveltejs/adapter-netlify": { "version": "5.2.4", "dev": true, @@ -3704,6 +3799,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mjml": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/mjml/-/mjml-5.0.0.tgz", + "integrity": "sha512-z6P0yjEOVOz6cZVyD3vkPSifzVH0fa9M8BFM8Jl9HtpeFkBJyCZHRLPx1uFnkZijgAYmtO/JfSbp4EF1fNwsXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mjml-core": "*" + } + }, + "node_modules/@types/mjml-core": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/mjml-core/-/mjml-core-5.0.0.tgz", + "integrity": "sha512-E1Rho2ZfVEqZekQoESDuPAw7C3MrzdUvS6YAiEPGdhQQqAchMXfdChXlSi6ly9YhZgUP026ujrRlEGJn9o/zAg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mkdirp": { "version": "1.0.2", "dev": true, @@ -3719,7 +3831,7 @@ }, "node_modules/@types/node": { "version": "24.9.1", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -4753,6 +4865,15 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/accepts": { "version": "1.3.8", "dev": true, @@ -4857,7 +4978,6 @@ }, "node_modules/ansi-colors": { "version": "4.1.3", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4890,7 +5010,6 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4898,7 +5017,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4912,7 +5030,6 @@ }, "node_modules/anymatch": { "version": "3.1.3", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -4926,7 +5043,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -5051,7 +5167,6 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "dev": true, "license": "MIT" }, "node_modules/bare-events": { @@ -5172,7 +5287,6 @@ }, "node_modules/binary-extensions": { "version": "2.3.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5183,12 +5297,10 @@ }, "node_modules/boolbase": { "version": "1.0.0", - "dev": true, "license": "ISC" }, "node_modules/brace-expansion": { "version": "2.0.2", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -5196,7 +5308,6 @@ }, "node_modules/braces": { "version": "3.0.3", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -5247,7 +5358,7 @@ }, "node_modules/buffer-from": { "version": "1.1.2", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/bundle-name": { @@ -5331,6 +5442,16 @@ "node": ">=6" } }, + "node_modules/camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==", + "license": "MIT", + "dependencies": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, "node_modules/camelcase": { "version": "6.3.0", "dev": true, @@ -5465,9 +5586,70 @@ "node": ">= 16" } }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/chokidar": { "version": "3.6.0", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -5536,6 +5718,27 @@ "devtools-protocol": "*" } }, + "node_modules/clean-css": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", + "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "dev": true, @@ -5549,7 +5752,6 @@ }, "node_modules/cliui": { "version": "8.0.1", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -5562,12 +5764,10 @@ }, "node_modules/cliui/node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -5580,7 +5780,6 @@ }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -5659,7 +5858,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -5670,7 +5868,6 @@ }, "node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/colord": { @@ -5749,6 +5946,12 @@ "node": ">=12.17" } }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, "node_modules/commondir": { "version": "1.0.1", "dev": true, @@ -5812,6 +6015,16 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "dev": true, @@ -5858,7 +6071,7 @@ }, "node_modules/copy-anything": { "version": "2.0.6", - "devOptional": true, + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -5895,7 +6108,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -5961,7 +6173,6 @@ }, "node_modules/css-select": { "version": "5.2.2", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", @@ -5988,7 +6199,6 @@ }, "node_modules/css-what": { "version": "6.2.2", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">= 6" @@ -6317,6 +6527,12 @@ "node": ">=8" } }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, "node_modules/devalue": { "version": "5.8.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", @@ -6365,7 +6581,6 @@ }, "node_modules/dom-serializer": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", @@ -6376,20 +6591,8 @@ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "4.5.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/domelementtype": { "version": "2.3.0", - "dev": true, "funding": [ { "type": "github", @@ -6400,7 +6603,6 @@ }, "node_modules/domhandler": { "version": "5.0.3", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" @@ -6414,7 +6616,6 @@ }, "node_modules/domutils": { "version": "3.2.2", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", @@ -6446,6 +6647,39 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/editorconfig": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "^9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/ee-first": { "version": "1.1.1", "dev": true, @@ -6456,6 +6690,12 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, "node_modules/end-of-stream": { "version": "1.4.5", "dev": true, @@ -6490,6 +6730,18 @@ "node": ">=8.6" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "dev": true, @@ -6605,12 +6857,23 @@ }, "node_modules/escalade": { "version": "3.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/escape-html": { "version": "1.0.3", "dev": true, @@ -7254,7 +7517,6 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -7319,6 +7581,22 @@ "dev": true, "license": "ISC" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fresh": { "version": "0.5.2", "dev": true, @@ -7377,7 +7655,6 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -7457,9 +7734,29 @@ "version": "2.0.0", "license": "ISC" }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -7558,7 +7855,7 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/has-flag": { @@ -7814,6 +8111,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/highlight.js": { "version": "11.11.1", "license": "BSD-3-Clause", @@ -7831,6 +8137,27 @@ "dev": true, "license": "MIT" }, + "node_modules/html-minifier": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz", + "integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==", + "license": "MIT", + "dependencies": { + "camel-case": "^3.0.0", + "clean-css": "^4.2.1", + "commander": "^2.19.0", + "he": "^1.2.0", + "param-case": "^2.1.1", + "relateurl": "^0.2.7", + "uglify-js": "^3.5.1" + }, + "bin": { + "html-minifier": "cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/html-tags": { "version": "5.1.0", "dev": true, @@ -7850,6 +8177,25 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/http-assert": { "version": "1.5.0", "dev": true, @@ -7974,6 +8320,7 @@ }, "node_modules/image-size": { "version": "0.5.5", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -8087,7 +8434,6 @@ }, "node_modules/ini": { "version": "1.3.8", - "dev": true, "license": "ISC" }, "node_modules/internal-ip": { @@ -8148,7 +8494,6 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -8208,7 +8553,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8216,7 +8560,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8242,7 +8585,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -8300,7 +8642,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -8386,7 +8727,7 @@ }, "node_modules/is-what": { "version": "3.14.1", - "devOptional": true, + "dev": true, "license": "MIT", "peer": true }, @@ -8424,7 +8765,6 @@ }, "node_modules/isexe": { "version": "2.0.0", - "dev": true, "license": "ISC" }, "node_modules/isobject": { @@ -8468,6 +8808,21 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest-worker": { "version": "27.5.1", "dev": true, @@ -8497,7 +8852,7 @@ }, "node_modules/jiti": { "version": "2.6.1", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -8510,6 +8865,33 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz", + "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "dev": true, @@ -8546,6 +8928,18 @@ "dev": true, "license": "MIT" }, + "node_modules/json2mjml": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/json2mjml/-/json2mjml-1.0.3.tgz", + "integrity": "sha512-dxQZZiQKSUSWVE46D3vf8NDjd/mgN+ejy/vR2DXz8/QUUMHGlMm3WeCqPw/5LoCKt+dH382q7QXbl9ZxDCUecw==", + "license": "MIT", + "dependencies": { + "commander": "^2.11.0" + }, + "bin": { + "json2mjml": "lib/cli.js" + } + }, "node_modules/jsonfile": { "version": "4.0.0", "dev": true, @@ -8554,6 +8948,34 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/juice": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/juice/-/juice-10.0.1.tgz", + "integrity": "sha512-ZhJT1soxJCkOiO55/mz8yeBKTAJhRzX9WBO+16ZTqNTONnnVlUPyVBIzQ7lDRjaBdTbid+bAnyIon/GM3yp4cA==", + "license": "MIT", + "dependencies": { + "cheerio": "1.0.0-rc.12", + "commander": "^6.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^6.0.1" + }, + "bin": { + "juice": "bin/juice" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/juice/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/keygrip": { "version": "1.1.0", "dev": true, @@ -8712,7 +9134,7 @@ }, "node_modules/less": { "version": "4.5.1", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "peer": true, @@ -8764,6 +9186,7 @@ }, "node_modules/less/node_modules/errno": { "version": "0.1.8", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -8776,6 +9199,7 @@ }, "node_modules/less/node_modules/make-dir": { "version": "2.1.0", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -8789,6 +9213,7 @@ }, "node_modules/less/node_modules/mime": { "version": "1.6.0", + "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -8801,6 +9226,7 @@ }, "node_modules/less/node_modules/semver": { "version": "5.7.2", + "dev": true, "license": "ISC", "optional": true, "peer": true, @@ -8810,6 +9236,7 @@ }, "node_modules/less/node_modules/source-map": { "version": "0.6.1", + "dev": true, "license": "BSD-3-Clause", "optional": true, "peer": true, @@ -8939,6 +9366,12 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, "node_modules/lodash.camelcase": { "version": "4.3.0", "dev": true, @@ -9024,6 +9457,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "8.0.5", "dev": true, @@ -9076,16 +9515,6 @@ "markdown-it": "bin/markdown-it.mjs" } }, - "node_modules/markdown-it/node_modules/entities": { - "version": "4.5.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/marky": { "version": "1.3.0", "dev": true, @@ -9177,6 +9606,12 @@ "node": ">= 0.6" } }, + "node_modules/mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==", + "license": "MIT" + }, "node_modules/meow": { "version": "14.0.0", "dev": true, @@ -9226,6 +9661,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/mime-db": { "version": "1.52.0", "dev": true, @@ -9286,7 +9733,6 @@ "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.2" @@ -9298,180 +9744,635 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mitt": { "version": "3.0.1", "dev": true, "license": "MIT" }, - "node_modules/mkdirp": { - "version": "1.0.4", - "dev": true, + "node_modules/mjml": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml/-/mjml-4.18.0.tgz", + "integrity": "sha512-rQM4aqFRrNvV1k733e8hJSopBjZvoSdBpRYzNTMAN+As0jqJsO5eN0wTT2IFtfe4PREzzu5b06RkPiUQdd0IIg==", "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" + "dependencies": { + "@babel/runtime": "^7.28.4", + "mjml-cli": "4.18.0", + "mjml-core": "4.18.0", + "mjml-migrate": "4.18.0", + "mjml-preset-core": "4.18.0", + "mjml-validator": "4.18.0" }, - "engines": { - "node": ">=10" + "bin": { + "mjml": "bin/mjml" } }, - "node_modules/mri": { - "version": "1.2.0", - "dev": true, + "node_modules/mjml-accordion": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-accordion/-/mjml-accordion-4.18.0.tgz", + "integrity": "sha512-9PUmy2JxIOGgAaVHvgVYX21nVAo3o/+wJckTTF/YTLGAqB+nm+44buxRzaXxVk7qXRwbCNfE8c8mlGVNh7vB1g==", "license": "MIT", - "engines": { - "node": ">=4" + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" } }, - "node_modules/mrmime": { - "version": "2.0.1", - "devOptional": true, + "node_modules/mjml-body": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-body/-/mjml-body-4.18.0.tgz", + "integrity": "sha512-34AwX70/7NkRIajPsa5j6NySRiNrlLatTKhiLwTVFiVtrEFlfCcbeMNmdVixI3Ldvs8209ZC6euaAnXDRyR1zw==", "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" } }, - "node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/nanocolors": { - "version": "0.2.13", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", - "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/mjml-button": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-button/-/mjml-button-4.18.0.tgz", + "integrity": "sha512-ZsWMI0j7EcFCMqbqdVwMWhmsVc03FhmypWXokKopGhwySn4IAB4AOURonRmFrO7k6sDeQ+iJ9QtTu7jA+S8wmg==", "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" } }, - "node_modules/nanostores": { - "version": "1.1.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/mjml-carousel": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-carousel/-/mjml-carousel-4.18.0.tgz", + "integrity": "sha512-wY4g1CHCOoVSZuar7CLFon/qkPbICu71IT+6pa4BDwkAiaAMAemZPyy+a+iIUgdc8kHgSuHGsGf6PQzBSMWRZA==", "license": "MIT", - "engines": { - "node": "^20.0.0 || >=22.0.0" + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/needle": { - "version": "3.3.1", + "node_modules/mjml-cli": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-cli/-/mjml-cli-4.18.0.tgz", + "integrity": "sha512-N6CnA4o/q/VRnGPxTzvVnjAEcF7WUVVQGYfS9SPAp0qwyf7RysMmewdS9yN8GwXwZV6L2sKdn+3ANNi2FNsJ7w==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "iconv-lite": "^0.6.3", - "sax": "^1.2.4" + "@babel/runtime": "^7.28.4", + "chokidar": "^3.0.0", + "glob": "^10.3.10", + "html-minifier": "^4.0.0", + "js-beautify": "^1.6.14", + "lodash": "^4.17.21", + "minimatch": "^9.0.3", + "mjml-core": "4.18.0", + "mjml-migrate": "4.18.0", + "mjml-parser-xml": "4.18.0", + "mjml-validator": "4.18.0", + "yargs": "^17.7.2" }, "bin": { - "needle": "bin/needle" - }, - "engines": { - "node": ">= 4.4.x" + "mjml-cli": "bin/mjml" } }, - "node_modules/needle/node_modules/iconv-lite": { - "version": "0.6.3", + "node_modules/mjml-column": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-column/-/mjml-column-4.18.0.tgz", + "integrity": "sha512-0QZ1whxbHUmJaRT8tW+wmr3fWZ/kpsHKAd24c7Z/N1Otm/U2G0T/FFEFJ6cB25X6ZN0K40QZ8L9gdLfiSVuRbA==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" } }, - "node_modules/negotiator": { - "version": "0.6.3", - "dev": true, + "node_modules/mjml-core": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-4.18.0.tgz", + "integrity": "sha512-yey72LszXvIo5p0R6DB+YU8er/nP2wPsqpLKQCB0H8vG0WRT1sbSUvnCUOkKGn7subuyWDTdzHKbQO3XYIOmvg==", "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "@babel/runtime": "^7.28.4", + "cheerio": "1.0.0-rc.12", + "detect-node": "^2.0.4", + "html-minifier": "^4.0.0", + "js-beautify": "^1.6.14", + "juice": "^10.0.0", + "lodash": "^4.17.21", + "mjml-migrate": "4.18.0", + "mjml-parser-xml": "4.18.0", + "mjml-validator": "4.18.0" } }, - "node_modules/neo-async": { - "version": "2.6.2", - "dev": true, - "license": "MIT" - }, - "node_modules/netmask": { - "version": "2.0.2", - "dev": true, + "node_modules/mjml-divider": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-divider/-/mjml-divider-4.18.0.tgz", + "integrity": "sha512-FmGUVJqi4RYroh7y85vDx0aUKZgECkxHtMQ4pkLGQbZ2g93/Qt0Ek88DVCNJ5XwUAQQkE/TvrGMLHp3CIqpQ9Q==", "license": "MIT", - "engines": { - "node": ">= 0.4.0" + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "dev": true, + "node_modules/mjml-group": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-group/-/mjml-group-4.18.0.tgz", + "integrity": "sha512-28ABkXsKljBqj7XCC8GkQ94xz8HEU2XTyD+9LTlkDafzGp/MGJb8DcLh/7IkxCwqkQWyeMiDNLf1djsQ909Vxw==", "license": "MIT", "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" } }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "dev": true, - "license": "MIT" + "node_modules/mjml-head": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-head/-/mjml-head-4.18.0.tgz", + "integrity": "sha512-DS0adpIAsVMDIk2DOsHzjg+RNjQU0fF8jiVP9BmdRHVGrLPmpL9wIHZk2KvsKvZe7VaXXBijFt3DZ5/CQ/+D7Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "dev": true, - "license": "BSD-2-Clause" + "node_modules/mjml-head-attributes": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-head-attributes/-/mjml-head-attributes-4.18.0.tgz", + "integrity": "sha512-nLzix1wrMnojE0RPGhk4iKqSRwHKjie2EPzgKT7CDzfqN+Ref03E5Q19x3cQTLgxvq3C3CnvCQBfnhoS3Eakug==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "dev": true, + "node_modules/mjml-head-breakpoint": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-head-breakpoint/-/mjml-head-breakpoint-4.18.0.tgz", + "integrity": "sha512-k6rwff+7i+vTQYJ/CjBfE20qNqPaW60IRH2x2oEPuCzmwDmoVWOcplJIuotSqIAdfwF9hLkICknisp1BpczVlQ==", "license": "MIT", "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" } }, - "node_modules/node-releases": { + "node_modules/mjml-head-font": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-head-font/-/mjml-head-font-4.18.0.tgz", + "integrity": "sha512-ao8HB5nf+Dmxw4GO6lMMOlnj1lNZONai0GC9RobrZgPlghZw6hpURWGpkON7pQcy6XnOHwYwkV7Go/npzA2i7w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-head-html-attributes": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-head-html-attributes/-/mjml-head-html-attributes-4.18.0.tgz", + "integrity": "sha512-xaQE1rthe0RrNotwEr71X1tE+QQ489Yc0ynMm3oNMrohDI/TaCeazx8GAHPMM7VLduDA8D4A5wkZ6PuEvlJu4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-head-preview": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-head-preview/-/mjml-head-preview-4.18.0.tgz", + "integrity": "sha512-2JvYqhbLyU/+Te6/1AXxzTNoHYCDYhXOVZP7wMvU4t7K34pXqyRUNO405atyHUY1MRafrl6RJ8cIx0x5vUX7PA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-head-style": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-head-style/-/mjml-head-style-4.18.0.tgz", + "integrity": "sha512-nEwDHkAqY3Fm7QWeAZc/a7MakZpXh6THfrE8/AWrfpgzTHrD/wihNUc09ztNpr6z/K1+JWgQfSF2BRc+X3P46g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-head-title": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-head-title/-/mjml-head-title-4.18.0.tgz", + "integrity": "sha512-0Hm8o50rPMUQLSCOOa4D4pz9NajmCDccLvBYE4fwKdeUXjSJ6bwAYeMpveel8oNZMDUVJ4Hx+PskisEGHMHM2w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-hero": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-hero/-/mjml-hero-4.18.0.tgz", + "integrity": "sha512-rujm0ROM4QGWw77vnl3NaVaCKXrT4xTSHeAnkHKiY5AuRf6HPTgEtutq5pdel/y6Q9GrmxvN3HRESum7tpJCJw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-image": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-image/-/mjml-image-4.18.0.tgz", + "integrity": "sha512-e09NkoYwvzMcTv7V6H5doWD6Te2E1y2EvOLQJoXKVdQpDwyBWGdfnZke0scJGdA58HLAB+0mLYogpLwmfLaP5Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-migrate": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-migrate/-/mjml-migrate-4.18.0.tgz", + "integrity": "sha512-qfNCgW9zhJIsbPyXFA5RT/WY4mlje3N0WhHHOsHc0nY89Q01DenyslUy9nLLGXwi4K5FHS58oCjwWbMhwDcj1w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "js-beautify": "^1.6.14", + "lodash": "^4.17.21", + "mjml-core": "4.18.0", + "mjml-parser-xml": "4.18.0", + "yargs": "^17.7.2" + }, + "bin": { + "migrate": "lib/cli.js" + } + }, + "node_modules/mjml-navbar": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-navbar/-/mjml-navbar-4.18.0.tgz", + "integrity": "sha512-uho/MS2tfNAe+V9u2X7NoCco34MDbdp30ETA8009Qo1VCP/D8lZ+s69WGRPu6hvN/Y2pzBgZly++CMg3qFZqBQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-parser-xml": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-parser-xml/-/mjml-parser-xml-4.18.0.tgz", + "integrity": "sha512-sHSsZg4afY1heThuJzxa1Kvfh/QzB7/9P5fFUHeVnnxb07ZTXnhXWA6YbobdND5/l9+5yjN5/UgqDZm3tIT4Uw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "detect-node": "2.1.0", + "htmlparser2": "^9.1.0", + "lodash": "^4.17.21" + } + }, + "node_modules/mjml-parser-xml/node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/mjml-preset-core": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-preset-core/-/mjml-preset-core-4.18.0.tgz", + "integrity": "sha512-x3l8vMVtsaqM/jauMeZIN7HFD2t5A28J4U0o4849yIlRxiWguLFV5l3BL8Byol+YLkoLuT9PjaZs9RYv+FGfeg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "mjml-accordion": "4.18.0", + "mjml-body": "4.18.0", + "mjml-button": "4.18.0", + "mjml-carousel": "4.18.0", + "mjml-column": "4.18.0", + "mjml-divider": "4.18.0", + "mjml-group": "4.18.0", + "mjml-head": "4.18.0", + "mjml-head-attributes": "4.18.0", + "mjml-head-breakpoint": "4.18.0", + "mjml-head-font": "4.18.0", + "mjml-head-html-attributes": "4.18.0", + "mjml-head-preview": "4.18.0", + "mjml-head-style": "4.18.0", + "mjml-head-title": "4.18.0", + "mjml-hero": "4.18.0", + "mjml-image": "4.18.0", + "mjml-navbar": "4.18.0", + "mjml-raw": "4.18.0", + "mjml-section": "4.18.0", + "mjml-social": "4.18.0", + "mjml-spacer": "4.18.0", + "mjml-table": "4.18.0", + "mjml-text": "4.18.0", + "mjml-wrapper": "4.18.0" + } + }, + "node_modules/mjml-raw": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-raw/-/mjml-raw-4.18.0.tgz", + "integrity": "sha512-F/kViAwXm3ccPP52kw++/mHQbcYbYYxC8JH15TZxH8GLVZkX5CGKgcBrHhDK7WoIlfEIsVRZ6IZdlHjH8vgyxw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-section": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-section/-/mjml-section-4.18.0.tgz", + "integrity": "sha512-bB8My9zvIEkTOxej+TrjEeaeRT0lsypGeRADtdrRZXeqUClkkuCnCXlsNKSLGT8ZRqjUqWRc5z8ubDOvGk2+Gg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-social": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-social/-/mjml-social-4.18.0.tgz", + "integrity": "sha512-iAQc9g59L6L3VHDd55BxeIvk/zHkxflxmvuyYyOOvpmmKAvUBC//ULfpxiiM4yupofsThqFfrO+wc8d4kTRkbQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-spacer": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-spacer/-/mjml-spacer-4.18.0.tgz", + "integrity": "sha512-FK/0f5IBiONgaRpwNBs7G8EbLdAbmYqcIfHR8O8tP4LipAChLQKHO9vX3vrRMGLBZZNTESLObcFSVWmA40Mfpw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-table": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-table/-/mjml-table-4.18.0.tgz", + "integrity": "sha512-vJysCPUL3CHcsQDAFpW+skzBtY0RYsmMBYswI4WX0B05GLKlOjXqpYOwcmAupWeGoBVL5r/t28ynu2PqnOlN3w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-text": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-text/-/mjml-text-4.18.0.tgz", + "integrity": "sha512-hBLmF3JgveUKktKQFWHqHAr7qr92j1CxAvq7mtpDUgiWgyPFzqRX8mUsFYgZ7DmRxG4UE+Kzpt8/YFd9+E98lw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-validator": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-4.18.0.tgz", + "integrity": "sha512-JmpWAsNTUlAxJOz2zHYfF8Vod8OzM3Qp5JXtrVw5tivZQzq88ZfqVGuqsas51z0pp1/ilfD4lC17YGfGwKGyhA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + } + }, + "node_modules/mjml-wrapper": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-wrapper/-/mjml-wrapper-4.18.0.tgz", + "integrity": "sha512-TZeOvLjIhXEK60rjWNiYhEYNlv5GKYahE+96ifcT5OGkWkRA0DsQDfp+6VI32OS5VxsfKq2h/UdERPlQijjpAQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0", + "mjml-section": "4.18.0" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/nanocolors": { + "version": "0.2.13", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nanostores": { + "version": "1.1.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/needle": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "license": "MIT", + "dependencies": { + "lower-case": "^1.1.1" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-releases": { "version": "2.0.27", "dev": true, "license": "MIT" }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9501,7 +10402,6 @@ }, "node_modules/nth-check": { "version": "2.1.1", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" @@ -9814,6 +10714,12 @@ "node": ">= 14" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/package-manager-detector": { "version": "0.2.11", "dev": true, @@ -9822,6 +10728,15 @@ "quansync": "^0.2.7" } }, + "node_modules/param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==", + "license": "MIT", + "dependencies": { + "no-case": "^2.2.0" + } + }, "node_modules/parent-module": { "version": "1.0.1", "dev": true, @@ -9852,7 +10767,7 @@ }, "node_modules/parse-node-version": { "version": "1.0.1", - "devOptional": true, + "dev": true, "license": "MIT", "peer": true, "engines": { @@ -9863,6 +10778,43 @@ "version": "6.0.1", "license": "MIT" }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "dev": true, @@ -9889,7 +10841,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9900,6 +10851,28 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/path-type": { "version": "4.0.0", "dev": true, @@ -9946,7 +10919,7 @@ }, "node_modules/pify": { "version": "4.0.1", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -10908,6 +11881,12 @@ "prosemirror-transform": "^1.1.0" } }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "license": "ISC" + }, "node_modules/proxy-agent": { "version": "6.5.0", "dev": true, @@ -10941,6 +11920,7 @@ }, "node_modules/prr": { "version": "1.0.1", + "dev": true, "license": "MIT", "optional": true, "peer": true @@ -11151,7 +12131,6 @@ }, "node_modules/readdirp": { "version": "3.6.0", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -11164,7 +12143,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -11405,9 +12383,17 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/require-directory": { "version": "2.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12379,14 +13365,14 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/sax": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "devOptional": true, + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -12453,7 +13439,6 @@ }, "node_modules/semver": { "version": "7.7.3", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -12485,7 +13470,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -12496,7 +13480,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12588,7 +13571,6 @@ }, "node_modules/signal-exit": { "version": "4.1.0", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -12658,6 +13640,15 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==", + "license": "MIT (http://mootools.net/license.txt)", + "engines": { + "node": "*" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "dev": true, @@ -12711,7 +13702,7 @@ }, "node_modules/source-map-support": { "version": "0.5.21", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -12720,7 +13711,7 @@ }, "node_modules/source-map-support/node_modules/source-map": { "version": "0.6.1", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -12865,6 +13856,71 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "license": "MIT", @@ -12879,7 +13935,19 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -13632,7 +14700,7 @@ }, "node_modules/terser": { "version": "5.44.0", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -13708,11 +14776,6 @@ } } }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "devOptional": true, - "license": "MIT" - }, "node_modules/text-decoder": { "version": "1.2.3", "dev": true, @@ -13772,7 +14835,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -13864,7 +14926,7 @@ }, "node_modules/tslib": { "version": "2.8.1", - "devOptional": true, + "dev": true, "license": "0BSD" }, "node_modules/tsscmp": { @@ -13931,7 +14993,7 @@ }, "node_modules/typescript": { "version": "5.9.3", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -14000,9 +15062,21 @@ "version": "2.1.0", "license": "MIT" }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/undici-types": { "version": "7.16.0", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/unicorn-magic": { @@ -14164,6 +15238,12 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==", + "license": "MIT" + }, "node_modules/uri-js": { "version": "4.4.1", "dev": true, @@ -14198,6 +15278,15 @@ "node": ">=10.12.0" } }, + "node_modules/valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/vary": { "version": "1.1.2", "dev": true, @@ -14351,6 +15440,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14367,6 +15457,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14383,6 +15474,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14399,6 +15491,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14413,6 +15506,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14429,6 +15523,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14445,6 +15540,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14461,6 +15557,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14477,6 +15574,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14493,6 +15591,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14509,6 +15608,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14525,6 +15625,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14541,6 +15642,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14557,6 +15659,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14573,6 +15676,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14589,6 +15693,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14605,6 +15710,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14621,6 +15727,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14637,6 +15744,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14653,6 +15761,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14669,6 +15778,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14685,6 +15795,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14701,6 +15812,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14717,6 +15829,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14733,6 +15846,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14749,6 +15863,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15015,6 +16130,120 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-resource-inliner": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz", + "integrity": "sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "node-fetch": "^2.6.0", + "valid-data-url": "^3.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/web-resource-inliner/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/dom-serializer/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.0.1" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domutils/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/htmlparser2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/fb55/htmlparser2?sponsor=1" + } + }, "node_modules/webdriver-bidi-protocol": { "version": "0.3.8", "dev": true, @@ -15205,7 +16434,6 @@ }, "node_modules/which": { "version": "2.0.2", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -15253,6 +16481,100 @@ "node": ">=12.17" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "dev": true, @@ -15320,7 +16642,6 @@ }, "node_modules/y18n": { "version": "5.0.8", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -15343,7 +16664,6 @@ }, "node_modules/yargs": { "version": "17.7.2", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -15360,7 +16680,6 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", - "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -15368,12 +16687,10 @@ }, "node_modules/yargs/node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -15448,6 +16765,7 @@ "@hbsnow/rehype-sectionize": "^1.0.7", "@stackoverflow/stacks": "*", "@stackoverflow/stacks-editor": "*", + "@stackoverflow/stacks-email": "*", "@stackoverflow/stacks-icons": "*", "@stackoverflow/stacks-icons-legacy": "*", "@stackoverflow/stacks-svelte": "*", @@ -15727,6 +17045,93 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "packages/stacks-email": { + "name": "@stackoverflow/stacks-email", + "version": "0.0.1", + "dependencies": { + "@stackoverflow/stacks": "*", + "@stackoverflow/stacks-svelte": "*", + "highlight.js": "^11.11.1", + "json2mjml": "^1.0.3", + "markdown-it": "^14.1.0", + "mjml": "^4.17.1", + "zod": "^4.1.12" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^6.1.0", + "@sveltejs/kit": "^2.48.5", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@types/mjml": "^5.0.0", + "eslint": "^9.39.2", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.14.1", + "globals": "^17.0.0", + "mdsvex": "^0.12.3", + "prettier": "^3.8.3", + "prettier-plugin-svelte": "^3.4.0", + "svelte": "^5.55.9", + "svelte-check": "^4.3.5", + "typescript": "^5.9.3", + "typescript-eslint": "^8.53.0", + "vite": "^7.3.3" + } + }, + "packages/stacks-email/node_modules/eslint-plugin-svelte": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.18.0.tgz", + "integrity": "sha512-vc3P37lrDronWDb2kPXiG8sqkuiMqitGXSSaflb7Y+jpDgNoAzW8i7tdqyJKpcLZmFIqZCD+je2oZRf9qyRyBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.6.1", + "@jridgewell/sourcemap-codec": "^1.5.0", + "esutils": "^2.0.3", + "globals": "^16.0.0", + "known-css-properties": "^0.37.0", + "postcss": "^8.4.49", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^7.0.0", + "semver": "^7.6.3", + "svelte-eslint-parser": "^1.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^8.57.1 || ^9.0.0 || ^10.0.0", + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "packages/stacks-email/node_modules/eslint-plugin-svelte/node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/stacks-email/node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "packages/stacks-svelte": { "name": "@stackoverflow/stacks-svelte", "version": "1.0.0-beta.32", diff --git a/packages/stacks-docs/README.md b/packages/stacks-docs/README.md index 7c31b252e2..6b3501b7dc 100644 --- a/packages/stacks-docs/README.md +++ b/packages/stacks-docs/README.md @@ -161,3 +161,10 @@ The private docs follow the same structure and conventions as public docs. Mark 3. **Add any images or assets** to the same directory as your markdown file 4. **Reference assets** using relative paths (e.g., `./image.svg`) in your markdown + +## Email compile API auth (optional) + +`POST /api/email/compile` supports an optional shared Bearer token. + +- If `STACKS_EMAIL_AUTH_TOKEN` is not set: auth is disabled. +- If `STACKS_EMAIL_AUTH_TOKEN` is set: the request must include `Authorization: Bearer `. diff --git a/packages/stacks-docs/_redirects b/packages/stacks-docs/_redirects index 25c51278eb..37ada0fa19 100644 --- a/packages/stacks-docs/_redirects +++ b/packages/stacks-docs/_redirects @@ -2,9 +2,6 @@ https://alpha.stackoverflow.design/* https://stackoverflow.design/:splat 301 https://beta.stackoverflow.design/* https://stackoverflow.design/:splat 302 -# Email section → v2 docs -/email/* https://v2.stackoverflow.design/email/:splat 302 - # Redirects from v2 urls /product /system/develop/using-stacks/ 302 /base/* /system/base/:splat 302 diff --git a/packages/stacks-docs/package.json b/packages/stacks-docs/package.json index ec738643b8..a6aeb39788 100644 --- a/packages/stacks-docs/package.json +++ b/packages/stacks-docs/package.json @@ -25,6 +25,7 @@ "dependencies": { "@docsearch/css": "^4.3.2", "@docsearch/js": "^4.3.2", + "@stackoverflow/stacks-email": "*", "@hbsnow/rehype-sectionize": "^1.0.7", "@stackoverflow/stacks": "*", "@stackoverflow/stacks-editor": "*", diff --git a/packages/stacks-docs/src/app.css b/packages/stacks-docs/src/app.css index 7d3f7fc100..e79257d309 100644 --- a/packages/stacks-docs/src/app.css +++ b/packages/stacks-docs/src/app.css @@ -307,7 +307,7 @@ body { .ff-stack-sans-headline, .ff-stack-sans-headline-notch { - font-family: "Stack Sans Headline"; + font-family: "Stack Sans Headline" !important; } .ff-stack-sans-headline-notch { font-feature-settings: "ss01" 1 !important; @@ -550,6 +550,18 @@ h1 { padding: var(--su16); } +/* Docs section card – used with a thumbnail to link to other areas in the docs i.e, for index pages */ + +.docs-index-card { + border: var(--su1) solid var(--black-225); +} +.docs-index-card .docs-heading { + margin: 0 var(--su16) var(--su4); +} +.docs-index-card .docs-heading + p.docs-copy { + margin: 0 var(--su16) var(--su16); +} + /* Notices in doc content get breathing room below */ .docs .s-notice { margin-bottom: var(--su16); diff --git a/packages/stacks-docs/src/components/EmailOptionsTable.svelte b/packages/stacks-docs/src/components/EmailOptionsTable.svelte new file mode 100644 index 0000000000..df120d7dbd --- /dev/null +++ b/packages/stacks-docs/src/components/EmailOptionsTable.svelte @@ -0,0 +1,88 @@ + + +
+ + + + + + + + + + + {#each resolvedRows as row (row.argument)} + + + + + + + {/each} + +
ArgumentTypeDefaultDescription
{row.argument}{row.type} + {#if hasDefaultValue(row.defaultValue)} + {#if row.defaultValueCode === false} + {row.defaultValue} + {:else} + {row.defaultValue} + {/if} + {/if} + {row.description}
+
diff --git a/packages/stacks-docs/src/components/StacksEmailEmbed.svelte b/packages/stacks-docs/src/components/StacksEmailEmbed.svelte new file mode 100644 index 0000000000..dde89b5281 --- /dev/null +++ b/packages/stacks-docs/src/components/StacksEmailEmbed.svelte @@ -0,0 +1,401 @@ + + +
+
+ + {#each tabOptions as tab (tab.id)} + (activeTab = tab.id)} + /> + {/each} + + + + + {#if activeTab === "preview"} + + {#each viewportOptions as viewport (viewport.id)} + (previewViewport = viewport.id)} + /> + {/each} + + {/if} + + {#if activeTab !== "preview" && (compiled || activeTab === "usage")} + + {/if} +
+ + {#if loading} +

Compiling…

+ {:else if errorMessage} +

{errorMessage}

+ {:else if compiled} + {#if activeTab === "preview"} + + {:else} + {@html highlightedCodeBlock} + {/if} + + {#if showTokens && catalogItem && catalogItem.tokens.length > 0} +
    + {#each catalogItem.tokens as token (token.token)} +
  • + [[{token.token}]] — {token.description} +
  • + {/each} +
+ {/if} + {/if} +
+ + diff --git a/packages/stacks-docs/src/docs/public/email/components/button.md b/packages/stacks-docs/src/docs/public/email/components/button.md new file mode 100644 index 0000000000..338a88f0e3 --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/button.md @@ -0,0 +1,27 @@ +--- +title: Button +description: Reusable CTA primitive used across text and card blocks. +--- + + + +## Variants + +### Filled + + + +### Tonal + + + +### Inverted + + + +## Options + + diff --git a/packages/stacks-docs/src/docs/public/email/components/cards.md b/packages/stacks-docs/src/docs/public/email/components/cards.md new file mode 100644 index 0000000000..00bcb2e235 --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/cards.md @@ -0,0 +1,26 @@ +--- +title: Cards +description: Multi-card content block documentation is in progress. +--- + + + +## Variants + +Coming soon. + +## Options + + diff --git a/packages/stacks-docs/src/docs/public/email/components/component-button.svg b/packages/stacks-docs/src/docs/public/email/components/component-button.svg new file mode 100644 index 0000000000..99235f9bcf --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/component-button.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/stacks-docs/src/docs/public/email/components/component-cards.svg b/packages/stacks-docs/src/docs/public/email/components/component-cards.svg new file mode 100644 index 0000000000..12c58f8932 --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/component-cards.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/stacks-docs/src/docs/public/email/components/component-dividers.svg b/packages/stacks-docs/src/docs/public/email/components/component-dividers.svg new file mode 100644 index 0000000000..0890dddbc6 --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/component-dividers.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/stacks-docs/src/docs/public/email/components/component-footer.svg b/packages/stacks-docs/src/docs/public/email/components/component-footer.svg new file mode 100644 index 0000000000..aa8bdca855 --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/component-footer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/stacks-docs/src/docs/public/email/components/component-graphic.svg b/packages/stacks-docs/src/docs/public/email/components/component-graphic.svg new file mode 100644 index 0000000000..cee269442e --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/component-graphic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/stacks-docs/src/docs/public/email/components/component-header.svg b/packages/stacks-docs/src/docs/public/email/components/component-header.svg new file mode 100644 index 0000000000..2040c8809f --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/component-header.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/stacks-docs/src/docs/public/email/components/component-headline.svg b/packages/stacks-docs/src/docs/public/email/components/component-headline.svg new file mode 100644 index 0000000000..e806138fd4 --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/component-headline.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/stacks-docs/src/docs/public/email/components/component-link.svg b/packages/stacks-docs/src/docs/public/email/components/component-link.svg new file mode 100644 index 0000000000..9aaee84af8 --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/component-link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/stacks-docs/src/docs/public/email/components/component-preview.svg b/packages/stacks-docs/src/docs/public/email/components/component-preview.svg new file mode 100644 index 0000000000..1353bd6a8a --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/component-preview.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/stacks-docs/src/docs/public/email/components/component-spacers.svg b/packages/stacks-docs/src/docs/public/email/components/component-spacers.svg new file mode 100644 index 0000000000..9915a4de39 --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/component-spacers.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/stacks-docs/src/docs/public/email/components/component-subtitle.svg b/packages/stacks-docs/src/docs/public/email/components/component-subtitle.svg new file mode 100644 index 0000000000..498ba7d05f --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/component-subtitle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/stacks-docs/src/docs/public/email/components/component-text.svg b/packages/stacks-docs/src/docs/public/email/components/component-text.svg new file mode 100644 index 0000000000..65b3b10787 --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/component-text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/stacks-docs/src/docs/public/email/components/component-title.svg b/packages/stacks-docs/src/docs/public/email/components/component-title.svg new file mode 100644 index 0000000000..81fdfad4e7 --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/component-title.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/stacks-docs/src/docs/public/email/components/dividers.md b/packages/stacks-docs/src/docs/public/email/components/dividers.md new file mode 100644 index 0000000000..17a04d49be --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/dividers.md @@ -0,0 +1,26 @@ +--- +title: Dividers +description: Divider block documentation is in progress. +--- + + + +## Variants + +Coming soon. + +## Options + + diff --git a/packages/stacks-docs/src/docs/public/email/components/footer.md b/packages/stacks-docs/src/docs/public/email/components/footer.md new file mode 100644 index 0000000000..fac75d8135 --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/footer.md @@ -0,0 +1,33 @@ +--- +title: Footer +description: Footer scaffolds for dark and light email shells. +--- + + + +## Variants + +### Default + +No reason copy shown. + + + +### Reason + +Includes recipient reason copy. + + + +### Social + +Includes reason copy plus social links. + + + +## Options + + diff --git a/packages/stacks-docs/src/docs/public/email/components/graphic.md b/packages/stacks-docs/src/docs/public/email/components/graphic.md new file mode 100644 index 0000000000..eb84574bd9 --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/graphic.md @@ -0,0 +1,27 @@ +--- +title: Graphic +description: Image block variants for spot and hero placements. +--- + + + +## Variants + +### Spot + +140x140 left-aligned PNG placeholder. + + + +### Hero + +1200x630 full-width placeholder with left/right container padding. + + + +## Options + + diff --git a/packages/stacks-docs/src/docs/public/email/components/header.md b/packages/stacks-docs/src/docs/public/email/components/header.md new file mode 100644 index 0000000000..8cbb9dce59 --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/header.md @@ -0,0 +1,35 @@ +--- +title: Header +description: Brand strip and utility-nav email header scaffolds. +--- + + + +## Variants + +### Transactional + + + +### Brand + + + +### Center + + + +### Inverted + + + +### Stack Overflow Business + + + +## Options + + diff --git a/packages/stacks-docs/src/docs/public/email/components/headline.md b/packages/stacks-docs/src/docs/public/email/components/headline.md new file mode 100644 index 0000000000..c62a49957f --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/headline.md @@ -0,0 +1,25 @@ +--- +title: Headline +description: Large headline block with default and highlighted background treatments. +--- + + + +## Variants + +### Default + + + +### Highlight + +Inline highlighted `` wrapper + + + +## Options + + diff --git a/packages/stacks-docs/src/docs/public/email/components/preview.md b/packages/stacks-docs/src/docs/public/email/components/preview.md new file mode 100644 index 0000000000..a1685e6f79 --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/preview.md @@ -0,0 +1,26 @@ +--- +title: Preview Text +description: Template compile option for controlling inbox preheader content. +--- + +Preview text is a template-level option, not a standalone visual component. + +When compiling any template, pass `PREVIEW_TEXT` in `props` to inject `` content: + +```ts +import { compileEmailRenderable } from "@stackoverflow/stacks-email"; + +const compiled = compileEmailRenderable({ + kind: "template", + slug: "transactional", + target: "preview", + props: { + PREVIEW_TEXT: "Reset your password in one click.", + }, +}); + +console.log(compiled.renderedMjml); +// Includes: Reset your password in one click. +``` + +Pass `PREVIEW_TEXT: ""` to omit the preview tag. diff --git a/packages/stacks-docs/src/docs/public/email/components/spacers.md b/packages/stacks-docs/src/docs/public/email/components/spacers.md new file mode 100644 index 0000000000..98314b7396 --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/spacers.md @@ -0,0 +1,23 @@ +--- +title: Spacers +description: Vertical spacing primitive built from `mj-spacer` wrapped in a section. +--- + + + +## Variants + +### Medium + + + +### Large + + + +## Options + + diff --git a/packages/stacks-docs/src/docs/public/email/components/subtitle.md b/packages/stacks-docs/src/docs/public/email/components/subtitle.md new file mode 100644 index 0000000000..a77106a42b --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/subtitle.md @@ -0,0 +1,26 @@ +--- +title: Subtitle +description: Subtitle component documentation is in progress. +--- + + + +## Variants + +Coming soon. + +## Options + + diff --git a/packages/stacks-docs/src/docs/public/email/components/text.md b/packages/stacks-docs/src/docs/public/email/components/text.md new file mode 100644 index 0000000000..802eb9a625 --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/text.md @@ -0,0 +1,24 @@ +--- +title: Text +description: Body copy plus alert, quote, and highlight component examples. +updated: 2026-05-31 +--- + + + +## Variants + +### Default + + + +### Center + + + +## Options + + diff --git a/packages/stacks-docs/src/docs/public/email/components/title.md b/packages/stacks-docs/src/docs/public/email/components/title.md new file mode 100644 index 0000000000..58f814134b --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/components/title.md @@ -0,0 +1,23 @@ +--- +title: Title +description: Section title block with default and inverted background treatments. +--- + + + +## Variants + +### Default + + + +### Invert + + + +## Options + + diff --git a/packages/stacks-docs/src/docs/public/email/overview.md b/packages/stacks-docs/src/docs/public/email/overview.md new file mode 100644 index 0000000000..f67e73c74b --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/overview.md @@ -0,0 +1,279 @@ +--- +title: Email +description: Patterns and guidelines for creating and sending emails to Stack Overflow users & customers. +figma: https://www.figma.com/design/1uHwOvLiVtTwqv9vGegp8a/ +updated: 2026-06-01 +--- + + + +## Introduction + +Emails are a great opportunity to showcase the brand’s personality to loyal users or perspective customers who may otherwise only interact with the product itself. + +From transactional updates to editorial moments, every email is a chance to strengthen familiarity with the brand and build a more connected experience. + +These guidelines are designed to provide a foundation for what those emails can be, while leaving room to evolve and expand over time. + +## Creating emails + +Our documentation is built from components built with [MJML](https://mjml.io/) – an open-source email framework that abstracts away the need to manually code email HTML. [Read the full documentation](https://documentation.mjml.io). + +## Templates + +We have a range of email templates, each serving a distinct purpose while demonstrating how communication can scale from functional to expressive. + +
+ + Functional + + + — + + + Expressive + +
+ + + + +[![Transactional email template preview](./templates/email-template-transactional.png)](./templates/transactional) + +### [Transactional](./templates/transactional) + +A transactional email is functional. It is triggered by an event and usually is a short single message and call to action. + + + + +[![Newsletter email template preview](./templates/email-template-newsletter.png)](./templates/newsletter) + +### [Newsletter](./templates/newsletter) + +A newsletter is a recurring pieces of comms that may contain various items and call to actions. + + + + +[![Promotional email template preview](./templates/email-template-promotional.png)](./templates/promotional) + +### [Promotional](./templates/promotional) + +Typically single-message communications - short, punchy, and to the point — designed to quickly capture attention and drive engagement. + + + + + +## Components + +Each email is built from reusable component blocks. The set below is the canonical starting library. + + + + +[![Header component preview](./components/component-header.svg)](./components/header) + +### [Header](./components/header) + +Top brand strip and utility nav variations. + + + + +[![Footer component preview](./components/component-footer.svg)](./components/footer) + +### [Footer](./components/footer) + +Legal metadata and recipient preference links. + + + + +[![Title component preview](./components/component-title.svg)](./components/title) + +### [Title](./components/title) + +Section title treatments. + + + + +[![Headline component preview](./components/component-headline.svg)](./components/headline) + +### [Headline](./components/headline) + +Large hero headline treatments. + + + + +[![Button component preview](./components/component-button.svg)](./components/button) + +### [Button](./components/button) + +Reusable CTA primitive used across blocks. + + + + +[![Subtitle component preview](./components/component-subtitle.svg)](./components/subtitle) + +### [Subtitle](./components/subtitle) + +Supporting labels and secondary lines. + + + + +[![Text component preview](./components/component-text.svg)](./components/text) + +### [Text](./components/text) + +Body copy plus alert, quote, and highlight component examples. + + + + +[![Cards component preview](./components/component-cards.svg)](./components/cards) + +### [Cards](./components/cards) + +Simple, link, and CTA card layouts. + + + + + +[![Graphic component preview](./components/component-graphic.svg)](./components/graphic) + +### [Graphic](./components/graphic) + +Standalone illustration placeholder block. + + + + +[![Dividers component preview](./components/component-dividers.svg)](./components/dividers) + +### [Dividers](./components/dividers) + +Subtle and strong horizontal separators. + + + + +[![Spacers component preview](./components/component-spacers.svg)](./components/spacers) + +### [Spacers](./components/spacers) + +Preset vertical rhythm utilities. + + + + + +## Usage + + +

Warning: This functionality is experimental and may change. + + +If you are running the `@stackoverflow/stacks-email` package, you can compose and render email markup by POSTing a JSON block list to the compile API. + +### POST /api/compile + +**Paramaters** + +- `template`: currently supports `"transactional"`. +- `target`: one of `"preview"`, `"dotnet"`, or `"braze"`. +- `blocks`: ordered array of block definitions. +- `previewText`: optional template preheader/inbox snippet text. + +**Example request:** + +```json +{ + "template": "transactional", + "target": "preview", + "previewText": "Reset your password in one click.", + "blocks": [ + { + "type": "headline", + "variant": "highlight", + "props": { + "textContent": "Reset your password", + "textHighlight": true + } + }, + { + "type": "text", + "variant": "body", + "props": { + "textContent": "Hi [[FIRST_NAME]], click below to continue." + } + }, + { + "type": "button", + "variant": "primary", + "props": { + "href": "[[CTA_URL]]", + "text": "Reset password" + } + } + ] +} +``` + +
+ +**Response** + +Successful responses include compiled `html`, final `mjml`, `renderedMjml`, compile `errors`, and metadata such as `template`, `target`, and `blockCount`. + +
+ +## Target clients + +[Litmus](https://www.litmus.com/) publishes a regularly updated list of [email clients and their observed market share](https://www.litmus.com/email-client-market-share), you can use this as a rough guideline when testing and making decisons about compatability. + +| Client | Share (%) | +| -------------- | --------- | +| Apple | 45.51 | +| Gmail | 23.54 | +| Outlook | 5.67 | +| Yahoo Mail | 2.06 | +| Google Android | 1.34 | +| Outlook.com | 0.40 | +| Thunderbird | 0.17 | +| Orange.fr | 0.08 | +| Bell Email | 0.02 | +| Samsung Mail | 0.02 | + +

+ +## Other resources + +### [Email gallery](https://email.stackoverflow.design/) + +[email.stackoverflow.design](https://email.stackoverflow.design/) + +Our own gallery of email designs. [These templates](https://github.com/StackExchange/Stacks/tree/main/packages/stacks-email/templates) are used to build the examples in this section lives along side the [email components](https://github.com/StackExchange/Stacks/tree/main/packages/stacks-email/components) so feel free to add your templates back in to serve as inspiration for others. + +### [Can I email?](https://www.caniemail.com/) + +[caniemail.com](https://www.caniemail.com/) + +On occasion you may need to hard code some elements of email, if straying from the components here or implementing something ourside of the scope of MJML. ‘Can I email’ is a great resource for dealing with the eccentricities of email development. + +### [Really Good Emails](https://reallygoodemails.com/) + +[reallygoodemails.com](https://reallygoodemails.com/) + +A constantly evolving gallery of email designs from across the web. Espeically useful for designers or product managers planning out a new email. diff --git a/packages/stacks-docs/src/docs/public/email/templates/email-template-newsletter.png b/packages/stacks-docs/src/docs/public/email/templates/email-template-newsletter.png new file mode 100644 index 0000000000000000000000000000000000000000..b8379f91aec5b43e0c4c351ab5cde4a5347596cd GIT binary patch literal 42877 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¨V+iz5lNasd=<*H_ppMEZ=l{myDDovIgCn|- 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 literal 0 HcmV?d00001 diff --git a/packages/stacks-docs/src/docs/public/email/templates/email-template-transactional.png b/packages/stacks-docs/src/docs/public/email/templates/email-template-transactional.png new file mode 100644 index 0000000000000000000000000000000000000000..f71af8f428e519ebe7b4f62f19e1e52486de20c7 GIT binary patch literal 50358 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 + import { Icon } from "@stackoverflow/stacks-svelte"; + import { IconCheckFillCircle } from "@stackoverflow/stacks-icons/icons"; + + +## Requirements + +See the overview below outlining the required components, their variations, and any optional add-ons that may be needed. + +

+ +**Coming soon** diff --git a/packages/stacks-docs/src/docs/public/email/templates/promotional.md b/packages/stacks-docs/src/docs/public/email/templates/promotional.md new file mode 100644 index 0000000000..20693665bd --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/templates/promotional.md @@ -0,0 +1,128 @@ +--- +title: Promotional +description: Typically single-message communications - short, punchy, and to the point — designed to quickly capture attention and drive engagement. +updated: 2026-06-01 +--- + + + +## Requirements + +See the overview below outlining the required components, their variations, and any optional add-ons that may be needed. + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ComponentQuantityDescriptionRequired
Header component1 + Use the Stack Overflow wordmark and icon on every email. For B2B-focused sends, use the Stack Overflow Business header. + + +
Headline components (5 variants)1 + Select the headline variant that best matches campaign content. Keep copy concise and impactful; this often carries the primary message. + + +
Footer1End all emails with a simple, branded footer. + +
Text block (2 variants)0-1 + Optional supporting copy block. Keep content concise, include links where relevant, and focus on one clear CTA. +
Secondary content0-1 + Optional secondary module for additional, unrelated content when needed. +
DividersAs needed + Use visual dividers to separate repeated simple-card style blocks. +
CTA cards0-2 + Optional graphic-led cards combining short copy and a clear CTA. +
Link cards0-4 + Optional cards for highlighting resources without extra assets or long context. +
Secondary information (3 variants)0-1 + Optional secondary information block with variant styles for different contexts. +
Quote0-1 + Optional quote block for additional context or color, limited to one per email. +
Highlights0-1 + Optional text-plus-illustration block for an eye-catching end section. +
+
+ +**Coming soon** diff --git a/packages/stacks-docs/src/docs/public/email/templates/transactional.md b/packages/stacks-docs/src/docs/public/email/templates/transactional.md new file mode 100644 index 0000000000..d360eb90a0 --- /dev/null +++ b/packages/stacks-docs/src/docs/public/email/templates/transactional.md @@ -0,0 +1,95 @@ +--- +title: Transactional +description: A transactional email is functional. It is triggered by an event and usually is a short single message and call to action. +updated: 2026-06-01 +--- + + + +## Requirements + +See the overview below outlining the required components, their variations, and any optional add-ons that may be needed. + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ComponentQuantityDescriptionRequired
Headerx 1Use the Stack Overflow wordmark and icon at the top of every email. + +
Headline (2 variants)x 1 + Keep headline copy ideally under 50 characters and make the core message clear without relying on body text. + + +
Text block + primary CTAx 1 + Body copy can be longer but should stay concise, include links where needed, and focus on one clear CTA per transactional email. + + +
Footerx 1End all emails with a simple branded footer and utility links. + +
Illustrationx 1 + Optional branded illustration for additional context. Use no more than one. +
Alertx 1 + Optional secondary message not directly tied to the primary communication. Keep copy under 150 characters where possible and use no more than one. +
+
+ +## Short + + + +## Long + + diff --git a/packages/stacks-docs/src/routes/[category]/[[section]]/[subsection]/+page.svelte b/packages/stacks-docs/src/routes/[category]/[[section]]/[subsection]/+page.svelte index 5e26e3c260..59bcfaed35 100644 --- a/packages/stacks-docs/src/routes/[category]/[[section]]/[subsection]/+page.svelte +++ b/packages/stacks-docs/src/routes/[category]/[[section]]/[subsection]/+page.svelte @@ -13,8 +13,16 @@ day: 'numeric' })); - const pageTitle = $derived(data.active.title ? `${data.active.title} - Stack Overflow Design System` : 'Stack Overflow Design System'); - const pageDescription = $derived(data?.metadata?.description || `Documentation for ${data.active.title} in the Stack Overflow Design System`); + const activeTitle = $derived.by(() => { + const metadataTitle = + typeof data?.metadata?.title === 'string' ? data.metadata.title : undefined; + return data?.active?.title ?? metadataTitle ?? 'Documentation'; + }); + const pageTitle = $derived(`${activeTitle} - Stack Overflow Design System`); + const pageDescription = $derived( + data?.metadata?.description || + `Documentation for ${activeTitle} in the Stack Overflow Design System` + ); async function copyPageUrl() { await navigator.clipboard.writeText(page.url.href); @@ -85,7 +93,7 @@ {/if}

- {data.active.title} + {activeTitle}

{#if data?.metadata?.description} diff --git a/packages/stacks-docs/src/routes/api/email/catalog/+server.ts b/packages/stacks-docs/src/routes/api/email/catalog/+server.ts new file mode 100644 index 0000000000..51fe5db41e --- /dev/null +++ b/packages/stacks-docs/src/routes/api/email/catalog/+server.ts @@ -0,0 +1,6 @@ +import { json } from "@sveltejs/kit"; +import type { RequestHandler } from "./$types"; + +import { getEmailCatalog } from "@stackoverflow/stacks-email"; + +export const GET: RequestHandler = async () => json(getEmailCatalog()); diff --git a/packages/stacks-docs/src/routes/api/email/compile/+server.ts b/packages/stacks-docs/src/routes/api/email/compile/+server.ts new file mode 100644 index 0000000000..f7268face9 --- /dev/null +++ b/packages/stacks-docs/src/routes/api/email/compile/+server.ts @@ -0,0 +1,66 @@ +import { json } from "@sveltejs/kit"; +import type { RequestHandler } from "./$types"; +import { env } from "$env/dynamic/private"; + +import { + compileEmailRenderable, + compileEmailRenderableInputSchema, +} from "@stackoverflow/stacks-email"; + +const hasValidBearerToken = (request: Request): boolean => { + const expectedToken = env.STACKS_EMAIL_AUTH_TOKEN?.trim(); + if (!expectedToken) { + return true; + } + + const authorization = request.headers.get("authorization"); + if (!authorization) { + return false; + } + + const [scheme, token] = authorization.trim().split(/\s+/, 2); + return scheme?.toLowerCase() === "bearer" && token === expectedToken; +}; + +export const POST: RequestHandler = async ({ request }) => { + if (!hasValidBearerToken(request)) { + return json({ error: "Unauthorized." }, { status: 401 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch { + return json( + { error: "Request body must be valid JSON." }, + { status: 400 } + ); + } + + const parsed = compileEmailRenderableInputSchema.safeParse(body); + if (!parsed.success) { + return json( + { + error: parsed.error.issues.map((issue) => issue.message).join(" "), + }, + { status: 400 } + ); + } + + try { + const compiled = compileEmailRenderable(parsed.data); + + return json(compiled); + } catch (error) { + return json( + { + error: + error instanceof Error + ? error.message + : "Failed to compile email renderable.", + }, + { status: 404 } + ); + } +}; diff --git a/packages/stacks-docs/src/structure.yaml b/packages/stacks-docs/src/structure.yaml index 3dd553d43b..28eec5aa13 100644 --- a/packages/stacks-docs/src/structure.yaml +++ b/packages/stacks-docs/src/structure.yaml @@ -321,18 +321,63 @@ navigation: - title: "Vote" slug: "vote" - # - title: "Email" - # slug: "email" - # description: "Patterns and guidelines for creating and sending emails to Stack Overflow users." - # items: - # - title: "Account" - # slug: "account" + - title: "Email" + slug: "email" + description: "Patterns and tooling for composing tokenized MJML emails." + items: + - title: "Overview" + slug: "overview" + image: "/images/heros/email-overview.svg" + + - title: "Templates" + slug: "templates" + image: "/images/heros/email-types.svg" + items: + - title: "Transactional" + slug: "transactional" + + - title: "Newsletter" + slug: "newsletter" + + - title: "Promotional" + slug: "promotional" + + - title: "Components" + slug: "components" + image: "/images/heros/email-authoring.svg" + items: + - title: "Header" + slug: "header" + + - title: "Footer" + slug: "footer" - # - title: "Transactional" - # slug: "tranactional" + - title: "Headline" + slug: "headline" - # - title: "Marketing" - # slug: "marketing" + - title: "Title" + slug: "title" + + - title: "Subtitle" + slug: "subtitle" + + - title: "Text" + slug: "text" + + - title: "Cards" + slug: "cards" + + - title: "Graphic" + slug: "graphic" + + - title: "Dividers" + slug: "dividers" + + - title: "Spacers" + slug: "spacers" + + - title: "Button" + slug: "button" - title: "Handbook" slug: "handbook" @@ -366,6 +411,10 @@ navigation: slug: "icons" image: "/images/heros/product.svg" + - title: "Email gallery" + slug: "emails" + externalUrl: https://email.stackoverflow.design + - title: "Trademark guidelines" slug: "trademarks" image: "/images/heros/strategy.svg" diff --git a/packages/stacks-docs/static/email b/packages/stacks-docs/static/email new file mode 120000 index 0000000000..cb3a3d36a4 --- /dev/null +++ b/packages/stacks-docs/static/email @@ -0,0 +1 @@ +../../stacks-email/static/email \ No newline at end of file diff --git a/packages/stacks-docs/static/images/heros/email-authoring.svg b/packages/stacks-docs/static/images/heros/email-authoring.svg new file mode 100644 index 0000000000..a2e8cdfb4f --- /dev/null +++ b/packages/stacks-docs/static/images/heros/email-authoring.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/stacks-docs/static/images/heros/email-components.svg b/packages/stacks-docs/static/images/heros/email-components.svg new file mode 100644 index 0000000000..b908f5b815 --- /dev/null +++ b/packages/stacks-docs/static/images/heros/email-components.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/stacks-docs/static/images/heros/email-overview.svg b/packages/stacks-docs/static/images/heros/email-overview.svg new file mode 100644 index 0000000000..27504f8eb4 --- /dev/null +++ b/packages/stacks-docs/static/images/heros/email-overview.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/packages/stacks-docs/static/images/heros/email-types.svg b/packages/stacks-docs/static/images/heros/email-types.svg new file mode 100644 index 0000000000..2827983609 --- /dev/null +++ b/packages/stacks-docs/static/images/heros/email-types.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/stacks-docs/static/social/instagram.png b/packages/stacks-docs/static/social/instagram.png new file mode 100644 index 0000000000000000000000000000000000000000..c91e85e8215304c3a91022869e4d648d8a741807 GIT binary patch literal 906 zcmV;519kj~P)P`@6f|3cK6Eqv7nV`)C z&7*dps3*kP<%&%+!v=f%iu4CvP+v83Mif?FqCw~H6wh${8UpvBfyG_Ul6t-ke;LWeK?zoa5 z9Y>opvMf>$>-BmLOKf<#*h*Z-yO-s1Io5qUoz9Hjav>uFv8hAHdt1ke4?-~eU&M7d zZCM(!oxBKtf=m*Af-*BR0d($lU+5fE5!9VCbncNqIaa8o9$Vi^GXZpNNLJ1^iff5Y zg-%%Ha^*Z3lshQ{y!%9lCCw+~^35t;WdxlNzluz9u#B?%;oX$KnIsVy_Y{a=f^hE7 zx())R`zU`COX~hN6sj+zv)-ztnX;9t#)x(G5+)_fOcxWR13AVk`9m4?hyoBw@T^0E zg+?uy8ubu=GA+NL00a?~J!-=oZxSW`Ul-c6hBcQ z&Pg-h$V$NGo@QXxRS3<|ZQ0rEg%Cj)qBmtYd1PR%p6mN2tu9H5c zOI(&zS)sA#x*}7-%8ykia!bca0KIdJVP%ojIXx&Uf%w(b*kxEr8W62<5JAlP6|>Hd z{c`G6_8|(5H1j+u#4{rKvzy*riE2C~0=(92ZeD2}nOnrV5UcFQjpJ@J~WX&X@S(&Udjw#F_hschCpEZpSeCL!kJ2&+Jp|Br5i;5`m| zasVfS1KpOUX^2CM9AFg)h7j~u{y6WWU_Qwag+{&G?Xokz$Fzo2BPhlbP24skY0+hA zP4J6z_m}Fk>^74(pm0X8y{gLy=ac+6mcY9dBkd@tC_g6GuNr3wWkpbLGk9OihJAJ% zeKJNXdREnLde%Lbl3d3_`VWC+>2|vln~#hxBKd9e<5Y1is7OXOzuE+1txb`}RCWcV ze1+>mfgyrlw&&C$Q0^%)`GQo{Bb{Ru)|0{2`ZlUXAO!U}HV$i%n5-wg~ zRlLBec!5>nY!+I0Rn7ATt;}?25o5(z;l5qe4002ovPDHLkV1nF)|EvH2 literal 0 HcmV?d00001 diff --git a/packages/stacks-docs/static/social/threads.png b/packages/stacks-docs/static/social/threads.png new file mode 100644 index 0000000000000000000000000000000000000000..c9c391aaab100d6dbbd0104c55da0230b24a1cf1 GIT binary patch literal 1111 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 + node?.data?.disableHeadingAnchor !== true, properties: { className: ["docs-heading-anchor"], ariaHidden: "true", @@ -79,6 +81,134 @@ function exposeToc() { }; } +const VOID_TAGS = new Set([ + "area", + "base", + "br", + "col", + "embed", + "hr", + "img", + "input", + "link", + "meta", + "param", + "source", + "track", + "wbr", +]); + +const RAW_TAG_PATTERN = + /<\/?([A-Za-z][A-Za-z0-9:._-]*)(?:(?:\s[^<>]*)?)\/?\s*>/g; + +function headingRank(node) { + if (node?.type !== "element" || !/^h[1-6]$/.test(node.tagName)) { + return null; + } + return Number.parseInt(node.tagName.slice(1), 10); +} + +function updateRawTagStack(raw, stack) { + RAW_TAG_PATTERN.lastIndex = 0; + + for (const match of raw.matchAll(RAW_TAG_PATTERN)) { + const fullMatch = match[0]; + const tagName = match[1]; + const isClosing = fullMatch.startsWith(""); + const isVoid = VOID_TAGS.has(tagName.toLowerCase()); + + if (isClosing) { + const lastIndex = stack.lastIndexOf(tagName); + if (lastIndex !== -1) { + stack.splice(lastIndex, 1); + } + continue; + } + + if (!isSelfClosing && !isVoid) { + stack.push(tagName); + } + } +} + +function isInsideTag(rawTagStack, tagName) { + const target = tagName.toLowerCase(); + return rawTagStack.some((openTag) => openTag.toLowerCase() === target); +} + +// Mark headings inside blocks so autolink anchors can skip them. +function markHeadingsInsideGrid() { + return function (tree) { + const rawTagStack = []; + + for (const node of tree.children) { + if (node.type === "raw") { + updateRawTagStack(node.value, rawTagStack); + } + + const rank = headingRank(node); + if (rank !== null && isInsideTag(rawTagStack, "grid")) { + node.data = { ...node.data, disableHeadingAnchor: true }; + } + } + }; +} + +// Sectionize top-level markdown headings, but skip headings rendered inside raw +// blocks/components (e.g. markdown inside slots). +function sectionizeTopLevelHeadings() { + const createSection = (rank, headingNode = null) => { + const headingId = headingNode?.properties?.id; + + return { + type: "element", + tagName: "section", + properties: { + className: ["heading"], + dataHeadingRank: rank, + ...(typeof headingId === "string" + ? { ariaLabelledby: headingId } + : {}), + }, + children: headingNode ? [headingNode] : [], + }; + }; + + return function (tree) { + const rootWrapper = createSection(0); + const wrapperStack = [rootWrapper]; + const rawTagStack = []; + + const currentWrapper = () => wrapperStack[wrapperStack.length - 1]; + const currentRank = () => currentWrapper().properties.dataHeadingRank; + + for (const node of tree.children) { + if (node.type === "raw") { + updateRawTagStack(node.value, rawTagStack); + } + + const rank = headingRank(node); + const shouldSectionize = rank !== null && rawTagStack.length === 0; + + if (!shouldSectionize) { + currentWrapper().children.push(node); + continue; + } + + while (rank <= currentRank()) { + wrapperStack.pop(); + } + + const section = createSection(rank, node); + currentWrapper().children.push(section); + wrapperStack.push(section); + } + + tree.children = rootWrapper.children; + }; +} + // Custom plugin to add individual docs-* classes to generated elements. // These replace the old .doc X descendant-selector pattern so each element // carries its own class and does not depend on a parent .doc wrapper. diff --git a/packages/stacks-email/README.md b/packages/stacks-email/README.md new file mode 100644 index 0000000000..c253887b51 --- /dev/null +++ b/packages/stacks-email/README.md @@ -0,0 +1,96 @@ +# Stacks Email + +Stack Overflow’s MJML powered email compile engine with a tokenized component library and template library + +- Primary docs/UI lives in `@stackoverflow/stacks-docs` under [`src/docs/public/email`](https://github.com/StackExchange/Stacks/tree/main/packages/stacks-docs/src/docs/public/email) or available on [stackoverflow.design/email](https://stackoverflow.design/email/). +- Full email preview gallery is available at [email.stackoverflow.design](https://email.stackoverflow.design). + +## Token placeholders + +Template authors should use the neutral placeholder syntax: + +- `[[FIRST_NAME]]` +- `[[CTA_URL]]` +- `[[UNSUBSCRIBE_URL]]` +- `[[COMPANY_NAME]]` + +During compilation, placeholders are transformed by target: + +- `preview` -> concrete example values +- `dotnet` -> Razor expressions +- `braze` -> Liquid expressions + +## Component render standard + +Component partials are compiled inside a shared MJML wrapper so they inherit global classes/styles from `mjml-config.ts`: + +```mjml + + + + + + + + +``` + +For component compiles, marker comments are injected via `mj-raw`: + +```mjml + + + + + +``` + +The pipeline extracts HTML between markers as `componentHtml` for copy/paste use, while keeping full `html` for previews. + +## Public API usage + +```ts +import { + getEmailCatalog, + compileEmailRenderable, +} from "@stackoverflow/stacks-email"; + +const catalog = getEmailCatalog(); + +const compiled = compileEmailRenderable({ + kind: "component", + slug: "button", + target: "preview", +}); + +// Full document (for iframe preview) +const fullHtml = compiled.html; + +// Extracted component fragment (for component copy/paste) +if (compiled.kind === "component") { + const fragmentHtml = compiled.componentHtml; +} +``` + +## Run local sandbox + +```bash +npm install +npm run dev -w @stackoverflow/stacks-email +``` + +## API auth (optional) + +`POST /api/compile` supports an optional shared Bearer token. + +- If `STACKS_EMAIL_AUTH_TOKEN` is not set: auth is disabled. +- If `STACKS_EMAIL_AUTH_TOKEN` is set: the request must include `Authorization: Bearer `. + +Example: + +```bash +curl -X POST http://localhost:5173/api/compile \ + -H "Authorization: Bearer $STACKS_EMAIL_AUTH_TOKEN" \ + -H "Content-Type: application/json" \ + --data '{"template":"transactional","target":"preview","blocks":[{"type":"headline"}]}' +``` diff --git a/packages/stacks-email/components/button.ts b/packages/stacks-email/components/button.ts new file mode 100644 index 0000000000..4af61d7b96 --- /dev/null +++ b/packages/stacks-email/components/button.ts @@ -0,0 +1,150 @@ +import type { EmailComponentMeta, MjmlNode } from "../types"; +import { z } from "zod/v4"; + +import { tokens } from "../tokens"; +import { Section } from "./section"; + +const buttonOptionsSchema = z.object({ + align: z.string().optional(), + className: z.string().optional(), + cssClass: z.string().optional(), + href: z.string().optional(), + text: z.string().optional(), +}); + +type ButtonOptions = z.input; + +const buttonOptionRows: NonNullable = [ + { + argument: "variant", + type: '"primary" | "secondary" | "inverted"', + description: "Selects the button style baseline.", + }, + { + argument: "options.align", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Maps to the MJML align attribute.", + }, + { + argument: "options.className", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Button mj-class override.", + }, + { + argument: "options.cssClass", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Raw CSS class applied to the button node.", + }, + { + argument: "options.href", + type: "string", + defaultValue: "[[BUTTON_URL]]", + defaultValueCode: true, + description: "Target link URL.", + }, + { + argument: "options.text", + type: "string", + defaultValue: "[[BUTTON_LABEL]]", + defaultValueCode: true, + description: "Button label content.", + }, +]; + +export const meta: EmailComponentMeta = { + slug: "button", + defaultVariant: "primary", + htmlExtraction: { + targetTag: "mj-button", + }, + variants: [ + { + id: "primary", + props: { + BUTTON_CLASS: "button", + BUTTON_HOVER_CLASS: "button-hover", + BUTTON_TEXT: "Filled button", + ALIGNMENT: "left", + }, + }, + { + id: "secondary", + props: { + BUTTON_CLASS: "button button__tonal", + BUTTON_HOVER_CLASS: "button-hover", + BUTTON_TEXT: "Tonal button", + ALIGNMENT: "left", + }, + }, + { + id: "inverted", + props: { + BUTTON_CLASS: "button button__inverted", + BUTTON_HOVER_CLASS: "button-hover-inverted", + BUTTON_TEXT: "Inverted button", + ALIGNMENT: "left", + }, + }, + ], + tokens: [ + { + token: "BUTTON_LABEL", + description: "The text displayed for the button.", + }, + { + token: "BUTTON_URL", + description: "Destination URL for the button.", + }, + ], + options: buttonOptionRows, +}; + +export type ButtonVariantId = "primary" | "secondary" | "inverted"; + +const getButtonVariantProps = (variant: ButtonVariantId) => + meta.variants.find((entry) => entry.id === variant)?.props ?? + meta.variants[0].props; + +export const Button = ( + variant: ButtonVariantId, + options: ButtonOptions = {} +): MjmlNode => { + const parsedOptions = buttonOptionsSchema.safeParse(options); + const normalizedOptions = parsedOptions.success ? parsedOptions.data : options; + const variantDefaults = getButtonVariantProps(variant); + + return { + tagName: "mj-button", + attributes: { + "mj-class": normalizedOptions.className ?? variantDefaults.BUTTON_CLASS, + "css-class": + normalizedOptions.cssClass ?? variantDefaults.BUTTON_HOVER_CLASS, + "href": normalizedOptions.href ?? "[[BUTTON_URL]]", + "align": normalizedOptions.align ?? variantDefaults.ALIGNMENT, + "padding": `0px ${tokens.layout.containerXPadding}`, + }, + content: normalizedOptions.text ?? "[[BUTTON_LABEL]]", + }; +}; + +export const source: MjmlNode[] = [ + Section([ + Button("primary", { + className: "{{BUTTON_CLASS}}", + cssClass: "{{BUTTON_HOVER_CLASS}}", + href: "[[BUTTON_URL]]", + align: "{{ALIGNMENT}}", + text: "[[BUTTON_LABEL]]", + }), + ]), +]; + +export const definition = { meta, source } as const; + +export default definition; diff --git a/packages/stacks-email/components/footer.ts b/packages/stacks-email/components/footer.ts new file mode 100644 index 0000000000..b055e03a83 --- /dev/null +++ b/packages/stacks-email/components/footer.ts @@ -0,0 +1,425 @@ +import { Header, type HeaderVariantId } from "./header"; +import type { EmailComponentMeta, MjmlNode } from "../types"; +import { z } from "zod/v4"; + +import { tokens } from "../tokens"; + +export type FooterVariantId = "default" | "reason" | "social"; + +const footerOptionsSchema = z.object({ + wrapperClass: z.string().optional(), + textClass: z.string().optional(), + linkClass: z.string().optional(), + headerVariant: z + .enum(["transactional", "brand", "brand-center", "inverted", "business"]) + .optional(), + logoSrc: z.string().optional(), + logoAlt: z.string().optional(), + logoUrl: z.string().optional(), + logoWidth: z.string().optional(), + unsubscribeUrl: z.string().optional(), + settingsUrl: z.string().optional(), + contactUrl: z.string().optional(), + privacyUrl: z.string().optional(), + addressHtml: z.string().optional(), + reasonText: z.string().optional(), + reasonPadding: z.string().optional(), + socialClass: z.string().optional(), + showSocialIcons: z.union([z.boolean(), z.string()]).optional(), + socialIconBasePath: z.string().optional(), +}); + +type FooterOptions = z.input; + +const footerOptionRows: NonNullable = [ + { + argument: "variant", + type: '"default" | "reason" | "social"', + description: "Selects reason/social behavior and baseline classes.", + }, + { + argument: "options.wrapperClass", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Wrapper mj-class on mj-wrapper.", + }, + { + argument: "options.textClass", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Body text class for footer copy rows.", + }, + { + argument: "options.linkClass", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Link class for unsubscribe/settings/contact/privacy links.", + }, + { + argument: "options.headerVariant", + type: "HeaderVariantId", + defaultValue: "inverted", + defaultValueCode: true, + description: "Variant passed through to nested Header.", + }, + { + argument: "options.unsubscribeUrl", + type: "string", + defaultValue: "[[UNSUBSCRIBE_URL]]", + defaultValueCode: true, + description: "Recipient-specific unsubscribe destination.", + }, + { + argument: "options.settingsUrl", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Email settings URL override.", + }, + { + argument: "options.contactUrl", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Contact URL override.", + }, + { + argument: "options.privacyUrl", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Privacy policy URL override.", + }, + { + argument: "options.reasonText", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Optional recipient-reason copy block.", + }, + { + argument: "options.showSocialIcons", + type: "boolean | string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Shows or hides social icon row.", + }, + { + argument: "options.socialIconBasePath", + type: "string", + defaultValue: "/email/social", + defaultValueCode: true, + description: "Base path for social icon image assets.", + }, + { + argument: "options.addressHtml", + type: "string", + defaultValue: "14 Wall Street, 20th Floor, New York, NY 10005", + defaultValueCode: true, + description: "Footer address line HTML/text.", + }, +]; + +export const meta: EmailComponentMeta = { + slug: "footer", + defaultVariant: "default", + variants: [ + { + id: "default", + props: { + FOOTER_WRAPPER_CLASS: "bg-invert", + FOOTER_TEXT_CLASS: "fc-text-footer", + FOOTER_LINK_CLASS: "footer-link fc-text-footer", + FOOTER_REASON_TEXT: "", + FOOTER_SOCIAL_ICONS: "false", + FOOTER_SOCIAL_CLASS: "footer-social-hidden", + FOOTER_SOCIAL_ICON_BASE_PATH: "/email/social", + SETTINGS_URL: + "https://stackoverflow.com/users/email/settings/current", + CONTACT_URL: "https://stackoverflow.com/company/contact", + PRIVACY_URL: "https://stackoverflow.com/legal/privacy-policy", + }, + }, + { + id: "reason", + props: { + FOOTER_WRAPPER_CLASS: "bg-invert", + FOOTER_TEXT_CLASS: "fc-text-footer", + FOOTER_LINK_CLASS: "footer-link fc-text-footer", + FOOTER_REASON_TEXT: + "You’re receiving this email because [[FOOTER_REASON]]", + FOOTER_SOCIAL_ICONS: "false", + FOOTER_SOCIAL_CLASS: "footer-social-hidden", + FOOTER_SOCIAL_ICON_BASE_PATH: "/email/social", + SETTINGS_URL: + "https://stackoverflow.com/users/email/settings/current", + CONTACT_URL: "https://stackoverflow.com/company/contact", + PRIVACY_URL: "https://stackoverflow.com/legal/privacy-policy", + }, + }, + { + id: "social", + props: { + FOOTER_WRAPPER_CLASS: "bg-invert", + FOOTER_TEXT_CLASS: "fc-text-footer", + FOOTER_LINK_CLASS: "footer-link fc-text-footer", + FOOTER_REASON_TEXT: + "You’re receiving this email because [[FOOTER_REASON]]", + FOOTER_SOCIAL_ICONS: "true", + FOOTER_SOCIAL_CLASS: "footer-social-visible", + FOOTER_SOCIAL_ICON_BASE_PATH: "/email/social", + SETTINGS_URL: + "https://stackoverflow.com/users/email/settings/current", + CONTACT_URL: "https://stackoverflow.com/company/contact", + PRIVACY_URL: "https://stackoverflow.com/legal/privacy-policy", + }, + }, + ], + tokens: [ + { + token: "UNSUBSCRIBE_URL", + description: "Recipient-specific unsubscribe destination", + }, + { + token: "FOOTER_REASON", + description: "Recipient-specific reason for receiving the email", + }, + ], + options: footerOptionRows, +}; + +const getFooterVariantProps = (variant: FooterVariantId) => + meta.variants.find((entry) => entry.id === variant)?.props ?? + meta.variants[0].props; + +export const Footer = ( + variant: FooterVariantId, + options: FooterOptions = {} +): MjmlNode => { + const parsedOptions = footerOptionsSchema.safeParse(options); + const normalizedOptions = parsedOptions.success ? parsedOptions.data : options; + const defaults = getFooterVariantProps(variant); + + const wrapperClass = + normalizedOptions.wrapperClass ?? defaults.FOOTER_WRAPPER_CLASS; + const textClass = normalizedOptions.textClass ?? defaults.FOOTER_TEXT_CLASS; + const linkClass = normalizedOptions.linkClass ?? defaults.FOOTER_LINK_CLASS; + + const unsubscribeUrl = normalizedOptions.unsubscribeUrl ?? "[[UNSUBSCRIBE_URL]]"; + const settingsUrl = normalizedOptions.settingsUrl ?? defaults.SETTINGS_URL; + const contactUrl = normalizedOptions.contactUrl ?? defaults.CONTACT_URL; + const privacyUrl = normalizedOptions.privacyUrl ?? defaults.PRIVACY_URL; + const reasonText = normalizedOptions.reasonText ?? defaults.FOOTER_REASON_TEXT; + const hasReasonText = reasonText.trim().length > 0; + const socialClass = normalizedOptions.socialClass ?? defaults.FOOTER_SOCIAL_CLASS; + const socialFlag = + normalizedOptions.showSocialIcons ?? defaults.FOOTER_SOCIAL_ICONS; + const showSocialIcons = String(socialFlag).trim().toLowerCase() === "true"; + const socialIconBasePath = + normalizedOptions.socialIconBasePath ?? + defaults.FOOTER_SOCIAL_ICON_BASE_PATH; + const addressHtml = + normalizedOptions.addressHtml ?? + "14 Wall Street, 20th Floor, New York, NY 10005"; + + const children: MjmlNode[] = [ + Header(normalizedOptions.headerVariant ?? "inverted", { + logoSrc: normalizedOptions.logoSrc, + logoAlt: normalizedOptions.logoAlt, + logoUrl: normalizedOptions.logoUrl, + logoWidth: normalizedOptions.logoWidth, + }), + ]; + + if (showSocialIcons) { + children.push({ + tagName: "mj-section", + attributes: { + "css-class": socialClass, + }, + children: [ + { + tagName: "mj-column", + children: [ + { + tagName: "mj-social", + attributes: { + align: "left", + "padding-top": "0px", + "padding-bottom": "0px", + "padding-left": "15px", + "icon-padding": "0 5px 0 5px", + "font-size": "13px", + "icon-size": "20px", + mode: "horizontal", + }, + children: [ + { + tagName: "mj-social-element", + attributes: { + src: `${socialIconBasePath}/linkedin.png`, + href: "https://linkedin.com/company/stack-overflow/", + }, + }, + { + tagName: "mj-social-element", + attributes: { + src: `${socialIconBasePath}/x.png`, + href: "https://x.com/stackoverflow/", + }, + }, + { + tagName: "mj-social-element", + attributes: { + src: `${socialIconBasePath}/threads.png`, + href: "https://www.threads.net/@thestackoverflow", + }, + }, + { + tagName: "mj-social-element", + attributes: { + src: `${socialIconBasePath}/instagram.png`, + href: "https://www.instagram.com/thestackoverflow/", + }, + }, + { + tagName: "mj-social-element", + attributes: { + src: `${socialIconBasePath}/youtube.png`, + href: "https://www.youtube.com/c/StackOverflowOfficial", + }, + }, + ], + }, + ], + }, + ], + }); + } + + children.push({ + tagName: "mj-section", + attributes: { + "padding-top": "40px", + "padding-bottom": "40px", + "padding-left": tokens.layout.containerXPadding, + "padding-right": tokens.layout.containerXPadding, + }, + children: [ + { + tagName: "mj-column", + children: [ + ...(hasReasonText + ? [ + { + tagName: "mj-text", + attributes: { + "font-size": "14px", + "mj-class": textClass, + "padding-bottom": "40px", + }, + content: reasonText, + } satisfies MjmlNode, + ] + : []), + { + tagName: "mj-text", + attributes: { + "font-size": "14px", + "padding-bottom": "8px", + }, + content: + `
Unsubscribe ` + + `Edit email settings ` + + `Contact us ` + + `Privacy `, + }, + { + tagName: "mj-text", + attributes: { + "font-size": "14px", + "mj-class": textClass, + }, + content: addressHtml, + }, + ], + }, + ], + }); + + children.push({ + tagName: "mj-section", + attributes: { + "padding-left": tokens.layout.containerXPadding, + "padding-right": tokens.layout.containerXPadding, + "padding-bottom": tokens.layout.containerYPadding, + }, + children: [ + { + tagName: "mj-group", + children: [ + { + tagName: "mj-column", + children: [ + { + tagName: "mj-text", + attributes: { + "font-size": "14px", + "mj-class": textClass, + }, + content: "© Stack Exchange Inc.", + }, + ], + }, + { + tagName: "mj-column", + children: [ + { + tagName: "mj-text", + attributes: { + "font-size": "14px", + "mj-class": textClass, + align: "right", + }, + content: "All rights reserved", + }, + ], + }, + ], + }, + ], + }); + + return { + tagName: "mj-wrapper", + attributes: { + "mj-class": wrapperClass, + "padding-top": tokens.layout.containerYPadding, + }, + children, + }; +}; + +export const source: MjmlNode[] = [ + Footer("default", { + wrapperClass: "{{FOOTER_WRAPPER_CLASS}}", + textClass: "{{FOOTER_TEXT_CLASS}}", + linkClass: "{{FOOTER_LINK_CLASS}}", + headerVariant: "inverted", + unsubscribeUrl: "[[UNSUBSCRIBE_URL]]", + settingsUrl: "{{SETTINGS_URL}}", + contactUrl: "{{CONTACT_URL}}", + privacyUrl: "{{PRIVACY_URL}}", + reasonText: "{{FOOTER_REASON_TEXT}}", + socialClass: "{{FOOTER_SOCIAL_CLASS}}", + showSocialIcons: true, + socialIconBasePath: "{{FOOTER_SOCIAL_ICON_BASE_PATH}}", + }), +]; + +export const definition = { meta, source } as const; + +export default definition; diff --git a/packages/stacks-email/components/graphic.ts b/packages/stacks-email/components/graphic.ts new file mode 100644 index 0000000000..a452151a8f --- /dev/null +++ b/packages/stacks-email/components/graphic.ts @@ -0,0 +1,220 @@ +import type { EmailComponentMeta, MjmlNode } from "../types"; +import { z } from "zod/v4"; + +import { tokens } from "../tokens"; +import { Section } from "./section"; + +export type GraphicVariantId = "spot" | "hero"; + +const graphicOptionsSchema = z.object({ + sectionClass: z.string().optional(), + imageSrc: z.string().optional(), + imageAlt: z.string().optional(), + imageWidth: z.string().optional(), + imageHeight: z.string().optional(), + imageAlign: z.string().optional(), + imagePaddingTop: z.string().optional(), + imagePaddingBottom: z.string().optional(), + imagePaddingLeft: z.string().optional(), + imagePaddingRight: z.string().optional(), +}); + +type GraphicOptions = z.input; + +const graphicVariantDefaults: Record< + GraphicVariantId, + { + GRAPHIC_SECTION_CLASS: string; + GRAPHIC_IMAGE_SRC: string; + GRAPHIC_IMAGE_ALT: string; + GRAPHIC_IMAGE_WIDTH: string; + GRAPHIC_IMAGE_HEIGHT: string; + GRAPHIC_IMAGE_ALIGN: string; + GRAPHIC_IMAGE_PADDING_TOP: string; + GRAPHIC_IMAGE_PADDING_BOTTOM: string; + GRAPHIC_IMAGE_PADDING_LEFT: string; + GRAPHIC_IMAGE_PADDING_RIGHT: string; + } +> = { + spot: { + GRAPHIC_SECTION_CLASS: "bg-block", + GRAPHIC_IMAGE_SRC: "/email/spots/SpotLock.png", + GRAPHIC_IMAGE_ALT: "Spot placeholder image", + GRAPHIC_IMAGE_WIDTH: "140px", + GRAPHIC_IMAGE_HEIGHT: "140px", + GRAPHIC_IMAGE_ALIGN: "left", + GRAPHIC_IMAGE_PADDING_TOP: "0px", + GRAPHIC_IMAGE_PADDING_BOTTOM: "0px", + GRAPHIC_IMAGE_PADDING_LEFT: tokens.layout.containerXPadding, + GRAPHIC_IMAGE_PADDING_RIGHT: tokens.layout.containerXPadding, + }, + hero: { + GRAPHIC_SECTION_CLASS: "bg-block", + GRAPHIC_IMAGE_SRC: "/email/hero/1200x630.png", + GRAPHIC_IMAGE_ALT: "Hero placeholder image", + GRAPHIC_IMAGE_WIDTH: "1200px", + GRAPHIC_IMAGE_HEIGHT: "auto", + GRAPHIC_IMAGE_ALIGN: "center", + GRAPHIC_IMAGE_PADDING_TOP: "0px", + GRAPHIC_IMAGE_PADDING_BOTTOM: "0px", + GRAPHIC_IMAGE_PADDING_LEFT: tokens.layout.containerXPadding, + GRAPHIC_IMAGE_PADDING_RIGHT: tokens.layout.containerXPadding, + }, +}; + +const graphicOptionRows: NonNullable = [ + { + argument: "variant", + type: '"spot" | "hero"', + description: "Selects baseline size, alignment, and source asset.", + }, + { + argument: "options.sectionClass", + type: "string", + defaultValue: "bg-block", + defaultValueCode: true, + description: "Section class applied to the wrapper section.", + }, + { + argument: "options.imageSrc", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Image URL/path override.", + }, + { + argument: "options.imageAlt", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Accessible image alt text.", + }, + { + argument: "options.imageWidth", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: 'Rendered width (for example "140px").', + }, + { + argument: "options.imageHeight", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: 'Rendered height (for example "630px").', + }, + { + argument: "options.imageAlign", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "MJML image alignment value.", + }, + { + argument: "options.imagePaddingTop", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Top padding override.", + }, + { + argument: "options.imagePaddingBottom", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Bottom padding override.", + }, + { + argument: "options.imagePaddingLeft", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Left padding override.", + }, + { + argument: "options.imagePaddingRight", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Right padding override.", + }, +]; + +export const meta: EmailComponentMeta = { + slug: "graphic", + defaultVariant: "spot", + variants: [ + { + id: "spot", + props: graphicVariantDefaults.spot, + }, + { + id: "hero", + props: graphicVariantDefaults.hero, + }, + ], + tokens: [], + options: graphicOptionRows, +}; + +const getGraphicVariantProps = (variant: GraphicVariantId) => + meta.variants.find((entry) => entry.id === variant)?.props ?? + meta.variants[0].props; + +export const Graphic = ( + variant: GraphicVariantId, + options: GraphicOptions = {} +): MjmlNode => { + const parsedOptions = graphicOptionsSchema.safeParse(options); + const normalizedOptions = parsedOptions.success ? parsedOptions.data : options; + const defaults = getGraphicVariantProps(variant); + + return Section( + [ + { + tagName: "mj-image", + attributes: { + src: normalizedOptions.imageSrc ?? defaults.GRAPHIC_IMAGE_SRC, + alt: normalizedOptions.imageAlt ?? defaults.GRAPHIC_IMAGE_ALT, + width: normalizedOptions.imageWidth ?? defaults.GRAPHIC_IMAGE_WIDTH, + height: normalizedOptions.imageHeight ?? defaults.GRAPHIC_IMAGE_HEIGHT, + align: normalizedOptions.imageAlign ?? defaults.GRAPHIC_IMAGE_ALIGN, + "padding-top": + normalizedOptions.imagePaddingTop ?? + defaults.GRAPHIC_IMAGE_PADDING_TOP, + "padding-bottom": + normalizedOptions.imagePaddingBottom ?? + defaults.GRAPHIC_IMAGE_PADDING_BOTTOM, + "padding-left": + normalizedOptions.imagePaddingLeft ?? + defaults.GRAPHIC_IMAGE_PADDING_LEFT, + "padding-right": + normalizedOptions.imagePaddingRight ?? + defaults.GRAPHIC_IMAGE_PADDING_RIGHT, + }, + }, + ], + { + sectionClass: normalizedOptions.sectionClass ?? defaults.GRAPHIC_SECTION_CLASS, + } + ); +}; + +export const source: MjmlNode[] = [ + Graphic("spot", { + sectionClass: "{{GRAPHIC_SECTION_CLASS}}", + imageSrc: "{{GRAPHIC_IMAGE_SRC}}", + imageAlt: "{{GRAPHIC_IMAGE_ALT}}", + imageWidth: "{{GRAPHIC_IMAGE_WIDTH}}", + imageHeight: "{{GRAPHIC_IMAGE_HEIGHT}}", + imageAlign: "{{GRAPHIC_IMAGE_ALIGN}}", + imagePaddingTop: "{{GRAPHIC_IMAGE_PADDING_TOP}}", + imagePaddingBottom: "{{GRAPHIC_IMAGE_PADDING_BOTTOM}}", + imagePaddingLeft: "{{GRAPHIC_IMAGE_PADDING_LEFT}}", + imagePaddingRight: "{{GRAPHIC_IMAGE_PADDING_RIGHT}}", + }), +]; + +export const definition = { meta, source } as const; + +export default definition; diff --git a/packages/stacks-email/components/header.ts b/packages/stacks-email/components/header.ts new file mode 100644 index 0000000000..0bb55ef1ed --- /dev/null +++ b/packages/stacks-email/components/header.ts @@ -0,0 +1,196 @@ +import type { EmailComponentMeta, MjmlNode } from "../types"; +import { z } from "zod/v4"; + +export type HeaderVariantId = "transactional" | "brand" | "brand-center" | "inverted" | "business"; + +const headerOptionsSchema = z.object({ + sectionClass: z.string().optional(), + logoSrc: z.string().optional(), + logoAlt: z.string().optional(), + logoUrl: z.string().optional(), + logoWidth: z.string().optional(), + logoAlign: z.string().optional(), +}); + +type HeaderOptions = z.input; + +type HeaderVariantProps = { + HEADER_SECTION_CLASS: string; + HEADER_LOGO_SRC: string; + HEADER_LOGO_ALT: string; + HEADER_LOGO_URL: string; + HEADER_LOGO_WIDTH: string; + HEADER_LOGO_ALIGN: string; +}; + +const sharedVariantProps: Omit = { + HEADER_LOGO_SRC: "/email/stack-overflow-logo.png", + HEADER_LOGO_ALT: "Stack Overflow", + HEADER_LOGO_URL: "https://stackoverflow.com/", + HEADER_LOGO_WIDTH: "158px", + HEADER_LOGO_ALIGN: "left", +}; + +const headerVariantDefaults: Record = { + transactional: { + ...sharedVariantProps, + HEADER_SECTION_CLASS: "bg-block", + }, + brand: { + ...sharedVariantProps, + HEADER_SECTION_CLASS: "bg-brand", + }, + "brand-center": { + ...sharedVariantProps, + HEADER_SECTION_CLASS: "bg-brand", + HEADER_LOGO_ALIGN: "center", + }, + inverted: { + ...sharedVariantProps, + HEADER_SECTION_CLASS: "bg-invert", + HEADER_LOGO_SRC: "/email/stack-overflow-logo-off-white.png", + }, + business: { + ...sharedVariantProps, + HEADER_SECTION_CLASS: "bg-invert", + HEADER_LOGO_SRC: "/email/stack-overflow-business-logo.png", + HEADER_LOGO_URL: "https://stackoverflow.co/", + }, +}; + +const headerOptionRows: NonNullable = [ + { + argument: "variant", + type: '"transactional" | "brand" | "brand-center" | "inverted" | "business"', + description: "Selects section background and logo defaults.", + }, + { + argument: "options.sectionClass", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Header section mj-class override.", + }, + { + argument: "options.logoSrc", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Logo image source path/URL override.", + }, + { + argument: "options.logoAlt", + type: "string", + defaultValue: "Stack Overflow", + defaultValueCode: true, + description: "Logo alt text.", + }, + { + argument: "options.logoUrl", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Logo link destination.", + }, + { + argument: "options.logoWidth", + type: "string", + defaultValue: "158px", + defaultValueCode: true, + description: "Logo width override.", + }, + { + argument: "options.logoAlign", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "MJML image alignment override.", + }, +]; + +export const Header = ( + variant: HeaderVariantId, + options: HeaderOptions = {} +): MjmlNode => { + const parsedOptions = headerOptionsSchema.safeParse(options); + const normalizedOptions = parsedOptions.success ? parsedOptions.data : options; + const variantDefaults = headerVariantDefaults[variant]; + + return { + tagName: "mj-section", + attributes: { + "mj-class": + normalizedOptions.sectionClass ?? variantDefaults.HEADER_SECTION_CLASS, + "padding-top": "20px", + "padding-bottom": "20px", + "padding-left": "24px", + "padding-right": "24px", + }, + children: [ + { + tagName: "mj-column", + children: [ + { + tagName: "mj-image", + attributes: { + src: normalizedOptions.logoSrc ?? variantDefaults.HEADER_LOGO_SRC, + alt: normalizedOptions.logoAlt ?? variantDefaults.HEADER_LOGO_ALT, + width: + normalizedOptions.logoWidth ?? + variantDefaults.HEADER_LOGO_WIDTH, + href: normalizedOptions.logoUrl ?? variantDefaults.HEADER_LOGO_URL, + align: normalizedOptions.logoAlign ?? variantDefaults.HEADER_LOGO_ALIGN, + padding: "0px", + }, + }, + ], + }, + ], + }; +}; + +export const meta: EmailComponentMeta = { + slug: "header", + defaultVariant: "transactional", + variants: [ + { + id: "transactional", + props: headerVariantDefaults.transactional, + }, + { + id: "brand", + props: headerVariantDefaults.brand, + }, + { + id: "brand-center", + props: headerVariantDefaults["brand-center"], + }, + { + id: "inverted", + props: headerVariantDefaults.inverted, + }, + { + id: "business", + props: headerVariantDefaults.business, + }, + ], + tokens: [], + options: headerOptionRows, +}; + +export const source: MjmlNode[] = [ + { + ...Header("transactional", { + sectionClass: "{{HEADER_SECTION_CLASS}}", + logoSrc: "{{HEADER_LOGO_SRC}}", + logoAlt: "{{HEADER_LOGO_ALT}}", + logoWidth: "{{HEADER_LOGO_WIDTH}}", + logoUrl: "{{HEADER_LOGO_URL}}", + logoAlign: "{{HEADER_LOGO_ALIGN}}", + }), + }, +]; + +export const definition = { meta, source } as const; + +export default definition; diff --git a/packages/stacks-email/components/headline.ts b/packages/stacks-email/components/headline.ts new file mode 100644 index 0000000000..e75e356c33 --- /dev/null +++ b/packages/stacks-email/components/headline.ts @@ -0,0 +1,192 @@ +import type { EmailComponentMeta, MjmlNode } from "../types"; +import { z } from "zod/v4"; + +import { tokens } from "../tokens"; +import { Section } from "./section"; + +export type HeadlineVariantId = "default" | "highlight"; + +const headlineOptionsSchema = z.object({ + sectionClass: z.string().optional(), + textClass: z.string().optional(), + textAlign: z.string().optional(), + textContent: z.string().optional(), + textHighlight: z.union([z.boolean(), z.string()]).optional(), + textHighlightStart: z.string().optional(), + textHighlightEnd: z.string().optional(), +}); + +type HeadlineOptions = z.input; + +const headlineOptionRows: NonNullable = [ + { + argument: "variant", + type: '"default" | "highlight"', + description: "Selects baseline highlight wrapper behavior.", + }, + { + argument: "options.sectionClass", + type: "string", + defaultValue: "bg-block", + defaultValueCode: true, + description: "Section class for the headline row.", + }, + { + argument: "options.textClass", + type: "string", + defaultValue: "s-email-text-headline", + defaultValueCode: true, + description: "Text styling class for the headline node.", + }, + { + argument: "options.textAlign", + type: "string", + defaultValue: "left", + defaultValueCode: true, + description: "MJML text alignment.", + }, + { + argument: "options.textContent", + type: "string", + defaultValue: "Please verify your email address", + defaultValueCode: true, + description: "Headline copy content.", + }, + { + argument: "options.textHighlight", + type: "boolean | string", + defaultValue: "Variant behavior when omitted", + defaultValueCode: false, + description: + "When true, forces inline highlighted output; when false, disables highlighting.", + }, + { + argument: "options.textHighlightStart", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Optional opening wrapper around headline content.", + }, + { + argument: "options.textHighlightEnd", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Optional closing wrapper around headline content.", + }, +]; + +const headlineVariantDefaults: Record< + HeadlineVariantId, + { + HEADLINE_SECTION_CLASS: string; + HEADLINE_TEXT_CLASS: string; + HEADLINE_TEXT_ALIGN: string; + HEADLINE_TEXT_CONTENT: string; + HEADLINE_TEXT_HIGHLIGHT_START: string; + HEADLINE_TEXT_HIGHLIGHT_END: string; + } +> = { + default: { + HEADLINE_SECTION_CLASS: "bg-block", + HEADLINE_TEXT_CLASS: "s-email-text-headline", + HEADLINE_TEXT_ALIGN: "left", + HEADLINE_TEXT_CONTENT: "Please verify your email address", + HEADLINE_TEXT_HIGHLIGHT_START: "", + HEADLINE_TEXT_HIGHLIGHT_END: "", + }, + highlight: { + HEADLINE_SECTION_CLASS: "bg-block", + HEADLINE_TEXT_CLASS: "s-email-text-headline", + HEADLINE_TEXT_ALIGN: "left", + HEADLINE_TEXT_CONTENT: "Please verify your email address", + HEADLINE_TEXT_HIGHLIGHT_START: ``, + HEADLINE_TEXT_HIGHLIGHT_END: "", + }, +}; + +const getHeadlineVariantProps = (variant: HeadlineVariantId) => + headlineVariantDefaults[variant] ?? headlineVariantDefaults.default; + +const withHighlightedText = (text: string) => + `${text}`; + +export const Headline = ( + variant: HeadlineVariantId, + options: HeadlineOptions = {} +): MjmlNode => { + const parsedOptions = headlineOptionsSchema.safeParse(options); + const normalizedOptions = parsedOptions.success ? parsedOptions.data : options; + const defaults = getHeadlineVariantProps(variant); + const textContent = + normalizedOptions.textContent ?? defaults.HEADLINE_TEXT_CONTENT; + const textHighlightStart = + normalizedOptions.textHighlightStart ?? + defaults.HEADLINE_TEXT_HIGHLIGHT_START; + const textHighlightEnd = + normalizedOptions.textHighlightEnd ?? defaults.HEADLINE_TEXT_HIGHLIGHT_END; + const textHighlightFlag = normalizedOptions.textHighlight; + const textHighlight = + textHighlightFlag === undefined + ? null + : String(textHighlightFlag).trim().toLowerCase() === "true"; + const renderedTextContent = + textHighlight === null + ? `${textHighlightStart}${textContent}${textHighlightEnd}` + : textHighlight + ? withHighlightedText(textContent) + : textContent; + + return Section( + [ + { + tagName: "mj-text", + attributes: { + "mj-class": + normalizedOptions.textClass ?? defaults.HEADLINE_TEXT_CLASS, + align: normalizedOptions.textAlign ?? defaults.HEADLINE_TEXT_ALIGN, + "padding-top": tokens.layout.containerYPadding, + "padding-bottom": tokens.layout.containerYPadding, + "padding-left": tokens.layout.containerXPadding, + "padding-right": tokens.layout.containerXPadding, + }, + content: renderedTextContent, + }, + ], + { + sectionClass: + normalizedOptions.sectionClass ?? defaults.HEADLINE_SECTION_CLASS, + } + ); +}; + +export const meta: EmailComponentMeta = { + slug: "headline", + defaultVariant: "default", + variants: [ + { + id: "default", + props: headlineVariantDefaults.default, + }, + { + id: "highlight", + props: headlineVariantDefaults.highlight, + }, + ], + tokens: [], + options: headlineOptionRows, +}; + +export const source: MjmlNode[] = [ + Headline("default", { + sectionClass: "{{HEADLINE_SECTION_CLASS}}", + textClass: "{{HEADLINE_TEXT_CLASS}}", + textAlign: "{{HEADLINE_TEXT_ALIGN}}", + textContent: "{{HEADLINE_TEXT_CONTENT}}", + textHighlightStart: "{{HEADLINE_TEXT_HIGHLIGHT_START}}", + textHighlightEnd: "{{HEADLINE_TEXT_HIGHLIGHT_END}}", + }), +]; + +export const definition = { meta, source } as const; +export default definition; diff --git a/packages/stacks-email/components/index.ts b/packages/stacks-email/components/index.ts new file mode 100644 index 0000000000..63aaf00dc0 --- /dev/null +++ b/packages/stacks-email/components/index.ts @@ -0,0 +1,11 @@ +export { default as button } from "./button"; +export { default as footer } from "./footer"; +export { default as graphic } from "./graphic"; +export { default as headline } from "./headline"; +export { default as header } from "./header"; +export { default as preview } from "./preview"; +export { default as spacers } from "./spacers"; +export { default as text } from "./text"; +export { default as title } from "./title"; +export { Section } from "./section"; +export { Spacer } from "./spacer"; diff --git a/packages/stacks-email/components/preview.ts b/packages/stacks-email/components/preview.ts new file mode 100644 index 0000000000..263c61317d --- /dev/null +++ b/packages/stacks-email/components/preview.ts @@ -0,0 +1,85 @@ +import type { EmailComponentMeta, MjmlNode } from "../types"; +import { z } from "zod/v4"; + +export type PreviewVariantId = "default"; + +const previewOptionsSchema = z.object({ + textContent: z.string().optional(), +}); + +type PreviewOptions = z.input; + +const previewOptionRows: NonNullable = [ + { + argument: "variant", + type: '"default"', + description: "Baseline preview text output.", + }, + { + argument: "options.textContent", + type: "string", + defaultValue: "[[PREVIEW_TEXT]]", + defaultValueCode: true, + description: + "Hidden inbox preview snippet shown by supporting email clients.", + }, +]; + +const previewVariantDefaults: Record< + PreviewVariantId, + { + PREVIEW_TEXT_CONTENT: string; + } +> = { + default: { + PREVIEW_TEXT_CONTENT: "[[PREVIEW_TEXT]]", + }, +}; + +const getPreviewVariantProps = (variant: PreviewVariantId) => + previewVariantDefaults[variant] ?? previewVariantDefaults.default; + +export const Preview = ( + variant: PreviewVariantId = "default", + options: PreviewOptions = {} +): MjmlNode => { + const parsedOptions = previewOptionsSchema.safeParse(options); + const normalizedOptions = parsedOptions.success ? parsedOptions.data : options; + const defaults = getPreviewVariantProps(variant); + + return { + tagName: "mj-preview", + content: normalizedOptions.textContent ?? defaults.PREVIEW_TEXT_CONTENT, + }; +}; + +export const meta: EmailComponentMeta = { + slug: "preview", + defaultVariant: "default", + htmlExtraction: { + targetTag: "mj-preview", + }, + variants: [ + { + id: "default", + props: previewVariantDefaults.default, + }, + ], + tokens: [ + { + token: "PREVIEW_TEXT", + description: "Inbox preview snippet shown next to subject lines.", + }, + ], + options: previewOptionRows, +}; + +export const source: MjmlNode[] = [ + Preview("default", { + textContent: "{{PREVIEW_TEXT_CONTENT}}", + }), +]; + +export const definition = { meta, source } as const; + +export default definition; diff --git a/packages/stacks-email/components/section.ts b/packages/stacks-email/components/section.ts new file mode 100644 index 0000000000..30ca234500 --- /dev/null +++ b/packages/stacks-email/components/section.ts @@ -0,0 +1,51 @@ +import type { MjmlNode } from "../types"; +import { z } from "zod/v4"; + +const mjmlAttributeSchema = z.union([z.string(), z.number(), z.boolean()]); + +const sectionOptionsSchema = z.object({ + sectionClass: z.string().optional(), + sectionAttributes: z.record(z.string(), mjmlAttributeSchema).optional(), + columnClass: z.string().optional(), + columnAttributes: z.record(z.string(), mjmlAttributeSchema).optional(), +}); + +type SectionOptions = z.input; + +export const Section = ( + children: MjmlNode[], + options: SectionOptions = {} +): MjmlNode => { + const parsedOptions = sectionOptionsSchema.safeParse(options); + const normalizedOptions = parsedOptions.success ? parsedOptions.data : options; + + const sectionAttributes: NonNullable = { + ...(normalizedOptions.sectionClass + ? { "mj-class": normalizedOptions.sectionClass } + : {}), + ...(normalizedOptions.sectionAttributes ?? {}), + }; + + const columnAttributes: NonNullable = { + ...(normalizedOptions.columnClass + ? { "mj-class": normalizedOptions.columnClass } + : {}), + ...(normalizedOptions.columnAttributes ?? {}), + }; + + return { + tagName: "mj-section", + attributes: Object.keys(sectionAttributes).length + ? sectionAttributes + : undefined, + children: [ + { + tagName: "mj-column", + attributes: Object.keys(columnAttributes).length + ? columnAttributes + : undefined, + children, + }, + ], + }; +}; diff --git a/packages/stacks-email/components/spacer.ts b/packages/stacks-email/components/spacer.ts new file mode 100644 index 0000000000..a4bfa647dc --- /dev/null +++ b/packages/stacks-email/components/spacer.ts @@ -0,0 +1,39 @@ +import type { MjmlNode } from "../types"; +import { z } from "zod/v4"; +import { Section } from "./section"; + +const spacerHeights = { + medium: "20px", + large: "40px", +} as const; + +export type SpacerSize = keyof typeof spacerHeights; + +const spacerOptionsSchema = z.object({ + sectionClass: z.string().optional(), + height: z.string().optional(), +}); + +type SpacerOptions = z.input; + +export const Spacer = ( + size: SpacerSize = "medium", + options: SpacerOptions = {} +): MjmlNode => { + const parsedOptions = spacerOptionsSchema.safeParse(options); + const normalizedOptions = parsedOptions.success ? parsedOptions.data : options; + + return Section( + [ + { + tagName: "mj-spacer", + attributes: { + height: normalizedOptions.height ?? spacerHeights[size], + }, + }, + ], + { + sectionClass: normalizedOptions.sectionClass ?? "bg-block", + } + ); +}; diff --git a/packages/stacks-email/components/spacers.ts b/packages/stacks-email/components/spacers.ts new file mode 100644 index 0000000000..311463b0bb --- /dev/null +++ b/packages/stacks-email/components/spacers.ts @@ -0,0 +1,76 @@ +import type { EmailComponentMeta, MjmlNode } from "../types"; + +import { Spacer } from "./spacer"; + +export type SpacersVariantId = "medium" | "large"; + +type SpacerVariantDefaults = { + SPACER_SECTION_CLASS: string; + SPACER_HEIGHT: string; +}; + +const spacerVariantDefaults: Record = { + medium: { + SPACER_SECTION_CLASS: "bg-block", + SPACER_HEIGHT: "20px", + }, + large: { + SPACER_SECTION_CLASS: "bg-block", + SPACER_HEIGHT: "40px", + }, +}; + +const spacerOptionRows: NonNullable = [ + { + argument: "size", + type: '"medium" | "large"', + defaultValue: "medium", + defaultValueCode: true, + description: "Preset height token applied to the underlying mj-spacer.", + }, + { + argument: "options.sectionClass", + type: "string", + defaultValue: "bg-block", + defaultValueCode: true, + description: "Applied to the wrapper section (mj-class).", + }, + { + argument: "options.height", + type: "string", + defaultValue: "From size preset", + defaultValueCode: false, + description: 'Explicit spacer height override, for example "64px".', + }, +]; + +export const meta: EmailComponentMeta = { + slug: "spacers", + defaultVariant: "medium", + htmlExtraction: { + targetTag: "mj-spacer", + }, + variants: [ + { + id: "medium", + props: spacerVariantDefaults.medium, + }, + { + id: "large", + props: spacerVariantDefaults.large, + }, + ], + tokens: [], + options: spacerOptionRows, +}; + +export const source: MjmlNode[] = [ + Spacer("medium", { + sectionClass: "{{SPACER_SECTION_CLASS}}", + height: "{{SPACER_HEIGHT}}", + }), +]; + +export const definition = { meta, source } as const; + +export default definition; diff --git a/packages/stacks-email/components/text.ts b/packages/stacks-email/components/text.ts new file mode 100644 index 0000000000..5c1c99ec6f --- /dev/null +++ b/packages/stacks-email/components/text.ts @@ -0,0 +1,210 @@ +import type { EmailComponentMeta, MjmlNode } from "../types"; +import MarkdownIt from "markdown-it"; +import { z } from "zod/v4"; + +import { tokens } from "../tokens"; +import { Section } from "./section"; + +export type TextVariantId = "body" | "centered"; + +const textOptionsSchema = z.object({ + columnClass: z.string().optional(), + sectionClass: z.string().optional(), + textAlign: z.string().optional(), + textClass: z.string().optional(), + textContent: z.string().optional(), +}); + +type TextOptions = z.input; + +const BODY_PARAGRAPH_MARGIN = "0 0 16px"; +const TEMPLATE_PROP_PATTERN = /^\{\{[A-Z0-9_]+\}\}$/; + +const markdown = new MarkdownIt({ + html: false, + breaks: true, + linkify: true, + typographer: true, +}); + +markdown.renderer.rules.link_open = (tokenList, index, options, env, self) => { + tokenList[index].attrJoin("class", "link"); + return self.renderToken(tokenList, index, options); +}; + +markdown.renderer.rules.paragraph_open = ( + tokenList, + index, + options, + env, + self +) => { + const hasAnotherParagraph = tokenList + .slice(index + 1) + .some((token) => token.type === "paragraph_open"); + + tokenList[index].attrSet( + "style", + `margin:${hasAnotherParagraph ? BODY_PARAGRAPH_MARGIN : "0"};` + ); + + return self.renderToken(tokenList, index, options); +}; + +const renderMarkdown = (value: string) => markdown.render(value.trim()).trim(); + +const looksLikeHtml = (value: string) => /<\/?[a-z][\s\S]*>/i.test(value); + +const renderTextContent = (value: string | undefined) => { + const content = value?.trim() ?? ""; + if (!content) { + return ""; + } + + if (TEMPLATE_PROP_PATTERN.test(content)) { + return content; + } + + if (looksLikeHtml(content)) { + return content; + } + + return renderMarkdown(content); +}; + +const textOptionRows: NonNullable = [ + { + argument: "variant", + type: '"body" | "centered"', + description: "Selects layout and alignment defaults.", + }, + { + argument: "options.columnClass", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Column mj-class override.", + }, + { + argument: "options.sectionClass", + type: "string", + description: "Optional section mj-class override.", + }, + { + argument: "options.textAlign", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "MJML text alignment override.", + }, + { + argument: "options.textClass", + type: "string", + defaultValue: "s-email-text-body", + defaultValueCode: true, + description: "Text mj-class override.", + }, + { + argument: "options.textContent", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Raw content string before markdown/HTML handling.", + }, +]; + +export const meta: EmailComponentMeta = { + slug: "text", + defaultVariant: "body", + variants: [ + { + id: "body", + props: { + TEXT_COLUMN_CLASS: "bg-block", + TEXT_CLASS: "s-email-text-body", + TEXT_ALIGN: "left", + TEXT_CONTENT: renderTextContent( + ` +Dear [[FIRST_NAME]], + +The entire [software development lifecycle](https://stackoverflow.com) has been dramatically changed by AI, introducing a new model for team organization and leadership. + +AI has accelerated coding, allowing developers to dedicate more time to complex and creative tasks. **Simultaneously**, it enables teams to clear bottlenecks of repetitive tasks [through automation](https://stackoverflow.com), allowing leaders to create more agile teams and focus on higher-level strategic problems. + +Ultimately, it is really AI’s ability to automate the __"work around the work"__ that is proving to be transformative for organizations. + ` + ), + }, + }, + { + id: "centered", + props: { + TEXT_COLUMN_CLASS: "bg-block", + TEXT_CLASS: "s-email-text-body", + TEXT_CONTENT: renderTextContent( + ` + A starting point for more simple transactional emails with a single, center-aligned message. It can [contain links](https://stackoverflow.com) or **rich text**. + ` + ), + TEXT_ALIGN: "center", + }, + }, + ], + tokens: [ + { + token: "FIRST_NAME", + description: "Recipient first name for personalized body copy.", + }, + ], + options: textOptionRows, +}; + +const getTextVariantProps = (variant: TextVariantId) => + meta.variants.find((entry) => entry.id === variant)?.props ?? + meta.variants[0].props; + +export const Text = ( + variant: TextVariantId, + options: TextOptions = {} +): MjmlNode => { + const parsedOptions = textOptionsSchema.safeParse(options); + const normalizedOptions = parsedOptions.success ? parsedOptions.data : options; + const defaults = getTextVariantProps(variant); + + return Section( + [ + { + tagName: "mj-text", + attributes: { + "mj-class": normalizedOptions.textClass ?? defaults.TEXT_CLASS, + align: normalizedOptions.textAlign ?? defaults.TEXT_ALIGN, + "padding-top": tokens.layout.containerYPadding, + "padding-bottom": tokens.layout.containerYPadding, + "padding-left": tokens.layout.containerXPadding, + "padding-right": tokens.layout.containerXPadding, + }, + content: renderTextContent( + normalizedOptions.textContent ?? defaults.TEXT_CONTENT + ), + }, + ], + { + sectionClass: normalizedOptions.sectionClass, + columnClass: + normalizedOptions.columnClass ?? defaults.TEXT_COLUMN_CLASS, + } + ); +}; + +export const source: MjmlNode[] = [ + Text("body", { + columnClass: "{{TEXT_COLUMN_CLASS}}", + textClass: "{{TEXT_CLASS}}", + textAlign: "{{TEXT_ALIGN}}", + textContent: "{{TEXT_CONTENT}}", + }), +]; + +export const definition = { meta, source } as const; + +export default definition; diff --git a/packages/stacks-email/components/title.ts b/packages/stacks-email/components/title.ts new file mode 100644 index 0000000000..f0fe326a74 --- /dev/null +++ b/packages/stacks-email/components/title.ts @@ -0,0 +1,143 @@ +import type { EmailComponentMeta, MjmlNode } from "../types"; +import { z } from "zod/v4"; + +import { tokens } from "../tokens"; +import { Section } from "./section"; + +export type TitleVariantId = "default" | "invert"; + +const titleOptionsSchema = z.object({ + sectionClass: z.string().optional(), + textClass: z.string().optional(), + textAlign: z.string().optional(), + textContent: z.string().optional(), +}); + +type TitleOptions = z.input; + +const titleVariantDefaults: Record< + TitleVariantId, + { + TITLE_SECTION_CLASS: string; + TITLE_TEXT_CLASS: string; + TITLE_TEXT_ALIGN: string; + TITLE_TEXT_CONTENT: string; + } +> = { + default: { + TITLE_SECTION_CLASS: "bg-block", + TITLE_TEXT_CLASS: "s-email-text-title", + TITLE_TEXT_ALIGN: "left", + TITLE_TEXT_CONTENT: "Featured", + }, + invert: { + TITLE_SECTION_CLASS: "bg-invert", + TITLE_TEXT_CLASS: "s-email-text-title fc-text-invert", + TITLE_TEXT_ALIGN: "left", + TITLE_TEXT_CONTENT: "Featured", + }, +}; + +const titleOptionRows: NonNullable = [ + { + argument: "variant", + type: '"default" | "invert"', + description: "Selects baseline section and text styling.", + }, + { + argument: "options.sectionClass", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Section mj-class override.", + }, + { + argument: "options.textClass", + type: "string", + defaultValue: "From selected variant", + defaultValueCode: false, + description: "Title text mj-class override.", + }, + { + argument: "options.textAlign", + type: "string", + defaultValue: "left", + defaultValueCode: true, + description: "MJML text alignment override.", + }, + { + argument: "options.textContent", + type: "string", + defaultValue: "Featured", + defaultValueCode: true, + description: "Title copy content.", + }, +]; + +const getTitleVariantProps = (variant: TitleVariantId) => + titleVariantDefaults[variant] ?? titleVariantDefaults.default; + +export const Title = ( + variant: TitleVariantId, + options: TitleOptions = {} +): MjmlNode => { + const parsedOptions = titleOptionsSchema.safeParse(options); + const normalizedOptions = parsedOptions.success ? parsedOptions.data : options; + + return Section( + [ + { + tagName: "mj-text", + attributes: { + "mj-class": + normalizedOptions.textClass ?? + getTitleVariantProps(variant).TITLE_TEXT_CLASS, + align: + normalizedOptions.textAlign ?? + getTitleVariantProps(variant).TITLE_TEXT_ALIGN, + "padding-top": tokens.layout.containerYPadding, + "padding-bottom": tokens.layout.containerYPadding, + "padding-left": tokens.layout.containerXPadding, + "padding-right": tokens.layout.containerXPadding, + }, + content: + normalizedOptions.textContent ?? + getTitleVariantProps(variant).TITLE_TEXT_CONTENT, + }, + ], + { + sectionClass: + normalizedOptions.sectionClass ?? + getTitleVariantProps(variant).TITLE_SECTION_CLASS, + } + ); +}; + +export const meta: EmailComponentMeta = { + slug: "title", + defaultVariant: "default", + variants: [ + { + id: "default", + props: titleVariantDefaults.default, + }, + { + id: "invert", + props: titleVariantDefaults.invert, + }, + ], + tokens: [], + options: titleOptionRows, +}; + +export const source: MjmlNode[] = [ + Title("default", { + sectionClass: "{{TITLE_SECTION_CLASS}}", + textClass: "{{TITLE_TEXT_CLASS}}", + textAlign: "{{TITLE_TEXT_ALIGN}}", + textContent: "{{TITLE_TEXT_CONTENT}}", + }), +]; + +export const definition = { meta, source } as const; +export default definition; diff --git a/packages/stacks-email/eslint.config.js b/packages/stacks-email/eslint.config.js new file mode 100644 index 0000000000..f60aa92137 --- /dev/null +++ b/packages/stacks-email/eslint.config.js @@ -0,0 +1,29 @@ +import js from "@eslint/js"; +import globals from "globals"; +import svelte from "eslint-plugin-svelte"; +import tseslint from "typescript-eslint"; +import prettier from "eslint-config-prettier"; + +export default tseslint.config( + js.configs.recommended, + ...tseslint.configs.recommended, + ...svelte.configs["flat/recommended"], + prettier, + ...svelte.configs["flat/prettier"], + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + }, + }, + }, + { + files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"], + languageOptions: { + parserOptions: { + parser: tseslint.parser, + }, + }, + } +); diff --git a/packages/stacks-email/mjml-config.ts b/packages/stacks-email/mjml-config.ts new file mode 100644 index 0000000000..d162f3263f --- /dev/null +++ b/packages/stacks-email/mjml-config.ts @@ -0,0 +1,246 @@ +import { mjmlJsonToString } from "./mjml-json"; +import { tokens } from "./tokens"; +import type { MjmlNode } from "./types"; + +const { color, font, spacing, layout, border } = tokens; +const fontWeightSemibold = "600"; + +export const BODY_PARAGRAPH_MARGIN = "0 0 16px"; +export const BODY_LIST_MARGIN = "0 0 16px 24px"; +export const BODY_LIST_PADDING = "0"; +export const BODY_LIST_ITEM_MARGIN = "0 0 8px"; + +const createMjClass = ( + name: string, + attributes: Record +): MjmlNode => ({ + tagName: "mj-class", + attributes: { + name, + ...attributes, + }, +}); + +const generatedBackgroundClasses = color.backgroundClasses.map( + ({ name, value }) => + createMjClass(`bg-${name}`, { + "background-color": value, + }) +); + +const generatedFontClasses = color.fontClasses.map(({ name, value }) => + createMjClass(`fc-${name}`, { + color: value, + }) +); + +const attributesChildren: MjmlNode[] = [ + { + tagName: "mj-all", + attributes: { + "font-family": font.family, + "color": color.text, + "font-size": "16px", + "line-height": "120%", + }, + }, + { + tagName: "mj-body", + attributes: { + "width": layout.maxWidth, + "background-color": color.background, + }, + }, + { + tagName: "mj-text", + attributes: { + "padding": "0px", + }, + }, + { + tagName: "mj-image", + attributes: { + "padding": "0px", + }, + }, + { + tagName: "mj-button", + attributes: { + "padding": "0px", + }, + }, + { + tagName: "mj-table", + attributes: { + "padding": "0px", + }, + }, + { + tagName: "mj-column", + attributes: { + "padding": "0px", + }, + }, + { + tagName: "mj-section", + attributes: { + "padding": "0px", + }, + }, + { + tagName: "mj-wrapper", + attributes: { + "padding": "0px", + }, + }, + + // Utility classes generated from design-token arrays. + ...generatedBackgroundClasses, + ...generatedFontClasses, + + createMjClass("s-email-text-title", { + "color": color.text, + "font-family": "Stack Sans Notch, Arial, Helvetica, sans-serif", + "font-size": "24px", + "font-weight": font.weightNormal, + "line-height": "100%", + "padding": "0", + }), + createMjClass("s-email-text-headline", { + "color": color.text, + "font-family": "Stack Sans Notch, Arial, Helvetica, sans-serif", + "font-size": "36px", + "font-weight": font.weightNormal, + "line-height": "100%", + "padding": "0", + }), + + createMjClass("s-email-text-subtitle", { + "color": color.text, + "font-size": "14px", + "font-weight": font.weightBold, + "line-height": "120%", + "padding": "0", + }), + createMjClass("s-email-text-secondary-information", { + "color": color.textMuted, + "font-size": "12px", + "font-weight": font.weightNormal, + "line-height": "120%", + "padding": "0", + }), + createMjClass("s-email-text-body", { + "color": color.text, + "font-size": "16px", + "font-weight": font.weightNormal, + "line-height": "120%", + }), + createMjClass("s-email-text-caption", { + "color": color.textMuted, + "font-size": "14px", + "font-weight": font.weightNormal, + "line-height": "120%", + }), + createMjClass("s-email-text-alert", { + "color": color.text, + "font-size": "16px", + "font-weight": fontWeightSemibold, + "line-height": "120%", + "padding": "0", + }), + + createMjClass("button", { + "background-color": color.brandDark, + "color": "#ffffff", + "border-radius": border.radius, + "font-size": "14px", + "font-weight": font.weightBold, + "line-height": "120%", + "inner-padding": `12px 18px`, + }), + createMjClass("button__tonal", { + "background-color": color.brandOffWhite, + "color": color.text, + }), + createMjClass("button__inverted", { + "background-color": color.background, + "color": color.text, + }), +]; + +const linkStyles = ` +a.link { + color: ${color.link}; + text-decoration: underline; + font-size: 16px; + line-height: normal; + font-weight: ${font.weightNormal}; +} +a.footer-link { + color: ${color.textFooter}; + text-decoration: underline; + font-size: 14px; + line-height: 120%; + margin-right: 10px; +} +`.trim(); + +// Head-only Progressive enhancement: hover states only apply in clients that support head CSS and :hover. +const hoverStyles = ` +a.link:hover { + color: ${color.linkHover} !important; +} +a.footer-link:hover, +a.footer-link-light:hover, +a.footer-link:hover { + opacity: 0.85 !important; +} +.button-hover a:hover { + background-color: #47484d !important; + color: #fff !important; +} +.button-hover-inverted a:hover { + background-color: ${color.brandOffWhite} !important; +} + +.footer-social-hidden { + display: none !important; + max-height: 0 !important; + overflow: hidden !important; + mso-hide: all !important; +} +`.trim(); + +export const mjmlConfigNodes: MjmlNode[] = [ + { + tagName: "mj-font", + attributes: { + name: "Stack Sans Headline", + href: "https://fonts.googleapis.com/css2?family=Stack+Sans+Headline:wght@400;600&display=swap", + }, + }, + { + tagName: "mj-font", + attributes: { + name: "Stack Sans Notch", + href: "https://fonts.googleapis.com/css2?family=Stack+Sans+Notch:wght@400&display=swap", + }, + }, + { + tagName: "mj-attributes", + children: attributesChildren, + }, + { + tagName: "mj-style", + attributes: { + inline: "inline", + }, + content: linkStyles, + }, + { + tagName: "mj-style", + content: hoverStyles, + }, +]; + +export const mjmlConfig = mjmlJsonToString(mjmlConfigNodes); diff --git a/packages/stacks-email/mjml-json.ts b/packages/stacks-email/mjml-json.ts new file mode 100644 index 0000000000..5f793f4dcf --- /dev/null +++ b/packages/stacks-email/mjml-json.ts @@ -0,0 +1,19 @@ +import json2mjmlModule from "json2mjml"; + +import type { MjmlNode } from "./types"; + +export const mjmlJsonToString = (source: MjmlNode | MjmlNode[]) => { + const json2mjml = + typeof json2mjmlModule === "function" + ? json2mjmlModule + : (json2mjmlModule as { default?: (node: unknown) => string }) + .default; + + if (typeof json2mjml !== "function") { + throw new Error("json2mjml export is not callable"); + } + + const nodes = Array.isArray(source) ? source : [source]; + + return nodes.map((node) => json2mjml(node as never)).join("\n"); +}; diff --git a/packages/stacks-email/package.json b/packages/stacks-email/package.json new file mode 100644 index 0000000000..b109044e8d --- /dev/null +++ b/packages/stacks-email/package.json @@ -0,0 +1,46 @@ +{ + "name": "@stackoverflow/stacks-email", + "version": "0.0.1", + "private": true, + "type": "module", + "exports": { + ".": "./src/lib/public/index.ts" + }, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "npm run check && eslint .", + "format": "prettier --write ." + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^6.1.0", + "@sveltejs/kit": "^2.48.5", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@types/mjml": "^5.0.0", + "eslint": "^9.39.2", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.14.1", + "globals": "^17.0.0", + "mdsvex": "^0.12.3", + "prettier": "^3.8.3", + "prettier-plugin-svelte": "^3.4.0", + "svelte": "^5.55.9", + "svelte-check": "^4.3.5", + "typescript": "^5.9.3", + "typescript-eslint": "^8.53.0", + "vite": "^7.3.3" + }, + "dependencies": { + "@stackoverflow/stacks": "*", + "@stackoverflow/stacks-svelte": "*", + "highlight.js": "^11.11.1", + "json2mjml": "^1.0.3", + "markdown-it": "^14.1.0", + "mjml": "^4.17.1", + "zod": "^4.1.12" + } +} diff --git a/packages/stacks-email/registry.ts b/packages/stacks-email/registry.ts new file mode 100644 index 0000000000..ac3122d835 --- /dev/null +++ b/packages/stacks-email/registry.ts @@ -0,0 +1,35 @@ +import button from "./components/button"; +import footer from "./components/footer"; +import graphic from "./components/graphic"; +import headline from "./components/headline"; +import header from "./components/header"; +import spacers from "./components/spacers"; +import text from "./components/text"; +import title from "./components/title"; + +import transactional from "./templates/transactional"; + +export const componentDefinitions = [ + button, + footer, + graphic, + headline, + header, + spacers, + text, + title, +] as const; + +export const templateDefinitions = [transactional] as const; + +export { + button, + footer, + graphic, + headline, + header, + spacers, + text, + title, + transactional, +}; diff --git a/packages/stacks-email/src/app.css b/packages/stacks-email/src/app.css new file mode 100644 index 0000000000..6c79ac5b9b --- /dev/null +++ b/packages/stacks-email/src/app.css @@ -0,0 +1 @@ +@import "@stackoverflow/stacks/dist/css/stacks.css"; diff --git a/packages/stacks-email/src/app.d.ts b/packages/stacks-email/src/app.d.ts new file mode 100644 index 0000000000..5b77823d77 --- /dev/null +++ b/packages/stacks-email/src/app.d.ts @@ -0,0 +1,12 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/packages/stacks-email/src/app.html b/packages/stacks-email/src/app.html new file mode 100644 index 0000000000..1966776910 --- /dev/null +++ b/packages/stacks-email/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/packages/stacks-email/src/components/TemplateSidebar.svelte b/packages/stacks-email/src/components/TemplateSidebar.svelte new file mode 100644 index 0000000000..d9beb9b6bf --- /dev/null +++ b/packages/stacks-email/src/components/TemplateSidebar.svelte @@ -0,0 +1,59 @@ + + + diff --git a/packages/stacks-email/src/lib/highlight/highlight.ts b/packages/stacks-email/src/lib/highlight/highlight.ts new file mode 100644 index 0000000000..73aa2664ca --- /dev/null +++ b/packages/stacks-email/src/lib/highlight/highlight.ts @@ -0,0 +1,29 @@ +import hljs from "highlight.js"; + +type SupportedLanguage = "html" | "xml"; + +const escapeHtml = (input: string) => + input + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">"); + +const resolveLanguage = (language: SupportedLanguage) => + hljs.getLanguage(language) ? language : "plaintext"; + +export const highlightCode = async ( + code: string, + language: SupportedLanguage +) => { + try { + const resolvedLanguage = resolveLanguage(language); + const highlighted = hljs.highlight(code, { + language: resolvedLanguage, + }).value; + + return `
${highlighted}
`; + } catch { + const escaped = escapeHtml(code); + return `
${escaped}
`; + } +}; diff --git a/packages/stacks-email/src/lib/pipeline/compile.ts b/packages/stacks-email/src/lib/pipeline/compile.ts new file mode 100644 index 0000000000..141c844be0 --- /dev/null +++ b/packages/stacks-email/src/lib/pipeline/compile.ts @@ -0,0 +1,165 @@ +import mjml2html from "mjml"; + +import { + applyTemplateProps, + extractComponentHtml, + extractTagMarkup, + wrapComponentWithMarkers, + wrapTagWithMarkers, +} from "./template"; + +import { targets, tokens, type CompileTarget } from "../../../tokens"; +import { mjmlConfig } from "../../../mjml-config"; +import { transformTokens } from "./transform"; + +type MjmlCompileResult = { + html: string; + errors: { + line: number | undefined; + message: string; + tagName?: string; + }[]; +}; + +const mjml2htmlSync = mjml2html as unknown as ( + mjml: string, + options?: { + validationLevel?: "strict" | "soft" | "skip"; + keepComments?: boolean; + minify?: boolean; + } +) => MjmlCompileResult; + +const mjmlTagPattern = /]/i; +const mjHeadOpenPattern = //i; +const mjHeadClosePattern = /<\/mj-head>/i; +const mjmlOpenTagPattern = /]*>/i; + +const escapePreviewText = (value: string) => + value + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">"); + +const buildPreviewHeadNode = (previewText: string | undefined) => { + const normalizedPreviewText = previewText?.trim(); + if (!normalizedPreviewText) { + return ""; + } + + return `${escapePreviewText(normalizedPreviewText)}\n`; +}; + +const wrapInDocument = (mjmlSource: string, previewText: string | undefined) => ` + + + ${buildPreviewHeadNode(previewText)}${mjmlConfig} + + + + ${mjmlSource} + + + +`; + +const injectConfigIntoDocument = ( + documentSource: string, + previewText: string | undefined +) => { + const previewHeadNode = buildPreviewHeadNode(previewText); + + if (mjHeadOpenPattern.test(documentSource)) { + return documentSource.replace( + mjHeadClosePattern, + `${previewHeadNode}${mjmlConfig}\n` + ); + } + + if (mjmlOpenTagPattern.test(documentSource)) { + return documentSource.replace( + mjmlOpenTagPattern, + (openTag) => + `${openTag}\n${previewHeadNode}${mjmlConfig}` + ); + } + + return wrapInDocument(documentSource, previewText); +}; + +export type CompileMjmlInput = { + mjml: string; + target: CompileTarget; + props?: Record; + previewText?: string; + extractComponentName?: string; + extractComponentTag?: string; +}; + +export type CompileMjmlOutput = { + html: string; + componentHtml: string | null; + componentMjml: string | null; + mjml: string; + renderedMjml: string; + errors: { + line: number | undefined; + message: string; + tagName?: string; + }[]; +}; + +export const compileMjml = ({ + mjml, + target, + props = {}, + previewText, + extractComponentName, + extractComponentTag, +}: CompileMjmlInput): CompileMjmlOutput => { + const renderedMjml = applyTemplateProps(mjml, props); + const mjmlForCompile = extractComponentName + ? extractComponentTag + ? wrapTagWithMarkers( + renderedMjml, + extractComponentName, + extractComponentTag + ) + : wrapComponentWithMarkers(renderedMjml, extractComponentName) + : renderedMjml; + + const fullMjml = mjmlTagPattern.test(mjmlForCompile) + ? injectConfigIntoDocument(mjmlForCompile, previewText) + : wrapInDocument(mjmlForCompile, previewText); + + const compileResult = mjml2htmlSync(fullMjml, { + validationLevel: "soft", + keepComments: true, + minify: false, + }); + + const replacements = targets[target].tokens; + const targetRenderedMjml = transformTokens(renderedMjml, replacements); + const html = transformTokens(compileResult.html, replacements); + const componentMjml = extractComponentName + ? extractComponentTag + ? extractTagMarkup(targetRenderedMjml, extractComponentTag) + : targetRenderedMjml.trim() + : null; + const componentHtml = extractComponentName + ? extractComponentHtml(html, extractComponentName) + : null; + + return { + html, + componentMjml, + componentHtml, + mjml: fullMjml, + renderedMjml: targetRenderedMjml, + errors: compileResult.errors.map((issue) => ({ + line: issue.line, + message: issue.message, + tagName: issue.tagName, + })), + }; +}; diff --git a/packages/stacks-email/src/lib/pipeline/template.ts b/packages/stacks-email/src/lib/pipeline/template.ts new file mode 100644 index 0000000000..83abe92c19 --- /dev/null +++ b/packages/stacks-email/src/lib/pipeline/template.ts @@ -0,0 +1,92 @@ +const markerStart = (name: string) => ``; +const markerEnd = (name: string) => ``; + +const escapeRegExp = (value: string) => + value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +const buildPairTagPattern = (tagName: string) => + new RegExp( + `(<${tagName}\\b[^>]*>[\\s\\S]*?<\\/${tagName}>)`, + "i" + ); + +const buildSelfClosingTagPattern = (tagName: string) => + new RegExp(`(<${tagName}\\b[^>]*/>)`, "i"); + +const buildMarkerPattern = (name: string) => + new RegExp( + `${escapeRegExp(markerStart(name))}[\\s\\S]*?${escapeRegExp( + markerEnd(name) + )}`, + "i" + ); + +export const applyTemplateProps = ( + source: string, + props: Record +) => + Object.entries(props).reduce( + (next, [key, value]) => + next.replaceAll(`{{${key}}}`, String(value ?? "")), + source + ); + +export const wrapComponentWithMarkers = (mjml: string, name: string) => ` +${markerStart(name)} +${mjml} +${markerEnd(name)} +`; + +export const wrapTagWithMarkers = ( + mjml: string, + name: string, + tagName: string +) => { + const pairPattern = buildPairTagPattern(tagName); + const selfClosingPattern = buildSelfClosingTagPattern(tagName); + + if (pairPattern.test(mjml)) { + return mjml.replace( + pairPattern, + `${markerStart(name)}\n$1\n${markerEnd( + name + )}` + ); + } + + if (selfClosingPattern.test(mjml)) { + return mjml.replace( + selfClosingPattern, + `${markerStart(name)}\n$1\n${markerEnd( + name + )}` + ); + } + + return wrapComponentWithMarkers(mjml, name); +}; + +export const extractBetweenMarkers = (source: string, name: string) => { + const pattern = buildMarkerPattern(name); + const match = source.match(pattern); + + if (!match) { + return null; + } + + return match[0] + .replace(markerStart(name), "") + .replace(markerEnd(name), "") + .trim(); +}; + +export const extractComponentHtml = (html: string, name: string) => + extractBetweenMarkers(html, name); + +export const extractTagMarkup = (source: string, tagName: string) => { + const pairPattern = buildPairTagPattern(tagName); + const selfClosingPattern = buildSelfClosingTagPattern(tagName); + const match = source.match(pairPattern) ?? source.match(selfClosingPattern); + + return match?.[0]?.trim() ?? null; +}; diff --git a/packages/stacks-email/src/lib/pipeline/transform.ts b/packages/stacks-email/src/lib/pipeline/transform.ts new file mode 100644 index 0000000000..716303099c --- /dev/null +++ b/packages/stacks-email/src/lib/pipeline/transform.ts @@ -0,0 +1,12 @@ +const tokenPattern = /\[\[([A-Z0-9_]+)\]\]/g; + +export const transformTokens = ( + input: string, + replacements: Record +) => + input.replace(tokenPattern, (match, token: string) => { + if (token in replacements) { + return replacements[token]; + } + return match; + }); diff --git a/packages/stacks-email/src/lib/public/catalog.ts b/packages/stacks-email/src/lib/public/catalog.ts new file mode 100644 index 0000000000..a08b6d9731 --- /dev/null +++ b/packages/stacks-email/src/lib/public/catalog.ts @@ -0,0 +1,15 @@ +import { + listEmailComponents, + type EmailComponentCatalogItem, +} from "./components"; +import { listEmailTemplates, type EmailTemplateCatalogItem } from "./templates"; + +export type EmailCatalog = { + components: EmailComponentCatalogItem[]; + templates: EmailTemplateCatalogItem[]; +}; + +export const getEmailCatalog = (): EmailCatalog => ({ + components: listEmailComponents(), + templates: listEmailTemplates(), +}); diff --git a/packages/stacks-email/src/lib/public/compile.ts b/packages/stacks-email/src/lib/public/compile.ts new file mode 100644 index 0000000000..274157aa0f --- /dev/null +++ b/packages/stacks-email/src/lib/public/compile.ts @@ -0,0 +1,63 @@ +import { + compileEmailComponent, + getEmailComponentMeta, + type CompileComponentOutput, +} from "./components"; +import { + compileEmailTemplate, + getEmailTemplateMeta, + type CompileTemplateOutput, +} from "./templates"; +import type { CompileTarget } from "../../../tokens"; +import { compileEmailRenderableInputSchema } from "./validation"; + +export type EmailRenderableKind = "component" | "template"; + +export type CompileEmailRenderableInput = { + kind: EmailRenderableKind; + slug: string; + target: CompileTarget; + props?: Record; +}; + +export type CompileEmailRenderableOutput = + | CompileComponentOutput + | CompileTemplateOutput; + +export const compileEmailRenderable = ({ + kind, + slug, + target, + props = {}, +}: CompileEmailRenderableInput): CompileEmailRenderableOutput => { + const parsedInput = compileEmailRenderableInputSchema.parse({ + kind, + slug, + target, + props, + }); + + if (parsedInput.kind === "component") { + return compileEmailComponent({ + slug: parsedInput.slug, + target: parsedInput.target, + }); + } + + return compileEmailTemplate({ + slug: parsedInput.slug, + target: parsedInput.target, + props: parsedInput.props, + }); +}; + +export const getEmailRenderableMeta = ( + kind: EmailRenderableKind, + slug: string +) => { + if (kind === "component") { + return getEmailComponentMeta(slug); + } + + return getEmailTemplateMeta(slug); +}; diff --git a/packages/stacks-email/src/lib/public/components.ts b/packages/stacks-email/src/lib/public/components.ts new file mode 100644 index 0000000000..742b1f6742 --- /dev/null +++ b/packages/stacks-email/src/lib/public/components.ts @@ -0,0 +1,165 @@ +import { mjmlJsonToString } from "../../../mjml-json"; +import { componentDefinitions } from "../../../registry"; +import { renderVariantSource } from "../../../variants"; +import type { CompileTarget } from "../../../tokens"; +import type { + ComponentCategory, + EmailComponentMeta, + ComponentOptionReference, +} from "../../../types"; +import { compileMjml, type CompileMjmlOutput } from "../pipeline/compile"; +import { compileComponentInputSchema } from "./validation"; + +type ExpandedComponentRecord = { + slug: string; + name: string; + description: string; + category: ComponentCategory; + tokens: NonNullable; + options: NonNullable; + source: string; + htmlExtractionTag?: string; +}; + +export type EmailComponentCatalogItem = { + slug: string; + name: string; + description: string; + category: ComponentCategory; + tokens: NonNullable; + options: NonNullable; +}; + +export type CompileComponentInput = { + slug: string; + target: CompileTarget; +}; + +export type CompileComponentOutput = Omit< + CompileMjmlOutput, + "componentHtml" | "componentMjml" +> & { + kind: "component"; + slug: string; + componentHtml: string; + componentMjml: string; + meta: EmailComponentCatalogItem; +}; + +const DEFAULT_COMPONENT_CATEGORY: ComponentCategory = "Primitive"; + +const toLabel = (value: string) => + value + .split(/[-_]/g) + .filter(Boolean) + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); + +const expandComponentRecords = (): ExpandedComponentRecord[] => + componentDefinitions + .map((definition) => ({ + meta: definition.meta, + source: mjmlJsonToString(definition.source), + })) + .flatMap((record) => { + const { meta } = record; + + return meta.variants.map((variant, index) => { + const isDefault = + variant.id === meta.defaultVariant || + (!meta.defaultVariant && index === 0); + const slug = isDefault + ? meta.slug + : `${meta.slug}-${variant.id}`; + const baseName = meta.name ?? toLabel(meta.slug); + const baseDescription = meta.description ?? ""; + const variantName = variant.name ?? toLabel(variant.id); + const variantDescription = variant.description ?? baseDescription; + + return { + slug, + name: + meta.variants.length > 1 + ? `${baseName} — ${variantName}` + : baseName, + description: + meta.variants.length > 1 + ? variantDescription + : baseDescription, + category: meta.category ?? DEFAULT_COMPONENT_CATEGORY, + tokens: meta.tokens ?? [], + options: meta.options ?? [], + source: renderVariantSource(record, variant), + htmlExtractionTag: meta.htmlExtraction?.targetTag, + }; + }); + }) + .sort((a, b) => a.name.localeCompare(b.name)); + +const expandedComponentRecords = expandComponentRecords(); + +const componentBySlug = new Map( + expandedComponentRecords.map((record) => [record.slug, record] as const) +); + +const toCatalogItem = ( + record: ExpandedComponentRecord +): EmailComponentCatalogItem => ({ + slug: record.slug, + name: record.name, + description: record.description, + category: record.category, + tokens: record.tokens, + options: record.options, +}); + +export const listEmailComponents = () => + expandedComponentRecords.map((record) => toCatalogItem(record)); + +export const getEmailComponentMeta = (slug: string) => { + const record = componentBySlug.get(slug); + if (!record) { + return null; + } + return toCatalogItem(record); +}; + +export const getEmailComponentOptions = ( + slug: string +): ComponentOptionReference[] | null => { + const record = componentBySlug.get(slug); + if (!record) { + return null; + } + + return record.options; +}; + +export const compileEmailComponent = ({ + slug, + target, +}: CompileComponentInput): CompileComponentOutput => { + const parsedInput = compileComponentInputSchema.parse({ slug, target }); + const record = componentBySlug.get(parsedInput.slug); + + if (!record) { + throw new Error(`Unknown email component slug: ${parsedInput.slug}`); + } + + const result = compileMjml({ + mjml: record.source, + target: parsedInput.target, + props: {}, + extractComponentName: record.slug, + extractComponentTag: record.htmlExtractionTag, + }); + + return { + ...result, + kind: "component", + slug: parsedInput.slug, + componentHtml: result.componentHtml ?? result.html, + componentMjml: result.componentMjml ?? result.renderedMjml, + meta: toCatalogItem(record), + }; +}; diff --git a/packages/stacks-email/src/lib/public/index.ts b/packages/stacks-email/src/lib/public/index.ts new file mode 100644 index 0000000000..f201277498 --- /dev/null +++ b/packages/stacks-email/src/lib/public/index.ts @@ -0,0 +1,53 @@ +export { + tokens, + targets, + targetNames, + isCompileTarget, + type Tokens, + type CompileTarget, +} from "../../../tokens"; +export { + transformTokens, +} from "../pipeline/transform"; +export { + compileMjml, + type CompileMjmlInput, + type CompileMjmlOutput, +} from "../pipeline/compile"; + +export { + listEmailComponents, + getEmailComponentMeta, + getEmailComponentOptions, + compileEmailComponent, + type EmailComponentCatalogItem, + type CompileComponentInput, + type CompileComponentOutput, +} from "./components"; + +export { + listEmailTemplates, + getEmailTemplateMeta, + compileEmailTemplate, + type EmailTemplateCatalogItem, + type CompileTemplateInput, + type CompileTemplateOutput, +} from "./templates"; + +export { getEmailCatalog, type EmailCatalog } from "./catalog"; + +export { + compileEmailRenderable, + getEmailRenderableMeta, + type EmailRenderableKind, + type CompileEmailRenderableInput, + type CompileEmailRenderableOutput, +} from "./compile"; + +export { + compileTargetSchema, + emailRenderableKindSchema, + compileComponentInputSchema, + compileTemplateInputSchema, + compileEmailRenderableInputSchema, +} from "./validation"; diff --git a/packages/stacks-email/src/lib/public/templates.ts b/packages/stacks-email/src/lib/public/templates.ts new file mode 100644 index 0000000000..672f220ecb --- /dev/null +++ b/packages/stacks-email/src/lib/public/templates.ts @@ -0,0 +1,291 @@ +import { mjmlJsonToString } from "../../../mjml-json"; +import { + BODY_LIST_ITEM_MARGIN, + BODY_LIST_MARGIN, + BODY_LIST_PADDING, + BODY_PARAGRAPH_MARGIN, +} from "../../../mjml-config"; +import { templateDefinitions } from "../../../registry"; +import { renderTemplateVariantSource } from "../../../variants"; +import type { CompileTarget } from "../../../tokens"; +import type { + EmailTemplateCategory, + EmailTemplateMeta, + EmailTemplateModule, + EmailTemplateVariant, +} from "../../../types"; +import MarkdownIt from "markdown-it"; +import { compileMjml, type CompileMjmlOutput } from "../pipeline/compile"; +import { compileTemplateInputSchema } from "./validation"; + +type ExpandedTemplateRecord = { + slug: string; + name: string; + description: string; + category: EmailTemplateCategory; + tokens: NonNullable; + variantProps: Record; + variant: EmailTemplateVariant; + template: EmailTemplateModule; +}; + +export type EmailTemplateCatalogItem = { + slug: string; + name: string; + description: string; + category: EmailTemplateCategory; + tokens: NonNullable; +}; + +export type CompileTemplateInput = { + slug: string; + target: CompileTarget; + props?: Record; +}; + +export type CompileTemplateOutput = CompileMjmlOutput & { + kind: "template"; + slug: string; + meta: EmailTemplateCatalogItem; +}; + +const DEFAULT_TEMPLATE_CATEGORY: EmailTemplateCategory = "Transactional"; +const DEFAULT_TEMPLATE_PREVIEW_TEXT = "Stack Overflow update"; + +const TEMPLATE_PREVIEW_TOKEN: NonNullable[number] = { + token: "PREVIEW_TEXT", + description: + "Inbox preheader text inserted into `` for all template compiles.", +}; + +const withSharedTemplateTokens = ( + tokens: NonNullable +) => { + const uniqueByToken = new Map(); + + for (const token of [TEMPLATE_PREVIEW_TOKEN, ...tokens]) { + if (!uniqueByToken.has(token.token)) { + uniqueByToken.set(token.token, token); + } + } + + return [...uniqueByToken.values()]; +}; + +const markdown = new MarkdownIt({ + html: false, + breaks: true, + linkify: true, + typographer: true, +}); + +markdown.renderer.rules.link_open = (tokenList, index, options, env, self) => { + tokenList[index].attrJoin("class", "link"); + return self.renderToken(tokenList, index, options); +}; + +markdown.renderer.rules.paragraph_open = ( + tokenList, + index, + options, + env, + self +) => { + const hasAnotherParagraph = tokenList + .slice(index + 1) + .some((token) => token.type === "paragraph_open"); + + tokenList[index].attrSet( + "style", + `margin:${hasAnotherParagraph ? BODY_PARAGRAPH_MARGIN : "0"};` + ); + + return self.renderToken(tokenList, index, options); +}; + +markdown.renderer.rules.bullet_list_open = ( + tokenList, + index, + options, + env, + self +) => { + tokenList[index].attrSet( + "style", + `margin:${BODY_LIST_MARGIN};padding:${BODY_LIST_PADDING};` + ); + + return self.renderToken(tokenList, index, options); +}; + +markdown.renderer.rules.list_item_open = ( + tokenList, + index, + options, + env, + self +) => { + const hasAnotherListItem = tokenList + .slice(index + 1) + .some((token) => token.type === "list_item_open"); + + tokenList[index].attrSet( + "style", + `margin:${hasAnotherListItem ? BODY_LIST_ITEM_MARGIN : "0"};` + ); + + return self.renderToken(tokenList, index, options); +}; + +const renderBodyMarkdown = (value: string) => { + const content = value.trim(); + + if (!content) { + return ""; + } + + return markdown.render(content).trim(); +}; + +const resolveTemplateCompileProps = ( + record: ExpandedTemplateRecord, + inputProps: Record +) => { + const compileProps = { ...inputProps }; + const defaultBodyMarkdown = record.variantProps.BODY_DEFAULT_MARKDOWN; + + if (typeof compileProps.BODY_MARKDOWN === "string") { + compileProps.BODY_CONTENT = renderBodyMarkdown(compileProps.BODY_MARKDOWN); + } else if (typeof compileProps.BODY_CONTENT !== "string") { + compileProps.BODY_CONTENT = renderBodyMarkdown(defaultBodyMarkdown ?? ""); + } + + if (typeof compileProps.PREVIEW_TEXT !== "string") { + compileProps.PREVIEW_TEXT = + record.description.trim() || + record.name.trim() || + DEFAULT_TEMPLATE_PREVIEW_TEXT; + } + + return compileProps; +}; + +const renderTemplateSource = ( + record: ExpandedTemplateRecord, + compileProps: Record +) => { + const variant: EmailTemplateVariant = { + ...record.variant, + props: { + ...record.variant.props, + ...compileProps, + }, + }; + const source = mjmlJsonToString(record.template.document(variant)); + return renderTemplateVariantSource( + { + meta: record.template.meta, + source, + }, + variant + ); +}; + +const toLabel = (value: string) => + value + .split(/[-_]/g) + .filter(Boolean) + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); + +const expandTemplateRecords = (): ExpandedTemplateRecord[] => + templateDefinitions + .flatMap((definition) => { + const { meta } = definition; + return meta.variants.map((variant, index) => { + const isDefault = + variant.id === meta.defaultVariant || + (!meta.defaultVariant && index === 0); + const slug = isDefault + ? meta.slug + : `${meta.slug}-${variant.id}`; + const baseName = meta.name ?? toLabel(meta.slug); + const baseDescription = meta.description ?? ""; + const variantName = variant.name ?? toLabel(variant.id); + const variantDescription = variant.description ?? baseDescription; + + return { + slug, + name: + meta.variants.length > 1 + ? variantName + : baseName, + description: + meta.variants.length > 1 + ? variantDescription + : baseDescription, + category: meta.category ?? DEFAULT_TEMPLATE_CATEGORY, + tokens: meta.tokens ?? [], + variantProps: variant.props, + variant, + template: definition, + }; + }); + }) + .sort((a, b) => a.name.localeCompare(b.name)); + +const expandedTemplateRecords = expandTemplateRecords(); + +const templateBySlug = new Map( + expandedTemplateRecords.map((record) => [record.slug, record] as const) +); + +const toCatalogItem = ( + record: ExpandedTemplateRecord +): EmailTemplateCatalogItem => ({ + slug: record.slug, + name: record.name, + description: record.description, + category: record.category, + tokens: withSharedTemplateTokens(record.tokens), +}); + +export const listEmailTemplates = () => + expandedTemplateRecords.map((record) => toCatalogItem(record)); + +export const getEmailTemplateMeta = (slug: string) => { + const record = templateBySlug.get(slug); + if (!record) { + return null; + } + return toCatalogItem(record); +}; + +export const compileEmailTemplate = ({ + slug, + target, + props = {}, +}: CompileTemplateInput): CompileTemplateOutput => { + const parsedInput = compileTemplateInputSchema.parse({ slug, target, props }); + const record = templateBySlug.get(parsedInput.slug); + + if (!record) { + throw new Error(`Unknown email template slug: ${parsedInput.slug}`); + } + + const compileProps = resolveTemplateCompileProps(record, parsedInput.props ?? {}); + const source = renderTemplateSource(record, compileProps); + const result = compileMjml({ + mjml: source, + target: parsedInput.target, + props: {}, + previewText: compileProps.PREVIEW_TEXT, + }); + + return { + ...result, + kind: "template", + slug: parsedInput.slug, + meta: toCatalogItem(record), + }; +}; diff --git a/packages/stacks-email/src/lib/public/validation.ts b/packages/stacks-email/src/lib/public/validation.ts new file mode 100644 index 0000000000..6f7c4ca91e --- /dev/null +++ b/packages/stacks-email/src/lib/public/validation.ts @@ -0,0 +1,38 @@ +import { z } from "zod/v4"; + +import type { CompileTarget } from "../../../tokens"; + +const compileTargetValues = ["preview", "dotnet", "braze"] as const satisfies readonly CompileTarget[]; + +const slugSchema = z + .string({ error: "`slug` must be a non-empty string." }) + .trim() + .min(1, { error: "`slug` must be a non-empty string." }); + +export const compileTargetSchema = z.enum(compileTargetValues, { + error: "`target` must be one of `preview`, `dotnet`, or `braze`.", +}); + +export const emailRenderableKindSchema = z.enum(["component", "template"], { + error: "`kind` must be `component` or `template`.", +}); + +export const compileComponentInputSchema = z.object({ + slug: slugSchema, + target: compileTargetSchema, +}); + +const compilePropsSchema = z.record(z.string(), z.string()).optional(); + +export const compileTemplateInputSchema = z.object({ + slug: slugSchema, + target: compileTargetSchema, + props: compilePropsSchema, +}); + +export const compileEmailRenderableInputSchema = z.object({ + kind: emailRenderableKindSchema, + slug: slugSchema, + target: compileTargetSchema, + props: compilePropsSchema, +}); diff --git a/packages/stacks-email/src/routes/+layout.svelte b/packages/stacks-email/src/routes/+layout.svelte new file mode 100644 index 0000000000..5225fcff98 --- /dev/null +++ b/packages/stacks-email/src/routes/+layout.svelte @@ -0,0 +1,7 @@ + + +{@render children()} diff --git a/packages/stacks-email/src/routes/+layout.ts b/packages/stacks-email/src/routes/+layout.ts new file mode 100644 index 0000000000..d43d0cd2a5 --- /dev/null +++ b/packages/stacks-email/src/routes/+layout.ts @@ -0,0 +1 @@ +export const prerender = false; diff --git a/packages/stacks-email/src/routes/+page.server.ts b/packages/stacks-email/src/routes/+page.server.ts new file mode 100644 index 0000000000..77ebd7fcf8 --- /dev/null +++ b/packages/stacks-email/src/routes/+page.server.ts @@ -0,0 +1,7 @@ +import type { PageServerLoad } from "./$types"; + +import { listEmailTemplates } from "$lib/public/templates"; + +export const load: PageServerLoad = async () => ({ + templates: listEmailTemplates(), +}); diff --git a/packages/stacks-email/src/routes/+page.svelte b/packages/stacks-email/src/routes/+page.svelte new file mode 100644 index 0000000000..d0f01956ea --- /dev/null +++ b/packages/stacks-email/src/routes/+page.svelte @@ -0,0 +1,22 @@ + + +
+ + +
+
+
+ +

Email template gallery

+

Choose a template from the sidebar to open preview, MJML, and compiled HTML output.

+
+
+
+
diff --git a/packages/stacks-email/src/routes/api/compile/+server.ts b/packages/stacks-email/src/routes/api/compile/+server.ts new file mode 100644 index 0000000000..83b355f982 --- /dev/null +++ b/packages/stacks-email/src/routes/api/compile/+server.ts @@ -0,0 +1,231 @@ +import { json } from "@sveltejs/kit"; +import type { RequestHandler } from "./$types"; +import { z } from "zod/v4"; +import { env } from "$env/dynamic/private"; + +import { compileMjml } from "$lib/pipeline/compile"; +import { compileTargetSchema } from "$lib/public/validation"; +import { Button } from "../../../../components/button"; +import { Footer } from "../../../../components/footer"; +import { Headline } from "../../../../components/headline"; +import { Header } from "../../../../components/header"; +import { Text } from "../../../../components/text"; +import { Title } from "../../../../components/title"; +import { Section } from "../../../../components/section"; +import { Spacer } from "../../../../components/spacer"; +import { mjmlJsonToString } from "../../../../mjml-json"; +import { tokens } from "../../../../tokens"; +import type { MjmlNode } from "../../../../types"; + +const headlineBlockSchema = z.object({ + type: z.literal("headline"), + variant: z.enum(["default", "highlight"]).optional().default("default"), + props: z + .object({ + sectionClass: z.string().optional(), + textClass: z.string().optional(), + textAlign: z.string().optional(), + textContent: z.string().optional(), + textHighlight: z.union([z.boolean(), z.string()]).optional(), + }) + .optional(), +}); + +const textBlockSchema = z.object({ + type: z.literal("text"), + variant: z.enum(["body", "centered"]).optional().default("body"), + props: z + .object({ + columnClass: z.string().optional(), + sectionClass: z.string().optional(), + textAlign: z.string().optional(), + textClass: z.string().optional(), + textContent: z.string().optional(), + }) + .optional(), +}); + +const buttonBlockSchema = z.object({ + type: z.literal("button"), + variant: z + .enum(["primary", "secondary", "inverted"]) + .optional() + .default("primary"), + props: z + .object({ + sectionClass: z.string().optional(), + align: z.string().optional(), + className: z.string().optional(), + cssClass: z.string().optional(), + href: z.string().optional(), + text: z.string().optional(), + }) + .optional(), +}); + +const titleBlockSchema = z.object({ + type: z.literal("title"), + variant: z.enum(["default", "invert"]).optional().default("default"), + props: z + .object({ + sectionClass: z.string().optional(), + textClass: z.string().optional(), + textAlign: z.string().optional(), + textContent: z.string().optional(), + }) + .optional(), +}); + +const spacerBlockSchema = z.object({ + type: z.literal("spacer"), + size: z.enum(["medium", "large"]).optional().default("medium"), + props: z + .object({ + sectionClass: z.string().optional(), + height: z.string().optional(), + }) + .optional(), +}); + +const composeBlockSchema = z.discriminatedUnion("type", [ + headlineBlockSchema, + textBlockSchema, + buttonBlockSchema, + titleBlockSchema, + spacerBlockSchema, +]); + +const composeRequestSchema = z.object({ + template: z.literal("transactional"), + target: compileTargetSchema, + previewText: z.string().optional(), + blocks: z + .array(composeBlockSchema) + .min(1, { error: "`blocks` must contain at least one block." }), +}); + +type ComposeBlock = z.infer; + +const hasValidBearerToken = (request: Request): boolean => { + const expectedToken = env.STACKS_EMAIL_AUTH_TOKEN?.trim(); + if (!expectedToken) { + return true; + } + + const authorization = request.headers.get("authorization"); + if (!authorization) { + return false; + } + + const [scheme, token] = authorization.trim().split(/\s+/, 2); + return scheme?.toLowerCase() === "bearer" && token === expectedToken; +}; + +const renderTransactionalBlock = (block: ComposeBlock): MjmlNode => { + switch (block.type) { + case "headline": + return Headline(block.variant, block.props ?? {}); + case "text": + return Text(block.variant, block.props ?? {}); + case "button": { + const sectionClass = block.props?.sectionClass ?? "bg-block"; + const buttonProps = { + align: block.props?.align, + className: block.props?.className, + cssClass: block.props?.cssClass, + href: block.props?.href, + text: block.props?.text, + }; + return Section([Button(block.variant, buttonProps)], { + sectionClass, + }); + } + case "title": + return Title(block.variant, block.props ?? {}); + case "spacer": + return Spacer(block.size, block.props ?? {}); + } +}; + +const buildTransactionalDocument = (blocks: ComposeBlock[]): MjmlNode => ({ + tagName: "mjml", + children: [ + { + tagName: "mj-body", + attributes: { + "background-color": tokens.color.bodyBackground, + }, + children: [ + Spacer("large", { + sectionClass: "bg-page", + }), + Header("transactional"), + ...blocks.map((block) => renderTransactionalBlock(block)), + Spacer("medium", { + sectionClass: "bg-block", + }), + Footer("default", { + unsubscribeUrl: "[[UNSUBSCRIBE_URL]]", + }), + Spacer("large", { + sectionClass: "bg-page", + }), + ], + }, + ], +}); + +export const POST: RequestHandler = async ({ request }) => { + if (!hasValidBearerToken(request)) { + return json({ error: "Unauthorized." }, { status: 401 }); + } + + let body: unknown; + + try { + body = await request.json(); + } catch { + return json( + { error: "Request body must be valid JSON." }, + { status: 400 } + ); + } + + const parsed = composeRequestSchema.safeParse(body); + if (!parsed.success) { + return json( + { + error: parsed.error.issues.map((issue) => issue.message).join(" "), + }, + { status: 400 } + ); + } + + try { + const document = buildTransactionalDocument(parsed.data.blocks); + const mjml = mjmlJsonToString(document); + const compiled = compileMjml({ + mjml, + target: parsed.data.target, + props: {}, + previewText: parsed.data.previewText, + }); + + return json({ + ...compiled, + template: parsed.data.template, + target: parsed.data.target, + blockCount: parsed.data.blocks.length, + }); + } catch (error) { + return json( + { + error: + error instanceof Error + ? error.message + : "Failed to compose and compile transactional email.", + }, + { status: 500 } + ); + } +}; diff --git a/packages/stacks-email/src/routes/emails/[slug]/+page.server.ts b/packages/stacks-email/src/routes/emails/[slug]/+page.server.ts new file mode 100644 index 0000000000..d5eeca142f --- /dev/null +++ b/packages/stacks-email/src/routes/emails/[slug]/+page.server.ts @@ -0,0 +1,73 @@ +import { error } from "@sveltejs/kit"; +import type { PageServerLoad } from "./$types"; + +import { highlightCode } from "$lib/highlight/highlight"; +import { targetNames, type CompileTarget } from "../../../../tokens"; +import { + compileEmailTemplate, + getEmailTemplateMeta, + listEmailTemplates, +} from "$lib/public/templates"; + +export const load: PageServerLoad = async ({ params, url }) => { + const template = getEmailTemplateMeta(params.slug); + + if (!template) { + throw error(404, "Template not found"); + } + + const requestedTarget = url.searchParams.get("target"); + const target = ( + requestedTarget && + targetNames.includes(requestedTarget as CompileTarget) + ? requestedTarget + : "preview" + ) as CompileTarget; + + const compiledByTargetEntries = await Promise.all( + targetNames.map(async (compileTarget) => { + const compiled = compileEmailTemplate({ + slug: params.slug, + target: compileTarget, + }); + + const highlightedHtml = await highlightCode(compiled.html, "html"); + + return [ + compileTarget, + { + html: compiled.html, + highlightedHtml, + errors: compiled.errors, + }, + ] as const; + }) + ); + + const renderedMjml = compileEmailTemplate({ + slug: params.slug, + target: "preview", + }).renderedMjml; + + const highlightedMjml = await highlightCode(renderedMjml, "xml"); + + return { + templates: listEmailTemplates(), + template, + target, + renderedMjml, + highlightedMjml, + compiledByTarget: Object.fromEntries(compiledByTargetEntries) as Record< + CompileTarget, + { + html: string; + highlightedHtml: string; + errors: { + line: number | undefined; + message: string; + tagName?: string; + }[]; + } + >, + }; +}; diff --git a/packages/stacks-email/src/routes/emails/[slug]/+page.svelte b/packages/stacks-email/src/routes/emails/[slug]/+page.svelte new file mode 100644 index 0000000000..53bb89b026 --- /dev/null +++ b/packages/stacks-email/src/routes/emails/[slug]/+page.svelte @@ -0,0 +1,167 @@ + + + + +
+ + +
+
+ + {#each tabOptions as tab (tab.id)} + (activeTab = tab.id)} + /> + {/each} + + +
+ + + +
+
+ +
+ {#if activeCompiled.errors.length > 0} + + MJML reported {activeCompiled.errors.length} issue(s) for {activeTarget}. + + {/if} + +
+ {#if activeTab !== "preview"} +
+ {@html highlightedCode} +
+ {/if} + + +
+
+
+
diff --git a/packages/stacks-email/src/types/json2mjml.d.ts b/packages/stacks-email/src/types/json2mjml.d.ts new file mode 100644 index 0000000000..579f6b5e7d --- /dev/null +++ b/packages/stacks-email/src/types/json2mjml.d.ts @@ -0,0 +1,4 @@ +declare module "json2mjml" { + const json2mjml: (node: unknown) => string; + export default json2mjml; +} diff --git a/packages/stacks-email/src/types/stacks-icons.d.ts b/packages/stacks-email/src/types/stacks-icons.d.ts new file mode 100644 index 0000000000..273bb934f7 --- /dev/null +++ b/packages/stacks-email/src/types/stacks-icons.d.ts @@ -0,0 +1 @@ +declare module "@stackoverflow/stacks-icons"; diff --git a/packages/stacks-email/static/email/hero/1200x630.png b/packages/stacks-email/static/email/hero/1200x630.png new file mode 100644 index 0000000000000000000000000000000000000000..2bbd4bf221b03f54352bf5ff54c2f0a3574b9f4b GIT binary patch literal 24010 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 z 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 IN56#s0x?g$t^fc4 literal 0 HcmV?d00001 diff --git a/packages/stacks-email/static/email/social/instagram.png b/packages/stacks-email/static/email/social/instagram.png new file mode 100644 index 0000000000000000000000000000000000000000..c91e85e8215304c3a91022869e4d648d8a741807 GIT binary patch literal 906 zcmV;519kj~P)P`@6f|3cK6Eqv7nV`)C z&7*dps3*kP<%&%+!v=f%iu4CvP+v83Mif?FqCw~H6wh${8UpvBfyG_Ul6t-ke;LWeK?zoa5 z9Y>opvMf>$>-BmLOKf<#*h*Z-yO-s1Io5qUoz9Hjav>uFv8hAHdt1ke4?-~eU&M7d zZCM(!oxBKtf=m*Af-*BR0d($lU+5fE5!9VCbncNqIaa8o9$Vi^GXZpNNLJ1^iff5Y zg-%%Ha^*Z3lshQ{y!%9lCCw+~^35t;WdxlNzluz9u#B?%;oX$KnIsVy_Y{a=f^hE7 zx())R`zU`COX~hN6sj+zv)-ztnX;9t#)x(G5+)_fOcxWR13AVk`9m4?hyoBw@T^0E zg+?uy8ubu=GA+NL00a?~J!-=oZxSW`Ul-c6hBcQ z&Pg-h$V$NGo@QXxRS3<|ZQ0rEg%Cj)qBmtYd1PR%p6mN2tu9H5c zOI(&zS)sA#x*}7-%8ykia!bca0KIdJVP%ojIXx&Uf%w(b*kxEr8W62<5JAlP6|>Hd z{c`G6_8|(5H1j+u#4{rKvzy*riE2C~0=(92ZeD2}nOnrV5UcFQjpJ@J~WX&X@S(&Udjw#F_hschCpEZpSeCL!kJ2&+Jp|Br5i;5`m| zasVfS1KpOUX^2CM9AFg)h7j~u{y6WWU_Qwag+{&G?Xokz$Fzo2BPhlbP24skY0+hA zP4J6z_m}Fk>^74(pm0X8y{gLy=ac+6mcY9dBkd@tC_g6GuNr3wWkpbLGk9OihJAJ% zeKJNXdREnLde%Lbl3d3_`VWC+>2|vln~#hxBKd9e<5Y1is7OXOzuE+1txb`}RCWcV ze1+>mfgyrlw&&C$Q0^%)`GQo{Bb{Ru)|0{2`ZlUXAO!U}HV$i%n5-wg~ zRlLBec!5>nY!+I0Rn7ATt;}?25o5(z;l5qe4002ovPDHLkV1nF)|EvH2 literal 0 HcmV?d00001 diff --git a/packages/stacks-email/static/email/social/threads.png b/packages/stacks-email/static/email/social/threads.png new file mode 100644 index 0000000000000000000000000000000000000000..c9c391aaab100d6dbbd0104c55da0230b24a1cf1 GIT binary patch literal 1111 zcmV-d1gQIoP)Qjnkfig{>F#tw^o383fGS`*o!0Ra zeSCae1VIqun@e4qQ;PrZIEgyLWKa?A;{iFZ69w-bVI@fqiu64}OtWLxEb)+}o}f~GI<9p%QQ`2R>Z&%_oUd*D4F-_Ved0xgg^TK-|IM2%S^K*wv0X1q6<5b&S`^=VHKO=cvg60)`s6bOmo0NEB;jhg#OENsv+7BVI>W`r?glYH6U zn7=X44LrM>?^7cOFzL#mG11sUuE&c&dqbq&;;3SAav8w(e zGr2219Q;ilYr+Bf!AKCqdbAn)7MTtAhgr;^Ehb%;Y)1E6EbR65 zwRL!SIHHmSHK~x6sniN`$~3Y%Nc?o17^k!*$Z{9jetUZxLe8TrBeHHWCjYCdTiYkj zkdYhO$O%H#_mY;!U^dUdG0tg&sUi~^+O?-z!w}mktvxv9g>Yb2@_~k*-52Th9F|u$}^#@wkfCMB$KyJwl|Tb8b-Ap!gXrI+ir~vXts_I~%7_XHa+KF0L8AXPl|=q!BIjsZ@VdB5Na18t zAMj(jxO-*6ay0V7;_N?OIP9}inV^FtV}kz^IMg39Jbj>?p*^PDlLqE^hI8QotXX1h#Ja!Q=1blIhg%p} zh-Qp0m&;su0OaPJ9NN$D`P;T8unimz2U2lTwtKx^1VaDt8p7yYcQ6`_646ZV33C%h zZh^IN-8yL-2!eoQ&&u}sd=BwTDehv7J!|vka2|>p>~_1Ef!xm~a)tf6K{%f1@u;iC z6wW~O(G-tF9_#*y+k=-5hr>tV09iLk@n}$Mtlo8nLsL8|avMgDe#}%jB5;`mwO;U; zX$}gth^$hlfUYpc`f4oX-=!D)+2&~-%VL36yKgFx}94oz+# zw`fbwn_#g(${3GSHF-SV+(2lrU2@%r5;Ukap({*{B}g~F%DEsXtxhmT2)vg+X=V zedE!apsgxK`9ud}tGXy2H?*bd#PfKZ2uFAkRODhtuDU5~C79&~a*9W1f~qQx8M&LP zSW7WN5su0&HQYc>@#s`6_*TWNs3^;z65}?gnZ_P$L&| za!udEwkywY1Fh;lx@xx=g(z)@o=$RA?q|XQh9Y;3$6sp0&XYEEi*T8G5j6b+(55Qp zMXn9DcnmB?P3fd{Rg9CIFvV;niwa%udoYlr{6)m8UxO5pVGGuxB`C$fX5)W25P`yJ eD>`)O;Ew+w3ODid4}IhS0000b@?P)s6@%eUz)=OUOM=gCFJ*L7o*vFCXkYAOh^DIOHESPcOQ)_Cz{mF$5arG7I@oQ4Tr-UK~XtD6G_ol1QmwpjJ^|wMzkup$4P=$)FLV?!ZoE65f|hflD8^! z>jT7mIukL$GPz}`d70FTU<&dWY5ZX9n;}x9ahaw`iRtm`R#i5{=RA&N_i|CeHi%WA z1<^&XWDlPzFk5g-mVhC%Ra7HUK$-{K_8$U)RN%^`z8uhGcRv29SaRd#VArztTEgOXugOx;-kQ+LSy+v|aA{ zh~}i@!N^Ir(ug~tG>V8&8ol&WH8Mv+O;^)+d~%Hqz|>k>3&7B)?n(e70gT|Bex=6W z4{&ya(0Cx{NkIFGIgnaB*y7B$Y!>Q3CJ^Z=z3R{lE`&52RkaGq1KT7?sn@Ak>WLAl z6VC1n<=(Y%;;4jL)!r0IG51SuKY3}+LYgPB%+=W(Sf9tqo^}lPOSSxvi>J=doNmjq zQ;aR^OFK1Np0%a@2aUFhZC6R;J6<7js!L$$;tox9Xw+b0V3WM}4#{_Smr06|%@cJ2 zpOreJ6ZJY>UUl&`IM>rgMr*Ok(bR~^PvGBa`k3D_i;XG@Hej<1?iwx`8$#EIWayT` zE_=O`N1hsMt~!0z4pG&F&z1R$YD*rEk{9fm#e68H!EEnj_sfh* z{B$%ah2_5@MzXHH{lawO&iA3R;d(Z6xIcMQSeQXQolVDIHN8))$0F}RA7mO?w7*$; zfv7vaHi5pRH@?goB5*iSYIkmg{8?PKW4!Gm&y;z9u)GXoA)d=-6rhc<0*tCPD44RL z;|g#4?yc#%pu7rgx@h24g<0K=;cE%(T(9@?kPBf3&pvOT(zd>LxJz0KD_pT<6SBbe z!!OQprsO!XlPb1BGb<2si7rClgFrCioqqD#m4-*yXZVM^Zoqw<2%h(Cla4*QRmy(9 z;q^eV>rK>^6Ma0yd+57;+W+aJ7W{iUA4CE#cQZmd=zZ1V-=V3g=axmbw)d|W{j2kT zc=R`ZChwA`niAt=(I|IgH*wjhLRi+96or1^Ygp;dFu2gFTW`it zB)u}e2%K5e*}E9$X@NL}(6_tuZbK-oC`1w2v#o#^E2ajesJ3FfHrEW`2dq{5Yykn^ zXqHr|R``>d3xQdU4)+ewW{zmVDa0j}yTaaGz)%8&t4%-Ur)a@L-uCLhv5)_Bu`CdX z(lXN^D>rSFHX{+~LDx(zs9PFE*HdQ(nz@O8DK1VpRiyc(|rumhFl!Imj6Jghhl00i~L4CyJ15 z{cXbIB;CGDAOS5ngP{e`;Y&OijM@HnKV~D~`nX{Q5Z(ivSs9GmcuWJUlO9CUE&eUA zR!sI@4eVDB&$--iG+`gH&!_NSMT9NjXv9~8jfXp#*_7~qPZQxLIz(;DC=Bw=nfcXt n`~AlJpU2K$eP&&lg>7RYE*kJOBVds0IS+0RU*yPkRs!*3<0Tqvr6m z;Cg{fy#WBqm;VhkH9fZDry!cQo~k0CX8Z;6i9B;s&{6;Z>J#wqY%rfVk&m*8kG`j) zkH5_a2Y`yRle-P1KHUxgpwU+YDi{Qy9aw67fd8Dsa^Pq*f9-$Okx2nGRY;|1Y79?5 zfbNdl)^=awt&e&bGNc=mIAPitkvL$Eg)Y8!l5{c`njHH;tvn~f9pC3UsZ7RLSs z8|}C+5Y4~^&BDS0$|x7|G1i)bl$5l79W;{zfKe!)q(5jPhf>%A>zbR>Dr25N@!KMD zJ26**t@DYbskru@{ePmY+WagDQJxGVnzW~O4|>w6jG2p6eZE9=zai2BoQjQ!F;U~0 z{=98Fx_bap1_FUqfb`Ac3+7}iAh3VCF*EkWvPnp@=E!CXV8M2 zT5fU=f;?a$4>#M`cjFI8E2t~JGq{rIs85Ye<&n5tAmQcuZ8~(2VGYu@43nT|<#w%N zF=9v*H<0iP8ZWl^9YPTPGJl5oIVAGeorG&ZICi$w{U5`{-yFeX3jqMS&_z_<5Wfw?6oMd9$XW-qRy%i%FO9~~l_P1TnjT1YlBbuzt}iT}E4 z3%@$nh`Y%TJ8yfDv-I^G9X4Zb&3gt))@JP@GN}kw2Mg$WqwC2OADvyp44*AR`Euy= zZa>az$NV5MHq%wgEl16giw1q?%16(hrJ#;XT%|qbhtA`3*()?;hnnzw zgbPR&xgK2~z1Zw|sk42k;h^MDEI1M8)2*kPF><5E`H#b?eCg+S%sB5O+I?Wg@b0Pi zmQM$KVPVLv9B39!#ofG6fNdmE=L_~4F{S*dfu%HD*p#@xi-UK6t&#?gxvV}=HIzMa zHJ&oB({i%wQPdADg20E7o~V1dsqhANp|7>$zI6j0U#+=o1wEVz?$|b@tUhDJ3^Jgm zz+1%UqeUf79&lC2<%w);DP#7(3qY@5bmx@|KdL?B6Y7MS>*ue631HX?(PAy zeEEoU*_l9+^7!rZ#%X!(TLU5#D9F)0M(i;$bF;TIVTyt2K~v<@RPXVM_bx}p=Ky4@ zHrSB(&+1-5ZThCE1kGpimT|TzxZb`ha zh4V&E#w}TL2Zfu!ij-YA zLm5Gw4hd5){bvEMI$*yuy{)OFz}}3PhC=7J@_cLh>~YO~FYG)A&}w`Jz`3osCg!9g zm#N5EN|RS!6AnG?#h*;NOVHYj7bY8VG-T!#>G?Sopv>#ds%i956VKwy~ zk)${$+cEcxZ*cVcN#oZ&WC+)0$GQRkibd8mw+sIEpJGAF-h8ALD1UK^@y={g21&vG zc)%@#0mJAhcjEOz*VVElX8GHEN+tf*Zph2rQnW7qHPd>xF4U(<)sH`M{1$|trP9J3 z4%^Yd09>Upv=xRJtr8rJid(WZAMI^ALrp48*-Wo;&p7L*W~rpYP!~9ygJR*)+qx9O zqbX;vo(}Yt**=K&49kLzcS|?UY+-T1&Gfy|1=4!K{>A-zzoLuA=A^F{H7Xw<2X zk%SS-MnpP{nCOd#$$n?X`jkdmRuCGE<4Viv{MxRbGB{f*aJ#{_#G^3~3tCp!9TGDT^k4HaaVR#>V)|dg}lx~d28!NDo+f}evGjI3$M8LVHd-yFIU3W6f zlvYl7Z1yg-JO;{TZ?J#AeHZdqizczc@}G>=q9*32(|}%4y7GM{B!QmrMS@TH0Z;;i zZqIwO37;&zhAIh8$L$YqC6Ze1O68DZ(krO)?pm%!?gb}1f98k`uDjyeMy$@h%&br? zDn)$LplzvZd{z2Q#nYKSVBc`4?|V_|FIj)i16?mlkD}c7#EY?u4s07MrFJUAONUml z7bo8jB?1xhLP3n&Pb*ele#1gHRLFi#(YN z*T5IR#V>Vg>{LxY$#R$WE6ns!?LXMt!`AY9r(?ZxeQiN5e95M&!(ba4cA&)Y; zp}@SoE6N&Aqd%nNoz+kG3y+-K-_R~LtEWnYVpv8O?Ra4*(Wzp0#BxY&jfrCXHPG*y zg}7Lyr+M?7-LLGV65*5MXOK#)0QP;A?Ddvs37zWZH>0?`lMhpouBxi?=)Se>+%8OF ztyfURE}9Ur*5`4&!NhtX6CMYJd*#W!7{S5O01qkdN!_zWH^8gJ*70{dJ!Lx>WeQp+ zy6*BfM*G+XVAxnwgUY_|$RG-TwVR#Q$+vkdC&O|QW7^VR&R7{nrDeOyhn}%&*V(Lt4UNHKbEpC$U zzHD%4*dXN}b;(mq$2lfLeBPB?^>+r?GJF=Bn2O?G-#U%%r-`TP6ie@BbNJp#|i}g*m=oCVISqQ%Vq&(?RMmG477+OCq^ag0ycHO&1m)E-|5WU$!8wMW>oF7v?!&!w=k^KGdkz$IIsCbrm>kL+fZZK`_#FDS}5B8wtx)^S@0@%Ob# zjmTrb6ulOD&Ce1KJlyUr1@@>1qXt8;8N)tI*U@{jDPt~(dL|u5K9jg7xZy7>(fYJZ zCH9_Wb{j1)tUmrU>pCH`9d@(Y4>eq0mR5*$*JX|r{roN=*B*BeoQ9_v-p9O1O@3qPk>F(>_O z5vQ}02We7_M!EDO75qFH?(uCbt8e4Sl>>Me9{l@z2K{qJTFpop`W)d@TkHh=$ra1Q z%os~4!uwUWwSamBzC|($KzwROVkIs4CK3C#MRZjeZ;R_@_c2M#n*{w?^aXt#7+UxC zuyx2LZRg~b%=`F4+yS9h8Qf-NmbZ*yCzm~8d#`YbhKt?Xw3%oe@=c^aY$t^$F5 zXll=08g$|iY2>NI5B`QZtGK1?Lz1tDbA|ONKE-~(_yb;hzU#N7`_PQimxWIqa>|l@ zl=2V z%kpKn`V-GP2Yee=XuInD0|OXO*CzV)O$b>34_B(l9bEB8+=ci2^GtD_ua}MgI(V@hnIUP zIK|tCQ5)btwu5$;-`1_>Z%FQx#j4bC5krzgN*J?`p}Db$;9dEDemhwI<|Rsb(fy?dl1F=gMdv||Z!*WOFC_&6 zi?!J;BR=KtMtvGS9DrCRIwcOMBxd+gbZd+dxmZ!VCy;hzu_Sl@zBO7#d=9$7DbKsk zNHCNRg$QcDllLbJq(e<^&uR3{Gdm;fk`Kgq~g*Fn#?nAFtA2N6k_l z%PC%~b`+ANGtBYs&L%lOL(JF}u<*fopML&Z=KXqu2wDM-1|bK4xjv)KP-c;*Kbl-bl?LlZaIR-vB8}wSw8`tO7o%<7Pm9-| z3hz{YGo6RP8aeU>3g1>P`34>em{hHa#Z$cqE~$;*J#;op=1R(le&%kU_Hky6WvFW! zi>boCD^3*++SyVyhT(SnxvUv(XF9YU=sa3d={{wnngCs@twuT!kLj32EDhU@L*LiF zt9Si@dA^oAd4KH|@B{TLy#?_W_4yNF6Cz`kc zI%Q-lXqOp{t)^^$)fCl;{wx%}OTs`o;0JW%GiMuIrkcQT5o^1S zFcsxW_OF5mKD~Pr1W)m=I+l`+iuG}(rj_Fu(0&@ zJUvGwm6CHfqEKEJwQW+b<@VB_pI!X=s8%NiU!t?*#efd*I%I4=$!*#m;MD4uiA`8{ z(k^aeMJV{&u?7m1=8#~Lm*QM4UP zW+U)qen*PFBS)y0HO(0Qm|VVmBuv(@gE|bn3z)p!)<2a*YbD>`6}6nA&Z zaJ~zumv04!PRTK+JrrmFA0MumqBb-(FA%3bZHU}H^Ojg_56Ox_o96g;5rvPN-pNM? zghW~`@n|}w>OU@01P1{Reu_uYjlTWMJc0Gu&TLr2>o0YI}cn4*EH!W~X z;iEPR(UO?|vRxSGYC^EjrQk9dHS~(RP1E}a2kvAyqg}=F&#gW;q4?Xes47(Zjc!(l zGx%~#E7;yD+Xu@Af36hNXQ^xP%Vd(ODdNSyVx=#I`DT;DxY(ajkk+$b^Rh|5?)lh- zl0UYex;_YJvIseJYg`0nd7N|`rQmV|OGzo&mdeQo0VxL)g)thnkwYA*9L2CPHN8OZ zbm0P9;?$S)&l|@9 zZpU||_aVq?B8V~JNN{Y}gVxN>?-6}CyCl>}OHA_1L(sg>ltO#U4U--V0jq063n zng<%?+CqkD!ewGf_oh`#As|o2dPN=x5H4Kb3{Iwjg_4kv)R|% z8#VgFTj0dLLZ)fDR|Xe1Y2UfRjQFBGaATzWba6D9B~U-rZ$JV2WAqDfj6OPn zQEog>E;J~R_&LaBUnl8P=` zH319O`^O+1Dr;d<{ql<|$i9G{P}iL62%KD~vZxe0`CTF+SA)+p_V(o4o!IQQ&KL5q z-YguCwC9T;4ZC0|Cm$qnS^G3>Ggw`&L{l|uGIj&;>p=vjtzA?3FHyccw?M_e{7zr} zYzCUCJsZ7K9A~0hl{*~{{Vvp?bnqQx3U_^3k`A-Wx1#vDpD+&r2vpZtU(m(&jD_&I zP>YRg1R2`MkjJ8z4sh8$*YS*dY$T&JqW?n^C&)t={H0&b39MzcbLFMCfKF%*;tBQ6@da6f}#Rc#S3s zhBy6QeDDm9SEh+PoHseR{EBS+gNJZx# ztDIq_lvY+EXLwW*V`Jk5Ut>(jK2lkkVd%9Cs8lz;e1Y*nF$QX&DG7%0f-Y+qrr2Pc z)RDt3f<*4BtRV|xF4n&*+ko!|74#$>dHD>vC0+vpD*5=t^zXEuNq>%X$zu&(j-h|q z8i)n|mqosqEd}&C3w>-a=lC`@Hj-mF|1E(m*D*r6y|`a2-y55o#g_0G0Sfr^o*w`t zX!R{mO_Vf-=wOhbV?Q(I@9bELUffMt6&0yQ_<2AH?d2UFPmC~UMMv99t z3+BoBqdp!UY&l+F{9}^0(`T#Q)48TkFr0_B-c^0+NLJBu8o4c0r>hq=k?MUV$^H8) zhPOOnk=^5;Wg=)Q`IWq3JLRd(j7~DhZ)DSw78H8pKxtwTMkK}c2eGh|6K*gn0#Zm3 z#Ht25WtG!~r|*12sZGfLXq-=!9rL}CSoPwcFPY{V(o3-x(9#|$wl$utF2 z@UviG$1t9q4ea)=QlI$wU#Y9CO@_(CT8+(>Ge5wM&pF)epio;lKMuP1FkE2L~W z1aEWiXg+^oEH>5HA=!x+$d!R0{5B}!pgKZ5k@pViR8~S)Y&m_v)Txj@Q;7GIK^%jH$xOO`VT9J@7x2+oSdI)e5Ew{ z_~Zx(hm;`#Ml217Caoq%G$0c&U}aa&D@+C%+Vj%s)2JgKP;$r2dyf_S;+_uQKN@2t zl8ypD3Dr5Gb~BH^!&?l$^*osm{|nba*Nim|x&202yL!U00009a7bBm000&x z000&x0ZCFM@Bjb+0drDELIAGL9O(c600d`2O+f$vv5yP!lmk}I%O zft3m(RbaUSLluOsAUN-d#gpeG%>3Xivs%pT1*Bxk!T){uNCMdV?CjijNwfv-?(V8j zpFVxSCyf{}V#J&bWDeRpL& zADP~W5&H+8m-SzN{Z;eV{{)Xde*5jWm;8Ln&)N+fU4#PLxMrT9-_K7vY1) zWO^d9{~&doMqX|Bz3Z4-?n8*?zy7_F^iK(3GCW@Ig4;_b`j_+w5GGf19ZspgV1-!l zdkv4xk7HnaA2OCdQ+)0LBf|?}bw+@e8No%!2p*d0N&Z;HYAOSEfxO}}o7hXt=+F7R zH~u{??}BX`=k$fa80VIW{y9A`MD>CVsiH!}G>v~Nq(9#&j~ec;*8SjjS_muuR)+Vr zc2gKi3KhiXPoF-W`u071a%UbTnBG)^lZ%*MCNnkbD`lQ*(&>oDfk<_4c`Qz7ARyIq z`da9xr4i_pTDf<{{qsdTAxfd{wBU}qruc3OA9Jh}P=Vd<3J@VAxI`X~>CJNr*^=39 zzHI;34eXTWlg1>Enl}^ef3r2OC{PCyj-cp4?Xb8fvxLfSkA6sZ5hldi_=1~i!msS z<9k|SLs- zSu>nX-*93t2c(T$1fXof?7Q-wV}yCo^m?WJ!a$w zz(8JGi%F>s&+-ELeQ6=)LF78#@W-nJQ#s>uFh*LNJ43wt!Dg1b@I6lZ8u4e80H=lF ze;(9NRShxerA{@)q1wBHaxo4SWkTq_DNW~ScZdzIkoL?ri-YAZyf1vMb|=I_EekW?Hp&Ka~~}h&%j*lzJC3 z@FbvGLB&cjJ1P527J(GesAWJc&rDm3+Lz7DBV||OWC*cHF*|_Ms&#-vh{x$=O6uU} zmqJI{d$8x#UY^(C3!L@`O27+m3UM90H^oWcGKMv!(ai^*gl~muJUI-Yy$uftr861p zCQKmOidwc&rl~E6RuJB6vQJ9^V*xkKI4#9Tw~C0=b1&2ZL>^i9L0E}$ zd%7$8j}SvC1Dj_$hdRIuml<}@gV-&z%L?I1>0$fYa9lM&x+A6DA{m}jCLpO=<%y)# zfhU?KCCpRI##mTeUpF6raPDa_@G- z@Gy`$b_Ce>pz>g4rcA~s_Pw9ITaXG)Xgg#x7ud73`%K_lGdwSr&8iO=`a-S{*$$gT zK&i7`w$=+614tM&0M;3ZDN}?4*Zowv-SYjUiWx$fPoq4lJ^DpohYKmjC)(eZRN()X zv963P+&DC(OQKyphmOPNKxDCVaBbH5+@TU%Yvpbrrc!zDg_c+w7YB_Q%EG3N_2R&* z01V2%z=k2BcO6?1u`)x&(A0+nvZMkZejJg9Dpyq#ukrKnVn*0W0zC1q&=#vbPW5(| zODDi&u9RXs`yww6GrkVxniNXNu6&E&8n2A!xcK+XDgzRRulnUc`nXo=^m>*NQe>%# zu-grFs&HC0tVkROF_r0<;`!Ff?f34B^o2H&I+_Me$vAXl!oLqmS?F@d0psY?oqxG} zy?1(2EhOkLT|m1@L>BXq_70`ZR&)lK-iFZ|LP%xsoQe$a+|`(*I!2(^ua%ePTz;Fdwem`9L_9tY21$2-&V6bD45lyxJ|r;)Kf)J!w7ARNNpN5R~X-6ajhk@U1X zx@5YAot=U*lHSr7g!X#O!335yM?7YD z58mUjFBqK)eIS`4!^+6Bh6fW96rK{k-oXRn>(06MJ@IxmR0tXL#>n%;tJ?=ir&7QA zp$){atC6niy-G1y>8m6(4>PSz0_U-GC<80J@@SsZFQUqv4=LLR7Ek5&a<=8xLM+0{ z?agX$NHV6`#;_A@q&sHZ)>t2!5OfQ*mf?uGOK4-AaqSP4K#3Hu&?U(i4Z(JT{n19Lu;`h9@w zTU2xeMIY0SVW>b^aaR#r)CO*^sZ#E1%79@bL!K4I%iYRJ5uj4;ZN=;at#WTu-%_>K zu-|Q%rnd0N0^U*r#)6%+DSl1}?e+S~GAD#U|7#1b9~}V+G<+VasF}Pee5X?KZSeWE zR3hoJH&8*s5-AJnFM?hdoH<3)V z`^@Csy9U#Au%x4gG9YS$_U<%MFU?7@ob1;>(rJrdsJ{35*H?QTL~W{-{2K3jZ!DOW z{?BPoC_UJx2}we)!~T@}O)dPquQ12IjNWvI2&&_k+eIFt2$@>p40gBi** z+EZ9DaRbnXH<+1j`qQw*c1%tSIL7-*nhjGFshNT_8?9@@!l5Q1Vjk{g32k^OAsQ`h zj~{8r0OJ#z;Y%+Z;|TUGsdtodOK8AD5OPajF6z>-7BWHGJ^S3^5{ zgk#;k)x2@{oZ(eMF{&_)Z1BIN;Xs+;#H}cw>7^!pPdehZ%;@pZYagUM?$^Uyi5s z>o~`SVZVcqfWkCl$6IJo0lv*!Wq3-0mOrlKa-ycs0L_Uz2_n^pfW5%jM|n^D$I6{@ zJ_Ipoy|_0cXt4x6hEQUNHtZxDz|gJO@QUiJZHBiFKM2r@nInv^!~qFs{CQ4!z+$H{ z&g^)=5C{>``ptRbd|_oM*6Uk{T6x8iD$fM;vdTp3w94PyQp)F{0Du_}h8IHYf9Dc>{5*Kv(v~m`o23)xAq>Qts$SS?UXVgk@6Z^ zieR-q!Q@8j(5(8~?TQ>21_N>GaYY5dnR=uL0N;>Ze(xDi=~G^dD&p#3nK&poB|U?}f}JYcHl8IG{;FDf`qi+)2Ut;CHCvWG^_8AnR(i zK1rjZ4BV{`5Is{Kp>7w6^6%B3^EuW|N3l#WRuDH%0-^+D4=yHf?O)7v7qrJP1dZe0 zfB!wHU7^j|f*yFkJ7}f|EGE>)!mHb&G)-3)JuG%|x@u~|Sdt7@Up!kG`&PA%F07Zj z3NmV}=-NcaJ=t5CwYtkh)0`I{tiux|7D)}zW}--W%zI^7+tg&i$&zp)J7pZxdt>xh zr$)gRX)rq{k+u|pCZ!q+!nKxORC9Il@&DdHsry@Fi(Yb{?B|9>dw=}#$5n*(Vr_1< zz-5uB!*Z(N_r2|imboYR-3y1CKq{k$!_NKz%R7h6nqZw?zmk(x#Nb&)-dF2poPNBd zU_e~jyYxT2HL@B_CAO)iIC}NETc7r8NrYOv=Q(-b6`Xi1o>9CTIv=wC(G9y2GNfOh z_)%6x5vSNH2BrV~?k>NMIj2nZAx0(z$#8>b*|pwUK_mv$c@TgzTY zCQN6KQM{TeCYhV}y}YN1dRYvqp4i*RdY(U{j7uiy=|n$jNCqjDe@>ZLtxxSpGCP4|q_K#Cn@IZbANjO=vgi+s1W(?Uo&aFWW!k%l(B5%D zo14hI}IlQLlQ4_Q9VjF;zNoWP=-Uh zi+!+YCn-Uns{#jxj3Lg6YiSFt*8fp)lKZ~q*;9~=D44BJT^>w>t0(v$$6L~#8>{o! zJE<+18uK!kppT}P4?Qfh>^mg@qnZpmcoaa|!(AO7;6n~XjUJEXp&c@5+@Y%_>2RXQ zWPM&+YO?3%r|m&5@|X+LI9wH+n(YB&_&oPKJ|D(W|M&8y3^=A&NP`{SKGi>T7utiV zqbW$%&9|Jf(i}?xdi0xMk_*j$Fv&gbVJKz=rz?~q*a3p?zB+`)!q|?;Y*Xx*calTOm!r1A z#jG7FH6uR>01n&I*-o+jsALQyL!U00009a7bBm000&x z000&x0ZCFM@Bjb+0drDELIAGL9O(c600d`2O+f$vv5yPdJ zh~5lOWOnP)2BtS)z<|9)f0cGwyb+llfvykg?SKIT_7wdY-gtBdO6A`B1JfHYV8EVY z3maaEYOPK?9$SDekMqx;=P+QvfB}1ejTjzbV0r@v40vqVnBfryrZ-@~fX9R&!%HWV zS$=6&v&nI*&g7!{iFR}et&lc-!VEF9x*#~ zLcBpw#2m)sQ(w5r!1M+T*bm^EJ;7fTNpawLB+EbDAM=`B7Js#G;=Qf3zI}(cbDrSh zYZ#5LB*ynGnVxOy5$;~=NmipA#xTdGE?~gkgC&kHL~A&Ybqw(k zVkX=|!c(vEPj^?&J71CMvB<89-|){_$8JW*u$OPU)bd8ThG>3oT2f>agJkr)IE==Whj;5hWmO5Yo_H^uh81U)*-Kjm5H(^{qY zW7@j*0lrTVgPAt2&}o^k09L(<;*Zq=4bG(^hIoW?e}V6MHI08Q3SJMXa3^cT&3>i% z-B*YV&oGn#%;3IBpFKOT+xH%l>D3Yzq|g`K-r{}w?C?^OP6vD)7}b>--3&Gby^9N+ z=xktm7Ftc6T?0)}D3E;DPKdscPgHl+IZ#O-NN!zlwD${Sw=DTS*5jb$}(9 zqWHm4uay3wbyRNAWkl!l%93n>zhM2QY-axY1FB+KT`Fmn%ZQa)8leZPdVt39&vre# zg4A9@OkL^Z5G6a^#h_>&-{CVjhd7s^d8Ga($xU0*+iFbfw6I2KzEhm<6KB3P{10V{ zENDgn|G4^4qurD*5Z$(+)DJ?ri(NQ?nD&zL$mz2}+AEWJsLy@p$TL3PzIf&_A`Z_; zR+o+;zYE4r!1lecBSXz$oSX(bD;QXLfKig{2$v!C=@qs|XuE!$fBM9d9x-YB|0Tpi z!I|X5uw(TysIDn^MK&m)32FBW#}3Q>+4rh(G?ZxM*B1e24Ix9j<%|4tKKGQB_Nm|R z1zeA%x7}SHL zw5M7Re^|-m(V;i()p$T#IByRT4j?K07_Ts({v~HA4?4HBbkr|+CJ4N>>R45;)y+(50uoqQ+25NlG{hG8<`Q z#Xi>6GdmWz@FN&q8@$muVL*G28hxkG7ghEo$JVO;bRSl!SVoCv8yWsJEb#ZRc{0kx>q)US!%I#4(~Xzeu|$GuJAc~H z@eUj42qv-jy!y+>Fs&ppnRoSwza;YXBWo|_NaNBT&} z75oI3t?KxE|1*8CUq5>5KnbIv`b7ZgLAFup@&!v=q4#}iBIw)AohtfDy!-_n2T-X@ z!h(Y333OvBTzgmjVa7taeKE%RQ(O-`2c!wnoZwtM7dN`tdC)kIb-Js6#$d`-`pnX; zAl2&E&U(-EaF58!>zdb+W{9Fsp!V_Zjv{q@naIMXq&Cr+?R!rGCI65SA!S0tNM@YA=$266f}n^hA(k zj0BN_T;No=xRUO0A)a{D`s1J-nzDqucyRjM0bEB9OG@`M2;s@-LgobxDchaHQl-tu zm)&LvNh*|7V68IxWmH_kr8JL4A+1KCDq59xQ-yOooo`HooE zcFc|-*(+=9^?CxW+A9?3E|R*MC~Z8nfk&w2o(((3rufakw5LHBD;>3ldHfX!8*VK+ zLTi@}92GU0LI`bVlc)kG8T~XWZ5ejii!eLhj6bMyB6A_bC3WH*?tHgRM}^+%>|-sE z`6en5dWA|wXQO=%fq1BX(60SJt9K9!3bHL_S3HAvHzzD=5yMakMs1k5&pruwlZ|;I zZCsDlvcMhDWW4sP5K8_w;9AhQPPaoHC+;2 zX0VTtX%Bj>odj*;Wzlxf9I$XibVO3PbBwjU9=aSB)g$~)O`>Y6Rep_ z!{%n}9JjC~$R=W#Ue7@23f1}F$>bNDxHu^(R9>4VR1z<}B)IF8Mb~}T7SU5W@&#sk zVa6zRuWQ&Vlo267@`7_0kR0ck<@J*|%R;D6oeMlchBSPtOkrO^Qj*)Z40?b!T*=mI z?7F#l->Rr9Gl9F&WOm)v%2X@CgKe=cR%?b;S`%H(UYyr*_{6r*SsoK_7lB3~G}{c3 zv-kCSl9y~3p#iE}HH<*_m`IDdZ(nzZgQ%nuZD^^FY8SREsf zEyZj}v5xM7a>G-m&LbmF?B*_^%YWc8;UPBLQV1V1s=mj=k6Mv(=6M}}tOdHLX!_<~C7#IcSd^5V zrNy-R7@~{zLTlgW@|#fo+Uz+De-<=8+gL&;zLEn-%M1ML5@JB79*T0YO+26j-oykb z6M9ew+kIU-(sX4f5+fAK53MrtbaE&#|A>ULj}?jFWPuI5%969E9Oc!Y;@E0ikHfrp zBU<3xxiqy}%Z|e$tN#;ymbMNeB09{yiYRCMpSl8>faATEUL=$rXx9A1J1tC1n$V7I zcPud$te3~T``IVnX!I1KAs&X$A^hG8v(ZUm2>Lv>4R+I!XrRA(yoPkbX5q`Ufs4+q zzhatFgn}MlyVrye3WyBndW?`&`hz}mW`en0VMvIhOz(MU9v~eWK~(Cj9FYk2zHpB3r6YP8;F(YF`dRR0~6TWoms^)>fE_e z6z32Dn}**-(9XyiOqh9ayr6sBuAc)#m)CMN#hZT28*ENJ=_nuE`rne038-JiI7fQ~ z$$eh31v2CBHcMD=z&-`W*FO&t8@eOpzBopRqdkTaGZJO)bHY0M&wr$`sm(zv`2f*` zD6O0Ly9#3*_}#`EwDs{PyD;Jt!Ab|vdBFr@!)i9j76hA5AIM=3RUBJ)I7m^%d5M*N z%Qnb5%vsELUFwwWvmglt?NbZqB*kcSi7Lczt=)QB9TS28_$Zi(2b?x;RGBm)Q?=U! zq8H8$6C-%lS-+z;Y5GnLLGtvNU?_;&u?50`9plarCwm^*UBDyp1jZhLn7k8Qf07;^ z{*D^n+pagc&(el2omH2@v$S^CdpVld?$2EtBe?I3o6K}45?%*(l3mq%Dxq319!Z9F zU%ux{Z6nxxdbP{D3lcM!+(^<3>!WnGQjB63KnqPgVZz_fAtp~?$=8?FL+NghxpKou zyvNkch7%>>cu&eybBKNMj2Ej>bYezic7(DB2eoTKO4UNrS1q6MwI>JO3guqZzuv%U zJ27ccmqL+Qlw;&u!bf|hzV^~ySD#6G7Hr3S=D1ytOS{eEh@!d2F;O|;a8XW+jiF#` zX14+@80j$lI+&PLo%zajPR;{z3i#97;NahNs6q8-QFVDbX^7{XnWSo$l#Q8o^ks3v zk`0UO>VPLi^e+0rM%u%}E9{3L5pwc`3#Z@w-L6XL2lx3JHUSZTQJ%Mq%h3)DW1OCl zB;=~XJcpVRP+an99jJ(3A6UoEf!H7O%Wdsv3I zLv$_}!s;w-rJ*Z;E@eIT@P1Xc{3R)-U5AG+oOIG_V4FArgL_8``;h8D2N5!Y14p&3 z+Scy31bZ|LHiVz?vK{L>Mq1%aF_Q^E=a)s-nNVmBDQibmw&f0QalLBj+yHCG8{d1% zsm()I!_}ktib=BNBJEvu(B6sZs;;hiK-qmdGPfvnE|8hQ|A1se&7g}(3L4x)tF$|X z5r=i?+t9i}3!lO=HptrI1E(L4JXGCihc|n#PzU87>|4jWu6!5gU`;=rfbL!n44y;E z6Sr*;E9K+skwjMZ>^TTV6tMag{zkqI$Z5Uy8C%gFf#xdIXO7qGLeu%cP=W7@7Cg-l z%kX-oJdXo2s!bA)8f2WiY-h;>px&-ah)wQU#kh^f;b+@*^kw)8u5G@y&|;57DH-T{ zdCV=-cn*ECX0MS^BtUdNzB+V(W+G2lteIXf8q8?!9vic(z_i!rduD#|0~?%u-4`TAcvllrXipohtP-+0W>!- za@YW?ueUT#lxCYuTXN3eVcTL;@HEFiVE0@lSTu*EPx#b8J@~t-hdgX(cMJ4+u}S^d zzf { + const language = hljs.getLanguage(lang) + ? lang + : "plaintext"; + const highlighted = hljs.highlight(code, { + language, + }).value; + const escaped = highlighted + .replace(/\{/g, "{") + .replace(/\}/g, "}"); + return `
${escaped}
`; + }, + }, + }), + ], + kit: { + adapter: adapter(), + }, + extensions: [".svelte", ".md"], +}; + +export default config; diff --git a/packages/stacks-email/templates/transactional.ts b/packages/stacks-email/templates/transactional.ts new file mode 100644 index 0000000000..9d7fc881f3 --- /dev/null +++ b/packages/stacks-email/templates/transactional.ts @@ -0,0 +1,138 @@ +import { Button } from "../components/button"; +import { Footer } from "../components/footer"; +import { Headline } from "../components/headline"; +import { Header } from "../components/header"; +import { Section } from "../components/section"; +import { Spacer } from "../components/spacer"; +import { Text } from "../components/text"; +import { Graphic } from "../components/graphic"; + +import { tokens } from "../tokens"; + +import type { EmailTemplateMeta, EmailTemplateModule, MjmlNode } from "../types"; + +export const meta: EmailTemplateMeta = { + slug: "transactional", + defaultVariant: "short", + variants: [ + { + id: "short", + props: { + HEADLINE_TEXT: "Reset your password", + BODY_DEFAULT_MARKDOWN: ` +**Hi [[FIRST_NAME]]**. We received a request to reset your password. Use the button below to choose a new password. + `, + CTA_TEXT: "Reset password", + }, + }, + { + id: "long", + props: { + HEADLINE_TEXT: "Privacy Policy Update", + BODY_DEFAULT_MARKDOWN: ` +We're writing to let you know that we've updated our Privacy Policy, effective **1 January 1970**. + +As part of our ongoing commitment to transparency and data protection, we've made several changes to how we collect, use, and store your personal information. Here's a summary of what's changed: + +**What's new:** +- **Data retention periods** – We've clarified how long we keep your data and the criteria used to determine retention timelines. +- **Third-party sharing** – We've updated our list of trusted partners with whom we may share aggregated, anonymized data to improve our services. +- **Your rights** – We've expanded the section outlining your rights under applicable privacy laws, including the right to access, correct, and delete your data. + +These changes do not affect how we use your data for core service delivery. Your continued use of Stack Overflow after **1 January 1970** constitutes acceptance of the updated policy. + +You can review the full Privacy Policy at any time here: + `, + CTA_TEXT: "View privacy policy", + GRAPHIC_PATH: "/email/spots/SpotLock.png", + }, + }, + ], + tokens: [ + { + token: "FIRST_NAME", + description: "Recipient first name used in the short greeting.", + }, + { + token: "CTA_URL", + description: + "Primary call-to-action URL (password reset for short; policy link for long).", + }, + { + token: "UNSUBSCRIBE_URL", + description: "Recipient-specific unsubscribe URL.", + }, + { + token: "GRAPHIC_PATH", + description: + "Optional spot graphic path for the long variant. Leave empty to hide the graphic.", + }, + ], +}; + +export const document = (variant = meta.variants[0]): MjmlNode => { + const isLongVariant = variant.id === "long"; + const headlineVariant = isLongVariant ? "default" : "highlight"; + + const graphicPath = variant.props.GRAPHIC_PATH?.trim(); + const graphicBlock = isLongVariant + ? graphicPath + ? [ + Graphic("spot", { + imageSrc: graphicPath, + }), + ] + : [] + : []; + + return { + tagName: "mjml", + children: [ + { + tagName: "mj-body", + attributes: { + "background-color": tokens.color.bodyBackground, + }, + children: [ + Spacer("large", { + sectionClass: "bg-page", + }), + Header("transactional"), + Headline(headlineVariant, { + textContent: "{{HEADLINE_TEXT}}", + }), + ...graphicBlock, + Text("body", { + textContent: "{{BODY_CONTENT}}", + }), + Section( + [ + Button("primary", { + href: "[[CTA_URL]]", + align: "left", + text: "{{CTA_TEXT}}", + }), + ], + { + sectionClass: "bg-block", + } + ), + Spacer("large"), + Footer("default", { + unsubscribeUrl: "[[UNSUBSCRIBE_URL]]", + }), + Spacer("large", { + sectionClass: "bg-page", + }), + ], + }, + ], + }; +}; + +const template: EmailTemplateModule = { + meta, + document, +}; + +export default template; diff --git a/packages/stacks-email/tokens.ts b/packages/stacks-email/tokens.ts new file mode 100644 index 0000000000..d8be5db718 --- /dev/null +++ b/packages/stacks-email/tokens.ts @@ -0,0 +1,161 @@ +/** + * Color tokens used for all email component palettes, state surfaces, and links. + */ +const color = { + bodyBackground: "#eee", + background: "#fff", + blockBackground: "#fff", + accent: "#ffcc01", + border: "#d6d9dc", + brand: "#FF5E00", + brandDark: "#201C1D", + brandOffWhite: "#eeeeee", + text: "#211d1e", + textMuted: "#6B6B6B", + textInvert: "#ffffff", + textFooter: "#cdc8c2", + link: "#0000ef", + linkHover: "#5074ef", +} as const; + +/** + * Utility-class background color tokens (bg-[color]). + */ +const backgroundClasses = [ + { name: "brand", value: color.brand }, + { name: "invert", value: color.brandDark }, + { name: "accent", value: color.accent }, + { name: "block", value: color.blockBackground }, + { name: "page", value: color.bodyBackground }, +] as const; + +/** + * Utility-class font color tokens (fc-[color]). + */ +const fontClasses = [ + { name: "text", value: color.text }, + { name: "text-muted", value: color.textMuted }, + { name: "text-invert", value: color.textInvert }, + { name: "text-footer", value: color.textFooter }, +] as const; + +/** + * Typography tokens for all email copy scales and weights. + */ +const font = { + family: "Arial, Helvetica, sans-serif", + sizeBase: "16px", + sizeSm: "14px", + sizeLg: "20px", + sizeXl: "24px", + sizeXxl: "32px", + weightNormal: "400", + weightBold: "700", + lineHeightBase: "1.5", + lineHeightTight: "1.25", +} as const; + +/** + * Spacing tokens for layout rhythm, padding, and separation. + */ +const spacing = { + xs: "4px", + sm: "8px", + md: "16px", + lg: "24px", + xl: "40px", + xxl: "56px", +} as const; + +/** + * Layout tokens for widths and global content padding. + */ +const layout = { + maxWidth: "600px", + containerXPadding: "24px", + containerYPadding: "20px", + logoWidth: "120px", + heroImageWidth: "552px", + socialIconSize: "20px", + cardImageWidth: "252px", + illustrationWidth: "552px", +} as const; + +/** + * Border tokens for shared radius and divider styles. + */ +const border = { + radius: "1000px", + radiusLg: "8px", + style: `1px solid ${color.border}`, + sectionDivider: `1px solid ${color.border}`, +} as const; + +export const tokens = { + color: { + ...color, + backgroundClasses, + fontClasses, + }, + font, + spacing, + layout, + border, +} as const; + +export type Tokens = typeof tokens; + +/** + * Compile targets and token substitutions for each downstream renderer. + */ +export const targets = { + preview: { + tokens: { + FIRST_NAME: "Jane", + BUTTON_LABEL: "Learn more", + BUTTON_URL: "https://example.com", + LINK_URL: "https://example.com/read-more", + PREVIEW_TEXT: "You have a new update from Stack Overflow.", + CARD_ONE_URL: "https://example.com/story-one", + CARD_TWO_URL: "https://example.com/story-two", + FOOTER_REASON: "you subscribed to Stack Overflow updates.", + UNSUBSCRIBE_URL: "https://example.com/unsubscribe", + COMPANY_NAME: "Acme Corp", + }, + }, + dotnet: { + tokens: { + FIRST_NAME: "@Model.FirstName", + BUTTON_LABEL: "@Model.ButtonLabel", + BUTTON_URL: "@Model.ButtonText", + LINK_URL: "@Model.LinkUrl", + PREVIEW_TEXT: "@Model.PreviewText", + CARD_ONE_URL: "@Model.CardOneUrl", + CARD_TWO_URL: "@Model.CardTwoUrl", + FOOTER_REASON: "@Model.FooterReason", + UNSUBSCRIBE_URL: "@Model.UnsubscribeUrl", + COMPANY_NAME: "@Model.CompanyName", + }, + }, + braze: { + tokens: { + FIRST_NAME: "{{${first_name}}}", + BUTTON_LABEL: "{{custom_attribute.${cta_label}}}", + BUTTON_URL: "{{custom_attribute.${cta_url}}}", + LINK_URL: "{{custom_attribute.${link_url}}}", + PREVIEW_TEXT: "{{custom_attribute.${preview_text}}}", + CARD_ONE_URL: "{{custom_attribute.${card_one_url}}}", + CARD_TWO_URL: "{{custom_attribute.${card_two_url}}}", + FOOTER_REASON: "{{custom_attribute.${footer_reason}}}", + UNSUBSCRIBE_URL: "{{${unsubscribe_url}}}", + COMPANY_NAME: "{{custom_attribute.${company_name}}}", + }, + }, +} as const; + +export type CompileTarget = keyof typeof targets; + +export const targetNames = Object.keys(targets) as CompileTarget[]; + +export const isCompileTarget = (value: string): value is CompileTarget => + targetNames.includes(value as CompileTarget); diff --git a/packages/stacks-email/tsconfig.json b/packages/stacks-email/tsconfig.json new file mode 100644 index 0000000000..104691d2d5 --- /dev/null +++ b/packages/stacks-email/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} diff --git a/packages/stacks-email/types.ts b/packages/stacks-email/types.ts new file mode 100644 index 0000000000..a30420ca5d --- /dev/null +++ b/packages/stacks-email/types.ts @@ -0,0 +1,86 @@ +export type MjmlAttributeValue = string | number | boolean; + +export type MjmlNode = { + tagName: string; + attributes?: Record; + children?: MjmlNode[]; + content?: string; +}; + +export type VariantProps = Record; + +export type EmailVariant = { + id: string; + name?: string; + description?: string; + props: VariantProps; +}; + +export type EmailTokenReference = { + token: string; + description: string; +}; + +export type ComponentCategory = + | "Structure" + | "Content" + | "Layout" + | "Primitive" + | "Interactive"; + +export type ComponentVariant = EmailVariant; +export type ComponentTokenReference = EmailTokenReference; +export type ComponentOptionReference = { + argument: string; + type: string; + defaultValue?: string; + defaultValueCode?: boolean; + description: string; +}; + +export type EmailComponentMeta = { + slug: string; + name?: string; + description?: string; + category?: ComponentCategory; + variants: ComponentVariant[]; + defaultVariant?: string; + tokens?: ComponentTokenReference[]; + options?: ComponentOptionReference[]; + htmlExtraction?: { + targetTag: string; + }; +}; + +export type EmailComponentRecord = { + meta: EmailComponentMeta; + source: string; +}; + +export type EmailTemplateCategory = + | "Transactional" + | "Marketing" + | "Onboarding"; + +export type EmailTemplateVariant = EmailVariant; +export type EmailTemplateTokenReference = EmailTokenReference; + +export type EmailTemplateMeta = { + slug: string; + name?: string; + description?: string; + category?: EmailTemplateCategory; + variants: EmailTemplateVariant[]; + defaultVariant?: string; + tokens?: EmailTemplateTokenReference[]; +}; + +export type EmailTemplateRecord = { + meta: EmailTemplateMeta; + source: string; +}; + +export type EmailTemplateModule = { + meta: EmailTemplateMeta; + document: (variant?: EmailTemplateVariant) => MjmlNode; +}; diff --git a/packages/stacks-email/variants.ts b/packages/stacks-email/variants.ts new file mode 100644 index 0000000000..8b038f9ae9 --- /dev/null +++ b/packages/stacks-email/variants.ts @@ -0,0 +1,44 @@ +import { applyTemplateProps } from "./src/lib/pipeline/template"; +import type { + ComponentVariant, + EmailComponentMeta, + EmailComponentRecord, + EmailTemplateMeta, + EmailTemplateRecord, + EmailTemplateVariant, + EmailVariant, +} from "./types"; + +const getBaseVariantById = ( + variants: TVariant[], + defaultVariant: string | undefined, + variantId: string | null | undefined +) => + variants.find((variant) => variant.id === variantId) ?? + variants.find((variant) => variant.id === defaultVariant) ?? + variants[0]; + +export const getVariantById = ( + meta: EmailComponentMeta, + variantId: string | null | undefined +) => getBaseVariantById(meta.variants, meta.defaultVariant, variantId); + +export const getTemplateVariantById = ( + meta: EmailTemplateMeta, + variantId: string | null | undefined +) => getBaseVariantById(meta.variants, meta.defaultVariant, variantId); + +const renderSource = ( + source: string, + variant: ComponentVariant | EmailTemplateVariant +) => applyTemplateProps(source, variant.props); + +export const renderVariantSource = ( + record: EmailComponentRecord, + variant: ComponentVariant +) => renderSource(record.source, variant); + +export const renderTemplateVariantSource = ( + record: EmailTemplateRecord, + variant: EmailTemplateVariant +) => renderSource(record.source, variant); diff --git a/packages/stacks-email/vite.config.ts b/packages/stacks-email/vite.config.ts new file mode 100644 index 0000000000..4a79a4b1d8 --- /dev/null +++ b/packages/stacks-email/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from "@sveltejs/kit/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [sveltekit()], +});