@comet/mail-react 9.0.0-beta.2 → 9.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -15
- package/lib/__stories__/examples/CustomBackgroundForFooter.stories.js +32 -0
- package/lib/__stories__/layout-patterns/AsymmetricTwoColumnLayout.stories.d.ts +6 -0
- package/lib/__stories__/layout-patterns/AsymmetricTwoColumnLayout.stories.js +107 -0
- package/lib/__stories__/layout-patterns/SymmetricFourColumnLayout.stories.d.ts +4 -0
- package/lib/__stories__/layout-patterns/SymmetricFourColumnLayout.stories.js +58 -0
- package/lib/__stories__/layout-patterns/SymmetricThreeColumnLayout.stories.d.ts +5 -0
- package/lib/__stories__/layout-patterns/SymmetricThreeColumnLayout.stories.js +104 -0
- package/lib/__stories__/layout-patterns/SymmetricTwoColumnLayout.stories.d.ts +4 -0
- package/lib/__stories__/layout-patterns/SymmetricTwoColumnLayout.stories.js +47 -0
- package/lib/blocks/factories/BlocksBlock.d.ts +1 -1
- package/lib/blocks/factories/OneOfBlock.d.ts +2 -2
- package/lib/blocks/factories/OptionalBlock.d.ts +1 -1
- package/lib/blocks/factories/types.d.ts +1 -1
- package/lib/blocks/pixelImage/HtmlPixelImageBlock.d.ts +11 -0
- package/lib/blocks/pixelImage/HtmlPixelImageBlock.js +27 -0
- package/lib/blocks/pixelImage/MjmlPixelImageBlock.d.ts +9 -0
- package/lib/blocks/pixelImage/MjmlPixelImageBlock.js +24 -0
- package/lib/blocks/pixelImage/__stories__/HtmlPixelImageBlock.stories.d.ts +7 -0
- package/lib/blocks/pixelImage/__stories__/HtmlPixelImageBlock.stories.js +54 -0
- package/lib/blocks/pixelImage/__stories__/MjmlPixelImageBlock.stories.d.ts +7 -0
- package/lib/blocks/pixelImage/__stories__/MjmlPixelImageBlock.stories.js +54 -0
- package/lib/blocks/pixelImage/__stories__/exampleBlockData.d.ts +2 -0
- package/lib/blocks/pixelImage/__stories__/exampleBlockData.js +19 -0
- package/lib/blocks/pixelImage/__tests__/usePixelImageBlockConfig.test.d.ts +1 -0
- package/lib/blocks/pixelImage/__tests__/usePixelImageBlockConfig.test.js +21 -0
- package/lib/blocks/pixelImage/__tests__/usePixelImageBlockData.test.d.ts +1 -0
- package/lib/blocks/pixelImage/__tests__/usePixelImageBlockData.test.js +205 -0
- package/lib/blocks/pixelImage/common.d.ts +18 -0
- package/lib/blocks/pixelImage/common.js +1 -0
- package/lib/blocks/pixelImage/usePixelImageBlockConfig.d.ts +5 -0
- package/lib/blocks/pixelImage/usePixelImageBlockConfig.js +11 -0
- package/lib/blocks/pixelImage/usePixelImageBlockData.d.ts +16 -0
- package/lib/blocks/pixelImage/usePixelImageBlockData.js +80 -0
- package/lib/client/renderMailHtml.d.ts +1 -1
- package/lib/components/mailRoot/MjmlMailRoot.d.ts +13 -4
- package/lib/components/mailRoot/MjmlMailRoot.js +10 -5
- package/lib/components/section/MjmlSection.d.ts +1 -1
- package/lib/components/section/MjmlSection.js +8 -4
- package/lib/components/text/HtmlText.d.ts +1 -1
- package/lib/components/text/textStyles.js +10 -5
- package/lib/components/wrapper/InsideMjmlWrapperContext.d.ts +3 -0
- package/lib/components/wrapper/InsideMjmlWrapperContext.js +6 -0
- package/lib/components/wrapper/MjmlWrapper.d.ts +7 -0
- package/lib/components/wrapper/MjmlWrapper.js +12 -0
- package/lib/components/wrapper/__stories__/MjmlWrapper.stories.d.ts +10 -0
- package/lib/components/wrapper/__stories__/MjmlWrapper.stories.js +36 -0
- package/lib/config/ConfigProvider.d.ts +43 -0
- package/lib/config/ConfigProvider.js +16 -0
- package/lib/config/ConfigProvider.test.d.ts +8 -0
- package/lib/config/ConfigProvider.test.js +30 -0
- package/lib/index.d.ts +8 -1
- package/lib/index.js +5 -1
- package/lib/server/renderMailHtml.d.ts +1 -1
- package/lib/server/renderMailHtml.test.js +10 -1
- package/lib/storybook/preview.d.ts +34 -0
- package/lib/storybook/preview.js +22 -0
- package/lib/theme/responsiveValue.js +2 -1
- package/package.json +17 -13
- package/lib/__stories__/examples/TextWithImageEmail.stories.js +0 -56
- /package/lib/__stories__/examples/{TextWithImageEmail.stories.d.ts → CustomBackgroundForFooter.stories.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,26 +1,53 @@
|
|
|
1
1
|
# @comet/mail-react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Single-package solution for building HTML emails with React and MJML. Consumers install only `@comet/mail-react` (plus `react`) — never `@faire/mjml-react` directly.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The package:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- re-exports all `@faire/mjml-react` MJML components (with prop types renamed to drop the `I` prefix);
|
|
8
|
+
- extends some of them with extra props and features;
|
|
9
|
+
- adds new components and utilities — block factories (`BlocksBlock`, `ListBlock`, `OneOfBlock`, `OptionalBlock`) for rendering Comet CMS block data, and a `css()` tagged template literal for IDE syntax highlighting.
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
Some components also ship in an `Html` variant for use inside MJML ending tags. These variants are additive — separate components alongside the `Mjml` versions, with their own API.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
- Open the `packages/mail-react/` directory directly in the IDE, as OpenSpec does not currently support monorepos
|
|
13
|
+
## Internal development
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
We extend `@faire/mjml-react` rather than fork it. A few rules keep that working:
|
|
15
16
|
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
- Use `/opsx:archive` to archive the change and merge delta specs into the main specs, then commit
|
|
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
|
+
- **Wrap, don't reimplement.** Custom components delegate to `@faire/mjml-react`. Less to maintain, and we stay close to upstream behaviour.
|
|
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
20
|
|
|
21
|
-
|
|
21
|
+
### Styling
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
- **Inline first.** Components must render correctly without any `<style>` block — clients like Outlook ignore them.
|
|
24
|
+
- **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
|
+
- **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)`.
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
### File layout
|
|
28
|
+
|
|
29
|
+
- **Location.** Custom components live in `src/components/<concern>/` (e.g. `src/components/section/MjmlSection.tsx`).
|
|
30
|
+
- **File order.** Imports → types/props → component → styles. `registerStyles` calls go below the component.
|
|
31
|
+
- **Module format.** ESM only (`type: module`). Use `.js` extensions in imports.
|
|
32
|
+
- **TSDoc.** Short TSDoc — one line where possible — on exported components and props.
|
|
33
|
+
|
|
34
|
+
### Storybook
|
|
35
|
+
|
|
36
|
+
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
|
+
|
|
38
|
+
### Changesets
|
|
39
|
+
|
|
40
|
+
A changeset describes what changes for the consumer. If the public API and end-user behavior are unchanged, no changeset is needed.
|
|
41
|
+
|
|
42
|
+
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
|
+
|
|
44
|
+
## Living document
|
|
45
|
+
|
|
46
|
+
If a change reverses a decision or shifts a convention recorded here, update this README in the same PR.
|
|
47
|
+
|
|
48
|
+
## Consumer-facing companions
|
|
49
|
+
|
|
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)
|
|
52
|
+
|
|
53
|
+
When a change here affects usage patterns, component APIs, or styling conventions, update these alongside the library change.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { MjmlColumn, MjmlSpacer, MjmlTable } from "@faire/mjml-react";
|
|
3
|
+
import { HtmlInlineLink } from "../../components/inlineLink/HtmlInlineLink.js";
|
|
4
|
+
import { MjmlMailRoot } from "../../components/mailRoot/MjmlMailRoot.js";
|
|
5
|
+
import { MjmlSection } from "../../components/section/MjmlSection.js";
|
|
6
|
+
import { HtmlText } from "../../components/text/HtmlText.js";
|
|
7
|
+
import { MjmlText } from "../../components/text/MjmlText.js";
|
|
8
|
+
import { MjmlWrapper } from "../../components/wrapper/MjmlWrapper.js";
|
|
9
|
+
import { createTheme } from "../../theme/createTheme.js";
|
|
10
|
+
const config = {
|
|
11
|
+
title: "Examples/CustomBackgroundForFooter",
|
|
12
|
+
parameters: { mailRoot: false },
|
|
13
|
+
};
|
|
14
|
+
export default config;
|
|
15
|
+
export const Default = {
|
|
16
|
+
render: () => {
|
|
17
|
+
const theme = createTheme({
|
|
18
|
+
text: {
|
|
19
|
+
defaultVariant: "body",
|
|
20
|
+
variants: {
|
|
21
|
+
heading: { fontSize: "22px", lineHeight: "28px", fontWeight: "bold" },
|
|
22
|
+
body: { fontSize: "14px", lineHeight: "20px" },
|
|
23
|
+
legal: { fontSize: "12px", lineHeight: "18px" },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
const footerTheme = structuredClone(theme);
|
|
28
|
+
footerTheme.colors.background.content = "#2d4a6e";
|
|
29
|
+
footerTheme.text.color = "#c8d8e9";
|
|
30
|
+
return (_jsxs(MjmlMailRoot, { theme: theme, children: [_jsx(MjmlSection, { backgroundColor: "#1a1a1a", indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 15 }), _jsx(MjmlText, { color: "#ffffff", fontWeight: "bold", align: "center", children: "Company Name" }), _jsx(MjmlSpacer, { height: 15 })] }) }), _jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 30 }), _jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "This is a notification email" }), _jsx(MjmlText, { bottomSpacing: true, children: "Minima ea distinctio quisquam. Illo reiciendis non officiis consectetur. Ratione perferendis distinctio sapiente est. Dolor consequatur qui excepturi natus." }), _jsx(MjmlText, { children: "Numquam aut voluptas numquam aspernatur. Consequatur quidem omnis dolorem natus quis soluta. Est recusandae delectus sed sed deserunt velit quia. Occaecati vel possimus similique reiciendis possimus iure rerum sit architecto." }), _jsx(MjmlSpacer, { height: 30 })] }) }), _jsxs(MjmlWrapper, { backgroundColor: "#dddddd", children: [_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 20 }), _jsx(MjmlText, { align: "center", bottomSpacing: true, children: "\u00A9 2026 Company Name \u2013 All rights reserved" }), _jsx(MjmlText, { variant: "legal", align: "center", bottomSpacing: true, children: "Legal text, corporis eos et quia. Assumenda eum maiores esse. Voluptas laudantium cupiditate aut repudiandae iste fugiat nam. Quas in debitis. Sed laudantium illum aut occaecati excepturi veniam harum reprehenderit." })] }) }), _jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlTable, { width: "auto", align: "center", children: _jsx("tbody", { children: _jsxs("tr", { children: [_jsx(HtmlText, { align: "center", children: _jsx(HtmlInlineLink, { href: "https://example.com/privacy", children: "Privacy Policy" }) }), _jsx("td", { width: "20px" }), _jsx(HtmlText, { align: "center", children: _jsx(HtmlInlineLink, { href: "https://example.com/imprint", children: "Imprint" }) }), _jsx("td", { width: "20px" }), _jsx(HtmlText, { align: "center", children: _jsx(HtmlInlineLink, { href: "https://example.com", children: "Website" }) })] }) }) }), _jsx(MjmlSpacer, { height: 20 })] }) })] })] }));
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
declare const config: Meta;
|
|
3
|
+
export default config;
|
|
4
|
+
export declare const SmallColumnLeft: StoryObj;
|
|
5
|
+
export declare const SmallColumnRight: StoryObj;
|
|
6
|
+
export declare const ReversedMobileStackOrder: StoryObj;
|
|
@@ -0,0 +1,107 @@
|
|
|
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";
|
|
3
|
+
import { MjmlSection } from "../../components/section/MjmlSection.js";
|
|
4
|
+
import { MjmlText } from "../../components/text/MjmlText.js";
|
|
5
|
+
import { registerStyles } from "../../styles/registerStyles.js";
|
|
6
|
+
import { createTheme } from "../../theme/createTheme.js";
|
|
7
|
+
import { getDefaultFromResponsiveValue } from "../../theme/responsiveValue.js";
|
|
8
|
+
import { css } from "../../utils/css.js";
|
|
9
|
+
const config = {
|
|
10
|
+
title: "Layout Patterns/Asymmetric Two-Column Layout",
|
|
11
|
+
};
|
|
12
|
+
export default config;
|
|
13
|
+
const theme = createTheme({
|
|
14
|
+
text: {
|
|
15
|
+
defaultVariant: "body",
|
|
16
|
+
variants: {
|
|
17
|
+
heading: { fontSize: "22px", lineHeight: "28px", fontWeight: "bold" },
|
|
18
|
+
body: { fontSize: "16px", lineHeight: "24px" },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
const SMALL_COLUMN_WIDTH = 120;
|
|
23
|
+
const COLUMN_GAP = 20;
|
|
24
|
+
const sectionIndent = getDefaultFromResponsiveValue(theme.sizes.contentIndentation);
|
|
25
|
+
const sectionInnerWidth = theme.sizes.bodyWidth - 2 * sectionIndent;
|
|
26
|
+
const fluidColumnWidth = sectionInnerWidth - SMALL_COLUMN_WIDTH;
|
|
27
|
+
registerStyles((theme) => css `
|
|
28
|
+
${theme.breakpoints.default.belowMediaQuery} {
|
|
29
|
+
.asymmetricLayoutLeft__fluidColumn {
|
|
30
|
+
width: calc(100% - ${SMALL_COLUMN_WIDTH}px) !important;
|
|
31
|
+
max-width: calc(100% - ${SMALL_COLUMN_WIDTH}px) !important;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
${theme.breakpoints.mobile.belowMediaQuery} {
|
|
36
|
+
.asymmetricLayoutLeft__fluidColumn {
|
|
37
|
+
width: 100% !important;
|
|
38
|
+
max-width: 100% !important;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.asymmetricLayoutLeft__smallColumn {
|
|
42
|
+
margin-bottom: 10px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.asymmetricLayoutLeft__fluidColumn > table > tbody > tr > td {
|
|
46
|
+
padding-left: 0 !important;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
`);
|
|
50
|
+
export const SmallColumnLeft = {
|
|
51
|
+
parameters: { theme },
|
|
52
|
+
render: () => (_jsxs(_Fragment, { children: [_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 30 }), _jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Small column on the left" }), _jsx(MjmlText, { children: "A fixed-width column on the left with a fluid column taking the remaining space. On mobile, columns stack vertically with the small column on top." }), _jsx(MjmlSpacer, { height: 30 })] }) }), _jsxs(MjmlSection, { indent: true, children: [_jsx(MjmlColumn, { className: "asymmetricLayoutLeft__smallColumn", width: `${SMALL_COLUMN_WIDTH}px`, verticalAlign: "middle", children: _jsx(MjmlImage, { src: `https://picsum.photos/seed/1/${SMALL_COLUMN_WIDTH}/150`, alt: "Placeholder", align: "center", width: SMALL_COLUMN_WIDTH }) }), _jsxs(MjmlColumn, { className: "asymmetricLayoutLeft__fluidColumn", width: `${fluidColumnWidth}px`, paddingLeft: `${COLUMN_GAP}px`, verticalAlign: "middle", children: [_jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Fluid column" }), _jsx(MjmlText, { children: "This column takes the remaining width after the fixed-width column. The gap between columns is created via padding on the fluid column's inner edge." })] })] }), _jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlSpacer, { height: 30 }) }) })] })),
|
|
53
|
+
};
|
|
54
|
+
registerStyles((theme) => css `
|
|
55
|
+
${theme.breakpoints.default.belowMediaQuery} {
|
|
56
|
+
.asymmetricLayoutRight__fluidColumn {
|
|
57
|
+
width: calc(100% - ${SMALL_COLUMN_WIDTH}px) !important;
|
|
58
|
+
max-width: calc(100% - ${SMALL_COLUMN_WIDTH}px) !important;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
${theme.breakpoints.mobile.belowMediaQuery} {
|
|
63
|
+
.asymmetricLayoutRight__fluidColumn {
|
|
64
|
+
width: 100% !important;
|
|
65
|
+
max-width: 100% !important;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.asymmetricLayoutRight__smallColumn {
|
|
69
|
+
margin-top: 10px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.asymmetricLayoutRight__fluidColumn > table > tbody > tr > td {
|
|
73
|
+
padding-right: 0 !important;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
`);
|
|
77
|
+
export const SmallColumnRight = {
|
|
78
|
+
parameters: { theme },
|
|
79
|
+
render: () => (_jsxs(_Fragment, { children: [_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 30 }), _jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Small column on the right" }), _jsx(MjmlText, { children: "A fluid column on the left with a fixed-width column on the right. On mobile, columns stack vertically with the fluid column on top." }), _jsx(MjmlSpacer, { height: 30 })] }) }), _jsxs(MjmlSection, { indent: true, children: [_jsxs(MjmlColumn, { className: "asymmetricLayoutRight__fluidColumn", width: `${fluidColumnWidth}px`, paddingRight: `${COLUMN_GAP}px`, verticalAlign: "middle", children: [_jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Fluid column" }), _jsx(MjmlText, { children: "This column takes the remaining width after the fixed-width column. The gap between columns is created via padding on the fluid column's inner edge." })] }), _jsx(MjmlColumn, { className: "asymmetricLayoutRight__smallColumn", width: `${SMALL_COLUMN_WIDTH}px`, verticalAlign: "middle", children: _jsx(MjmlImage, { src: `https://picsum.photos/seed/2/${SMALL_COLUMN_WIDTH}/150`, alt: "Placeholder", align: "center", width: SMALL_COLUMN_WIDTH }) })] }), _jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlSpacer, { height: 30 }) }) })] })),
|
|
80
|
+
};
|
|
81
|
+
registerStyles((theme) => css `
|
|
82
|
+
${theme.breakpoints.default.belowMediaQuery} {
|
|
83
|
+
.asymmetricLayoutRtl__fluidColumn {
|
|
84
|
+
width: calc(100% - ${SMALL_COLUMN_WIDTH}px) !important;
|
|
85
|
+
max-width: calc(100% - ${SMALL_COLUMN_WIDTH}px) !important;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
${theme.breakpoints.mobile.belowMediaQuery} {
|
|
90
|
+
.asymmetricLayoutRtl__fluidColumn {
|
|
91
|
+
width: 100% !important;
|
|
92
|
+
max-width: 100% !important;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.asymmetricLayoutRtl__smallColumn {
|
|
96
|
+
margin-bottom: 10px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.asymmetricLayoutRtl__fluidColumn > table > tbody > tr > td {
|
|
100
|
+
padding-right: 0 !important;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
`);
|
|
104
|
+
export const ReversedMobileStackOrder = {
|
|
105
|
+
parameters: { theme },
|
|
106
|
+
render: () => (_jsxs(_Fragment, { children: [_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 30 }), _jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Small column on the right, stacks on top on mobile" }), _jsxs(MjmlText, { children: ["Uses ", _jsx("code", { children: "direction=\"rtl\"" }), " on the section to visually place the small column on the right on desktop, while keeping it first in source order so it stacks on top on mobile. A wrapper handles the indentation separately to avoid a 1px line artifact in Outlook."] }), _jsx(MjmlSpacer, { height: 30 })] }) }), _jsx(MjmlWrapper, { padding: `0 ${sectionIndent}px`, backgroundColor: theme.colors.background.content, children: _jsxs(MjmlSection, { direction: "rtl", children: [_jsx(MjmlColumn, { className: "asymmetricLayoutRtl__smallColumn", width: `${SMALL_COLUMN_WIDTH}px`, verticalAlign: "middle", children: _jsx(MjmlImage, { src: `https://picsum.photos/seed/3/${SMALL_COLUMN_WIDTH}/150`, alt: "Placeholder", align: "center", width: SMALL_COLUMN_WIDTH }) }), _jsxs(MjmlColumn, { className: "asymmetricLayoutRtl__fluidColumn", width: `${fluidColumnWidth}px`, paddingRight: `${COLUMN_GAP}px`, verticalAlign: "middle", children: [_jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Fluid column" }), _jsxs(MjmlText, { children: ["Source order: small column first, fluid column second. On desktop, ", _jsx("code", { children: "direction=\"rtl\"" }), " flips the visual order so the fluid column appears on the left."] })] })] }) }), _jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlSpacer, { height: 30 }) }) })] })),
|
|
107
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { MjmlColumn, MjmlSpacer } from "@faire/mjml-react";
|
|
3
|
+
import { MjmlSection } from "../../components/section/MjmlSection.js";
|
|
4
|
+
import { MjmlText } from "../../components/text/MjmlText.js";
|
|
5
|
+
import { registerStyles } from "../../styles/registerStyles.js";
|
|
6
|
+
import { createTheme } from "../../theme/createTheme.js";
|
|
7
|
+
import { getDefaultFromResponsiveValue } from "../../theme/responsiveValue.js";
|
|
8
|
+
import { css } from "../../utils/css.js";
|
|
9
|
+
const config = {
|
|
10
|
+
title: "Layout Patterns/Symmetric Four-Column Layout",
|
|
11
|
+
};
|
|
12
|
+
export default config;
|
|
13
|
+
const theme = createTheme({
|
|
14
|
+
text: {
|
|
15
|
+
defaultVariant: "body",
|
|
16
|
+
variants: {
|
|
17
|
+
heading: { fontSize: "22px", lineHeight: "28px", fontWeight: "bold" },
|
|
18
|
+
subheading: { fontSize: "16px", lineHeight: "22px", fontWeight: "bold" },
|
|
19
|
+
body: { fontSize: "14px", lineHeight: "20px" },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
registerStyles((theme) => css `
|
|
24
|
+
${theme.breakpoints.default.belowMediaQuery} {
|
|
25
|
+
.symmetricFourColumnsSection > table > tbody > tr > td {
|
|
26
|
+
display: flex !important;
|
|
27
|
+
flex-direction: column !important;
|
|
28
|
+
gap: 20px !important;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.symmetricFourColumnsSection__column {
|
|
32
|
+
flex: none !important;
|
|
33
|
+
width: 100% !important;
|
|
34
|
+
max-width: 100% !important;
|
|
35
|
+
display: block !important;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.symmetricFourColumnsSection__column > table > tbody > tr > td {
|
|
39
|
+
padding-left: 0 !important;
|
|
40
|
+
padding-right: 0 !important;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
`);
|
|
44
|
+
export const Default = {
|
|
45
|
+
parameters: { theme },
|
|
46
|
+
render: () => {
|
|
47
|
+
const SymmetricFourColumnSection = () => {
|
|
48
|
+
const columnGap = 20;
|
|
49
|
+
const halfColumnGap = columnGap / 2;
|
|
50
|
+
const availableContentWidth = theme.sizes.bodyWidth - 2 * getDefaultFromResponsiveValue(theme.sizes.contentIndentation);
|
|
51
|
+
const contentWidthPerColumn = (availableContentWidth - 3 * columnGap) / 4;
|
|
52
|
+
const outerColumnWidth = `${((contentWidthPerColumn + halfColumnGap) / availableContentWidth) * 100}%`;
|
|
53
|
+
const innerColumnWidth = `${((contentWidthPerColumn + columnGap) / availableContentWidth) * 100}%`;
|
|
54
|
+
return (_jsxs(MjmlSection, { indent: true, className: "symmetricFourColumnsSection", children: [_jsxs(MjmlColumn, { width: outerColumnWidth, paddingRight: halfColumnGap, className: "symmetricFourColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "First" }), _jsx(MjmlText, { children: "Minima ea distinctio quisquam. Illo reiciendis non officiis consectetur." })] }), _jsxs(MjmlColumn, { width: innerColumnWidth, paddingLeft: halfColumnGap, paddingRight: halfColumnGap, className: "symmetricFourColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Second" }), _jsx(MjmlText, { children: "Numquam aut voluptas numquam aspernatur. Consequatur quidem omnis dolorem natus." })] }), _jsxs(MjmlColumn, { width: innerColumnWidth, paddingLeft: halfColumnGap, paddingRight: halfColumnGap, className: "symmetricFourColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Third" }), _jsx(MjmlText, { children: "Occaecati vel possimus similique reiciendis iure rerum sit architecto." })] }), _jsxs(MjmlColumn, { width: outerColumnWidth, paddingLeft: halfColumnGap, className: "symmetricFourColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Fourth" }), _jsx(MjmlText, { children: "Voluptas laudantium cupiditate aut repudiandae iste fugiat nam quas debitis." })] })] }));
|
|
55
|
+
};
|
|
56
|
+
return (_jsxs(_Fragment, { children: [_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 30 }), _jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Four column layout" }), _jsx(MjmlText, { children: "Four columns follow the same compensation pattern as three columns. The two inner columns each get a wider width to absorb their double-sided padding. On mobile, a flex reset neutralizes the desktop widths so columns can stack equally." }), _jsx(MjmlSpacer, { height: 30 })] }) }), _jsx(SymmetricFourColumnSection, {}), _jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlSpacer, { height: 30 }) }) })] }));
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { MjmlColumn, MjmlSpacer } from "@faire/mjml-react";
|
|
3
|
+
import { MjmlSection } from "../../components/section/MjmlSection.js";
|
|
4
|
+
import { MjmlText } from "../../components/text/MjmlText.js";
|
|
5
|
+
import { registerStyles } from "../../styles/registerStyles.js";
|
|
6
|
+
import { createTheme } from "../../theme/createTheme.js";
|
|
7
|
+
import { getDefaultFromResponsiveValue } from "../../theme/responsiveValue.js";
|
|
8
|
+
import { css } from "../../utils/css.js";
|
|
9
|
+
const config = {
|
|
10
|
+
title: "Layout Patterns/Symmetric Three-Column Layout",
|
|
11
|
+
};
|
|
12
|
+
export default config;
|
|
13
|
+
const theme = createTheme({
|
|
14
|
+
text: {
|
|
15
|
+
defaultVariant: "body",
|
|
16
|
+
variants: {
|
|
17
|
+
heading: { fontSize: "22px", lineHeight: "28px", fontWeight: "bold" },
|
|
18
|
+
subheading: { fontSize: "16px", lineHeight: "22px", fontWeight: "bold" },
|
|
19
|
+
body: { fontSize: "14px", lineHeight: "20px" },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
registerStyles((theme) => css `
|
|
24
|
+
${theme.breakpoints.default.belowMediaQuery} {
|
|
25
|
+
.symmetricThreeColumnsSection > table > tbody > tr > td {
|
|
26
|
+
display: flex !important;
|
|
27
|
+
gap: 20px !important;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.symmetricThreeColumnsSection__column {
|
|
31
|
+
flex: 1 1 0% !important;
|
|
32
|
+
width: auto !important;
|
|
33
|
+
max-width: none !important;
|
|
34
|
+
display: block !important;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.symmetricThreeColumnsSection__column > table > tbody > tr > td {
|
|
38
|
+
padding-left: 0 !important;
|
|
39
|
+
padding-right: 0 !important;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
${theme.breakpoints.mobile.belowMediaQuery} {
|
|
44
|
+
.symmetricThreeColumnsSection > table > tbody > tr > td {
|
|
45
|
+
flex-direction: column !important;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.symmetricThreeColumnsSection__column {
|
|
49
|
+
flex: none !important;
|
|
50
|
+
width: 100% !important;
|
|
51
|
+
max-width: 100% !important;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
`);
|
|
55
|
+
export const Default = {
|
|
56
|
+
parameters: { theme },
|
|
57
|
+
render: () => {
|
|
58
|
+
const SymmetricThreeColumnSection = () => {
|
|
59
|
+
const columnGap = 20;
|
|
60
|
+
const halfColumnGap = columnGap / 2;
|
|
61
|
+
const availableContentWidth = theme.sizes.bodyWidth - 2 * getDefaultFromResponsiveValue(theme.sizes.contentIndentation);
|
|
62
|
+
const contentWidthPerColumn = (availableContentWidth - 2 * columnGap) / 3;
|
|
63
|
+
const outerColumnWidth = `${((contentWidthPerColumn + halfColumnGap) / availableContentWidth) * 100}%`;
|
|
64
|
+
const innerColumnWidth = `${((contentWidthPerColumn + columnGap) / availableContentWidth) * 100}%`;
|
|
65
|
+
return (_jsxs(MjmlSection, { indent: true, className: "symmetricThreeColumnsSection", children: [_jsxs(MjmlColumn, { width: outerColumnWidth, paddingRight: halfColumnGap, className: "symmetricThreeColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "First column" }), _jsx(MjmlText, { children: "Minima ea distinctio quisquam. Illo reiciendis non officiis consectetur. Ratione perferendis distinctio sapiente est." })] }), _jsxs(MjmlColumn, { width: innerColumnWidth, paddingLeft: halfColumnGap, paddingRight: halfColumnGap, className: "symmetricThreeColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Second column" }), _jsx(MjmlText, { children: "Numquam aut voluptas numquam aspernatur. Consequatur quidem omnis dolorem natus quis soluta. Est recusandae delectus." })] }), _jsxs(MjmlColumn, { width: outerColumnWidth, paddingLeft: halfColumnGap, className: "symmetricThreeColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Third column" }), _jsx(MjmlText, { children: "Occaecati vel possimus similique reiciendis iure rerum sit architecto. Voluptas laudantium cupiditate aut repudiandae iste." })] })] }));
|
|
66
|
+
};
|
|
67
|
+
return (_jsxs(_Fragment, { children: [_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 30 }), _jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Three column layout" }), _jsx(MjmlText, { children: "Three columns require explicit width compensation. Inner columns have padding on both sides while outer columns only have it on one side, so inner columns are given a wider width to keep all content areas equal. On mobile, a flex reset neutralizes the desktop widths so columns can size equally and then stack." }), _jsx(MjmlSpacer, { height: 30 })] }) }), _jsx(SymmetricThreeColumnSection, {}), _jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlSpacer, { height: 30 }) }) })] }));
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
registerStyles((theme) => css `
|
|
71
|
+
${theme.breakpoints.default.belowMediaQuery} {
|
|
72
|
+
.neverStackingThreeColumnsSection > table > tbody > tr > td > div {
|
|
73
|
+
display: flex !important;
|
|
74
|
+
gap: 20px !important;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.neverStackingThreeColumnsSection__column {
|
|
78
|
+
flex: 1 1 0% !important;
|
|
79
|
+
width: auto !important;
|
|
80
|
+
max-width: none !important;
|
|
81
|
+
display: block !important;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.neverStackingThreeColumnsSection__column > table > tbody > tr > td {
|
|
85
|
+
padding-left: 0 !important;
|
|
86
|
+
padding-right: 0 !important;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
`);
|
|
90
|
+
export const NeverStacks = {
|
|
91
|
+
parameters: { theme },
|
|
92
|
+
render: () => {
|
|
93
|
+
const NeverStackingThreeColumnSection = () => {
|
|
94
|
+
const columnGap = 20;
|
|
95
|
+
const halfColumnGap = columnGap / 2;
|
|
96
|
+
const availableContentWidth = theme.sizes.bodyWidth - 2 * getDefaultFromResponsiveValue(theme.sizes.contentIndentation);
|
|
97
|
+
const contentWidthPerColumn = (availableContentWidth - 2 * columnGap) / 3;
|
|
98
|
+
const outerColumnWidth = `${((contentWidthPerColumn + halfColumnGap) / availableContentWidth) * 100}%`;
|
|
99
|
+
const innerColumnWidth = `${((contentWidthPerColumn + columnGap) / availableContentWidth) * 100}%`;
|
|
100
|
+
return (_jsxs(MjmlSection, { indent: true, disableResponsiveBehavior: true, className: "neverStackingThreeColumnsSection", children: [_jsxs(MjmlColumn, { width: outerColumnWidth, paddingRight: halfColumnGap, className: "neverStackingThreeColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Foo" }), _jsx(MjmlText, { children: "Lorem" })] }), _jsxs(MjmlColumn, { width: innerColumnWidth, paddingLeft: halfColumnGap, paddingRight: halfColumnGap, className: "neverStackingThreeColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Bar" }), _jsx(MjmlText, { children: "Ipsum" })] }), _jsxs(MjmlColumn, { width: outerColumnWidth, paddingLeft: halfColumnGap, className: "neverStackingThreeColumnsSection__column", children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Baz" }), _jsx(MjmlText, { children: "Dolor" })] })] }));
|
|
101
|
+
};
|
|
102
|
+
return (_jsxs(_Fragment, { children: [_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 30 }), _jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Three columns that never stack" }), _jsx(MjmlText, { bottomSpacing: true, children: "Same width compensation and flex-reset mechanic as the default three-column layout, without the mobile stacking override so columns stay side-by-side at every viewport." }), _jsxs(MjmlText, { bottomSpacing: true, children: [_jsx("code", { children: "disableResponsiveBehavior" }), " wraps the columns in an ", _jsx("code", { children: "MjmlGroup" }), " internally so MJML's own mobile auto-stack is suppressed even in clients that ignore the flex CSS. Because of that wrapper, the flex reset targets one level deeper (", _jsx("code", { children: "\u2026 > td > div" }), ") than in the default layout."] }), _jsx(MjmlText, { children: "Suitable for short, fixed-value rows \u2014 metrics, numeric summaries, icon strips \u2014 that remain readable even when narrow." }), _jsx(MjmlSpacer, { height: 30 })] }) }), _jsx(NeverStackingThreeColumnSection, {}), _jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlSpacer, { height: 30 }) }) })] }));
|
|
103
|
+
},
|
|
104
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { MjmlColumn, MjmlSpacer } from "@faire/mjml-react";
|
|
3
|
+
import { MjmlSection } from "../../components/section/MjmlSection.js";
|
|
4
|
+
import { MjmlText } from "../../components/text/MjmlText.js";
|
|
5
|
+
import { registerStyles } from "../../styles/registerStyles.js";
|
|
6
|
+
import { createTheme } from "../../theme/createTheme.js";
|
|
7
|
+
import { css } from "../../utils/css.js";
|
|
8
|
+
const config = {
|
|
9
|
+
title: "Layout Patterns/Symmetric Two-Column Layout",
|
|
10
|
+
};
|
|
11
|
+
export default config;
|
|
12
|
+
const theme = createTheme({
|
|
13
|
+
text: {
|
|
14
|
+
defaultVariant: "body",
|
|
15
|
+
variants: {
|
|
16
|
+
heading: { fontSize: "22px", lineHeight: "28px", fontWeight: "bold" },
|
|
17
|
+
subheading: { fontSize: "16px", lineHeight: "22px", fontWeight: "bold" },
|
|
18
|
+
body: { fontSize: "14px", lineHeight: "20px" },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
registerStyles((theme) => css `
|
|
23
|
+
${theme.breakpoints.mobile.belowMediaQuery} {
|
|
24
|
+
.twoColumnsSection__leftColumn > table > tbody > tr > td {
|
|
25
|
+
padding-right: 0 !important;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.twoColumnsSection__rightColumn > table > tbody > tr > td {
|
|
29
|
+
padding-left: 0 !important;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.twoColumnsSection__leftColumn {
|
|
33
|
+
margin-bottom: 20px;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
`);
|
|
37
|
+
export const Default = {
|
|
38
|
+
parameters: { theme },
|
|
39
|
+
render: () => {
|
|
40
|
+
const TwoColumnsSection = () => {
|
|
41
|
+
const columnGap = 20;
|
|
42
|
+
const halfGap = columnGap / 2;
|
|
43
|
+
return (_jsxs(MjmlSection, { indent: true, className: "twoColumnsSection", children: [_jsxs(MjmlColumn, { className: "twoColumnsSection__leftColumn", paddingRight: halfGap, children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Left column" }), _jsx(MjmlText, { children: "Minima ea distinctio quisquam. Illo reiciendis non officiis consectetur. Ratione perferendis distinctio sapiente est. Dolor consequatur qui excepturi natus." })] }), _jsxs(MjmlColumn, { className: "twoColumnsSection__rightColumn", paddingLeft: halfGap, children: [_jsx(MjmlText, { variant: "subheading", bottomSpacing: true, children: "Right column" }), _jsx(MjmlText, { children: "Numquam aut voluptas numquam aspernatur. Consequatur quidem omnis dolorem natus quis soluta. Est recusandae delectus sed sed deserunt velit quia." })] })] }));
|
|
44
|
+
};
|
|
45
|
+
return (_jsxs(_Fragment, { children: [_jsx(MjmlSection, { indent: true, children: _jsxs(MjmlColumn, { children: [_jsx(MjmlSpacer, { height: 30 }), _jsx(MjmlText, { variant: "heading", bottomSpacing: true, children: "Two column layout" }), _jsx(MjmlText, { children: "Two columns with equal widths split evenly by MJML. Each column gets half-gap padding on its inner side to create the 20px gap. No explicit width calculation is needed." }), _jsx(MjmlSpacer, { height: 30 })] }) }), _jsx(TwoColumnsSection, {}), _jsx(MjmlSection, { indent: true, children: _jsx(MjmlColumn, { children: _jsx(MjmlSpacer, { height: 30 }) }) })] }));
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { PropsWithChildren } from "react";
|
|
2
|
+
import type { SupportedBlocks } from "./types.js";
|
|
3
3
|
interface Props extends PropsWithChildren {
|
|
4
4
|
data: {
|
|
5
5
|
block?: {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
+
import type { PixelImageBlockBaseProps } from "./common.js";
|
|
3
|
+
export type HtmlPixelImageBlockProps = Omit<ComponentProps<"img">, "src" | "width" | "height"> & PixelImageBlockBaseProps;
|
|
4
|
+
/**
|
|
5
|
+
* Renders a pixel-image from the DAM as a raw `<img>` tag.
|
|
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 `MjmlPixelImageBlock`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function HtmlPixelImageBlock({ data, width, largestPossibleRenderWidth, aspectRatio, className, ...imgProps }: HtmlPixelImageBlockProps): ReactNode;
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
import { usePixelImageBlockData } from "./usePixelImageBlockData.js";
|
|
6
|
+
/**
|
|
7
|
+
* Renders a pixel-image from the DAM as a raw `<img>` tag.
|
|
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 `MjmlPixelImageBlock`.
|
|
12
|
+
*/
|
|
13
|
+
export function HtmlPixelImageBlock({ data, width, largestPossibleRenderWidth, aspectRatio, className, ...imgProps }) {
|
|
14
|
+
const imageData = usePixelImageBlockData({ data, defaultRenderWidth: width, largestPossibleRenderWidth, aspectRatio });
|
|
15
|
+
if (!imageData) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return (_jsx("img", { src: imageData.imageUrl, width: imageData.defaultRenderWidth, height: imageData.desktopImageHeight, alt: imageData.alt, title: imageData.title, className: clsx("htmlPixelImageBlock", className), ...imgProps }));
|
|
19
|
+
}
|
|
20
|
+
registerStyles((theme) => css `
|
|
21
|
+
${theme.breakpoints.default.belowMediaQuery} {
|
|
22
|
+
.htmlPixelImageBlock {
|
|
23
|
+
width: 100%;
|
|
24
|
+
height: auto;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
`);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type IMjmlImageProps } from "@faire/mjml-react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { PixelImageBlockBaseProps } from "./common.js";
|
|
4
|
+
export type MjmlPixelImageBlockProps = Omit<IMjmlImageProps, "src" | "width" | "height"> & PixelImageBlockBaseProps;
|
|
5
|
+
/**
|
|
6
|
+
* Renders a pixel-image from the DAM as `MjmlImage`. Must be placed within an
|
|
7
|
+
* `MjmlColumn`. For raw HTML context, use `HtmlPixelImageBlock`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function MjmlPixelImageBlock({ data, width, largestPossibleRenderWidth, aspectRatio, className, ...imageProps }: MjmlPixelImageBlockProps): ReactNode;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { MjmlImage } from "@faire/mjml-react";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { registerStyles } from "../../styles/registerStyles.js";
|
|
5
|
+
import { css } from "../../utils/css.js";
|
|
6
|
+
import { usePixelImageBlockData } from "./usePixelImageBlockData.js";
|
|
7
|
+
/**
|
|
8
|
+
* Renders a pixel-image from the DAM as `MjmlImage`. Must be placed within an
|
|
9
|
+
* `MjmlColumn`. For raw HTML context, use `HtmlPixelImageBlock`.
|
|
10
|
+
*/
|
|
11
|
+
export function MjmlPixelImageBlock({ data, width, largestPossibleRenderWidth, aspectRatio, className, ...imageProps }) {
|
|
12
|
+
const imageData = usePixelImageBlockData({ data, defaultRenderWidth: width, largestPossibleRenderWidth, aspectRatio });
|
|
13
|
+
if (!imageData) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return (_jsx(MjmlImage, { src: imageData.imageUrl, width: imageData.defaultRenderWidth, height: imageData.desktopImageHeight, alt: imageData.alt, title: imageData.title, className: clsx("mjmlPixelImageBlock", className), ...imageProps }));
|
|
17
|
+
}
|
|
18
|
+
registerStyles((theme) => css `
|
|
19
|
+
${theme.breakpoints.default.belowMediaQuery} {
|
|
20
|
+
.mjmlPixelImageBlock img {
|
|
21
|
+
height: auto !important;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
`);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { HtmlPixelImageBlock } from "../HtmlPixelImageBlock.js";
|
|
3
|
+
type Story = StoryObj<typeof HtmlPixelImageBlock>;
|
|
4
|
+
declare const config: Meta<typeof HtmlPixelImageBlock>;
|
|
5
|
+
export default config;
|
|
6
|
+
export declare const Default: Story;
|
|
7
|
+
export declare const AspectRatioOverride: Story;
|