@comet/mail-react 9.0.0-beta.1 → 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 (69) hide show
  1. package/lib/__stories__/examples/CustomBackgroundForFooter.stories.d.ts +5 -0
  2. package/lib/__stories__/examples/CustomBackgroundForFooter.stories.js +32 -0
  3. package/lib/__stories__/examples/CustomNestedFooterTheme.stories.d.ts +5 -0
  4. package/lib/__stories__/examples/CustomNestedFooterTheme.stories.js +32 -0
  5. package/lib/__stories__/examples/NotificationEmail.stories.d.ts +5 -0
  6. package/lib/__stories__/examples/NotificationEmail.stories.js +25 -0
  7. package/lib/__stories__/layout-patterns/AsymmetricTwoColumnLayout.stories.d.ts +6 -0
  8. package/lib/__stories__/layout-patterns/AsymmetricTwoColumnLayout.stories.js +107 -0
  9. package/lib/__stories__/layout-patterns/SymmetricFourColumnLayout.stories.d.ts +4 -0
  10. package/lib/__stories__/layout-patterns/SymmetricFourColumnLayout.stories.js +58 -0
  11. package/lib/__stories__/layout-patterns/SymmetricThreeColumnLayout.stories.d.ts +5 -0
  12. package/lib/__stories__/layout-patterns/SymmetricThreeColumnLayout.stories.js +104 -0
  13. package/lib/__stories__/layout-patterns/SymmetricTwoColumnLayout.stories.d.ts +4 -0
  14. package/lib/__stories__/layout-patterns/SymmetricTwoColumnLayout.stories.js +47 -0
  15. package/lib/blocks/factories/BlocksBlock.d.ts +1 -1
  16. package/lib/blocks/factories/OneOfBlock.d.ts +2 -2
  17. package/lib/blocks/factories/OptionalBlock.d.ts +1 -1
  18. package/lib/blocks/factories/types.d.ts +1 -1
  19. package/lib/client/renderMailHtml.d.ts +1 -1
  20. package/lib/components/inlineLink/HtmlInlineLink.d.ts +10 -0
  21. package/lib/components/inlineLink/HtmlInlineLink.js +35 -0
  22. package/lib/components/inlineLink/__stories__/HtmlInlineLink.stories.d.ts +12 -0
  23. package/lib/components/inlineLink/__stories__/HtmlInlineLink.stories.js +56 -0
  24. package/lib/components/mailRoot/MjmlMailRoot.js +1 -1
  25. package/lib/components/mailRoot/__stories__/MjmlMailRoot.stories.d.ts +1 -0
  26. package/lib/components/mailRoot/__stories__/MjmlMailRoot.stories.js +6 -1
  27. package/lib/components/section/MjmlSection.d.ts +2 -2
  28. package/lib/components/section/MjmlSection.js +10 -5
  29. package/lib/components/section/__stories__/MjmlSection.stories.d.ts +1 -0
  30. package/lib/components/section/__stories__/MjmlSection.stories.js +6 -0
  31. package/lib/components/text/HtmlText.d.ts +48 -0
  32. package/lib/components/text/HtmlText.js +47 -0
  33. package/lib/components/text/MjmlText.d.ts +37 -0
  34. package/lib/components/text/MjmlText.js +65 -0
  35. package/lib/components/text/OutlookTextStyleContext.d.ts +9 -0
  36. package/lib/components/text/OutlookTextStyleContext.js +10 -0
  37. package/lib/components/text/__stories__/HtmlText.stories.d.ts +12 -0
  38. package/lib/components/text/__stories__/HtmlText.stories.js +77 -0
  39. package/lib/components/text/__stories__/MjmlText.stories.d.ts +10 -0
  40. package/lib/components/text/__stories__/MjmlText.stories.js +71 -0
  41. package/lib/components/text/__tests__/HtmlText.test.d.ts +1 -0
  42. package/lib/components/text/__tests__/HtmlText.test.js +157 -0
  43. package/lib/components/text/__tests__/MjmlText.test.d.ts +1 -0
  44. package/lib/components/text/__tests__/MjmlText.test.js +112 -0
  45. package/lib/components/text/textStyles.d.ts +12 -0
  46. package/lib/components/text/textStyles.js +74 -0
  47. package/lib/components/wrapper/InsideMjmlWrapperContext.d.ts +3 -0
  48. package/lib/components/wrapper/InsideMjmlWrapperContext.js +6 -0
  49. package/lib/components/wrapper/MjmlWrapper.d.ts +7 -0
  50. package/lib/components/wrapper/MjmlWrapper.js +12 -0
  51. package/lib/components/wrapper/__stories__/MjmlWrapper.stories.d.ts +10 -0
  52. package/lib/components/wrapper/__stories__/MjmlWrapper.stories.js +36 -0
  53. package/lib/index.d.ts +10 -2
  54. package/lib/index.js +5 -1
  55. package/lib/server/renderMailHtml.d.ts +1 -1
  56. package/lib/storybook/MailRendererDecorator.d.ts +7 -1
  57. package/lib/storybook/MailRendererDecorator.js +2 -2
  58. package/lib/storybook/preview.d.ts +34 -0
  59. package/lib/storybook/preview.js +22 -0
  60. package/lib/theme/__stories__/ThemeProvider.stories.js +2 -2
  61. package/lib/theme/createTheme.d.ts +5 -1
  62. package/lib/theme/createTheme.js +6 -0
  63. package/lib/theme/createTheme.test.js +32 -0
  64. package/lib/theme/defaultTheme.js +12 -0
  65. package/lib/theme/responsiveValue.d.ts +6 -0
  66. package/lib/theme/responsiveValue.js +11 -0
  67. package/lib/theme/responsiveValue.test.js +18 -1
  68. package/lib/theme/themeTypes.d.ts +59 -0
  69. package/package.json +11 -8
@@ -0,0 +1,157 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { renderToStaticMarkup } from "react-dom/server";
3
+ import { describe, expect, it } from "vitest";
4
+ import { createTheme } from "../../../theme/createTheme.js";
5
+ import { ThemeProvider } from "../../../theme/ThemeProvider.js";
6
+ import { HtmlText } from "../HtmlText.js";
7
+ // Type-level tests — validated by tsc, not at runtime.
8
+ // An unused @ts-expect-error directive causes a compile error if the overloads
9
+ // stop rejecting the invalid usage, so these act as regression guards.
10
+ // @ts-expect-error href is not valid on the default <td>
11
+ void (_jsx(HtmlText, { href: "/foo", children: "text" }));
12
+ void ((
13
+ // @ts-expect-error colSpan is not valid on <a>
14
+ _jsx(HtmlText, { element: "a", colSpan: 2, children: "text" })));
15
+ // Own props are always accepted regardless of element — no error expected
16
+ void (_jsx(HtmlText, { element: "div", variant: "heading", bottomSpacing: true, children: "text" }));
17
+ void (_jsx(HtmlText, { variant: "heading", bottomSpacing: true, children: "text" }));
18
+ function renderHtmlText(element) {
19
+ return renderToStaticMarkup(element);
20
+ }
21
+ describe("HtmlText", () => {
22
+ it("renders a <td> with base theme styles as inline styles", () => {
23
+ const theme = createTheme();
24
+ const html = renderHtmlText(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlText, { children: "Hello" }) }));
25
+ expect(html).toContain("<td");
26
+ expect(html).toContain("Hello");
27
+ expect(html).toContain("font-family:Arial, sans-serif");
28
+ expect(html).toContain("font-size:16px");
29
+ expect(html).toContain("line-height:20px");
30
+ });
31
+ it("applies variant styles over base styles", () => {
32
+ const theme = createTheme({
33
+ text: {
34
+ variants: {
35
+ heading: { fontSize: { default: "32px", mobile: "24px" }, fontWeight: 700 },
36
+ },
37
+ },
38
+ });
39
+ const html = renderHtmlText(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlText, { variant: "heading", children: "Title" }) }));
40
+ expect(html).toContain("font-size:32px");
41
+ expect(html).toContain("font-weight:700");
42
+ });
43
+ it("inherits base styles not overridden by variant", () => {
44
+ const theme = createTheme({
45
+ text: {
46
+ fontFamily: "Georgia, serif",
47
+ variants: {
48
+ heading: { fontSize: "32px" },
49
+ },
50
+ },
51
+ });
52
+ const html = renderHtmlText(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlText, { variant: "heading", children: "Title" }) }));
53
+ expect(html).toContain("font-family:Georgia, serif");
54
+ expect(html).toContain("font-size:32px");
55
+ });
56
+ it("applies defaultVariant when no variant prop is specified", () => {
57
+ const theme = createTheme({
58
+ text: {
59
+ defaultVariant: "body",
60
+ variants: {
61
+ body: { fontSize: "14px" },
62
+ },
63
+ },
64
+ });
65
+ const html = renderHtmlText(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlText, { children: "Text" }) }));
66
+ expect(html).toContain("font-size:14px");
67
+ });
68
+ it("applies bottomSpacing as padding-bottom", () => {
69
+ const theme = createTheme({ text: { bottomSpacing: "16px" } });
70
+ const html = renderHtmlText(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlText, { bottomSpacing: true, children: "Text" }) }));
71
+ expect(html).toContain("padding-bottom:16px");
72
+ });
73
+ it("uses variant bottomSpacing over base", () => {
74
+ const theme = createTheme({
75
+ text: {
76
+ bottomSpacing: "16px",
77
+ variants: {
78
+ heading: { bottomSpacing: { default: "24px", mobile: "16px" } },
79
+ },
80
+ },
81
+ });
82
+ const html = renderHtmlText(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlText, { variant: "heading", bottomSpacing: true, children: "Title" }) }));
83
+ expect(html).toContain("padding-bottom:24px");
84
+ });
85
+ it("does not apply padding-bottom when bottomSpacing is not set", () => {
86
+ const theme = createTheme({ text: { bottomSpacing: "16px" } });
87
+ const html = renderHtmlText(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlText, { children: "Text" }) }));
88
+ expect(html).not.toContain("padding-bottom");
89
+ });
90
+ it("always applies the htmlText base class", () => {
91
+ const theme = createTheme();
92
+ const html = renderHtmlText(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlText, { children: "Hello" }) }));
93
+ expect(html).toContain('class="htmlText"');
94
+ });
95
+ it("applies variant modifier class", () => {
96
+ const theme = createTheme({
97
+ text: { variants: { heading: { fontSize: "32px" } } },
98
+ });
99
+ const html = renderHtmlText(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlText, { variant: "heading", children: "Title" }) }));
100
+ expect(html).toContain("htmlText--heading");
101
+ });
102
+ it("applies bottomSpacing modifier class", () => {
103
+ const theme = createTheme();
104
+ const html = renderHtmlText(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlText, { bottomSpacing: true, children: "Text" }) }));
105
+ expect(html).toContain("htmlText--bottomSpacing");
106
+ });
107
+ it("merges consumer className", () => {
108
+ const theme = createTheme();
109
+ const html = renderHtmlText(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlText, { className: "custom", children: "Text" }) }));
110
+ expect(html).toContain("htmlText");
111
+ expect(html).toContain("custom");
112
+ });
113
+ it("lets user style prop override theme styles", () => {
114
+ const theme = createTheme({ text: { fontFamily: "Arial, sans-serif" } });
115
+ const html = renderHtmlText(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlText, { style: { fontFamily: "Georgia" }, children: "Hello" }) }));
116
+ expect(html).toContain("font-family:Georgia");
117
+ });
118
+ it("forwards standard td attributes", () => {
119
+ const theme = createTheme();
120
+ const html = renderHtmlText(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlText, { colSpan: 2, align: "center", children: "Hello" }) }));
121
+ expect(html).toContain('colSpan="2"');
122
+ expect(html).toContain('align="center"');
123
+ });
124
+ it("renders a <div> when element is 'div'", () => {
125
+ const theme = createTheme();
126
+ const html = renderHtmlText(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlText, { element: "div", children: "Hello" }) }));
127
+ expect(html).toContain("<div");
128
+ expect(html).not.toContain("<td");
129
+ expect(html).toContain("Hello");
130
+ expect(html).toContain("font-family:Arial, sans-serif");
131
+ });
132
+ it("renders an <a> with href when element is 'a'", () => {
133
+ const theme = createTheme();
134
+ const html = renderHtmlText(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlText, { element: "a", href: "/link", children: "Click" }) }));
135
+ expect(html).toContain("<a");
136
+ expect(html).not.toContain("<td");
137
+ expect(html).toContain('href="/link"');
138
+ expect(html).toContain("Click");
139
+ });
140
+ it("applies theme styles and CSS classes to non-td elements", () => {
141
+ const theme = createTheme({
142
+ text: {
143
+ variants: {
144
+ heading: { fontSize: "32px", fontWeight: 700 },
145
+ },
146
+ },
147
+ });
148
+ const html = renderHtmlText(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlText, { element: "div", variant: "heading", bottomSpacing: true, className: "custom", children: "Title" }) }));
149
+ expect(html).toContain("<div");
150
+ expect(html).toContain("font-size:32px");
151
+ expect(html).toContain("font-weight:700");
152
+ expect(html).toContain("htmlText");
153
+ expect(html).toContain("htmlText--heading");
154
+ expect(html).toContain("htmlText--bottomSpacing");
155
+ expect(html).toContain("custom");
156
+ });
157
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,112 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { MjmlColumn } from "@faire/mjml-react";
3
+ import { describe, expect, it } from "vitest";
4
+ import { renderMailHtml } from "../../../server/renderMailHtml.js";
5
+ import { createTheme } from "../../../theme/createTheme.js";
6
+ import { MjmlMailRoot } from "../../mailRoot/MjmlMailRoot.js";
7
+ import { MjmlSection } from "../../section/MjmlSection.js";
8
+ import { generateTextStyles, MjmlText } from "../MjmlText.js";
9
+ describe("MjmlText integration", () => {
10
+ // Full render pipeline: mjml2html must report no errors for this MjmlText + theme setup.
11
+ it("produces no MJML warnings with a variant theme", () => {
12
+ const theme = createTheme({
13
+ text: {
14
+ variants: {
15
+ heading: {
16
+ fontSize: { default: "32px", mobile: "24px" },
17
+ fontWeight: 700,
18
+ lineHeight: { default: "40px", mobile: "30px" },
19
+ },
20
+ body: { fontSize: "14px", lineHeight: "22px" },
21
+ },
22
+ },
23
+ });
24
+ const { mjmlWarnings } = renderMailHtml(_jsx(MjmlMailRoot, { theme: theme, children: _jsx(MjmlSection, { children: _jsxs(MjmlColumn, { children: [_jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Heading" }), _jsx(MjmlText, { variant: "body", children: "Body text" }), _jsx(MjmlText, { children: "Base text" })] }) }) }));
25
+ expect(mjmlWarnings).toEqual([]);
26
+ });
27
+ });
28
+ describe("generateTextStyles", () => {
29
+ it("returns empty CSS when no variants are defined", () => {
30
+ const theme = createTheme();
31
+ const result = generateTextStyles(theme);
32
+ expect(result).toBe("");
33
+ });
34
+ it("emits style overrides with correct selector for a variant", () => {
35
+ const theme = createTheme({
36
+ text: {
37
+ variants: {
38
+ heading: {
39
+ fontSize: { default: "32px", mobile: "24px" },
40
+ },
41
+ },
42
+ },
43
+ });
44
+ const result = generateTextStyles(theme);
45
+ expect(result).toContain(".mjmlText--heading > div");
46
+ expect(result).toContain("font-size: 24px !important");
47
+ });
48
+ it("groups multiple properties into a single media query per breakpoint", () => {
49
+ const theme = createTheme({
50
+ text: {
51
+ variants: {
52
+ heading: {
53
+ fontSize: { default: "32px", mobile: "24px" },
54
+ lineHeight: { default: "40px", mobile: "30px" },
55
+ },
56
+ },
57
+ },
58
+ });
59
+ const result = generateTextStyles(theme);
60
+ const mobileMediaQuery = `@media (max-width: ${theme.breakpoints.mobile.value - 1}px)`;
61
+ expect(result).toContain(mobileMediaQuery);
62
+ // Both declarations should appear within the same media query block
63
+ const mediaBlockStart = result.indexOf(mobileMediaQuery);
64
+ const blockContent = result.slice(mediaBlockStart);
65
+ expect(blockContent).toContain("font-size: 24px !important");
66
+ expect(blockContent).toContain("line-height: 30px !important");
67
+ });
68
+ it("uses compound selector for responsive bottomSpacing", () => {
69
+ const theme = createTheme({
70
+ text: {
71
+ variants: {
72
+ heading: {
73
+ bottomSpacing: { default: "24px", mobile: "16px" },
74
+ },
75
+ },
76
+ },
77
+ });
78
+ const result = generateTextStyles(theme);
79
+ expect(result).toContain(".mjmlText--bottomSpacing.mjmlText--heading");
80
+ expect(result).toContain("padding-bottom: 16px !important");
81
+ });
82
+ it("emits no media queries for non-responsive variant", () => {
83
+ const theme = createTheme({
84
+ text: {
85
+ variants: {
86
+ body: { fontSize: "14px", lineHeight: "22px" },
87
+ },
88
+ },
89
+ });
90
+ const result = generateTextStyles(theme);
91
+ expect(result).not.toContain("@media");
92
+ });
93
+ it("keeps style and bottomSpacing overrides separate", () => {
94
+ const theme = createTheme({
95
+ text: {
96
+ variants: {
97
+ heading: {
98
+ fontSize: { default: "32px", mobile: "24px" },
99
+ bottomSpacing: { default: "24px", mobile: "16px" },
100
+ },
101
+ },
102
+ },
103
+ });
104
+ const result = generateTextStyles(theme);
105
+ // Style overrides use "> div" selector
106
+ expect(result).toContain(".mjmlText--heading > div");
107
+ expect(result).toContain("font-size: 24px !important");
108
+ // Spacing overrides use compound selector (no "> div")
109
+ expect(result).toContain(".mjmlText--bottomSpacing.mjmlText--heading");
110
+ expect(result).toContain("padding-bottom: 16px !important");
111
+ });
112
+ });
@@ -0,0 +1,12 @@
1
+ import type { TextVariantStyles, Theme } from "../../theme/themeTypes.js";
2
+ type StylePropertyKey = Exclude<keyof TextVariantStyles, "bottomSpacing">;
3
+ export declare const textStyleCssProperties: ReadonlyArray<[StylePropertyKey, string]>;
4
+ interface GenerateResponsiveTextCssOptions {
5
+ /** Selector for text style overrides, given a variant name. */
6
+ styleSelector: (variantName: string) => string;
7
+ /** Selector for bottomSpacing overrides, given a variant name. */
8
+ spacingSelector: (variantName: string) => string;
9
+ }
10
+ /** Generates responsive CSS media queries for text variant overrides. */
11
+ export declare function generateResponsiveTextCss(theme: Theme, options: GenerateResponsiveTextCssOptions): string;
12
+ export {};
@@ -0,0 +1,74 @@
1
+ import { getResponsiveOverrides } from "../../theme/responsiveValue.js";
2
+ import { css } from "../../utils/css.js";
3
+ export const textStyleCssProperties = [
4
+ ["fontFamily", "font-family"],
5
+ ["fontSize", "font-size"],
6
+ ["fontWeight", "font-weight"],
7
+ ["fontStyle", "font-style"],
8
+ ["lineHeight", "line-height"],
9
+ ["letterSpacing", "letter-spacing"],
10
+ ["textDecoration", "text-decoration"],
11
+ ["textTransform", "text-transform"],
12
+ ["color", "color"],
13
+ ];
14
+ /** Generates responsive CSS media queries for text variant overrides. */
15
+ export function generateResponsiveTextCss(theme, options) {
16
+ const { variants } = theme.text;
17
+ if (!variants) {
18
+ return css ``;
19
+ }
20
+ const cssChunks = [];
21
+ for (const [variantName, variantStyles] of Object.entries(variants)) {
22
+ if (!variantStyles) {
23
+ continue;
24
+ }
25
+ const styleOverrides = new Map();
26
+ const spacingOverrides = new Map();
27
+ for (const [themeKey, cssProperty] of textStyleCssProperties) {
28
+ const value = variantStyles[themeKey];
29
+ if (value === undefined) {
30
+ continue;
31
+ }
32
+ for (const { breakpointKey, value: breakpointValue } of getResponsiveOverrides(value)) {
33
+ const declarations = styleOverrides.get(breakpointKey) ?? [];
34
+ declarations.push(`${cssProperty}: ${String(breakpointValue)} !important`);
35
+ styleOverrides.set(breakpointKey, declarations);
36
+ }
37
+ }
38
+ const bottomSpacingValue = variantStyles.bottomSpacing;
39
+ if (bottomSpacingValue !== undefined) {
40
+ for (const { breakpointKey, value: breakpointValue } of getResponsiveOverrides(bottomSpacingValue)) {
41
+ const declarations = spacingOverrides.get(breakpointKey) ?? [];
42
+ declarations.push(`padding-bottom: ${String(breakpointValue)} !important`);
43
+ spacingOverrides.set(breakpointKey, declarations);
44
+ }
45
+ }
46
+ for (const [breakpointKey, declarations] of styleOverrides) {
47
+ const breakpoint = theme.breakpoints[breakpointKey];
48
+ if (!breakpoint) {
49
+ continue;
50
+ }
51
+ cssChunks.push(css `
52
+ ${breakpoint.belowMediaQuery} {
53
+ ${options.styleSelector(variantName)} {
54
+ ${declarations.join(";\n")}
55
+ }
56
+ }
57
+ `);
58
+ }
59
+ for (const [breakpointKey, declarations] of spacingOverrides) {
60
+ const breakpoint = theme.breakpoints[breakpointKey];
61
+ if (!breakpoint) {
62
+ continue;
63
+ }
64
+ cssChunks.push(css `
65
+ ${breakpoint.belowMediaQuery} {
66
+ ${options.spacingSelector(variantName)} {
67
+ ${declarations.join(";\n")}
68
+ }
69
+ }
70
+ `);
71
+ }
72
+ }
73
+ return cssChunks.join("\n");
74
+ }
@@ -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
@@ -4,17 +4,25 @@ export { OneOfBlock } from "./blocks/factories/OneOfBlock.js";
4
4
  export { OptionalBlock } from "./blocks/factories/OptionalBlock.js";
5
5
  export type { SupportedBlocks } from "./blocks/factories/types.js";
6
6
  export type { PropsWithData } from "./blocks/helpers/PropsWithData.js";
7
+ export type { HtmlInlineLinkProps } from "./components/inlineLink/HtmlInlineLink.js";
8
+ export { HtmlInlineLink } from "./components/inlineLink/HtmlInlineLink.js";
7
9
  export { MjmlMailRoot } from "./components/mailRoot/MjmlMailRoot.js";
8
10
  export type { MjmlSectionProps } from "./components/section/MjmlSection.js";
9
11
  export { MjmlSection } from "./components/section/MjmlSection.js";
12
+ export type { HtmlTextProps } from "./components/text/HtmlText.js";
13
+ export { HtmlText } from "./components/text/HtmlText.js";
14
+ export type { MjmlTextProps } from "./components/text/MjmlText.js";
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";
10
18
  export { registerStyles } from "./styles/registerStyles.js";
11
19
  export { createBreakpoint } from "./theme/createBreakpoint.js";
12
20
  export { createTheme } from "./theme/createTheme.js";
13
21
  export type { ResponsiveValue } from "./theme/responsiveValue.js";
14
22
  export { getDefaultFromResponsiveValue, getResponsiveOverrides } from "./theme/responsiveValue.js";
15
23
  export { ThemeProvider, useTheme } from "./theme/ThemeProvider.js";
16
- export type { Theme, ThemeBreakpoint, ThemeBreakpoints, ThemeSizes } from "./theme/themeTypes.js";
24
+ export type { TextStyles, TextVariants, TextVariantStyles, Theme, ThemeBackgroundColors, ThemeBreakpoint, ThemeBreakpoints, ThemeColors, ThemeSizes, ThemeText, } from "./theme/themeTypes.js";
17
25
  export { css } from "./utils/css.js";
18
- 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, MjmlText, type IMjmlTextProps as MjmlTextProps, 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";
19
27
  export { MjmlComment, MjmlConditionalComment, MjmlHtml, MjmlTrackingPixel, MjmlYahooStyle } from "@faire/mjml-react/extensions/index.js";
20
28
  export { renderToMjml } from "@faire/mjml-react/utils/renderToMjml.js";
package/lib/index.js CHANGED
@@ -2,14 +2,18 @@ export { BlocksBlock } from "./blocks/factories/BlocksBlock.js";
2
2
  export { ListBlock } from "./blocks/factories/ListBlock.js";
3
3
  export { OneOfBlock } from "./blocks/factories/OneOfBlock.js";
4
4
  export { OptionalBlock } from "./blocks/factories/OptionalBlock.js";
5
+ export { HtmlInlineLink } from "./components/inlineLink/HtmlInlineLink.js";
5
6
  export { MjmlMailRoot } from "./components/mailRoot/MjmlMailRoot.js";
6
7
  export { MjmlSection } from "./components/section/MjmlSection.js";
8
+ export { HtmlText } from "./components/text/HtmlText.js";
9
+ export { MjmlText } from "./components/text/MjmlText.js";
10
+ export { MjmlWrapper } from "./components/wrapper/MjmlWrapper.js";
7
11
  export { registerStyles } from "./styles/registerStyles.js";
8
12
  export { createBreakpoint } from "./theme/createBreakpoint.js";
9
13
  export { createTheme } from "./theme/createTheme.js";
10
14
  export { getDefaultFromResponsiveValue, getResponsiveOverrides } from "./theme/responsiveValue.js";
11
15
  export { ThemeProvider, useTheme } from "./theme/ThemeProvider.js";
12
16
  export { css } from "./utils/css.js";
13
- 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, MjmlText, 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";
14
18
  export { MjmlComment, MjmlConditionalComment, MjmlHtml, MjmlTrackingPixel, MjmlYahooStyle } from "@faire/mjml-react/extensions/index.js";
15
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): {
@@ -1 +1,7 @@
1
- export declare function MailRendererDecorator(Story: () => React.JSX.Element): import("react/jsx-runtime").JSX.Element;
1
+ import type { Theme } from "../theme/themeTypes.js";
2
+ export declare function MailRendererDecorator(Story: () => React.JSX.Element, context: {
3
+ parameters: {
4
+ mailRoot?: boolean;
5
+ theme?: Theme;
6
+ };
7
+ }): import("react/jsx-runtime").JSX.Element;
@@ -4,10 +4,10 @@ import { renderMailHtml } from "../client/renderMailHtml.js";
4
4
  import { MjmlMailRoot } from "../components/mailRoot/MjmlMailRoot.js";
5
5
  import { replaceImagesWithPublicUrl } from "./replaceImagesWithPublicUrl.js";
6
6
  const RENDER_RESULT_EVENT = "comet-mail-render-result";
7
- export function MailRendererDecorator(Story) {
7
+ export function MailRendererDecorator(Story, context) {
8
8
  const [globals] = useGlobals();
9
9
  const emit = useChannel({});
10
- const { html: rawHtml, mjmlWarnings } = renderMailHtml(_jsx(MjmlMailRoot, { children: _jsx(Story, {}) }));
10
+ const { html: rawHtml, mjmlWarnings } = renderMailHtml(context.parameters.mailRoot === false ? (_jsx(Story, {})) : (_jsx(MjmlMailRoot, { theme: context.parameters.theme, children: _jsx(Story, {}) })));
11
11
  for (const warning of mjmlWarnings) {
12
12
  console.warn("MJML warning:", warning);
13
13
  }
@@ -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
+ };
@@ -11,7 +11,7 @@ const config = {
11
11
  };
12
12
  export default config;
13
13
  export const Basic = {
14
- render: () => (_jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlText, { children: "Using the default theme from the decorator." }) }) })),
14
+ render: () => (_jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlText, { children: "Using the default theme from the decorator." }) }) })),
15
15
  };
16
16
  const narrowTheme = createTheme({
17
17
  sizes: { bodyWidth: 400 },
@@ -19,5 +19,5 @@ const narrowTheme = createTheme({
19
19
  });
20
20
  export const CustomTheme = {
21
21
  parameters: { theme: narrowTheme },
22
- render: () => (_jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlText, { children: "This email uses a 400px body width and 360px mobile breakpoint." }) }) })),
22
+ render: () => (_jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlText, { children: "This email uses a 400px body width and 360px mobile breakpoint." }) }) })),
23
23
  };
@@ -1,7 +1,11 @@
1
- import type { Theme, ThemeBreakpoints, ThemeSizes } from "./themeTypes.js";
1
+ import type { Theme, ThemeBackgroundColors, ThemeBreakpoints, ThemeColors, ThemeSizes, ThemeText } from "./themeTypes.js";
2
2
  type CreateThemeOverrides = {
3
3
  sizes?: Partial<ThemeSizes>;
4
4
  breakpoints?: Partial<ThemeBreakpoints>;
5
+ text?: Partial<ThemeText>;
6
+ colors?: {
7
+ background?: Partial<ThemeBackgroundColors>;
8
+ } & Partial<Omit<ThemeColors, "background">>;
5
9
  };
6
10
  /**
7
11
  * Creates a complete theme by merging optional overrides onto the default