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

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 (64) hide show
  1. package/README.md +94 -15
  2. package/lib/__stories__/layout-patterns/AsymmetricTwoColumnLayout.stories.js +2 -1
  3. package/lib/blocks/pixelImage/HtmlPixelImageBlock.d.ts +11 -0
  4. package/lib/blocks/pixelImage/HtmlPixelImageBlock.js +18 -0
  5. package/lib/blocks/pixelImage/MjmlPixelImageBlock.d.ts +9 -0
  6. package/lib/blocks/pixelImage/MjmlPixelImageBlock.js +15 -0
  7. package/lib/blocks/pixelImage/__stories__/HtmlPixelImageBlock.stories.d.ts +7 -0
  8. package/lib/blocks/pixelImage/__stories__/HtmlPixelImageBlock.stories.js +54 -0
  9. package/lib/blocks/pixelImage/__stories__/MjmlPixelImageBlock.stories.d.ts +7 -0
  10. package/lib/blocks/pixelImage/__stories__/MjmlPixelImageBlock.stories.js +54 -0
  11. package/lib/blocks/pixelImage/__stories__/exampleBlockData.d.ts +2 -0
  12. package/lib/blocks/pixelImage/__stories__/exampleBlockData.js +19 -0
  13. package/lib/blocks/pixelImage/__tests__/usePixelImageBlockConfig.test.d.ts +1 -0
  14. package/lib/blocks/pixelImage/__tests__/usePixelImageBlockConfig.test.js +21 -0
  15. package/lib/blocks/pixelImage/__tests__/usePixelImageBlockData.test.d.ts +1 -0
  16. package/lib/blocks/pixelImage/__tests__/usePixelImageBlockData.test.js +205 -0
  17. package/lib/blocks/pixelImage/common.d.ts +18 -0
  18. package/lib/blocks/pixelImage/common.js +1 -0
  19. package/lib/blocks/pixelImage/usePixelImageBlockConfig.d.ts +5 -0
  20. package/lib/blocks/pixelImage/usePixelImageBlockConfig.js +11 -0
  21. package/lib/blocks/pixelImage/usePixelImageBlockData.d.ts +16 -0
  22. package/lib/blocks/pixelImage/usePixelImageBlockData.js +80 -0
  23. package/lib/components/divider/HtmlDivider.d.ts +9 -0
  24. package/lib/components/divider/HtmlDivider.js +43 -0
  25. package/lib/components/divider/MjmlDivider.d.ts +7 -0
  26. package/lib/components/divider/MjmlDivider.js +13 -0
  27. package/lib/components/divider/__stories__/HtmlDivider.stories.d.ts +10 -0
  28. package/lib/components/divider/__stories__/HtmlDivider.stories.js +60 -0
  29. package/lib/components/divider/__stories__/MjmlDivider.stories.d.ts +10 -0
  30. package/lib/components/divider/__stories__/MjmlDivider.stories.js +60 -0
  31. package/lib/components/divider/__tests__/HtmlDivider.test.d.ts +1 -0
  32. package/lib/components/divider/__tests__/HtmlDivider.test.js +144 -0
  33. package/lib/components/divider/__tests__/MjmlDivider.test.d.ts +1 -0
  34. package/lib/components/divider/__tests__/MjmlDivider.test.js +43 -0
  35. package/lib/components/divider/defaultDividerStyles.d.ts +2 -0
  36. package/lib/components/divider/defaultDividerStyles.js +4 -0
  37. package/lib/components/divider/dividerProps.d.ts +38 -0
  38. package/lib/components/divider/dividerProps.js +1 -0
  39. package/lib/components/divider/generateResponsiveDividerCss.d.ts +8 -0
  40. package/lib/components/divider/generateResponsiveDividerCss.js +55 -0
  41. package/lib/components/image/HtmlImage.d.ts +11 -0
  42. package/lib/components/image/HtmlImage.js +23 -0
  43. package/lib/components/image/MjmlImage.d.ts +10 -0
  44. package/lib/components/image/MjmlImage.js +22 -0
  45. package/lib/components/image/__stories__/HtmlImage.stories.d.ts +7 -0
  46. package/lib/components/image/__stories__/HtmlImage.stories.js +32 -0
  47. package/lib/components/image/__stories__/MjmlImage.stories.d.ts +7 -0
  48. package/lib/components/image/__stories__/MjmlImage.stories.js +32 -0
  49. package/lib/components/mailRoot/MjmlMailRoot.d.ts +13 -4
  50. package/lib/components/mailRoot/MjmlMailRoot.js +10 -5
  51. package/lib/components/text/textStyles.d.ts +1 -3
  52. package/lib/components/text/textStyles.js +1 -1
  53. package/lib/config/ConfigProvider.d.ts +43 -0
  54. package/lib/config/ConfigProvider.js +16 -0
  55. package/lib/config/ConfigProvider.test.d.ts +8 -0
  56. package/lib/config/ConfigProvider.test.js +30 -0
  57. package/lib/index.d.ts +15 -2
  58. package/lib/index.js +8 -1
  59. package/lib/server/renderMailHtml.test.js +10 -1
  60. package/lib/theme/createTheme.d.ts +2 -1
  61. package/lib/theme/createTheme.js +1 -0
  62. package/lib/theme/defaultTheme.js +2 -0
  63. package/lib/theme/themeTypes.d.ts +41 -0
  64. package/package.json +13 -9
@@ -0,0 +1,80 @@
1
+ import { useTheme } from "../../theme/ThemeProvider.js";
2
+ import { usePixelImageBlockConfig } from "./usePixelImageBlockConfig.js";
3
+ export function usePixelImageBlockData({ data: { damFile, cropArea, urlTemplate }, defaultRenderWidth, largestPossibleRenderWidth: passedLargestPossibleRenderWidth, aspectRatio: passedAspectRatio, }) {
4
+ const theme = useTheme();
5
+ const { validSizes, baseUrl } = usePixelImageBlockConfig();
6
+ const largestPossibleRenderWidth = passedLargestPossibleRenderWidth ?? theme.sizes.bodyWidth;
7
+ if (!damFile?.image) {
8
+ return null;
9
+ }
10
+ const usedCropArea = cropArea ?? damFile.image.cropArea;
11
+ const aspectRatio = passedAspectRatio !== undefined ? parseAspectRatio(passedAspectRatio) : calculateAspectRatio(damFile.image, usedCropArea);
12
+ const optimalWidth = getOptimalAllowedImageWidth(validSizes, defaultRenderWidth, largestPossibleRenderWidth);
13
+ const resolvedImageUrl = generateImageUrl(urlTemplate, optimalWidth, aspectRatio);
14
+ return {
15
+ imageUrl: isAbsoluteUrl(resolvedImageUrl) ? resolvedImageUrl : `${baseUrl}${resolvedImageUrl}`,
16
+ defaultRenderWidth,
17
+ desktopImageHeight: Math.round(defaultRenderWidth / aspectRatio),
18
+ alt: damFile.altText,
19
+ title: damFile.title,
20
+ };
21
+ }
22
+ function isAbsoluteUrl(url) {
23
+ return !url.startsWith("/");
24
+ }
25
+ function getOptimalAllowedImageWidth(validSizes, defaultRenderWidth, largestPossibleRenderWidth) {
26
+ const sortedValidSizes = validSizes.sort((a, b) => a - b);
27
+ let width = null;
28
+ const largestPossibleWidth = sortedValidSizes[sortedValidSizes.length - 1];
29
+ sortedValidSizes.forEach((validWidth) => {
30
+ if (defaultRenderWidth === largestPossibleRenderWidth) {
31
+ width = largestPossibleRenderWidth * 2;
32
+ }
33
+ else if (!width && validWidth >= defaultRenderWidth * 2) {
34
+ width = validWidth;
35
+ }
36
+ });
37
+ if (!width) {
38
+ return largestPossibleWidth;
39
+ }
40
+ return width;
41
+ }
42
+ // Copied from `calculateInheritAspectRatio` in `@comet/site-react` (`src/image/image.utils.ts`).
43
+ // Keep in sync with the site-react version when changes are made.
44
+ function calculateAspectRatio(image, cropArea) {
45
+ if (cropArea.focalPoint === "SMART") {
46
+ return image.width / image.height;
47
+ }
48
+ if (cropArea.width === undefined || cropArea.height === undefined) {
49
+ throw new Error("Missing crop dimensions");
50
+ }
51
+ return (cropArea.width * image.width) / (cropArea.height * image.height);
52
+ }
53
+ // Copied from `generateImageUrl` in `@comet/site-react` (`src/image/image.utils.ts`).
54
+ // Keep in sync with the site-react version when changes are made.
55
+ function generateImageUrl(urlTemplate, width, aspectRatio) {
56
+ return urlTemplate.replace("$resizeWidth", String(width)).replace("$resizeHeight", String(Math.ceil(width / aspectRatio)));
57
+ }
58
+ // Copied from `parseAspectRatio` in `@comet/site-react` (`src/image/image.utils.ts`).
59
+ // Keep in sync with the site-react version when changes are made.
60
+ function parseAspectRatio(value) {
61
+ let width;
62
+ let height;
63
+ if (typeof value === "string") {
64
+ [width, height] = value.split(/[x/:]/).map((part) => {
65
+ const parsed = parseFloat(part);
66
+ return isNaN(parsed) ? undefined : parsed;
67
+ });
68
+ if (width && !height) {
69
+ height = 1;
70
+ }
71
+ }
72
+ else {
73
+ width = value;
74
+ height = 1;
75
+ }
76
+ if (!width || !height) {
77
+ throw new Error(`An error occurred while parsing the aspect ratio: ${value}`);
78
+ }
79
+ return width / height;
80
+ }
@@ -0,0 +1,9 @@
1
+ import type { ReactNode } from "react";
2
+ import type { Theme } from "../../theme/themeTypes.js";
3
+ import type { DividerProps } from "./dividerProps.js";
4
+ export type HtmlDividerProps = DividerProps;
5
+ /**
6
+ * Themed divider for use inside MJML ending tags or outside of the MJML context.
7
+ */
8
+ export declare function HtmlDivider({ variant: variantProp, height: heightProp, backgroundColor: backgroundColorProp, backgroundImage: backgroundImageProp, className, style, }: HtmlDividerProps): ReactNode;
9
+ export declare function generateHtmlDividerStyles(theme: Theme): string;
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import clsx from "clsx";
3
+ import { registerStyles } from "../../styles/registerStyles.js";
4
+ import { getDefaultOrUndefined } from "../../theme/responsiveValue.js";
5
+ import { useOptionalTheme } from "../../theme/ThemeProvider.js";
6
+ import { defaultDividerStyles } from "./defaultDividerStyles.js";
7
+ import { generateResponsiveDividerCss } from "./generateResponsiveDividerCss.js";
8
+ // U+200B keeps the cell from collapsing in clients that drop empty <td>s,
9
+ // without contributing visible width.
10
+ const zeroWidthSpace = String.fromCharCode(0x200b);
11
+ /**
12
+ * Themed divider for use inside MJML ending tags or outside of the MJML context.
13
+ */
14
+ export function HtmlDivider({ variant: variantProp, height: heightProp, backgroundColor: backgroundColorProp, backgroundImage: backgroundImageProp, className, style, }) {
15
+ const theme = useOptionalTheme();
16
+ if (theme === null && variantProp !== undefined) {
17
+ throw new Error("The `variant` prop requires being wrapped in a ThemeProvider or MjmlMailRoot.");
18
+ }
19
+ const themeDivider = theme?.divider ?? defaultDividerStyles;
20
+ const { defaultVariant, variants, ...baseStyles } = themeDivider;
21
+ const activeVariant = variantProp ?? defaultVariant;
22
+ const variantStyles = activeVariant ? variants?.[activeVariant] : undefined;
23
+ const mergedStyles = variantStyles ? { ...baseStyles, ...variantStyles } : baseStyles;
24
+ const height = heightProp ?? getDefaultOrUndefined(mergedStyles.height);
25
+ const backgroundColor = backgroundColorProp ?? getDefaultOrUndefined(mergedStyles.backgroundColor);
26
+ const backgroundImage = backgroundImageProp ?? getDefaultOrUndefined(mergedStyles.backgroundImage);
27
+ const dividerStyle = {
28
+ height,
29
+ lineHeight: height === undefined ? undefined : `${height}px`,
30
+ fontSize: 0,
31
+ backgroundColor,
32
+ backgroundImage,
33
+ ...{ msoLineHeightRule: "exactly" },
34
+ ...style,
35
+ };
36
+ return (_jsx("table", { role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, width: "100%", className: clsx("htmlDivider", activeVariant && `htmlDivider--${activeVariant}`, className), children: _jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { bgcolor: backgroundColor, height: height, style: dividerStyle, children: zeroWidthSpace }) }) }) }));
37
+ }
38
+ export function generateHtmlDividerStyles(theme) {
39
+ return generateResponsiveDividerCss(theme, {
40
+ styleSelector: (variantName) => `.htmlDivider--${variantName} td`,
41
+ });
42
+ }
43
+ registerStyles(generateHtmlDividerStyles);
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from "react";
2
+ import type { DividerProps } from "./dividerProps.js";
3
+ export type MjmlDividerProps = DividerProps;
4
+ /**
5
+ * Themed divider for use inside an `MjmlColumn`.
6
+ */
7
+ export declare function MjmlDivider({ variant, height, backgroundColor, backgroundImage, className, style }: MjmlDividerProps): ReactNode;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { MjmlRaw } from "@faire/mjml-react";
3
+ import clsx from "clsx";
4
+ import { useOptionalTheme } from "../../theme/ThemeProvider.js";
5
+ import { HtmlDivider } from "./HtmlDivider.js";
6
+ /**
7
+ * Themed divider for use inside an `MjmlColumn`.
8
+ */
9
+ export function MjmlDivider({ variant, height, backgroundColor, backgroundImage, className, style }) {
10
+ const theme = useOptionalTheme();
11
+ const activeVariant = variant ?? theme?.divider.defaultVariant;
12
+ return (_jsx(MjmlRaw, { children: _jsx(HtmlDivider, { variant: variant, height: height, backgroundColor: backgroundColor, backgroundImage: backgroundImage, className: clsx("mjmlDivider", activeVariant && `mjmlDivider--${activeVariant}`, className), style: style }) }));
13
+ }
@@ -0,0 +1,10 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { HtmlDivider } from "../HtmlDivider.js";
3
+ type Story = StoryObj<typeof HtmlDivider>;
4
+ declare const config: Meta<typeof HtmlDivider>;
5
+ export default config;
6
+ export declare const Default: Story;
7
+ export declare const CustomHeightAndColor: Story;
8
+ export declare const WithTheme: Story;
9
+ export declare const WithVariants: Story;
10
+ export declare const GradientWithFallback: Story;
@@ -0,0 +1,60 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { MjmlColumn, MjmlRaw, MjmlSpacer } from "@faire/mjml-react";
3
+ import { createTheme } from "../../../theme/createTheme.js";
4
+ import { MjmlSection } from "../../section/MjmlSection.js";
5
+ import { HtmlDivider } from "../HtmlDivider.js";
6
+ const config = {
7
+ title: "Components/HtmlDivider",
8
+ component: HtmlDivider,
9
+ tags: ["autodocs"],
10
+ argTypes: {
11
+ variant: { control: "text" },
12
+ height: { control: "number" },
13
+ backgroundColor: { control: "text" },
14
+ backgroundImage: { control: "text" },
15
+ className: { control: "text" },
16
+ style: { control: false },
17
+ },
18
+ render: (args) => (_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: "16px" }), _jsx(MjmlRaw, { children: _jsx(HtmlDivider, { ...args }) }), _jsx(MjmlSpacer, { height: "16px" })] }) })),
19
+ };
20
+ export default config;
21
+ export const Default = {};
22
+ export const CustomHeightAndColor = {
23
+ args: {
24
+ height: 8,
25
+ backgroundColor: "#FF6B6B",
26
+ },
27
+ };
28
+ export const WithTheme = {
29
+ parameters: {
30
+ theme: createTheme({
31
+ divider: {
32
+ height: 2,
33
+ backgroundColor: "#5B4FC7",
34
+ },
35
+ }),
36
+ },
37
+ };
38
+ export const WithVariants = {
39
+ parameters: {
40
+ theme: createTheme({
41
+ divider: {
42
+ defaultVariant: "thin",
43
+ variants: {
44
+ thin: { height: 1, backgroundColor: "#999999" },
45
+ thick: { height: { default: 12, mobile: 8 }, backgroundColor: "#222222" },
46
+ },
47
+ },
48
+ }),
49
+ },
50
+ render: (args) => (_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: "16px" }), _jsx(MjmlRaw, { children: _jsx(HtmlDivider, { ...args }) }), _jsx(MjmlSpacer, { height: "16px" }), _jsx(MjmlRaw, { children: _jsx(HtmlDivider, { ...args, variant: "thick" }) }), _jsx(MjmlSpacer, { height: "16px" })] }) })),
51
+ };
52
+ export const GradientWithFallback = {
53
+ // `backgroundColor` is the solid fallback for clients that
54
+ // don't render `background-image` (notably older Outlook).
55
+ args: {
56
+ height: 6,
57
+ backgroundColor: "#5B4FC7",
58
+ backgroundImage: "linear-gradient(to right, #5B4FC7, #FF6B6B, #FFD166)",
59
+ },
60
+ };
@@ -0,0 +1,10 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { MjmlDivider } from "../MjmlDivider.js";
3
+ type Story = StoryObj<typeof MjmlDivider>;
4
+ declare const config: Meta<typeof MjmlDivider>;
5
+ export default config;
6
+ export declare const Default: Story;
7
+ export declare const CustomHeightAndColor: Story;
8
+ export declare const WithTheme: Story;
9
+ export declare const WithVariants: Story;
10
+ export declare const GradientWithFallback: Story;
@@ -0,0 +1,60 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { MjmlColumn, MjmlSpacer } from "@faire/mjml-react";
3
+ import { createTheme } from "../../../theme/createTheme.js";
4
+ import { MjmlSection } from "../../section/MjmlSection.js";
5
+ import { MjmlDivider } from "../MjmlDivider.js";
6
+ const config = {
7
+ title: "Components/MjmlDivider",
8
+ component: MjmlDivider,
9
+ tags: ["autodocs"],
10
+ argTypes: {
11
+ variant: { control: "text" },
12
+ height: { control: "number" },
13
+ backgroundColor: { control: "text" },
14
+ backgroundImage: { control: "text" },
15
+ className: { control: "text" },
16
+ style: { control: false },
17
+ },
18
+ render: (args) => (_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: "16px" }), _jsx(MjmlDivider, { ...args }), _jsx(MjmlSpacer, { height: "16px" })] }) })),
19
+ };
20
+ export default config;
21
+ export const Default = {};
22
+ export const CustomHeightAndColor = {
23
+ args: {
24
+ height: 8,
25
+ backgroundColor: "#FF6B6B",
26
+ },
27
+ };
28
+ export const WithTheme = {
29
+ parameters: {
30
+ theme: createTheme({
31
+ divider: {
32
+ height: 2,
33
+ backgroundColor: "#5B4FC7",
34
+ },
35
+ }),
36
+ },
37
+ };
38
+ export const WithVariants = {
39
+ parameters: {
40
+ theme: createTheme({
41
+ divider: {
42
+ defaultVariant: "thin",
43
+ variants: {
44
+ thin: { height: 1, backgroundColor: "#999999" },
45
+ thick: { height: { default: 12, mobile: 8 }, backgroundColor: "#222222" },
46
+ },
47
+ },
48
+ }),
49
+ },
50
+ render: (args) => (_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: "16px" }), _jsx(MjmlDivider, { ...args }), _jsx(MjmlSpacer, { height: "16px" }), _jsx(MjmlDivider, { ...args, variant: "thick" }), _jsx(MjmlSpacer, { height: "16px" })] }) })),
51
+ };
52
+ export const GradientWithFallback = {
53
+ // `backgroundColor` is the solid fallback for clients that
54
+ // don't render `background-image` (notably older Outlook).
55
+ args: {
56
+ height: 6,
57
+ backgroundColor: "#5B4FC7",
58
+ backgroundImage: "linear-gradient(to right, #5B4FC7, #FF6B6B, #FFD166)",
59
+ },
60
+ };
@@ -0,0 +1,144 @@
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 { generateHtmlDividerStyles, HtmlDivider } from "../HtmlDivider.js";
7
+ function renderHtmlDivider(element) {
8
+ return renderToStaticMarkup(element);
9
+ }
10
+ describe("HtmlDivider", () => {
11
+ it("renders default theme styles across modern, Outlook, and legacy clients", () => {
12
+ const theme = createTheme();
13
+ const html = renderHtmlDivider(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlDivider, {}) }));
14
+ // Modern clients: inline height and background color.
15
+ expect(html).toContain("height:4px");
16
+ expect(html).toContain("background-color:#000000");
17
+ // Outlook: mso-line-height-rule with matching line-height; font-size:0 keeps the spacer character from adding height.
18
+ expect(html).toContain("mso-line-height-rule:exactly");
19
+ expect(html).toContain("line-height:4px");
20
+ expect(html).toContain("font-size:0");
21
+ // Legacy Outlook: attribute fallbacks for clients that ignore inline CSS.
22
+ expect(html).toContain('bgcolor="#000000"');
23
+ expect(html).toContain('height="4"');
24
+ });
25
+ it("applies variant styles over base theme styles", () => {
26
+ const theme = createTheme({
27
+ divider: {
28
+ variants: {
29
+ thick: { height: 10, backgroundColor: "#222222" },
30
+ },
31
+ },
32
+ });
33
+ const html = renderHtmlDivider(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlDivider, { variant: "thick" }) }));
34
+ expect(html).toContain("height:10px");
35
+ expect(html).toContain("background-color:#222222");
36
+ expect(html).toContain("htmlDivider--thick");
37
+ });
38
+ it("inherits base divider styles not overridden by variant", () => {
39
+ const theme = createTheme({
40
+ divider: {
41
+ backgroundColor: "#222222",
42
+ variants: {
43
+ thick: { height: 10 },
44
+ },
45
+ },
46
+ });
47
+ const html = renderHtmlDivider(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlDivider, { variant: "thick" }) }));
48
+ expect(html).toContain("height:10px");
49
+ expect(html).toContain("background-color:#222222");
50
+ });
51
+ it("applies defaultVariant when no variant prop is specified", () => {
52
+ const theme = createTheme({
53
+ divider: {
54
+ defaultVariant: "thin",
55
+ variants: {
56
+ thin: { height: 1 },
57
+ },
58
+ },
59
+ });
60
+ const html = renderHtmlDivider(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlDivider, {}) }));
61
+ expect(html).toContain("height:1px");
62
+ expect(html).toContain("htmlDivider--thin");
63
+ });
64
+ it("per-instance height prop overrides theme and variant", () => {
65
+ const theme = createTheme({
66
+ divider: {
67
+ height: 4,
68
+ variants: { thin: { height: 1 } },
69
+ },
70
+ });
71
+ const html = renderHtmlDivider(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlDivider, { variant: "thin", height: 12 }) }));
72
+ expect(html).toContain("height:12px");
73
+ });
74
+ it("applies variant backgroundImage with the variant backgroundColor as fallback", () => {
75
+ const theme = createTheme({
76
+ divider: {
77
+ variants: {
78
+ gradient: {
79
+ backgroundColor: "#5B4FC7",
80
+ backgroundImage: "linear-gradient(to right, red, blue)",
81
+ },
82
+ },
83
+ },
84
+ });
85
+ const html = renderHtmlDivider(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlDivider, { variant: "gradient" }) }));
86
+ expect(html).toContain("background-color:#5B4FC7");
87
+ expect(html).toContain("background-image:linear-gradient(to right, red, blue)");
88
+ });
89
+ it("lets consumer style prop layer over theme styles", () => {
90
+ const theme = createTheme();
91
+ const html = renderHtmlDivider(_jsx(ThemeProvider, { theme: theme, children: _jsx(HtmlDivider, { style: { backgroundImage: "linear-gradient(to right, red, blue)" } }) }));
92
+ // Solid color fallback stays for clients that don't render gradients.
93
+ expect(html).toContain("background-color:#000000");
94
+ expect(html).toContain("background-image:linear-gradient(to right, red, blue)");
95
+ });
96
+ it("falls back to default styles when used without a ThemeProvider", () => {
97
+ const html = renderHtmlDivider(_jsx(HtmlDivider, {}));
98
+ expect(html).toContain("height:4px");
99
+ expect(html).toContain("background-color:#000000");
100
+ });
101
+ it("throws when variant is set without a ThemeProvider", () => {
102
+ expect(() => renderHtmlDivider(_jsx(HtmlDivider, { variant: "thin" }))).toThrow();
103
+ });
104
+ });
105
+ describe("generateHtmlDividerStyles", () => {
106
+ it("returns empty CSS when no variants are defined", () => {
107
+ const theme = createTheme();
108
+ expect(generateHtmlDividerStyles(theme)).toBe("");
109
+ });
110
+ it("emits height and line-height together for responsive variants", () => {
111
+ const theme = createTheme({
112
+ divider: {
113
+ variants: {
114
+ thin: { height: { default: 4, mobile: 2 } },
115
+ },
116
+ },
117
+ });
118
+ const result = generateHtmlDividerStyles(theme);
119
+ expect(result).toContain(".htmlDivider--thin td");
120
+ expect(result).toContain("height: 2px !important");
121
+ expect(result).toContain("line-height: 2px !important");
122
+ });
123
+ it("emits background-color overrides for responsive variants", () => {
124
+ const theme = createTheme({
125
+ divider: {
126
+ variants: {
127
+ accent: { backgroundColor: { default: "#5B4FC7", mobile: "#FF6B6B" } },
128
+ },
129
+ },
130
+ });
131
+ const result = generateHtmlDividerStyles(theme);
132
+ expect(result).toContain("background-color: #FF6B6B !important");
133
+ });
134
+ it("emits no media queries for a non-responsive variant", () => {
135
+ const theme = createTheme({
136
+ divider: {
137
+ variants: {
138
+ thick: { height: 8 },
139
+ },
140
+ },
141
+ });
142
+ expect(generateHtmlDividerStyles(theme)).not.toContain("@media");
143
+ });
144
+ });
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { MjmlColumn } from "@faire/mjml-react";
3
+ import { renderToStaticMarkup } from "react-dom/server";
4
+ import { describe, expect, it } from "vitest";
5
+ import { renderMailHtml } from "../../../server/renderMailHtml.js";
6
+ import { createTheme } from "../../../theme/createTheme.js";
7
+ import { MjmlMailRoot } from "../../mailRoot/MjmlMailRoot.js";
8
+ import { MjmlSection } from "../../section/MjmlSection.js";
9
+ import { MjmlDivider } from "../MjmlDivider.js";
10
+ describe("MjmlDivider integration", () => {
11
+ it("produces no MJML warnings inside an MjmlMailRoot", () => {
12
+ const theme = createTheme({
13
+ divider: {
14
+ variants: {
15
+ thin: { height: 1, backgroundColor: "#999999" },
16
+ thick: { height: 8, backgroundColor: "#222222" },
17
+ },
18
+ },
19
+ });
20
+ const { mjmlWarnings } = renderMailHtml(_jsx(MjmlMailRoot, { theme: theme, children: _jsx(MjmlSection, { children: _jsxs(MjmlColumn, { children: [_jsx(MjmlDivider, {}), _jsx(MjmlDivider, { variant: "thin" }), _jsx(MjmlDivider, { variant: "thick" })] }) }) }));
21
+ expect(mjmlWarnings).toEqual([]);
22
+ });
23
+ it("applies a variant modifier class to both mjmlDivider and htmlDivider blocks", () => {
24
+ const theme = createTheme({
25
+ divider: {
26
+ variants: {
27
+ thick: { height: 8 },
28
+ },
29
+ },
30
+ });
31
+ const { html } = renderMailHtml(_jsx(MjmlMailRoot, { theme: theme, children: _jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlDivider, { variant: "thick" }) }) }) }));
32
+ expect(html).toContain("mjmlDivider--thick");
33
+ expect(html).toContain("htmlDivider--thick");
34
+ });
35
+ it("falls back to default theme values when used without a ThemeProvider", () => {
36
+ const html = renderToStaticMarkup(_jsx(MjmlDivider, {}));
37
+ expect(html).toContain("height:4px");
38
+ expect(html).toContain("background-color:#000000");
39
+ });
40
+ it("throws when variant is set without a ThemeProvider", () => {
41
+ expect(() => renderToStaticMarkup(_jsx(MjmlDivider, { variant: "thin" }))).toThrow();
42
+ });
43
+ });
@@ -0,0 +1,2 @@
1
+ import type { DividerStyles } from "../../theme/themeTypes.js";
2
+ export declare const defaultDividerStyles: DividerStyles;
@@ -0,0 +1,4 @@
1
+ export const defaultDividerStyles = {
2
+ height: 4,
3
+ backgroundColor: "#000000",
4
+ };
@@ -0,0 +1,38 @@
1
+ import type { CSSProperties } from "react";
2
+ import type { DividerVariantName } from "../../theme/themeTypes.js";
3
+ export interface DividerProps {
4
+ /**
5
+ * The component's variant to apply, as defined in the theme.
6
+ *
7
+ * Custom variants should be defined in the theme through module augmentation:
8
+ *
9
+ * ```ts
10
+ * declare module "@comet/mail-react" {
11
+ * interface DividerVariants { thin: true; thick: true }
12
+ * }
13
+ * ```
14
+ *
15
+ * ```ts
16
+ * const theme = createTheme({
17
+ * divider: {
18
+ * variants: {
19
+ * thin: { height: 1 },
20
+ * thick: { height: 8 },
21
+ * },
22
+ * },
23
+ * });
24
+ * ```
25
+ */
26
+ variant?: DividerVariantName;
27
+ /** Height of the divider in pixels. */
28
+ height?: number;
29
+ /** Background color of the divider (e.g. `"#FF0000"`). */
30
+ backgroundColor?: string;
31
+ /**
32
+ * Background image for the divider — typically a gradient. Clients that
33
+ * don't render `background-image` fall back to the solid `backgroundColor`.
34
+ */
35
+ backgroundImage?: string;
36
+ className?: string;
37
+ style?: CSSProperties;
38
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { Theme } from "../../theme/themeTypes.js";
2
+ interface GenerateResponsiveDividerCssOptions {
3
+ /** Selector for divider style overrides, given a variant name. */
4
+ styleSelector: (variantName: string) => string;
5
+ }
6
+ /** Generates responsive CSS media queries for divider variant overrides. */
7
+ export declare function generateResponsiveDividerCss(theme: Theme, options: GenerateResponsiveDividerCssOptions): string;
8
+ export {};
@@ -0,0 +1,55 @@
1
+ import { getResponsiveOverrides } from "../../theme/responsiveValue.js";
2
+ import { css } from "../../utils/css.js";
3
+ const colorCssProperties = [
4
+ ["backgroundColor", "background-color"],
5
+ ["backgroundImage", "background-image"],
6
+ ];
7
+ /** Generates responsive CSS media queries for divider variant overrides. */
8
+ export function generateResponsiveDividerCss(theme, options) {
9
+ const { variants } = theme.divider;
10
+ if (!variants) {
11
+ return css ``;
12
+ }
13
+ const cssChunks = [];
14
+ for (const [variantName, variantStyles] of Object.entries(variants)) {
15
+ if (!variantStyles) {
16
+ continue;
17
+ }
18
+ const overrides = new Map();
19
+ const heightValue = variantStyles.height;
20
+ if (heightValue !== undefined) {
21
+ for (const { breakpointKey, value: breakpointValue } of getResponsiveOverrides(heightValue)) {
22
+ const declarations = overrides.get(breakpointKey) ?? [];
23
+ // line-height matches height so Outlook honors the declared cell height.
24
+ declarations.push(`height: ${breakpointValue}px !important`);
25
+ declarations.push(`line-height: ${breakpointValue}px !important`);
26
+ overrides.set(breakpointKey, declarations);
27
+ }
28
+ }
29
+ for (const [themeKey, cssProperty] of colorCssProperties) {
30
+ const value = variantStyles[themeKey];
31
+ if (value === undefined) {
32
+ continue;
33
+ }
34
+ for (const { breakpointKey, value: breakpointValue } of getResponsiveOverrides(value)) {
35
+ const declarations = overrides.get(breakpointKey) ?? [];
36
+ declarations.push(`${cssProperty}: ${breakpointValue} !important`);
37
+ overrides.set(breakpointKey, declarations);
38
+ }
39
+ }
40
+ for (const [breakpointKey, declarations] of overrides) {
41
+ const breakpoint = theme.breakpoints[breakpointKey];
42
+ if (!breakpoint) {
43
+ continue;
44
+ }
45
+ cssChunks.push(css `
46
+ ${breakpoint.belowMediaQuery} {
47
+ ${options.styleSelector(variantName)} {
48
+ ${declarations.join(";\n")}
49
+ }
50
+ }
51
+ `);
52
+ }
53
+ }
54
+ return cssChunks.join("\n");
55
+ }
@@ -0,0 +1,11 @@
1
+ import type { ComponentProps, ReactNode } from "react";
2
+ export type HtmlImageProps = ComponentProps<"img">;
3
+ /**
4
+ * Renders an `<img>` tag that adapts to its container width below the default
5
+ * breakpoint.
6
+ *
7
+ * Use within raw HTML context — HTML-only emails or
8
+ * [MJML ending tags](https://documentation.mjml.io/#ending-tags) like `MjmlRaw`.
9
+ * For MJML context, use `MjmlImage`.
10
+ */
11
+ export declare function HtmlImage({ className, ...restProps }: HtmlImageProps): ReactNode;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import clsx from "clsx";
3
+ import { registerStyles } from "../../styles/registerStyles.js";
4
+ import { css } from "../../utils/css.js";
5
+ /**
6
+ * Renders an `<img>` tag that adapts to its container width below the default
7
+ * breakpoint.
8
+ *
9
+ * Use within raw HTML context — HTML-only emails or
10
+ * [MJML ending tags](https://documentation.mjml.io/#ending-tags) like `MjmlRaw`.
11
+ * For MJML context, use `MjmlImage`.
12
+ */
13
+ export function HtmlImage({ className, ...restProps }) {
14
+ return _jsx("img", { className: clsx("htmlImage", className), ...restProps });
15
+ }
16
+ registerStyles((theme) => css `
17
+ ${theme.breakpoints.default.belowMediaQuery} {
18
+ .htmlImage {
19
+ width: 100%;
20
+ height: auto;
21
+ }
22
+ }
23
+ `);