@comet/mail-react 9.0.0-beta.2 → 9.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/lib/__stories__/examples/CustomBackgroundForFooter.stories.js +32 -0
  2. package/lib/__stories__/layout-patterns/AsymmetricTwoColumnLayout.stories.d.ts +6 -0
  3. package/lib/__stories__/layout-patterns/AsymmetricTwoColumnLayout.stories.js +107 -0
  4. package/lib/__stories__/layout-patterns/SymmetricFourColumnLayout.stories.d.ts +4 -0
  5. package/lib/__stories__/layout-patterns/SymmetricFourColumnLayout.stories.js +58 -0
  6. package/lib/__stories__/layout-patterns/SymmetricThreeColumnLayout.stories.d.ts +5 -0
  7. package/lib/__stories__/layout-patterns/SymmetricThreeColumnLayout.stories.js +104 -0
  8. package/lib/__stories__/layout-patterns/SymmetricTwoColumnLayout.stories.d.ts +4 -0
  9. package/lib/__stories__/layout-patterns/SymmetricTwoColumnLayout.stories.js +47 -0
  10. package/lib/blocks/factories/BlocksBlock.d.ts +1 -1
  11. package/lib/blocks/factories/OneOfBlock.d.ts +2 -2
  12. package/lib/blocks/factories/OptionalBlock.d.ts +1 -1
  13. package/lib/blocks/factories/types.d.ts +1 -1
  14. package/lib/client/renderMailHtml.d.ts +1 -1
  15. package/lib/components/section/MjmlSection.d.ts +1 -1
  16. package/lib/components/section/MjmlSection.js +8 -4
  17. package/lib/components/text/HtmlText.d.ts +1 -1
  18. package/lib/components/text/textStyles.js +10 -5
  19. package/lib/components/wrapper/InsideMjmlWrapperContext.d.ts +3 -0
  20. package/lib/components/wrapper/InsideMjmlWrapperContext.js +6 -0
  21. package/lib/components/wrapper/MjmlWrapper.d.ts +7 -0
  22. package/lib/components/wrapper/MjmlWrapper.js +12 -0
  23. package/lib/components/wrapper/__stories__/MjmlWrapper.stories.d.ts +10 -0
  24. package/lib/components/wrapper/__stories__/MjmlWrapper.stories.js +36 -0
  25. package/lib/index.d.ts +3 -1
  26. package/lib/index.js +2 -1
  27. package/lib/server/renderMailHtml.d.ts +1 -1
  28. package/lib/storybook/preview.d.ts +34 -0
  29. package/lib/storybook/preview.js +22 -0
  30. package/lib/theme/responsiveValue.js +2 -1
  31. package/package.json +7 -7
  32. package/lib/__stories__/examples/TextWithImageEmail.stories.js +0 -56
  33. /package/lib/__stories__/examples/{TextWithImageEmail.stories.d.ts → CustomBackgroundForFooter.stories.d.ts} +0 -0
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { MjmlColumn, MjmlSpacer, MjmlTable } from "@faire/mjml-react";
3
+ import { HtmlInlineLink } from "../../components/inlineLink/HtmlInlineLink.js";
4
+ import { MjmlMailRoot } from "../../components/mailRoot/MjmlMailRoot.js";
5
+ import { MjmlSection } from "../../components/section/MjmlSection.js";
6
+ import { HtmlText } from "../../components/text/HtmlText.js";
7
+ import { MjmlText } from "../../components/text/MjmlText.js";
8
+ import { MjmlWrapper } from "../../components/wrapper/MjmlWrapper.js";
9
+ import { createTheme } from "../../theme/createTheme.js";
10
+ const config = {
11
+ title: "Examples/CustomBackgroundForFooter",
12
+ parameters: { mailRoot: false },
13
+ };
14
+ export default config;
15
+ export const Default = {
16
+ render: () => {
17
+ const theme = createTheme({
18
+ text: {
19
+ defaultVariant: "body",
20
+ variants: {
21
+ heading: { fontSize: "22px", lineHeight: "28px", fontWeight: "bold" },
22
+ body: { fontSize: "14px", lineHeight: "20px" },
23
+ legal: { fontSize: "12px", lineHeight: "18px" },
24
+ },
25
+ },
26
+ });
27
+ const footerTheme = structuredClone(theme);
28
+ footerTheme.colors.background.content = "#2d4a6e";
29
+ footerTheme.text.color = "#c8d8e9";
30
+ return (_jsxs(MjmlMailRoot, { theme: theme, children: [_jsx(MjmlSection, { backgroundColor: "#1a1a1a", indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 15 }), _jsx(MjmlText, { color: "#ffffff", fontWeight: "bold", align: "center", children: "Company Name" }), _jsx(MjmlSpacer, { height: 15 })] }) }), _jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 30 }), _jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "This is a notification email" }), _jsx(MjmlText, { bottomSpacing: true, children: "Minima ea distinctio quisquam. Illo reiciendis non officiis consectetur. Ratione perferendis distinctio sapiente est. Dolor consequatur qui excepturi natus." }), _jsx(MjmlText, { children: "Numquam aut voluptas numquam aspernatur. Consequatur quidem omnis dolorem natus quis soluta. Est recusandae delectus sed sed deserunt velit quia. Occaecati vel possimus similique reiciendis possimus iure rerum sit architecto." }), _jsx(MjmlSpacer, { height: 30 })] }) }), _jsxs(MjmlWrapper, { backgroundColor: "#dddddd", children: [_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 20 }), _jsx(MjmlText, { align: "center", bottomSpacing: true, children: "\u00A9 2026 Company Name \u2013 All rights reserved" }), _jsx(MjmlText, { variant: "legal", align: "center", bottomSpacing: true, children: "Legal text, corporis eos et quia. Assumenda eum maiores esse. Voluptas laudantium cupiditate aut repudiandae iste fugiat nam. Quas in debitis. Sed laudantium illum aut occaecati excepturi veniam harum reprehenderit." })] }) }), _jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlTable, { width: "auto", align: "center", children: _jsx("tbody", { children: _jsxs("tr", { children: [_jsx(HtmlText, { align: "center", children: _jsx(HtmlInlineLink, { href: "https://example.com/privacy", children: "Privacy Policy" }) }), _jsx("td", { width: "20px" }), _jsx(HtmlText, { align: "center", children: _jsx(HtmlInlineLink, { href: "https://example.com/imprint", children: "Imprint" }) }), _jsx("td", { width: "20px" }), _jsx(HtmlText, { align: "center", children: _jsx(HtmlInlineLink, { href: "https://example.com", children: "Website" }) })] }) }) }), _jsx(MjmlSpacer, { height: 20 })] }) })] })] }));
31
+ },
32
+ };
@@ -0,0 +1,6 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ declare const config: Meta;
3
+ export default config;
4
+ export declare const SmallColumnLeft: StoryObj;
5
+ export declare const SmallColumnRight: StoryObj;
6
+ export declare const ReversedMobileStackOrder: StoryObj;
@@ -0,0 +1,107 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { MjmlColumn, MjmlImage, MjmlSpacer, MjmlWrapper } from "@faire/mjml-react";
3
+ import { MjmlSection } from "../../components/section/MjmlSection.js";
4
+ import { MjmlText } from "../../components/text/MjmlText.js";
5
+ import { registerStyles } from "../../styles/registerStyles.js";
6
+ import { createTheme } from "../../theme/createTheme.js";
7
+ import { getDefaultFromResponsiveValue } from "../../theme/responsiveValue.js";
8
+ import { css } from "../../utils/css.js";
9
+ const config = {
10
+ title: "Layout Patterns/Asymmetric Two-Column Layout",
11
+ };
12
+ export default config;
13
+ const theme = createTheme({
14
+ text: {
15
+ defaultVariant: "body",
16
+ variants: {
17
+ heading: { fontSize: "22px", lineHeight: "28px", fontWeight: "bold" },
18
+ body: { fontSize: "16px", lineHeight: "24px" },
19
+ },
20
+ },
21
+ });
22
+ const SMALL_COLUMN_WIDTH = 120;
23
+ const COLUMN_GAP = 20;
24
+ const sectionIndent = getDefaultFromResponsiveValue(theme.sizes.contentIndentation);
25
+ const sectionInnerWidth = theme.sizes.bodyWidth - 2 * sectionIndent;
26
+ const fluidColumnWidth = sectionInnerWidth - SMALL_COLUMN_WIDTH;
27
+ registerStyles((theme) => css `
28
+ ${theme.breakpoints.default.belowMediaQuery} {
29
+ .asymmetricLayoutLeft__fluidColumn {
30
+ width: calc(100% - ${SMALL_COLUMN_WIDTH}px) !important;
31
+ max-width: calc(100% - ${SMALL_COLUMN_WIDTH}px) !important;
32
+ }
33
+ }
34
+
35
+ ${theme.breakpoints.mobile.belowMediaQuery} {
36
+ .asymmetricLayoutLeft__fluidColumn {
37
+ width: 100% !important;
38
+ max-width: 100% !important;
39
+ }
40
+
41
+ .asymmetricLayoutLeft__smallColumn {
42
+ margin-bottom: 10px;
43
+ }
44
+
45
+ .asymmetricLayoutLeft__fluidColumn > table > tbody > tr > td {
46
+ padding-left: 0 !important;
47
+ }
48
+ }
49
+ `);
50
+ export const SmallColumnLeft = {
51
+ parameters: { theme },
52
+ render: () => (_jsxs(_Fragment, { children: [_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 30 }), _jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Small column on the left" }), _jsx(MjmlText, { children: "A fixed-width column on the left with a fluid column taking the remaining space. On mobile, columns stack vertically with the small column on top." }), _jsx(MjmlSpacer, { height: 30 })] }) }), _jsxs(MjmlSection, { indent: true, children: [_jsx(MjmlColumn, { className: "asymmetricLayoutLeft__smallColumn", width: `${SMALL_COLUMN_WIDTH}px`, verticalAlign: "middle", children: _jsx(MjmlImage, { src: `https://picsum.photos/seed/1/${SMALL_COLUMN_WIDTH}/150`, alt: "Placeholder", align: "center", width: SMALL_COLUMN_WIDTH }) }), _jsxs(MjmlColumn, { className: "asymmetricLayoutLeft__fluidColumn", width: `${fluidColumnWidth}px`, paddingLeft: `${COLUMN_GAP}px`, verticalAlign: "middle", children: [_jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Fluid column" }), _jsx(MjmlText, { children: "This column takes the remaining width after the fixed-width column. The gap between columns is created via padding on the fluid column's inner edge." })] })] }), _jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlSpacer, { height: 30 }) }) })] })),
53
+ };
54
+ registerStyles((theme) => css `
55
+ ${theme.breakpoints.default.belowMediaQuery} {
56
+ .asymmetricLayoutRight__fluidColumn {
57
+ width: calc(100% - ${SMALL_COLUMN_WIDTH}px) !important;
58
+ max-width: calc(100% - ${SMALL_COLUMN_WIDTH}px) !important;
59
+ }
60
+ }
61
+
62
+ ${theme.breakpoints.mobile.belowMediaQuery} {
63
+ .asymmetricLayoutRight__fluidColumn {
64
+ width: 100% !important;
65
+ max-width: 100% !important;
66
+ }
67
+
68
+ .asymmetricLayoutRight__smallColumn {
69
+ margin-top: 10px;
70
+ }
71
+
72
+ .asymmetricLayoutRight__fluidColumn > table > tbody > tr > td {
73
+ padding-right: 0 !important;
74
+ }
75
+ }
76
+ `);
77
+ export const SmallColumnRight = {
78
+ parameters: { theme },
79
+ render: () => (_jsxs(_Fragment, { children: [_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 30 }), _jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Small column on the right" }), _jsx(MjmlText, { children: "A fluid column on the left with a fixed-width column on the right. On mobile, columns stack vertically with the fluid column on top." }), _jsx(MjmlSpacer, { height: 30 })] }) }), _jsxs(MjmlSection, { indent: true, children: [_jsxs(MjmlColumn, { className: "asymmetricLayoutRight__fluidColumn", width: `${fluidColumnWidth}px`, paddingRight: `${COLUMN_GAP}px`, verticalAlign: "middle", children: [_jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Fluid column" }), _jsx(MjmlText, { children: "This column takes the remaining width after the fixed-width column. The gap between columns is created via padding on the fluid column's inner edge." })] }), _jsx(MjmlColumn, { className: "asymmetricLayoutRight__smallColumn", width: `${SMALL_COLUMN_WIDTH}px`, verticalAlign: "middle", children: _jsx(MjmlImage, { src: `https://picsum.photos/seed/2/${SMALL_COLUMN_WIDTH}/150`, alt: "Placeholder", align: "center", width: SMALL_COLUMN_WIDTH }) })] }), _jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlSpacer, { height: 30 }) }) })] })),
80
+ };
81
+ registerStyles((theme) => css `
82
+ ${theme.breakpoints.default.belowMediaQuery} {
83
+ .asymmetricLayoutRtl__fluidColumn {
84
+ width: calc(100% - ${SMALL_COLUMN_WIDTH}px) !important;
85
+ max-width: calc(100% - ${SMALL_COLUMN_WIDTH}px) !important;
86
+ }
87
+ }
88
+
89
+ ${theme.breakpoints.mobile.belowMediaQuery} {
90
+ .asymmetricLayoutRtl__fluidColumn {
91
+ width: 100% !important;
92
+ max-width: 100% !important;
93
+ }
94
+
95
+ .asymmetricLayoutRtl__smallColumn {
96
+ margin-bottom: 10px;
97
+ }
98
+
99
+ .asymmetricLayoutRtl__fluidColumn > table > tbody > tr > td {
100
+ padding-right: 0 !important;
101
+ }
102
+ }
103
+ `);
104
+ export const ReversedMobileStackOrder = {
105
+ parameters: { theme },
106
+ render: () => (_jsxs(_Fragment, { children: [_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 30 }), _jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Small column on the right, stacks on top on mobile" }), _jsxs(MjmlText, { children: ["Uses ", _jsx("code", { children: "direction=\"rtl\"" }), " on the section to visually place the small column on the right on desktop, while keeping it first in source order so it stacks on top on mobile. A wrapper handles the indentation separately to avoid a 1px line artifact in Outlook."] }), _jsx(MjmlSpacer, { height: 30 })] }) }), _jsx(MjmlWrapper, { padding: `0 ${sectionIndent}px`, backgroundColor: theme.colors.background.content, children: _jsxs(MjmlSection, { direction: "rtl", children: [_jsx(MjmlColumn, { className: "asymmetricLayoutRtl__smallColumn", width: `${SMALL_COLUMN_WIDTH}px`, verticalAlign: "middle", children: _jsx(MjmlImage, { src: `https://picsum.photos/seed/3/${SMALL_COLUMN_WIDTH}/150`, alt: "Placeholder", align: "center", width: SMALL_COLUMN_WIDTH }) }), _jsxs(MjmlColumn, { className: "asymmetricLayoutRtl__fluidColumn", width: `${fluidColumnWidth}px`, paddingRight: `${COLUMN_GAP}px`, verticalAlign: "middle", children: [_jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Fluid column" }), _jsxs(MjmlText, { children: ["Source order: small column first, fluid column second. On desktop, ", _jsx("code", { children: "direction=\"rtl\"" }), " flips the visual order so the fluid column appears on the left."] })] })] }) }), _jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlSpacer, { height: 30 }) }) })] })),
107
+ };
@@ -0,0 +1,4 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ declare const config: Meta;
3
+ export default config;
4
+ export declare const Default: StoryObj;
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { MjmlColumn, MjmlSpacer } from "@faire/mjml-react";
3
+ import { MjmlSection } from "../../components/section/MjmlSection.js";
4
+ import { MjmlText } from "../../components/text/MjmlText.js";
5
+ import { registerStyles } from "../../styles/registerStyles.js";
6
+ import { createTheme } from "../../theme/createTheme.js";
7
+ import { getDefaultFromResponsiveValue } from "../../theme/responsiveValue.js";
8
+ import { css } from "../../utils/css.js";
9
+ const config = {
10
+ title: "Layout Patterns/Symmetric Four-Column Layout",
11
+ };
12
+ export default config;
13
+ const theme = createTheme({
14
+ text: {
15
+ defaultVariant: "body",
16
+ variants: {
17
+ heading: { fontSize: "22px", lineHeight: "28px", fontWeight: "bold" },
18
+ subheading: { fontSize: "16px", lineHeight: "22px", fontWeight: "bold" },
19
+ body: { fontSize: "14px", lineHeight: "20px" },
20
+ },
21
+ },
22
+ });
23
+ registerStyles((theme) => css `
24
+ ${theme.breakpoints.default.belowMediaQuery} {
25
+ .symmetricFourColumnsSection > table > tbody > tr > td {
26
+ display: flex !important;
27
+ flex-direction: column !important;
28
+ gap: 20px !important;
29
+ }
30
+
31
+ .symmetricFourColumnsSection__column {
32
+ flex: none !important;
33
+ width: 100% !important;
34
+ max-width: 100% !important;
35
+ display: block !important;
36
+ }
37
+
38
+ .symmetricFourColumnsSection__column > table > tbody > tr > td {
39
+ padding-left: 0 !important;
40
+ padding-right: 0 !important;
41
+ }
42
+ }
43
+ `);
44
+ export const Default = {
45
+ parameters: { theme },
46
+ render: () => {
47
+ const SymmetricFourColumnSection = () => {
48
+ const columnGap = 20;
49
+ const halfColumnGap = columnGap / 2;
50
+ const availableContentWidth = theme.sizes.bodyWidth - 2 * getDefaultFromResponsiveValue(theme.sizes.contentIndentation);
51
+ const contentWidthPerColumn = (availableContentWidth - 3 * columnGap) / 4;
52
+ const outerColumnWidth = `${((contentWidthPerColumn + halfColumnGap) / availableContentWidth) * 100}%`;
53
+ const innerColumnWidth = `${((contentWidthPerColumn + columnGap) / availableContentWidth) * 100}%`;
54
+ return (_jsxs(MjmlSection, { indent: true, className: "symmetricFourColumnsSection", children: [_jsxs(MjmlColumn, { width: outerColumnWidth, paddingRight: halfColumnGap, className: "symmetricFourColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "First" }), _jsx(MjmlText, { children: "Minima ea distinctio quisquam. Illo reiciendis non officiis consectetur." })] }), _jsxs(MjmlColumn, { width: innerColumnWidth, paddingLeft: halfColumnGap, paddingRight: halfColumnGap, className: "symmetricFourColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Second" }), _jsx(MjmlText, { children: "Numquam aut voluptas numquam aspernatur. Consequatur quidem omnis dolorem natus." })] }), _jsxs(MjmlColumn, { width: innerColumnWidth, paddingLeft: halfColumnGap, paddingRight: halfColumnGap, className: "symmetricFourColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Third" }), _jsx(MjmlText, { children: "Occaecati vel possimus similique reiciendis iure rerum sit architecto." })] }), _jsxs(MjmlColumn, { width: outerColumnWidth, paddingLeft: halfColumnGap, className: "symmetricFourColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Fourth" }), _jsx(MjmlText, { children: "Voluptas laudantium cupiditate aut repudiandae iste fugiat nam quas debitis." })] })] }));
55
+ };
56
+ return (_jsxs(_Fragment, { children: [_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 30 }), _jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Four column layout" }), _jsx(MjmlText, { children: "Four columns follow the same compensation pattern as three columns. The two inner columns each get a wider width to absorb their double-sided padding. On mobile, a flex reset neutralizes the desktop widths so columns can stack equally." }), _jsx(MjmlSpacer, { height: 30 })] }) }), _jsx(SymmetricFourColumnSection, {}), _jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlSpacer, { height: 30 }) }) })] }));
57
+ },
58
+ };
@@ -0,0 +1,5 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ declare const config: Meta;
3
+ export default config;
4
+ export declare const Default: StoryObj;
5
+ export declare const NeverStacks: StoryObj;
@@ -0,0 +1,104 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { MjmlColumn, MjmlSpacer } from "@faire/mjml-react";
3
+ import { MjmlSection } from "../../components/section/MjmlSection.js";
4
+ import { MjmlText } from "../../components/text/MjmlText.js";
5
+ import { registerStyles } from "../../styles/registerStyles.js";
6
+ import { createTheme } from "../../theme/createTheme.js";
7
+ import { getDefaultFromResponsiveValue } from "../../theme/responsiveValue.js";
8
+ import { css } from "../../utils/css.js";
9
+ const config = {
10
+ title: "Layout Patterns/Symmetric Three-Column Layout",
11
+ };
12
+ export default config;
13
+ const theme = createTheme({
14
+ text: {
15
+ defaultVariant: "body",
16
+ variants: {
17
+ heading: { fontSize: "22px", lineHeight: "28px", fontWeight: "bold" },
18
+ subheading: { fontSize: "16px", lineHeight: "22px", fontWeight: "bold" },
19
+ body: { fontSize: "14px", lineHeight: "20px" },
20
+ },
21
+ },
22
+ });
23
+ registerStyles((theme) => css `
24
+ ${theme.breakpoints.default.belowMediaQuery} {
25
+ .symmetricThreeColumnsSection > table > tbody > tr > td {
26
+ display: flex !important;
27
+ gap: 20px !important;
28
+ }
29
+
30
+ .symmetricThreeColumnsSection__column {
31
+ flex: 1 1 0% !important;
32
+ width: auto !important;
33
+ max-width: none !important;
34
+ display: block !important;
35
+ }
36
+
37
+ .symmetricThreeColumnsSection__column > table > tbody > tr > td {
38
+ padding-left: 0 !important;
39
+ padding-right: 0 !important;
40
+ }
41
+ }
42
+
43
+ ${theme.breakpoints.mobile.belowMediaQuery} {
44
+ .symmetricThreeColumnsSection > table > tbody > tr > td {
45
+ flex-direction: column !important;
46
+ }
47
+
48
+ .symmetricThreeColumnsSection__column {
49
+ flex: none !important;
50
+ width: 100% !important;
51
+ max-width: 100% !important;
52
+ }
53
+ }
54
+ `);
55
+ export const Default = {
56
+ parameters: { theme },
57
+ render: () => {
58
+ const SymmetricThreeColumnSection = () => {
59
+ const columnGap = 20;
60
+ const halfColumnGap = columnGap / 2;
61
+ const availableContentWidth = theme.sizes.bodyWidth - 2 * getDefaultFromResponsiveValue(theme.sizes.contentIndentation);
62
+ const contentWidthPerColumn = (availableContentWidth - 2 * columnGap) / 3;
63
+ const outerColumnWidth = `${((contentWidthPerColumn + halfColumnGap) / availableContentWidth) * 100}%`;
64
+ const innerColumnWidth = `${((contentWidthPerColumn + columnGap) / availableContentWidth) * 100}%`;
65
+ return (_jsxs(MjmlSection, { indent: true, className: "symmetricThreeColumnsSection", children: [_jsxs(MjmlColumn, { width: outerColumnWidth, paddingRight: halfColumnGap, className: "symmetricThreeColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "First column" }), _jsx(MjmlText, { children: "Minima ea distinctio quisquam. Illo reiciendis non officiis consectetur. Ratione perferendis distinctio sapiente est." })] }), _jsxs(MjmlColumn, { width: innerColumnWidth, paddingLeft: halfColumnGap, paddingRight: halfColumnGap, className: "symmetricThreeColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Second column" }), _jsx(MjmlText, { children: "Numquam aut voluptas numquam aspernatur. Consequatur quidem omnis dolorem natus quis soluta. Est recusandae delectus." })] }), _jsxs(MjmlColumn, { width: outerColumnWidth, paddingLeft: halfColumnGap, className: "symmetricThreeColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Third column" }), _jsx(MjmlText, { children: "Occaecati vel possimus similique reiciendis iure rerum sit architecto. Voluptas laudantium cupiditate aut repudiandae iste." })] })] }));
66
+ };
67
+ return (_jsxs(_Fragment, { children: [_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 30 }), _jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Three column layout" }), _jsx(MjmlText, { children: "Three columns require explicit width compensation. Inner columns have padding on both sides while outer columns only have it on one side, so inner columns are given a wider width to keep all content areas equal. On mobile, a flex reset neutralizes the desktop widths so columns can size equally and then stack." }), _jsx(MjmlSpacer, { height: 30 })] }) }), _jsx(SymmetricThreeColumnSection, {}), _jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlSpacer, { height: 30 }) }) })] }));
68
+ },
69
+ };
70
+ registerStyles((theme) => css `
71
+ ${theme.breakpoints.default.belowMediaQuery} {
72
+ .neverStackingThreeColumnsSection > table > tbody > tr > td > div {
73
+ display: flex !important;
74
+ gap: 20px !important;
75
+ }
76
+
77
+ .neverStackingThreeColumnsSection__column {
78
+ flex: 1 1 0% !important;
79
+ width: auto !important;
80
+ max-width: none !important;
81
+ display: block !important;
82
+ }
83
+
84
+ .neverStackingThreeColumnsSection__column > table > tbody > tr > td {
85
+ padding-left: 0 !important;
86
+ padding-right: 0 !important;
87
+ }
88
+ }
89
+ `);
90
+ export const NeverStacks = {
91
+ parameters: { theme },
92
+ render: () => {
93
+ const NeverStackingThreeColumnSection = () => {
94
+ const columnGap = 20;
95
+ const halfColumnGap = columnGap / 2;
96
+ const availableContentWidth = theme.sizes.bodyWidth - 2 * getDefaultFromResponsiveValue(theme.sizes.contentIndentation);
97
+ const contentWidthPerColumn = (availableContentWidth - 2 * columnGap) / 3;
98
+ const outerColumnWidth = `${((contentWidthPerColumn + halfColumnGap) / availableContentWidth) * 100}%`;
99
+ const innerColumnWidth = `${((contentWidthPerColumn + columnGap) / availableContentWidth) * 100}%`;
100
+ return (_jsxs(MjmlSection, { indent: true, disableResponsiveBehavior: true, className: "neverStackingThreeColumnsSection", children: [_jsxs(MjmlColumn, { width: outerColumnWidth, paddingRight: halfColumnGap, className: "neverStackingThreeColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Foo" }), _jsx(MjmlText, { children: "Lorem" })] }), _jsxs(MjmlColumn, { width: innerColumnWidth, paddingLeft: halfColumnGap, paddingRight: halfColumnGap, className: "neverStackingThreeColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Bar" }), _jsx(MjmlText, { children: "Ipsum" })] }), _jsxs(MjmlColumn, { width: outerColumnWidth, paddingLeft: halfColumnGap, className: "neverStackingThreeColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Baz" }), _jsx(MjmlText, { children: "Dolor" })] })] }));
101
+ };
102
+ return (_jsxs(_Fragment, { children: [_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 30 }), _jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Three columns that never stack" }), _jsx(MjmlText, { bottomSpacing: true, children: "Same width compensation and flex-reset mechanic as the default three-column layout, without the mobile stacking override so columns stay side-by-side at every viewport." }), _jsxs(MjmlText, { bottomSpacing: true, children: [_jsx("code", { children: "disableResponsiveBehavior" }), " wraps the columns in an ", _jsx("code", { children: "MjmlGroup" }), " internally so MJML's own mobile auto-stack is suppressed even in clients that ignore the flex CSS. Because of that wrapper, the flex reset targets one level deeper (", _jsx("code", { children: "\u2026 > td > div" }), ") than in the default layout."] }), _jsx(MjmlText, { children: "Suitable for short, fixed-value rows \u2014 metrics, numeric summaries, icon strips \u2014 that remain readable even when narrow." }), _jsx(MjmlSpacer, { height: 30 })] }) }), _jsx(NeverStackingThreeColumnSection, {}), _jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlSpacer, { height: 30 }) }) })] }));
103
+ },
104
+ };
@@ -0,0 +1,4 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ declare const config: Meta;
3
+ export default config;
4
+ export declare const Default: StoryObj;
@@ -0,0 +1,47 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { MjmlColumn, MjmlSpacer } from "@faire/mjml-react";
3
+ import { MjmlSection } from "../../components/section/MjmlSection.js";
4
+ import { MjmlText } from "../../components/text/MjmlText.js";
5
+ import { registerStyles } from "../../styles/registerStyles.js";
6
+ import { createTheme } from "../../theme/createTheme.js";
7
+ import { css } from "../../utils/css.js";
8
+ const config = {
9
+ title: "Layout Patterns/Symmetric Two-Column Layout",
10
+ };
11
+ export default config;
12
+ const theme = createTheme({
13
+ text: {
14
+ defaultVariant: "body",
15
+ variants: {
16
+ heading: { fontSize: "22px", lineHeight: "28px", fontWeight: "bold" },
17
+ subheading: { fontSize: "16px", lineHeight: "22px", fontWeight: "bold" },
18
+ body: { fontSize: "14px", lineHeight: "20px" },
19
+ },
20
+ },
21
+ });
22
+ registerStyles((theme) => css `
23
+ ${theme.breakpoints.mobile.belowMediaQuery} {
24
+ .twoColumnsSection__leftColumn > table > tbody > tr > td {
25
+ padding-right: 0 !important;
26
+ }
27
+
28
+ .twoColumnsSection__rightColumn > table > tbody > tr > td {
29
+ padding-left: 0 !important;
30
+ }
31
+
32
+ .twoColumnsSection__leftColumn {
33
+ margin-bottom: 20px;
34
+ }
35
+ }
36
+ `);
37
+ export const Default = {
38
+ parameters: { theme },
39
+ render: () => {
40
+ const TwoColumnsSection = () => {
41
+ const columnGap = 20;
42
+ const halfGap = columnGap / 2;
43
+ return (_jsxs(MjmlSection, { indent: true, className: "twoColumnsSection", children: [_jsxs(MjmlColumn, { className: "twoColumnsSection__leftColumn", paddingRight: halfGap, children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Left column" }), _jsx(MjmlText, { children: "Minima ea distinctio quisquam. Illo reiciendis non officiis consectetur. Ratione perferendis distinctio sapiente est. Dolor consequatur qui excepturi natus." })] }), _jsxs(MjmlColumn, { className: "twoColumnsSection__rightColumn", paddingLeft: halfGap, children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Right column" }), _jsx(MjmlText, { children: "Numquam aut voluptas numquam aspernatur. Consequatur quidem omnis dolorem natus quis soluta. Est recusandae delectus sed sed deserunt velit quia." })] })] }));
44
+ };
45
+ return (_jsxs(_Fragment, { children: [_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 30 }), _jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Two column layout" }), _jsx(MjmlText, { children: "Two columns with equal widths split evenly by MJML. Each column gets half-gap padding on its inner side to create the 20px gap. No explicit width calculation is needed." }), _jsx(MjmlSpacer, { height: 30 })] }) }), _jsx(TwoColumnsSection, {}), _jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlSpacer, { height: 30 }) }) })] }));
46
+ },
47
+ };
@@ -1,4 +1,4 @@
1
- import { type SupportedBlocks } from "./types.js";
1
+ import type { SupportedBlocks } from "./types.js";
2
2
  interface Props {
3
3
  supportedBlocks: SupportedBlocks;
4
4
  data: {
@@ -1,5 +1,5 @@
1
- import { type PropsWithChildren } from "react";
2
- import { type SupportedBlocks } from "./types.js";
1
+ import type { PropsWithChildren } from "react";
2
+ import type { SupportedBlocks } from "./types.js";
3
3
  interface Props extends PropsWithChildren {
4
4
  data: {
5
5
  block?: {
@@ -1,4 +1,4 @@
1
- import { type PropsWithChildren, type ReactNode } from "react";
1
+ import type { PropsWithChildren, ReactNode } from "react";
2
2
  interface Props extends PropsWithChildren {
3
3
  block: (props: any) => ReactNode;
4
4
  data: {
@@ -1,4 +1,4 @@
1
- import { type ReactNode } from "react";
1
+ import type { ReactNode } from "react";
2
2
  export interface SupportedBlocks {
3
3
  [key: string]: (props: any) => ReactNode | undefined;
4
4
  }
@@ -1,5 +1,5 @@
1
1
  import mjml2html from "mjml-browser";
2
- import { type ReactElement } from "react";
2
+ import type { ReactElement } from "react";
3
3
  type MjmlOptions = Parameters<typeof mjml2html>[1];
4
4
  type MjmlWarning = ReturnType<typeof mjml2html>["errors"][number];
5
5
  export declare function renderMailHtml(element: ReactElement, options?: MjmlOptions): {
@@ -11,5 +11,5 @@ export type MjmlSectionProps = IMjmlSectionProps & {
11
11
  group?: Partial<IMjmlGroupProps>;
12
12
  };
13
13
  };
14
- /** A section wrapper for email layouts. Must be a direct child of `MjmlBody`. */
14
+ /** A section wrapper for email layouts. Must be a direct child of `MjmlBody` or `MjmlWrapper`. */
15
15
  export declare function MjmlSection({ children, indent, disableResponsiveBehavior, slotProps, className, ...restProps }: MjmlSectionProps): ReactNode;
@@ -5,12 +5,14 @@ import { registerStyles } from "../../styles/registerStyles.js";
5
5
  import { getDefaultFromResponsiveValue, getResponsiveOverrides } from "../../theme/responsiveValue.js";
6
6
  import { useOptionalTheme } from "../../theme/ThemeProvider.js";
7
7
  import { css } from "../../utils/css.js";
8
- /** A section wrapper for email layouts. Must be a direct child of `MjmlBody`. */
8
+ import { useIsInsideMjmlWrapper } from "../wrapper/InsideMjmlWrapperContext.js";
9
+ /** A section wrapper for email layouts. Must be a direct child of `MjmlBody` or `MjmlWrapper`. */
9
10
  export function MjmlSection({ children, indent, disableResponsiveBehavior, slotProps, className, ...restProps }) {
10
11
  const theme = useOptionalTheme();
12
+ const isInsideWrapper = useIsInsideMjmlWrapper();
11
13
  const indentProps = indent ? getIndentProps(theme) : {};
12
14
  const resolvedClassName = clsx("mjmlSection", indent && "mjmlSection--indented", className);
13
- const themeBackgroundProps = theme ? { backgroundColor: theme.colors.background.content } : {};
15
+ const themeBackgroundProps = theme && !isInsideWrapper ? { backgroundColor: theme.colors.background.content } : {};
14
16
  return (_jsx(BaseMjmlSection, { className: resolvedClassName, ...themeBackgroundProps, ...indentProps, ...restProps, children: disableResponsiveBehavior ? _jsx(MjmlGroup, { ...slotProps?.group, children: children }) : _jsx(_Fragment, { children: children }) }));
15
17
  }
16
18
  function getIndentProps(theme) {
@@ -24,13 +26,15 @@ function getIndentProps(theme) {
24
26
  }
25
27
  registerStyles((theme) => {
26
28
  const overrides = getResponsiveOverrides(theme.sizes.contentIndentation);
27
- if (overrides.length === 0)
29
+ if (overrides.length === 0) {
28
30
  return css ``;
31
+ }
29
32
  return overrides
30
33
  .map((override) => {
31
34
  const breakpoint = theme.breakpoints[override.breakpointKey];
32
- if (!breakpoint)
35
+ if (!breakpoint) {
33
36
  return "";
37
+ }
34
38
  return css `
35
39
  ${breakpoint.belowMediaQuery} {
36
40
  .mjmlSection--indented > table > tbody > tr > td {
@@ -1,4 +1,4 @@
1
- import { type ComponentPropsWithoutRef, type JSX, type ReactNode, type TdHTMLAttributes } from "react";
1
+ import type { ComponentPropsWithoutRef, JSX, ReactNode, TdHTMLAttributes } from "react";
2
2
  import type { VariantName } from "../../theme/themeTypes.js";
3
3
  interface HtmlTextOwnProps {
4
4
  /**
@@ -14,18 +14,21 @@ export const textStyleCssProperties = [
14
14
  /** Generates responsive CSS media queries for text variant overrides. */
15
15
  export function generateResponsiveTextCss(theme, options) {
16
16
  const { variants } = theme.text;
17
- if (!variants)
17
+ if (!variants) {
18
18
  return css ``;
19
+ }
19
20
  const cssChunks = [];
20
21
  for (const [variantName, variantStyles] of Object.entries(variants)) {
21
- if (!variantStyles)
22
+ if (!variantStyles) {
22
23
  continue;
24
+ }
23
25
  const styleOverrides = new Map();
24
26
  const spacingOverrides = new Map();
25
27
  for (const [themeKey, cssProperty] of textStyleCssProperties) {
26
28
  const value = variantStyles[themeKey];
27
- if (value === undefined)
29
+ if (value === undefined) {
28
30
  continue;
31
+ }
29
32
  for (const { breakpointKey, value: breakpointValue } of getResponsiveOverrides(value)) {
30
33
  const declarations = styleOverrides.get(breakpointKey) ?? [];
31
34
  declarations.push(`${cssProperty}: ${String(breakpointValue)} !important`);
@@ -42,8 +45,9 @@ export function generateResponsiveTextCss(theme, options) {
42
45
  }
43
46
  for (const [breakpointKey, declarations] of styleOverrides) {
44
47
  const breakpoint = theme.breakpoints[breakpointKey];
45
- if (!breakpoint)
48
+ if (!breakpoint) {
46
49
  continue;
50
+ }
47
51
  cssChunks.push(css `
48
52
  ${breakpoint.belowMediaQuery} {
49
53
  ${options.styleSelector(variantName)} {
@@ -54,8 +58,9 @@ export function generateResponsiveTextCss(theme, options) {
54
58
  }
55
59
  for (const [breakpointKey, declarations] of spacingOverrides) {
56
60
  const breakpoint = theme.breakpoints[breakpointKey];
57
- if (!breakpoint)
61
+ if (!breakpoint) {
58
62
  continue;
63
+ }
59
64
  cssChunks.push(css `
60
65
  ${breakpoint.belowMediaQuery} {
61
66
  ${options.spacingSelector(variantName)} {
@@ -0,0 +1,3 @@
1
+ export declare const InsideMjmlWrapperContext: import("react").Context<boolean>;
2
+ /** Returns `true` when rendered inside a custom `MjmlWrapper` subtree. Internal use only. */
3
+ export declare function useIsInsideMjmlWrapper(): boolean;
@@ -0,0 +1,6 @@
1
+ import { createContext, useContext } from "react";
2
+ export const InsideMjmlWrapperContext = createContext(false);
3
+ /** Returns `true` when rendered inside a custom `MjmlWrapper` subtree. Internal use only. */
4
+ export function useIsInsideMjmlWrapper() {
5
+ return useContext(InsideMjmlWrapperContext);
6
+ }
@@ -0,0 +1,7 @@
1
+ import { type IMjmlWrapperProps } from "@faire/mjml-react";
2
+ import type { ReactNode } from "react";
3
+ export type MjmlWrapperProps = IMjmlWrapperProps;
4
+ /**
5
+ * A wrapper that groups multiple sections sharing a background. Must be a direct child of MjmlBody.
6
+ */
7
+ export declare function MjmlWrapper({ children, ...restProps }: MjmlWrapperProps): ReactNode;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { MjmlWrapper as BaseMjmlWrapper } from "@faire/mjml-react";
3
+ import { useOptionalTheme } from "../../theme/ThemeProvider.js";
4
+ import { InsideMjmlWrapperContext } from "./InsideMjmlWrapperContext.js";
5
+ /**
6
+ * A wrapper that groups multiple sections sharing a background. Must be a direct child of MjmlBody.
7
+ */
8
+ export function MjmlWrapper({ children, ...restProps }) {
9
+ const theme = useOptionalTheme();
10
+ const themeBackgroundProps = theme ? { backgroundColor: theme.colors.background.content } : {};
11
+ return (_jsx(BaseMjmlWrapper, { ...themeBackgroundProps, ...restProps, children: _jsx(InsideMjmlWrapperContext.Provider, { value: true, children: children }) }));
12
+ }
@@ -0,0 +1,10 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { MjmlWrapper } from "../MjmlWrapper.js";
3
+ type Story = StoryObj<typeof MjmlWrapper>;
4
+ declare const config: Meta<typeof MjmlWrapper>;
5
+ export default config;
6
+ export declare const Primary: Story;
7
+ export declare const ExplicitBackgroundColor: Story;
8
+ export declare const MultipleWrappersWithDifferentBackgrounds: Story;
9
+ export declare const TransparentBackground: Story;
10
+ export declare const FullWidth: Story;
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { MjmlColumn, MjmlSpacer } from "@faire/mjml-react";
3
+ import { MjmlSection } from "../../section/MjmlSection.js";
4
+ import { MjmlText } from "../../text/MjmlText.js";
5
+ import { MjmlWrapper } from "../MjmlWrapper.js";
6
+ const config = {
7
+ title: "Components/MjmlWrapper",
8
+ component: MjmlWrapper,
9
+ tags: ["autodocs"],
10
+ };
11
+ export default config;
12
+ export const Primary = {
13
+ render: (args) => (_jsxs(MjmlWrapper, { ...args, children: [_jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlText, { children: "Section content inside a wrapper with the default theme background" }) }) }), _jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlText, { children: "A second section; both share the wrapper's background" }) }) })] })),
14
+ };
15
+ export const ExplicitBackgroundColor = {
16
+ args: {
17
+ backgroundColor: "#2d4a6e",
18
+ },
19
+ render: (args) => (_jsx(MjmlWrapper, { ...args, children: _jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 20 }), _jsx(MjmlText, { color: "#ffffff", bottomSpacing: true, children: "Explicit wrapper background; sections inside do not paint over it" }), _jsx(MjmlText, { color: "#ffffff", children: "A second section shares the wrapper background" }), _jsx(MjmlSpacer, { height: 20 })] }) }) })),
20
+ };
21
+ export const MultipleWrappersWithDifferentBackgrounds = {
22
+ render: (args) => (_jsxs(_Fragment, { children: [_jsxs(MjmlWrapper, { ...args, backgroundColor: "#8fa5bf", children: [_jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlSpacer, { height: 20 }) }) }), _jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlText, { children: "Section content inside a wrapper with a different background #1" }) }) }), _jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlSpacer, { height: 20 }) }) })] }), _jsx(MjmlWrapper, { ...args, backgroundColor: "#c0c1c4", children: _jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 20 }), _jsx(MjmlText, { children: "Section content inside a wrapper with a different background #2" }), _jsx(MjmlSpacer, { height: 20 })] }) }) }), _jsx(MjmlWrapper, { ...args, backgroundColor: "#ffffff", children: _jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 20 }), _jsx(MjmlText, { children: "Section content inside a wrapper with a different background #3" }), _jsx(MjmlSpacer, { height: 20 })] }) }) })] })),
23
+ };
24
+ export const TransparentBackground = {
25
+ args: {
26
+ backgroundColor: "transparent",
27
+ },
28
+ render: (args) => (_jsx(MjmlWrapper, { ...args, children: _jsx(MjmlSection, { children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 20 }), _jsx(MjmlText, { children: "Transparent wrapper; the body background shows through" }), _jsx(MjmlSpacer, { height: 20 })] }) }) })),
29
+ };
30
+ export const FullWidth = {
31
+ args: {
32
+ fullWidth: true,
33
+ backgroundColor: "#2d4a6e",
34
+ },
35
+ render: (args) => (_jsxs(MjmlWrapper, { ...args, children: [_jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlText, { color: "#ffffff", children: "Full-width wrapper background extends edge-to-edge" }) }) }), _jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlText, { color: "#ffffff", children: "The section inside does not override the wrapper's background" }) }) })] })),
36
+ };
package/lib/index.d.ts CHANGED
@@ -13,6 +13,8 @@ export type { HtmlTextProps } from "./components/text/HtmlText.js";
13
13
  export { HtmlText } from "./components/text/HtmlText.js";
14
14
  export type { MjmlTextProps } from "./components/text/MjmlText.js";
15
15
  export { MjmlText } from "./components/text/MjmlText.js";
16
+ export type { MjmlWrapperProps } from "./components/wrapper/MjmlWrapper.js";
17
+ export { MjmlWrapper } from "./components/wrapper/MjmlWrapper.js";
16
18
  export { registerStyles } from "./styles/registerStyles.js";
17
19
  export { createBreakpoint } from "./theme/createBreakpoint.js";
18
20
  export { createTheme } from "./theme/createTheme.js";
@@ -21,6 +23,6 @@ export { getDefaultFromResponsiveValue, getResponsiveOverrides } from "./theme/r
21
23
  export { ThemeProvider, useTheme } from "./theme/ThemeProvider.js";
22
24
  export type { TextStyles, TextVariants, TextVariantStyles, Theme, ThemeBackgroundColors, ThemeBreakpoint, ThemeBreakpoints, ThemeColors, ThemeSizes, ThemeText, } from "./theme/themeTypes.js";
23
25
  export { css } from "./utils/css.js";
24
- export { Mjml, MjmlAccordion, MjmlAccordionElement, type IMjmlAccordionElementProps as MjmlAccordionElementProps, type IMjmlAccordionProps as MjmlAccordionProps, MjmlAccordionText, type IMjmlAccordionTextProps as MjmlAccordionTextProps, MjmlAccordionTitle, type IMjmlAccordionTitleProps as MjmlAccordionTitleProps, MjmlAll, type IMjmlAllProps as MjmlAllProps, MjmlAttributes, type IMjmlAttributesProps as MjmlAttributesProps, MjmlBody, type IMjmlBodyProps as MjmlBodyProps, MjmlBreakpoint, type IMjmlBreakpointProps as MjmlBreakpointProps, MjmlButton, type IMjmlButtonProps as MjmlButtonProps, MjmlCarousel, MjmlCarouselImage, type IMjmlCarouselImageProps as MjmlCarouselImageProps, type IMjmlCarouselProps as MjmlCarouselProps, MjmlClass, type IMjmlClassProps as MjmlClassProps, MjmlColumn, type IMjmlColumnProps as MjmlColumnProps, MjmlDivider, type IMjmlDividerProps as MjmlDividerProps, MjmlFont, type IMjmlFontProps as MjmlFontProps, MjmlGroup, type IMjmlGroupProps as MjmlGroupProps, MjmlHead, type IMjmlHeadProps as MjmlHeadProps, MjmlHero, type IMjmlHeroProps as MjmlHeroProps, MjmlHtmlAttribute, type IMjmlHtmlAttributeProps as MjmlHtmlAttributeProps, MjmlHtmlAttributes, type IMjmlHtmlAttributesProps as MjmlHtmlAttributesProps, MjmlImage, type IMjmlImageProps as MjmlImageProps, MjmlInclude, type IMjmlIncludeProps as MjmlIncludeProps, MjmlNavbar, MjmlNavbarLink, type IMjmlNavbarLinkProps as MjmlNavbarLinkProps, type IMjmlNavbarProps as MjmlNavbarProps, MjmlPreview, type IMjmlPreviewProps as MjmlPreviewProps, type IMjmlProps as MjmlProps, MjmlRaw, type IMjmlRawProps as MjmlRawProps, MjmlSelector, type IMjmlSelectorProps as MjmlSelectorProps, MjmlSocial, MjmlSocialElement, type IMjmlSocialElementProps as MjmlSocialElementProps, type IMjmlSocialProps as MjmlSocialProps, MjmlSpacer, type IMjmlSpacerProps as MjmlSpacerProps, MjmlStyle, type IMjmlStyleProps as MjmlStyleProps, MjmlTable, type IMjmlTableProps as MjmlTableProps, MjmlTitle, type IMjmlTitleProps as MjmlTitleProps, MjmlWrapper, type IMjmlWrapperProps as MjmlWrapperProps, } from "@faire/mjml-react";
26
+ export { Mjml, MjmlAccordion, MjmlAccordionElement, type IMjmlAccordionElementProps as MjmlAccordionElementProps, type IMjmlAccordionProps as MjmlAccordionProps, MjmlAccordionText, type IMjmlAccordionTextProps as MjmlAccordionTextProps, MjmlAccordionTitle, type IMjmlAccordionTitleProps as MjmlAccordionTitleProps, MjmlAll, type IMjmlAllProps as MjmlAllProps, MjmlAttributes, type IMjmlAttributesProps as MjmlAttributesProps, MjmlBody, type IMjmlBodyProps as MjmlBodyProps, MjmlBreakpoint, type IMjmlBreakpointProps as MjmlBreakpointProps, MjmlButton, type IMjmlButtonProps as MjmlButtonProps, MjmlCarousel, MjmlCarouselImage, type IMjmlCarouselImageProps as MjmlCarouselImageProps, type IMjmlCarouselProps as MjmlCarouselProps, MjmlClass, type IMjmlClassProps as MjmlClassProps, MjmlColumn, type IMjmlColumnProps as MjmlColumnProps, MjmlDivider, type IMjmlDividerProps as MjmlDividerProps, MjmlFont, type IMjmlFontProps as MjmlFontProps, MjmlGroup, type IMjmlGroupProps as MjmlGroupProps, MjmlHead, type IMjmlHeadProps as MjmlHeadProps, MjmlHero, type IMjmlHeroProps as MjmlHeroProps, MjmlHtmlAttribute, type IMjmlHtmlAttributeProps as MjmlHtmlAttributeProps, MjmlHtmlAttributes, type IMjmlHtmlAttributesProps as MjmlHtmlAttributesProps, MjmlImage, type IMjmlImageProps as MjmlImageProps, MjmlInclude, type IMjmlIncludeProps as MjmlIncludeProps, MjmlNavbar, MjmlNavbarLink, type IMjmlNavbarLinkProps as MjmlNavbarLinkProps, type IMjmlNavbarProps as MjmlNavbarProps, MjmlPreview, type IMjmlPreviewProps as MjmlPreviewProps, type IMjmlProps as MjmlProps, MjmlRaw, type IMjmlRawProps as MjmlRawProps, MjmlSelector, type IMjmlSelectorProps as MjmlSelectorProps, MjmlSocial, MjmlSocialElement, type IMjmlSocialElementProps as MjmlSocialElementProps, type IMjmlSocialProps as MjmlSocialProps, MjmlSpacer, type IMjmlSpacerProps as MjmlSpacerProps, MjmlStyle, type IMjmlStyleProps as MjmlStyleProps, MjmlTable, type IMjmlTableProps as MjmlTableProps, MjmlTitle, type IMjmlTitleProps as MjmlTitleProps, } from "@faire/mjml-react";
25
27
  export { MjmlComment, MjmlConditionalComment, MjmlHtml, MjmlTrackingPixel, MjmlYahooStyle } from "@faire/mjml-react/extensions/index.js";
26
28
  export { renderToMjml } from "@faire/mjml-react/utils/renderToMjml.js";
package/lib/index.js CHANGED
@@ -7,12 +7,13 @@ export { MjmlMailRoot } from "./components/mailRoot/MjmlMailRoot.js";
7
7
  export { MjmlSection } from "./components/section/MjmlSection.js";
8
8
  export { HtmlText } from "./components/text/HtmlText.js";
9
9
  export { MjmlText } from "./components/text/MjmlText.js";
10
+ export { MjmlWrapper } from "./components/wrapper/MjmlWrapper.js";
10
11
  export { registerStyles } from "./styles/registerStyles.js";
11
12
  export { createBreakpoint } from "./theme/createBreakpoint.js";
12
13
  export { createTheme } from "./theme/createTheme.js";
13
14
  export { getDefaultFromResponsiveValue, getResponsiveOverrides } from "./theme/responsiveValue.js";
14
15
  export { ThemeProvider, useTheme } from "./theme/ThemeProvider.js";
15
16
  export { css } from "./utils/css.js";
16
- export { Mjml, MjmlAccordion, MjmlAccordionElement, MjmlAccordionText, MjmlAccordionTitle, MjmlAll, MjmlAttributes, MjmlBody, MjmlBreakpoint, MjmlButton, MjmlCarousel, MjmlCarouselImage, MjmlClass, MjmlColumn, MjmlDivider, MjmlFont, MjmlGroup, MjmlHead, MjmlHero, MjmlHtmlAttribute, MjmlHtmlAttributes, MjmlImage, MjmlInclude, MjmlNavbar, MjmlNavbarLink, MjmlPreview, MjmlRaw, MjmlSelector, MjmlSocial, MjmlSocialElement, MjmlSpacer, MjmlStyle, MjmlTable, MjmlTitle, MjmlWrapper, } from "@faire/mjml-react";
17
+ export { Mjml, MjmlAccordion, MjmlAccordionElement, MjmlAccordionText, MjmlAccordionTitle, MjmlAll, MjmlAttributes, MjmlBody, MjmlBreakpoint, MjmlButton, MjmlCarousel, MjmlCarouselImage, MjmlClass, MjmlColumn, MjmlDivider, MjmlFont, MjmlGroup, MjmlHead, MjmlHero, MjmlHtmlAttribute, MjmlHtmlAttributes, MjmlImage, MjmlInclude, MjmlNavbar, MjmlNavbarLink, MjmlPreview, MjmlRaw, MjmlSelector, MjmlSocial, MjmlSocialElement, MjmlSpacer, MjmlStyle, MjmlTable, MjmlTitle, } from "@faire/mjml-react";
17
18
  export { MjmlComment, MjmlConditionalComment, MjmlHtml, MjmlTrackingPixel, MjmlYahooStyle } from "@faire/mjml-react/extensions/index.js";
18
19
  export { renderToMjml } from "@faire/mjml-react/utils/renderToMjml.js";
@@ -1,5 +1,5 @@
1
1
  import mjml2html from "mjml";
2
- import { type ReactElement } from "react";
2
+ import type { ReactElement } from "react";
3
3
  type MjmlOptions = Parameters<typeof mjml2html>[1];
4
4
  type MjmlWarning = ReturnType<typeof mjml2html>["errors"][number];
5
5
  export declare function renderMailHtml(element: ReactElement, options?: MjmlOptions): {
@@ -3,3 +3,37 @@ export declare const decorators: (typeof MailRendererDecorator)[];
3
3
  export declare const initialGlobals: {
4
4
  usePublicImageUrls: boolean;
5
5
  };
6
+ export declare const parameters: {
7
+ viewport: {
8
+ options: {
9
+ mobile: {
10
+ name: string;
11
+ styles: {
12
+ width: string;
13
+ height: string;
14
+ };
15
+ };
16
+ tablet: {
17
+ name: string;
18
+ styles: {
19
+ width: string;
20
+ height: string;
21
+ };
22
+ };
23
+ emailWidth: {
24
+ name: string;
25
+ styles: {
26
+ width: string;
27
+ height: string;
28
+ };
29
+ };
30
+ desktop: {
31
+ name: string;
32
+ styles: {
33
+ width: string;
34
+ height: string;
35
+ };
36
+ };
37
+ };
38
+ };
39
+ };
@@ -3,3 +3,25 @@ export const decorators = [MailRendererDecorator];
3
3
  export const initialGlobals = {
4
4
  usePublicImageUrls: false,
5
5
  };
6
+ export const parameters = {
7
+ viewport: {
8
+ options: {
9
+ mobile: {
10
+ name: "Mobile (375px)",
11
+ styles: { width: "375px", height: "1024px" },
12
+ },
13
+ tablet: {
14
+ name: "Tablet (500px)",
15
+ styles: { width: "500px", height: "1024px" },
16
+ },
17
+ emailWidth: {
18
+ name: "Email max-width (600px)",
19
+ styles: { width: "600px", height: "1024px" },
20
+ },
21
+ desktop: {
22
+ name: "Desktop (768px)",
23
+ styles: { width: "768px", height: "1024px" },
24
+ },
25
+ },
26
+ },
27
+ };
@@ -14,8 +14,9 @@ export function getDefaultFromResponsiveValue(value) {
14
14
  * theme properties.
15
15
  */
16
16
  export function getDefaultOrUndefined(value) {
17
- if (value === undefined)
17
+ if (value === undefined) {
18
18
  return undefined;
19
+ }
19
20
  return getDefaultFromResponsiveValue(value);
20
21
  }
21
22
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comet/mail-react",
3
- "version": "9.0.0-beta.2",
3
+ "version": "9.0.0-beta.3",
4
4
  "description": "Utilities for building HTML emails with React",
5
5
  "license": "BSD-2-Clause",
6
6
  "type": "module",
@@ -14,15 +14,15 @@
14
14
  "lib/*"
15
15
  ],
16
16
  "dependencies": {
17
- "@faire/mjml-react": "^3.5.3",
17
+ "@faire/mjml-react": "^3.5.4",
18
18
  "clsx": "^2.1.1",
19
19
  "mjml": "^4.18.0",
20
20
  "mjml-browser": "^4.18.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@fission-ai/openspec": "^1.2.0",
24
- "@storybook/addon-docs": "^10.3.1",
25
- "@storybook/react-vite": "^10.3.1",
24
+ "@storybook/addon-docs": "^10.3.5",
25
+ "@storybook/react-vite": "^10.3.5",
26
26
  "@types/mjml": "^4.7.4",
27
27
  "@types/mjml-browser": "^4.15.0",
28
28
  "@types/react": "^19.2.10",
@@ -34,11 +34,11 @@
34
34
  "react": "^19.2.4",
35
35
  "react-dom": "^19.2.4",
36
36
  "rimraf": "^6.1.2",
37
- "storybook": "^10.3.1",
37
+ "storybook": "^10.3.5",
38
38
  "typescript": "^5.9.3",
39
39
  "vitest": "^4.0.16",
40
- "@comet/cli": "9.0.0-beta.2",
41
- "@comet/eslint-config": "9.0.0-beta.2"
40
+ "@comet/cli": "9.0.0-beta.3",
41
+ "@comet/eslint-config": "9.0.0-beta.3"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
@@ -1,56 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { MjmlColumn, MjmlImage, MjmlSpacer } from "@faire/mjml-react";
3
- import { MjmlMailRoot } from "../../components/mailRoot/MjmlMailRoot.js";
4
- import { MjmlSection } from "../../components/section/MjmlSection.js";
5
- import { MjmlText } from "../../components/text/MjmlText.js";
6
- import { registerStyles } from "../../styles/registerStyles.js";
7
- import { createTheme } from "../../theme/createTheme.js";
8
- import { getDefaultFromResponsiveValue } from "../../theme/responsiveValue.js";
9
- import { css } from "../../utils/css.js";
10
- const config = {
11
- title: "Examples/TextWithImageEmail",
12
- parameters: { mailRoot: false },
13
- };
14
- export default config;
15
- export const Default = {
16
- render: () => {
17
- const theme = createTheme({
18
- text: {
19
- defaultVariant: "body",
20
- variants: {
21
- heading: { fontSize: "22px", lineHeight: "28px", fontWeight: "bold" },
22
- body: { fontSize: "16px", lineHeight: "24px" },
23
- },
24
- },
25
- });
26
- const IMAGE_WIDTH = 120;
27
- const IMAGE_TEXT_GAP = 20;
28
- registerStyles((theme) => css `
29
- ${theme.breakpoints.default.belowMediaQuery} {
30
- .textWithImageEmail__textColumn {
31
- width: calc(100% - ${IMAGE_WIDTH}px) !important;
32
- max-width: calc(100% - ${IMAGE_WIDTH}px) !important;
33
- }
34
- }
35
-
36
- ${theme.breakpoints.mobile.belowMediaQuery} {
37
- .textWithImageEmail__imageColumn {
38
- margin-bottom: 10px;
39
- }
40
-
41
- .textWithImageEmail__textColumn {
42
- width: 100% !important;
43
- max-width: 100% !important;
44
- }
45
-
46
- .textWithImageEmail__textColumn > table > tbody > tr > td {
47
- padding-left: 0 !important;
48
- }
49
- }
50
- `);
51
- const sectionIndent = getDefaultFromResponsiveValue(theme.sizes.contentIndentation);
52
- const sectionInnerWidth = theme.sizes.bodyWidth - 2 * sectionIndent;
53
- const textColumnWidth = sectionInnerWidth - IMAGE_WIDTH;
54
- return (_jsxs(MjmlMailRoot, { theme: theme, children: [_jsx(MjmlSection, { backgroundColor: "#1a1a1a", indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 15 }), _jsx(MjmlText, { color: "#ffffff", fontWeight: "bold", align: "center", children: "Company Name" }), _jsx(MjmlSpacer, { height: 15 })] }) }), _jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlSpacer, { height: 30 }) }) }), _jsxs(MjmlSection, { indent: true, children: [_jsx(MjmlColumn, { className: "textWithImageEmail__imageColumn", width: `${IMAGE_WIDTH}px`, verticalAlign: "middle", children: _jsx(MjmlImage, { src: `https://picsum.photos/seed/1/${IMAGE_WIDTH}/150`, alt: "Featured image", align: "center", width: IMAGE_WIDTH }) }), _jsxs(MjmlColumn, { className: "textWithImageEmail__textColumn", width: `${textColumnWidth}px`, paddingLeft: `${IMAGE_TEXT_GAP}px`, verticalAlign: "middle", children: [_jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Responsive Text-Image" }), _jsx(MjmlText, { children: "Demonstrates a responsive text-image layout with a fixed-width image column and a text column taking up remaining space." })] })] }), _jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlSpacer, { height: 30 }) }) })] }));
55
- },
56
- };