Skip to content

Commit 100afc5

Browse files
committed
initial attempt at using tsup to build tailwind
1 parent 5b8095f commit 100afc5

File tree

165 files changed

+44450
-485
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

165 files changed

+44450
-485
lines changed

package.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,13 @@
3030
"vite": "5.4.6",
3131
"vitest": "2.0.5"
3232
},
33-
"packageManager": "[email protected]+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c"
33+
"packageManager": "[email protected]+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c",
34+
"pnpm": {
35+
"patchedDependencies": {
36+
37+
38+
39+
40+
}
41+
}
3442
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`<Body> component > renders correctly 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><!--$--><body>Lorem ipsum<!--/$--></body>"`;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { render } from "../render/node";
2+
import { Body } from "./body";
3+
4+
describe("<Body> component", () => {
5+
it("renders children correctly", async () => {
6+
const testMessage = "Test message";
7+
const html = await render(<Body>{testMessage}</Body>);
8+
expect(html).toContain(testMessage);
9+
});
10+
11+
it("passes style and other props correctly", async () => {
12+
const style = { backgroundColor: "red" };
13+
const html = await render(
14+
<Body data-testid="body-test" style={style}>
15+
Test
16+
</Body>,
17+
);
18+
expect(html).toContain('style="background-color:red"');
19+
expect(html).toContain('data-testid="body-test"');
20+
});
21+
22+
it("renders correctly", async () => {
23+
const actualOutput = await render(<Body>Lorem ipsum</Body>);
24+
expect(actualOutput).toMatchSnapshot();
25+
});
26+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as React from "react";
2+
3+
export type BodyProps = Readonly<React.HtmlHTMLAttributes<HTMLBodyElement>>;
4+
5+
export const Body = React.forwardRef<HTMLBodyElement, BodyProps>(
6+
({ children, style, ...props }, ref) => {
7+
return (
8+
<body {...props} ref={ref} style={style}>
9+
{children}
10+
</body>
11+
);
12+
},
13+
);
14+
15+
Body.displayName = "Body";
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`<Button> component > renders correctly with padding values from style prop 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><!--$--><a href="https://example.com" style="line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;padding:12px 20px 12px 20px" target="_blank"><span><!--[if mso]><i style="mso-font-width:500%;mso-text-raise:18" hidden>&#8202;&#8202;</i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px"></span><span><!--[if mso]><i style="mso-font-width:500%" hidden>&#8202;&#8202;&#8203;</i><![endif]--></span></a><!--/$-->"`;
4+
5+
exports[`<Button> component > renders the <Button> component with no padding value 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><!--$--><a href="https://example.com" style="line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;padding:0px 0px 0px 0px" target="_blank"><span><!--[if mso]><i style="mso-font-width:0%;mso-text-raise:0" hidden></i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:0"></span><span><!--[if mso]><i style="mso-font-width:0%" hidden>&#8203;</i><![endif]--></span></a><!--/$-->"`;
6+
7+
exports[`<Button> component > should allow users to overwrite style props 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><!--$--><a style="line-height:150%;text-decoration:underline red;display:block;max-width:50%;mso-padding-alt:0px;padding:0px 0px 0px 0px" target="_blank"><span><!--[if mso]><i style="mso-font-width:0%;mso-text-raise:0" hidden></i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:0"></span><span><!--[if mso]><i style="mso-font-width:0%" hidden>&#8203;</i><![endif]--></span></a><!--/$-->"`;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { render } from "../render/node";
2+
import { Button } from "./button";
3+
4+
describe("<Button> component", () => {
5+
it("renders children correctly", async () => {
6+
const testMessage = "Test message";
7+
const html = await render(<Button>{testMessage}</Button>);
8+
expect(html).toContain(testMessage);
9+
});
10+
11+
it("passes style and other props correctly", async () => {
12+
const style = { backgroundColor: "red" };
13+
const html = await render(
14+
<Button data-testid="button-test" style={style}>
15+
Test
16+
</Button>,
17+
);
18+
expect(html).toContain("background-color:red");
19+
expect(html).toContain('data-testid="button-test"');
20+
});
21+
22+
it("renders correctly with padding values from style prop", async () => {
23+
const actualOutput = await render(
24+
<Button href="https://example.com" style={{ padding: "12px 20px" }} />,
25+
);
26+
expect(actualOutput).toMatchSnapshot();
27+
});
28+
29+
it("renders the <Button> component with no padding value", async () => {
30+
const actualOutput = await render(<Button href="https://example.com" />);
31+
expect(actualOutput).toMatchSnapshot();
32+
});
33+
34+
it("should allow users to overwrite style props", async () => {
35+
const actualOutput = await render(
36+
<Button
37+
style={{
38+
lineHeight: "150%",
39+
display: "block",
40+
textDecoration: "underline red",
41+
maxWidth: "50%",
42+
}}
43+
/>,
44+
);
45+
expect(actualOutput).toMatchSnapshot();
46+
});
47+
});
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import * as React from "react";
2+
import { parsePadding } from "./utils/parse-padding";
3+
import { pxToPt } from "./utils/px-to-pt";
4+
5+
export type ButtonProps = Readonly<React.ComponentPropsWithoutRef<"a">>;
6+
7+
const maxFontWidth = 5;
8+
9+
/**
10+
* Computes a msoFontWidth \<= 5 and a count of space characters that,
11+
* when applied, end up being as close to `expectedWidth` as possible.
12+
*/
13+
function computeFontWidthAndSpaceCount(expectedWidth: number) {
14+
if (expectedWidth === 0) return [0, 0];
15+
16+
let smallestSpaceCount = 0;
17+
18+
const computeRequiredFontWidth = () => {
19+
if (smallestSpaceCount > 0) {
20+
return expectedWidth / smallestSpaceCount / 2;
21+
}
22+
23+
return Infinity;
24+
};
25+
26+
while (computeRequiredFontWidth() > maxFontWidth) {
27+
smallestSpaceCount++;
28+
}
29+
30+
return [computeRequiredFontWidth(), smallestSpaceCount] as const;
31+
}
32+
33+
export const Button = React.forwardRef<HTMLAnchorElement, ButtonProps>(
34+
({ children, style, target = "_blank", ...props }, ref) => {
35+
const { pt, pr, pb, pl } = parsePadding({
36+
padding: style?.padding,
37+
paddingLeft: style?.paddingLeft ?? style?.paddingInline,
38+
paddingRight: style?.paddingRight ?? style?.paddingInline,
39+
paddingTop: style?.paddingTop ?? style?.paddingBlock,
40+
paddingBottom: style?.paddingBottom ?? style?.paddingBlock,
41+
});
42+
43+
const y = pt + pb;
44+
const textRaise = pxToPt(y);
45+
46+
const [plFontWidth, plSpaceCount] = computeFontWidthAndSpaceCount(pl);
47+
const [prFontWidth, prSpaceCount] = computeFontWidthAndSpaceCount(pr);
48+
49+
return (
50+
<a
51+
{...props}
52+
ref={ref}
53+
style={buttonStyle({ ...style, pt, pr, pb, pl })}
54+
target={target}
55+
>
56+
<span
57+
dangerouslySetInnerHTML={{
58+
// The `&#8202;` is as close to `1px` of an empty character as we can get, then, we use the `mso-font-width`
59+
// to scale it according to what padding the developer wants. `mso-font-width` also does not allow for percentages
60+
// >= 500% so we need to add extra spaces accordingly.
61+
//
62+
// See https://github.com/resend/react-email/issues/1512 for why we do not use letter-spacing instead.
63+
__html: `<!--[if mso]><i style="mso-font-width:${
64+
plFontWidth * 100
65+
}%;mso-text-raise:${textRaise}" hidden>${"&#8202;".repeat(
66+
plSpaceCount,
67+
)}</i><![endif]-->`,
68+
}}
69+
/>
70+
<span style={buttonTextStyle(pb)}>{children}</span>
71+
<span
72+
dangerouslySetInnerHTML={{
73+
__html: `<!--[if mso]><i style="mso-font-width:${
74+
prFontWidth * 100
75+
}%" hidden>${"&#8202;".repeat(
76+
prSpaceCount,
77+
)}&#8203;</i><![endif]-->`,
78+
}}
79+
/>
80+
</a>
81+
);
82+
},
83+
);
84+
85+
Button.displayName = "Button";
86+
87+
const buttonStyle = (
88+
style?: React.CSSProperties & {
89+
pt: number;
90+
pr: number;
91+
pb: number;
92+
pl: number;
93+
},
94+
) => {
95+
const { pt, pr, pb, pl, ...rest } = style || {};
96+
97+
return {
98+
lineHeight: "100%",
99+
textDecoration: "none",
100+
display: "inline-block",
101+
maxWidth: "100%",
102+
msoPaddingAlt: "0px",
103+
...rest,
104+
padding: `${pt}px ${pr}px ${pb}px ${pl}px`,
105+
};
106+
};
107+
108+
const buttonTextStyle = (pb?: number) => {
109+
return {
110+
maxWidth: "100%",
111+
display: "inline-block",
112+
lineHeight: "120%",
113+
msoPaddingAlt: "0px",
114+
msoTextRaise: pxToPt(pb || 0),
115+
};
116+
};
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
type PaddingType = string | number | undefined;
2+
3+
interface PaddingProperties {
4+
padding: PaddingType;
5+
paddingTop?: PaddingType;
6+
paddingRight?: PaddingType;
7+
paddingBottom?: PaddingType;
8+
paddingLeft?: PaddingType;
9+
}
10+
11+
/**
12+
* converts padding value to `px` equivalent.
13+
* @example "1em" =\> 16
14+
*/
15+
export function convertToPx(value: PaddingType) {
16+
let px = 0;
17+
18+
if (!value) {
19+
return px;
20+
}
21+
22+
if (typeof value === "number") {
23+
return value;
24+
}
25+
26+
const matches = /^([\d.]+)(px|em|rem|%)$/.exec(value);
27+
28+
if (matches && matches.length === 3) {
29+
const numValue = parseFloat(matches[1]);
30+
const unit = matches[2];
31+
32+
switch (unit) {
33+
case "px":
34+
return numValue;
35+
case "em":
36+
case "rem":
37+
px = numValue * 16;
38+
return px;
39+
case "%":
40+
px = (numValue / 100) * 600;
41+
return px;
42+
default:
43+
return numValue;
44+
}
45+
} else {
46+
return 0;
47+
}
48+
}
49+
50+
/**
51+
* Parses all the values out of a padding string to get the value for all padding props in `px`
52+
* @example e.g. "10px" =\> pt: 10, pr: 10, pb: 10, pl: 10
53+
*/
54+
export function parsePadding({
55+
padding = "",
56+
paddingTop,
57+
paddingRight,
58+
paddingBottom,
59+
paddingLeft,
60+
}: PaddingProperties) {
61+
let pt = 0;
62+
let pr = 0;
63+
let pb = 0;
64+
let pl = 0;
65+
66+
if (typeof padding === "number") {
67+
pt = padding;
68+
pr = padding;
69+
pb = padding;
70+
pl = padding;
71+
} else {
72+
const values = padding.split(/\s+/);
73+
74+
switch (values.length) {
75+
case 1:
76+
pt = convertToPx(values[0]);
77+
pr = convertToPx(values[0]);
78+
pb = convertToPx(values[0]);
79+
pl = convertToPx(values[0]);
80+
break;
81+
case 2:
82+
pt = convertToPx(values[0]);
83+
pb = convertToPx(values[0]);
84+
pr = convertToPx(values[1]);
85+
pl = convertToPx(values[1]);
86+
break;
87+
case 3:
88+
pt = convertToPx(values[0]);
89+
pr = convertToPx(values[1]);
90+
pl = convertToPx(values[1]);
91+
pb = convertToPx(values[2]);
92+
break;
93+
case 4:
94+
pt = convertToPx(values[0]);
95+
pr = convertToPx(values[1]);
96+
pb = convertToPx(values[2]);
97+
pl = convertToPx(values[3]);
98+
break;
99+
default:
100+
break;
101+
}
102+
}
103+
104+
return {
105+
pt: paddingTop ? convertToPx(paddingTop) : pt,
106+
pr: paddingRight ? convertToPx(paddingRight) : pr,
107+
pb: paddingBottom ? convertToPx(paddingBottom) : pb,
108+
pl: paddingLeft ? convertToPx(paddingLeft) : pl,
109+
};
110+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const pxToPt = (px: number): number | null =>
2+
typeof px === "number" && !isNaN(Number(px)) ? (px * 3) / 4 : null;

0 commit comments

Comments
 (0)