@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.
Files changed (90) hide show
  1. package/README.md +26 -0
  2. package/lib/__stories__/examples/CustomNestedFooterTheme.stories.d.ts +5 -0
  3. package/lib/__stories__/examples/CustomNestedFooterTheme.stories.js +32 -0
  4. package/lib/__stories__/examples/NotificationEmail.stories.d.ts +5 -0
  5. package/lib/__stories__/examples/NotificationEmail.stories.js +25 -0
  6. package/lib/__stories__/examples/TextWithImageEmail.stories.d.ts +5 -0
  7. package/lib/__stories__/examples/TextWithImageEmail.stories.js +56 -0
  8. package/lib/client/index.d.ts +1 -0
  9. package/lib/client/index.js +1 -0
  10. package/lib/client/renderMailHtml.d.ts +9 -0
  11. package/lib/client/renderMailHtml.js +7 -0
  12. package/lib/components/inlineLink/HtmlInlineLink.d.ts +10 -0
  13. package/lib/components/inlineLink/HtmlInlineLink.js +35 -0
  14. package/lib/components/inlineLink/__stories__/HtmlInlineLink.stories.d.ts +12 -0
  15. package/lib/components/inlineLink/__stories__/HtmlInlineLink.stories.js +56 -0
  16. package/lib/components/mailRoot/MjmlMailRoot.d.ts +22 -0
  17. package/lib/components/mailRoot/MjmlMailRoot.js +20 -0
  18. package/lib/components/mailRoot/__stories__/MjmlMailRoot.stories.d.ts +7 -0
  19. package/lib/components/mailRoot/__stories__/MjmlMailRoot.stories.js +18 -0
  20. package/lib/components/section/MjmlSection.d.ts +15 -0
  21. package/lib/components/section/MjmlSection.js +44 -0
  22. package/lib/components/section/__stories__/MjmlSection.stories.d.ts +9 -0
  23. package/lib/components/section/__stories__/MjmlSection.stories.js +30 -0
  24. package/lib/components/text/HtmlText.d.ts +48 -0
  25. package/lib/components/text/HtmlText.js +47 -0
  26. package/lib/components/text/MjmlText.d.ts +37 -0
  27. package/lib/components/text/MjmlText.js +65 -0
  28. package/lib/components/text/OutlookTextStyleContext.d.ts +9 -0
  29. package/lib/components/text/OutlookTextStyleContext.js +10 -0
  30. package/lib/components/text/__stories__/HtmlText.stories.d.ts +12 -0
  31. package/lib/components/text/__stories__/HtmlText.stories.js +77 -0
  32. package/lib/components/text/__stories__/MjmlText.stories.d.ts +10 -0
  33. package/lib/components/text/__stories__/MjmlText.stories.js +71 -0
  34. package/lib/components/text/__tests__/HtmlText.test.d.ts +1 -0
  35. package/lib/components/text/__tests__/HtmlText.test.js +157 -0
  36. package/lib/components/text/__tests__/MjmlText.test.d.ts +1 -0
  37. package/lib/components/text/__tests__/MjmlText.test.js +112 -0
  38. package/lib/components/text/textStyles.d.ts +12 -0
  39. package/lib/components/text/textStyles.js +69 -0
  40. package/lib/index.d.ts +17 -1
  41. package/lib/index.js +11 -1
  42. package/lib/server/index.d.ts +1 -0
  43. package/lib/server/index.js +1 -0
  44. package/lib/server/renderMailHtml.d.ts +9 -0
  45. package/lib/server/renderMailHtml.js +7 -0
  46. package/lib/server/renderMailHtml.test.d.ts +1 -0
  47. package/lib/server/renderMailHtml.test.js +43 -0
  48. package/lib/storybook/CopyMailHtmlButton.d.ts +4 -0
  49. package/lib/storybook/CopyMailHtmlButton.js +33 -0
  50. package/lib/storybook/MailRendererDecorator.d.ts +7 -0
  51. package/lib/storybook/MailRendererDecorator.js +17 -0
  52. package/lib/storybook/MjmlWarningsPanel.d.ts +7 -0
  53. package/lib/storybook/MjmlWarningsPanel.js +33 -0
  54. package/lib/storybook/UsePublicImageUrlsToggle.d.ts +4 -0
  55. package/lib/storybook/UsePublicImageUrlsToggle.js +14 -0
  56. package/lib/storybook/index.d.ts +2 -0
  57. package/lib/storybook/index.js +9 -0
  58. package/lib/storybook/manager.d.ts +1 -0
  59. package/lib/storybook/manager.js +25 -0
  60. package/lib/storybook/preview.d.ts +5 -0
  61. package/lib/storybook/preview.js +5 -0
  62. package/lib/storybook/replaceImagesWithPublicUrl.d.ts +1 -0
  63. package/lib/storybook/replaceImagesWithPublicUrl.js +15 -0
  64. package/lib/storybook/replaceImagesWithPublicUrl.test.d.ts +1 -0
  65. package/lib/storybook/replaceImagesWithPublicUrl.test.js +55 -0
  66. package/lib/styles/Styles.d.ts +6 -0
  67. package/lib/styles/Styles.js +16 -0
  68. package/lib/styles/registerStyles.d.ts +19 -0
  69. package/lib/styles/registerStyles.js +14 -0
  70. package/lib/theme/ThemeProvider.d.ts +8 -0
  71. package/lib/theme/ThemeProvider.js +17 -0
  72. package/lib/theme/__stories__/ThemeProvider.stories.d.ts +7 -0
  73. package/lib/theme/__stories__/ThemeProvider.stories.js +23 -0
  74. package/lib/theme/createBreakpoint.d.ts +7 -0
  75. package/lib/theme/createBreakpoint.js +11 -0
  76. package/lib/theme/createTheme.d.ts +22 -0
  77. package/lib/theme/createTheme.js +30 -0
  78. package/lib/theme/createTheme.test.d.ts +1 -0
  79. package/lib/theme/createTheme.test.js +52 -0
  80. package/lib/theme/defaultTheme.d.ts +2 -0
  81. package/lib/theme/defaultTheme.js +23 -0
  82. package/lib/theme/responsiveValue.d.ts +31 -0
  83. package/lib/theme/responsiveValue.js +33 -0
  84. package/lib/theme/responsiveValue.test.d.ts +1 -0
  85. package/lib/theme/responsiveValue.test.js +62 -0
  86. package/lib/theme/themeTypes.d.ts +106 -0
  87. package/lib/theme/themeTypes.js +1 -0
  88. package/lib/utils/css.test.d.ts +1 -0
  89. package/lib/utils/css.test.js +26 -0
  90. package/package.json +35 -11
package/lib/index.d.ts CHANGED
@@ -4,7 +4,23 @@ 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";
9
+ export { MjmlMailRoot } from "./components/mailRoot/MjmlMailRoot.js";
10
+ export type { MjmlSectionProps } from "./components/section/MjmlSection.js";
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 { registerStyles } from "./styles/registerStyles.js";
17
+ export { createBreakpoint } from "./theme/createBreakpoint.js";
18
+ export { createTheme } from "./theme/createTheme.js";
19
+ export type { ResponsiveValue } from "./theme/responsiveValue.js";
20
+ export { getDefaultFromResponsiveValue, getResponsiveOverrides } from "./theme/responsiveValue.js";
21
+ export { ThemeProvider, useTheme } from "./theme/ThemeProvider.js";
22
+ export type { TextStyles, TextVariants, TextVariantStyles, Theme, ThemeBackgroundColors, ThemeBreakpoint, ThemeBreakpoints, ThemeColors, ThemeSizes, ThemeText, } from "./theme/themeTypes.js";
7
23
  export { css } from "./utils/css.js";
8
- 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, MjmlSection, type IMjmlSectionProps as MjmlSectionProps, 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";
24
+ export { Mjml, MjmlAccordion, MjmlAccordionElement, type IMjmlAccordionElementProps as MjmlAccordionElementProps, type IMjmlAccordionProps as MjmlAccordionProps, MjmlAccordionText, type IMjmlAccordionTextProps as MjmlAccordionTextProps, MjmlAccordionTitle, type IMjmlAccordionTitleProps as MjmlAccordionTitleProps, MjmlAll, type IMjmlAllProps as MjmlAllProps, MjmlAttributes, type IMjmlAttributesProps as MjmlAttributesProps, MjmlBody, type IMjmlBodyProps as MjmlBodyProps, MjmlBreakpoint, type IMjmlBreakpointProps as MjmlBreakpointProps, MjmlButton, type IMjmlButtonProps as MjmlButtonProps, MjmlCarousel, MjmlCarouselImage, type IMjmlCarouselImageProps as MjmlCarouselImageProps, type IMjmlCarouselProps as MjmlCarouselProps, MjmlClass, type IMjmlClassProps as MjmlClassProps, MjmlColumn, type IMjmlColumnProps as MjmlColumnProps, MjmlDivider, type IMjmlDividerProps as MjmlDividerProps, MjmlFont, type IMjmlFontProps as MjmlFontProps, MjmlGroup, type IMjmlGroupProps as MjmlGroupProps, MjmlHead, type IMjmlHeadProps as MjmlHeadProps, MjmlHero, type IMjmlHeroProps as MjmlHeroProps, MjmlHtmlAttribute, type IMjmlHtmlAttributeProps as MjmlHtmlAttributeProps, MjmlHtmlAttributes, type IMjmlHtmlAttributesProps as MjmlHtmlAttributesProps, MjmlImage, type IMjmlImageProps as MjmlImageProps, MjmlInclude, type IMjmlIncludeProps as MjmlIncludeProps, MjmlNavbar, MjmlNavbarLink, type IMjmlNavbarLinkProps as MjmlNavbarLinkProps, type IMjmlNavbarProps as MjmlNavbarProps, MjmlPreview, type IMjmlPreviewProps as MjmlPreviewProps, type IMjmlProps as MjmlProps, MjmlRaw, type IMjmlRawProps as MjmlRawProps, MjmlSelector, type IMjmlSelectorProps as MjmlSelectorProps, MjmlSocial, MjmlSocialElement, type IMjmlSocialElementProps as MjmlSocialElementProps, type IMjmlSocialProps as MjmlSocialProps, MjmlSpacer, type IMjmlSpacerProps as MjmlSpacerProps, MjmlStyle, type IMjmlStyleProps as MjmlStyleProps, MjmlTable, type IMjmlTableProps as MjmlTableProps, MjmlTitle, type IMjmlTitleProps as MjmlTitleProps, MjmlWrapper, type IMjmlWrapperProps as MjmlWrapperProps, } from "@faire/mjml-react";
9
25
  export { MjmlComment, MjmlConditionalComment, MjmlHtml, MjmlTrackingPixel, MjmlYahooStyle } from "@faire/mjml-react/extensions/index.js";
10
26
  export { renderToMjml } from "@faire/mjml-react/utils/renderToMjml.js";
package/lib/index.js CHANGED
@@ -2,7 +2,17 @@ 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";
6
+ export { MjmlMailRoot } from "./components/mailRoot/MjmlMailRoot.js";
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 { registerStyles } from "./styles/registerStyles.js";
11
+ export { createBreakpoint } from "./theme/createBreakpoint.js";
12
+ export { createTheme } from "./theme/createTheme.js";
13
+ export { getDefaultFromResponsiveValue, getResponsiveOverrides } from "./theme/responsiveValue.js";
14
+ export { ThemeProvider, useTheme } from "./theme/ThemeProvider.js";
5
15
  export { css } from "./utils/css.js";
6
- 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, MjmlSection, MjmlSelector, MjmlSocial, MjmlSocialElement, MjmlSpacer, MjmlStyle, MjmlTable, MjmlText, MjmlTitle, MjmlWrapper, } from "@faire/mjml-react";
16
+ export { Mjml, MjmlAccordion, MjmlAccordionElement, MjmlAccordionText, MjmlAccordionTitle, MjmlAll, MjmlAttributes, MjmlBody, MjmlBreakpoint, MjmlButton, MjmlCarousel, MjmlCarouselImage, MjmlClass, MjmlColumn, MjmlDivider, MjmlFont, MjmlGroup, MjmlHead, MjmlHero, MjmlHtmlAttribute, MjmlHtmlAttributes, MjmlImage, MjmlInclude, MjmlNavbar, MjmlNavbarLink, MjmlPreview, MjmlRaw, MjmlSelector, MjmlSocial, MjmlSocialElement, MjmlSpacer, MjmlStyle, MjmlTable, MjmlTitle, MjmlWrapper, } from "@faire/mjml-react";
7
17
  export { MjmlComment, MjmlConditionalComment, MjmlHtml, MjmlTrackingPixel, MjmlYahooStyle } from "@faire/mjml-react/extensions/index.js";
8
18
  export { renderToMjml } from "@faire/mjml-react/utils/renderToMjml.js";
@@ -0,0 +1 @@
1
+ export { renderMailHtml } from "./renderMailHtml.js";
@@ -0,0 +1 @@
1
+ export { renderMailHtml } from "./renderMailHtml.js";
@@ -0,0 +1,9 @@
1
+ import mjml2html from "mjml";
2
+ import { type ReactElement } from "react";
3
+ type MjmlOptions = Parameters<typeof mjml2html>[1];
4
+ type MjmlWarning = ReturnType<typeof mjml2html>["errors"][number];
5
+ export declare function renderMailHtml(element: ReactElement, options?: MjmlOptions): {
6
+ html: string;
7
+ mjmlWarnings: MjmlWarning[];
8
+ };
9
+ export {};
@@ -0,0 +1,7 @@
1
+ import { renderToMjml } from "@faire/mjml-react/utils/renderToMjml.js";
2
+ import mjml2html from "mjml";
3
+ export function renderMailHtml(element, options) {
4
+ const mjmlString = renderToMjml(element);
5
+ const { html, errors: mjmlWarnings } = mjml2html(mjmlString, { validationLevel: "soft", ...options });
6
+ return { html, mjmlWarnings };
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { MjmlColumn, MjmlText } from "@faire/mjml-react";
3
+ import { describe, expect, it } from "vitest";
4
+ import { MjmlMailRoot } from "../components/mailRoot/MjmlMailRoot.js";
5
+ import { MjmlSection } from "../components/section/MjmlSection.js";
6
+ import { registerStyles } from "../styles/registerStyles.js";
7
+ import { css } from "../utils/css.js";
8
+ import { renderMailHtml } from "./renderMailHtml.js";
9
+ describe("server/renderMailHtml", () => {
10
+ it("produces HTML with a doctype", () => {
11
+ const { html } = renderMailHtml(_jsx(MjmlMailRoot, { children: _jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlText, { children: "Hello" }) }) }) }));
12
+ expect(html.toLowerCase()).toMatch(/^<!doctype html>/);
13
+ });
14
+ it("includes the passed-in text content", () => {
15
+ const textContent = "Welcome to @comet/mail-react";
16
+ const { html } = renderMailHtml(_jsx(MjmlMailRoot, { children: _jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlText, { children: textContent }) }) }) }));
17
+ expect(html).toContain(textContent);
18
+ });
19
+ it("includes registered styles in the rendered HTML", () => {
20
+ registerStyles(css `
21
+ .myComponent {
22
+ color: red;
23
+ }
24
+ `);
25
+ const { html } = renderMailHtml(_jsx(MjmlMailRoot, { children: _jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlText, { children: "Hello" }) }) }) }));
26
+ expect(html).toContain(".myComponent");
27
+ expect(html).toContain("color: red");
28
+ });
29
+ it("includes function-style registered styles resolved with the theme", () => {
30
+ registerStyles((theme) => css `
31
+ .themed {
32
+ width: ${theme.sizes.bodyWidth}px;
33
+ }
34
+ `);
35
+ const { html } = renderMailHtml(_jsx(MjmlMailRoot, { children: _jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlText, { children: "Hello" }) }) }) }));
36
+ expect(html).toContain(".themed");
37
+ expect(html).toContain("width: 600px");
38
+ });
39
+ it("produces no MJML warnings for a valid component tree", () => {
40
+ const { mjmlWarnings } = renderMailHtml(_jsx(MjmlMailRoot, { children: _jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlText, { children: "No warnings expected" }) }) }) }));
41
+ expect(mjmlWarnings).toEqual([]);
42
+ });
43
+ });
@@ -0,0 +1,4 @@
1
+ /** @jsxRuntime classic */
2
+ /** @jsx React.createElement */
3
+ import React from "react";
4
+ export declare function CopyMailHtmlButton(): React.JSX.Element;
@@ -0,0 +1,33 @@
1
+ /** @jsxRuntime classic */
2
+ /** @jsx React.createElement */
3
+ import React, { useCallback, useEffect, useRef, useState } from "react";
4
+ import { Button } from "storybook/internal/components";
5
+ import { useChannel } from "storybook/manager-api";
6
+ const RENDER_RESULT_EVENT = "comet-mail-render-result";
7
+ export function CopyMailHtmlButton() {
8
+ const htmlRef = useRef("");
9
+ const [copied, setCopied] = useState(false);
10
+ const copiedTimeoutRef = useRef(undefined);
11
+ useChannel({
12
+ [RENDER_RESULT_EVENT]: ({ html }) => {
13
+ htmlRef.current = html;
14
+ setCopied(false);
15
+ },
16
+ });
17
+ useEffect(() => {
18
+ return () => {
19
+ if (copiedTimeoutRef.current) {
20
+ clearTimeout(copiedTimeoutRef.current);
21
+ }
22
+ };
23
+ }, []);
24
+ const handleClick = useCallback(async () => {
25
+ await navigator.clipboard.writeText(htmlRef.current);
26
+ setCopied(true);
27
+ if (copiedTimeoutRef.current) {
28
+ clearTimeout(copiedTimeoutRef.current);
29
+ }
30
+ copiedTimeoutRef.current = setTimeout(() => setCopied(false), 2000);
31
+ }, []);
32
+ return (React.createElement(Button, { size: "small", onClick: handleClick }, copied ? "Copied to clipboard!" : "Copy Mail HTML"));
33
+ }
@@ -0,0 +1,7 @@
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;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useChannel, useGlobals } from "storybook/preview-api";
3
+ import { renderMailHtml } from "../client/renderMailHtml.js";
4
+ import { MjmlMailRoot } from "../components/mailRoot/MjmlMailRoot.js";
5
+ import { replaceImagesWithPublicUrl } from "./replaceImagesWithPublicUrl.js";
6
+ const RENDER_RESULT_EVENT = "comet-mail-render-result";
7
+ export function MailRendererDecorator(Story, context) {
8
+ const [globals] = useGlobals();
9
+ const emit = useChannel({});
10
+ const { html: rawHtml, mjmlWarnings } = renderMailHtml(context.parameters.mailRoot === false ? (_jsx(Story, {})) : (_jsx(MjmlMailRoot, { theme: context.parameters.theme, children: _jsx(Story, {}) })));
11
+ for (const warning of mjmlWarnings) {
12
+ console.warn("MJML warning:", warning);
13
+ }
14
+ const html = globals.usePublicImageUrls ? replaceImagesWithPublicUrl(rawHtml) : rawHtml;
15
+ emit(RENDER_RESULT_EVENT, { html, mjmlWarnings });
16
+ return _jsx("div", { dangerouslySetInnerHTML: { __html: html } });
17
+ }
@@ -0,0 +1,7 @@
1
+ /** @jsxRuntime classic */
2
+ /** @jsx React.createElement */
3
+ import React from "react";
4
+ export declare function MjmlWarningsPanel({ active }: {
5
+ active: boolean;
6
+ }): React.JSX.Element;
7
+ export declare function MjmlWarningsPanelTitle(): React.JSX.Element;
@@ -0,0 +1,33 @@
1
+ /** @jsxRuntime classic */
2
+ /** @jsx React.createElement */
3
+ import React, { useState } from "react";
4
+ import { AddonPanel, Badge } from "storybook/internal/components";
5
+ import { useChannel } from "storybook/manager-api";
6
+ const RENDER_RESULT_EVENT = "comet-mail-render-result";
7
+ export function MjmlWarningsPanel({ active }) {
8
+ const [renderResult, setRenderResult] = useState(null);
9
+ useChannel({
10
+ [RENDER_RESULT_EVENT]: (result) => {
11
+ setRenderResult(result);
12
+ },
13
+ });
14
+ return (React.createElement(AddonPanel, { active: active }, renderResult && (React.createElement("div", { style: { padding: 16 } }, renderResult.mjmlWarnings.length === 0 ? (React.createElement("p", { style: { color: "green" } }, "\u2713 No MJML warnings")) : (React.createElement("ul", { style: { margin: 0, padding: 0, listStyle: "none" } }, renderResult.mjmlWarnings.map((warning, index) => (React.createElement("li", { key: index, style: { marginBottom: 8 } },
15
+ React.createElement("strong", null, warning.tagName),
16
+ " (line ",
17
+ warning.line,
18
+ "): ",
19
+ warning.message)))))))));
20
+ }
21
+ export function MjmlWarningsPanelTitle() {
22
+ const [renderResult, setRenderResult] = useState(null);
23
+ useChannel({
24
+ [RENDER_RESULT_EVENT]: (result) => {
25
+ setRenderResult(result);
26
+ },
27
+ });
28
+ const warningCount = renderResult?.mjmlWarnings.length ?? 0;
29
+ const hasWarnings = warningCount > 0;
30
+ return (React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 6 } },
31
+ React.createElement("span", null, "MJML Warnings"),
32
+ React.createElement(Badge, { compact: true, status: hasWarnings ? "negative" : "positive" }, hasWarnings ? warningCount : "✓")));
33
+ }
@@ -0,0 +1,4 @@
1
+ /** @jsxRuntime classic */
2
+ /** @jsx React.createElement */
3
+ import React from "react";
4
+ export declare function UsePublicImageUrlsToggle(): React.JSX.Element;
@@ -0,0 +1,14 @@
1
+ /** @jsxRuntime classic */
2
+ /** @jsx React.createElement */
3
+ import React from "react";
4
+ import { IconButton, TooltipNote, WithTooltip } from "storybook/internal/components";
5
+ import { useGlobals } from "storybook/manager-api";
6
+ export function UsePublicImageUrlsToggle() {
7
+ const [globals, updateGlobals] = useGlobals();
8
+ const isActive = Boolean(globals.usePublicImageUrls);
9
+ return (React.createElement(WithTooltip, { tooltip: React.createElement(TooltipNote, { note: "Helpful to test with real images on external devices that cannot access localhost, e.g. Email on Acid." }), trigger: "hover" },
10
+ React.createElement(IconButton, { size: "small", active: isActive, onClick: () => updateGlobals({ usePublicImageUrls: !isActive }) },
11
+ React.createElement("input", { type: "checkbox", checked: isActive, readOnly: true, style: { pointerEvents: "none", marginLeft: 4 } }),
12
+ "Use public image URLs",
13
+ React.createElement("span", { role: "img", "aria-label": "info" }, "\u2139\uFE0F"))));
14
+ }
@@ -0,0 +1,2 @@
1
+ export declare function managerEntries(existing?: string[]): string[];
2
+ export declare function previewAnnotations(input?: string[]): string[];
@@ -0,0 +1,9 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ const directoryOfThisFile = dirname(fileURLToPath(import.meta.url));
4
+ export function managerEntries(existing = []) {
5
+ return [...existing, resolve(directoryOfThisFile, "manager.js")];
6
+ }
7
+ export function previewAnnotations(input = []) {
8
+ return [...input, resolve(directoryOfThisFile, "preview.js")];
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ /** @jsxRuntime classic */
2
+ /** @jsx React.createElement */
3
+ import React from "react";
4
+ import { addons, types } from "storybook/manager-api";
5
+ import { CopyMailHtmlButton } from "./CopyMailHtmlButton.js";
6
+ import { MjmlWarningsPanel, MjmlWarningsPanelTitle } from "./MjmlWarningsPanel.js";
7
+ import { UsePublicImageUrlsToggle } from "./UsePublicImageUrlsToggle.js";
8
+ const ADDON_ID = "comet-mail-react";
9
+ addons.register(ADDON_ID, () => {
10
+ addons.add(`${ADDON_ID}/copy-html`, {
11
+ type: types.TOOL,
12
+ title: "Copy Mail HTML",
13
+ render: () => React.createElement(CopyMailHtmlButton, null),
14
+ });
15
+ addons.add(`${ADDON_ID}/public-urls`, {
16
+ type: types.TOOL,
17
+ title: "Use public image URLs",
18
+ render: () => React.createElement(UsePublicImageUrlsToggle, null),
19
+ });
20
+ addons.add(`${ADDON_ID}/mjml-warnings`, {
21
+ type: types.PANEL,
22
+ title: () => React.createElement(MjmlWarningsPanelTitle, null),
23
+ render: ({ active }) => React.createElement(MjmlWarningsPanel, { active: Boolean(active) }),
24
+ });
25
+ });
@@ -0,0 +1,5 @@
1
+ import { MailRendererDecorator } from "./MailRendererDecorator.js";
2
+ export declare const decorators: (typeof MailRendererDecorator)[];
3
+ export declare const initialGlobals: {
4
+ usePublicImageUrls: boolean;
5
+ };
@@ -0,0 +1,5 @@
1
+ import { MailRendererDecorator } from "./MailRendererDecorator.js";
2
+ export const decorators = [MailRendererDecorator];
3
+ export const initialGlobals = {
4
+ usePublicImageUrls: false,
5
+ };
@@ -0,0 +1 @@
1
+ export declare function replaceImagesWithPublicUrl(html: string): string;
@@ -0,0 +1,15 @@
1
+ export function replaceImagesWithPublicUrl(html) {
2
+ let seedCounter = 0;
3
+ return html.replace(/<img\b[^>]*>/gi, (imgTag) => {
4
+ const widthMatch = imgTag.match(/\bwidth="(\d+)"/);
5
+ const heightMatch = imgTag.match(/\bheight="(\d+)"/);
6
+ if (!widthMatch || !heightMatch) {
7
+ return imgTag;
8
+ }
9
+ const retinaWidth = parseInt(widthMatch[1], 10) * 2;
10
+ const retinaHeight = parseInt(heightMatch[1], 10) * 2;
11
+ const seed = seedCounter++;
12
+ const publicUrl = `https://picsum.photos/seed/${seed}/${retinaWidth}/${retinaHeight}`;
13
+ return imgTag.replace(/\bsrc="[^"]*"/, `src="${publicUrl}"`);
14
+ });
15
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { replaceImagesWithPublicUrl } from "./replaceImagesWithPublicUrl.js";
3
+ describe("replaceImagesWithPublicUrl", () => {
4
+ it("replaces src with picsum URL using doubled dimensions", () => {
5
+ const html = '<img src="original.jpg" width="300" height="200" />';
6
+ const result = replaceImagesWithPublicUrl(html);
7
+ expect(result).toBe('<img src="https://picsum.photos/seed/0/600/400" width="300" height="200" />');
8
+ });
9
+ it("handles height before width", () => {
10
+ const html = '<img src="original.jpg" height="200" width="300" />';
11
+ const result = replaceImagesWithPublicUrl(html);
12
+ expect(result).toBe('<img src="https://picsum.photos/seed/0/600/400" height="200" width="300" />');
13
+ });
14
+ it("handles attributes before width and height", () => {
15
+ const html = '<img alt="photo" src="original.jpg" width="100" height="50" />';
16
+ const result = replaceImagesWithPublicUrl(html);
17
+ expect(result).toBe('<img alt="photo" src="https://picsum.photos/seed/0/200/100" width="100" height="50" />');
18
+ });
19
+ it("handles attributes between width and height", () => {
20
+ const html = '<img src="original.jpg" width="100" alt="photo" height="50" />';
21
+ const result = replaceImagesWithPublicUrl(html);
22
+ expect(result).toBe('<img src="https://picsum.photos/seed/0/200/100" width="100" alt="photo" height="50" />');
23
+ });
24
+ it("handles attributes after width and height", () => {
25
+ const html = '<img src="original.jpg" width="100" height="50" alt="photo" />';
26
+ const result = replaceImagesWithPublicUrl(html);
27
+ expect(result).toBe('<img src="https://picsum.photos/seed/0/200/100" width="100" height="50" alt="photo" />');
28
+ });
29
+ it("leaves img tags without width untouched", () => {
30
+ const html = '<img src="original.jpg" height="200" />';
31
+ const result = replaceImagesWithPublicUrl(html);
32
+ expect(result).toBe('<img src="original.jpg" height="200" />');
33
+ });
34
+ it("leaves img tags without height untouched", () => {
35
+ const html = '<img src="original.jpg" width="300" />';
36
+ const result = replaceImagesWithPublicUrl(html);
37
+ expect(result).toBe('<img src="original.jpg" width="300" />');
38
+ });
39
+ it("increments seed for each image", () => {
40
+ const html = '<img src="a.jpg" width="100" height="100" /><img src="b.jpg" width="200" height="150" />';
41
+ const result = replaceImagesWithPublicUrl(html);
42
+ expect(result).toBe('<img src="https://picsum.photos/seed/0/200/200" width="100" height="100" />' +
43
+ '<img src="https://picsum.photos/seed/1/400/300" width="200" height="150" />');
44
+ });
45
+ it("returns unchanged html when there are no img tags", () => {
46
+ const html = "<p>Hello world</p>";
47
+ const result = replaceImagesWithPublicUrl(html);
48
+ expect(result).toBe("<p>Hello world</p>");
49
+ });
50
+ it("handles non-self-closing img tags", () => {
51
+ const html = '<img src="original.jpg" width="100" height="50">';
52
+ const result = replaceImagesWithPublicUrl(html);
53
+ expect(result).toBe('<img src="https://picsum.photos/seed/0/200/100" width="100" height="50">');
54
+ });
55
+ });
@@ -0,0 +1,6 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * Internal component that iterates the styles registry and renders one
4
+ * `<MjmlStyle>` element per entry.
5
+ */
6
+ export declare function Styles(): ReactNode;
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { MjmlStyle } from "@faire/mjml-react";
3
+ import { useTheme } from "../theme/ThemeProvider.js";
4
+ import { getRegisteredStyles } from "./registerStyles.js";
5
+ /**
6
+ * Internal component that iterates the styles registry and renders one
7
+ * `<MjmlStyle>` element per entry.
8
+ */
9
+ export function Styles() {
10
+ const theme = useTheme();
11
+ const entries = Array.from(getRegisteredStyles().values());
12
+ return (_jsx(_Fragment, { children: entries.map((entry, index) => {
13
+ const cssString = typeof entry.styles === "function" ? entry.styles(theme) : entry.styles;
14
+ return (_jsx(MjmlStyle, { ...entry.mjmlStyleProps, children: cssString }, index));
15
+ }) }));
16
+ }
@@ -0,0 +1,19 @@
1
+ import type { IMjmlStyleProps } from "@faire/mjml-react";
2
+ import type { Theme } from "../theme/themeTypes.js";
3
+ import type { css } from "../utils/css.js";
4
+ type StylesPayload = ReturnType<typeof css> | ((theme: Theme) => ReturnType<typeof css>);
5
+ type MjmlStyleOptions = Partial<Omit<IMjmlStyleProps, "children">>;
6
+ interface StyleRegistryEntry {
7
+ styles: StylesPayload;
8
+ mjmlStyleProps?: MjmlStyleOptions;
9
+ }
10
+ /**
11
+ * Registers CSS styles that will be rendered in the email head as `<mj-style>` elements.
12
+ *
13
+ * Styles can be static CSS strings (via the `css` tagged template) or functions that
14
+ * receive the theme and return a CSS string.
15
+ */
16
+ export declare function registerStyles(styles: StylesPayload, mjmlStyleProps?: MjmlStyleOptions): void;
17
+ /** Returns all registered style entries. */
18
+ export declare function getRegisteredStyles(): ReadonlyMap<StylesPayload, StyleRegistryEntry>;
19
+ export {};
@@ -0,0 +1,14 @@
1
+ const registry = new Map();
2
+ /**
3
+ * Registers CSS styles that will be rendered in the email head as `<mj-style>` elements.
4
+ *
5
+ * Styles can be static CSS strings (via the `css` tagged template) or functions that
6
+ * receive the theme and return a CSS string.
7
+ */
8
+ export function registerStyles(styles, mjmlStyleProps) {
9
+ registry.set(styles, { styles, mjmlStyleProps });
10
+ }
11
+ /** Returns all registered style entries. */
12
+ export function getRegisteredStyles() {
13
+ return registry;
14
+ }
@@ -0,0 +1,8 @@
1
+ import { type PropsWithChildren, type ReactNode } from "react";
2
+ import type { Theme } from "./themeTypes.js";
3
+ export declare function ThemeProvider({ theme, children }: PropsWithChildren<{
4
+ theme: Theme;
5
+ }>): ReactNode;
6
+ export declare function useTheme(): Theme;
7
+ /** Returns the current theme or `null` if no `ThemeProvider` is present. Internal use only. */
8
+ export declare function useOptionalTheme(): Theme | null;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
3
+ const ThemeContext = createContext(null);
4
+ export function ThemeProvider({ theme, children }) {
5
+ return _jsx(ThemeContext.Provider, { value: theme, children: children });
6
+ }
7
+ export function useTheme() {
8
+ const theme = useContext(ThemeContext);
9
+ if (theme === null) {
10
+ throw new Error("useTheme must be used within a ThemeProvider (or MjmlMailRoot).");
11
+ }
12
+ return theme;
13
+ }
14
+ /** Returns the current theme or `null` if no `ThemeProvider` is present. Internal use only. */
15
+ export function useOptionalTheme() {
16
+ return useContext(ThemeContext);
17
+ }
@@ -0,0 +1,7 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { ThemeProvider } from "../ThemeProvider.js";
3
+ type Story = StoryObj<typeof ThemeProvider>;
4
+ declare const config: Meta<typeof ThemeProvider>;
5
+ export default config;
6
+ export declare const Basic: Story;
7
+ export declare const CustomTheme: Story;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { MjmlColumn, MjmlText } from "@faire/mjml-react";
3
+ import { MjmlSection } from "../../components/section/MjmlSection.js";
4
+ import { createBreakpoint } from "../createBreakpoint.js";
5
+ import { createTheme } from "../createTheme.js";
6
+ import { ThemeProvider } from "../ThemeProvider.js";
7
+ const config = {
8
+ title: "Components/ThemeProvider",
9
+ component: ThemeProvider,
10
+ tags: ["autodocs"],
11
+ };
12
+ export default config;
13
+ export const Basic = {
14
+ render: () => (_jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlText, { children: "Using the default theme from the decorator." }) }) })),
15
+ };
16
+ const narrowTheme = createTheme({
17
+ sizes: { bodyWidth: 400 },
18
+ breakpoints: { mobile: createBreakpoint(360) },
19
+ });
20
+ export const CustomTheme = {
21
+ parameters: { theme: narrowTheme },
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
+ };
@@ -0,0 +1,7 @@
1
+ import type { ThemeBreakpoint } from "./themeTypes.js";
2
+ /**
3
+ * Constructs a `ThemeBreakpoint` for use in `createTheme` overrides or
4
+ * `ThemeBreakpoints` module augmentation. Guarantees the `belowMediaQuery`
5
+ * string is always formatted correctly.
6
+ */
7
+ export declare function createBreakpoint(value: number): ThemeBreakpoint;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Constructs a `ThemeBreakpoint` for use in `createTheme` overrides or
3
+ * `ThemeBreakpoints` module augmentation. Guarantees the `belowMediaQuery`
4
+ * string is always formatted correctly.
5
+ */
6
+ export function createBreakpoint(value) {
7
+ return {
8
+ value,
9
+ belowMediaQuery: `@media (max-width: ${value - 1}px)`,
10
+ };
11
+ }
@@ -0,0 +1,22 @@
1
+ import type { Theme, ThemeBackgroundColors, ThemeBreakpoints, ThemeColors, ThemeSizes, ThemeText } from "./themeTypes.js";
2
+ type CreateThemeOverrides = {
3
+ sizes?: Partial<ThemeSizes>;
4
+ breakpoints?: Partial<ThemeBreakpoints>;
5
+ text?: Partial<ThemeText>;
6
+ colors?: {
7
+ background?: Partial<ThemeBackgroundColors>;
8
+ } & Partial<Omit<ThemeColors, "background">>;
9
+ };
10
+ /**
11
+ * Creates a complete theme by merging optional overrides onto the default
12
+ * theme values.
13
+ *
14
+ * Breakpoint overrides must be `ThemeBreakpoint` objects constructed via
15
+ * `createBreakpoint`. Arbitrary breakpoint keys from `ThemeBreakpoints`
16
+ * module augmentation are supported and flow through to the result.
17
+ *
18
+ * `breakpoints.default` is always auto-derived from `sizes.bodyWidth` unless
19
+ * explicitly overridden.
20
+ */
21
+ export declare function createTheme(overrides?: CreateThemeOverrides): Theme;
22
+ export {};
@@ -0,0 +1,30 @@
1
+ import { createBreakpoint } from "./createBreakpoint.js";
2
+ import { defaultTheme } from "./defaultTheme.js";
3
+ /**
4
+ * Creates a complete theme by merging optional overrides onto the default
5
+ * theme values.
6
+ *
7
+ * Breakpoint overrides must be `ThemeBreakpoint` objects constructed via
8
+ * `createBreakpoint`. Arbitrary breakpoint keys from `ThemeBreakpoints`
9
+ * module augmentation are supported and flow through to the result.
10
+ *
11
+ * `breakpoints.default` is always auto-derived from `sizes.bodyWidth` unless
12
+ * explicitly overridden.
13
+ */
14
+ export function createTheme(overrides) {
15
+ const resolvedSizes = { ...defaultTheme.sizes, ...overrides?.sizes };
16
+ return {
17
+ sizes: resolvedSizes,
18
+ breakpoints: {
19
+ ...defaultTheme.breakpoints,
20
+ default: createBreakpoint(resolvedSizes.bodyWidth),
21
+ ...overrides?.breakpoints,
22
+ },
23
+ text: { ...defaultTheme.text, ...overrides?.text },
24
+ colors: {
25
+ ...defaultTheme.colors,
26
+ ...overrides?.colors,
27
+ background: { ...defaultTheme.colors.background, ...overrides?.colors?.background },
28
+ },
29
+ };
30
+ }
@@ -0,0 +1 @@
1
+ export {};