@dextinity/mail-react 2.0.0-canary-20260729062014
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/LICENSE +24 -0
- package/README.md +112 -0
- package/lib/blocks/factories/BlocksBlock.d.ts +14 -0
- package/lib/blocks/factories/BlocksBlock.js +15 -0
- package/lib/blocks/factories/ListBlock.d.ts +13 -0
- package/lib/blocks/factories/ListBlock.js +5 -0
- package/lib/blocks/factories/OneOfBlock.d.ts +14 -0
- package/lib/blocks/factories/OneOfBlock.js +15 -0
- package/lib/blocks/factories/OptionalBlock.d.ts +10 -0
- package/lib/blocks/factories/OptionalBlock.js +7 -0
- package/lib/blocks/factories/types.d.ts +4 -0
- package/lib/blocks/factories/types.js +1 -0
- package/lib/blocks/helpers/PropsWithData.d.ts +3 -0
- package/lib/blocks/helpers/PropsWithData.js +1 -0
- package/lib/blocks/pixelImage/HtmlPixelImageBlock.d.ts +11 -0
- package/lib/blocks/pixelImage/HtmlPixelImageBlock.js +18 -0
- package/lib/blocks/pixelImage/MjmlPixelImageBlock.d.ts +9 -0
- package/lib/blocks/pixelImage/MjmlPixelImageBlock.js +15 -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/blocks/richText/common.d.ts +66 -0
- package/lib/blocks/richText/common.js +1 -0
- package/lib/blocks/richText/createRichTextBlock.d.ts +30 -0
- package/lib/blocks/richText/createRichTextBlock.js +81 -0
- package/lib/blocks/richText/createRichTextRenderers.d.ts +17 -0
- package/lib/blocks/richText/createRichTextRenderers.js +83 -0
- package/lib/blocks.generated.d.ts +140 -0
- package/lib/blocks.generated.js +1 -0
- package/lib/client/index.d.ts +1 -0
- package/lib/client/index.js +1 -0
- package/lib/client/renderMailHtml.d.ts +9 -0
- package/lib/client/renderMailHtml.js +7 -0
- package/lib/components/button/HtmlButton.d.ts +9 -0
- package/lib/components/button/HtmlButton.js +58 -0
- package/lib/components/button/MjmlButton.d.ts +10 -0
- package/lib/components/button/MjmlButton.js +91 -0
- package/lib/components/button/buttonProps.d.ts +35 -0
- package/lib/components/button/buttonProps.js +1 -0
- package/lib/components/button/defaultButtonStyles.d.ts +2 -0
- package/lib/components/button/defaultButtonStyles.js +9 -0
- package/lib/components/button/generateResponsiveButtonCss.d.ts +8 -0
- package/lib/components/button/generateResponsiveButtonCss.js +21 -0
- package/lib/components/divider/HtmlDivider.d.ts +9 -0
- package/lib/components/divider/HtmlDivider.js +43 -0
- package/lib/components/divider/MjmlDivider.d.ts +7 -0
- package/lib/components/divider/MjmlDivider.js +13 -0
- package/lib/components/divider/defaultDividerStyles.d.ts +2 -0
- package/lib/components/divider/defaultDividerStyles.js +4 -0
- package/lib/components/divider/dividerProps.d.ts +51 -0
- package/lib/components/divider/dividerProps.js +1 -0
- package/lib/components/divider/generateResponsiveDividerCss.d.ts +8 -0
- package/lib/components/divider/generateResponsiveDividerCss.js +15 -0
- package/lib/components/image/HtmlImage.d.ts +11 -0
- package/lib/components/image/HtmlImage.js +23 -0
- package/lib/components/image/MjmlImage.d.ts +10 -0
- package/lib/components/image/MjmlImage.js +22 -0
- package/lib/components/inlineLink/HtmlInlineLink.d.ts +17 -0
- package/lib/components/inlineLink/HtmlInlineLink.js +35 -0
- package/lib/components/mailRoot/MjmlMailRoot.d.ts +32 -0
- package/lib/components/mailRoot/MjmlMailRoot.js +25 -0
- package/lib/components/section/MjmlSection.d.ts +21 -0
- package/lib/components/section/MjmlSection.js +48 -0
- package/lib/components/text/HtmlText.d.ts +51 -0
- package/lib/components/text/HtmlText.js +47 -0
- package/lib/components/text/MjmlText.d.ts +40 -0
- package/lib/components/text/MjmlText.js +65 -0
- package/lib/components/text/OutlookTextStyleContext.d.ts +9 -0
- package/lib/components/text/OutlookTextStyleContext.js +10 -0
- package/lib/components/text/textStyles.d.ts +10 -0
- package/lib/components/text/textStyles.js +24 -0
- package/lib/components/wrapper/InsideMjmlWrapperContext.d.ts +3 -0
- package/lib/components/wrapper/InsideMjmlWrapperContext.js +6 -0
- package/lib/components/wrapper/MjmlWrapper.d.ts +14 -0
- package/lib/components/wrapper/MjmlWrapper.js +12 -0
- package/lib/config/ConfigProvider.d.ts +43 -0
- package/lib/config/ConfigProvider.js +16 -0
- package/lib/index.d.ts +47 -0
- package/lib/index.js +29 -0
- package/lib/server/index.d.ts +1 -0
- package/lib/server/index.js +1 -0
- package/lib/server/renderMailHtml.d.ts +9 -0
- package/lib/server/renderMailHtml.js +7 -0
- package/lib/storybook/CopyMailHtmlButton.d.ts +4 -0
- package/lib/storybook/CopyMailHtmlButton.js +33 -0
- package/lib/storybook/MailRendererDecorator.d.ts +7 -0
- package/lib/storybook/MailRendererDecorator.js +17 -0
- package/lib/storybook/MjmlWarningsPanel.d.ts +7 -0
- package/lib/storybook/MjmlWarningsPanel.js +33 -0
- package/lib/storybook/UsePublicImageUrlsToggle.d.ts +4 -0
- package/lib/storybook/UsePublicImageUrlsToggle.js +14 -0
- package/lib/storybook/index.d.ts +2 -0
- package/lib/storybook/index.js +9 -0
- package/lib/storybook/manager.d.ts +1 -0
- package/lib/storybook/manager.js +25 -0
- package/lib/storybook/preview.d.ts +39 -0
- package/lib/storybook/preview.js +27 -0
- package/lib/storybook/replaceImagesWithPublicUrl.d.ts +1 -0
- package/lib/storybook/replaceImagesWithPublicUrl.js +15 -0
- package/lib/styles/Styles.d.ts +6 -0
- package/lib/styles/Styles.js +16 -0
- package/lib/styles/generateResponsiveVariantCss.d.ts +26 -0
- package/lib/styles/generateResponsiveVariantCss.js +66 -0
- package/lib/styles/registerStyles.d.ts +19 -0
- package/lib/styles/registerStyles.js +14 -0
- package/lib/theme/ThemeProvider.d.ts +8 -0
- package/lib/theme/ThemeProvider.js +17 -0
- package/lib/theme/createBreakpoint.d.ts +7 -0
- package/lib/theme/createBreakpoint.js +11 -0
- package/lib/theme/createTheme.d.ts +24 -0
- package/lib/theme/createTheme.js +32 -0
- package/lib/theme/defaultTheme.d.ts +2 -0
- package/lib/theme/defaultTheme.js +27 -0
- package/lib/theme/responsiveValue.d.ts +31 -0
- package/lib/theme/responsiveValue.js +34 -0
- package/lib/theme/themeTypes.d.ts +204 -0
- package/lib/theme/themeTypes.js +1 -0
- package/lib/utils/css.d.ts +4 -0
- package/lib/utils/css.js +4 -0
- package/package.json +91 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023, Vivid Planet Software GmbH
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
19
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
20
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
23
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
24
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# @dextinity/mail-react
|
|
2
|
+
|
|
3
|
+
Single-package solution for building HTML emails with React and MJML. Consumers install only `@dextinity/mail-react` (plus `react`) — never `@faire/mjml-react` directly.
|
|
4
|
+
|
|
5
|
+
The package:
|
|
6
|
+
|
|
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.
|
|
10
|
+
|
|
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.
|
|
12
|
+
|
|
13
|
+
## Internal development
|
|
14
|
+
|
|
15
|
+
We extend `@faire/mjml-react` rather than fork it. A few rules keep that working:
|
|
16
|
+
|
|
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
|
+
- **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.
|
|
21
|
+
|
|
22
|
+
### Styling
|
|
23
|
+
|
|
24
|
+
- **Inline first.** Components must render correctly without any `<style>` block — clients like Outlook ignore them.
|
|
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.
|
|
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)`.
|
|
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
|
+
|
|
36
|
+
### File layout
|
|
37
|
+
|
|
38
|
+
- **Location.** Custom components live in `src/components/<concern>/` (e.g. `src/components/section/MjmlSection.tsx`).
|
|
39
|
+
- **File order.** Imports → types/props → component → styles. `registerStyles` calls go below the component.
|
|
40
|
+
- **Module format.** ESM only (`type: module`). Use `.js` extensions in imports.
|
|
41
|
+
- **TSDoc.** Short — one line where possible — on exported components and props.
|
|
42
|
+
- **Prop defaults.** Add `@defaultValue` when the default isn't obvious from the type — runtime defaults, and theme-derived ones the consumer can't see in the shipped types.
|
|
43
|
+
- **Phrasing a default.** Name where the value comes from, not the value itself. One theme token: name it in backticks, as in "The theme's `colors.background.content`". Computed from several: name what it depends on, as in "The theme's `divider` height for the active variant".
|
|
44
|
+
- **Defaults on inherited props.** Redeclare the prop locally (`Omit` it from the base, re-add it with the same type) so the tag is on the prop itself. Never document the default in the component's TSDoc.
|
|
45
|
+
|
|
46
|
+
### Storybook
|
|
47
|
+
|
|
48
|
+
Every custom component has a story in `src/components/<concern>/__stories__/<Component>.stories.tsx`. Wrap non-section stories in `<MjmlSection indent>` to show a realistic indented layout.
|
|
49
|
+
|
|
50
|
+
Placeholder images use the `picsum.photos` seed URL pattern — `https://picsum.photos/seed/<seed>/<width>/<height>` — so the same seed renders the same image on every build.
|
|
51
|
+
|
|
52
|
+
### Changesets
|
|
53
|
+
|
|
54
|
+
When a custom component replaces a re-export, describe the change against the previous re-export — the added props, features, or behavior. Consumers don't need to know the internal component is new; they only see what's different in the API they use.
|
|
55
|
+
|
|
56
|
+
## Internal documentation per feature
|
|
57
|
+
|
|
58
|
+
Features substantial enough to live in their own directory should have a `README.md` that describes the current state and how the feature changes it — and, when a maintainer might assume otherwise, the boundaries it deliberately doesn't cross. These READMEs are internal — written for the people (and agents) maintaining the code, not for consumers. End-user usage docs live elsewhere (see [Consumer-facing documentation](#consumer-facing-documentation)).
|
|
59
|
+
|
|
60
|
+
### What is a feature
|
|
61
|
+
|
|
62
|
+
A feature is any self-contained unit of behavior worth describing on its own — a component (`InlineLink`), a utility (`css` helper), an addon (the Storybook addon), or the package itself. Features nest: this README documents `@dextinity/mail-react` as a feature, and the components inside it are features in their own right.
|
|
63
|
+
|
|
64
|
+
A feature README describes **only its own feature**. It does not describe parent features that contain it, nor sub-features it contains — each of those has its own README.
|
|
65
|
+
|
|
66
|
+
### Where they live
|
|
67
|
+
|
|
68
|
+
A feature that warrants a README is organized as a directory, with the README at the directory root (e.g. `src/components/inline-link/README.md`). Small features that live as a single file inside a parent don't need their own README — they're just part of the parent. Promote a file to a directory at the same time you give it a README.
|
|
69
|
+
|
|
70
|
+
### What goes in a feature README
|
|
71
|
+
|
|
72
|
+
**Title.** Use the exact identifier when the feature is a single component or function (e.g. `MjmlSection`); otherwise use a sentence-case name (e.g. _Inline link_).
|
|
73
|
+
|
|
74
|
+
Two sections, in order. Only the intro is required.
|
|
75
|
+
|
|
76
|
+
1. **Intro.** One short paragraph: the current state, and how the feature changes it. Describe the current state as plain facts — what's there, what's required, what's missing — rather than framing it as "a problem the feature solves". Maintainers read this to work on the code, not to be sold on the feature's existence.
|
|
77
|
+
2. **Non-goals** (optional). Things the feature deliberately doesn't do — only when a reader would reasonably assume it does, typically because the feature's name, its domain, or a sibling feature suggests so. The test: would a maintainer look here for this and be surprised it's missing? If not, leave it out. Skip the obvious; skip future work; skip rejected alternatives — those belong in the commit that made the choice.
|
|
78
|
+
|
|
79
|
+
Write each bullet as a noun phrase naming the thing not done (`Not a heading component`, `No CSS variables`). Add a single follow-up sentence only when the reader needs to be redirected to the alternative or told why.
|
|
80
|
+
|
|
81
|
+
Other sections should be rare — only when content is durable, feature-specific, and doesn't fit the two above. Specifically not warranted: no Architecture (the code shows it), no Design decisions (commits carry them), no Usage (consumer docs), no Dependencies (imports show them; non-obvious cross-feature coupling belongs in the intro).
|
|
82
|
+
|
|
83
|
+
**Express the rule, not the code.** Every line should say something the code doesn't. Don't restate type signatures, prop lists, formulas, or control flow — the code already shows those.
|
|
84
|
+
|
|
85
|
+
**Length.** Simple feature → one paragraph. Complex feature → one screen, no scrolling. Past a screen and you're probably duplicating commit history or describing what the code shows.
|
|
86
|
+
|
|
87
|
+
### Template
|
|
88
|
+
|
|
89
|
+
```md
|
|
90
|
+
# <feature-name>
|
|
91
|
+
|
|
92
|
+
<One short paragraph: the current state, and how the feature changes it.>
|
|
93
|
+
|
|
94
|
+
## Non-goals <!-- only if any -->
|
|
95
|
+
|
|
96
|
+
- <Noun phrase naming what the feature deliberately doesn't do.> <Optional follow-up sentence pointing to the alternative or stating the rationale.>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Living documents
|
|
100
|
+
|
|
101
|
+
Feature READMEs (this one included) should stay current. Update them in the same PR as any change that makes them inaccurate or adds context worth recording.
|
|
102
|
+
|
|
103
|
+
## Usage documentation
|
|
104
|
+
|
|
105
|
+
Two artifacts document how to _use_ this package, for two readers:
|
|
106
|
+
|
|
107
|
+
- **Docs** — [docs/docs/3-features-modules/13-building-html-emails/](../../docs/docs/3-features-modules/13-building-html-emails/) — for people building emails with the package.
|
|
108
|
+
- **Agent skill** — [skills/comet-mail-react/SKILL.md](../../skills/comet-mail-react/SKILL.md) — the same guidance for AI agents working in a project that uses this package.
|
|
109
|
+
|
|
110
|
+
Both teach the same things: what the package is for, how to build emails with it, the patterns to reach for, and the email-client pitfalls — markup that's fine in a browser can render wrong in clients like Outlook. They differ in one respect — an agent already has the package's types and TSDoc in its project, so the skill doesn't restate type signatures, prop lists, or defaults; those live in the types and TSDoc, and it spends its words on everything they can't convey on their own.
|
|
111
|
+
|
|
112
|
+
When a change affects how the package is used — components added/removed/renamed, changed behavior, new usage patterns, or styling conventions — update both in the same PR; consider a separate docs commit so reviewers can take them on their own.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SupportedBlocks } from "./types.js";
|
|
2
|
+
interface Props {
|
|
3
|
+
supportedBlocks: SupportedBlocks;
|
|
4
|
+
data: {
|
|
5
|
+
blocks: Array<{
|
|
6
|
+
key: string;
|
|
7
|
+
type: string;
|
|
8
|
+
visible: boolean;
|
|
9
|
+
props: unknown;
|
|
10
|
+
}>;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export declare const BlocksBlock: ({ supportedBlocks, data: { blocks } }: Props) => import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { MjmlText } from "@faire/mjml-react";
|
|
3
|
+
import { Fragment } from "react";
|
|
4
|
+
export const BlocksBlock = ({ supportedBlocks, data: { blocks } }) => {
|
|
5
|
+
return (_jsx(_Fragment, { children: blocks.map((block) => {
|
|
6
|
+
const blockFunction = supportedBlocks[block.type];
|
|
7
|
+
if (!blockFunction) {
|
|
8
|
+
if (process.env.NODE_ENV === "development") {
|
|
9
|
+
return (_jsxs(MjmlText, { children: ["Unknown type (", block.type, "): ", JSON.stringify(block.props)] }, block.key));
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return _jsx(Fragment, { children: blockFunction(block.props) }, block.key);
|
|
14
|
+
}) }));
|
|
15
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
interface Props {
|
|
3
|
+
block: (props: any) => ReactNode;
|
|
4
|
+
data: {
|
|
5
|
+
blocks: Array<{
|
|
6
|
+
key: string;
|
|
7
|
+
visible: boolean;
|
|
8
|
+
props: unknown;
|
|
9
|
+
}>;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export declare const ListBlock: ({ block: blockFunction, data: { blocks } }: Props) => import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment } from "react";
|
|
3
|
+
export const ListBlock = ({ block: blockFunction, data: { blocks } }) => {
|
|
4
|
+
return (_jsx(_Fragment, { children: blocks.map((block) => (_jsx(Fragment, { children: blockFunction(block.props) }, block.key))) }));
|
|
5
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PropsWithChildren } from "react";
|
|
2
|
+
import type { SupportedBlocks } from "./types.js";
|
|
3
|
+
interface Props extends PropsWithChildren {
|
|
4
|
+
data: {
|
|
5
|
+
block?: {
|
|
6
|
+
type: string;
|
|
7
|
+
props: any;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
supportedBlocks: SupportedBlocks;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const OneOfBlock: ({ data: { block, ...additionalProps }, supportedBlocks, children, className }: Props) => import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsxs as _jsxs, Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { MjmlText } from "@faire/mjml-react";
|
|
3
|
+
export const OneOfBlock = ({ data: { block, ...additionalProps }, supportedBlocks, children, className }) => {
|
|
4
|
+
if (!block) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const blockFunction = supportedBlocks[block.type];
|
|
8
|
+
if (!blockFunction) {
|
|
9
|
+
if (process.env.NODE_ENV === "development") {
|
|
10
|
+
return (_jsxs(MjmlText, { children: ["Unknown type (", block.type, "): ", JSON.stringify(block.props)] }));
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return _jsx(_Fragment, { children: blockFunction({ ...block.props, ...additionalProps, children, className }) });
|
|
15
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PropsWithChildren, ReactNode } from "react";
|
|
2
|
+
interface Props extends PropsWithChildren {
|
|
3
|
+
block: (props: any) => ReactNode;
|
|
4
|
+
data: {
|
|
5
|
+
visible: boolean;
|
|
6
|
+
block?: any;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export declare const OptionalBlock: ({ block: blockFunction, data: { visible, block }, children }: Props) => import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export const OptionalBlock = ({ block: blockFunction, data: { visible, block }, children }) => {
|
|
3
|
+
if (!visible || !block) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
return _jsx(_Fragment, { children: blockFunction({ ...block, children }) });
|
|
7
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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,18 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { HtmlImage } from "../../components/image/HtmlImage.js";
|
|
4
|
+
import { usePixelImageBlockData } from "./usePixelImageBlockData.js";
|
|
5
|
+
/**
|
|
6
|
+
* Renders a pixel-image from the DAM as a raw `<img>` tag.
|
|
7
|
+
*
|
|
8
|
+
* Use within raw HTML context — HTML-only emails or
|
|
9
|
+
* [MJML ending tags](https://documentation.mjml.io/#ending-tags) like `MjmlRaw`.
|
|
10
|
+
* For MJML context, use `MjmlPixelImageBlock`.
|
|
11
|
+
*/
|
|
12
|
+
export function HtmlPixelImageBlock({ data, width, largestPossibleRenderWidth, aspectRatio, className, ...imgProps }) {
|
|
13
|
+
const imageData = usePixelImageBlockData({ data, defaultRenderWidth: width, largestPossibleRenderWidth, aspectRatio });
|
|
14
|
+
if (!imageData) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return (_jsx(HtmlImage, { src: imageData.imageUrl, width: imageData.defaultRenderWidth, height: imageData.desktopImageHeight, alt: imageData.alt, title: imageData.title, className: clsx("htmlPixelImageBlock", className), ...imgProps }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type MjmlImageProps } from "../../components/image/MjmlImage.js";
|
|
3
|
+
import type { PixelImageBlockBaseProps } from "./common.js";
|
|
4
|
+
export type MjmlPixelImageBlockProps = Omit<MjmlImageProps, "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,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { MjmlImage } from "../../components/image/MjmlImage.js";
|
|
4
|
+
import { usePixelImageBlockData } from "./usePixelImageBlockData.js";
|
|
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 function MjmlPixelImageBlock({ data, width, largestPossibleRenderWidth, aspectRatio, className, ...imageProps }) {
|
|
10
|
+
const imageData = usePixelImageBlockData({ data, defaultRenderWidth: width, largestPossibleRenderWidth, aspectRatio });
|
|
11
|
+
if (!imageData) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return (_jsx(MjmlImage, { src: imageData.imageUrl, width: imageData.defaultRenderWidth, height: imageData.desktopImageHeight, alt: imageData.alt, title: imageData.title, className: clsx("mjmlPixelImageBlock", className), ...imageProps }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PixelImageBlockData } from "../../blocks.generated.js";
|
|
2
|
+
export type PixelImageBlockBaseProps = {
|
|
3
|
+
/** The block data to render. */
|
|
4
|
+
data: PixelImageBlockData;
|
|
5
|
+
/** Width at which the image is rendered, in the default/desktop breakpoint. */
|
|
6
|
+
width: number;
|
|
7
|
+
/**
|
|
8
|
+
* Largest possible width the image can be rendered at across breakpoints.
|
|
9
|
+
* Defaults to `theme.sizes.bodyWidth`. Use this when the image can stretch
|
|
10
|
+
* wider on a narrower breakpoint than its desktop render width.
|
|
11
|
+
*/
|
|
12
|
+
largestPossibleRenderWidth?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Aspect ratio for the rendered image.
|
|
15
|
+
* @example "16x9"
|
|
16
|
+
*/
|
|
17
|
+
aspectRatio?: number | string;
|
|
18
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useConfig } from "../../config/ConfigProvider.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reads `config.pixelImageBlock` from the configuration context and returns it narrowed to non-null.
|
|
4
|
+
*/
|
|
5
|
+
export function usePixelImageBlockConfig() {
|
|
6
|
+
const { pixelImageBlock } = useConfig();
|
|
7
|
+
if (!pixelImageBlock) {
|
|
8
|
+
throw new Error("`pixelImageBlock` must be set in `config` on `MjmlMailRoot` or `ConfigProvider` to use the pixel-image configuration.");
|
|
9
|
+
}
|
|
10
|
+
return pixelImageBlock;
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PixelImageBlockData as PixelImageBlockSourceData } from "../../blocks.generated.js";
|
|
2
|
+
interface UsePixelImageBlockDataProps {
|
|
3
|
+
data: PixelImageBlockSourceData;
|
|
4
|
+
defaultRenderWidth: number;
|
|
5
|
+
largestPossibleRenderWidth?: number;
|
|
6
|
+
aspectRatio?: number | string;
|
|
7
|
+
}
|
|
8
|
+
interface PixelImageBlockData {
|
|
9
|
+
imageUrl: string;
|
|
10
|
+
defaultRenderWidth: number;
|
|
11
|
+
desktopImageHeight: number;
|
|
12
|
+
alt: string | undefined;
|
|
13
|
+
title: string | undefined;
|
|
14
|
+
}
|
|
15
|
+
export declare function usePixelImageBlockData({ data: { damFile, cropArea, urlTemplate }, defaultRenderWidth, largestPossibleRenderWidth: passedLargestPossibleRenderWidth, aspectRatio: passedAspectRatio, }: UsePixelImageBlockDataProps): PixelImageBlockData | null;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useTheme } from "../../theme/ThemeProvider.js";
|
|
2
|
+
import { usePixelImageBlockConfig } from "./usePixelImageBlockConfig.js";
|
|
3
|
+
export function usePixelImageBlockData({ data: { damFile, cropArea, urlTemplate }, defaultRenderWidth, largestPossibleRenderWidth: passedLargestPossibleRenderWidth, aspectRatio: passedAspectRatio, }) {
|
|
4
|
+
const theme = useTheme();
|
|
5
|
+
const { validSizes, baseUrl } = usePixelImageBlockConfig();
|
|
6
|
+
const largestPossibleRenderWidth = passedLargestPossibleRenderWidth ?? theme.sizes.bodyWidth;
|
|
7
|
+
if (!damFile?.image) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const usedCropArea = cropArea ?? damFile.image.cropArea;
|
|
11
|
+
const aspectRatio = passedAspectRatio !== undefined ? parseAspectRatio(passedAspectRatio) : calculateAspectRatio(damFile.image, usedCropArea);
|
|
12
|
+
const optimalWidth = getOptimalAllowedImageWidth(validSizes, defaultRenderWidth, largestPossibleRenderWidth);
|
|
13
|
+
const resolvedImageUrl = generateImageUrl(urlTemplate, optimalWidth, aspectRatio);
|
|
14
|
+
return {
|
|
15
|
+
imageUrl: isAbsoluteUrl(resolvedImageUrl) ? resolvedImageUrl : `${baseUrl}${resolvedImageUrl}`,
|
|
16
|
+
defaultRenderWidth,
|
|
17
|
+
desktopImageHeight: Math.round(defaultRenderWidth / aspectRatio),
|
|
18
|
+
alt: damFile.altText,
|
|
19
|
+
title: damFile.title,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function isAbsoluteUrl(url) {
|
|
23
|
+
return !url.startsWith("/");
|
|
24
|
+
}
|
|
25
|
+
function getOptimalAllowedImageWidth(validSizes, defaultRenderWidth, largestPossibleRenderWidth) {
|
|
26
|
+
const sortedValidSizes = validSizes.sort((a, b) => a - b);
|
|
27
|
+
let width = null;
|
|
28
|
+
const largestPossibleWidth = sortedValidSizes[sortedValidSizes.length - 1];
|
|
29
|
+
sortedValidSizes.forEach((validWidth) => {
|
|
30
|
+
if (defaultRenderWidth === largestPossibleRenderWidth) {
|
|
31
|
+
width = largestPossibleRenderWidth * 2;
|
|
32
|
+
}
|
|
33
|
+
else if (!width && validWidth >= defaultRenderWidth * 2) {
|
|
34
|
+
width = validWidth;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
if (!width) {
|
|
38
|
+
return largestPossibleWidth;
|
|
39
|
+
}
|
|
40
|
+
return width;
|
|
41
|
+
}
|
|
42
|
+
// Copied from `calculateInheritAspectRatio` in `@dextinity/site-react` (`src/image/image.utils.ts`).
|
|
43
|
+
// Keep in sync with the site-react version when changes are made.
|
|
44
|
+
function calculateAspectRatio(image, cropArea) {
|
|
45
|
+
if (cropArea.focalPoint === "SMART") {
|
|
46
|
+
return image.width / image.height;
|
|
47
|
+
}
|
|
48
|
+
if (cropArea.width === undefined || cropArea.height === undefined) {
|
|
49
|
+
throw new Error("Missing crop dimensions");
|
|
50
|
+
}
|
|
51
|
+
return (cropArea.width * image.width) / (cropArea.height * image.height);
|
|
52
|
+
}
|
|
53
|
+
// Copied from `generateImageUrl` in `@dextinity/site-react` (`src/image/image.utils.ts`).
|
|
54
|
+
// Keep in sync with the site-react version when changes are made.
|
|
55
|
+
function generateImageUrl(urlTemplate, width, aspectRatio) {
|
|
56
|
+
return urlTemplate.replace("$resizeWidth", String(width)).replace("$resizeHeight", String(Math.ceil(width / aspectRatio)));
|
|
57
|
+
}
|
|
58
|
+
// Copied from `parseAspectRatio` in `@dextinity/site-react` (`src/image/image.utils.ts`).
|
|
59
|
+
// Keep in sync with the site-react version when changes are made.
|
|
60
|
+
function parseAspectRatio(value) {
|
|
61
|
+
let width;
|
|
62
|
+
let height;
|
|
63
|
+
if (typeof value === "string") {
|
|
64
|
+
[width, height] = value.split(/[x/:]/).map((part) => {
|
|
65
|
+
const parsed = parseFloat(part);
|
|
66
|
+
return isNaN(parsed) ? undefined : parsed;
|
|
67
|
+
});
|
|
68
|
+
if (width && !height) {
|
|
69
|
+
height = 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
width = value;
|
|
74
|
+
height = 1;
|
|
75
|
+
}
|
|
76
|
+
if (!width || !height) {
|
|
77
|
+
throw new Error(`An error occurred while parsing the aspect ratio: ${value}`);
|
|
78
|
+
}
|
|
79
|
+
return width / height;
|
|
80
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { TextStyles, VariantName } from "../../theme/themeTypes.js";
|
|
3
|
+
import type { PropsWithData } from "../helpers/PropsWithData.js";
|
|
4
|
+
export interface RichTextBlockData {
|
|
5
|
+
/** Draft.js raw content state (`{ blocks, entityMap }`), as produced by the CMS RichText block. */
|
|
6
|
+
draftContent: unknown;
|
|
7
|
+
}
|
|
8
|
+
export type RichTextBlockProps = PropsWithData<RichTextBlockData>;
|
|
9
|
+
/**
|
|
10
|
+
* Styling for all draft blocks of one type, applied on top of the base theme text styles.
|
|
11
|
+
*
|
|
12
|
+
* Style props accept plain values only. For responsive styling, use a theme
|
|
13
|
+
* variant, or set a `className` and register responsive CSS via `registerStyles`.
|
|
14
|
+
*/
|
|
15
|
+
export type RichTextBlockTypeProps = Omit<TextStyles, "bottomSpacing"> & {
|
|
16
|
+
/**
|
|
17
|
+
* The text component's variant to apply, as defined in the theme.
|
|
18
|
+
*
|
|
19
|
+
* @defaultValue The theme's `text.defaultVariant`, when set
|
|
20
|
+
*/
|
|
21
|
+
variant?: VariantName;
|
|
22
|
+
className?: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Resolves the href of one link block type from the link block's props.
|
|
26
|
+
*
|
|
27
|
+
* Return `undefined` to render the linked text without a link.
|
|
28
|
+
*/
|
|
29
|
+
export type RichTextLinkHrefResolver<TProps = unknown> = (props: TProps) => string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Renders the text spanned by one draft-js inline style (e.g. `BOLD`).
|
|
32
|
+
*
|
|
33
|
+
* `key` must be set on the returned element's root.
|
|
34
|
+
*/
|
|
35
|
+
export type RichTextInlineRenderer = (children: ReactNode, options: {
|
|
36
|
+
key: string;
|
|
37
|
+
}) => ReactNode;
|
|
38
|
+
export interface CreateRichTextBlockOptions<TLinkTypes extends Record<string, unknown> = Record<string, unknown>> {
|
|
39
|
+
/**
|
|
40
|
+
* Maps draft block types (e.g. `"header-one"`, `"paragraph-standard"`) to the
|
|
41
|
+
* styling of the text component that renders them.
|
|
42
|
+
*
|
|
43
|
+
* Unmapped block types render with the base theme text styles.
|
|
44
|
+
*/
|
|
45
|
+
blockTypes?: Record<string, RichTextBlockTypeProps>;
|
|
46
|
+
/**
|
|
47
|
+
* Maps the application's link block types within `LINK` entities to a
|
|
48
|
+
* resolver returning the link's href.
|
|
49
|
+
*
|
|
50
|
+
* Merged on top of the built-in `external` link type. Link types without
|
|
51
|
+
* a resolver render their text without a link.
|
|
52
|
+
*/
|
|
53
|
+
linkTypes?: {
|
|
54
|
+
[TLinkType in keyof TLinkTypes]: RichTextLinkHrefResolver<TLinkTypes[TLinkType]>;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Maps draft-js inline style names to renderers, keyed by the style name as
|
|
58
|
+
* it appears in the content's `inlineStyleRanges`.
|
|
59
|
+
*
|
|
60
|
+
* Merged on top of the built-in styles (`BOLD`, `ITALIC`, `SUB`, `SUP`,
|
|
61
|
+
* `STRIKETHROUGH`): use it to override a built-in style, or to render a
|
|
62
|
+
* custom inline style the application defines in its RTE (e.g. `HIGHLIGHT`),
|
|
63
|
+
* which has no built-in renderer.
|
|
64
|
+
*/
|
|
65
|
+
inline?: Record<string, RichTextInlineRenderer>;
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { CreateRichTextBlockOptions, RichTextBlockProps } from "./common.js";
|
|
3
|
+
/**
|
|
4
|
+
* Creates a pair of rich-text block components that render CMS RichText block
|
|
5
|
+
* data (draft-js raw content) as themed text.
|
|
6
|
+
*
|
|
7
|
+
* Call the factory once per configuration — at the top level of a file, not
|
|
8
|
+
* inside a component — and reuse the returned components. Call it again for
|
|
9
|
+
* differently-configured rich-text blocks (e.g. a generic and a headline-only
|
|
10
|
+
* one).
|
|
11
|
+
*
|
|
12
|
+
* `MjmlRichTextBlock` renders each draft block as `MjmlText` and must be
|
|
13
|
+
* placed within an `MjmlColumn`. `HtmlRichTextBlock` renders each draft block
|
|
14
|
+
* as `HtmlText` for raw-HTML contexts (e.g. inside `MjmlRaw`).
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* export const { MjmlRichTextBlock, HtmlRichTextBlock } = createRichTextBlock({
|
|
18
|
+
* blockTypes: {
|
|
19
|
+
* "header-one": { variant: "heading1" },
|
|
20
|
+
* "paragraph-standard": { variant: "body" },
|
|
21
|
+
* },
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function createRichTextBlock<TLinkTypes extends Record<string, unknown> = Record<string, unknown>>(options?: CreateRichTextBlockOptions<TLinkTypes>): {
|
|
26
|
+
/** Renders CMS RichText block data (draft-js raw content) as one `MjmlText` per draft block. Must be placed within an `MjmlColumn`. */
|
|
27
|
+
MjmlRichTextBlock: (props: RichTextBlockProps) => ReactNode;
|
|
28
|
+
/** Renders CMS RichText block data (draft-js raw content) as one `HtmlText` div per draft block, for raw-HTML contexts such as `MjmlRaw`. */
|
|
29
|
+
HtmlRichTextBlock: (props: RichTextBlockProps) => ReactNode;
|
|
30
|
+
};
|