@comet/mail-react 9.0.0-beta.4 → 9.0.0-beta.6

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 (99) hide show
  1. package/README.md +68 -9
  2. package/lib/blocks/pixelImage/HtmlPixelImageBlock.js +2 -11
  3. package/lib/blocks/pixelImage/MjmlPixelImageBlock.d.ts +2 -2
  4. package/lib/blocks/pixelImage/MjmlPixelImageBlock.js +1 -10
  5. package/lib/blocks.generated.d.ts +1 -0
  6. package/lib/components/button/HtmlButton.d.ts +9 -0
  7. package/lib/components/button/HtmlButton.js +58 -0
  8. package/lib/components/button/MjmlButton.d.ts +10 -0
  9. package/lib/components/button/MjmlButton.js +91 -0
  10. package/lib/components/button/buttonProps.d.ts +35 -0
  11. package/lib/components/button/defaultButtonStyles.d.ts +2 -0
  12. package/lib/components/button/defaultButtonStyles.js +9 -0
  13. package/lib/components/button/generateResponsiveButtonCss.d.ts +8 -0
  14. package/lib/components/button/generateResponsiveButtonCss.js +21 -0
  15. package/lib/components/divider/HtmlDivider.d.ts +9 -0
  16. package/lib/components/divider/HtmlDivider.js +43 -0
  17. package/lib/components/divider/MjmlDivider.d.ts +7 -0
  18. package/lib/components/divider/MjmlDivider.js +13 -0
  19. package/lib/components/divider/defaultDividerStyles.d.ts +2 -0
  20. package/lib/components/divider/defaultDividerStyles.js +4 -0
  21. package/lib/components/divider/dividerProps.d.ts +51 -0
  22. package/lib/components/divider/generateResponsiveDividerCss.d.ts +8 -0
  23. package/lib/components/divider/generateResponsiveDividerCss.js +15 -0
  24. package/lib/components/image/HtmlImage.d.ts +11 -0
  25. package/lib/components/image/HtmlImage.js +23 -0
  26. package/lib/components/image/MjmlImage.d.ts +10 -0
  27. package/lib/components/image/MjmlImage.js +22 -0
  28. package/lib/components/inlineLink/HtmlInlineLink.d.ts +8 -1
  29. package/lib/components/mailRoot/MjmlMailRoot.d.ts +3 -2
  30. package/lib/components/section/MjmlSection.d.ts +7 -1
  31. package/lib/components/text/HtmlText.d.ts +3 -0
  32. package/lib/components/text/MjmlText.d.ts +3 -0
  33. package/lib/components/text/textStyles.d.ts +1 -3
  34. package/lib/components/text/textStyles.js +20 -70
  35. package/lib/components/wrapper/MjmlWrapper.d.ts +9 -2
  36. package/lib/components/wrapper/MjmlWrapper.js +1 -1
  37. package/lib/index.d.ts +14 -2
  38. package/lib/index.js +7 -1
  39. package/lib/styles/generateResponsiveVariantCss.d.ts +26 -0
  40. package/lib/styles/generateResponsiveVariantCss.js +66 -0
  41. package/lib/theme/createTheme.d.ts +3 -1
  42. package/lib/theme/createTheme.js +2 -0
  43. package/lib/theme/defaultTheme.js +4 -0
  44. package/lib/theme/themeTypes.d.ts +98 -0
  45. package/package.json +14 -7
  46. package/lib/__stories__/examples/CustomBackgroundForFooter.stories.d.ts +0 -5
  47. package/lib/__stories__/examples/CustomBackgroundForFooter.stories.js +0 -32
  48. package/lib/__stories__/examples/CustomNestedFooterTheme.stories.d.ts +0 -5
  49. package/lib/__stories__/examples/CustomNestedFooterTheme.stories.js +0 -32
  50. package/lib/__stories__/examples/NotificationEmail.stories.d.ts +0 -5
  51. package/lib/__stories__/examples/NotificationEmail.stories.js +0 -25
  52. package/lib/__stories__/layout-patterns/AsymmetricTwoColumnLayout.stories.d.ts +0 -6
  53. package/lib/__stories__/layout-patterns/AsymmetricTwoColumnLayout.stories.js +0 -107
  54. package/lib/__stories__/layout-patterns/SymmetricFourColumnLayout.stories.d.ts +0 -4
  55. package/lib/__stories__/layout-patterns/SymmetricFourColumnLayout.stories.js +0 -58
  56. package/lib/__stories__/layout-patterns/SymmetricThreeColumnLayout.stories.d.ts +0 -5
  57. package/lib/__stories__/layout-patterns/SymmetricThreeColumnLayout.stories.js +0 -104
  58. package/lib/__stories__/layout-patterns/SymmetricTwoColumnLayout.stories.d.ts +0 -4
  59. package/lib/__stories__/layout-patterns/SymmetricTwoColumnLayout.stories.js +0 -47
  60. package/lib/blocks/pixelImage/__stories__/HtmlPixelImageBlock.stories.d.ts +0 -7
  61. package/lib/blocks/pixelImage/__stories__/HtmlPixelImageBlock.stories.js +0 -54
  62. package/lib/blocks/pixelImage/__stories__/MjmlPixelImageBlock.stories.d.ts +0 -7
  63. package/lib/blocks/pixelImage/__stories__/MjmlPixelImageBlock.stories.js +0 -54
  64. package/lib/blocks/pixelImage/__stories__/exampleBlockData.d.ts +0 -2
  65. package/lib/blocks/pixelImage/__stories__/exampleBlockData.js +0 -19
  66. package/lib/blocks/pixelImage/__tests__/usePixelImageBlockConfig.test.js +0 -21
  67. package/lib/blocks/pixelImage/__tests__/usePixelImageBlockData.test.js +0 -205
  68. package/lib/components/inlineLink/__stories__/HtmlInlineLink.stories.d.ts +0 -12
  69. package/lib/components/inlineLink/__stories__/HtmlInlineLink.stories.js +0 -56
  70. package/lib/components/mailRoot/__stories__/MjmlMailRoot.stories.d.ts +0 -7
  71. package/lib/components/mailRoot/__stories__/MjmlMailRoot.stories.js +0 -18
  72. package/lib/components/section/__stories__/MjmlSection.stories.d.ts +0 -9
  73. package/lib/components/section/__stories__/MjmlSection.stories.js +0 -30
  74. package/lib/components/text/__stories__/HtmlText.stories.d.ts +0 -12
  75. package/lib/components/text/__stories__/HtmlText.stories.js +0 -77
  76. package/lib/components/text/__stories__/MjmlText.stories.d.ts +0 -10
  77. package/lib/components/text/__stories__/MjmlText.stories.js +0 -71
  78. package/lib/components/text/__tests__/HtmlText.test.d.ts +0 -1
  79. package/lib/components/text/__tests__/HtmlText.test.js +0 -157
  80. package/lib/components/text/__tests__/MjmlText.test.d.ts +0 -1
  81. package/lib/components/text/__tests__/MjmlText.test.js +0 -112
  82. package/lib/components/wrapper/__stories__/MjmlWrapper.stories.d.ts +0 -10
  83. package/lib/components/wrapper/__stories__/MjmlWrapper.stories.js +0 -36
  84. package/lib/config/ConfigProvider.test.d.ts +0 -8
  85. package/lib/config/ConfigProvider.test.js +0 -30
  86. package/lib/server/renderMailHtml.test.d.ts +0 -1
  87. package/lib/server/renderMailHtml.test.js +0 -52
  88. package/lib/storybook/replaceImagesWithPublicUrl.test.d.ts +0 -1
  89. package/lib/storybook/replaceImagesWithPublicUrl.test.js +0 -55
  90. package/lib/theme/__stories__/ThemeProvider.stories.d.ts +0 -7
  91. package/lib/theme/__stories__/ThemeProvider.stories.js +0 -23
  92. package/lib/theme/createTheme.test.d.ts +0 -1
  93. package/lib/theme/createTheme.test.js +0 -52
  94. package/lib/theme/responsiveValue.test.d.ts +0 -1
  95. package/lib/theme/responsiveValue.test.js +0 -62
  96. package/lib/utils/css.test.d.ts +0 -1
  97. package/lib/utils/css.test.js +0 -26
  98. /package/lib/{blocks/pixelImage/__tests__/usePixelImageBlockConfig.test.d.ts → components/button/buttonProps.js} +0 -0
  99. /package/lib/{blocks/pixelImage/__tests__/usePixelImageBlockData.test.d.ts → components/divider/dividerProps.js} +0 -0
package/README.md CHANGED
@@ -17,6 +17,7 @@ We extend `@faire/mjml-react` rather than fork it. A few rules keep that working
17
17
  - **Additive only.** A custom component must work everywhere the base did. Only add props — never remove or rename them, and don't require new providers or context. For theme access, prefer `useOptionalTheme()` over `useTheme()`.
18
18
  - **Wrap, don't reimplement.** Custom components delegate to `@faire/mjml-react`. Less to maintain, and we stay close to upstream behaviour.
19
19
  - **One export per name.** When we ship a custom version, it replaces the re-export. Consumers should never need to import from `@faire/mjml-react` directly.
20
+ - **Public-facing documentation doesn't reference `@faire/mjml-react`.** TSDoc, story descriptions, and changeset entries describe behavior in terms of this package only. Internal places (feature READMEs, commit messages) can mention upstream when it adds maintainer context.
20
21
 
21
22
  ### Styling
22
23
 
@@ -24,30 +25,88 @@ We extend `@faire/mjml-react` rather than fork it. A few rules keep that working
24
25
  - **Media queries** via `registerStyles` are progressive enhancement for mobile, where modern CSS (flex, grid) is fine. Use `theme.breakpoints.*.belowMediaQuery` (max-width queries) to target viewports below a breakpoint.
25
26
  - **BEM, camelCase blocks.** Block `mjmlSection`, element `mjmlSection__item`, modifier `mjmlSection--indented`. Every component applies its block class and merges any consumer-provided `className` with `clsx`: `clsx("mjmlSection", className)`.
26
27
 
28
+ ### Theme variants
29
+
30
+ Themed components expose their styling through the theme: a flat base entry and an optional, consumer-extensible variants map.
31
+
32
+ - **Base theme entry covers the unstyled use.** Components render from `theme.<component>` when no variant is picked. The `variant` prop is optional; the package ships no built-in variants.
33
+ - **Variants are declared by the consumer.** Variant names are added via TypeScript module augmentation against the variant-name type for that component. A `defaultVariant` on the theme entry makes one of them the implicit pick.
34
+ - **Responsive values appear only inside variants.** Base entries use plain types per property. Variant entries can declare a `ResponsiveValue<T>` — a `{ default, ...overrides }` shape with override keys drawn from `theme.breakpoints`.
35
+
27
36
  ### File layout
28
37
 
29
38
  - **Location.** Custom components live in `src/components/<concern>/` (e.g. `src/components/section/MjmlSection.tsx`).
30
39
  - **File order.** Imports → types/props → component → styles. `registerStyles` calls go below the component.
31
40
  - **Module format.** ESM only (`type: module`). Use `.js` extensions in imports.
32
- - **TSDoc.** Short TSDoc — one line where possible — on exported components and props.
41
+ - **TSDoc.** Short — one line where possible — on exported components and props.
42
+ - **Prop defaults.** Add `@defaultValue` when the default isn't obvious from the type — runtime defaults, and theme-derived ones the consumer can't see in the shipped types.
43
+ - **Phrasing a default.** Name where the value comes from, not the value itself. One theme token: name it in backticks, as in "The theme's `colors.background.content`". Computed from several: name what it depends on, as in "The theme's `divider` height for the active variant".
44
+ - **Defaults on inherited props.** Redeclare the prop locally (`Omit` it from the base, re-add it with the same type) so the tag is on the prop itself. Never document the default in the component's TSDoc.
33
45
 
34
46
  ### Storybook
35
47
 
36
48
  Every custom component has a story in `src/components/<concern>/__stories__/<Component>.stories.tsx`. Wrap non-section stories in `<MjmlSection indent>` to show a realistic indented layout.
37
49
 
38
- ### Changesets
50
+ Placeholder images use the `picsum.photos` seed URL pattern — `https://picsum.photos/seed/<seed>/<width>/<height>` — so the same seed renders the same image on every build.
39
51
 
40
- A changeset describes what changes for the consumer. If the public API and end-user behavior are unchanged, no changeset is needed.
52
+ ### Changesets
41
53
 
42
54
  When a custom component replaces a re-export, describe the change against the previous re-export — the added props, features, or behavior. Consumers don't need to know the internal component is new; they only see what's different in the API they use.
43
55
 
44
- ## Living document
56
+ ## Internal documentation per feature
57
+
58
+ Features substantial enough to live in their own directory should have a `README.md` that describes the current state and how the feature changes it — and, when a maintainer might assume otherwise, the boundaries it deliberately doesn't cross. These READMEs are internal — written for the people (and agents) maintaining the code, not for consumers. End-user usage docs live elsewhere (see [Consumer-facing documentation](#consumer-facing-documentation)).
59
+
60
+ ### What is a feature
61
+
62
+ A feature is any self-contained unit of behavior worth describing on its own — a component (`InlineLink`), a utility (`css` helper), an addon (the Storybook addon), or the package itself. Features nest: this README documents `@comet/mail-react` as a feature, and the components inside it are features in their own right.
63
+
64
+ A feature README describes **only its own feature**. It does not describe parent features that contain it, nor sub-features it contains — each of those has its own README.
65
+
66
+ ### Where they live
67
+
68
+ A feature that warrants a README is organized as a directory, with the README at the directory root (e.g. `src/components/inline-link/README.md`). Small features that live as a single file inside a parent don't need their own README — they're just part of the parent. Promote a file to a directory at the same time you give it a README.
69
+
70
+ ### What goes in a feature README
71
+
72
+ **Title.** Use the exact identifier when the feature is a single component or function (e.g. `MjmlSection`); otherwise use a sentence-case name (e.g. _Inline link_).
73
+
74
+ Two sections, in order. Only the intro is required.
75
+
76
+ 1. **Intro.** One short paragraph: the current state, and how the feature changes it. Describe the current state as plain facts — what's there, what's required, what's missing — rather than framing it as "a problem the feature solves". Maintainers read this to work on the code, not to be sold on the feature's existence.
77
+ 2. **Non-goals** (optional). Things the feature deliberately doesn't do — only when a reader would reasonably assume it does, typically because the feature's name, its domain, or a sibling feature suggests so. The test: would a maintainer look here for this and be surprised it's missing? If not, leave it out. Skip the obvious; skip future work; skip rejected alternatives — those belong in the commit that made the choice.
78
+
79
+ Write each bullet as a noun phrase naming the thing not done (`Not a heading component`, `No CSS variables`). Add a single follow-up sentence only when the reader needs to be redirected to the alternative or told why.
80
+
81
+ Other sections should be rare — only when content is durable, feature-specific, and doesn't fit the two above. Specifically not warranted: no Architecture (the code shows it), no Design decisions (commits carry them), no Usage (consumer docs), no Dependencies (imports show them; non-obvious cross-feature coupling belongs in the intro).
82
+
83
+ **Express the rule, not the code.** Every line should say something the code doesn't. Don't restate type signatures, prop lists, formulas, or control flow — the code already shows those.
84
+
85
+ **Length.** Simple feature → one paragraph. Complex feature → one screen, no scrolling. Past a screen and you're probably duplicating commit history or describing what the code shows.
86
+
87
+ ### Template
88
+
89
+ ```md
90
+ # <feature-name>
91
+
92
+ <One short paragraph: the current state, and how the feature changes it.>
93
+
94
+ ## Non-goals <!-- only if any -->
95
+
96
+ - <Noun phrase naming what the feature deliberately doesn't do.> <Optional follow-up sentence pointing to the alternative or stating the rationale.>
97
+ ```
98
+
99
+ ### Living documents
100
+
101
+ Feature READMEs (this one included) should stay current. Update them in the same PR as any change that makes them inaccurate or adds context worth recording.
102
+
103
+ ## Usage documentation
45
104
 
46
- If a change reverses a decision or shifts a convention recorded here, update this README in the same PR.
105
+ Two artifacts document how to _use_ this package, for two readers:
47
106
 
48
- ## Consumer-facing companions
107
+ - **Docs** — [docs/docs/3-features-modules/13-building-html-emails/](../../docs/docs/3-features-modules/13-building-html-emails/) — for people building emails with the package.
108
+ - **Agent skill** — [skills/comet-mail-react/SKILL.md](../../skills/comet-mail-react/SKILL.md) — the same guidance for AI agents working in a project that uses this package.
49
109
 
50
- - Docs: [docs/docs/3-features-modules/13-building-html-emails/](../../docs/docs/3-features-modules/13-building-html-emails/)
51
- - Agent skill: [skills/comet-mail-react/SKILL.md](../../skills/comet-mail-react/SKILL.md)
110
+ Both teach the same things: what the package is for, how to build emails with it, the patterns to reach for, and the email-client pitfalls — markup that's fine in a browser can render wrong in clients like Outlook. They differ in one respect — an agent already has the package's types and TSDoc in its project, so the skill doesn't restate type signatures, prop lists, or defaults; those live in the types and TSDoc, and it spends its words on everything they can't convey on their own.
52
111
 
53
- When a change here affects usage patterns, component APIs, or styling conventions, update these alongside the library change.
112
+ When a change affects how the package is used — components added/removed/renamed, changed behavior, new usage patterns, or styling conventions update both in the same PR; consider a separate docs commit so reviewers can take them on their own.
@@ -1,7 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import clsx from "clsx";
3
- import { registerStyles } from "../../styles/registerStyles.js";
4
- import { css } from "../../utils/css.js";
3
+ import { HtmlImage } from "../../components/image/HtmlImage.js";
5
4
  import { usePixelImageBlockData } from "./usePixelImageBlockData.js";
6
5
  /**
7
6
  * Renders a pixel-image from the DAM as a raw `<img>` tag.
@@ -15,13 +14,5 @@ export function HtmlPixelImageBlock({ data, width, largestPossibleRenderWidth, a
15
14
  if (!imageData) {
16
15
  return null;
17
16
  }
18
- return (_jsx("img", { src: imageData.imageUrl, width: imageData.defaultRenderWidth, height: imageData.desktopImageHeight, alt: imageData.alt, title: imageData.title, className: clsx("htmlPixelImageBlock", className), ...imgProps }));
17
+ return (_jsx(HtmlImage, { src: imageData.imageUrl, width: imageData.defaultRenderWidth, height: imageData.desktopImageHeight, alt: imageData.alt, title: imageData.title, className: clsx("htmlPixelImageBlock", className), ...imgProps }));
19
18
  }
20
- registerStyles((theme) => css `
21
- ${theme.breakpoints.default.belowMediaQuery} {
22
- .htmlPixelImageBlock {
23
- width: 100%;
24
- height: auto;
25
- }
26
- }
27
- `);
@@ -1,7 +1,7 @@
1
- import { type IMjmlImageProps } from "@faire/mjml-react";
2
1
  import type { ReactNode } from "react";
2
+ import { type MjmlImageProps } from "../../components/image/MjmlImage.js";
3
3
  import type { PixelImageBlockBaseProps } from "./common.js";
4
- export type MjmlPixelImageBlockProps = Omit<IMjmlImageProps, "src" | "width" | "height"> & PixelImageBlockBaseProps;
4
+ export type MjmlPixelImageBlockProps = Omit<MjmlImageProps, "src" | "width" | "height"> & PixelImageBlockBaseProps;
5
5
  /**
6
6
  * Renders a pixel-image from the DAM as `MjmlImage`. Must be placed within an
7
7
  * `MjmlColumn`. For raw HTML context, use `HtmlPixelImageBlock`.
@@ -1,8 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { MjmlImage } from "@faire/mjml-react";
3
2
  import clsx from "clsx";
4
- import { registerStyles } from "../../styles/registerStyles.js";
5
- import { css } from "../../utils/css.js";
3
+ import { MjmlImage } from "../../components/image/MjmlImage.js";
6
4
  import { usePixelImageBlockData } from "./usePixelImageBlockData.js";
7
5
  /**
8
6
  * Renders a pixel-image from the DAM as `MjmlImage`. Must be placed within an
@@ -15,10 +13,3 @@ export function MjmlPixelImageBlock({ data, width, largestPossibleRenderWidth, a
15
13
  }
16
14
  return (_jsx(MjmlImage, { src: imageData.imageUrl, width: imageData.defaultRenderWidth, height: imageData.desktopImageHeight, alt: imageData.alt, title: imageData.title, className: clsx("mjmlPixelImageBlock", className), ...imageProps }));
17
15
  }
18
- registerStyles((theme) => css `
19
- ${theme.breakpoints.default.belowMediaQuery} {
20
- .mjmlPixelImageBlock img {
21
- height: auto !important;
22
- }
23
- }
24
- `);
@@ -54,6 +54,7 @@ export interface EmailLinkBlockData {
54
54
  export interface ExternalLinkBlockData {
55
55
  targetUrl?: string;
56
56
  openInNewWindow: boolean;
57
+ noFollow: boolean;
57
58
  }
58
59
  export interface InternalLinkBlockData {
59
60
  targetPageAnchor?: string;
@@ -0,0 +1,9 @@
1
+ import type { AnchorHTMLAttributes, ReactNode } from "react";
2
+ import type { Theme } from "../../theme/themeTypes.js";
3
+ import type { ButtonProps } from "./buttonProps.js";
4
+ export type HtmlButtonProps = ButtonProps & Omit<AnchorHTMLAttributes<HTMLAnchorElement>, keyof ButtonProps>;
5
+ /**
6
+ * Themed button for use inside MJML ending tags or outside of the MJML context.
7
+ */
8
+ export declare function HtmlButton({ variant: variantProp, fullWidth, target, href, className, style, children, ...restProps }: HtmlButtonProps): ReactNode;
9
+ export declare function generateHtmlButtonStyles(theme: Theme): string;
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import clsx from "clsx";
3
+ import { registerStyles } from "../../styles/registerStyles.js";
4
+ import { defaultTheme } from "../../theme/defaultTheme.js";
5
+ import { getDefaultOrUndefined } from "../../theme/responsiveValue.js";
6
+ import { useOptionalTheme } from "../../theme/ThemeProvider.js";
7
+ import { defaultButtonStyles } from "./defaultButtonStyles.js";
8
+ import { generateResponsiveButtonCss } from "./generateResponsiveButtonCss.js";
9
+ /**
10
+ * Themed button for use inside MJML ending tags or outside of the MJML context.
11
+ */
12
+ export function HtmlButton({ variant: variantProp, fullWidth, target = "_blank", href = "#", className, style, children, ...restProps }) {
13
+ const theme = useOptionalTheme();
14
+ if (theme === null && variantProp !== undefined) {
15
+ throw new Error("The `variant` prop requires being wrapped in a ThemeProvider or MjmlMailRoot.");
16
+ }
17
+ const themeButton = theme?.button ?? defaultButtonStyles;
18
+ const textStyles = theme?.text ?? defaultTheme.text;
19
+ const { defaultVariant, variants, ...baseStyles } = themeButton;
20
+ const activeVariant = variantProp ?? defaultVariant;
21
+ const variantStyles = activeVariant ? variants?.[activeVariant] : undefined;
22
+ const mergedStyles = { ...baseStyles, ...variantStyles };
23
+ const backgroundColor = getDefaultOrUndefined(mergedStyles.backgroundColor);
24
+ const borderRadius = getDefaultOrUndefined(mergedStyles.borderRadius);
25
+ const innerPadding = getDefaultOrUndefined(mergedStyles.padding);
26
+ const cellStyle = {
27
+ border: getDefaultOrUndefined(mergedStyles.border),
28
+ borderRadius,
29
+ backgroundColor,
30
+ msoPaddingAlt: innerPadding,
31
+ };
32
+ const anchorStyle = {
33
+ display: fullWidth ? "block" : "inline-block",
34
+ boxSizing: fullWidth ? "border-box" : undefined,
35
+ width: fullWidth ? "100%" : undefined,
36
+ backgroundColor,
37
+ backgroundImage: getDefaultOrUndefined(mergedStyles.backgroundImage),
38
+ color: getDefaultOrUndefined(mergedStyles.color),
39
+ fontFamily: getDefaultOrUndefined(mergedStyles.fontFamily) ?? textStyles.fontFamily,
40
+ fontSize: getDefaultOrUndefined(mergedStyles.fontSize) ?? textStyles.fontSize,
41
+ fontWeight: getDefaultOrUndefined(mergedStyles.fontWeight) ?? textStyles.fontWeight,
42
+ lineHeight: getDefaultOrUndefined(mergedStyles.lineHeight) ?? textStyles.lineHeight,
43
+ margin: 0,
44
+ textAlign: "center",
45
+ textDecoration: "none",
46
+ padding: innerPadding,
47
+ borderRadius,
48
+ msoPaddingAlt: "0px",
49
+ ...style,
50
+ };
51
+ return (_jsx("table", { role: "presentation", cellPadding: 0, cellSpacing: 0, border: 0, width: fullWidth ? "100%" : undefined, className: clsx("htmlButton", activeVariant && `htmlButton--${activeVariant}`, fullWidth && "htmlButton--fullWidth", className), style: { borderCollapse: "separate", lineHeight: "100%", msoLineHeightRule: "exactly" }, children: _jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { align: "center", valign: "middle", bgcolor: backgroundColor, style: cellStyle, children: _jsx("a", { ...restProps, href: href, target: target, style: anchorStyle, children: children }) }) }) }) }));
52
+ }
53
+ export function generateHtmlButtonStyles(theme) {
54
+ return generateResponsiveButtonCss(theme, {
55
+ styleSelector: (variantName) => `.htmlButton--${variantName} a`,
56
+ });
57
+ }
58
+ registerStyles(generateHtmlButtonStyles);
@@ -0,0 +1,10 @@
1
+ import { type IMjmlButtonProps } from "@faire/mjml-react";
2
+ import type { ReactNode } from "react";
3
+ import type { Theme } from "../../theme/themeTypes.js";
4
+ import type { ButtonProps } from "./buttonProps.js";
5
+ export type MjmlButtonProps = Omit<IMjmlButtonProps, "href" | "target"> & ButtonProps;
6
+ /**
7
+ * Themed button for use inside an `MjmlColumn`.
8
+ */
9
+ export declare function MjmlButton({ variant: variantProp, fullWidth, width, href, target, className, children, ...restProps }: MjmlButtonProps): ReactNode;
10
+ export declare function generateMjmlButtonStyles(theme: Theme): string;
@@ -0,0 +1,91 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { MjmlButton as BaseMjmlButton } 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 { css } from "../../utils/css.js";
8
+ import { defaultButtonStyles } from "./defaultButtonStyles.js";
9
+ import { generateResponsiveButtonCss } from "./generateResponsiveButtonCss.js";
10
+ /**
11
+ * Themed button for use inside an `MjmlColumn`.
12
+ */
13
+ export function MjmlButton({ variant: variantProp, fullWidth, width, href = "#", target = "_blank", className, children, ...restProps }) {
14
+ const theme = useOptionalTheme();
15
+ const themedProps = getThemedProps(theme, variantProp);
16
+ return (_jsx(BaseMjmlButton, { ...themedProps.baseProps, ...restProps, href: href, target: target, width: fullWidth ? "100%" : width, className: clsx("mjmlButton", themedProps.activeVariant && `mjmlButton--${themedProps.activeVariant}`, fullWidth && "mjmlButton--fullWidth", className), children: children }));
17
+ }
18
+ function getThemedProps(theme, variantProp) {
19
+ if (theme === null && variantProp !== undefined) {
20
+ throw new Error("The `variant` prop requires being wrapped in a ThemeProvider or MjmlMailRoot.");
21
+ }
22
+ const themeButton = theme?.button ?? defaultButtonStyles;
23
+ const { defaultVariant, variants, ...baseStyles } = themeButton;
24
+ const activeVariant = variantProp ?? defaultVariant;
25
+ const variantStyles = activeVariant ? variants?.[activeVariant] : undefined;
26
+ const mergedStyles = { ...baseStyles, ...variantStyles };
27
+ const fontWeightDefault = getDefaultOrUndefined(mergedStyles.fontWeight);
28
+ return {
29
+ activeVariant,
30
+ baseProps: {
31
+ color: getDefaultOrUndefined(mergedStyles.color),
32
+ backgroundColor: getDefaultOrUndefined(mergedStyles.backgroundColor),
33
+ border: getDefaultOrUndefined(mergedStyles.border),
34
+ borderRadius: getDefaultOrUndefined(mergedStyles.borderRadius),
35
+ fontFamily: getDefaultOrUndefined(mergedStyles.fontFamily),
36
+ fontSize: getDefaultOrUndefined(mergedStyles.fontSize),
37
+ fontWeight: fontWeightDefault !== undefined ? String(fontWeightDefault) : undefined,
38
+ lineHeight: getDefaultOrUndefined(mergedStyles.lineHeight),
39
+ innerPadding: getDefaultOrUndefined(mergedStyles.padding),
40
+ },
41
+ };
42
+ }
43
+ // The wrapped MJML button has no `background-image` prop, so a gradient goes in a `<style>`
44
+ // rule over the solid `backgroundColor`; variant rules come after the base so they override it.
45
+ // Only the static value lives here; responsive overrides flow through generateResponsiveButtonCss.
46
+ function generateStaticBackgroundImageCss(theme) {
47
+ const { variants, ...baseStyles } = theme.button;
48
+ const cssChunks = [];
49
+ const baseBackgroundImage = getDefaultOrUndefined(baseStyles.backgroundImage);
50
+ if (baseBackgroundImage !== undefined) {
51
+ cssChunks.push(css `
52
+ .mjmlButton a {
53
+ background-image: ${baseBackgroundImage} !important;
54
+ }
55
+ `);
56
+ }
57
+ if (variants) {
58
+ for (const [variantName, variantStyles] of Object.entries(variants)) {
59
+ const backgroundImage = getDefaultOrUndefined(variantStyles?.backgroundImage);
60
+ if (backgroundImage !== undefined) {
61
+ cssChunks.push(css `
62
+ .mjmlButton--${variantName} a {
63
+ background-image: ${backgroundImage} !important;
64
+ }
65
+ `);
66
+ }
67
+ }
68
+ }
69
+ return cssChunks.join("\n");
70
+ }
71
+ export function generateMjmlButtonStyles(theme) {
72
+ return [
73
+ generateStaticBackgroundImageCss(theme),
74
+ generateResponsiveButtonCss(theme, {
75
+ styleSelector: (variantName) => `.mjmlButton--${variantName} a`,
76
+ }),
77
+ ]
78
+ .filter(Boolean)
79
+ .join("\n");
80
+ }
81
+ registerStyles(generateMjmlButtonStyles);
82
+ // The cell is already full-width; this widens the anchor inside it so a gradient covers the
83
+ // whole bar and the whole bar is clickable. Inlined so it survives clients that drop `<style>`.
84
+ registerStyles(css `
85
+ .mjmlButton--fullWidth a {
86
+ display: block !important;
87
+ width: 100% !important;
88
+ box-sizing: border-box !important;
89
+ text-align: center !important;
90
+ }
91
+ `, { inline: true });
@@ -0,0 +1,35 @@
1
+ import type { AnchorHTMLAttributes } from "react";
2
+ import type { ButtonVariantName } from "../../theme/themeTypes.js";
3
+ export interface ButtonProps {
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 ButtonVariants { primary: true; secondary: true }
12
+ * }
13
+ * ```
14
+ *
15
+ * ```ts
16
+ * const theme = createTheme({
17
+ * button: {
18
+ * variants: {
19
+ * primary: { backgroundColor: "#5B4FC7", color: "#FFFFFF" },
20
+ * secondary: { backgroundColor: "#EEEEEE", color: "#222222" },
21
+ * },
22
+ * },
23
+ * });
24
+ * ```
25
+ *
26
+ * @defaultValue The theme's `button.defaultVariant`, when set
27
+ */
28
+ variant?: ButtonVariantName;
29
+ /** When true, the button spans the full width of its container. */
30
+ fullWidth?: boolean;
31
+ /** @defaultValue "#" */
32
+ href?: string;
33
+ /** @defaultValue "_blank" */
34
+ target?: AnchorHTMLAttributes<HTMLAnchorElement>["target"];
35
+ }
@@ -0,0 +1,2 @@
1
+ import type { ButtonStyles } from "../../theme/themeTypes.js";
2
+ export declare const defaultButtonStyles: ButtonStyles;
@@ -0,0 +1,9 @@
1
+ export const defaultButtonStyles = {
2
+ color: "#ffffff",
3
+ backgroundColor: "#414141",
4
+ border: "none",
5
+ borderRadius: "3px",
6
+ fontSize: "13px",
7
+ lineHeight: "120%",
8
+ padding: "10px 25px",
9
+ };
@@ -0,0 +1,8 @@
1
+ import type { Theme } from "../../theme/themeTypes.js";
2
+ interface GenerateResponsiveButtonCssOptions {
3
+ /** Selector for variant style overrides, given a variant name. */
4
+ styleSelector: (variantName: string) => string;
5
+ }
6
+ /** Generates responsive CSS media queries for button variant overrides. */
7
+ export declare function generateResponsiveButtonCss(theme: Theme, options: GenerateResponsiveButtonCssOptions): string;
8
+ export {};
@@ -0,0 +1,21 @@
1
+ import { generateResponsiveVariantCss } from "../../styles/generateResponsiveVariantCss.js";
2
+ const buttonProperties = [
3
+ "color",
4
+ "backgroundColor",
5
+ "backgroundImage",
6
+ "border",
7
+ "borderRadius",
8
+ "fontFamily",
9
+ "fontSize",
10
+ "fontWeight",
11
+ "lineHeight",
12
+ "padding",
13
+ ];
14
+ /** Generates responsive CSS media queries for button variant overrides. */
15
+ export function generateResponsiveButtonCss(theme, options) {
16
+ return generateResponsiveVariantCss({
17
+ breakpoints: theme.breakpoints,
18
+ variants: theme.button.variants,
19
+ groups: [{ selector: options.styleSelector, properties: buttonProperties }],
20
+ });
21
+ }
@@ -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,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,51 @@
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. Requires a
6
+ * theme (`ThemeProvider` or `MjmlMailRoot`) when set.
7
+ *
8
+ * Custom variants should be defined in the theme through module augmentation:
9
+ *
10
+ * ```ts
11
+ * declare module "@comet/mail-react" {
12
+ * interface DividerVariants { thin: true; thick: true }
13
+ * }
14
+ * ```
15
+ *
16
+ * ```ts
17
+ * const theme = createTheme({
18
+ * divider: {
19
+ * variants: {
20
+ * thin: { height: 1 },
21
+ * thick: { height: 8 },
22
+ * },
23
+ * },
24
+ * });
25
+ * ```
26
+ *
27
+ * @defaultValue The theme's `divider.defaultVariant`, when set
28
+ */
29
+ variant?: DividerVariantName;
30
+ /**
31
+ * Height of the divider in pixels.
32
+ *
33
+ * @defaultValue The theme's `divider` height for the active variant
34
+ */
35
+ height?: number;
36
+ /**
37
+ * Background color of the divider (e.g. `"#FF0000"`).
38
+ *
39
+ * @defaultValue The theme's `divider` background color for the active variant
40
+ */
41
+ backgroundColor?: string;
42
+ /**
43
+ * Background image for the divider — typically a gradient. Clients that
44
+ * don't render `background-image` fall back to the solid `backgroundColor`.
45
+ *
46
+ * @defaultValue The theme's `divider` background image for the active variant
47
+ */
48
+ backgroundImage?: string;
49
+ className?: string;
50
+ style?: CSSProperties;
51
+ }
@@ -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,15 @@
1
+ import { generateResponsiveVariantCss } from "../../styles/generateResponsiveVariantCss.js";
2
+ const dividerProperties = [
3
+ // line-height matches height so Outlook honors the declared cell height.
4
+ { themeKey: "height", cssProperties: ["height", "line-height"], unit: "px" },
5
+ "backgroundColor",
6
+ "backgroundImage",
7
+ ];
8
+ /** Generates responsive CSS media queries for divider variant overrides. */
9
+ export function generateResponsiveDividerCss(theme, options) {
10
+ return generateResponsiveVariantCss({
11
+ breakpoints: theme.breakpoints,
12
+ variants: theme.divider.variants,
13
+ groups: [{ selector: options.styleSelector, properties: dividerProperties }],
14
+ });
15
+ }
@@ -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
+ `);