Skip to content

Commit a6b666c

Browse files
committed
feat: expose configurable styles for emails & fix a number of styling bugs
1 parent bfd6dc0 commit a6b666c

File tree

6 files changed

+340
-37
lines changed

6 files changed

+340
-37
lines changed

examples/05-interoperability/08-converting-blocks-to-react-email/src/styles.css

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121

2222
.email {
2323
min-height: 500px;
24-
display: flex;
25-
align-items: stretch;
2624
}
2725

2826
/* hack to get react-email to show on website */

packages/xl-email-exporter/src/react-email/defaultSchema/blocks.tsx

Lines changed: 214 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {
2+
BlockMapping,
23
DefaultBlockSchema,
34
mapTableCell,
45
pageBreakSchema,
56
StyledText,
67
} from "@blocknote/core";
7-
import { BlockMapping } from "@blocknote/core/src/exporter/mapping.js";
88
import {
99
CodeBlock,
1010
dracula,
@@ -15,27 +15,168 @@ import {
1515
Text,
1616
} from "@react-email/components";
1717

18-
export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
18+
// Define TextProps type based on React Email Text component
19+
type TextProps = React.ComponentPropsWithoutRef<typeof Text>;
20+
21+
// Define the styles interface for configurable Text components
22+
export interface ReactEmailTextStyles {
23+
paragraph?: Partial<TextProps>;
24+
bulletListItem?: Partial<TextProps>;
25+
toggleListItem?: Partial<TextProps>;
26+
numberedListItem?: Partial<TextProps>;
27+
checkListItem?: Partial<TextProps>;
28+
quote?: Partial<TextProps>;
29+
tableError?: Partial<TextProps>;
30+
tableCell?: Partial<TextProps>;
31+
caption?: Partial<TextProps>;
32+
heading1?: Partial<TextProps>;
33+
heading2?: Partial<TextProps>;
34+
heading3?: Partial<TextProps>;
35+
heading4?: Partial<TextProps>;
36+
heading5?: Partial<TextProps>;
37+
heading6?: Partial<TextProps>;
38+
codeBlock?: Partial<React.ComponentProps<typeof CodeBlock>>;
39+
}
40+
41+
const defaultTextStyle: TextProps["style"] = {
42+
fontSize: 16,
43+
lineHeight: 1.5,
44+
margin: 3,
45+
};
46+
47+
// Default styles for Text components
48+
export const defaultReactEmailTextStyles = {
49+
paragraph: {
50+
style: defaultTextStyle,
51+
},
52+
bulletListItem: {
53+
style: defaultTextStyle,
54+
},
55+
toggleListItem: {
56+
style: defaultTextStyle,
57+
},
58+
numberedListItem: {
59+
style: defaultTextStyle,
60+
},
61+
checkListItem: {
62+
style: defaultTextStyle,
63+
},
64+
quote: {
65+
style: defaultTextStyle,
66+
},
67+
tableError: {
68+
style: defaultTextStyle,
69+
},
70+
tableCell: {
71+
style: defaultTextStyle,
72+
},
73+
caption: {
74+
style: defaultTextStyle,
75+
},
76+
heading1: {
77+
style: {
78+
fontSize: 48,
79+
margin: 3,
80+
},
81+
},
82+
heading2: {
83+
style: {
84+
fontSize: 36,
85+
margin: 3,
86+
},
87+
},
88+
heading3: {
89+
style: {
90+
fontSize: 24,
91+
margin: 3,
92+
},
93+
},
94+
heading4: {
95+
style: {
96+
fontSize: 20,
97+
margin: 3,
98+
},
99+
},
100+
heading5: {
101+
style: {
102+
fontSize: 18,
103+
margin: 3,
104+
},
105+
},
106+
heading6: {
107+
style: {
108+
fontSize: 16,
109+
margin: 3,
110+
},
111+
},
112+
codeBlock: {
113+
style: defaultTextStyle,
114+
},
115+
} satisfies ReactEmailTextStyles;
116+
117+
export const createReactEmailBlockMappingForDefaultSchema = (
118+
textStyles: ReactEmailTextStyles = defaultReactEmailTextStyles,
119+
): BlockMapping<
19120
DefaultBlockSchema & typeof pageBreakSchema.blockSchema,
20121
any,
21122
any,
22123
React.ReactElement<any>,
23124
React.ReactElement<typeof Link> | React.ReactElement<HTMLSpanElement>
24-
> = {
125+
> => ({
25126
paragraph: (block, t) => {
26-
return <Text>{t.transformInlineContent(block.content)}</Text>;
127+
return (
128+
<Text
129+
{...textStyles.paragraph}
130+
style={{
131+
...defaultReactEmailTextStyles.paragraph.style,
132+
...textStyles.paragraph?.style,
133+
}}
134+
>
135+
{t.transformInlineContent(block.content)}
136+
</Text>
137+
);
27138
},
28139
bulletListItem: (block, t) => {
29140
// Return only the <li> for grouping in the exporter
30-
return <Text>{t.transformInlineContent(block.content)}</Text>;
141+
return (
142+
<Text
143+
{...textStyles.bulletListItem}
144+
style={{
145+
...defaultReactEmailTextStyles.bulletListItem.style,
146+
...textStyles.bulletListItem?.style,
147+
}}
148+
>
149+
{t.transformInlineContent(block.content)}
150+
</Text>
151+
);
31152
},
32153
toggleListItem: (block, t) => {
33154
// Return only the <li> for grouping in the exporter
34-
return <Text>{t.transformInlineContent(block.content)}</Text>;
155+
return (
156+
<Text
157+
{...textStyles.toggleListItem}
158+
style={{
159+
...defaultReactEmailTextStyles.toggleListItem.style,
160+
...textStyles.toggleListItem?.style,
161+
}}
162+
>
163+
{t.transformInlineContent(block.content)}
164+
</Text>
165+
);
35166
},
36167
numberedListItem: (block, t, _nestingLevel) => {
37168
// Return only the <li> for grouping in the exporter
38-
return <Text>{t.transformInlineContent(block.content)}</Text>;
169+
return (
170+
<Text
171+
{...textStyles.numberedListItem}
172+
style={{
173+
...defaultReactEmailTextStyles.numberedListItem.style,
174+
...textStyles.numberedListItem?.style,
175+
}}
176+
>
177+
{t.transformInlineContent(block.content)}
178+
</Text>
179+
);
39180
},
40181
checkListItem: (block, t) => {
41182
// Render a checkbox using inline SVG for better appearance in email
@@ -85,15 +226,28 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
85226
</svg>
86227
);
87228
return (
88-
<Text>
229+
<Text
230+
{...textStyles.checkListItem}
231+
style={{
232+
...defaultReactEmailTextStyles.checkListItem.style,
233+
...textStyles.checkListItem?.style,
234+
}}
235+
>
89236
{checkboxSvg}
90237
<span>{t.transformInlineContent(block.content)}</span>
91238
</Text>
92239
);
93240
},
94241
heading: (block, t) => {
95242
return (
96-
<Heading as={`h${block.props.level}`}>
243+
<Heading
244+
as={`h${block.props.level}`}
245+
{...textStyles[`heading${block.props.level}`]}
246+
style={{
247+
...defaultReactEmailTextStyles[`heading${block.props.level}`].style,
248+
...textStyles[`heading${block.props.level}`]?.style,
249+
}}
250+
>
97251
{t.transformInlineContent(block.content)}
98252
</Heading>
99253
);
@@ -108,6 +262,11 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
108262
fontFamily="'CommitMono', monospace"
109263
language={block.props.language as PrismLanguage}
110264
theme={dracula}
265+
{...textStyles.codeBlock}
266+
style={{
267+
...defaultReactEmailTextStyles.codeBlock.style,
268+
...textStyles.codeBlock?.style,
269+
}}
111270
/>
112271
);
113272
},
@@ -136,7 +295,11 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
136295
defaultText="Open audio file"
137296
icon={icon}
138297
/>
139-
<Caption caption={block.props.caption} width={previewWidth} />
298+
<Caption
299+
caption={block.props.caption}
300+
width={previewWidth}
301+
textStyles={textStyles}
302+
/>
140303
</div>
141304
);
142305
},
@@ -165,7 +328,11 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
165328
defaultText="Open video file"
166329
icon={icon}
167330
/>
168-
<Caption caption={block.props.caption} width={previewWidth} />
331+
<Caption
332+
caption={block.props.caption}
333+
width={previewWidth}
334+
textStyles={textStyles}
335+
/>
169336
</div>
170337
);
171338
},
@@ -194,7 +361,11 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
194361
defaultText="Open file"
195362
icon={icon}
196363
/>
197-
<Caption caption={block.props.caption} width={previewWidth} />
364+
<Caption
365+
caption={block.props.caption}
366+
width={previewWidth}
367+
textStyles={textStyles}
368+
/>
198369
</div>
199370
);
200371
},
@@ -211,7 +382,7 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
211382
// Render table using standard HTML table elements for email compatibility
212383
const table = block.content;
213384
if (!table || typeof table !== "object" || !Array.isArray(table.rows)) {
214-
return <Text>Table data not available</Text>;
385+
return <Text {...textStyles.tableError}>Table data not available</Text>;
215386
}
216387
const headerRowsCount = (table.headerRows as number) ?? 0;
217388
const headerColsCount = (table.headerCols as number) ?? 0;
@@ -258,6 +429,8 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
258429
normalizedCell.props.textColor !== "default"
259430
? normalizedCell.props.textColor
260431
: "inherit",
432+
...defaultReactEmailTextStyles.tableCell.style,
433+
...textStyles.tableCell?.style,
261434
}}
262435
{...((normalizedCell.props.colspan || 1) > 1 && {
263436
colSpan: normalizedCell.props.colspan || 1,
@@ -280,6 +453,7 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
280453
// Render block quote with a left border and subtle background for email compatibility
281454
return (
282455
<Text
456+
{...textStyles.quote}
283457
style={{
284458
borderLeft: "4px solid #bdbdbd",
285459
background: "#f9f9f9",
@@ -288,6 +462,8 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
288462
fontStyle: "italic",
289463
color: "#555",
290464
display: "block",
465+
...defaultReactEmailTextStyles.quote.style,
466+
...textStyles.quote?.style,
291467
}}
292468
>
293469
{t.transformInlineContent(block.content)}
@@ -306,7 +482,11 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
306482
/>
307483
);
308484
},
309-
};
485+
});
486+
487+
// Export the original mapping for backward compatibility
488+
export const reactEmailBlockMappingForDefaultSchema =
489+
createReactEmailBlockMappingForDefaultSchema();
310490

311491
// Helper for file-like blocks (audio, video, file)
312492
function FileLink({
@@ -338,12 +518,30 @@ function FileLink({
338518
);
339519
}
340520

341-
function Caption({ caption, width }: { caption?: string; width?: number }) {
521+
function Caption({
522+
caption,
523+
width,
524+
textStyles,
525+
}: {
526+
caption?: string;
527+
width?: number;
528+
textStyles: ReactEmailTextStyles;
529+
}) {
342530
if (!caption) {
343531
return null;
344532
}
345533
return (
346-
<Text style={{ width, fontSize: 13, color: "#888", margin: "4px 0 0 0" }}>
534+
<Text
535+
{...textStyles.caption}
536+
style={{
537+
width,
538+
fontSize: 13,
539+
color: "#888",
540+
margin: "4px 0 0 0",
541+
...defaultReactEmailTextStyles.caption.style,
542+
...textStyles.caption?.style,
543+
}}
544+
>
347545
{caption}
348546
</Text>
349547
);

0 commit comments

Comments
 (0)