@depup/react-email 6.9.3-depup.0 → 6.11.0-depup.0

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 (32) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +8 -9
  3. package/changes.json +7 -11
  4. package/dist/cli/index.mjs +74 -33
  5. package/dist/components/container/container.cjs +18 -3
  6. package/dist/components/container/container.d.cts +3 -1
  7. package/dist/components/container/container.d.mts +3 -1
  8. package/dist/components/container/container.mjs +18 -3
  9. package/dist/components/element-marker.cjs +8 -2
  10. package/dist/components/element-marker.d.cts +14 -0
  11. package/dist/components/element-marker.d.mts +14 -0
  12. package/dist/components/element-marker.mjs +8 -3
  13. package/dist/components/index.d.cts +1 -0
  14. package/dist/components/index.d.mts +1 -0
  15. package/dist/components/section/section.cjs +18 -3
  16. package/dist/components/section/section.d.cts +3 -1
  17. package/dist/components/section/section.d.mts +3 -1
  18. package/dist/components/section/section.mjs +18 -3
  19. package/dist/components/tailwind/utils/tailwindcss/clone-element-with-inlined-styles.cjs +34 -21
  20. package/dist/components/tailwind/utils/tailwindcss/clone-element-with-inlined-styles.mjs +34 -21
  21. package/dist/index.cjs +2 -0
  22. package/dist/index.d.cts +2 -1
  23. package/dist/index.d.mts +2 -1
  24. package/dist/index.mjs +2 -1
  25. package/package.json +16 -20
  26. package/readme.md +8 -0
  27. package/src/components/container/container.tsx +66 -46
  28. package/src/components/element-marker.ts +25 -2
  29. package/src/components/index.ts +5 -0
  30. package/src/components/section/section.tsx +66 -46
  31. package/src/components/tailwind/tailwind.spec.tsx +13 -0
  32. package/src/components/tailwind/utils/tailwindcss/clone-element-with-inlined-styles.ts +46 -37
@@ -3,57 +3,77 @@ import { markAsElement } from '../element-marker.js';
3
3
 
4
4
  export type SectionProps = Readonly<React.ComponentPropsWithoutRef<'table'>>;
5
5
 
6
- export const Section = React.forwardRef<HTMLTableElement, SectionProps>(
7
- ({ children, style = {}, ...props }, ref) => {
8
- // Split padding styles to improve compatibility with Klaviyo and Outlook,
9
- // while preserving user-provided style property order without allocating
10
- // entry arrays on each render.
11
- const tdStyle: React.CSSProperties = {};
12
- const tableStyle: React.CSSProperties = {};
13
-
14
- const styleRecord = style as Record<string, unknown>;
15
-
16
- for (const key in styleRecord) {
17
- if (!Object.hasOwn(styleRecord, key)) {
18
- continue;
19
- }
6
+ export const Section = React.forwardRef<
7
+ HTMLTableElement,
8
+ SectionProps & { tdClassName?: string }
9
+ >(({ children, style = {}, tdClassName, ...props }, ref) => {
10
+ // Split padding styles to improve compatibility with Klaviyo and Outlook,
11
+ // while preserving user-provided style property order without allocating
12
+ // entry arrays on each render.
13
+ const tdStyle: React.CSSProperties = {};
14
+ const tableStyle: React.CSSProperties = {};
15
+
16
+ const styleRecord = style as Record<string, unknown>;
17
+
18
+ for (const key in styleRecord) {
19
+ if (!Object.hasOwn(styleRecord, key)) {
20
+ continue;
21
+ }
22
+
23
+ const value = styleRecord[key];
20
24
 
21
- const value = styleRecord[key];
25
+ if (key.startsWith('padding')) {
26
+ (tdStyle as Record<string, unknown>)[key] = value;
27
+ } else {
28
+ (tableStyle as Record<string, unknown>)[key] = value;
29
+ }
30
+ }
22
31
 
32
+ return (
33
+ <table
34
+ align="center"
35
+ width="100%"
36
+ border={0}
37
+ cellPadding="0"
38
+ cellSpacing="0"
39
+ role="presentation"
40
+ {...props}
41
+ ref={ref}
42
+ style={tableStyle}
43
+ >
44
+ <tbody>
45
+ <tr>
46
+ <td className={tdClassName} style={tdStyle}>
47
+ {children}
48
+ </td>
49
+ </tr>
50
+ </tbody>
51
+ </table>
52
+ );
53
+ });
54
+
55
+ Section.displayName = 'Section';
56
+ markAsElement<SectionProps & { tdClassName?: string }>(Section, {
57
+ resolveTailwind(props, { style, className, classProperties }) {
58
+ const tableClasses: string[] = [];
59
+ const tdClasses = props.tdClassName ? [props.tdClassName] : [];
60
+ for (const name of className?.split(' ') ?? []) {
61
+ const properties = classProperties[name];
23
62
  if (
24
- key === 'padding' ||
25
- key === 'paddingTop' ||
26
- key === 'paddingRight' ||
27
- key === 'paddingBottom' ||
28
- key === 'paddingLeft'
63
+ properties &&
64
+ properties.length > 0 &&
65
+ properties.every((property) => property.startsWith('padding'))
29
66
  ) {
30
- (tdStyle as Record<string, unknown>)[key] = value;
67
+ tdClasses.push(name);
31
68
  } else {
32
- (tableStyle as Record<string, unknown>)[key] = value;
69
+ tableClasses.push(name);
33
70
  }
34
71
  }
35
-
36
- return (
37
- <table
38
- align="center"
39
- width="100%"
40
- border={0}
41
- cellPadding="0"
42
- cellSpacing="0"
43
- role="presentation"
44
- {...props}
45
- ref={ref}
46
- style={tableStyle}
47
- >
48
- <tbody>
49
- <tr>
50
- <td style={tdStyle}>{children}</td>
51
- </tr>
52
- </tbody>
53
- </table>
54
- );
72
+ return {
73
+ ...props,
74
+ style,
75
+ className: tableClasses.length > 0 ? tableClasses.join(' ') : undefined,
76
+ tdClassName: tdClasses.length > 0 ? tdClasses.join(' ') : undefined,
77
+ };
55
78
  },
56
- );
57
-
58
- Section.displayName = 'Section';
59
- markAsElement(Section);
79
+ });
@@ -275,6 +275,19 @@ describe('Tailwind component', () => {
275
275
  expect(html).not.toMatch(/<table[^>]*style="[^"]*padding:1rem/);
276
276
  });
277
277
 
278
+ it('routes responsive padding classes on <Section> to the inner <td>', async () => {
279
+ const html = await render(
280
+ <Tailwind>
281
+ <Head />
282
+ <Section className="max-sm:px-5 max-sm:bg-red-500 px-9">x</Section>
283
+ </Tailwind>,
284
+ );
285
+
286
+ expect(html).toMatchInlineSnapshot(
287
+ `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><head><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/><style>@media (max-width:40rem){.max-sm_bg-red-500{background-color:rgb(251,44,54)!important}}@media (max-width:40rem){.max-sm_px-5{padding-right:1.25rem!important;padding-left:1.25rem!important}}</style></head><!--$--><!--head--><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" class="max-sm_bg-red-500"><tbody><tr><td class="max-sm_px-5" style="padding-right:2.25rem;padding-left:2.25rem">x</td></tr></tbody></table><!--/$-->"`,
288
+ );
289
+ });
290
+
278
291
  it('inlines Tailwind classes on <Column> onto its <td>', async () => {
279
292
  const html = await render(
280
293
  <Tailwind>
@@ -1,5 +1,6 @@
1
- import type { Rule } from 'css-tree';
1
+ import { type Rule, walk } from 'css-tree';
2
2
  import React from 'react';
3
+ import { getElementOptions } from '../../../element-marker.js';
3
4
  import type { EmailElementProps } from '../../tailwind.js';
4
5
  import { sanitizeClassName } from '../compatibility/sanitize-class-name.js';
5
6
  import type { CustomProperties } from '../css/get-custom-properties.js';
@@ -12,50 +13,58 @@ export function cloneElementWithInlinedStyles(
12
13
  nonInlinableRules: Map<string, Rule[]>,
13
14
  customProperties: CustomProperties,
14
15
  ) {
15
- const propsToOverwrite: Partial<EmailElementProps> = {};
16
+ if (!element.props.className || isComponent(element)) {
17
+ return React.cloneElement(element, element.props, element.props.children);
18
+ }
16
19
 
17
- if (element.props.className && !isComponent(element)) {
18
- const classes = element.props.className.trim().split(/\s+/);
20
+ const classes = element.props.className.trim().split(/\s+/);
19
21
 
20
- const residualClasses: string[] = [];
22
+ const residualClasses: string[] = [];
23
+ const classProperties: Record<string, string[]> = Object.create(null);
21
24
 
22
- const rules: Rule[] = [];
23
- for (const className of classes) {
24
- const classRules = inlinableRules.get(className);
25
- if (classRules) {
26
- rules.push(...classRules);
27
- }
28
- if (nonInlinableRules.has(className)) {
29
- residualClasses.push(className);
30
- } else if (!classRules) {
31
- residualClasses.push(className);
32
- }
25
+ const rules: Rule[] = [];
26
+ for (const className of classes) {
27
+ const classRules = inlinableRules.get(className);
28
+ if (classRules) {
29
+ rules.push(...classRules);
33
30
  }
34
-
35
- const styles = makeInlineStylesFor(rules, customProperties);
36
- propsToOverwrite.style = {
37
- ...styles,
38
- ...element.props.style,
39
- };
40
-
41
- if (residualClasses.length > 0) {
42
- propsToOverwrite.className = residualClasses
43
- .map((className) => {
44
- if (nonInlinableRules.has(className)) {
45
- return sanitizeClassName(className);
46
- }
47
- return className;
48
- })
49
- .join(' ');
50
- } else {
51
- propsToOverwrite.className = undefined;
31
+ const nonInlinable = nonInlinableRules.get(className);
32
+ if (nonInlinable) {
33
+ const sanitized = sanitizeClassName(className);
34
+ residualClasses.push(sanitized);
35
+ const properties: string[] = [];
36
+ for (const rule of nonInlinable) {
37
+ walk(rule, {
38
+ visit: 'Declaration',
39
+ enter(declaration) {
40
+ properties.push(declaration.property);
41
+ },
42
+ });
43
+ }
44
+ classProperties[sanitized] = properties;
45
+ } else if (!classRules) {
46
+ residualClasses.push(className);
52
47
  }
53
48
  }
54
49
 
55
- const newProps = {
56
- ...element.props,
57
- ...propsToOverwrite,
50
+ const resolved = {
51
+ style: {
52
+ ...makeInlineStylesFor(rules, customProperties),
53
+ ...element.props.style,
54
+ },
55
+ className:
56
+ residualClasses.length > 0 ? residualClasses.join(' ') : undefined,
57
+ classProperties,
58
58
  };
59
59
 
60
+ const resolveTailwind = getElementOptions(element.type)?.resolveTailwind;
61
+ const newProps = resolveTailwind
62
+ ? (resolveTailwind(element.props, resolved) as EmailElementProps)
63
+ : {
64
+ ...element.props,
65
+ style: resolved.style,
66
+ className: resolved.className,
67
+ };
68
+
60
69
  return React.cloneElement(element, newProps, newProps.children);
61
70
  }