@dextinity/agent-features 10.0.1-canary-20260814073232 → 10.0.1

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 (38) hide show
  1. package/package.json +6 -1
  2. package/rules/coding-guidelines/api-nestjs.instructions.md +107 -0
  3. package/rules/coding-guidelines/cdn.instructions.md +24 -0
  4. package/rules/coding-guidelines/general.instructions.md +30 -0
  5. package/rules/coding-guidelines/git.instructions.md +37 -0
  6. package/rules/coding-guidelines/kubernetes.instructions.md +59 -0
  7. package/rules/coding-guidelines/libraries.instructions.md +34 -0
  8. package/rules/coding-guidelines/naming.instructions.md +39 -0
  9. package/rules/coding-guidelines/postgresql.instructions.md +40 -0
  10. package/rules/coding-guidelines/react.instructions.md +102 -0
  11. package/rules/coding-guidelines/security.instructions.md +44 -0
  12. package/rules/coding-guidelines/styling.instructions.md +50 -0
  13. package/rules/coding-guidelines/typescript.instructions.md +50 -0
  14. package/skills/.gitkeep +0 -0
  15. package/skills/dev-pm/SKILL.md +100 -0
  16. package/skills/dextinity-admin-ui/SKILL.md +544 -0
  17. package/skills/dextinity-block/SKILL.md +252 -0
  18. package/skills/dextinity-block/references/admin-patterns.md +192 -0
  19. package/skills/dextinity-block/references/api-patterns.md +183 -0
  20. package/skills/dextinity-block/references/block-loader.md +368 -0
  21. package/skills/dextinity-block/references/block-types.md +210 -0
  22. package/skills/dextinity-block/references/custom-block-field.md +266 -0
  23. package/skills/dextinity-block/references/fixtures.md +436 -0
  24. package/skills/dextinity-block/references/image.md +341 -0
  25. package/skills/dextinity-block/references/migration.md +597 -0
  26. package/skills/dextinity-block/references/registration.md +167 -0
  27. package/skills/dextinity-block/references/response-summary.md +102 -0
  28. package/skills/dextinity-block/references/rich-text.md +309 -0
  29. package/skills/dextinity-block/references/select.md +176 -0
  30. package/skills/dextinity-block/references/site-patterns.md +202 -0
  31. package/skills/dextinity-core-admin-component-authoring/SKILL.md +92 -0
  32. package/skills/dextinity-mail-react/SKILL.md +647 -0
  33. package/skills/dextinity-mail-react/references/components-and-theme.md +448 -0
  34. package/skills/dextinity-mail-react/references/layout-patterns.md +315 -0
  35. package/skills/dextinity-mail-react/references/styling-and-customization.md +306 -0
  36. package/skills/dextinity-major-migration/SKILL.md +161 -0
  37. package/skills/dextinity-major-migration/references/migration-smoke-test.md +196 -0
  38. package/skills/dextinity-minor-update/SKILL.md +191 -0
@@ -0,0 +1,647 @@
1
+ ---
2
+ name: dextinity-mail-react
3
+ description: Guide for building HTML emails with @dextinity/mail-react and MJML. Use whenever working on email templates, mail markup, MJML components, email theming, email styling, responsive emails, column layouts, multi-column email sections, rendering Dextinity CMS block data (such as pixel-image blocks) in emails, or anything involving @dextinity/mail-react or HTML email development — even for seemingly simple tasks like putting content side-by-side in columns, since email client compatibility is a minefield that requires specific patterns and research before implementing.
4
+ ---
5
+
6
+ # Building HTML Emails with @dextinity/mail-react
7
+
8
+ `@dextinity/mail-react` lets you build responsive, themed HTML emails using React components. Under the hood it uses [MJML](https://documentation.mjml.io/) to generate cross-client-compatible HTML. The library provides a theme system, higher-level wrapper components, a style utility layer, and Storybook integration for live previewing emails during development.
9
+
10
+ ---
11
+
12
+ ## Research Before You Code
13
+
14
+ Email development is fundamentally different from web development. There is no shared rendering engine across email clients — the most constrained major client, Outlook on Windows (2007–2019), uses **Microsoft Word** to render HTML, supporting only a fraction of modern CSS. What works perfectly in a browser will often break in email clients, sometimes in surprising ways.
15
+
16
+ Before implementing any visual technique — even things that seem basic like rounded corners, background images, custom fonts, or flexbox layouts — **verify support across email clients**. Many common CSS properties are partially or fully unsupported. This isn't a "nice to have" step — it prevents hours of debugging and rework.
17
+
18
+ ### Essential Resources
19
+
20
+ Keep these open during email development:
21
+
22
+ | Resource | What it's for | URL |
23
+ | ------------------------------ | -------------------------------------------------------------------------------- | ------------------------------------ |
24
+ | **Can I email** | Check CSS/HTML feature support across email clients (like caniuse.com for email) | https://www.caniemail.com/ |
25
+ | **MJML Documentation** | Full reference for all MJML tags and their attributes | https://documentation.mjml.io/ |
26
+ | **Litmus Blog & Resources** | Email development best practices, testing guides, client quirks | https://www.litmus.com/blog/ |
27
+ | **Campaign Monitor CSS Guide** | Comprehensive CSS support tables per email client | https://www.campaignmonitor.com/css/ |
28
+ | **Bulletproof Backgrounds** | VML-based background image generator for Outlook | https://www.backgrounds.cm/ |
29
+ | **Bulletproof Buttons** | VML-based rounded button generator for Outlook | https://www.buttons.cm/ |
30
+
31
+ ### The Research Habit
32
+
33
+ When implementing any visual feature:
34
+
35
+ 1. Check [Can I email](https://www.caniemail.com/) for the CSS properties involved
36
+ 2. If the property isn't supported in Outlook, search for VML workarounds or provide a graceful fallback (skipping border-radius is generally acceptable)
37
+ 3. Test in Storybook with the MJML Warnings panel open
38
+ 4. When uncertain, consult the Litmus blog or Campaign Monitor guide for known patterns
39
+
40
+ This applies to seemingly simple things: `border-radius`, `background-image`, `flexbox`, `gap`, custom fonts — all have partial or no support in major email clients.
41
+
42
+ ### Library Documentation
43
+
44
+ Full documentation for `@dextinity/mail-react`: https://cms-docs.dextinity.com/docs/features-modules/building-html-emails/
45
+
46
+ ---
47
+
48
+ ## The MJML Layout Model
49
+
50
+ MJML enforces a strict **section → column → content** nesting hierarchy:
51
+
52
+ - **`MjmlSection`** — a full-width horizontal row
53
+ - **`MjmlColumn`** — divides a section into vertical columns (stacking on mobile by default)
54
+ - **Content components** (`MjmlText`, `MjmlImage`, `MjmlButton`, etc.) — placed inside a column
55
+
56
+ Content components placed outside this hierarchy produce MJML validation warnings and broken layouts. The MJML Warnings panel in Storybook surfaces these during development.
57
+
58
+ ```tsx
59
+ <MjmlSection indent>
60
+ <MjmlColumn>
61
+ <MjmlText variant="heading">Section title</MjmlText>
62
+ <MjmlText variant="body" bottomSpacing>
63
+ Body paragraph with spacing below.
64
+ </MjmlText>
65
+ <MjmlImage src="https://example.com/image.jpg" alt="Example" />
66
+ </MjmlColumn>
67
+ </MjmlSection>
68
+ ```
69
+
70
+ ### Multi-Column Layouts
71
+
72
+ MJML has no `gap` property. Column padding reduces the content area _inside_ the column — it doesn't add space between column cells. To create a visual gap between columns, apply padding to the inner edges of adjacent columns (`paddingRight` on the left column, `paddingLeft` on the right column). Their sum becomes the visible gap.
73
+
74
+ For two equal columns, apply half the desired gap to each column's inner edge. Both columns have the same total padding, so MJML's equal-width split produces equal content areas without explicit `width` props:
75
+
76
+ ```tsx
77
+ const columnGap = 20;
78
+ const halfGap = columnGap / 2;
79
+
80
+ <MjmlSection indent className="twoColumnsSection">
81
+ <MjmlColumn className="twoColumnsSection__leftColumn" paddingRight={halfGap}>
82
+ <MjmlText>Left column</MjmlText>
83
+ </MjmlColumn>
84
+ <MjmlColumn className="twoColumnsSection__rightColumn" paddingLeft={halfGap}>
85
+ <MjmlText>Right column</MjmlText>
86
+ </MjmlColumn>
87
+ </MjmlSection>;
88
+ ```
89
+
90
+ **Do not** apply equal padding on all sides of every column — this adds extra outer-edge spacing that compounds with `indent`/`contentIndentation`, pushing content inward beyond the theme's intended margins.
91
+
92
+ On mobile, reset the gap padding so content stretches full-width, and add a vertical margin between the stacked columns. Column padding compiles to an inner `<td>`, so target it via `.className > table > tbody > tr > td`.
93
+
94
+ → For complete two-column patterns (equal-width and fixed+fluid) with responsive styles, CSS targeting rules, and the `direction="rtl"` technique for controlling mobile stack order, read [`references/layout-patterns.md`](references/layout-patterns.md).
95
+
96
+ ### Ending Tags
97
+
98
+ Some MJML components are [**ending tags**](https://documentation.mjml.io/#ending-tags) — they accept raw HTML as children but **cannot** contain other MJML components. The most common: `MjmlText`, `MjmlButton`, `MjmlTable`, `MjmlRaw`.
99
+
100
+ Once inside an ending tag, you are in **HTML-land for the entire subtree**. Use HTML elements (`<span>`, `<a>`, `<table>`, `<td>`) but not MJML components. The library provides `HtmlText` and `HtmlInlineLink` for themed text and links inside ending tags.
101
+
102
+ For raw HTML layouts outside text, use `MjmlRaw` (or `MjmlTable`). These are escape hatches for cases MJML components can't handle — use them as a last resort.
103
+
104
+ ---
105
+
106
+ ## The Styling Model
107
+
108
+ Email styling follows a **desktop-first** approach:
109
+
110
+ 1. **Base/default styles are inline** — applied through MJML component props or explicit `style` attributes on HTML elements. Desktop clients like Outlook ignore `<style>` blocks entirely, so the base rendering must look correct with inline styles alone.
111
+
112
+ 2. **Responsive overrides are progressive enhancement** — registered via `registerStyles` with media queries targeting mobile viewports. Clients that support media queries also support modern CSS, so properties like `flex`, and CSS custom properties are safe inside media queries.
113
+
114
+ 3. **`!important` is required** in media query overrides — because inline styles take precedence over `<style>` block rules, responsive overrides need `!important` to win.
115
+
116
+ Never rely on `<style>` blocks for base/desktop layout. Set all default styles inline via MJML component props.
117
+
118
+ ### Prefer Theme Breakpoints
119
+
120
+ Always use `theme.breakpoints.*.belowMediaQuery` inside `registerStyles` instead of hardcoding media query values. This keeps responsive styles in sync with the theme configuration. If a breakpoint value is needed repeatedly but doesn't exist in the theme, add it via `createBreakpoint` and module augmentation rather than duplicating raw media queries. Reserve hardcoded media queries for genuinely one-off values.
121
+
122
+ → For the full `registerStyles` API, `css` helper, and custom component patterns, read [`references/styling-and-customization.md`](references/styling-and-customization.md).
123
+
124
+ ---
125
+
126
+ ## Common Pitfalls
127
+
128
+ ### Start Raw Content Inside a Column With `<tr>`
129
+
130
+ `mj-column` wraps every child in its own `<tr><td>` — except `MjmlRaw` content, which goes straight into the column's table unwrapped. A `<table>`, `<div>`, or `<img>` in that position ends up outside the column's table instead, and MJML reports no error. This covers the `Html*` components. Open with a `<tr>` and put the markup in a `<td>`:
131
+
132
+ ```tsx
133
+ <MjmlColumn>
134
+ <MjmlRaw>
135
+ <tr>
136
+ <td>{/* your raw markup */}</td>
137
+ </tr>
138
+ </MjmlRaw>
139
+ </MjmlColumn>
140
+ ```
141
+
142
+ `HtmlText` is the exception: it renders the `<td>` itself, so a surrounding `<tr>` is all it needs — unless its `element` prop renders something else, which needs a `<td>` again.
143
+
144
+ `MjmlColumn` and `MjmlHero` are both affected — `MjmlSection` and `MjmlWrapper` already place their children inside a shared cell, so any root element is safe there. `MjmlDivider` supplies both the row and the cell; use it instead of a hand-wrapped `HtmlDivider` whenever you are in an `MjmlColumn`.
145
+
146
+ ### Avoid Block-Level HTML Elements Inside Ending Tags
147
+
148
+ Don't use `<p>`, `<h1>`, `<h2>`, or other block-level HTML elements inside ending tags. They have wildly inconsistent default margins and spacing across email clients and add no rendering value in email HTML. Instead, use `<td>`, `<div>`, and `<span>` for structure, and build your typography hierarchy through `MjmlText`/`HtmlText` variants rather than HTML semantics. If a block-level element is truly unavoidable, always reset its margins inline: `style={{ margin: 0 }}`.
149
+
150
+ ### Set `mso-line-height-rule: exactly` on Every Manual `line-height`
151
+
152
+ Outlook calculates line-height using its own rules, causing unexpected vertical spacing. **Every time** you set `line-height` on a raw HTML element inside an ending tag (`MjmlRaw`, `MjmlText`, etc.), you must also set `mso-line-height-rule: exactly` as an inline style on the same element. This applies to `<span>`, `<td>`, `<div>`, or any other element where you manually control line height. `HtmlText` and built-in MJML components handle this automatically — but any hand-written HTML needs it explicitly.
153
+
154
+ ### No CSS `background-image` in Outlook
155
+
156
+ Outlook ignores `background-image` entirely. Use a VML-based workaround for Outlook support, or provide a `background-color` fallback for graceful degradation. See [Bulletproof Backgrounds](https://www.backgrounds.cm/).
157
+
158
+ ### No CSS `border-radius` in Outlook
159
+
160
+ Outlook ignores `border-radius` — rounded corners render as sharp rectangles. The workaround is VML `v:roundrect` in conditional comments (`<!--[if mso]>`). See [Bulletproof Buttons](https://www.buttons.cm/) and the [Litmus VML button snippet](https://litmus.com/community/snippets/7-bulletproof-button-vml-approach).
161
+
162
+ ---
163
+
164
+ ## Theme Setup & Type-Safety
165
+
166
+ Create a theme with `createTheme()` and pass it to `MjmlMailRoot`:
167
+
168
+ ```tsx
169
+ import { createTheme, MjmlMailRoot } from "@dextinity/mail-react";
170
+
171
+ const theme = createTheme({
172
+ sizes: {
173
+ bodyWidth: 700,
174
+ contentIndentation: { default: 40, mobile: 20 },
175
+ },
176
+ text: {
177
+ fontFamily: "Georgia, serif",
178
+ fontSize: "18px",
179
+ },
180
+ colors: {
181
+ background: { body: "#EAEAEA", content: "#FAFAFA" },
182
+ },
183
+ });
184
+
185
+ function MyEmail() {
186
+ return <MjmlMailRoot theme={theme}>{/* email content */}</MjmlMailRoot>;
187
+ }
188
+ ```
189
+
190
+ ### Contributing to `<MjmlHead>` and `<MjmlAttributes>`
191
+
192
+ `MjmlMailRoot` accepts `head` and `attributes` slot props for content that can't be expressed via `registerStyles`:
193
+
194
+ - `head` — use for `<MjmlFont>`, `<MjmlConditionalComment>`, `<MjmlPreview>`, or `<MjmlStyle>` content that depends on React context at render time.
195
+ - `attributes` — use for `<MjmlClass>` or per-element defaults (e.g. `<MjmlText fontSize="14px" />`).
196
+
197
+ Pass the children directly — do not wrap them in another `<MjmlHead>` / `<MjmlAttributes>`:
198
+
199
+ ```tsx
200
+ <MjmlMailRoot theme={theme} attributes={<MjmlClass name="link" color="blue" />} head={<MjmlFont name="Foo" href="https://example.com/foo.css" />}>
201
+ {/* email content */}
202
+ </MjmlMailRoot>
203
+ ```
204
+
205
+ For module-scoped responsive CSS that depends only on the theme, prefer `registerStyles`.
206
+
207
+ ### Module Augmentation for Type-Safety
208
+
209
+ `@dextinity/mail-react` uses TypeScript module augmentation to make custom theme tokens type-safe. Always augment these interfaces when extending the theme — TypeScript will then error on typos or unknown keys.
210
+
211
+ **Text Variants** — restrict `variant` prop to defined names:
212
+
213
+ ```ts
214
+ declare module "@dextinity/mail-react" {
215
+ interface TextVariants {
216
+ heading: true;
217
+ body: true;
218
+ caption: true;
219
+ }
220
+ }
221
+ ```
222
+
223
+ **Custom Breakpoints** — make new breakpoint keys available in responsive values:
224
+
225
+ ```ts
226
+ declare module "@dextinity/mail-react" {
227
+ interface ThemeBreakpoints {
228
+ tablet: ThemeBreakpoint;
229
+ }
230
+ }
231
+ ```
232
+
233
+ **Custom Colors** — add project-specific color tokens:
234
+
235
+ ```ts
236
+ declare module "@dextinity/mail-react" {
237
+ interface ThemeBackgroundColors {
238
+ highlight: string;
239
+ }
240
+ interface ThemeColors {
241
+ brand: { primary: string; secondary: string };
242
+ }
243
+ }
244
+ ```
245
+
246
+ Place `declare module` blocks in your theme file below the `createTheme()` call.
247
+
248
+ → For the full theme structure, responsive values, module augmentation, and scoped theming, read [`references/components-and-theme.md`](references/components-and-theme.md).
249
+
250
+ ---
251
+
252
+ ## Configuration
253
+
254
+ `Config` exposes environment-specific values — e.g. asset base URLs — to every component in the tree. Add keys via module augmentation:
255
+
256
+ ```ts
257
+ declare module "@dextinity/mail-react" {
258
+ interface Config {
259
+ assetBaseUrl?: string;
260
+ }
261
+ }
262
+ ```
263
+
264
+ Wire at the root and read from any descendant:
265
+
266
+ ```tsx
267
+ import { MjmlMailRoot, useConfig, type Config } from "@dextinity/mail-react";
268
+
269
+ const config: Config = { assetBaseUrl: process.env.ASSET_BASE_URL };
270
+
271
+ <MjmlMailRoot config={config}>{/* descendants can call useConfig() */}</MjmlMailRoot>;
272
+ ```
273
+
274
+ ---
275
+
276
+ ## Components Overview
277
+
278
+ ### MJML Components (Layout Level)
279
+
280
+ | Component | Purpose | CSS Classes |
281
+ | --------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------- |
282
+ | `MjmlMailRoot` | Root element, provides theme, renders `<mjml>` skeleton | — |
283
+ | `MjmlWrapper` | Groups sections sharing a background; theme-aware default bg | — |
284
+ | `MjmlSection` | Full-width row; theme indentation, columns stack on mobile | `.mjmlSection`, `.mjmlSection--indented` |
285
+ | `MjmlColumn` | Vertical column inside a section | — |
286
+ | `MjmlText` | Themed text block with typography variants | `.mjmlText`, `.mjmlText--{variant}`, `.mjmlText--bottomSpacing` |
287
+ | `MjmlImage` | Responsive image | `.mjmlImage` |
288
+ | `MjmlPixelImageBlock` | Renders a Dextinity CMS `PixelImageBlockData` via `MjmlImage` | `.mjmlPixelImageBlock` |
289
+ | `MjmlButton` | Themed button (ending tag), theme styling and variants | `.mjmlButton`, `.mjmlButton--{variant}` |
290
+ | `MjmlDivider` | Themed horizontal divider, configurable through theme and variants | `.mjmlDivider`, `.mjmlDivider--{variant}` |
291
+ | `MjmlSpacer` | Vertical spacing | — |
292
+ | `MjmlRaw` | Raw HTML escape hatch (ending tag) | — |
293
+
294
+ ### HTML Components (Inside Ending Tags)
295
+
296
+ | Component | Purpose | CSS Classes |
297
+ | --------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------- |
298
+ | `HtmlText` | Themed text rendered as an HTML element | `.htmlText`, `.htmlText--{variant}`, `.htmlText--bottomSpacing` |
299
+ | `HtmlInlineLink` | `<a>` that inherits parent text styles, works in Outlook | `.htmlInlineLink` |
300
+ | `HtmlImage` | Responsive image (`<img>`) | `.htmlImage` |
301
+ | `HtmlPixelImageBlock` | Renders a Dextinity CMS `PixelImageBlockData` as `<img>` | `.htmlPixelImageBlock` |
302
+ | `HtmlButton` | Themed button for ending tags or non-MJML contexts | `.htmlButton`, `.htmlButton--{variant}` |
303
+ | `HtmlDivider` | Themed horizontal divider, configurable through theme and variants | `.htmlDivider`, `.htmlDivider--{variant}` |
304
+
305
+ Variants are named typography presets (font size, weight, line height, color) defined in the theme; their values can change per breakpoint.
306
+
307
+ All components are imported from `@dextinity/mail-react` — never from `@faire/mjml-react` directly.
308
+
309
+ → For theme tokens, responsive values, component behavior, scoped theming, and MJML re-exports, read [`references/components-and-theme.md`](references/components-and-theme.md).
310
+
311
+ ---
312
+
313
+ ## Blocks
314
+
315
+ `@dextinity/mail-react` ships components that render Dextinity CMS block data — currently pixel-image and rich-text blocks. Reach for these instead of hand-rolled markup whenever the source is a CMS block-data record.
316
+
317
+ ### Pixel-image blocks
318
+
319
+ | Component | Renders | Use within |
320
+ | --------------------- | ----------------------- | ---------------------------------------------- |
321
+ | `MjmlPixelImageBlock` | re-exported `MjmlImage` | an `MjmlColumn` (standard MJML layout) |
322
+ | `HtmlPixelImageBlock` | raw `<img>` | raw HTML or MJML ending tags such as `MjmlRaw` |
323
+
324
+ Inside `MjmlRaw` in an `MjmlColumn`, `HtmlPixelImageBlock` needs its own `<tr>` and `<td>` — see _Start Raw Content Inside a Column With `<tr>`_ above.
325
+
326
+ ```tsx
327
+ <MjmlSection indent>
328
+ <MjmlColumn>
329
+ <MjmlPixelImageBlock data={pixelImageData} width={536} />
330
+ </MjmlColumn>
331
+ </MjmlSection>
332
+ ```
333
+
334
+ #### Configuration
335
+
336
+ Both blocks require `config.pixelImageBlock` and throw without it. Wire it once on `MjmlMailRoot.config`:
337
+
338
+ ```tsx
339
+ const config: Config = {
340
+ pixelImageBlock: {
341
+ validSizes: [...dextinityConfig.images.imageSizes, ...dextinityConfig.images.deviceSizes],
342
+ baseUrl: process.env.API_URL,
343
+ },
344
+ };
345
+ ```
346
+
347
+ #### Render width
348
+
349
+ The block picks the source size from the configured `validSizes`, accounting for retina displays. Use `largestPossibleRenderWidth` when the image stretches wider on a narrower breakpoint than its desktop width — e.g. a two-column layout that stacks on mobile.
350
+
351
+ ```tsx
352
+ <MjmlPixelImageBlock data={pixelImageData} width={300} largestPossibleRenderWidth={420} />
353
+ ```
354
+
355
+ #### Aspect ratio
356
+
357
+ Without `aspectRatio`, the rendered ratio comes from the DAM crop area. Pass `aspectRatio` to override it.
358
+
359
+ ```tsx
360
+ <MjmlPixelImageBlock data={pixelImageData} width={536} aspectRatio="16x9" />
361
+ ```
362
+
363
+ When `data.damFile?.image` is absent, both blocks render nothing — no element, no error. Render a placeholder at the call site if you need one.
364
+
365
+ ### Rich-text blocks
366
+
367
+ `createRichTextBlock` renders CMS RichText block data (draft-js raw content). **Call the factory once per configuration — at the top level of a file, never inside a component** — and export the returned pair; one configuration drives both rendering contexts.
368
+
369
+ ```tsx title="src/emails/blocks/richText.ts"
370
+ export const { MjmlRichTextBlock, HtmlRichTextBlock } = createRichTextBlock({
371
+ blockTypes: {
372
+ "header-one": { variant: "heading1" },
373
+ "header-two": { variant: "heading2" },
374
+ "paragraph-standard": { variant: "body" },
375
+ },
376
+ });
377
+ ```
378
+
379
+ | Component | Renders each draft block as | Use within |
380
+ | ------------------- | --------------------------- | ---------------------------------------------- |
381
+ | `MjmlRichTextBlock` | `MjmlText` | an `MjmlColumn` (standard MJML layout) |
382
+ | `HtmlRichTextBlock` | `HtmlText` (`<div>`) | raw HTML or MJML ending tags such as `MjmlRaw` |
383
+
384
+ Inside `MjmlRaw` in an `MjmlColumn`, `HtmlRichTextBlock` needs its own `<tr>` and `<td>` — see _Start Raw Content Inside a Column With `<tr>`_ above.
385
+
386
+ Usage sites pass only `data`:
387
+
388
+ ```tsx
389
+ <MjmlSection indent>
390
+ <MjmlColumn>
391
+ <MjmlRichTextBlock data={richTextData} />
392
+ </MjmlColumn>
393
+ </MjmlSection>
394
+ ```
395
+
396
+ Key behaviors:
397
+
398
+ - **Works without variants.** `createRichTextBlock()` with no options renders every draft block with the base `theme.text` styles — map block types to variants later as the theme grows. Unmapped block types also fall back to base styles.
399
+ - **`blockTypes` values are text-component props**: a theme `variant`, plain (non-responsive) style values (`color`, `fontSize`, `fontWeight`, …), and a `className`. For responsive styling without a variant, set a `className` and register CSS via `registerStyles`:
400
+
401
+ ```tsx
402
+ const { MjmlRichTextBlock } = createRichTextBlock({
403
+ blockTypes: { "header-one": { className: "richTextHeadlineOne" } },
404
+ });
405
+
406
+ registerStyles(
407
+ (theme) => css`
408
+ ${theme.breakpoints.default.belowMediaQuery} {
409
+ .richTextHeadlineOne > div {
410
+ font-size: 24px !important;
411
+ }
412
+ }
413
+ `,
414
+ );
415
+ ```
416
+
417
+ For a list block type, target `.<className> .richTextBlock__listItemText` instead of `> div` — the list's cells carry their own font styles, which outrank a rule on the block's element.
418
+
419
+ - **Multiple configurations per app.** Each factory call is independent — rename the destructured components per use case:
420
+
421
+ ```tsx
422
+ export const { MjmlRichTextBlock: MjmlHeadlineRichTextBlock, HtmlRichTextBlock: HtmlHeadlineRichTextBlock } = createRichTextBlock({
423
+ blockTypes: { "header-one": { variant: "heading1" }, "header-two": { variant: "heading2" } },
424
+ });
425
+ ```
426
+
427
+ - **Links**: the `external` link type is built in — `LINK` entities with an `external` link block render as `HtmlInlineLink`. Add the application's other link types via `linkTypes`, a resolver per link block type that receives the link block's props and returns the `href` (or `undefined` for no link). Annotate the resolver parameter with the app's generated block-data type so the props are typed without redeclaring their shape. Unconfigured link types render as plain text:
428
+
429
+ ```tsx
430
+ import type { PhoneLinkBlockData } from "@src/blocks.generated";
431
+
432
+ const { MjmlRichTextBlock, HtmlRichTextBlock } = createRichTextBlock({
433
+ linkTypes: {
434
+ phone: (props: PhoneLinkBlockData) => (props.phone ? `tel:${props.phone}` : undefined),
435
+ },
436
+ });
437
+ ```
438
+
439
+ - **Inline styles**: built-in styles (`BOLD`, `ITALIC`, `SUB`, `SUP`, `STRIKETHROUGH`) render out of the box. The `inline` option maps a draft-js inline style name to a renderer and merges over the built-ins — override one, or render a custom style the app adds to its RTE (`customInlineStyles` on `IRteOptions`). The RTE stores only the style name, so the email defines the appearance. Register under the exact style name, use an inline element known to render across email clients (`<span>`, `<strong>`, `<em>` — not `<mark>`), and set explicit styles (email clients apply little of their own):
440
+
441
+ ```tsx
442
+ const { MjmlRichTextBlock, HtmlRichTextBlock } = createRichTextBlock({
443
+ inline: {
444
+ HIGHLIGHT: (children, { key }) => (
445
+ <span key={key} style={{ backgroundColor: "#ff0000", color: "#ffffff" }}>
446
+ {children}
447
+ </span>
448
+ ),
449
+ },
450
+ });
451
+ ```
452
+
453
+ - **Lists** render as a table inside one text component, with a row per item, a marker cell and a text cell — the indent and the marker gap are cell padding, which is the only spacing Outlook on Windows applies reliably.
454
+ - **List spacing** comes from the theme's `list.indent` (before the marker), `list.markerGap` (between the marker and the text) and `list.itemSpacing` (between items, and above a nested level's first item), all responsive and all applying to every list the block renders. To override it, register a rule scoped to a list's type, depth or variant modifier with `{ inline: true }`, which has MJML write the declaration into the cell's `style` attribute at compile time so it also reaches Outlook.
455
+ - **List markers** come from the theme's `list.unorderedMarker` and `list.orderedMarker`, each either a fixed node (`unorderedMarker: "▪"`) or a function of the item's `index` and its list's `depth`.
456
+ - Spacing between blocks comes from the theme's `bottomSpacing` (the last block gets none); headings are styled text, not semantic `<h1>` elements.
457
+ - Rendered elements carry `richTextBlock__text`, `richTextBlock__list`, `richTextBlock__listItem`, `richTextBlock__listItemMarker`, `richTextBlock__listItemText`, and `richTextBlock__link` class names for targeting with `registerStyles`. The list table also carries `richTextBlock__list--ordered` or `richTextBlock__list--unordered`, and `richTextBlock__list--depth<Level>` naming its nesting level, counting the outermost as zero, with `richTextBlock__list--nested` on every level below that one. Only the outermost table names the text variant its items render with, such as `richTextBlock__list--variantBody`, and a rule scoped to that modifier applies to the nested levels as well. The rows carry `richTextBlock__listItem--itemSpacing`, or `richTextBlock__listItem--blockSpacing` on the last row when spacing follows the list, and `richTextBlock__listItem--itemSpacingAbove` on a nested level's first row, which carries the item spacing as `padding-top`. The cells restate the text styles inline, so a rule targeting list text needs `!important`.
458
+
459
+ ---
460
+
461
+ ## Custom Components
462
+
463
+ When built-in components don't cover your needs, create custom components. **Always try to compose layouts using MJML components first** (`MjmlSection`, `MjmlColumn`, `MjmlText`, `MjmlImage`, etc.) — they handle cross-client compatibility automatically. Only drop into raw HTML (`MjmlRaw`, `MjmlTable`) when the MJML layout model genuinely can't express the structure you need. Raw HTML is an escape hatch, not the default approach.
464
+
465
+ When raw HTML is necessary, follow these conventions:
466
+
467
+ ### BEM Class Naming
468
+
469
+ Use BEM with camelCase blocks for CSS class names:
470
+
471
+ | BEM Part | Pattern | Example |
472
+ | -------- | ----------------------------- | ------------------------- |
473
+ | Block | `componentName` | `calloutBox` |
474
+ | Element | `componentName__elementName` | `calloutBox__title` |
475
+ | Modifier | `componentName--modifierName` | `calloutBox--highlighted` |
476
+
477
+ ### The Pattern
478
+
479
+ 1. **Inline styles** for base/desktop rendering
480
+ 2. **BEM class names** on elements that need responsive overrides
481
+ 3. **`registerStyles`** at module level with media queries
482
+ 4. **`!important`** on all responsive overrides
483
+
484
+ ```tsx
485
+ import { css, MjmlColumn, MjmlRaw, MjmlSection, registerStyles } from "@dextinity/mail-react";
486
+
487
+ function CalloutBox({ title, children }: { title: string; children: React.ReactNode }) {
488
+ return (
489
+ <MjmlSection>
490
+ <MjmlColumn>
491
+ <MjmlRaw>
492
+ <tr>
493
+ <td className="calloutBox" style={{ border: "2px solid #0066cc", borderRadius: "8px", padding: "20px" }}>
494
+ <span
495
+ className="calloutBox__title"
496
+ style={{ display: "block", margin: "0 0 8px 0", fontSize: "18px", lineHeight: "24px", msoLineHeightRule: "exactly" }}
497
+ >
498
+ {title}
499
+ </span>
500
+ <div>{children}</div>
501
+ </td>
502
+ </tr>
503
+ </MjmlRaw>
504
+ </MjmlColumn>
505
+ </MjmlSection>
506
+ );
507
+ }
508
+
509
+ registerStyles(
510
+ (theme) => css`
511
+ ${theme.breakpoints.mobile.belowMediaQuery} {
512
+ .calloutBox {
513
+ padding: 12px !important;
514
+ }
515
+ .calloutBox__title {
516
+ font-size: 16px !important;
517
+ }
518
+ }
519
+ `,
520
+ );
521
+ ```
522
+
523
+ → For the full `registerStyles` API, `belowMediaQuery` pattern, overriding built-in components, and `slotProps`, read [`references/styling-and-customization.md`](references/styling-and-customization.md).
524
+
525
+ ---
526
+
527
+ ## Rendering
528
+
529
+ Use `renderMailHtml` to convert the React tree to final HTML for sending:
530
+
531
+ ```tsx
532
+ import { MjmlMailRoot, MjmlSection, MjmlColumn, MjmlText } from "@dextinity/mail-react";
533
+ import { renderMailHtml } from "@dextinity/mail-react/server";
534
+
535
+ const { html, mjmlWarnings } = renderMailHtml(
536
+ <MjmlMailRoot theme={theme}>
537
+ <MjmlSection indent>
538
+ <MjmlColumn>
539
+ <MjmlText>Hello, world!</MjmlText>
540
+ </MjmlColumn>
541
+ </MjmlSection>
542
+ </MjmlMailRoot>,
543
+ );
544
+ ```
545
+
546
+ - **Server** (`@dextinity/mail-react/server`) — uses `mjml`, requires Node.js
547
+ - **Client** (`@dextinity/mail-react/client`) — uses `mjml-browser`, works without `fs`
548
+ - `renderMailHtml` is **not** on the main `@dextinity/mail-react` barrel — always import from `/server` or `/client`
549
+ - Returns `{ html: string; mjmlWarnings: MjmlWarning[] }` — warnings are collected, not thrown
550
+
551
+ ### Logging MJML Warnings
552
+
553
+ When generating emails outside Storybook (e.g., in a mail template's `generateMail` method), always log `mjmlWarnings` in development to catch structural issues early:
554
+
555
+ ```tsx
556
+ const { html, mjmlWarnings } = renderMailHtml(/* ... */);
557
+
558
+ if (process.env.NODE_ENV === "development" && mjmlWarnings.length) {
559
+ console.warn(`${mjmlWarnings.length} MJML Warnings`, mjmlWarnings);
560
+ }
561
+ ```
562
+
563
+ **Never log MJML warnings in production.** These warnings flag structural MJML issues (e.g., content outside the section → column → content hierarchy) and are useful during development, but the rendered HTML is always produced successfully regardless of warnings. In rare cases, achieving a specific layout intentionally requires a technically invalid MJML structure — logging these in production would spam error trackers like Sentry with noise that cannot be acted upon.
564
+
565
+ Outside Storybook, wrap content in `MjmlMailRoot` yourself (the Storybook decorator handles it automatically).
566
+
567
+ ---
568
+
569
+ ## Storybook Development
570
+
571
+ ### Setup
572
+
573
+ Add the addon to `.storybook/main.ts`:
574
+
575
+ ```ts
576
+ const config = {
577
+ addons: [
578
+ // ... other addons
579
+ "@dextinity/mail-react/storybook",
580
+ ],
581
+ };
582
+ ```
583
+
584
+ This single entry auto-configures:
585
+
586
+ - A **decorator** that wraps each story in `MjmlMailRoot`, converts MJML to HTML, and displays the rendered email
587
+ - A **Copy Mail HTML** toolbar button for copying rendered HTML to the clipboard
588
+ - A **Use Public Image URLs** toggle that replaces image sources with public placeholders — useful for testing on external services (Litmus, Email on Acid) that can't reach localhost
589
+ - An **MJML Warnings** panel for debugging validation issues
590
+
591
+ ### Writing Stories
592
+
593
+ Stories only define the email content — the decorator handles `MjmlMailRoot`. Every story should render the actual component being demonstrated (not just surrounding context):
594
+
595
+ ```tsx
596
+ import { MjmlColumn, MjmlSection, MjmlText } from "@dextinity/mail-react";
597
+ import type { Meta, StoryObj } from "@storybook/react-vite";
598
+
599
+ const config: Meta = { title: "Mails/WelcomeEmail" };
600
+ export default config;
601
+
602
+ export const Basic: StoryObj = {
603
+ render: () => (
604
+ <MjmlSection indent>
605
+ <MjmlColumn>
606
+ <MjmlText>Hello from my first email!</MjmlText>
607
+ </MjmlColumn>
608
+ </MjmlSection>
609
+ ),
610
+ };
611
+ ```
612
+
613
+ Pass a custom theme via `parameters.theme`:
614
+
615
+ ```tsx
616
+ export const CustomTheme: StoryObj = {
617
+ parameters: { theme: createTheme({ sizes: { bodyWidth: 500 } }) },
618
+ render: () => (
619
+ <MjmlSection indent>
620
+ <MjmlColumn>
621
+ <MjmlText>Narrower email at 500px</MjmlText>
622
+ </MjmlColumn>
623
+ </MjmlSection>
624
+ ),
625
+ };
626
+ ```
627
+
628
+ ### Development Workflow
629
+
630
+ 1. Write email templates as Storybook stories
631
+ 2. Preview rendered HTML in the Storybook canvas
632
+ 3. Check the **MJML Warnings** panel for validation issues
633
+ 4. Use **Copy Mail HTML** to test in external services (Litmus, Email on Acid)
634
+ 5. Enable **Use Public Image URLs** when testing on services that can't reach localhost (e.g., Litmus, Email on Acid)
635
+
636
+ ### Cross-Client Testing
637
+
638
+ Storybook previews show how the email renders in a web browser, but email clients vary dramatically. Use services like [Litmus](https://www.litmus.com/) or [Email on Acid](https://www.emailonacid.com/) to test the rendered HTML across real email clients and devices. The **Copy Mail HTML** button and **Use Public Image URLs** toggle in Storybook are designed for this workflow.
639
+
640
+ ---
641
+
642
+ ## Related Modules
643
+
644
+ The `@dextinity/mail-react` package focuses on building email markup. For sending emails and managing templates in a Dextinity project:
645
+
646
+ - **Mail Templates Module** — server-side template registration, dependency injection, and sending. Integrates with `@dextinity/mail-react` via `renderMailHtml`. Docs: https://cms-docs.dextinity.com/docs/features-modules/mail-templates-module/
647
+ - **Mailer Module** — lower-level mail sending service. Docs: https://cms-docs.dextinity.com/docs/features-modules/mailer-module/