Skip to content

Commit edd488b

Browse files
committed
Energize ⚡
0 parents  commit edd488b

9 files changed

+8594
-0
lines changed

.editorconfig

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 2
6+
end_of_line = lf
7+
charset = utf-8
8+
trim_trailing_whitespace = true
9+
insert_final_newline = true

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.vscode
2+
node_modules
3+
npm-debug.log
4+
yarn-error.log

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2022 Cosmin Popovici
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# tailwindcss-email-variants
2+
3+
A plugin that provides variants for email client targeting hacks used in HTML emails.
4+
5+
## Installation
6+
7+
Install the plugin from npm:
8+
9+
```sh
10+
npm install -D tailwindcss-email-variants
11+
```
12+
13+
Then add the plugin to your `tailwind.config.js` file:
14+
15+
```js
16+
// tailwind.config.js
17+
module.exports = {
18+
theme: {
19+
// ...
20+
},
21+
plugins: [
22+
require('tailwindcss-email-variants'),
23+
// ...
24+
],
25+
}
26+
```
27+
28+
## Usage
29+
30+
Use the available variants to generate utilities that target specific email clients.
31+
32+
### Outlook.com dark mode
33+
34+
Change `color` and `background-color` of elements in [Outlook.com dark mode](https://www.hteumeuleu.com/2021/emails-react-outlook-com-dark-mode/).
35+
36+
```html
37+
<!-- Color -->
38+
<div class="ogsc:text-slate-200">...</div>
39+
40+
<!-- Background color -->
41+
<div class="ogsb:bg-slate-900">...</div>
42+
```
43+
44+
Result:
45+
46+
```css
47+
[data-ogsc] .ogsc\:text-slate-200 {
48+
color: #e2e8f0;
49+
}
50+
51+
[data-ogsb] .ogsb\:bg-slate-900 {
52+
background-color: #0f172a;
53+
}
54+
```
55+
56+
### Gmail
57+
58+
Use the `gmail` variant to target Gmail's webmail:
59+
60+
```html
61+
<div class="gmail:hidden">...</div>
62+
```
63+
64+
Result:
65+
66+
```css
67+
u + .body .gmail\:hidden {
68+
display: none;
69+
}
70+
```
71+
72+
### Gmail (Android)
73+
74+
Use the `gmail-android` variant to target Gmail on Android devices:
75+
76+
```html
77+
<div class="gmail-android:hidden">...</div>
78+
```
79+
80+
Result:
81+
82+
```css
83+
div > u + .body .gmail-android\:hidden {
84+
display: none;
85+
}
86+
```
87+
88+
### iOS Mail (10+)
89+
90+
Use the `ios` variant to target iOS Mail 10 and up:
91+
92+
```html
93+
<div class="ios:hidden">...</div>
94+
```
95+
96+
Result:
97+
98+
```css
99+
@supports (-webkit-overflow-scrolling:touch) and (color:#ffff) {
100+
.ios\:hidden {
101+
display: none;
102+
}
103+
}
104+
```
105+
106+
### iOS Mail (15)
107+
108+
Use the `ios-15` variant to target iOS Mail 15 specifically:
109+
110+
```html
111+
<div class="ios-15:hidden">...</div>
112+
```
113+
114+
Result:
115+
116+
```css
117+
@supports (-webkit-overflow-scrolling:touch) and (aspect-ratio: 1 / 1) {
118+
.ios-15\:hidden {
119+
display: none;
120+
}
121+
}
122+
```
123+
124+
### Outlook (webmail)
125+
126+
Use the `outlook-web` variant to target iOS Mail 15 specifically:
127+
128+
```html
129+
<div class="outlook-web:hidden">...</div>
130+
```
131+
132+
Result:
133+
134+
```css
135+
[class~="x_outlook-web\:hidden"] {
136+
display: none;
137+
}
138+
```
139+
140+
## Configuration
141+
142+
You can add your own variants by passing a configuration object to the plugin.
143+
144+
```js
145+
// tailwind.config.js
146+
module.exports = {
147+
plugins: [
148+
require('tailwindcss-email-variants')({
149+
thunderbird: '.moz-text-html &', // & is the utility class
150+
example: ctx => `.example ${ctx.container.nodes[0].selector}` // using a function
151+
}),
152+
// ...
153+
],
154+
}
155+
```
156+
157+
Use it:
158+
159+
```html
160+
<div class="thunderbird:hidden example:flex">...</div>
161+
```
162+
163+
Result:
164+
165+
```css
166+
.moz-text-html .thunderbird\:hidden {
167+
display: none;
168+
}
169+
170+
.example .flex {
171+
display: flex;
172+
}
173+
```

jest/customMatchers.js

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
const prettier = require('prettier')
2+
const { diff } = require('jest-diff')
3+
4+
function format(input) {
5+
return prettier.format(input, {
6+
parser: 'css',
7+
printWidth: 100,
8+
})
9+
}
10+
11+
expect.extend({
12+
// Compare two CSS strings with all whitespace removed
13+
// This is probably naive but it's fast and works well enough.
14+
toMatchCss(received, argument) {
15+
function stripped(str) {
16+
return str.replace(/\s/g, '').replace(/;/g, '')
17+
}
18+
19+
const options = {
20+
comment: 'stripped(received) === stripped(argument)',
21+
isNot: this.isNot,
22+
promise: this.promise,
23+
}
24+
25+
const pass = stripped(received) === stripped(argument)
26+
27+
const message = pass
28+
? () => {
29+
return (
30+
this.utils.matcherHint('toMatchCss', undefined, undefined, options) +
31+
'\n\n' +
32+
`Expected: not ${this.utils.printExpected(format(received))}\n` +
33+
`Received: ${this.utils.printReceived(format(argument))}`
34+
)
35+
}
36+
: () => {
37+
const actual = format(received)
38+
const expected = format(argument)
39+
40+
const diffString = diff(expected, actual, {
41+
expand: this.expand,
42+
})
43+
44+
return (
45+
this.utils.matcherHint('toMatchCss', undefined, undefined, options) +
46+
'\n\n' +
47+
(diffString && diffString.includes('- Expect')
48+
? `Difference:\n\n${diffString}`
49+
: `Expected: ${this.utils.printExpected(expected)}\n` +
50+
`Received: ${this.utils.printReceived(actual)}`)
51+
)
52+
}
53+
54+
return { actual: received, message, pass }
55+
},
56+
toIncludeCss(received, argument) {
57+
const options = {
58+
comment: 'stripped(received).includes(stripped(argument))',
59+
isNot: this.isNot,
60+
promise: this.promise,
61+
}
62+
63+
const actual = format(received)
64+
const expected = format(argument)
65+
66+
const pass = actual.includes(expected)
67+
68+
const message = pass
69+
? () => {
70+
return (
71+
this.utils.matcherHint('toIncludeCss', undefined, undefined, options) +
72+
'\n\n' +
73+
`Expected: not ${this.utils.printExpected(format(received))}\n` +
74+
`Received: ${this.utils.printReceived(format(argument))}`
75+
)
76+
}
77+
: () => {
78+
const diffString = diff(expected, actual, {
79+
expand: this.expand,
80+
})
81+
82+
return (
83+
this.utils.matcherHint('toIncludeCss', undefined, undefined, options) +
84+
'\n\n' +
85+
(diffString && diffString.includes('- Expect')
86+
? `Difference:\n\n${diffString}`
87+
: `Expected: ${this.utils.printExpected(expected)}\n` +
88+
`Received: ${this.utils.printReceived(actual)}`)
89+
)
90+
}
91+
92+
return { actual: received, message, pass }
93+
},
94+
})
95+
96+
expect.extend({
97+
// Compare two CSS strings with all whitespace removed
98+
// This is probably naive but it's fast and works well enough.
99+
toMatchFormattedCss(received, argument) {
100+
function format(input) {
101+
return prettier.format(input.replace(/\n/g, ''), {
102+
parser: 'css',
103+
printWidth: 100,
104+
})
105+
}
106+
const options = {
107+
comment: 'stripped(received) === stripped(argument)',
108+
isNot: this.isNot,
109+
promise: this.promise,
110+
}
111+
112+
let formattedReceived = format(received)
113+
let formattedArgument = format(argument)
114+
115+
const pass = formattedReceived === formattedArgument
116+
117+
const message = pass
118+
? () => {
119+
return (
120+
this.utils.matcherHint('toMatchCss', undefined, undefined, options) +
121+
'\n\n' +
122+
`Expected: not ${this.utils.printExpected(formattedReceived)}\n` +
123+
`Received: ${this.utils.printReceived(formattedArgument)}`
124+
)
125+
}
126+
: () => {
127+
const actual = formattedReceived
128+
const expected = formattedArgument
129+
130+
const diffString = diff(expected, actual, {
131+
expand: this.expand,
132+
})
133+
134+
return (
135+
this.utils.matcherHint('toMatchCss', undefined, undefined, options) +
136+
'\n\n' +
137+
(diffString && diffString.includes('- Expect')
138+
? `Difference:\n\n${diffString}`
139+
: `Expected: ${this.utils.printExpected(expected)}\n` +
140+
`Received: ${this.utils.printReceived(actual)}`)
141+
)
142+
}
143+
144+
return { actual: received, message, pass }
145+
},
146+
})

0 commit comments

Comments
 (0)