@comet/mail-react 9.0.0-beta.0 → 9.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/lib/__stories__/examples/CustomNestedFooterTheme.stories.d.ts +5 -0
- package/lib/__stories__/examples/CustomNestedFooterTheme.stories.js +32 -0
- package/lib/__stories__/examples/NotificationEmail.stories.d.ts +5 -0
- package/lib/__stories__/examples/NotificationEmail.stories.js +25 -0
- package/lib/__stories__/examples/TextWithImageEmail.stories.d.ts +5 -0
- package/lib/__stories__/examples/TextWithImageEmail.stories.js +56 -0
- package/lib/client/index.d.ts +1 -0
- package/lib/client/index.js +1 -0
- package/lib/client/renderMailHtml.d.ts +9 -0
- package/lib/client/renderMailHtml.js +7 -0
- package/lib/components/inlineLink/HtmlInlineLink.d.ts +10 -0
- package/lib/components/inlineLink/HtmlInlineLink.js +35 -0
- package/lib/components/inlineLink/__stories__/HtmlInlineLink.stories.d.ts +12 -0
- package/lib/components/inlineLink/__stories__/HtmlInlineLink.stories.js +56 -0
- package/lib/components/mailRoot/MjmlMailRoot.d.ts +22 -0
- package/lib/components/mailRoot/MjmlMailRoot.js +20 -0
- package/lib/components/mailRoot/__stories__/MjmlMailRoot.stories.d.ts +7 -0
- package/lib/components/mailRoot/__stories__/MjmlMailRoot.stories.js +18 -0
- package/lib/components/section/MjmlSection.d.ts +15 -0
- package/lib/components/section/MjmlSection.js +44 -0
- package/lib/components/section/__stories__/MjmlSection.stories.d.ts +9 -0
- package/lib/components/section/__stories__/MjmlSection.stories.js +30 -0
- package/lib/components/text/HtmlText.d.ts +48 -0
- package/lib/components/text/HtmlText.js +47 -0
- package/lib/components/text/MjmlText.d.ts +37 -0
- package/lib/components/text/MjmlText.js +65 -0
- package/lib/components/text/OutlookTextStyleContext.d.ts +9 -0
- package/lib/components/text/OutlookTextStyleContext.js +10 -0
- package/lib/components/text/__stories__/HtmlText.stories.d.ts +12 -0
- package/lib/components/text/__stories__/HtmlText.stories.js +77 -0
- package/lib/components/text/__stories__/MjmlText.stories.d.ts +10 -0
- package/lib/components/text/__stories__/MjmlText.stories.js +71 -0
- package/lib/components/text/__tests__/HtmlText.test.d.ts +1 -0
- package/lib/components/text/__tests__/HtmlText.test.js +157 -0
- package/lib/components/text/__tests__/MjmlText.test.d.ts +1 -0
- package/lib/components/text/__tests__/MjmlText.test.js +112 -0
- package/lib/components/text/textStyles.d.ts +12 -0
- package/lib/components/text/textStyles.js +69 -0
- package/lib/index.d.ts +17 -1
- package/lib/index.js +11 -1
- package/lib/server/index.d.ts +1 -0
- package/lib/server/index.js +1 -0
- package/lib/server/renderMailHtml.d.ts +9 -0
- package/lib/server/renderMailHtml.js +7 -0
- package/lib/server/renderMailHtml.test.d.ts +1 -0
- package/lib/server/renderMailHtml.test.js +43 -0
- package/lib/storybook/CopyMailHtmlButton.d.ts +4 -0
- package/lib/storybook/CopyMailHtmlButton.js +33 -0
- package/lib/storybook/MailRendererDecorator.d.ts +7 -0
- package/lib/storybook/MailRendererDecorator.js +17 -0
- package/lib/storybook/MjmlWarningsPanel.d.ts +7 -0
- package/lib/storybook/MjmlWarningsPanel.js +33 -0
- package/lib/storybook/UsePublicImageUrlsToggle.d.ts +4 -0
- package/lib/storybook/UsePublicImageUrlsToggle.js +14 -0
- package/lib/storybook/index.d.ts +2 -0
- package/lib/storybook/index.js +9 -0
- package/lib/storybook/manager.d.ts +1 -0
- package/lib/storybook/manager.js +25 -0
- package/lib/storybook/preview.d.ts +5 -0
- package/lib/storybook/preview.js +5 -0
- package/lib/storybook/replaceImagesWithPublicUrl.d.ts +1 -0
- package/lib/storybook/replaceImagesWithPublicUrl.js +15 -0
- package/lib/storybook/replaceImagesWithPublicUrl.test.d.ts +1 -0
- package/lib/storybook/replaceImagesWithPublicUrl.test.js +55 -0
- package/lib/styles/Styles.d.ts +6 -0
- package/lib/styles/Styles.js +16 -0
- package/lib/styles/registerStyles.d.ts +19 -0
- package/lib/styles/registerStyles.js +14 -0
- package/lib/theme/ThemeProvider.d.ts +8 -0
- package/lib/theme/ThemeProvider.js +17 -0
- package/lib/theme/__stories__/ThemeProvider.stories.d.ts +7 -0
- package/lib/theme/__stories__/ThemeProvider.stories.js +23 -0
- package/lib/theme/createBreakpoint.d.ts +7 -0
- package/lib/theme/createBreakpoint.js +11 -0
- package/lib/theme/createTheme.d.ts +22 -0
- package/lib/theme/createTheme.js +30 -0
- package/lib/theme/createTheme.test.d.ts +1 -0
- package/lib/theme/createTheme.test.js +52 -0
- package/lib/theme/defaultTheme.d.ts +2 -0
- package/lib/theme/defaultTheme.js +23 -0
- package/lib/theme/responsiveValue.d.ts +31 -0
- package/lib/theme/responsiveValue.js +33 -0
- package/lib/theme/responsiveValue.test.d.ts +1 -0
- package/lib/theme/responsiveValue.test.js +62 -0
- package/lib/theme/themeTypes.d.ts +106 -0
- package/lib/theme/themeTypes.js +1 -0
- package/lib/utils/css.test.d.ts +1 -0
- package/lib/utils/css.test.js +26 -0
- package/package.json +35 -11
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { MjmlText as BaseMjmlText } from "@faire/mjml-react";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { registerStyles } from "../../styles/registerStyles.js";
|
|
5
|
+
import { getDefaultOrUndefined } from "../../theme/responsiveValue.js";
|
|
6
|
+
import { useOptionalTheme } from "../../theme/ThemeProvider.js";
|
|
7
|
+
import { OutlookTextStyleProvider } from "./OutlookTextStyleContext.js";
|
|
8
|
+
import { generateResponsiveTextCss } from "./textStyles.js";
|
|
9
|
+
/**
|
|
10
|
+
* Text component that can be styled using the theme, optionally using a variant.
|
|
11
|
+
*
|
|
12
|
+
* Works without a `ThemeProvider` as a plain pass-through to the base MJML text component.
|
|
13
|
+
* The `variant` and `bottomSpacing` props require a `ThemeProvider` (or `MjmlMailRoot`).
|
|
14
|
+
*/
|
|
15
|
+
export function MjmlText({ variant: variantProp, bottomSpacing, className, children, ...restProps }) {
|
|
16
|
+
const theme = useOptionalTheme();
|
|
17
|
+
const themedProps = getThemedProps(theme, variantProp, bottomSpacing, restProps);
|
|
18
|
+
const resolvedClassName = clsx("mjmlText", themedProps.activeVariant && `mjmlText--${themedProps.activeVariant}`, bottomSpacing && "mjmlText--bottomSpacing", className);
|
|
19
|
+
return (_jsx(BaseMjmlText, { ...themedProps.baseProps, className: resolvedClassName, ...restProps, children: themedProps.outlookTextStyleValues !== null ? (_jsx(OutlookTextStyleProvider, { value: themedProps.outlookTextStyleValues, children: children })) : (children) }));
|
|
20
|
+
}
|
|
21
|
+
function getThemedProps(theme, variantProp, bottomSpacing, explicitProps) {
|
|
22
|
+
if (theme === null) {
|
|
23
|
+
if (variantProp !== undefined) {
|
|
24
|
+
throw new Error("The `variant` prop requires being wrapped in a ThemeProvider or MjmlMailRoot.");
|
|
25
|
+
}
|
|
26
|
+
if (bottomSpacing) {
|
|
27
|
+
throw new Error("The `bottomSpacing` prop requires being wrapped in a ThemeProvider or MjmlMailRoot.");
|
|
28
|
+
}
|
|
29
|
+
return { activeVariant: undefined, baseProps: {}, outlookTextStyleValues: null };
|
|
30
|
+
}
|
|
31
|
+
const { defaultVariant, variants, ...baseStyles } = theme.text;
|
|
32
|
+
const activeVariant = variantProp ?? defaultVariant;
|
|
33
|
+
const variantStyles = activeVariant ? variants?.[activeVariant] : undefined;
|
|
34
|
+
const mergedStyles = variantStyles ? { ...baseStyles, ...variantStyles } : baseStyles;
|
|
35
|
+
const fontWeightDefault = getDefaultOrUndefined(mergedStyles.fontWeight);
|
|
36
|
+
return {
|
|
37
|
+
activeVariant,
|
|
38
|
+
baseProps: {
|
|
39
|
+
fontFamily: getDefaultOrUndefined(mergedStyles.fontFamily),
|
|
40
|
+
fontSize: getDefaultOrUndefined(mergedStyles.fontSize),
|
|
41
|
+
fontWeight: fontWeightDefault !== undefined ? String(fontWeightDefault) : undefined,
|
|
42
|
+
fontStyle: getDefaultOrUndefined(mergedStyles.fontStyle),
|
|
43
|
+
lineHeight: getDefaultOrUndefined(mergedStyles.lineHeight),
|
|
44
|
+
letterSpacing: getDefaultOrUndefined(mergedStyles.letterSpacing),
|
|
45
|
+
textDecoration: getDefaultOrUndefined(mergedStyles.textDecoration),
|
|
46
|
+
textTransform: getDefaultOrUndefined(mergedStyles.textTransform),
|
|
47
|
+
color: getDefaultOrUndefined(mergedStyles.color),
|
|
48
|
+
paddingBottom: bottomSpacing ? getDefaultOrUndefined(mergedStyles.bottomSpacing) : undefined,
|
|
49
|
+
},
|
|
50
|
+
outlookTextStyleValues: {
|
|
51
|
+
fontFamily: explicitProps.fontFamily ?? getDefaultOrUndefined(mergedStyles.fontFamily),
|
|
52
|
+
fontSize: explicitProps.fontSize ?? getDefaultOrUndefined(mergedStyles.fontSize),
|
|
53
|
+
fontWeight: explicitProps.fontWeight ?? fontWeightDefault,
|
|
54
|
+
lineHeight: explicitProps.lineHeight ?? getDefaultOrUndefined(mergedStyles.lineHeight),
|
|
55
|
+
color: explicitProps.color ?? getDefaultOrUndefined(mergedStyles.color),
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function generateTextStyles(theme) {
|
|
60
|
+
return generateResponsiveTextCss(theme, {
|
|
61
|
+
styleSelector: (variantName) => `.mjmlText--${variantName} > div`,
|
|
62
|
+
spacingSelector: (variantName) => `.mjmlText--bottomSpacing.mjmlText--${variantName}`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
registerStyles(generateTextStyles);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
type OutlookTextStyleValues = Pick<CSSProperties, "fontFamily" | "fontSize" | "lineHeight" | "fontWeight" | "color">;
|
|
3
|
+
declare function OutlookTextStyleProvider({ value, children }: {
|
|
4
|
+
value: OutlookTextStyleValues;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}): ReactNode;
|
|
7
|
+
declare function useOutlookTextStyle(): OutlookTextStyleValues | null;
|
|
8
|
+
export { OutlookTextStyleProvider, useOutlookTextStyle };
|
|
9
|
+
export type { OutlookTextStyleValues };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
const OutlookTextStyleContext = createContext(null);
|
|
4
|
+
function OutlookTextStyleProvider({ value, children }) {
|
|
5
|
+
return _jsx(OutlookTextStyleContext, { value: value, children: children });
|
|
6
|
+
}
|
|
7
|
+
function useOutlookTextStyle() {
|
|
8
|
+
return useContext(OutlookTextStyleContext);
|
|
9
|
+
}
|
|
10
|
+
export { OutlookTextStyleProvider, useOutlookTextStyle };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { HtmlText } from "../HtmlText.js";
|
|
3
|
+
type Story = StoryObj<typeof HtmlText>;
|
|
4
|
+
declare const config: Meta<typeof HtmlText>;
|
|
5
|
+
export default config;
|
|
6
|
+
export declare const Default: Story;
|
|
7
|
+
export declare const WithVariants: Story;
|
|
8
|
+
export declare const ResponsiveVariants: Story;
|
|
9
|
+
export declare const BottomSpacing: Story;
|
|
10
|
+
export declare const DefaultVariant: Story;
|
|
11
|
+
export declare const ElementPropAsDiv: Story;
|
|
12
|
+
export declare const ElementPropAsAnchor: Story;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { MjmlColumn, MjmlRaw } from "@faire/mjml-react";
|
|
3
|
+
import { createTheme } from "../../../theme/createTheme.js";
|
|
4
|
+
import { MjmlSection } from "../../section/MjmlSection.js";
|
|
5
|
+
import { HtmlText } from "../HtmlText.js";
|
|
6
|
+
const config = {
|
|
7
|
+
title: "Components/HtmlText",
|
|
8
|
+
component: HtmlText,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
};
|
|
11
|
+
export default config;
|
|
12
|
+
export const Default = {
|
|
13
|
+
render: () => (_jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlRaw, { children: _jsx("table", { children: _jsx("tr", { children: _jsx(HtmlText, { children: "Default text with base theme styles" }) }) }) }) }) })),
|
|
14
|
+
};
|
|
15
|
+
export const WithVariants = {
|
|
16
|
+
parameters: {
|
|
17
|
+
theme: createTheme({
|
|
18
|
+
text: {
|
|
19
|
+
variants: {
|
|
20
|
+
heading: { fontSize: "32px", fontWeight: 700, lineHeight: "40px" },
|
|
21
|
+
body: { fontSize: "16px", lineHeight: "24px" },
|
|
22
|
+
caption: { fontSize: "12px", lineHeight: "16px", color: "#666666" },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
},
|
|
27
|
+
render: () => (_jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlRaw, { children: _jsxs("table", { children: [_jsx("tr", { children: _jsx(HtmlText, { variant: "heading", children: "Heading variant" }) }), _jsx("tr", { children: _jsx(HtmlText, { variant: "body", children: "Body variant" }) }), _jsx("tr", { children: _jsx(HtmlText, { variant: "caption", children: "Caption variant" }) })] }) }) }) })),
|
|
28
|
+
};
|
|
29
|
+
export const ResponsiveVariants = {
|
|
30
|
+
parameters: {
|
|
31
|
+
theme: createTheme({
|
|
32
|
+
text: {
|
|
33
|
+
variants: {
|
|
34
|
+
heading: {
|
|
35
|
+
fontSize: { default: "32px", mobile: "24px" },
|
|
36
|
+
lineHeight: { default: "40px", mobile: "30px" },
|
|
37
|
+
fontWeight: 700,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
render: () => (_jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlRaw, { children: _jsx("table", { children: _jsx("tr", { children: _jsx(HtmlText, { variant: "heading", children: "Responsive heading \u2014 shrinks on mobile" }) }) }) }) }) })),
|
|
44
|
+
};
|
|
45
|
+
export const BottomSpacing = {
|
|
46
|
+
parameters: {
|
|
47
|
+
theme: createTheme({
|
|
48
|
+
text: {
|
|
49
|
+
bottomSpacing: "20px",
|
|
50
|
+
variants: {
|
|
51
|
+
heading: { fontSize: "32px", fontWeight: 700, bottomSpacing: { default: "24px", mobile: "16px" } },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
},
|
|
56
|
+
render: () => (_jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlRaw, { children: _jsxs("table", { children: [_jsx("tr", { children: _jsx(HtmlText, { variant: "heading", bottomSpacing: true, children: "Heading with bottom spacing" }) }), _jsx("tr", { children: _jsx(HtmlText, { bottomSpacing: true, children: "Base text with bottom spacing" }) }), _jsx("tr", { children: _jsx(HtmlText, { children: "Text without bottom spacing" }) })] }) }) }) })),
|
|
57
|
+
};
|
|
58
|
+
export const DefaultVariant = {
|
|
59
|
+
parameters: {
|
|
60
|
+
theme: createTheme({
|
|
61
|
+
text: {
|
|
62
|
+
defaultVariant: "body",
|
|
63
|
+
variants: {
|
|
64
|
+
body: { fontSize: "14px", lineHeight: "22px" },
|
|
65
|
+
heading: { fontSize: "28px", fontWeight: 700 },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
70
|
+
render: () => (_jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlRaw, { children: _jsxs("table", { children: [_jsx("tr", { children: _jsx(HtmlText, { children: "Uses the default \"body\" variant automatically" }) }), _jsx("tr", { children: _jsx(HtmlText, { variant: "heading", children: "Explicit heading variant" }) })] }) }) }) })),
|
|
71
|
+
};
|
|
72
|
+
export const ElementPropAsDiv = {
|
|
73
|
+
render: () => (_jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlRaw, { children: _jsx(HtmlText, { element: "div", children: "Rendered as a div with theme styles" }) }) }) })),
|
|
74
|
+
};
|
|
75
|
+
export const ElementPropAsAnchor = {
|
|
76
|
+
render: () => (_jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlRaw, { children: _jsx(HtmlText, { element: "a", href: "https://example.com", style: { textDecoration: "underline" }, children: "Rendered as an anchor with href" }) }) }) })),
|
|
77
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { MjmlText } from "../MjmlText.js";
|
|
3
|
+
type Story = StoryObj<typeof MjmlText>;
|
|
4
|
+
declare const config: Meta<typeof MjmlText>;
|
|
5
|
+
export default config;
|
|
6
|
+
export declare const Default: Story;
|
|
7
|
+
export declare const WithVariants: Story;
|
|
8
|
+
export declare const ResponsiveVariants: Story;
|
|
9
|
+
export declare const BottomSpacing: Story;
|
|
10
|
+
export declare const DefaultVariant: Story;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { MjmlColumn } from "@faire/mjml-react";
|
|
3
|
+
import { createTheme } from "../../../theme/createTheme.js";
|
|
4
|
+
import { MjmlSection } from "../../section/MjmlSection.js";
|
|
5
|
+
import { MjmlText } from "../MjmlText.js";
|
|
6
|
+
const config = {
|
|
7
|
+
title: "Components/MjmlText",
|
|
8
|
+
component: MjmlText,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
};
|
|
11
|
+
export default config;
|
|
12
|
+
export const Default = {
|
|
13
|
+
render: () => (_jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlText, { children: "Default text with base theme styles" }) }) })),
|
|
14
|
+
};
|
|
15
|
+
export const WithVariants = {
|
|
16
|
+
parameters: {
|
|
17
|
+
theme: createTheme({
|
|
18
|
+
text: {
|
|
19
|
+
variants: {
|
|
20
|
+
heading: { fontSize: "32px", fontWeight: 700, lineHeight: "40px" },
|
|
21
|
+
body: { fontSize: "16px", lineHeight: "24px" },
|
|
22
|
+
caption: { fontSize: "12px", lineHeight: "16px", color: "#666666" },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
},
|
|
27
|
+
render: () => (_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlText, { variant: "heading", children: "Heading variant" }), _jsx(MjmlText, { variant: "body", children: "Body variant" }), _jsx(MjmlText, { variant: "caption", children: "Caption variant" })] }) })),
|
|
28
|
+
};
|
|
29
|
+
export const ResponsiveVariants = {
|
|
30
|
+
parameters: {
|
|
31
|
+
theme: createTheme({
|
|
32
|
+
text: {
|
|
33
|
+
variants: {
|
|
34
|
+
heading: {
|
|
35
|
+
fontSize: { default: "32px", mobile: "24px" },
|
|
36
|
+
lineHeight: { default: "40px", mobile: "30px" },
|
|
37
|
+
fontWeight: 700,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
render: () => (_jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlText, { variant: "heading", children: "Responsive heading \u2014 shrinks on mobile" }) }) })),
|
|
44
|
+
};
|
|
45
|
+
export const BottomSpacing = {
|
|
46
|
+
parameters: {
|
|
47
|
+
theme: createTheme({
|
|
48
|
+
text: {
|
|
49
|
+
bottomSpacing: "20px",
|
|
50
|
+
variants: {
|
|
51
|
+
heading: { fontSize: "32px", fontWeight: 700, bottomSpacing: { default: "24px", mobile: "16px" } },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
},
|
|
56
|
+
render: () => (_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Heading with bottom spacing" }), _jsx(MjmlText, { bottomSpacing: true, children: "Base text with bottom spacing" }), _jsx(MjmlText, { children: "Text without bottom spacing" })] }) })),
|
|
57
|
+
};
|
|
58
|
+
export const DefaultVariant = {
|
|
59
|
+
parameters: {
|
|
60
|
+
theme: createTheme({
|
|
61
|
+
text: {
|
|
62
|
+
defaultVariant: "body",
|
|
63
|
+
variants: {
|
|
64
|
+
body: { fontSize: "14px", lineHeight: "22px" },
|
|
65
|
+
heading: { fontSize: "28px", fontWeight: 700 },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
70
|
+
render: () => (_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlText, { children: "Uses the default \"body\" variant automatically" }), _jsx(MjmlText, { variant: "heading", children: "Explicit heading variant" })] }) })),
|
|
71
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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,69 @@
|
|
|
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
|
+
const cssChunks = [];
|
|
20
|
+
for (const [variantName, variantStyles] of Object.entries(variants)) {
|
|
21
|
+
if (!variantStyles)
|
|
22
|
+
continue;
|
|
23
|
+
const styleOverrides = new Map();
|
|
24
|
+
const spacingOverrides = new Map();
|
|
25
|
+
for (const [themeKey, cssProperty] of textStyleCssProperties) {
|
|
26
|
+
const value = variantStyles[themeKey];
|
|
27
|
+
if (value === undefined)
|
|
28
|
+
continue;
|
|
29
|
+
for (const { breakpointKey, value: breakpointValue } of getResponsiveOverrides(value)) {
|
|
30
|
+
const declarations = styleOverrides.get(breakpointKey) ?? [];
|
|
31
|
+
declarations.push(`${cssProperty}: ${String(breakpointValue)} !important`);
|
|
32
|
+
styleOverrides.set(breakpointKey, declarations);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const bottomSpacingValue = variantStyles.bottomSpacing;
|
|
36
|
+
if (bottomSpacingValue !== undefined) {
|
|
37
|
+
for (const { breakpointKey, value: breakpointValue } of getResponsiveOverrides(bottomSpacingValue)) {
|
|
38
|
+
const declarations = spacingOverrides.get(breakpointKey) ?? [];
|
|
39
|
+
declarations.push(`padding-bottom: ${String(breakpointValue)} !important`);
|
|
40
|
+
spacingOverrides.set(breakpointKey, declarations);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const [breakpointKey, declarations] of styleOverrides) {
|
|
44
|
+
const breakpoint = theme.breakpoints[breakpointKey];
|
|
45
|
+
if (!breakpoint)
|
|
46
|
+
continue;
|
|
47
|
+
cssChunks.push(css `
|
|
48
|
+
${breakpoint.belowMediaQuery} {
|
|
49
|
+
${options.styleSelector(variantName)} {
|
|
50
|
+
${declarations.join(";\n")}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
`);
|
|
54
|
+
}
|
|
55
|
+
for (const [breakpointKey, declarations] of spacingOverrides) {
|
|
56
|
+
const breakpoint = theme.breakpoints[breakpointKey];
|
|
57
|
+
if (!breakpoint)
|
|
58
|
+
continue;
|
|
59
|
+
cssChunks.push(css `
|
|
60
|
+
${breakpoint.belowMediaQuery} {
|
|
61
|
+
${options.spacingSelector(variantName)} {
|
|
62
|
+
${declarations.join(";\n")}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return cssChunks.join("\n");
|
|
69
|
+
}
|