@comet/mail-react 9.0.0-beta.4 → 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 (40) hide show
  1. package/README.md +57 -5
  2. package/lib/__stories__/layout-patterns/AsymmetricTwoColumnLayout.stories.js +2 -1
  3. package/lib/blocks/pixelImage/HtmlPixelImageBlock.js +2 -11
  4. package/lib/blocks/pixelImage/MjmlPixelImageBlock.d.ts +2 -2
  5. package/lib/blocks/pixelImage/MjmlPixelImageBlock.js +1 -10
  6. package/lib/components/divider/HtmlDivider.d.ts +9 -0
  7. package/lib/components/divider/HtmlDivider.js +43 -0
  8. package/lib/components/divider/MjmlDivider.d.ts +7 -0
  9. package/lib/components/divider/MjmlDivider.js +13 -0
  10. package/lib/components/divider/__stories__/HtmlDivider.stories.d.ts +10 -0
  11. package/lib/components/divider/__stories__/HtmlDivider.stories.js +60 -0
  12. package/lib/components/divider/__stories__/MjmlDivider.stories.d.ts +10 -0
  13. package/lib/components/divider/__stories__/MjmlDivider.stories.js +60 -0
  14. package/lib/components/divider/__tests__/HtmlDivider.test.d.ts +1 -0
  15. package/lib/components/divider/__tests__/HtmlDivider.test.js +144 -0
  16. package/lib/components/divider/__tests__/MjmlDivider.test.d.ts +1 -0
  17. package/lib/components/divider/__tests__/MjmlDivider.test.js +43 -0
  18. package/lib/components/divider/defaultDividerStyles.d.ts +2 -0
  19. package/lib/components/divider/defaultDividerStyles.js +4 -0
  20. package/lib/components/divider/dividerProps.d.ts +38 -0
  21. package/lib/components/divider/dividerProps.js +1 -0
  22. package/lib/components/divider/generateResponsiveDividerCss.d.ts +8 -0
  23. package/lib/components/divider/generateResponsiveDividerCss.js +55 -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/image/__stories__/HtmlImage.stories.d.ts +7 -0
  29. package/lib/components/image/__stories__/HtmlImage.stories.js +32 -0
  30. package/lib/components/image/__stories__/MjmlImage.stories.d.ts +7 -0
  31. package/lib/components/image/__stories__/MjmlImage.stories.js +32 -0
  32. package/lib/components/text/textStyles.d.ts +1 -3
  33. package/lib/components/text/textStyles.js +1 -1
  34. package/lib/index.d.ts +10 -2
  35. package/lib/index.js +5 -1
  36. package/lib/theme/createTheme.d.ts +2 -1
  37. package/lib/theme/createTheme.js +1 -0
  38. package/lib/theme/defaultTheme.js +2 -0
  39. package/lib/theme/themeTypes.d.ts +41 -0
  40. package/package.json +4 -4
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,6 +25,14 @@ 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`).
@@ -35,17 +44,60 @@ We extend `@faire/mjml-react` rather than fork it. A few rules keep that working
35
44
 
36
45
  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
46
 
38
- ### Changesets
47
+ 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
48
 
40
- A changeset describes what changes for the consumer. If the public API and end-user behavior are unchanged, no changeset is needed.
49
+ ### Changesets
41
50
 
42
51
  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
52
 
44
- ## Living document
53
+ ## Internal documentation per feature
54
+
55
+ 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)).
56
+
57
+ ### What is a feature
58
+
59
+ 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.
60
+
61
+ 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.
62
+
63
+ ### Where they live
64
+
65
+ 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.
66
+
67
+ ### What goes in a feature README
68
+
69
+ **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_).
70
+
71
+ Two sections, in order. Only the intro is required.
72
+
73
+ 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.
74
+ 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.
75
+
76
+ 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.
77
+
78
+ 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).
79
+
80
+ **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.
81
+
82
+ **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.
83
+
84
+ ### Template
85
+
86
+ ```md
87
+ # <feature-name>
88
+
89
+ <One short paragraph: the current state, and how the feature changes it.>
90
+
91
+ ## Non-goals <!-- only if any -->
92
+
93
+ - <Noun phrase naming what the feature deliberately doesn't do.> <Optional follow-up sentence pointing to the alternative or stating the rationale.>
94
+ ```
95
+
96
+ ### Living documents
45
97
 
46
- If a change reverses a decision or shifts a convention recorded here, update this README in the same PR.
98
+ 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.
47
99
 
48
- ## Consumer-facing companions
100
+ ## Consumer-facing documentation
49
101
 
50
102
  - Docs: [docs/docs/3-features-modules/13-building-html-emails/](../../docs/docs/3-features-modules/13-building-html-emails/)
51
103
  - Agent skill: [skills/comet-mail-react/SKILL.md](../../skills/comet-mail-react/SKILL.md)
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { MjmlColumn, MjmlImage, MjmlSpacer, MjmlWrapper } from "@faire/mjml-react";
2
+ import { MjmlColumn, MjmlSpacer, MjmlWrapper } from "@faire/mjml-react";
3
+ import { MjmlImage } from "../../components/image/MjmlImage.js";
3
4
  import { MjmlSection } from "../../components/section/MjmlSection.js";
4
5
  import { MjmlText } from "../../components/text/MjmlText.js";
5
6
  import { registerStyles } from "../../styles/registerStyles.js";
@@ -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
- `);
@@ -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
+ `);
@@ -0,0 +1,10 @@
1
+ import { type IMjmlImageProps } from "@faire/mjml-react";
2
+ import type { ReactNode } from "react";
3
+ export type MjmlImageProps = IMjmlImageProps;
4
+ /**
5
+ * Renders an MJML image that adapts to the viewport width below the default breakpoint.
6
+ *
7
+ * Must be placed within an `MjmlColumn`. For raw HTML context (e.g. inside `MjmlRaw`),
8
+ * use `HtmlImage` instead.
9
+ */
10
+ export declare function MjmlImage({ className, ...restProps }: MjmlImageProps): ReactNode;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { MjmlImage as BaseMjmlImage } from "@faire/mjml-react";
3
+ import clsx from "clsx";
4
+ import { registerStyles } from "../../styles/registerStyles.js";
5
+ import { css } from "../../utils/css.js";
6
+ /**
7
+ * Renders an MJML image that adapts to the viewport width below the default breakpoint.
8
+ *
9
+ * Must be placed within an `MjmlColumn`. For raw HTML context (e.g. inside `MjmlRaw`),
10
+ * use `HtmlImage` instead.
11
+ */
12
+ export function MjmlImage({ className, ...restProps }) {
13
+ return _jsx(BaseMjmlImage, { className: clsx("mjmlImage", className), ...restProps });
14
+ }
15
+ // MJML inlines a fixed `height` on the inner <img>; !important overrides it for responsive scaling.
16
+ registerStyles((theme) => css `
17
+ ${theme.breakpoints.default.belowMediaQuery} {
18
+ .mjmlImage img {
19
+ height: auto !important;
20
+ }
21
+ }
22
+ `);
@@ -0,0 +1,7 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { HtmlImage } from "../HtmlImage.js";
3
+ type Story = StoryObj<typeof HtmlImage>;
4
+ declare const config: Meta<typeof HtmlImage>;
5
+ export default config;
6
+ export declare const Default: Story;
7
+ export declare const FullWidth: Story;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { MjmlColumn, MjmlRaw } from "@faire/mjml-react";
3
+ import { defaultTheme } from "../../../theme/defaultTheme.js";
4
+ import { getDefaultFromResponsiveValue } from "../../../theme/responsiveValue.js";
5
+ import { MjmlSection } from "../../section/MjmlSection.js";
6
+ import { HtmlImage } from "../HtmlImage.js";
7
+ const sectionIndent = getDefaultFromResponsiveValue(defaultTheme.sizes.contentIndentation);
8
+ const sectionInnerWidth = defaultTheme.sizes.bodyWidth - 2 * sectionIndent;
9
+ const config = {
10
+ title: "Components/HtmlImage",
11
+ component: HtmlImage,
12
+ tags: ["autodocs"],
13
+ args: {
14
+ width: sectionInnerWidth,
15
+ height: 268,
16
+ alt: "Placeholder image",
17
+ },
18
+ argTypes: {
19
+ src: { control: false },
20
+ },
21
+ };
22
+ export default config;
23
+ export const Default = {
24
+ render: (args) => (_jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlRaw, { children: _jsx(HtmlImage, { ...args, src: `https://picsum.photos/seed/html-image/${args.width}/${args.height}` }) }) }) })),
25
+ };
26
+ export const FullWidth = {
27
+ args: {
28
+ width: defaultTheme.sizes.bodyWidth,
29
+ height: 300,
30
+ },
31
+ render: (args) => (_jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlRaw, { children: _jsx(HtmlImage, { ...args, src: `https://picsum.photos/seed/html-image-full-width/${args.width}/${args.height}` }) }) }) })),
32
+ };
@@ -0,0 +1,7 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { MjmlImage } from "../MjmlImage.js";
3
+ type Story = StoryObj<typeof MjmlImage>;
4
+ declare const config: Meta<typeof MjmlImage>;
5
+ export default config;
6
+ export declare const Default: Story;
7
+ export declare const FullWidth: Story;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { MjmlColumn } from "@faire/mjml-react";
3
+ import { defaultTheme } from "../../../theme/defaultTheme.js";
4
+ import { getDefaultFromResponsiveValue } from "../../../theme/responsiveValue.js";
5
+ import { MjmlSection } from "../../section/MjmlSection.js";
6
+ import { MjmlImage } from "../MjmlImage.js";
7
+ const sectionIndent = getDefaultFromResponsiveValue(defaultTheme.sizes.contentIndentation);
8
+ const sectionInnerWidth = defaultTheme.sizes.bodyWidth - 2 * sectionIndent;
9
+ const config = {
10
+ title: "Components/MjmlImage",
11
+ component: MjmlImage,
12
+ tags: ["autodocs"],
13
+ args: {
14
+ width: sectionInnerWidth,
15
+ height: 268,
16
+ alt: "Placeholder image",
17
+ },
18
+ argTypes: {
19
+ src: { control: false },
20
+ },
21
+ };
22
+ export default config;
23
+ export const Default = {
24
+ render: (args) => (_jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlImage, { ...args, src: `https://picsum.photos/seed/mjml-image/${args.width}/${args.height}` }) }) })),
25
+ };
26
+ export const FullWidth = {
27
+ args: {
28
+ width: defaultTheme.sizes.bodyWidth,
29
+ height: 300,
30
+ },
31
+ render: (args) => (_jsx(MjmlSection, { children: _jsx(MjmlColumn, { children: _jsx(MjmlImage, { ...args, src: `https://picsum.photos/seed/mjml-image-full-width/${args.width}/${args.height}` }) }) })),
32
+ };
@@ -1,6 +1,4 @@
1
- import type { TextVariantStyles, Theme } from "../../theme/themeTypes.js";
2
- type StylePropertyKey = Exclude<keyof TextVariantStyles, "bottomSpacing">;
3
- export declare const textStyleCssProperties: ReadonlyArray<[StylePropertyKey, string]>;
1
+ import type { Theme } from "../../theme/themeTypes.js";
4
2
  interface GenerateResponsiveTextCssOptions {
5
3
  /** Selector for text style overrides, given a variant name. */
6
4
  styleSelector: (variantName: string) => string;
@@ -1,6 +1,6 @@
1
1
  import { getResponsiveOverrides } from "../../theme/responsiveValue.js";
2
2
  import { css } from "../../utils/css.js";
3
- export const textStyleCssProperties = [
3
+ const textStyleCssProperties = [
4
4
  ["fontFamily", "font-family"],
5
5
  ["fontSize", "font-size"],
6
6
  ["fontWeight", "font-weight"],
package/lib/index.d.ts CHANGED
@@ -8,6 +8,14 @@ export type { HtmlPixelImageBlockProps } from "./blocks/pixelImage/HtmlPixelImag
8
8
  export { HtmlPixelImageBlock } from "./blocks/pixelImage/HtmlPixelImageBlock.js";
9
9
  export type { MjmlPixelImageBlockProps } from "./blocks/pixelImage/MjmlPixelImageBlock.js";
10
10
  export { MjmlPixelImageBlock } from "./blocks/pixelImage/MjmlPixelImageBlock.js";
11
+ export type { HtmlDividerProps } from "./components/divider/HtmlDivider.js";
12
+ export { HtmlDivider } from "./components/divider/HtmlDivider.js";
13
+ export type { MjmlDividerProps } from "./components/divider/MjmlDivider.js";
14
+ export { MjmlDivider } from "./components/divider/MjmlDivider.js";
15
+ export type { HtmlImageProps } from "./components/image/HtmlImage.js";
16
+ export { HtmlImage } from "./components/image/HtmlImage.js";
17
+ export type { MjmlImageProps } from "./components/image/MjmlImage.js";
18
+ export { MjmlImage } from "./components/image/MjmlImage.js";
11
19
  export type { HtmlInlineLinkProps } from "./components/inlineLink/HtmlInlineLink.js";
12
20
  export { HtmlInlineLink } from "./components/inlineLink/HtmlInlineLink.js";
13
21
  export { MjmlMailRoot } from "./components/mailRoot/MjmlMailRoot.js";
@@ -26,8 +34,8 @@ export { createTheme } from "./theme/createTheme.js";
26
34
  export type { ResponsiveValue } from "./theme/responsiveValue.js";
27
35
  export { getDefaultFromResponsiveValue, getResponsiveOverrides } from "./theme/responsiveValue.js";
28
36
  export { ThemeProvider, useTheme } from "./theme/ThemeProvider.js";
29
- export type { TextStyles, TextVariants, TextVariantStyles, Theme, ThemeBackgroundColors, ThemeBreakpoint, ThemeBreakpoints, ThemeColors, ThemeSizes, ThemeText, } from "./theme/themeTypes.js";
37
+ export type { DividerStyles, DividerVariants, DividerVariantStyles, TextStyles, TextVariants, TextVariantStyles, Theme, ThemeBackgroundColors, ThemeBreakpoint, ThemeBreakpoints, ThemeColors, ThemeDivider, ThemeSizes, ThemeText, } from "./theme/themeTypes.js";
30
38
  export { css } from "./utils/css.js";
31
- export { Mjml, MjmlAccordion, MjmlAccordionElement, type IMjmlAccordionElementProps as MjmlAccordionElementProps, type IMjmlAccordionProps as MjmlAccordionProps, MjmlAccordionText, type IMjmlAccordionTextProps as MjmlAccordionTextProps, MjmlAccordionTitle, type IMjmlAccordionTitleProps as MjmlAccordionTitleProps, MjmlAll, type IMjmlAllProps as MjmlAllProps, MjmlAttributes, type IMjmlAttributesProps as MjmlAttributesProps, MjmlBody, type IMjmlBodyProps as MjmlBodyProps, MjmlBreakpoint, type IMjmlBreakpointProps as MjmlBreakpointProps, MjmlButton, type IMjmlButtonProps as MjmlButtonProps, MjmlCarousel, MjmlCarouselImage, type IMjmlCarouselImageProps as MjmlCarouselImageProps, type IMjmlCarouselProps as MjmlCarouselProps, MjmlClass, type IMjmlClassProps as MjmlClassProps, MjmlColumn, type IMjmlColumnProps as MjmlColumnProps, MjmlDivider, type IMjmlDividerProps as MjmlDividerProps, MjmlFont, type IMjmlFontProps as MjmlFontProps, MjmlGroup, type IMjmlGroupProps as MjmlGroupProps, MjmlHead, type IMjmlHeadProps as MjmlHeadProps, MjmlHero, type IMjmlHeroProps as MjmlHeroProps, MjmlHtmlAttribute, type IMjmlHtmlAttributeProps as MjmlHtmlAttributeProps, MjmlHtmlAttributes, type IMjmlHtmlAttributesProps as MjmlHtmlAttributesProps, MjmlImage, type IMjmlImageProps as MjmlImageProps, MjmlInclude, type IMjmlIncludeProps as MjmlIncludeProps, MjmlNavbar, MjmlNavbarLink, type IMjmlNavbarLinkProps as MjmlNavbarLinkProps, type IMjmlNavbarProps as MjmlNavbarProps, MjmlPreview, type IMjmlPreviewProps as MjmlPreviewProps, type IMjmlProps as MjmlProps, MjmlRaw, type IMjmlRawProps as MjmlRawProps, MjmlSelector, type IMjmlSelectorProps as MjmlSelectorProps, MjmlSocial, MjmlSocialElement, type IMjmlSocialElementProps as MjmlSocialElementProps, type IMjmlSocialProps as MjmlSocialProps, MjmlSpacer, type IMjmlSpacerProps as MjmlSpacerProps, MjmlStyle, type IMjmlStyleProps as MjmlStyleProps, MjmlTable, type IMjmlTableProps as MjmlTableProps, MjmlTitle, type IMjmlTitleProps as MjmlTitleProps, } from "@faire/mjml-react";
39
+ 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, 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, MjmlInclude, type IMjmlIncludeProps as MjmlIncludeProps, MjmlNavbar, MjmlNavbarLink, type IMjmlNavbarLinkProps as MjmlNavbarLinkProps, type IMjmlNavbarProps as MjmlNavbarProps, MjmlPreview, type IMjmlPreviewProps as MjmlPreviewProps, type IMjmlProps as MjmlProps, MjmlRaw, type IMjmlRawProps as MjmlRawProps, MjmlSelector, type IMjmlSelectorProps as MjmlSelectorProps, MjmlSocial, MjmlSocialElement, type IMjmlSocialElementProps as MjmlSocialElementProps, type IMjmlSocialProps as MjmlSocialProps, MjmlSpacer, type IMjmlSpacerProps as MjmlSpacerProps, MjmlStyle, type IMjmlStyleProps as MjmlStyleProps, MjmlTable, type IMjmlTableProps as MjmlTableProps, MjmlTitle, type IMjmlTitleProps as MjmlTitleProps, } from "@faire/mjml-react";
32
40
  export { MjmlComment, MjmlConditionalComment, MjmlHtml, MjmlTrackingPixel, MjmlYahooStyle } from "@faire/mjml-react/extensions/index.js";
33
41
  export { renderToMjml } from "@faire/mjml-react/utils/renderToMjml.js";
package/lib/index.js CHANGED
@@ -4,6 +4,10 @@ export { OneOfBlock } from "./blocks/factories/OneOfBlock.js";
4
4
  export { OptionalBlock } from "./blocks/factories/OptionalBlock.js";
5
5
  export { HtmlPixelImageBlock } from "./blocks/pixelImage/HtmlPixelImageBlock.js";
6
6
  export { MjmlPixelImageBlock } from "./blocks/pixelImage/MjmlPixelImageBlock.js";
7
+ export { HtmlDivider } from "./components/divider/HtmlDivider.js";
8
+ export { MjmlDivider } from "./components/divider/MjmlDivider.js";
9
+ export { HtmlImage } from "./components/image/HtmlImage.js";
10
+ export { MjmlImage } from "./components/image/MjmlImage.js";
7
11
  export { HtmlInlineLink } from "./components/inlineLink/HtmlInlineLink.js";
8
12
  export { MjmlMailRoot } from "./components/mailRoot/MjmlMailRoot.js";
9
13
  export { MjmlSection } from "./components/section/MjmlSection.js";
@@ -17,6 +21,6 @@ export { createTheme } from "./theme/createTheme.js";
17
21
  export { getDefaultFromResponsiveValue, getResponsiveOverrides } from "./theme/responsiveValue.js";
18
22
  export { ThemeProvider, useTheme } from "./theme/ThemeProvider.js";
19
23
  export { css } from "./utils/css.js";
20
- export { Mjml, MjmlAccordion, MjmlAccordionElement, MjmlAccordionText, MjmlAccordionTitle, MjmlAll, MjmlAttributes, MjmlBody, MjmlBreakpoint, MjmlButton, MjmlCarousel, MjmlCarouselImage, MjmlClass, MjmlColumn, MjmlDivider, MjmlFont, MjmlGroup, MjmlHead, MjmlHero, MjmlHtmlAttribute, MjmlHtmlAttributes, MjmlImage, MjmlInclude, MjmlNavbar, MjmlNavbarLink, MjmlPreview, MjmlRaw, MjmlSelector, MjmlSocial, MjmlSocialElement, MjmlSpacer, MjmlStyle, MjmlTable, MjmlTitle, } from "@faire/mjml-react";
24
+ export { Mjml, MjmlAccordion, MjmlAccordionElement, MjmlAccordionText, MjmlAccordionTitle, MjmlAll, MjmlAttributes, MjmlBody, MjmlBreakpoint, MjmlButton, MjmlCarousel, MjmlCarouselImage, MjmlClass, MjmlColumn, MjmlFont, MjmlGroup, MjmlHead, MjmlHero, MjmlHtmlAttribute, MjmlHtmlAttributes, MjmlInclude, MjmlNavbar, MjmlNavbarLink, MjmlPreview, MjmlRaw, MjmlSelector, MjmlSocial, MjmlSocialElement, MjmlSpacer, MjmlStyle, MjmlTable, MjmlTitle, } from "@faire/mjml-react";
21
25
  export { MjmlComment, MjmlConditionalComment, MjmlHtml, MjmlTrackingPixel, MjmlYahooStyle } from "@faire/mjml-react/extensions/index.js";
22
26
  export { renderToMjml } from "@faire/mjml-react/utils/renderToMjml.js";
@@ -1,8 +1,9 @@
1
- import type { Theme, ThemeBackgroundColors, ThemeBreakpoints, ThemeColors, ThemeSizes, ThemeText } from "./themeTypes.js";
1
+ import type { Theme, ThemeBackgroundColors, ThemeBreakpoints, ThemeColors, ThemeDivider, ThemeSizes, ThemeText } from "./themeTypes.js";
2
2
  type CreateThemeOverrides = {
3
3
  sizes?: Partial<ThemeSizes>;
4
4
  breakpoints?: Partial<ThemeBreakpoints>;
5
5
  text?: Partial<ThemeText>;
6
+ divider?: Partial<ThemeDivider>;
6
7
  colors?: {
7
8
  background?: Partial<ThemeBackgroundColors>;
8
9
  } & Partial<Omit<ThemeColors, "background">>;
@@ -21,6 +21,7 @@ export function createTheme(overrides) {
21
21
  ...overrides?.breakpoints,
22
22
  },
23
23
  text: { ...defaultTheme.text, ...overrides?.text },
24
+ divider: { ...defaultTheme.divider, ...overrides?.divider },
24
25
  colors: {
25
26
  ...defaultTheme.colors,
26
27
  ...overrides?.colors,
@@ -1,3 +1,4 @@
1
+ import { defaultDividerStyles } from "../components/divider/defaultDividerStyles.js";
1
2
  import { createBreakpoint } from "./createBreakpoint.js";
2
3
  export const defaultTheme = {
3
4
  sizes: {
@@ -14,6 +15,7 @@ export const defaultTheme = {
14
15
  lineHeight: "20px",
15
16
  bottomSpacing: "16px",
16
17
  },
18
+ divider: defaultDividerStyles,
17
19
  colors: {
18
20
  background: {
19
21
  body: "#F2F2F2",
@@ -46,6 +46,46 @@ export interface ThemeText extends TextStyles {
46
46
  defaultVariant?: VariantName;
47
47
  variants?: VariantsRecord;
48
48
  }
49
+ /**
50
+ * Single source of truth for divider style property names and their value types.
51
+ * Both `DividerStyles` and `DividerVariantStyles` are derived from this interface.
52
+ */
53
+ interface DividerStyleMap {
54
+ height: number;
55
+ backgroundColor: string;
56
+ backgroundImage: string;
57
+ }
58
+ /** Base divider styles where each property holds a plain value. */
59
+ export type DividerStyles = {
60
+ [K in keyof DividerStyleMap]?: DividerStyleMap[K];
61
+ };
62
+ /** Variant divider styles where each property supports responsive values. */
63
+ export type DividerVariantStyles = {
64
+ [K in keyof DividerStyleMap]?: ResponsiveValue<DividerStyleMap[K]>;
65
+ };
66
+ /**
67
+ * Defines the variants available on the `MjmlDivider` and `HtmlDivider` components.
68
+ *
69
+ * ```ts
70
+ * declare module "@comet/mail-react" {
71
+ * interface DividerVariants {
72
+ * thin: true;
73
+ * thick: true;
74
+ * }
75
+ * }
76
+ * ```
77
+ */
78
+ export interface DividerVariants {
79
+ }
80
+ type DividerVariantsRecord = keyof DividerVariants extends never ? Record<string, DividerVariantStyles> : {
81
+ [K in keyof DividerVariants]?: DividerVariantStyles;
82
+ };
83
+ export type DividerVariantName = keyof DividerVariants extends never ? string : keyof DividerVariants;
84
+ /** Theme configuration for divider styles, variants, and default variant. */
85
+ export interface ThemeDivider extends DividerStyles {
86
+ defaultVariant?: DividerVariantName;
87
+ variants?: DividerVariantsRecord;
88
+ }
49
89
  /**
50
90
  * A resolved breakpoint with its numeric value and a ready-to-use media query
51
91
  * string that targets viewports narrower than this breakpoint.
@@ -101,6 +141,7 @@ export interface Theme {
101
141
  sizes: ThemeSizes;
102
142
  breakpoints: ThemeBreakpoints;
103
143
  text: ThemeText;
144
+ divider: ThemeDivider;
104
145
  colors: ThemeColors;
105
146
  }
106
147
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comet/mail-react",
3
- "version": "9.0.0-beta.4",
3
+ "version": "9.0.0-beta.5",
4
4
  "description": "Utilities for building HTML emails with React",
5
5
  "license": "BSD-2-Clause",
6
6
  "type": "module",
@@ -30,7 +30,7 @@
30
30
  "@storybook/react-vite": "^10.3.5",
31
31
  "@types/mjml": "^4.7.4",
32
32
  "@types/mjml-browser": "^4.15.0",
33
- "@types/react": "^19.2.14",
33
+ "@types/react": "^19.2.15",
34
34
  "@types/react-dom": "^19.2.3",
35
35
  "chokidar-cli": "^3.0.0",
36
36
  "eslint": "^9.39.4",
@@ -42,8 +42,8 @@
42
42
  "storybook": "^10.3.5",
43
43
  "typescript": "^5.9.3",
44
44
  "vitest": "^4.0.16",
45
- "@comet/cli": "9.0.0-beta.4",
46
- "@comet/eslint-config": "9.0.0-beta.4"
45
+ "@comet/eslint-config": "9.0.0-beta.5",
46
+ "@comet/cli": "9.0.0-beta.5"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": "^17.0.0 || ^18.0.0 || ^19.0.0",