Merge branch 'master' into patch-2

This commit is contained in:
Adam Wathan 2020-08-06 16:08:23 -04:00 • committed by GitHub
commit 6070ba72dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
93 changed files with 223000 additions and 2387 deletions

View file

@ -9,7 +9,8 @@
"extends": ["eslint-config-postcss", "prettier"],
"plugins": ["prettier"],
"rules": {
"no-unused-vars": [2, {"args": "all", "argsIgnorePattern": "^_"}],
"no-unused-vars": [2, { "args": "all", "argsIgnorePattern": "^_" }],
"no-warning-comments": 0,
"prettier/prettier": [
"error",
{

View file

@ -19,7 +19,7 @@ npm run style
To automatically fix any style violations in your code, you can run:
```sh
npm run style --fix
npm run style -- --fix
```
## Running tests
@ -43,19 +43,19 @@ npm run prepare
This will create new CSS files in the `/dist` folder which you can reference in your own test HTML file. We often test our own changes by creating an `index.html` file in the root of the Tailwind project itself that pulls in the `/dist/tailwind.css` stylesheet:
```html
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/dist/tailwind.css">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link rel="stylesheet" href="/dist/tailwind.css" />
<title>Hello, world!</title>
</head>
<body>
<h1 class="text-2xl font-bold text-center">Hello world!</h1>
</body>
</html>
````
```
You can then use a tool like [live-server](https://www.npmjs.com/package/live-server) to preview it in the browser:

View file

@ -2,10 +2,12 @@
👋 Hey, thanks for taking an interest in Tailwind!
Please only open an issue here if you have a bug to report or a feature proposal you'd like to discuss.
We don't accept issues that don't use our issue template.
If you need help, have questions about best practices, or want to start a discussion about anything else related to Tailwind, open an issue on the `tailwindcss/discuss` repo instead:
Please use this link to create your issue:
https://github.com/tailwindcss/discuss/issues
https://github.com/tailwindcss/tailwindcss/issues/new/choose
Issues that don't follow our issue template will be closed, as it's just impossible to triage and resolve issues without some sort of system with the small team we have.
-->

25
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View file

@ -0,0 +1,25 @@
---
name: 'Bug Report'
about: 'Report a reproducible bug in the framework.'
title: ''
labels: ''
assignees: ''
---
### Describe the problem:
<!--
Explain the behavior you're seeing that you think is a bug, and explain how you
think things should behave instead.
-->
### Link to a minimal reproduction:
<!--
Please provide a link to a GitHub repository that reliably reproduces the issue
with the least amount of extraneous code possible.
-->

11
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Feature Request
url: https://github.com/tailwindcss/tailwindcss/discussions/new?category_id=65335
about: 'Suggest any ideas you have using our discussion forums.'
- name: Help
url: https://github.com/tailwindcss/tailwindcss/discussions/new?category_id=65334
about: 'If you have a question or need help, ask a question on the discussion forums.'
- name: Documentation Issue
url: https://github.com/tailwindcss/docs
about: 'For documentation issues, suggest changes on our documentation repository.'

View file

@ -5,27 +5,26 @@ name: Node.js CI
on:
push:
branches: [ master ]
branches: [master]
pull_request:
branches: [ master ]
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x]
node-version: [8.x, 10.x, 12.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run prepare
- run: npm test
env:
CI: true
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run prepare
- run: npm test
env:
CI: true

View file

@ -1,5 +0,0 @@
/__tests__/
/jest/
/src/
index.html
yarn-error.log

View file

@ -1,9 +1,11 @@
<p align="center">
<a href="https://tailwindcss.com/" target="_blank"><img width="200" src="https://tailwindcss.com/img/tailwind.svg"></a><br>
<p>
<a href="https://tailwindcss.com/" target="_blank">
<img alt="Tailwind CSS" width="350" src="https://refactoringui.nyc3.cdn.digitaloceanspaces.com/tailwind-logo.svg">
</a><br>
A utility-first CSS framework for rapidly building custom user interfaces.
</p>
<p align="center">
<p>
<a href="https://travis-ci.org/tailwindcss/tailwindcss"><img src="https://img.shields.io/travis/tailwindcss/tailwindcss/master.svg" alt="Build Status"></a>
<a href="https://www.npmjs.com/package/tailwindcss"><img src="https://img.shields.io/npm/dt/tailwindcss.svg" alt="Total Downloads"></a>
<a href="https://github.com/tailwindcss/tailwindcss/releases"><img src="https://img.shields.io/npm/v/tailwindcss.svg" alt="Latest Release"></a>
@ -20,7 +22,7 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com/).
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/discuss/issues?q=is%3Aissue+sort%3Aupdated-desc)
[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions)
For casual chit-chat with others using the framework:

View file

@ -43,12 +43,20 @@ describe('cli', () => {
})
describe('build', () => {
it('compiles CSS file', () => {
it('compiles CSS file using an input css file', () => {
return cli(['build', inputCssPath]).then(() => {
expect(process.stdout.write.mock.calls[0][0]).toContain('.example')
})
})
it('compiles CSS file without an input css file', () => {
return cli(['build']).then(() => {
expect(process.stdout.write.mock.calls[0][0]).toContain('normalize.css') // base
expect(process.stdout.write.mock.calls[0][0]).toContain('.container') // components
expect(process.stdout.write.mock.calls[0][0]).toContain('.mx-auto') // utilities
})
})
it('compiles CSS file using custom configuration', () => {
return cli(['build', inputCssPath, '--config', customConfigPath]).then(() => {
expect(process.stdout.write.mock.calls[0][0]).toContain('400px')

View file

@ -21,27 +21,31 @@ function config(overrides) {
})
}
test.only('options are not required', () => {
test('options are not required', () => {
const { components } = processPlugins([container()], config())
expect(css(components)).toMatchCss(`
.container { width: 100% }
@media (min-width: 576px) {
.container { max-width: 576px }
}
@media (min-width: 768px) {
.container { max-width: 768px }
}
@media (min-width: 992px) {
.container { max-width: 992px }
}
@media (min-width: 1200px) {
.container { max-width: 1200px }
@layer components {
@variants {
.container { width: 100% }
@media (min-width: 576px) {
.container { max-width: 576px }
}
@media (min-width: 768px) {
.container { max-width: 768px }
}
@media (min-width: 992px) {
.container { max-width: 992px }
}
@media (min-width: 1200px) {
.container { max-width: 1200px }
}
}
}
`)
})
test.only('screens can be passed explicitly', () => {
test('screens can be passed explicitly', () => {
const { components } = processPlugins(
[container()],
config({
@ -54,17 +58,21 @@ test.only('screens can be passed explicitly', () => {
)
expect(css(components)).toMatchCss(`
.container { width: 100% }
@media (min-width: 400px) {
.container { max-width: 400px }
}
@media (min-width: 500px) {
.container { max-width: 500px }
@layer components {
@variants {
.container { width: 100% }
@media (min-width: 400px) {
.container { max-width: 400px }
}
@media (min-width: 500px) {
.container { max-width: 500px }
}
}
}
`)
})
test.only('screens are ordered ascending by min-width', () => {
test('screens are ordered ascending by min-width', () => {
const { components } = processPlugins(
[container()],
config({
@ -77,17 +85,21 @@ test.only('screens are ordered ascending by min-width', () => {
)
expect(css(components)).toMatchCss(`
.container { width: 100% }
@media (min-width: 400px) {
.container { max-width: 400px }
}
@media (min-width: 500px) {
.container { max-width: 500px }
@layer components {
@variants {
.container { width: 100% }
@media (min-width: 400px) {
.container { max-width: 400px }
}
@media (min-width: 500px) {
.container { max-width: 500px }
}
}
}
`)
})
test.only('screens are deduplicated by min-width', () => {
test('screens are deduplicated by min-width', () => {
const { components } = processPlugins(
[container()],
config({
@ -104,17 +116,21 @@ test.only('screens are deduplicated by min-width', () => {
)
expect(css(components)).toMatchCss(`
.container { width: 100% }
@media (min-width: 576px) {
.container { max-width: 576px }
}
@media (min-width: 768px) {
.container { max-width: 768px }
@layer components {
@variants {
.container { width: 100% }
@media (min-width: 576px) {
.container { max-width: 576px }
}
@media (min-width: 768px) {
.container { max-width: 768px }
}
}
}
`)
})
test.only('the container can be centered by default', () => {
test('the container can be centered by default', () => {
const { components } = processPlugins(
[container()],
config({
@ -127,27 +143,31 @@ test.only('the container can be centered by default', () => {
)
expect(css(components)).toMatchCss(`
.container {
width: 100%;
margin-right: auto;
margin-left: auto
}
@media (min-width: 576px) {
.container { max-width: 576px }
}
@media (min-width: 768px) {
.container { max-width: 768px }
}
@media (min-width: 992px) {
.container { max-width: 992px }
}
@media (min-width: 1200px) {
.container { max-width: 1200px }
@layer components {
@variants {
.container {
width: 100%;
margin-right: auto;
margin-left: auto
}
@media (min-width: 576px) {
.container { max-width: 576px }
}
@media (min-width: 768px) {
.container { max-width: 768px }
}
@media (min-width: 992px) {
.container { max-width: 992px }
}
@media (min-width: 1200px) {
.container { max-width: 1200px }
}
}
}
`)
})
test.only('horizontal padding can be included by default', () => {
test('horizontal padding can be included by default', () => {
const { components } = processPlugins(
[container()],
config({
@ -160,27 +180,85 @@ test.only('horizontal padding can be included by default', () => {
)
expect(css(components)).toMatchCss(`
.container {
width: 100%;
padding-right: 2rem;
padding-left: 2rem
}
@media (min-width: 576px) {
.container { max-width: 576px }
}
@media (min-width: 768px) {
.container { max-width: 768px }
}
@media (min-width: 992px) {
.container { max-width: 992px }
}
@media (min-width: 1200px) {
.container { max-width: 1200px }
@layer components {
@variants {
.container {
width: 100%;
padding-right: 2rem;
padding-left: 2rem
}
@media (min-width: 576px) {
.container { max-width: 576px }
}
@media (min-width: 768px) {
.container { max-width: 768px }
}
@media (min-width: 992px) {
.container { max-width: 992px }
}
@media (min-width: 1200px) {
.container { max-width: 1200px }
}
}
}
`)
})
test.only('setting all options at once', () => {
test('responsive horizontal padding can be included by default', () => {
const { components } = processPlugins(
[container()],
config({
theme: {
container: {
padding: {
default: '1rem',
sm: '2rem',
lg: '4rem',
xl: '5rem',
},
},
},
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.container {
width: 100%;
padding-right: 1rem;
padding-left: 1rem
}
@media (min-width: 576px) {
.container {
max-width: 576px;
padding-right: 2rem;
padding-left: 2rem
}
}
@media (min-width: 768px) {
.container { max-width: 768px }
}
@media (min-width: 992px) {
.container {
max-width: 992px;
padding-right: 4rem;
padding-left: 4rem
}
}
@media (min-width: 1200px) {
.container {
max-width: 1200px;
padding-right: 5rem;
padding-left: 5rem
}
}
}
}
`)
})
test('setting all options at once', () => {
const { components } = processPlugins(
[container()],
config({
@ -195,18 +273,58 @@ test.only('setting all options at once', () => {
)
expect(css(components)).toMatchCss(`
.container {
width: 100%;
margin-right: auto;
margin-left: auto;
padding-right: 2rem;
padding-left: 2rem
}
@media (min-width: 400px) {
.container { max-width: 400px }
}
@media (min-width: 500px) {
.container { max-width: 500px }
@layer components {
@variants {
.container {
width: 100%;
margin-right: auto;
margin-left: auto;
padding-right: 2rem;
padding-left: 2rem
}
@media (min-width: 400px) {
.container { max-width: 400px }
}
@media (min-width: 500px) {
.container { max-width: 500px }
}
}
}
`)
})
test('container can use variants', () => {
const { components } = processPlugins(
[container()],
config({
theme: {
container: {
screens: ['400px', '500px'],
},
},
variants: {
container: ['responsive', 'hover'],
},
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants responsive, hover {
.container {
width: 100%
}
@media (min-width: 400px) {
.container {
max-width: 400px
}
}
@media (min-width: 500px) {
.container {
max-width: 500px
}
}
}
}
`)
})

View file

@ -0,0 +1,39 @@
<!-- Basic HTML -->
<html>
<head>
<title>My Page</title>
</head>
<body>
<main>
<div class="bg-red-500 md:bg-blue-300 w-1/2"></div>
</main>
</body>
</html>
<!-- Vue dynamic classes -->
<span :class="{ block: enabled, 'md:flow-root': !enabled }"></span>
<!-- JSX with template strings -->
<script>
function Component() {
return <div class={`h-screen`}></div>
}
</script>
<!-- Custom classes with really weird characters -->
<div class="min-h-(screen-4) bg-black! font-%#$@ w-(1/2+8)"></div>
<!-- Pug -->
span.inline-grid.grid-cols-3(class="px-1.5")
.col-span-2
Hello
.col-span-1.text-center
World!
<!-- Haml -->
.flow-root
.text-green-700.bg-green-100
.text-left= content
%samp.font-mono{:data-foo => "bar"}= output
.col-span-4[aria-hidden=true]
.tracking-tight#headline

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,45 @@
import flattenColorPalette from '../src/util/flattenColorPalette'
test('it flattens nested color objects', () => {
expect(
flattenColorPalette({
purple: 'purple',
white: {
25: 'rgba(255,255,255,.25)',
50: 'rgba(255,255,255,.5)',
75: 'rgba(255,255,255,.75)',
default: '#fff',
},
red: {
1: 'rgb(33,0,0)',
2: 'rgb(67,0,0)',
3: 'rgb(100,0,0)',
},
green: {
1: 'rgb(0,33,0)',
2: 'rgb(0,67,0)',
3: 'rgb(0,100,0)',
},
blue: {
1: 'rgb(0,0,33)',
2: 'rgb(0,0,67)',
3: 'rgb(0,0,100)',
},
})
).toEqual({
purple: 'purple',
'white-25': 'rgba(255,255,255,.25)',
'white-50': 'rgba(255,255,255,.5)',
'white-75': 'rgba(255,255,255,.75)',
white: '#fff',
'red-1': 'rgb(33,0,0)',
'red-2': 'rgb(67,0,0)',
'red-3': 'rgb(100,0,0)',
'green-1': 'rgb(0,33,0)',
'green-2': 'rgb(0,67,0)',
'green-3': 'rgb(0,100,0)',
'blue-1': 'rgb(0,0,33)',
'blue-2': 'rgb(0,0,67)',
'blue-3': 'rgb(0,0,100)',
})
})

View file

@ -1,83 +0,0 @@
import _ from 'lodash'
import escapeClassName from '../../src/util/escapeClassName'
import plugin from '../../src/plugins/backgroundColor'
test('colors can be a nested object', () => {
const addedUtilities = []
const config = {
theme: {
backgroundColor: {
purple: 'purple',
white: {
25: 'rgba(255,255,255,.25)',
50: 'rgba(255,255,255,.5)',
75: 'rgba(255,255,255,.75)',
default: '#fff',
},
red: {
1: 'rgb(33,0,0)',
2: 'rgb(67,0,0)',
3: 'rgb(100,0,0)',
},
green: {
1: 'rgb(0,33,0)',
2: 'rgb(0,67,0)',
3: 'rgb(0,100,0)',
},
blue: {
1: 'rgb(0,0,33)',
2: 'rgb(0,0,67)',
3: 'rgb(0,0,100)',
},
},
},
variants: {
backgroundColor: ['responsive'],
},
}
const getConfigValue = (path, defaultValue) => _.get(config, path, defaultValue)
const pluginApi = {
config: getConfigValue,
e: escapeClassName,
theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue),
variants: (path, defaultValue) => {
if (_.isArray(config.variants)) {
return config.variants
}
return getConfigValue(`variants.${path}`, defaultValue)
},
addUtilities(utilities, variants) {
addedUtilities.push({
utilities,
variants,
})
},
}
plugin()(pluginApi)
expect(addedUtilities).toEqual([
{
utilities: {
'.bg-purple': { 'background-color': 'purple' },
'.bg-white-25': { 'background-color': 'rgba(255,255,255,.25)' },
'.bg-white-50': { 'background-color': 'rgba(255,255,255,.5)' },
'.bg-white-75': { 'background-color': 'rgba(255,255,255,.75)' },
'.bg-white': { 'background-color': '#fff' },
'.bg-red-1': { 'background-color': 'rgb(33,0,0)' },
'.bg-red-2': { 'background-color': 'rgb(67,0,0)' },
'.bg-red-3': { 'background-color': 'rgb(100,0,0)' },
'.bg-green-1': { 'background-color': 'rgb(0,33,0)' },
'.bg-green-2': { 'background-color': 'rgb(0,67,0)' },
'.bg-green-3': { 'background-color': 'rgb(0,100,0)' },
'.bg-blue-1': { 'background-color': 'rgb(0,0,33)' },
'.bg-blue-2': { 'background-color': 'rgb(0,0,67)' },
'.bg-blue-3': { 'background-color': 'rgb(0,0,100)' },
},
variants: ['responsive'],
},
])
})

View file

@ -1,83 +0,0 @@
import _ from 'lodash'
import escapeClassName from '../../src/util/escapeClassName'
import plugin from '../../src/plugins/borderColor'
test('colors can be a nested object', () => {
const addedUtilities = []
const config = {
theme: {
borderColor: {
purple: 'purple',
white: {
25: 'rgba(255,255,255,.25)',
50: 'rgba(255,255,255,.5)',
75: 'rgba(255,255,255,.75)',
default: '#fff',
},
red: {
1: 'rgb(33,0,0)',
2: 'rgb(67,0,0)',
3: 'rgb(100,0,0)',
},
green: {
1: 'rgb(0,33,0)',
2: 'rgb(0,67,0)',
3: 'rgb(0,100,0)',
},
blue: {
1: 'rgb(0,0,33)',
2: 'rgb(0,0,67)',
3: 'rgb(0,0,100)',
},
},
},
variants: {
borderColor: ['responsive'],
},
}
const getConfigValue = (path, defaultValue) => _.get(config, path, defaultValue)
const pluginApi = {
config: getConfigValue,
e: escapeClassName,
theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue),
variants: (path, defaultValue) => {
if (_.isArray(config.variants)) {
return config.variants
}
return getConfigValue(`variants.${path}`, defaultValue)
},
addUtilities(utilities, variants) {
addedUtilities.push({
utilities,
variants,
})
},
}
plugin()(pluginApi)
expect(addedUtilities).toEqual([
{
utilities: {
'.border-purple': { 'border-color': 'purple' },
'.border-white-25': { 'border-color': 'rgba(255,255,255,.25)' },
'.border-white-50': { 'border-color': 'rgba(255,255,255,.5)' },
'.border-white-75': { 'border-color': 'rgba(255,255,255,.75)' },
'.border-white': { 'border-color': '#fff' },
'.border-red-1': { 'border-color': 'rgb(33,0,0)' },
'.border-red-2': { 'border-color': 'rgb(67,0,0)' },
'.border-red-3': { 'border-color': 'rgb(100,0,0)' },
'.border-green-1': { 'border-color': 'rgb(0,33,0)' },
'.border-green-2': { 'border-color': 'rgb(0,67,0)' },
'.border-green-3': { 'border-color': 'rgb(0,100,0)' },
'.border-blue-1': { 'border-color': 'rgb(0,0,33)' },
'.border-blue-2': { 'border-color': 'rgb(0,0,67)' },
'.border-blue-3': { 'border-color': 'rgb(0,0,100)' },
},
variants: ['responsive'],
},
])
})

View file

@ -0,0 +1,75 @@
import invokePlugin from '../util/invokePlugin'
import plugin from '../../src/plugins/divideWidth'
test('generating divide width utilities', () => {
const config = {
target: 'relaxed',
theme: {
divideWidth: {
default: '1px',
'0': '0',
'2': '2px',
'4': '4px',
},
},
variants: {
divideWidth: ['responsive'],
},
}
const { utilities } = invokePlugin(plugin(), config)
expect(utilities).toEqual([
[
{
'.divide-y > :not(template) ~ :not(template)': {
'--divide-y-reverse': '0',
'border-top-width': 'calc(1px * calc(1 - var(--divide-y-reverse)))',
'border-bottom-width': 'calc(1px * var(--divide-y-reverse))',
},
'.divide-x > :not(template) ~ :not(template)': {
'--divide-x-reverse': '0',
'border-right-width': 'calc(1px * var(--divide-x-reverse))',
'border-left-width': 'calc(1px * calc(1 - var(--divide-x-reverse)))',
},
'.divide-y-0 > :not(template) ~ :not(template)': {
'--divide-y-reverse': '0',
'border-top-width': 'calc(0px * calc(1 - var(--divide-y-reverse)))',
'border-bottom-width': 'calc(0px * var(--divide-y-reverse))',
},
'.divide-x-0 > :not(template) ~ :not(template)': {
'--divide-x-reverse': '0',
'border-right-width': 'calc(0px * var(--divide-x-reverse))',
'border-left-width': 'calc(0px * calc(1 - var(--divide-x-reverse)))',
},
'.divide-y-2 > :not(template) ~ :not(template)': {
'--divide-y-reverse': '0',
'border-top-width': 'calc(2px * calc(1 - var(--divide-y-reverse)))',
'border-bottom-width': 'calc(2px * var(--divide-y-reverse))',
},
'.divide-x-2 > :not(template) ~ :not(template)': {
'--divide-x-reverse': '0',
'border-right-width': 'calc(2px * var(--divide-x-reverse))',
'border-left-width': 'calc(2px * calc(1 - var(--divide-x-reverse)))',
},
'.divide-y-4 > :not(template) ~ :not(template)': {
'--divide-y-reverse': '0',
'border-top-width': 'calc(4px * calc(1 - var(--divide-y-reverse)))',
'border-bottom-width': 'calc(4px * var(--divide-y-reverse))',
},
'.divide-x-4 > :not(template) ~ :not(template)': {
'--divide-x-reverse': '0',
'border-right-width': 'calc(4px * var(--divide-x-reverse))',
'border-left-width': 'calc(4px * calc(1 - var(--divide-x-reverse)))',
},
'.divide-y-reverse > :not(template) ~ :not(template)': {
'--divide-y-reverse': '1',
},
'.divide-x-reverse > :not(template) ~ :not(template)': {
'--divide-x-reverse': '1',
},
},
['responsive'],
],
])
})

View file

@ -1,83 +0,0 @@
import _ from 'lodash'
import escapeClassName from '../../src/util/escapeClassName'
import plugin from '../../src/plugins/fill'
test('colors can be a nested object', () => {
const addedUtilities = []
const config = {
theme: {
fill: {
purple: 'purple',
white: {
25: 'rgba(255,255,255,.25)',
50: 'rgba(255,255,255,.5)',
75: 'rgba(255,255,255,.75)',
default: '#fff',
},
red: {
1: 'rgb(33,0,0)',
2: 'rgb(67,0,0)',
3: 'rgb(100,0,0)',
},
green: {
1: 'rgb(0,33,0)',
2: 'rgb(0,67,0)',
3: 'rgb(0,100,0)',
},
blue: {
1: 'rgb(0,0,33)',
2: 'rgb(0,0,67)',
3: 'rgb(0,0,100)',
},
},
},
variants: {
fill: ['responsive'],
},
}
const getConfigValue = (path, defaultValue) => _.get(config, path, defaultValue)
const pluginApi = {
config: getConfigValue,
e: escapeClassName,
theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue),
variants: (path, defaultValue) => {
if (_.isArray(config.variants)) {
return config.variants
}
return getConfigValue(`variants.${path}`, defaultValue)
},
addUtilities(utilities, variants) {
addedUtilities.push({
utilities,
variants,
})
},
}
plugin()(pluginApi)
expect(addedUtilities).toEqual([
{
utilities: {
'.fill-purple': { fill: 'purple' },
'.fill-white-25': { fill: 'rgba(255,255,255,.25)' },
'.fill-white-50': { fill: 'rgba(255,255,255,.5)' },
'.fill-white-75': { fill: 'rgba(255,255,255,.75)' },
'.fill-white': { fill: '#fff' },
'.fill-red-1': { fill: 'rgb(33,0,0)' },
'.fill-red-2': { fill: 'rgb(67,0,0)' },
'.fill-red-3': { fill: 'rgb(100,0,0)' },
'.fill-green-1': { fill: 'rgb(0,33,0)' },
'.fill-green-2': { fill: 'rgb(0,67,0)' },
'.fill-green-3': { fill: 'rgb(0,100,0)' },
'.fill-blue-1': { fill: 'rgb(0,0,33)' },
'.fill-blue-2': { fill: 'rgb(0,0,67)' },
'.fill-blue-3': { fill: 'rgb(0,0,100)' },
},
variants: ['responsive'],
},
])
})

View file

@ -0,0 +1,86 @@
import invokePlugin from '../util/invokePlugin'
import plugin from '../../src/plugins/fontSize'
test('font-size utilities can include a default line-height', () => {
const config = {
theme: {
fontSize: {
sm: '12px',
md: ['16px', '24px'],
lg: ['20px', '28px'],
},
},
variants: {
fontSize: ['responsive'],
},
}
const { utilities } = invokePlugin(plugin(), config)
expect(utilities).toEqual([
[
{
'.text-sm': { 'font-size': '12px' },
'.text-md': { 'font-size': '16px', 'line-height': '24px' },
'.text-lg': { 'font-size': '20px', 'line-height': '28px' },
},
['responsive'],
],
])
})
test('font-size utilities can include a default letter-spacing', () => {
const config = {
theme: {
fontSize: {
sm: '12px',
md: ['16px', { letterSpacing: '-0.01em' }],
lg: ['20px', { letterSpacing: '-0.02em' }],
},
},
variants: {
fontSize: ['responsive'],
},
}
const { utilities } = invokePlugin(plugin(), config)
expect(utilities).toEqual([
[
{
'.text-sm': { 'font-size': '12px' },
'.text-md': { 'font-size': '16px', 'letter-spacing': '-0.01em' },
'.text-lg': { 'font-size': '20px', 'letter-spacing': '-0.02em' },
},
['responsive'],
],
])
})
test('font-size utilities can include a default line-height and letter-spacing', () => {
const config = {
theme: {
fontSize: {
sm: '12px',
md: ['16px', { lineHeight: '24px', letterSpacing: '-0.01em' }],
lg: ['20px', { lineHeight: '28px', letterSpacing: '-0.02em' }],
},
},
variants: {
fontSize: ['responsive'],
},
}
const { utilities } = invokePlugin(plugin(), config)
expect(utilities).toEqual([
[
{
'.text-sm': { 'font-size': '12px' },
'.text-md': { 'font-size': '16px', 'line-height': '24px', 'letter-spacing': '-0.01em' },
'.text-lg': { 'font-size': '20px', 'line-height': '28px', 'letter-spacing': '-0.02em' },
},
['responsive'],
],
])
})

View file

@ -0,0 +1,97 @@
import invokePlugin from '../util/invokePlugin'
import plugin from '../../src/plugins/space'
test('generating space utilities', () => {
const config = {
target: 'relaxed',
theme: {
space: {
'0': '0',
'1': '1px',
'2': '2px',
'4': '4px',
'-2': '-2px',
'-1': '-1px',
},
},
variants: {
space: ['responsive'],
},
}
const { utilities } = invokePlugin(plugin(), config)
expect(utilities).toEqual([
[
{
'.space-y-0 > :not(template) ~ :not(template)': {
'--space-y-reverse': '0',
'margin-top': 'calc(0px * calc(1 - var(--space-y-reverse)))',
'margin-bottom': 'calc(0px * var(--space-y-reverse))',
},
'.space-x-0 > :not(template) ~ :not(template)': {
'--space-x-reverse': '0',
'margin-right': 'calc(0px * var(--space-x-reverse))',
'margin-left': 'calc(0px * calc(1 - var(--space-x-reverse)))',
},
'.space-y-1 > :not(template) ~ :not(template)': {
'--space-y-reverse': '0',
'margin-top': 'calc(1px * calc(1 - var(--space-y-reverse)))',
'margin-bottom': 'calc(1px * var(--space-y-reverse))',
},
'.space-x-1 > :not(template) ~ :not(template)': {
'--space-x-reverse': '0',
'margin-right': 'calc(1px * var(--space-x-reverse))',
'margin-left': 'calc(1px * calc(1 - var(--space-x-reverse)))',
},
'.space-y-2 > :not(template) ~ :not(template)': {
'--space-y-reverse': '0',
'margin-top': 'calc(2px * calc(1 - var(--space-y-reverse)))',
'margin-bottom': 'calc(2px * var(--space-y-reverse))',
},
'.space-x-2 > :not(template) ~ :not(template)': {
'--space-x-reverse': '0',
'margin-right': 'calc(2px * var(--space-x-reverse))',
'margin-left': 'calc(2px * calc(1 - var(--space-x-reverse)))',
},
'.space-y-4 > :not(template) ~ :not(template)': {
'--space-y-reverse': '0',
'margin-top': 'calc(4px * calc(1 - var(--space-y-reverse)))',
'margin-bottom': 'calc(4px * var(--space-y-reverse))',
},
'.space-x-4 > :not(template) ~ :not(template)': {
'--space-x-reverse': '0',
'margin-right': 'calc(4px * var(--space-x-reverse))',
'margin-left': 'calc(4px * calc(1 - var(--space-x-reverse)))',
},
'.-space-y-2 > :not(template) ~ :not(template)': {
'--space-y-reverse': '0',
'margin-top': 'calc(-2px * calc(1 - var(--space-y-reverse)))',
'margin-bottom': 'calc(-2px * var(--space-y-reverse))',
},
'.-space-x-2 > :not(template) ~ :not(template)': {
'--space-x-reverse': '0',
'margin-right': 'calc(-2px * var(--space-x-reverse))',
'margin-left': 'calc(-2px * calc(1 - var(--space-x-reverse)))',
},
'.-space-y-1 > :not(template) ~ :not(template)': {
'--space-y-reverse': '0',
'margin-top': 'calc(-1px * calc(1 - var(--space-y-reverse)))',
'margin-bottom': 'calc(-1px * var(--space-y-reverse))',
},
'.-space-x-1 > :not(template) ~ :not(template)': {
'--space-x-reverse': '0',
'margin-right': 'calc(-1px * var(--space-x-reverse))',
'margin-left': 'calc(-1px * calc(1 - var(--space-x-reverse)))',
},
'.space-y-reverse > :not(template) ~ :not(template)': {
'--space-y-reverse': '1',
},
'.space-x-reverse > :not(template) ~ :not(template)': {
'--space-x-reverse': '1',
},
},
['responsive'],
],
])
})

View file

@ -1,83 +0,0 @@
import _ from 'lodash'
import escapeClassName from '../../src/util/escapeClassName'
import plugin from '../../src/plugins/stroke'
test('colors can be a nested object', () => {
const addedUtilities = []
const config = {
theme: {
stroke: {
purple: 'purple',
white: {
25: 'rgba(255,255,255,.25)',
50: 'rgba(255,255,255,.5)',
75: 'rgba(255,255,255,.75)',
default: '#fff',
},
red: {
1: 'rgb(33,0,0)',
2: 'rgb(67,0,0)',
3: 'rgb(100,0,0)',
},
green: {
1: 'rgb(0,33,0)',
2: 'rgb(0,67,0)',
3: 'rgb(0,100,0)',
},
blue: {
1: 'rgb(0,0,33)',
2: 'rgb(0,0,67)',
3: 'rgb(0,0,100)',
},
},
},
variants: {
stroke: ['responsive'],
},
}
const getConfigValue = (path, defaultValue) => _.get(config, path, defaultValue)
const pluginApi = {
config: getConfigValue,
e: escapeClassName,
theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue),
variants: (path, defaultValue) => {
if (_.isArray(config.variants)) {
return config.variants
}
return getConfigValue(`variants.${path}`, defaultValue)
},
addUtilities(utilities, variants) {
addedUtilities.push({
utilities,
variants,
})
},
}
plugin()(pluginApi)
expect(addedUtilities).toEqual([
{
utilities: {
'.stroke-purple': { stroke: 'purple' },
'.stroke-white-25': { stroke: 'rgba(255,255,255,.25)' },
'.stroke-white-50': { stroke: 'rgba(255,255,255,.5)' },
'.stroke-white-75': { stroke: 'rgba(255,255,255,.75)' },
'.stroke-white': { stroke: '#fff' },
'.stroke-red-1': { stroke: 'rgb(33,0,0)' },
'.stroke-red-2': { stroke: 'rgb(67,0,0)' },
'.stroke-red-3': { stroke: 'rgb(100,0,0)' },
'.stroke-green-1': { stroke: 'rgb(0,33,0)' },
'.stroke-green-2': { stroke: 'rgb(0,67,0)' },
'.stroke-green-3': { stroke: 'rgb(0,100,0)' },
'.stroke-blue-1': { stroke: 'rgb(0,0,33)' },
'.stroke-blue-2': { stroke: 'rgb(0,0,67)' },
'.stroke-blue-3': { stroke: 'rgb(0,0,100)' },
},
variants: ['responsive'],
},
])
})

View file

@ -1,83 +0,0 @@
import _ from 'lodash'
import escapeClassName from '../../src/util/escapeClassName'
import plugin from '../../src/plugins/textColor'
test('colors can be a nested object', () => {
const addedUtilities = []
const config = {
theme: {
textColor: {
purple: 'purple',
white: {
25: 'rgba(255,255,255,.25)',
50: 'rgba(255,255,255,.5)',
75: 'rgba(255,255,255,.75)',
default: '#fff',
},
red: {
1: 'rgb(33,0,0)',
2: 'rgb(67,0,0)',
3: 'rgb(100,0,0)',
},
green: {
1: 'rgb(0,33,0)',
2: 'rgb(0,67,0)',
3: 'rgb(0,100,0)',
},
blue: {
1: 'rgb(0,0,33)',
2: 'rgb(0,0,67)',
3: 'rgb(0,0,100)',
},
},
},
variants: {
textColor: ['responsive'],
},
}
const getConfigValue = (path, defaultValue) => _.get(config, path, defaultValue)
const pluginApi = {
config: getConfigValue,
e: escapeClassName,
theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue),
variants: (path, defaultValue) => {
if (_.isArray(config.variants)) {
return config.variants
}
return getConfigValue(`variants.${path}`, defaultValue)
},
addUtilities(utilities, variants) {
addedUtilities.push({
utilities,
variants,
})
},
}
plugin()(pluginApi)
expect(addedUtilities).toEqual([
{
utilities: {
'.text-purple': { color: 'purple' },
'.text-white-25': { color: 'rgba(255,255,255,.25)' },
'.text-white-50': { color: 'rgba(255,255,255,.5)' },
'.text-white-75': { color: 'rgba(255,255,255,.75)' },
'.text-white': { color: '#fff' },
'.text-red-1': { color: 'rgb(33,0,0)' },
'.text-red-2': { color: 'rgb(67,0,0)' },
'.text-red-3': { color: 'rgb(100,0,0)' },
'.text-green-1': { color: 'rgb(0,33,0)' },
'.text-green-2': { color: 'rgb(0,67,0)' },
'.text-green-3': { color: 'rgb(0,100,0)' },
'.text-blue-1': { color: 'rgb(0,0,33)' },
'.text-blue-2': { color: 'rgb(0,0,67)' },
'.text-blue-3': { color: 'rgb(0,0,100)' },
},
variants: ['responsive'],
},
])
})

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,319 @@
import fs from 'fs'
import path from 'path'
import postcss from 'postcss'
import tailwind from '../src/index'
import defaultConfig from '../stubs/defaultConfig.stub.js'
const config = {
...defaultConfig,
theme: {
extend: {
colors: {
'black!': '#000',
},
spacing: {
'1.5': '0.375rem',
'(1/2+8)': 'calc(50% + 2rem)',
},
minHeight: {
'(screen-4)': 'calc(100vh - 1rem)',
},
fontFamily: {
'%#$@': 'Comic Sans',
},
},
},
}
function assertPurged(result) {
expect(result.css).not.toContain('.bg-red-600')
expect(result.css).not.toContain('.w-1\\/3')
expect(result.css).not.toContain('.flex')
expect(result.css).not.toContain('.font-sans')
expect(result.css).not.toContain('.text-right')
expect(result.css).not.toContain('.px-4')
expect(result.css).not.toContain('.h-full')
expect(result.css).toContain('.bg-red-500')
expect(result.css).toContain('.md\\:bg-blue-300')
expect(result.css).toContain('.w-1\\/2')
expect(result.css).toContain('.block')
expect(result.css).toContain('.md\\:flow-root')
expect(result.css).toContain('.h-screen')
expect(result.css).toContain('.min-h-\\(screen-4\\)')
expect(result.css).toContain('.bg-black\\!')
expect(result.css).toContain('.font-\\%\\#\\$\\@')
expect(result.css).toContain('.w-\\(1\\/2\\+8\\)')
expect(result.css).toContain('.inline-grid')
expect(result.css).toContain('.grid-cols-3')
expect(result.css).toContain('.px-1\\.5')
expect(result.css).toContain('.col-span-2')
expect(result.css).toContain('.col-span-1')
expect(result.css).toContain('.text-center')
expect(result.css).toContain('.flow-root')
expect(result.css).toContain('.text-green-700')
expect(result.css).toContain('.bg-green-100')
expect(result.css).toContain('.text-left')
expect(result.css).toContain('.font-mono')
expect(result.css).toContain('.col-span-4')
expect(result.css).toContain('.tracking-tight')
expect(result.css).toContain('.tracking-tight')
}
test('purges unused classes', () => {
const OLD_NODE_ENV = process.env.NODE_ENV
process.env.NODE_ENV = 'production'
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
const input = fs.readFileSync(inputPath, 'utf8')
return postcss([
tailwind({
...config,
purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
}),
])
.process(input, { from: inputPath })
.then(result => {
process.env.NODE_ENV = OLD_NODE_ENV
assertPurged(result)
})
})
test('does not purge components', () => {
const OLD_NODE_ENV = process.env.NODE_ENV
process.env.NODE_ENV = 'production'
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
const input = fs.readFileSync(inputPath, 'utf8')
return postcss([
tailwind({
...config,
purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
}),
])
.process(input, { from: inputPath })
.then(result => {
process.env.NODE_ENV = OLD_NODE_ENV
expect(result.css).toContain('.container')
assertPurged(result)
})
})
test('does not purge except in production', () => {
const OLD_NODE_ENV = process.env.NODE_ENV
process.env.NODE_ENV = 'development'
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
const input = fs.readFileSync(inputPath, 'utf8')
return postcss([
tailwind({
...defaultConfig,
purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
}),
])
.process(input, { from: inputPath })
.then(result => {
process.env.NODE_ENV = OLD_NODE_ENV
const expected = fs.readFileSync(
path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
'utf8'
)
expect(result.css).toBe(expected)
})
})
test('does not purge if the array is empty', () => {
const OLD_NODE_ENV = process.env.NODE_ENV
process.env.NODE_ENV = 'production'
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
const input = fs.readFileSync(inputPath, 'utf8')
return postcss([
tailwind({
...defaultConfig,
purge: [],
}),
])
.process(input, { from: inputPath })
.then(result => {
process.env.NODE_ENV = OLD_NODE_ENV
const expected = fs.readFileSync(
path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
'utf8'
)
expect(result.css).toBe(expected)
})
})
test('does not purge if explicitly disabled', () => {
const OLD_NODE_ENV = process.env.NODE_ENV
process.env.NODE_ENV = 'production'
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
const input = fs.readFileSync(inputPath, 'utf8')
return postcss([
tailwind({
...defaultConfig,
purge: { enabled: false },
}),
])
.process(input, { from: inputPath })
.then(result => {
process.env.NODE_ENV = OLD_NODE_ENV
const expected = fs.readFileSync(
path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
'utf8'
)
expect(result.css).toBe(expected)
})
})
test('does not purge if purge is simply false', () => {
const OLD_NODE_ENV = process.env.NODE_ENV
process.env.NODE_ENV = 'production'
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
const input = fs.readFileSync(inputPath, 'utf8')
return postcss([
tailwind({
...defaultConfig,
purge: false,
}),
])
.process(input, { from: inputPath })
.then(result => {
process.env.NODE_ENV = OLD_NODE_ENV
const expected = fs.readFileSync(
path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
'utf8'
)
expect(result.css).toBe(expected)
})
})
test('purges outside of production if explicitly enabled', () => {
const OLD_NODE_ENV = process.env.NODE_ENV
process.env.NODE_ENV = 'development'
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
const input = fs.readFileSync(inputPath, 'utf8')
return postcss([
tailwind({
...config,
purge: { enabled: true, content: [path.resolve(`${__dirname}/fixtures/**/*.html`)] },
}),
])
.process(input, { from: inputPath })
.then(result => {
process.env.NODE_ENV = OLD_NODE_ENV
assertPurged(result)
})
})
test('purgecss options can be provided', () => {
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
const input = fs.readFileSync(inputPath, 'utf8')
return postcss([
tailwind({
...config,
purge: {
enabled: true,
options: {
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
whitelist: ['md:bg-green-500'],
},
},
}),
])
.process(input, { from: inputPath })
.then(result => {
expect(result.css).toContain('.md\\:bg-green-500')
assertPurged(result)
})
})
test('can purge all CSS, not just Tailwind classes', () => {
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
const input = fs.readFileSync(inputPath, 'utf8')
return postcss([
tailwind({
...config,
purge: {
enabled: true,
mode: 'all',
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
},
}),
function(css) {
// Remove any comments to avoid accidentally asserting against them
// instead of against real CSS rules.
css.walkComments(c => c.remove())
},
])
.process(input, { from: inputPath })
.then(result => {
expect(result.css).toContain('html')
expect(result.css).toContain('body')
expect(result.css).toContain('samp')
expect(result.css).not.toContain('button')
expect(result.css).not.toContain('legend')
expect(result.css).not.toContain('progress')
assertPurged(result)
})
})
test('the `conservative` mode can be set explicitly', () => {
const OLD_NODE_ENV = process.env.NODE_ENV
process.env.NODE_ENV = 'production'
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
const input = fs.readFileSync(inputPath, 'utf8')
return postcss([
tailwind({
...config,
purge: {
mode: 'conservative',
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
},
}),
])
.process(input, { from: inputPath })
.then(result => {
process.env.NODE_ENV = OLD_NODE_ENV
expect(result.css).not.toContain('.bg-red-600')
expect(result.css).not.toContain('.w-1\\/3')
expect(result.css).not.toContain('.flex')
expect(result.css).not.toContain('.font-sans')
expect(result.css).not.toContain('.text-right')
expect(result.css).not.toContain('.px-4')
expect(result.css).not.toContain('.h-full')
expect(result.css).toContain('.bg-red-500')
expect(result.css).toContain('.md\\:bg-blue-300')
expect(result.css).toContain('.w-1\\/2')
expect(result.css).toContain('.block')
expect(result.css).toContain('.md\\:flow-root')
expect(result.css).toContain('.h-screen')
expect(result.css).toContain('.min-h-\\(screen-4\\)')
expect(result.css).toContain('.bg-black\\!')
expect(result.css).toContain('.font-\\%\\#\\$\\@')
expect(result.css).toContain('.w-\\(1\\/2\\+8\\)')
expect(result.css).toContain('.inline-grid')
expect(result.css).toContain('.grid-cols-3')
expect(result.css).toContain('.px-1\\.5')
expect(result.css).toContain('.col-span-2')
expect(result.css).toContain('.col-span-1')
expect(result.css).toContain('.text-center')
})
})

View file

@ -12,23 +12,33 @@ test('it can generate responsive variants', () => {
.banana { color: yellow; }
.chocolate { color: brown; }
}
@tailwind screens;
`
const output = `
@layer utilities {
.banana { color: yellow; }
.chocolate { color: brown; }
@media (min-width: 500px) {
}
@media (min-width: 500px) {
@layer utilities {
.sm\\:banana { color: yellow; }
.sm\\:chocolate { color: brown; }
}
@media (min-width: 750px) {
}
@media (min-width: 750px) {
@layer utilities {
.md\\:banana { color: yellow; }
.md\\:chocolate { color: brown; }
}
@media (min-width: 1000px) {
}
@media (min-width: 1000px) {
@layer utilities {
.lg\\:banana { color: yellow; }
.lg\\:chocolate { color: brown; }
}
}
`
return run(input, {
@ -52,23 +62,33 @@ test('it can generate responsive variants with a custom separator', () => {
.banana { color: yellow; }
.chocolate { color: brown; }
}
@tailwind screens;
`
const output = `
@layer utilities {
.banana { color: yellow; }
.chocolate { color: brown; }
@media (min-width: 500px) {
}
@media (min-width: 500px) {
@layer utilities {
.sm__banana { color: yellow; }
.sm__chocolate { color: brown; }
}
@media (min-width: 750px) {
}
@media (min-width: 750px) {
@layer utilities {
.md__banana { color: yellow; }
.md__chocolate { color: brown; }
}
@media (min-width: 1000px) {
}
@media (min-width: 1000px) {
@layer utilities {
.lg__banana { color: yellow; }
.lg__chocolate { color: brown; }
}
}
`
return run(input, {
@ -92,23 +112,33 @@ test('it can generate responsive variants when classes have non-standard charact
.hover\\:banana { color: yellow; }
.chocolate-2\\.5 { color: brown; }
}
@tailwind screens;
`
const output = `
@layer utilities {
.hover\\:banana { color: yellow; }
.chocolate-2\\.5 { color: brown; }
@media (min-width: 500px) {
}
@media (min-width: 500px) {
@layer utilities {
.sm\\:hover\\:banana { color: yellow; }
.sm\\:chocolate-2\\.5 { color: brown; }
}
@media (min-width: 750px) {
}
@media (min-width: 750px) {
@layer utilities {
.md\\:hover\\:banana { color: yellow; }
.md\\:chocolate-2\\.5 { color: brown; }
}
@media (min-width: 1000px) {
}
@media (min-width: 1000px) {
@layer utilities {
.lg\\:hover\\:banana { color: yellow; }
.lg\\:chocolate-2\\.5 { color: brown; }
}
}
`
return run(input, {
@ -137,24 +167,42 @@ test('responsive variants are grouped', () => {
@responsive {
.chocolate { color: brown; }
}
@tailwind screens;
`
const output = `
@layer utilities {
.banana { color: yellow; }
.apple { color: red; }
}
.apple { color: red; }
@layer utilities {
.chocolate { color: brown; }
@media (min-width: 500px) {
}
@media (min-width: 500px) {
@layer utilities {
.sm\\:banana { color: yellow; }
}
@layer utilities {
.sm\\:chocolate { color: brown; }
}
@media (min-width: 750px) {
}
@media (min-width: 750px) {
@layer utilities {
.md\\:banana { color: yellow; }
}
@layer utilities {
.md\\:chocolate { color: brown; }
}
@media (min-width: 1000px) {
}
@media (min-width: 1000px) {
@layer utilities {
.lg\\:banana { color: yellow; }
}
@layer utilities {
.lg\\:chocolate { color: brown; }
}
}
`
return run(input, {
@ -181,39 +229,47 @@ test('it can generate responsive variants for nested at-rules', () => {
.grid\\:banana { color: blue; }
}
}
@tailwind screens;
`
const output = `
.banana {
color: yellow;
}
@supports(display: grid) {
.grid\\:banana {
color: blue;
}
}
@media (min-width: 500px) {
.sm\\:banana {
@layer utilities {
.banana {
color: yellow;
}
@supports(display: grid) {
.sm\\:grid\\:banana {
.grid\\:banana {
color: blue;
}
}
}
@media (min-width: 1000px) {
.lg\\:banana {
color: yellow;
}
@media (min-width: 500px) {
@layer utilities {
.sm\\:banana {
color: yellow;
}
@supports(display: grid) {
.lg\\:grid\\:banana {
color: blue;
@supports(display: grid) {
.sm\\:grid\\:banana {
color: blue;
}
}
}
}
@media (min-width: 1000px) {
@layer utilities {
.lg\\:banana {
color: yellow;
}
@supports(display: grid) {
.lg\\:grid\\:banana {
color: blue;
}
}
}
}
@ -244,44 +300,52 @@ test('it can generate responsive variants for deeply nested at-rules', () => {
}
}
}
@tailwind screens;
`
const output = `
.banana {
color: yellow;
}
@supports(display: grid) {
@supports(display: flex) {
.flex-grid\\:banana {
color: blue;
}
}
}
@media (min-width: 500px) {
.sm\\:banana {
@layer utilities {
.banana {
color: yellow;
}
@supports(display: grid) {
@supports(display: flex) {
.sm\\:flex-grid\\:banana {
.flex-grid\\:banana {
color: blue;
}
}
}
}
@media (min-width: 1000px) {
.lg\\:banana {
color: yellow;
}
@media (min-width: 500px) {
@layer utilities {
.sm\\:banana {
color: yellow;
}
@supports(display: grid) {
@supports(display: flex) {
.lg\\:flex-grid\\:banana {
color: blue;
@supports(display: grid) {
@supports(display: flex) {
.sm\\:flex-grid\\:banana {
color: blue;
}
}
}
}
}
@media (min-width: 1000px) {
@layer utilities {
.lg\\:banana {
color: yellow;
}
@supports(display: grid) {
@supports(display: flex) {
.lg\\:flex-grid\\:banana {
color: blue;
}
}
}
}
@ -307,19 +371,29 @@ test('screen prefix is only applied to the last class in a selector', () => {
@responsive {
.banana li * .sandwich #foo > div { color: yellow; }
}
@tailwind screens;
`
const output = `
@layer utilities {
.banana li * .sandwich #foo > div { color: yellow; }
@media (min-width: 500px) {
}
@media (min-width: 500px) {
@layer utilities {
.banana li * .sm\\:sandwich #foo > div { color: yellow; }
}
@media (min-width: 750px) {
}
@media (min-width: 750px) {
@layer utilities {
.banana li * .md\\:sandwich #foo > div { color: yellow; }
}
@media (min-width: 1000px) {
}
@media (min-width: 1000px) {
@layer utilities {
.banana li * .lg\\:sandwich #foo > div { color: yellow; }
}
}
`
return run(input, {
@ -342,19 +416,29 @@ test('responsive variants are generated for all selectors in a rule', () => {
@responsive {
.foo, .bar { color: yellow; }
}
@tailwind screens;
`
const output = `
@layer utilities {
.foo, .bar { color: yellow; }
@media (min-width: 500px) {
}
@media (min-width: 500px) {
@layer utilities {
.sm\\:foo, .sm\\:bar { color: yellow; }
}
@media (min-width: 750px) {
}
@media (min-width: 750px) {
@layer utilities {
.md\\:foo, .md\\:bar { color: yellow; }
}
@media (min-width: 1000px) {
}
@media (min-width: 1000px) {
@layer utilities {
.lg\\:foo, .lg\\:bar { color: yellow; }
}
}
`
return run(input, {
@ -377,6 +461,8 @@ test('selectors with no classes cannot be made responsive', () => {
@responsive {
div { color: yellow; }
}
@tailwind screens;
`
expect.assertions(1)
return run(input, {
@ -398,6 +484,8 @@ test('all selectors in a rule must contain classes', () => {
@responsive {
.foo, div { color: yellow; }
}
@tailwind screens;
`
expect.assertions(1)
return run(input, {

View file

@ -52,6 +52,56 @@ it('generates the right CSS when using @import instead of @tailwind', () => {
})
})
it('generates the right CSS when enabling flagged features', () => {
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
const input = fs.readFileSync(inputPath, 'utf8')
return postcss([
tailwind({
experimental: {
uniformColorPalette: true,
},
}),
])
.process(input, { from: inputPath })
.then(result => {
const expected = fs.readFileSync(
path.resolve(`${__dirname}/fixtures/tailwind-output-flagged.css`),
'utf8'
)
expect(result.css).toBe(expected)
})
})
// TODO: Move to per plugin unit tests for this sort of thing
it('generates the right CSS when color opacity plugins are disabled', () => {
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
const input = fs.readFileSync(inputPath, 'utf8')
return postcss([
tailwind({
...config,
corePlugins: {
textOpacity: false,
backgroundOpacity: false,
borderOpacity: false,
placeholderOpacity: false,
divideOpacity: false,
},
}),
])
.process(input, { from: inputPath })
.then(result => {
const expected = fs.readFileSync(
path.resolve(`${__dirname}/fixtures/tailwind-output-no-color-opacity.css`),
'utf8'
)
expect(result.css).toBe(expected)
})
})
it('does not add any CSS if no Tailwind features are used', () => {
return postcss([tailwind()])
.process('.foo { color: blue; }', { from: undefined })
@ -77,3 +127,19 @@ it('generates the right CSS with implicit screen utilities', () => {
expect(result.css).toBe(expected)
})
})
it('generates the right CSS when "important" is enabled', () => {
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
const input = fs.readFileSync(inputPath, 'utf8')
return postcss([tailwind({ ...config, important: true })])
.process(input, { from: inputPath })
.then(result => {
const expected = fs.readFileSync(
path.resolve(`${__dirname}/fixtures/tailwind-output-important.css`),
'utf8'
)
expect(result.css).toBe(expected)
})
})

View file

@ -0,0 +1,201 @@
import fs from 'fs'
import path from 'path'
import postcss from 'postcss'
import tailwind from '../src/index'
import config from '../stubs/defaultConfig.stub.js'
import processPlugins from '../src/util/processPlugins'
import runInTempDirectory from '../jest/runInTempDirectory'
function css(nodes) {
return postcss.root({ nodes }).toString()
}
it('generates the right CSS in IE11 mode', () => {
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
const input = fs.readFileSync(inputPath, 'utf8')
return postcss([tailwind({ ...config, target: 'ie11' })])
.process(input, { from: inputPath })
.then(result => {
const expected = fs.readFileSync(
path.resolve(`${__dirname}/fixtures/tailwind-output-ie11.css`),
'utf8'
)
expect(result.css).toBe(expected)
})
})
test('plugins can request the target for a specific plugin key', () => {
const { utilities } = processPlugins(
[
function({ addUtilities, target }) {
addUtilities({
'.testA': {
target: target('testPluginA'),
},
})
},
function({ addUtilities, target }) {
addUtilities({
'.testB': {
target: target('testPluginB'),
},
})
},
function({ addUtilities, target }) {
addUtilities({
'.testC': {
target: target('testPluginC'),
},
})
},
],
{
...config,
target: [
'ie11',
{
testPluginA: 'modern',
testPluginB: 'relaxed',
},
],
}
)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
.testA {
target: modern
}
}
}
@layer utilities {
@variants {
.testB {
target: relaxed
}
}
}
@layer utilities {
@variants {
.testC {
target: ie11
}
}
}
`)
})
test('browserslist target is translated to a target preset', () => {
return runInTempDirectory(() => {
fs.writeFileSync(
path.resolve('./.browserslistrc'),
`
last 2 versions
IE 11
`
)
const { utilities } = processPlugins(
[
function({ addUtilities, target }) {
addUtilities({
'.test': {
target: target('testPlugin'),
},
})
},
],
{
...config,
target: 'browserslist',
}
)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
.test {
target: ie11
}
}
}
`)
return Promise.resolve()
})
})
test('browserslist target is translated to a target preset with overrides', () => {
return runInTempDirectory(() => {
fs.writeFileSync(
path.resolve('./.browserslistrc'),
`
last 2 versions
IE 11
`
)
const { utilities } = processPlugins(
[
function({ addUtilities, target }) {
addUtilities({
'.testA': {
target: target('testPluginA'),
},
})
},
function({ addUtilities, target }) {
addUtilities({
'.testB': {
target: target('testPluginB'),
},
})
},
function({ addUtilities, target }) {
addUtilities({
'.testC': {
target: target('testPluginC'),
},
})
},
],
{
...config,
target: [
'browserslist',
{
testPluginA: 'modern',
testPluginB: 'relaxed',
},
],
}
)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
.testA {
target: modern
}
}
}
@layer utilities {
@variants {
.testB {
target: relaxed
}
}
}
@layer utilities {
@variants {
.testC {
target: ie11
}
}
}
`)
return Promise.resolve()
})
})

View file

@ -16,6 +16,15 @@ export default function(plugin, config) {
return getConfigValue(`variants.${path}`, defaultValue)
},
target: path => {
if (_.isString(config.target)) {
return config.target
}
const [defaultTarget, targetOverrides] = getConfigValue('target')
return _.get(targetOverrides, path, defaultTarget)
},
addUtilities(utilities, variants) {
addedUtilities.push([utilities, variants])
},
@ -24,6 +33,9 @@ export default function(plugin, config) {
plugin(pluginApi)
return {
utilities: addedUtilities,
utilities: addedUtilities.map(([utilities, variants]) => [
_.merge({}, ..._.castArray(utilities)),
variants,
]),
}
}

View file

@ -51,6 +51,27 @@ test('it can generate disabled variants', () => {
})
})
test('it can generate checked variants', () => {
const input = `
@variants checked {
.banana { color: yellow; }
.chocolate { color: brown; }
}
`
const output = `
.banana { color: yellow; }
.chocolate { color: brown; }
.checked\\:banana:checked { color: yellow; }
.checked\\:chocolate:checked { color: brown; }
`
return run(input).then(result => {
expect(result.css).toMatchCss(output)
expect(result.warnings().length).toBe(0)
})
})
test('it can generate active variants', () => {
const input = `
@variants active {
@ -135,6 +156,187 @@ test('it can generate focus-within variants', () => {
})
})
test('it can generate focus-visible variants', () => {
const input = `
@variants focus-visible {
.banana { color: yellow; }
.chocolate { color: brown; }
}
`
const output = `
.banana { color: yellow; }
.chocolate { color: brown; }
.focus-visible\\:banana:focus-visible { color: yellow; }
.focus-visible\\:chocolate:focus-visible { color: brown; }
`
return run(input).then(result => {
expect(result.css).toMatchCss(output)
expect(result.warnings().length).toBe(0)
})
})
test('it can generate motion-reduce variants', () => {
const input = `
@variants motion-reduce {
.banana { color: yellow; }
.chocolate { color: brown; }
}
`
const output = `
.banana { color: yellow; }
.chocolate { color: brown; }
@media (prefers-reduced-motion: reduce) {
.motion-reduce\\:banana { color: yellow; }
.motion-reduce\\:chocolate { color: brown; }
}
`
return run(input).then(result => {
expect(result.css).toMatchCss(output)
expect(result.warnings().length).toBe(0)
})
})
test('it can generate motion-safe variants', () => {
const input = `
@variants motion-safe {
.banana { color: yellow; }
.chocolate { color: brown; }
}
`
const output = `
.banana { color: yellow; }
.chocolate { color: brown; }
@media (prefers-reduced-motion: no-preference) {
.motion-safe\\:banana { color: yellow; }
.motion-safe\\:chocolate { color: brown; }
}
`
return run(input).then(result => {
expect(result.css).toMatchCss(output)
expect(result.warnings().length).toBe(0)
})
})
test('it can generate motion-safe and motion-reduce variants', () => {
const input = `
@variants motion-safe, motion-reduce {
.banana { color: yellow; }
.chocolate { color: brown; }
}
`
const output = `
.banana { color: yellow; }
.chocolate { color: brown; }
@media (prefers-reduced-motion: no-preference) {
.motion-safe\\:banana { color: yellow; }
.motion-safe\\:chocolate { color: brown; }
}
@media (prefers-reduced-motion: reduce) {
.motion-reduce\\:banana { color: yellow; }
.motion-reduce\\:chocolate { color: brown; }
}
`
return run(input).then(result => {
expect(result.css).toMatchCss(output)
expect(result.warnings().length).toBe(0)
})
})
test('motion-reduce variants stack with basic variants', () => {
const input = `
@variants motion-reduce, hover {
.banana { color: yellow; }
.chocolate { color: brown; }
}
`
const output = `
.banana { color: yellow; }
.chocolate { color: brown; }
.hover\\:banana:hover { color: yellow; }
.hover\\:chocolate:hover { color: brown; }
@media (prefers-reduced-motion: reduce) {
.motion-reduce\\:banana { color: yellow; }
.motion-reduce\\:chocolate { color: brown; }
.motion-reduce\\:hover\\:banana:hover { color: yellow; }
.motion-reduce\\:hover\\:chocolate:hover { color: brown; }
}
`
return run(input).then(result => {
expect(result.css).toMatchCss(output)
expect(result.warnings().length).toBe(0)
})
})
test('motion-safe variants stack with basic variants', () => {
const input = `
@variants motion-safe, hover {
.banana { color: yellow; }
.chocolate { color: brown; }
}
`
const output = `
.banana { color: yellow; }
.chocolate { color: brown; }
.hover\\:banana:hover { color: yellow; }
.hover\\:chocolate:hover { color: brown; }
@media (prefers-reduced-motion: no-preference) {
.motion-safe\\:banana { color: yellow; }
.motion-safe\\:chocolate { color: brown; }
.motion-safe\\:hover\\:banana:hover { color: yellow; }
.motion-safe\\:hover\\:chocolate:hover { color: brown; }
}
`
return run(input).then(result => {
expect(result.css).toMatchCss(output)
expect(result.warnings().length).toBe(0)
})
})
test('motion-safe and motion-reduce variants stack with basic variants', () => {
const input = `
@variants motion-reduce, motion-safe, hover {
.banana { color: yellow; }
.chocolate { color: brown; }
}
`
const output = `
.banana { color: yellow; }
.chocolate { color: brown; }
.hover\\:banana:hover { color: yellow; }
.hover\\:chocolate:hover { color: brown; }
@media (prefers-reduced-motion: reduce) {
.motion-reduce\\:banana { color: yellow; }
.motion-reduce\\:chocolate { color: brown; }
.motion-reduce\\:hover\\:banana:hover { color: yellow; }
.motion-reduce\\:hover\\:chocolate:hover { color: brown; }
}
@media (prefers-reduced-motion: no-preference) {
.motion-safe\\:banana { color: yellow; }
.motion-safe\\:chocolate { color: brown; }
.motion-safe\\:hover\\:banana:hover { color: yellow; }
.motion-safe\\:hover\\:chocolate:hover { color: brown; }
}
`
return run(input).then(result => {
expect(result.css).toMatchCss(output)
expect(result.warnings().length).toBe(0)
})
})
test('it can generate first-child variants', () => {
const input = `
@variants first {
@ -264,6 +466,51 @@ test('group-hover variants respect any configured prefix', () => {
})
})
test('it can generate group-focus variants', () => {
const input = `
@variants group-focus {
.banana { color: yellow; }
.chocolate { color: brown; }
}
`
const output = `
.banana { color: yellow; }
.chocolate { color: brown; }
.group:focus .group-focus\\:banana { color: yellow; }
.group:focus .group-focus\\:chocolate { color: brown; }
`
return run(input).then(result => {
expect(result.css).toMatchCss(output)
expect(result.warnings().length).toBe(0)
})
})
test('group-focus variants respect any configured prefix', () => {
const input = `
@variants group-focus {
.tw-banana { color: yellow; }
.tw-chocolate { color: brown; }
}
`
const output = `
.tw-banana { color: yellow; }
.tw-chocolate { color: brown; }
.tw-group:focus .group-focus\\:tw-banana { color: yellow; }
.tw-group:focus .group-focus\\:tw-chocolate { color: brown; }
`
return run(input, {
...config,
prefix: 'tw-',
}).then(result => {
expect(result.css).toMatchCss(output)
expect(result.warnings().length).toBe(0)
})
})
test('it can generate hover, active and focus variants', () => {
const input = `
@variants group-hover, hover, focus, active {
@ -370,7 +617,7 @@ test('variants are generated in the order specified', () => {
test('the built-in variant pseudo-selectors are appended before any pseudo-elements', () => {
const input = `
@variants hover, focus-within, focus, active, group-hover {
@variants hover, focus-within, focus-visible, focus, active, group-hover {
.placeholder-yellow::placeholder { color: yellow; }
}
`
@ -379,6 +626,7 @@ test('the built-in variant pseudo-selectors are appended before any pseudo-eleme
.placeholder-yellow::placeholder { color: yellow; }
.hover\\:placeholder-yellow:hover::placeholder { color: yellow; }
.focus-within\\:placeholder-yellow:focus-within::placeholder { color: yellow; }
.focus-visible\\:placeholder-yellow:focus-visible::placeholder { color: yellow; }
.focus\\:placeholder-yellow:focus::placeholder { color: yellow; }
.active\\:placeholder-yellow:active::placeholder { color: yellow; }
.group:hover .group-hover\\:placeholder-yellow::placeholder { color: yellow; }
@ -579,7 +827,10 @@ test('plugin variants can wrap rules in another at-rule using the raw PostCSS AP
...config.plugins,
function({ addVariant, e }) {
addVariant('supports-grid', ({ container, separator }) => {
const supportsRule = postcss.atRule({ name: 'supports', params: '(display: grid)' })
const supportsRule = postcss.atRule({
name: 'supports',
params: '(display: grid)',
})
supportsRule.nodes = container.nodes
container.nodes = [supportsRule]
supportsRule.walkRules(rule => {

View file

@ -0,0 +1,97 @@
import withAlphaVariable from '../src/util/withAlphaVariable'
test('it adds the right custom property', () => {
expect(
withAlphaVariable({ color: '#ff0000', property: 'color', variable: '--text-opacity' })
).toEqual({
'--text-opacity': '1',
color: ['#ff0000', 'rgba(255, 0, 0, var(--text-opacity))'],
})
})
test('it ignores colors that cannot be parsed', () => {
expect(
withAlphaVariable({
color: 'currentColor',
property: 'background-color',
variable: '--bg-opacity',
})
).toEqual({
'background-color': 'currentColor',
})
})
test('it ignores colors that already have an alpha channel', () => {
expect(
withAlphaVariable({
color: '#ff0000ff',
property: 'background-color',
variable: '--bg-opacity',
})
).toEqual({
'background-color': '#ff0000ff',
})
expect(
withAlphaVariable({
color: '#ff000080',
property: 'background-color',
variable: '--bg-opacity',
})
).toEqual({
'background-color': '#ff000080',
})
expect(
withAlphaVariable({
color: '#f00a',
property: 'background-color',
variable: '--bg-opacity',
})
).toEqual({
'background-color': '#f00a',
})
expect(
withAlphaVariable({
color: '#f00f',
property: 'background-color',
variable: '--bg-opacity',
})
).toEqual({
'background-color': '#f00f',
})
expect(
withAlphaVariable({
color: 'rgba(255, 255, 255, 1)',
property: 'background-color',
variable: '--bg-opacity',
})
).toEqual({
'background-color': 'rgba(255, 255, 255, 1)',
})
expect(
withAlphaVariable({
color: 'rgba(255, 255, 255, 0.5)',
property: 'background-color',
variable: '--bg-opacity',
})
).toEqual({
'background-color': 'rgba(255, 255, 255, 0.5)',
})
expect(
withAlphaVariable({
color: 'hsla(240, 100%, 50%, 1)',
property: 'background-color',
variable: '--bg-opacity',
})
).toEqual({
'background-color': 'hsla(240, 100%, 50%, 1)',
})
expect(
withAlphaVariable({
color: 'hsla(240, 100%, 50%, 0.5)',
property: 'background-color',
variable: '--bg-opacity',
})
).toEqual({
'background-color': 'hsla(240, 100%, 50%, 0.5)',
})
})

View file

@ -1,6 +1,6 @@
{
"name": "tailwindcss",
"version": "1.2.0",
"version": "1.6.2",
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
"license": "MIT",
"main": "lib/index.js",
@ -25,12 +25,20 @@
"style": "eslint .",
"test": "jest && eslint ."
},
"files": [
"dist/*.css",
"lib/*",
"scripts/*.js",
"stubs/*.stub.js",
"*.css",
"*.js"
],
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/node": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-jest": "^25.1.0",
"babel-jest": "^26.0.0",
"clean-css": "^4.1.9",
"eslint": "^6.0.1",
"eslint-config-postcss": "^2.0.2",
@ -41,11 +49,14 @@
"rimraf": "^3.0.0"
},
"dependencies": {
"@fullhuman/postcss-purgecss": "^2.1.2",
"autoprefixer": "^9.4.5",
"browserslist": "^4.12.0",
"bytes": "^3.0.0",
"chalk": "^4.0.0",
"chalk": "^3.0.0 || ^4.0.0",
"color": "^3.1.2",
"detective": "^5.2.0",
"fs-extra": "^9.0.0",
"fs-extra": "^8.0.0",
"lodash": "^4.17.15",
"node-emoji": "^1.8.1",
"normalize.css": "^8.0.1",

View file

@ -14,13 +14,9 @@ function buildDistFile(filename) {
.process(css, {
from: `./${filename}.css`,
to: `./dist/${filename}.css`,
map: { inline: false },
})
.then(result => {
fs.writeFileSync(`./dist/${filename}.css`, result.css)
if (result.map) {
fs.writeFileSync(`./dist/${filename}.css.map`, result.map)
}
return result
})
.then(result => {

View file

@ -39,6 +39,34 @@ Promise.all([
to: '__tests__/fixtures/tailwind-output-important.css',
config: { important: true },
}),
build({
from: '__tests__/fixtures/tailwind-input.css',
to: '__tests__/fixtures/tailwind-output-ie11.css',
config: { target: 'ie11' },
}),
build({
from: '__tests__/fixtures/tailwind-input.css',
to: '__tests__/fixtures/tailwind-output-no-color-opacity.css',
config: {
corePlugins: {
textOpacity: false,
backgroundOpacity: false,
borderOpacity: false,
placeholderOpacity: false,
divideOpacity: false,
},
},
}),
build({
from: '__tests__/fixtures/tailwind-input.css',
to: '__tests__/fixtures/tailwind-output-flagged.css',
config: {
future: {},
experimental: {
uniformColorPalette: true,
},
},
}),
]).then(() => {
console.log('\nFinished rebuilding fixtures.')
console.log(

View file

@ -4,7 +4,6 @@ import prettyHrtime from 'pretty-hrtime'
import tailwind from '../..'
import commands from '.'
import compile from '../compile'
import * as colors from '../colors'
import * as emoji from '../emoji'
@ -45,18 +44,6 @@ function stop(...msgs) {
utils.die()
}
/**
* Prints the error message and help for this command, then stops the process.
*
* @param {...string} [msgs]
*/
function stopWithHelp(...msgs) {
utils.header()
utils.error(...msgs)
commands.help.forCommand(commands.build)
utils.die()
}
/**
* Compiles CSS file and writes it to stdout.
*
@ -80,7 +67,12 @@ function buildToFile(compileOptions, startTime) {
utils.header()
utils.log()
utils.log(emoji.go, 'Building...', colors.file(inputFileSimplePath))
utils.log(
emoji.go,
...(inputFileSimplePath
? ['Building:', colors.file(inputFileSimplePath)]
: ['Building from default CSS...', colors.info('(No input file provided)')])
)
return compile(compileOptions).then(result => {
utils.writeFile(compileOptions.outputFile, result.css)
@ -112,8 +104,9 @@ export function run(cliParams, cliOptions) {
const inputFileSimplePath = utils.getSimplePath(inputFile)
const configFileSimplePath = utils.getSimplePath(configFile)
!inputFile && stopWithHelp('CSS file is required.')
!utils.exists(inputFile) && stop(colors.file(inputFileSimplePath), 'does not exist.')
if (inputFile) {
!utils.exists(inputFile) && stop(colors.file(inputFileSimplePath), 'does not exist.')
}
configFile &&
!utils.exists(configFile) &&

View file

@ -25,7 +25,14 @@ const defaultOptions = {
*/
export default function compile(options = {}) {
const config = { ...defaultOptions, ...options }
const css = utils.readFile(config.inputFile)
const css = config.inputFile
? utils.readFile(config.inputFile)
: `
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return new Promise((resolve, reject) => {
postcss(config.plugins)

View file

@ -5,3 +5,4 @@ export const no = get('no_entry_sign')
export const go = get('rocket')
export const pack = get('package')
export const disk = get('floppy_disk')
export const warning = get('warning')

View file

@ -1,5 +1,8 @@
import preflight from './plugins/preflight'
import container from './plugins/container'
import space from './plugins/space'
import divideWidth from './plugins/divideWidth'
import divideColor from './plugins/divideColor'
import accessibility from './plugins/accessibility'
import appearance from './plugins/appearance'
import backgroundAttachment from './plugins/backgroundAttachment'
@ -43,6 +46,7 @@ import objectPosition from './plugins/objectPosition'
import opacity from './plugins/opacity'
import outline from './plugins/outline'
import overflow from './plugins/overflow'
import overscrollBehavior from './plugins/overscrollBehavior'
import padding from './plugins/padding'
import placeholderColor from './plugins/placeholderColor'
import pointerEvents from './plugins/pointerEvents'
@ -89,6 +93,12 @@ import transitionProperty from './plugins/transitionProperty'
import transitionTimingFunction from './plugins/transitionTimingFunction'
import transitionDuration from './plugins/transitionDuration'
import transitionDelay from './plugins/transitionDelay'
import divideOpacity from './plugins/divideOpacity'
import backgroundOpacity from './plugins/backgroundOpacity'
import borderOpacity from './plugins/borderOpacity'
import textOpacity from './plugins/textOpacity'
import placeholderOpacity from './plugins/placeholderOpacity'
import animation from './plugins/animation'
import configurePlugins from './util/configurePlugins'
@ -96,15 +106,21 @@ export default function({ corePlugins: corePluginConfig }) {
return configurePlugins(corePluginConfig, {
preflight,
container,
space,
divideWidth,
divideColor,
divideOpacity,
accessibility,
appearance,
backgroundAttachment,
backgroundColor,
backgroundOpacity,
backgroundPosition,
backgroundRepeat,
backgroundSize,
borderCollapse,
borderColor,
borderOpacity,
borderRadius,
borderStyle,
borderWidth,
@ -126,6 +142,7 @@ export default function({ corePlugins: corePluginConfig }) {
fontFamily,
fontWeight,
height,
fontSize,
lineHeight,
listStylePosition,
listStyleType,
@ -139,8 +156,10 @@ export default function({ corePlugins: corePluginConfig }) {
opacity,
outline,
overflow,
overscrollBehavior,
padding,
placeholderColor,
placeholderOpacity,
pointerEvents,
position,
inset,
@ -152,7 +171,7 @@ export default function({ corePlugins: corePluginConfig }) {
tableLayout,
textAlign,
textColor,
fontSize,
textOpacity,
fontStyle,
textTransform,
textDecoration,
@ -185,5 +204,6 @@ export default function({ corePlugins: corePluginConfig }) {
transitionTimingFunction,
transitionDuration,
transitionDelay,
animation,
})
}

View file

@ -0,0 +1,132 @@
export default {
theme: {
colors: {
transparent: 'transparent',
current: 'currentColor',
black: '#000000',
white: '#ffffff',
gray: {
'50': '#f9fafb',
'100': '#f4f5f7',
'200': '#e5e7eb',
'300': '#d2d6dc',
'400': '#9fa6b2',
'500': '#6b7280',
'600': '#4b5563',
'700': '#374151',
'800': '#252f3f',
'900': '#161e2e',
},
red: {
'50': '#fdf2f2',
'100': '#fde8e8',
'200': '#fbd5d5',
'300': '#f8b4b4',
'400': '#f98080',
'500': '#f05252',
'600': '#e02424',
'700': '#c81e1e',
'800': '#9b1c1c',
'900': '#771d1d',
},
orange: {
'50': '#fff8f1',
'100': '#feecdc',
'200': '#fcd9bd',
'300': '#fdba8c',
'400': '#ff8a4c',
'500': '#ff5a1f',
'600': '#d03801',
'700': '#b43403',
'800': '#8a2c0d',
'900': '#73230d',
},
yellow: {
'50': '#fdfdea',
'100': '#fdf6b2',
'200': '#fce96a',
'300': '#faca15',
'400': '#e3a008',
'500': '#c27803',
'600': '#9f580a',
'700': '#8e4b10',
'800': '#723b13',
'900': '#633112',
},
green: {
'50': '#f3faf7',
'100': '#def7ec',
'200': '#bcf0da',
'300': '#84e1bc',
'400': '#31c48d',
'500': '#0e9f6e',
'600': '#057a55',
'700': '#046c4e',
'800': '#03543f',
'900': '#014737',
},
teal: {
'50': '#edfafa',
'100': '#d5f5f6',
'200': '#afecef',
'300': '#7edce2',
'400': '#16bdca',
'500': '#0694a2',
'600': '#047481',
'700': '#036672',
'800': '#05505c',
'900': '#014451',
},
blue: {
'50': '#ebf5ff',
'100': '#e1effe',
'200': '#c3ddfd',
'300': '#a4cafe',
'400': '#76a9fa',
'500': '#3f83f8',
'600': '#1c64f2',
'700': '#1a56db',
'800': '#1e429f',
'900': '#233876',
},
indigo: {
'50': '#f0f5ff',
'100': '#e5edff',
'200': '#cddbfe',
'300': '#b4c6fc',
'400': '#8da2fb',
'500': '#6875f5',
'600': '#5850ec',
'700': '#5145cd',
'800': '#42389d',
'900': '#362f78',
},
purple: {
'50': '#f6f5ff',
'100': '#edebfe',
'200': '#dcd7fe',
'300': '#cabffd',
'400': '#ac94fa',
'500': '#9061f9',
'600': '#7e3af2',
'700': '#6c2bd9',
'800': '#5521b5',
'900': '#4a1d96',
},
pink: {
'50': '#fdf2f8',
'100': '#fce8f3',
'200': '#fad1e8',
'300': '#f8b4d9',
'400': '#f17eb8',
'500': '#e74694',
'600': '#d61f69',
'700': '#bf125d',
'800': '#99154b',
'900': '#751a3d',
},
},
},
}

View file

@ -10,9 +10,20 @@ import processTailwindFeatures from './processTailwindFeatures'
import formatCSS from './lib/formatCSS'
import resolveConfig from './util/resolveConfig'
import { defaultConfigFile } from './constants'
import defaultConfig from '../stubs/defaultConfig.stub.js'
import uniformColorPalette from './flagged/uniformColorPalette.js'
function getDefaultConfigs(config) {
const configs = [defaultConfig]
if (_.get(config, ['experimental', 'uniformColorPalette'], false)) {
configs.unshift(uniformColorPalette)
}
return configs
}
function resolveConfigPath(filePath) {
// require('tailwindcss')({ theme: ..., variants: ... })
if (_.isObject(filePath) && !_.has(filePath, 'config') && !_.isEmpty(filePath)) {
@ -46,18 +57,21 @@ function resolveConfigPath(filePath) {
const getConfigFunction = config => () => {
if (_.isUndefined(config) && !_.isObject(config)) {
return resolveConfig([defaultConfig])
return resolveConfig([...getDefaultConfigs(defaultConfig)])
}
if (!_.isObject(config)) {
getModuleDependencies(config).forEach(mdl => {
delete require.cache[require.resolve(mdl.file)]
})
// Skip this if Jest is running: https://github.com/facebook/jest/pull/9841#issuecomment-621417584
if (process.env.JEST_WORKER_ID === undefined) {
if (!_.isObject(config)) {
getModuleDependencies(config).forEach(mdl => {
delete require.cache[require.resolve(mdl.file)]
})
}
}
const configObject = _.isObject(config) ? _.get(config, 'config', config) : require(config)
return resolveConfig([configObject, defaultConfig])
return resolveConfig([configObject, ...getDefaultConfigs(configObject)])
}
const plugin = postcss.plugin('tailwind', config => {

View file

@ -0,0 +1,13 @@
import postcss from 'postcss'
export default function convertLayerAtRulesToControlComments() {
return function(css) {
css.walkAtRules('layer', atRule => {
const layer = atRule.params
atRule.before(postcss.comment({ text: `tailwind start ${layer}` }))
atRule.before(atRule.nodes)
atRule.before(postcss.comment({ text: `tailwind end ${layer}` }))
atRule.remove()
})
}
}

View file

@ -0,0 +1,91 @@
import _ from 'lodash'
import postcss from 'postcss'
import purgecss from '@fullhuman/postcss-purgecss'
import chalk from 'chalk'
import { log } from '../cli/utils'
import * as emoji from '../cli/emoji'
function removeTailwindComments(css) {
css.walkComments(comment => {
switch (comment.text.trim()) {
case 'tailwind start components':
case 'tailwind start utilities':
case 'tailwind end components':
case 'tailwind end utilities':
comment.remove()
break
default:
break
}
})
}
export default function purgeUnusedUtilities(config) {
const purgeEnabled = _.get(
config,
'purge.enabled',
config.purge !== false && config.purge !== undefined && process.env.NODE_ENV === 'production'
)
if (!purgeEnabled) {
return removeTailwindComments
}
// Skip if `purge: []` since that's part of the default config
if (Array.isArray(config.purge) && config.purge.length === 0) {
log()
log(
emoji.warning,
chalk.yellow(
' Tailwind is not purging unused styles because no template paths have been provided.'
)
)
log(
chalk.white(
' If you have manually configured PurgeCSS outside of Tailwind or are deliberately not\n removing unused styles, set `purge: false` in your Tailwind config file to silence\n this warning.'
)
)
log(
chalk.white('\n https://tailwindcss.com/docs/controlling-file-size/#removing-unused-css')
)
return removeTailwindComments
}
return postcss([
function(css) {
const mode = _.get(config, 'purge.mode', 'conservative')
if (mode === 'conservative') {
css.prepend(postcss.comment({ text: 'purgecss start ignore' }))
css.append(postcss.comment({ text: 'purgecss end ignore' }))
css.walkComments(comment => {
switch (comment.text.trim()) {
case 'tailwind start utilities':
comment.text = 'purgecss end ignore'
break
case 'tailwind end utilities':
comment.text = 'purgecss start ignore'
break
default:
break
}
})
}
},
removeTailwindComments,
purgecss({
content: Array.isArray(config.purge) ? config.purge : config.purge.content,
defaultExtractor: content => {
// Capture as liberally as possible, including things like `h-(screen-1.5)`
const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || []
// Capture classes within other delimiters like .block(class="w-1/2") in Pug
const innerMatches = content.match(/[^<>"'`\s.(){}[\]#=%]*[^<>"'`\s.(){}[\]#=%:]/g) || []
return broadMatches.concat(innerMatches)
},
...config.purge.options,
}),
])
}

View file

@ -4,8 +4,51 @@ import cloneNodes from '../util/cloneNodes'
import buildMediaQuery from '../util/buildMediaQuery'
import buildSelectorVariant from '../util/buildSelectorVariant'
function isLayer(node) {
if (Array.isArray(node)) {
return node.length === 1 && isLayer(node[0])
}
return node.type === 'atrule' && node.name === 'layer'
}
function layerNodes(nodes) {
return isLayer(nodes) ? nodes[0].nodes : nodes
}
export default function(config) {
return function(css) {
// Wrap any `responsive` rules with a copy of their parent `layer` to
// ensure the layer isn't lost when copying to the `screens` location.
css.walkAtRules('layer', layerAtRule => {
const layer = layerAtRule.params
layerAtRule.walkAtRules('responsive', responsiveAtRule => {
const nestedlayerAtRule = postcss.atRule({
name: 'layer',
params: layer,
})
nestedlayerAtRule.prepend(responsiveAtRule.nodes)
responsiveAtRule.removeAll()
responsiveAtRule.prepend(nestedlayerAtRule)
})
})
// Find any `responsive` rules with no parent `layer` (these only come
// from the user's CSS, Tailwind never generates things this way itself)
// and wrap them with a default `layer` of `utilities`.
css.walkAtRules('responsive', responsiveAtRule => {
const [node] = responsiveAtRule.nodes
if (node.type === 'atrule' && node.name === 'layer') {
return
}
const nestedlayerAtRule = postcss.atRule({
name: 'layer',
params: 'utilities',
})
nestedlayerAtRule.prepend(responsiveAtRule.nodes)
responsiveAtRule.removeAll()
responsiveAtRule.prepend(nestedlayerAtRule)
})
const {
theme: { screens },
separator,
@ -16,7 +59,16 @@ export default function(config) {
css.walkAtRules('responsive', atRule => {
const nodes = atRule.nodes
responsiveRules.append(...cloneNodes(nodes))
atRule.before(nodes)
// If the parent is already a `layer` (this is true for anything coming from
// a plugin, including core plugins) we don't want to create a double nested
// layer, so only insert the layer children. If there is no parent layer,
// preserve the layer information when inserting the nodes.
if (isLayer(atRule.parent)) {
atRule.before(layerNodes(nodes))
} else {
atRule.before(nodes)
}
atRule.remove()
})
@ -43,22 +95,16 @@ export default function(config) {
const hasScreenRules = finalRules.some(i => i.nodes.length !== 0)
if (!hasScreenRules) {
return
}
let includesScreensExplicitly = false
css.walkAtRules('tailwind', atRule => {
if (atRule.params === 'screens') {
atRule.replaceWith(finalRules)
includesScreensExplicitly = true
if (atRule.params !== 'screens') {
return
}
})
if (!includesScreensExplicitly) {
css.append(finalRules)
return
}
if (hasScreenRules) {
atRule.before(finalRules)
}
atRule.remove()
})
}
}

View file

@ -40,6 +40,8 @@ export default function(
}
})
let includesScreensExplicitly = false
css.walkAtRules('tailwind', atRule => {
if (atRule.params === 'preflight') {
// prettier-ignore
@ -60,6 +62,14 @@ export default function(
atRule.before(updateSource(pluginUtilities, atRule.source))
atRule.remove()
}
if (atRule.params === 'screens') {
includesScreensExplicitly = true
}
})
if (!includesScreensExplicitly) {
css.append([postcss.atRule({ name: 'tailwind', params: 'screens' })])
}
}
}

View file

@ -23,6 +23,33 @@ function ensureIncludesDefault(variants) {
const defaultVariantGenerators = config => ({
default: generateVariantFunction(() => {}),
'motion-safe': generateVariantFunction(({ container, separator, modifySelectors }) => {
const modified = modifySelectors(({ selector }) => {
return selectorParser(selectors => {
selectors.walkClasses(sel => {
sel.value = `motion-safe${separator}${sel.value}`
})
}).processSync(selector)
})
const mediaQuery = postcss.atRule({
name: 'media',
params: '(prefers-reduced-motion: no-preference)',
})
mediaQuery.append(modified)
container.append(mediaQuery)
}),
'motion-reduce': generateVariantFunction(({ container, separator, modifySelectors }) => {
const modified = modifySelectors(({ selector }) => {
return selectorParser(selectors => {
selectors.walkClasses(sel => {
sel.value = `motion-reduce${separator}${sel.value}`
})
}).processSync(selector)
})
const mediaQuery = postcss.atRule({ name: 'media', params: '(prefers-reduced-motion: reduce)' })
mediaQuery.append(modified)
container.append(mediaQuery)
}),
'group-hover': generateVariantFunction(({ modifySelectors, separator }) => {
return modifySelectors(({ selector }) => {
return selectorParser(selectors => {
@ -36,18 +63,55 @@ const defaultVariantGenerators = config => ({
}).processSync(selector)
})
}),
'group-focus': generateVariantFunction(({ modifySelectors, separator }) => {
return modifySelectors(({ selector }) => {
return selectorParser(selectors => {
selectors.walkClasses(sel => {
sel.value = `group-focus${separator}${sel.value}`
sel.parent.insertBefore(
sel,
selectorParser().astSync(prefixSelector(config.prefix, '.group:focus '))
)
})
}).processSync(selector)
})
}),
hover: generatePseudoClassVariant('hover'),
'focus-within': generatePseudoClassVariant('focus-within'),
'focus-visible': generatePseudoClassVariant('focus-visible'),
focus: generatePseudoClassVariant('focus'),
active: generatePseudoClassVariant('active'),
visited: generatePseudoClassVariant('visited'),
disabled: generatePseudoClassVariant('disabled'),
checked: generatePseudoClassVariant('checked'),
first: generatePseudoClassVariant('first-child', 'first'),
last: generatePseudoClassVariant('last-child', 'last'),
odd: generatePseudoClassVariant('nth-child(odd)', 'odd'),
even: generatePseudoClassVariant('nth-child(even)', 'even'),
})
function prependStackableVariants(atRule, variants) {
const stackableVariants = ['motion-safe', 'motion-reduce']
if (!_.some(variants, v => stackableVariants.includes(v))) {
return variants
}
if (_.every(variants, v => stackableVariants.includes(v))) {
return variants
}
const variantsParent = postcss.atRule({
name: 'variants',
params: variants.filter(v => stackableVariants.includes(v)).join(', '),
})
atRule.before(variantsParent)
variantsParent.append(atRule)
variants = _.without(variants, ...stackableVariants)
return variants
}
export default function(config, { variantGenerators: pluginVariantGenerators }) {
return function(css) {
const variantGenerators = {
@ -55,20 +119,34 @@ export default function(config, { variantGenerators: pluginVariantGenerators })
...pluginVariantGenerators,
}
css.walkAtRules('variants', atRule => {
const variants = postcss.list.comma(atRule.params).filter(variant => variant !== '')
let variantsFound = false
if (variants.includes('responsive')) {
const responsiveParent = postcss.atRule({ name: 'responsive' })
atRule.before(responsiveParent)
responsiveParent.append(atRule)
}
do {
variantsFound = false
css.walkAtRules('variants', atRule => {
variantsFound = true
_.forEach(_.without(ensureIncludesDefault(variants), 'responsive'), variant => {
variantGenerators[variant](atRule, config)
let variants = postcss.list.comma(atRule.params).filter(variant => variant !== '')
if (variants.includes('responsive')) {
const responsiveParent = postcss.atRule({ name: 'responsive' })
atRule.before(responsiveParent)
responsiveParent.append(atRule)
}
const remainingVariants = prependStackableVariants(atRule, variants)
_.forEach(_.without(ensureIncludesDefault(remainingVariants), 'responsive'), variant => {
if (!variantGenerators[variant]) {
throw new Error(
`Your config mentions the "${variant}" variant, but "${variant}" doesn't appear to be a variant. Did you forget or misconfigure a plugin that supplies that variant?`
)
}
variantGenerators[variant](atRule, config)
})
atRule.remove()
})
atRule.remove()
})
} while (variantsFound)
}
}

26
src/plugins/animation.js Normal file
View file

@ -0,0 +1,26 @@
import _ from 'lodash'
export default function() {
return function({ addUtilities, e, theme, variants }) {
const keyframesConfig = theme('keyframes')
const keyframesStyles = _.fromPairs(
_.toPairs(keyframesConfig).map(([name, keyframes]) => {
return [`@keyframes ${name}`, keyframes]
})
)
addUtilities(keyframesStyles, { respectImportant: false })
const animationConfig = theme('animation')
const utilities = _.fromPairs(
_.toPairs(animationConfig).map(([suffix, animation]) => {
return [
`.${e(`animate-${suffix}`)}`,
{
animation,
},
]
})
)
addUtilities(utilities, variants('animation'))
}
}

View file

@ -1,16 +1,30 @@
import _ from 'lodash'
import flattenColorPalette from '../util/flattenColorPalette'
import withAlphaVariable from '../util/withAlphaVariable'
export default function() {
return function({ addUtilities, e, theme, variants }) {
return function({ addUtilities, e, theme, variants, target, corePlugins }) {
const colors = flattenColorPalette(theme('backgroundColor'))
const getProperties = value => {
if (target('backgroundColor') === 'ie11') {
return { 'background-color': value }
}
if (corePlugins('backgroundOpacity')) {
return withAlphaVariable({
color: value,
property: 'background-color',
variable: '--bg-opacity',
})
}
return { 'background-color': value }
}
const utilities = _.fromPairs(
_.map(flattenColorPalette(theme('backgroundColor')), (value, modifier) => {
return [
`.${e(`bg-${modifier}`)}`,
{
'background-color': value,
},
]
_.map(colors, (value, modifier) => {
return [`.${e(`bg-${modifier}`)}`, getProperties(value)]
})
)

View file

@ -0,0 +1,14 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function() {
return function({ target, ...args }) {
if (target('backgroundOpacity') === 'ie11') {
return
}
createUtilityPlugin('backgroundOpacity', [['bg-opacity', ['--bg-opacity']]])({
target,
...args,
})
}
}

View file

@ -1,18 +1,30 @@
import _ from 'lodash'
import flattenColorPalette from '../util/flattenColorPalette'
import withAlphaVariable from '../util/withAlphaVariable'
export default function() {
return function({ addUtilities, e, theme, variants }) {
return function({ addUtilities, e, theme, variants, target, corePlugins }) {
const colors = flattenColorPalette(theme('borderColor'))
const getProperties = value => {
if (target('borderColor') === 'ie11') {
return { 'border-color': value }
}
if (corePlugins('borderOpacity')) {
return withAlphaVariable({
color: value,
property: 'border-color',
variable: '--border-opacity',
})
}
return { 'border-color': value }
}
const utilities = _.fromPairs(
_.map(_.omit(colors, 'default'), (value, modifier) => {
return [
`.${e(`border-${modifier}`)}`,
{
'border-color': value,
},
]
return [`.${e(`border-${modifier}`)}`, getProperties(value)]
})
)

View file

@ -0,0 +1,14 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function() {
return function({ target, ...args }) {
if (target('borderOpacity') === 'ie11') {
return
}
createUtilityPlugin('borderOpacity', [['border-opacity', ['--border-opacity']]])({
target,
...args,
})
}
}

View file

@ -22,9 +22,64 @@ function extractMinWidths(breakpoints) {
})
}
function mapMinWidthsToPadding(minWidths, screens, paddings) {
if (typeof paddings === 'undefined') {
return []
}
if (!_.isObject(paddings)) {
return [
{
screen: 'default',
minWidth: 0,
padding: paddings,
},
]
}
const mapping = []
if (paddings.default) {
mapping.push({
screen: 'default',
minWidth: 0,
padding: paddings.default,
})
}
_.each(minWidths, minWidth => {
Object.keys(screens).forEach(screen => {
if (`${screens[screen]}` === `${minWidth}`) {
mapping.push({
screen,
minWidth,
padding: paddings[screen],
})
}
})
})
return mapping
}
module.exports = function() {
return function({ addComponents, theme }) {
const minWidths = extractMinWidths(theme('container.screens', theme('screens')))
return function({ addComponents, theme, variants }) {
const screens = theme('container.screens', theme('screens'))
const minWidths = extractMinWidths(screens)
const paddings = mapMinWidthsToPadding(minWidths, screens, theme('container.padding'))
const generatePaddingFor = minWidth => {
const paddingConfig = _.find(paddings, padding => `${padding.minWidth}` === `${minWidth}`)
if (!paddingConfig) {
return {}
}
return {
paddingRight: paddingConfig.padding,
paddingLeft: paddingConfig.padding,
}
}
const atRules = _(minWidths)
.sortBy(minWidth => parseInt(minWidth))
@ -34,26 +89,25 @@ module.exports = function() {
[`@media (min-width: ${minWidth})`]: {
'.container': {
'max-width': minWidth,
...generatePaddingFor(minWidth),
},
},
}
})
.value()
addComponents([
{
'.container': Object.assign(
{ width: '100%' },
theme('container.center', false) ? { marginRight: 'auto', marginLeft: 'auto' } : {},
_.has(theme('container', {}), 'padding')
? {
paddingRight: theme('container.padding'),
paddingLeft: theme('container.padding'),
}
: {}
),
},
...atRules,
])
addComponents(
[
{
'.container': Object.assign(
{ width: '100%' },
theme('container.center', false) ? { marginRight: 'auto', marginLeft: 'auto' } : {},
generatePaddingFor(0)
),
},
...atRules,
],
variants('container')
)
}
}

View file

@ -27,7 +27,6 @@ pre {
button {
background-color: transparent;
background-image: none;
padding: 0;
}
/**

View file

@ -1,5 +1,5 @@
export default function() {
return function({ addUtilities, variants }) {
return function({ addUtilities, variants, target }) {
addUtilities(
{
'.block': {
@ -11,21 +11,12 @@ export default function() {
'.inline': {
display: 'inline',
},
'.flow-root': {
display: 'flow-root',
},
'.flex': {
display: 'flex',
},
'.inline-flex': {
display: 'inline-flex',
},
'.grid': {
display: 'grid',
},
'.inline-grid': {
display: 'inline-grid',
},
'.table': {
display: 'table',
},
@ -53,6 +44,19 @@ export default function() {
'.table-row': {
display: 'table-row',
},
...(target('display') === 'ie11'
? {}
: {
'.flow-root': {
display: 'flow-root',
},
'.grid': {
display: 'grid',
},
'.inline-grid': {
display: 'inline-grid',
},
}),
'.hidden': {
display: 'none',
},

View file

@ -0,0 +1,36 @@
import _ from 'lodash'
import flattenColorPalette from '../util/flattenColorPalette'
import withAlphaVariable from '../util/withAlphaVariable'
export default function() {
return function({ addUtilities, e, theme, variants, target, corePlugins }) {
const colors = flattenColorPalette(theme('divideColor'))
const getProperties = value => {
if (target('divideColor') === 'ie11') {
return { 'border-color': value }
}
if (corePlugins('divideOpacity')) {
return withAlphaVariable({
color: value,
property: 'border-color',
variable: '--divide-opacity',
})
}
return { 'border-color': value }
}
const utilities = _.fromPairs(
_.map(_.omit(colors, 'default'), (value, modifier) => {
return [
`.${e(`divide-${modifier}`)} > :not(template) ~ :not(template)`,
getProperties(value),
]
})
)
addUtilities(utilities, variants('divideColor'))
}
}

View file

@ -0,0 +1,22 @@
import _ from 'lodash'
export default function() {
return function({ addUtilities, e, theme, variants, target }) {
if (target('divideOpacity') === 'ie11') {
return
}
const utilities = _.fromPairs(
_.map(theme('divideOpacity'), (value, modifier) => {
return [
`.${e(`divide-opacity-${modifier}`)} > :not(template) ~ :not(template)`,
{
'--divide-opacity': value,
},
]
})
)
addUtilities(utilities, variants('divideOpacity'))
}
}

View file

@ -0,0 +1,65 @@
import _ from 'lodash'
export default function() {
return function({ addUtilities, e, theme, variants, target }) {
if (target('divideWidth') === 'ie11') {
const generators = [
(size, modifier) => ({
[`.${e(`divide-y${modifier}`)} > :not(template) ~ :not(template)`]: {
'border-top-width': size,
},
[`.${e(`divide-x${modifier}`)} > :not(template) ~ :not(template)`]: {
'border-left-width': size,
},
}),
]
const utilities = _.flatMap(generators, generator => {
return _.flatMap(theme('divideWidth'), (value, modifier) => {
return generator(value, modifier === 'default' ? '' : `-${modifier}`)
})
})
addUtilities(utilities, variants('divideWidth'))
return
}
const generators = [
(size, modifier) => ({
[`.${e(`divide-y${modifier}`)} > :not(template) ~ :not(template)`]: {
'--divide-y-reverse': '0',
'border-top-width': `calc(${
size === '0' ? '0px' : size
} * calc(1 - var(--divide-y-reverse)))`,
'border-bottom-width': `calc(${size === '0' ? '0px' : size} * var(--divide-y-reverse))`,
},
[`.${e(`divide-x${modifier}`)} > :not(template) ~ :not(template)`]: {
'--divide-x-reverse': '0',
'border-right-width': `calc(${size === '0' ? '0px' : size} * var(--divide-x-reverse))`,
'border-left-width': `calc(${
size === '0' ? '0px' : size
} * calc(1 - var(--divide-x-reverse)))`,
},
}),
]
const utilities = _.flatMap(generators, generator => {
return [
..._.flatMap(theme('divideWidth'), (value, modifier) => {
return generator(value, modifier === 'default' ? '' : `-${modifier}`)
}),
{
'.divide-y-reverse > :not(template) ~ :not(template)': {
'--divide-y-reverse': '1',
},
'.divide-x-reverse > :not(template) ~ :not(template)': {
'--divide-x-reverse': '1',
},
},
]
})
addUtilities(utilities, variants('divideWidth'))
}
}

View file

@ -3,14 +3,11 @@ import flattenColorPalette from '../util/flattenColorPalette'
export default function() {
return function({ addUtilities, e, theme, variants }) {
const colors = flattenColorPalette(theme('fill'))
const utilities = _.fromPairs(
_.map(flattenColorPalette(theme('fill')), (value, modifier) => {
return [
`.${e(`fill-${modifier}`)}`,
{
fill: value,
},
]
_.map(colors, (value, modifier) => {
return [`.${e(`fill-${modifier}`)}`, { fill: value }]
})
)

View file

@ -4,10 +4,27 @@ export default function() {
return function({ addUtilities, e, theme, variants }) {
const utilities = _.fromPairs(
_.map(theme('fontSize'), (value, modifier) => {
const [fontSize, options] = Array.isArray(value) ? value : [value]
const { lineHeight, letterSpacing } = _.isPlainObject(options)
? options
: {
lineHeight: options,
}
return [
`.${e(`text-${modifier}`)}`,
{
'font-size': value,
'font-size': fontSize,
...(lineHeight === undefined
? {}
: {
'line-height': lineHeight,
}),
...(letterSpacing === undefined
? {}
: {
'letter-spacing': letterSpacing,
}),
},
]
})

View file

@ -1,9 +1,15 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function() {
return createUtilityPlugin('gap', [
['gap', ['gridGap', 'gap']],
['col-gap', ['gridColumnGap', 'columnGap']],
['row-gap', ['gridRowGap', 'rowGap']],
])
return function({ target, ...args }) {
if (target('gap') === 'ie11') {
return
}
createUtilityPlugin('gap', [
['gap', ['gridGap', 'gap']],
['col-gap', ['gridColumnGap', 'columnGap']],
['row-gap', ['gridRowGap', 'rowGap']],
])({ target, ...args })
}
}

View file

@ -1,5 +1,9 @@
export default function() {
return function({ addUtilities, variants }) {
return function({ addUtilities, variants, target }) {
if (target('gridAutoFlow') === 'ie11') {
return
}
addUtilities(
{
'.grid-flow-row': { gridAutoFlow: 'row' },

View file

@ -1,5 +1,11 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function() {
return createUtilityPlugin('gridColumn', [['col', ['gridColumn']]])
return function({ target, ...args }) {
if (target('gridColumn') === 'ie11') {
return
}
createUtilityPlugin('gridColumn', [['col', ['gridColumn']]])({ target, ...args })
}
}

View file

@ -1,5 +1,11 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function() {
return createUtilityPlugin('gridColumnEnd', [['col-end', ['gridColumnEnd']]])
return function({ target, ...args }) {
if (target('gridColumnEnd') === 'ie11') {
return
}
createUtilityPlugin('gridColumnEnd', [['col-end', ['gridColumnEnd']]])({ target, ...args })
}
}

View file

@ -1,5 +1,14 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function() {
return createUtilityPlugin('gridColumnStart', [['col-start', ['gridColumnStart']]])
return function({ target, ...args }) {
if (target('gridColumnStart') === 'ie11') {
return
}
createUtilityPlugin('gridColumnStart', [['col-start', ['gridColumnStart']]])({
target,
...args,
})
}
}

View file

@ -1,5 +1,11 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function() {
return createUtilityPlugin('gridRow', [['row', ['gridRow']]])
return function({ target, ...args }) {
if (target('gridRow') === 'ie11') {
return
}
createUtilityPlugin('gridRow', [['row', ['gridRow']]])({ target, ...args })
}
}

View file

@ -1,5 +1,11 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function() {
return createUtilityPlugin('gridRowEnd', [['row-end', ['gridRowEnd']]])
return function({ target, ...args }) {
if (target('gridRowEnd') === 'ie11') {
return
}
createUtilityPlugin('gridRowEnd', [['row-end', ['gridRowEnd']]])({ target, ...args })
}
}

View file

@ -1,5 +1,11 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function() {
return createUtilityPlugin('gridRowStart', [['row-start', ['gridRowStart']]])
return function({ target, ...args }) {
if (target('gridRowStart') === 'ie11') {
return
}
createUtilityPlugin('gridRowStart', [['row-start', ['gridRowStart']]])({ target, ...args })
}
}

View file

@ -1,5 +1,14 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function() {
return createUtilityPlugin('gridTemplateColumns', [['grid-cols', ['gridTemplateColumns']]])
return function({ target, ...args }) {
if (target('gridTemplateColumns') === 'ie11') {
return
}
createUtilityPlugin('gridTemplateColumns', [['grid-cols', ['gridTemplateColumns']]])({
target,
...args,
})
}
}

View file

@ -1,5 +1,14 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function() {
return createUtilityPlugin('gridTemplateRows', [['grid-rows', ['gridTemplateRows']]])
return function({ target, ...args }) {
if (target('gridTemplateRows') === 'ie11') {
return
}
createUtilityPlugin('gridTemplateRows', [['grid-rows', ['gridTemplateRows']]])({
target,
...args,
})
}
}

View file

@ -1,5 +1,9 @@
export default function() {
return function({ addUtilities, variants }) {
return function({ addUtilities, variants, target }) {
if (target('objectFit') === 'ie11') {
return
}
addUtilities(
{
'.object-contain': { 'object-fit': 'contain' },

View file

@ -1,7 +1,11 @@
import _ from 'lodash'
export default function() {
return function({ addUtilities, e, theme, variants }) {
return function({ addUtilities, e, theme, variants, target }) {
if (target('objectPosition') === 'ie11') {
return
}
const utilities = _.fromPairs(
_.map(theme('objectPosition'), (value, modifier) => {
return [

View file

@ -0,0 +1,22 @@
export default function() {
return function({ addUtilities, variants, target }) {
if (target('overscrollBehavior') === 'ie11') {
return
}
addUtilities(
{
'.overscroll-auto': { 'overscroll-behavior': 'auto' },
'.overscroll-contain': { 'overscroll-behavior': 'contain' },
'.overscroll-none': { 'overscroll-behavior': 'none' },
'.overscroll-y-auto': { 'overscroll-behavior-y': 'auto' },
'.overscroll-y-contain': { 'overscroll-behavior-y': 'contain' },
'.overscroll-y-none': { 'overscroll-behavior-y': 'none' },
'.overscroll-x-auto': { 'overscroll-behavior-x': 'auto' },
'.overscroll-x-contain': { 'overscroll-behavior-x': 'contain' },
'.overscroll-x-none': { 'overscroll-behavior-x': 'none' },
},
variants('overscrollBehavior')
)
}
}

View file

@ -1,16 +1,30 @@
import _ from 'lodash'
import flattenColorPalette from '../util/flattenColorPalette'
import withAlphaVariable from '../util/withAlphaVariable'
export default function() {
return function({ addUtilities, e, theme, variants }) {
return function({ addUtilities, e, theme, variants, target, corePlugins }) {
const colors = flattenColorPalette(theme('placeholderColor'))
const getProperties = value => {
if (target('placeholderColor') === 'ie11') {
return { color: value }
}
if (corePlugins('placeholderOpacity')) {
return withAlphaVariable({
color: value,
property: 'color',
variable: '--placeholder-opacity',
})
}
return { color: value }
}
const utilities = _.fromPairs(
_.map(flattenColorPalette(theme('placeholderColor')), (value, modifier) => {
return [
`.${e(`placeholder-${modifier}`)}::placeholder`,
{
color: value,
},
]
_.map(colors, (value, modifier) => {
return [`.${e(`placeholder-${modifier}`)}::placeholder`, getProperties(value)]
})
)

View file

@ -0,0 +1,22 @@
import _ from 'lodash'
export default function() {
return function({ addUtilities, e, theme, variants, target }) {
if (target('placeholderOpacity') === 'ie11') {
return
}
const utilities = _.fromPairs(
_.map(theme('placeholderOpacity'), (value, modifier) => {
return [
`.${e(`placeholder-opacity-${modifier}`)}::placeholder`,
{
'--placeholder-opacity': value,
},
]
})
)
addUtilities(utilities, variants('placeholderOpacity'))
}
}

View file

@ -1,12 +1,18 @@
export default function() {
return function({ addUtilities, variants }) {
return function({ addUtilities, variants, target }) {
addUtilities(
{
'.static': { position: 'static' },
'.fixed': { position: 'fixed' },
'.absolute': { position: 'absolute' },
'.relative': { position: 'relative' },
'.sticky': { position: 'sticky' },
...(target('position') === 'ie11'
? {}
: {
'.sticky': {
position: 'sticky',
},
}),
},
variants('position')
)

View file

@ -1,5 +1,15 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function() {
return createUtilityPlugin('rotate', [['rotate', ['--transform-rotate']]])
return function({ target, ...args }) {
if (target('rotate') === 'ie11') {
createUtilityPlugin('rotate', [['rotate', ['transform'], value => `rotate(${value})`]])({
target,
...args,
})
return
}
createUtilityPlugin('rotate', [['rotate', ['--transform-rotate']]])({ target, ...args })
}
}

View file

@ -1,9 +1,21 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function() {
return createUtilityPlugin('scale', [
['scale', ['--transform-scale-x', '--transform-scale-y']],
['scale-x', ['--transform-scale-x']],
['scale-y', ['--transform-scale-y']],
])
return function({ target, ...args }) {
if (target('scale') === 'ie11') {
createUtilityPlugin('scale', [
['scale', ['transform'], value => `scale(${value})`],
['scale-x', ['transform'], value => `scaleX(${value})`],
['scale-y', ['transform'], value => `scaleY(${value})`],
])({ target, ...args })
return
}
createUtilityPlugin('scale', [
['scale', ['--transform-scale-x', '--transform-scale-y']],
['scale-x', ['--transform-scale-x']],
['scale-y', ['--transform-scale-y']],
])({ target, ...args })
}
}

View file

@ -1,8 +1,19 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function() {
return createUtilityPlugin('skew', [
['skew-x', ['--transform-skew-x']],
['skew-y', ['--transform-skew-y']],
])
return function({ target, ...args }) {
if (target('skew') === 'ie11') {
createUtilityPlugin('skew', [
['skew-x', ['transform'], value => `skewX(${value})`],
['skew-y', ['transform'], value => `skewY(${value})`],
])({ target, ...args })
return
}
createUtilityPlugin('skew', [
['skew-x', ['--transform-skew-x']],
['skew-y', ['--transform-skew-y']],
])({ target, ...args })
}
}

62
src/plugins/space.js Normal file
View file

@ -0,0 +1,62 @@
import _ from 'lodash'
import prefixNegativeModifiers from '../util/prefixNegativeModifiers'
export default function() {
return function({ addUtilities, e, theme, variants, target }) {
if (target('space') === 'ie11') {
const generators = [
(size, modifier) => ({
[`.${e(
prefixNegativeModifiers('space-y', modifier)
)} > :not(template) ~ :not(template)`]: {
'margin-top': size,
},
[`.${e(
prefixNegativeModifiers('space-x', modifier)
)} > :not(template) ~ :not(template)`]: {
'margin-left': size,
},
}),
]
const utilities = _.flatMap(generators, generator => {
return _.flatMap(theme('space'), generator)
})
addUtilities(utilities, variants('space'))
return
}
const generators = [
(size, modifier) => ({
[`.${e(prefixNegativeModifiers('space-y', modifier))} > :not(template) ~ :not(template)`]: {
'--space-y-reverse': '0',
'margin-top': `calc(${size === '0' ? '0px' : size} * calc(1 - var(--space-y-reverse)))`,
'margin-bottom': `calc(${size === '0' ? '0px' : size} * var(--space-y-reverse))`,
},
[`.${e(prefixNegativeModifiers('space-x', modifier))} > :not(template) ~ :not(template)`]: {
'--space-x-reverse': '0',
'margin-right': `calc(${size === '0' ? '0px' : size} * var(--space-x-reverse))`,
'margin-left': `calc(${size === '0' ? '0px' : size} * calc(1 - var(--space-x-reverse)))`,
},
}),
]
const utilities = _.flatMap(generators, generator => {
return [
..._.flatMap(theme('space'), generator),
{
'.space-y-reverse > :not(template) ~ :not(template)': {
'--space-y-reverse': '1',
},
'.space-x-reverse > :not(template) ~ :not(template)': {
'--space-x-reverse': '1',
},
},
]
})
addUtilities(utilities, variants('space'))
}
}

View file

@ -3,14 +3,11 @@ import flattenColorPalette from '../util/flattenColorPalette'
export default function() {
return function({ addUtilities, e, theme, variants }) {
const colors = flattenColorPalette(theme('stroke'))
const utilities = _.fromPairs(
_.map(flattenColorPalette(theme('stroke')), (value, modifier) => {
return [
`.${e(`stroke-${modifier}`)}`,
{
stroke: value,
},
]
_.map(colors, (value, modifier) => {
return [`.${e(`stroke-${modifier}`)}`, { stroke: value }]
})
)

View file

@ -1,16 +1,30 @@
import _ from 'lodash'
import flattenColorPalette from '../util/flattenColorPalette'
import withAlphaVariable from '../util/withAlphaVariable'
export default function() {
return function({ addUtilities, e, theme, variants }) {
return function({ addUtilities, e, theme, variants, target, corePlugins }) {
const colors = flattenColorPalette(theme('textColor'))
const getProperties = value => {
if (target('textColor') === 'ie11') {
return { color: value }
}
if (corePlugins('textOpacity')) {
return withAlphaVariable({
color: value,
property: 'color',
variable: '--text-opacity',
})
}
return { color: value }
}
const utilities = _.fromPairs(
_.map(flattenColorPalette(theme('textColor')), (value, modifier) => {
return [
`.${e(`text-${modifier}`)}`,
{
color: value,
},
]
_.map(colors, (value, modifier) => {
return [`.${e(`text-${modifier}`)}`, getProperties(value)]
})
)

View file

@ -0,0 +1,11 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function() {
return function({ target, ...args }) {
if (target('textOpacity') === 'ie11') {
return
}
createUtilityPlugin('textOpacity', [['text-opacity', ['--text-opacity']]])({ target, ...args })
}
}

View file

@ -1,5 +1,9 @@
export default function() {
return function({ addUtilities, variants }) {
return function({ addUtilities, variants, target }) {
if (target('transform') === 'ie11') {
return
}
addUtilities(
{
'.transform': {

View file

@ -1,8 +1,19 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function() {
return createUtilityPlugin('translate', [
['translate-x', ['--transform-translate-x']],
['translate-y', ['--transform-translate-y']],
])
return function({ target, ...args }) {
if (target('translate') === 'ie11') {
createUtilityPlugin('translate', [
['translate-x', ['transform'], value => `translateX(${value})`],
['translate-y', ['transform'], value => `translateY(${value})`],
])({ target, ...args })
return
}
createUtilityPlugin('translate', [
['translate-x', ['--transform-translate-x']],
['translate-y', ['--transform-translate-y']],
])({ target, ...args })
}
}

View file

@ -5,24 +5,97 @@ import substituteTailwindAtRules from './lib/substituteTailwindAtRules'
import evaluateTailwindFunctions from './lib/evaluateTailwindFunctions'
import substituteVariantsAtRules from './lib/substituteVariantsAtRules'
import substituteResponsiveAtRules from './lib/substituteResponsiveAtRules'
import convertLayerAtRulesToControlComments from './lib/convertLayerAtRulesToControlComments'
import substituteScreenAtRules from './lib/substituteScreenAtRules'
import substituteClassApplyAtRules from './lib/substituteClassApplyAtRules'
import purgeUnusedStyles from './lib/purgeUnusedStyles'
import corePlugins from './corePlugins'
import processPlugins from './util/processPlugins'
import cloneNodes from './util/cloneNodes'
import chalk from 'chalk'
const log = {
info(message) {
console.log(chalk.bold.cyan(' info'), '-', message)
},
warn(message) {
console.log(chalk.bold.yellow(' warn'), '-', message)
},
risk(message) {
console.log(chalk.bold.magenta(' risk'), '-', message)
},
}
function issueFlagNotices(config) {
const featureFlags = {
future: [],
experimental: ['uniformColorPalette'],
}
if (_.some(featureFlags.future, flag => _.get(config, ['future', flag], false))) {
const changes = Object.keys(config.future)
.filter(flag => featureFlags.future.includes(flag) && config.future[flag])
.map(s => chalk.cyan(s))
.join(', ')
console.log()
log.info(`You have opted-in to future-facing breaking changes: ${changes}`)
log.info(
'These changes are stable and will be the default behavior in the next major version of Tailwind.'
)
}
if (_.some(featureFlags.experimental, flag => _.get(config, ['experimental', flag], false))) {
const changes = Object.keys(config.experimental)
.filter(flag => featureFlags.experimental.includes(flag) && config.experimental[flag])
.map(s => chalk.yellow(s))
.join(', ')
console.log()
log.warn(`You have enabled experimental features: ${changes}`)
log.warn(
'Experimental features are not covered by semver, may introduce breaking changes, and can change at any time.'
)
}
if (Object.keys(_.get(config, 'future', {})).length < featureFlags.future.length) {
const changes = featureFlags.future
.filter(flag => !_.has(config, ['future', flag]))
.map(s => chalk.magenta(s))
.join(', ')
console.log()
log.risk(`There are upcoming breaking changes: ${changes}`)
log.risk(
'We highly recommend opting-in to these changes now to simplify upgrading Tailwind in the future.'
)
}
}
export default function(getConfig) {
return function(css) {
const config = getConfig()
issueFlagNotices(config)
const processedPlugins = processPlugins([...corePlugins(config), ...config.plugins], config)
const getProcessedPlugins = function() {
return {
...processedPlugins,
base: cloneNodes(processedPlugins.base),
components: cloneNodes(processedPlugins.components),
utilities: cloneNodes(processedPlugins.utilities),
}
}
return postcss([
substituteTailwindAtRules(config, processedPlugins),
substituteTailwindAtRules(config, getProcessedPlugins()),
evaluateTailwindFunctions(config),
substituteVariantsAtRules(config, processedPlugins),
substituteVariantsAtRules(config, getProcessedPlugins()),
substituteResponsiveAtRules(config),
convertLayerAtRulesToControlComments(config),
substituteScreenAtRules(config),
substituteClassApplyAtRules(config, processedPlugins.utilities),
substituteClassApplyAtRules(config, getProcessedPlugins().utilities),
purgeUnusedStyles(config),
]).process(css, { from: _.get(css, 'source.input.file') })
}
}

View file

@ -1,3 +1,4 @@
import identity from 'lodash/identity'
import fromPairs from 'lodash/fromPairs'
import toPairs from 'lodash/toPairs'
import castArray from 'lodash/castArray'
@ -16,16 +17,18 @@ function className(classPrefix, key) {
export default function createUtilityPlugin(themeKey, utilityVariations) {
return function({ e, addUtilities, variants, theme }) {
const utilities = utilityVariations.map(([classPrefix, properties]) => {
return fromPairs(
toPairs(theme(themeKey)).map(([key, value]) => {
return [
`.${e(className(classPrefix, key))}`,
fromPairs(castArray(properties).map(property => [property, value])),
]
})
)
})
const utilities = utilityVariations.map(
([classPrefix, properties, transformValue = identity]) => {
return fromPairs(
toPairs(theme(themeKey)).map(([key, value]) => {
return [
`.${e(className(classPrefix, key))}`,
fromPairs(castArray(properties).map(property => [property, transformValue(value)])),
]
})
)
}
)
return addUtilities(utilities, variants(themeKey))
}
}

View file

@ -1,5 +1,6 @@
import _ from 'lodash'
import postcss from 'postcss'
import browserslist from 'browserslist'
import Node from 'postcss/lib/node'
import isFunction from 'lodash/isFunction'
import escapeClassName from '../util/escapeClassName'
@ -7,7 +8,9 @@ import generateVariantFunction from '../util/generateVariantFunction'
import parseObjectStyles from '../util/parseObjectStyles'
import prefixSelector from '../util/prefixSelector'
import wrapWithVariants from '../util/wrapWithVariants'
import cloneNodes from '../util/cloneNodes'
import increaseSpecificity from '../util/increaseSpecificity'
import selectorParser from 'postcss-selector-parser'
function parseStyles(styles) {
if (!Array.isArray(styles)) {
@ -17,6 +20,23 @@ function parseStyles(styles) {
return _.flatMap(styles, style => (style instanceof Node ? style : parseObjectStyles(style)))
}
function containsClass(value) {
return selectorParser(selectors => {
let classFound = false
selectors.walkClasses(() => (classFound = true))
return classFound
}).transformSync(value)
}
function wrapWithLayer(rules, layer) {
return postcss
.atRule({
name: 'layer',
params: layer,
})
.append(cloneNodes(Array.isArray(rules) ? rules : [rules]))
}
export default function(plugins, config) {
const pluginBaseStyles = []
const pluginComponents = []
@ -26,7 +46,9 @@ export default function(plugins, config) {
const applyConfiguredPrefix = selector => {
return prefixSelector(config.prefix, selector)
}
const getConfigValue = (path, defaultValue) => (path ? _.get(config, path, defaultValue) : config)
const browserslistTarget = browserslist().includes('ie 11') ? 'ie11' : 'relaxed'
plugins.forEach(plugin => {
if (plugin.__isOptionsFunction) {
@ -39,6 +61,13 @@ export default function(plugins, config) {
postcss,
config: getConfigValue,
theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue),
corePlugins: path => {
if (Array.isArray(config.corePlugins)) {
return config.corePlugins.includes(path)
}
return getConfigValue(`corePlugins.${path}`, true)
},
variants: (path, defaultValue) => {
if (Array.isArray(config.variants)) {
return config.variants
@ -46,6 +75,17 @@ export default function(plugins, config) {
return getConfigValue(`variants.${path}`, defaultValue)
},
target: path => {
if (_.isString(config.target)) {
return config.target === 'browserslist' ? browserslistTarget : config.target
}
const [defaultTarget, targetOverrides] = getConfigValue('target')
const target = _.get(targetOverrides, path, defaultTarget)
return target === 'browserslist' ? browserslistTarget : target
},
e: escapeClassName,
prefix: applyConfiguredPrefix,
addUtilities: (utilities, options) => {
@ -66,6 +106,12 @@ export default function(plugins, config) {
if (config.important === true) {
rule.walkDecls(decl => (decl.important = true))
} else if (typeof config.important === 'string') {
if (containsClass(config.important)) {
throw rule.error(
`Classes are not allowed when using the \`important\` option with a string argument. Please use an ID instead.`
)
}
rule.selectors = rule.selectors.map(selector => {
return increaseSpecificity(config.important, selector)
})
@ -73,10 +119,16 @@ export default function(plugins, config) {
}
})
pluginUtilities.push(wrapWithVariants(styles.nodes, options.variants))
pluginUtilities.push(
wrapWithLayer(wrapWithVariants(styles.nodes, options.variants), 'utilities')
)
},
addComponents: (components, options) => {
options = Object.assign({ respectPrefix: true }, options)
const defaultOptions = { variants: [], respectPrefix: true }
options = Array.isArray(options)
? Object.assign({}, defaultOptions, { variants: options })
: _.defaults(options, defaultOptions)
const styles = postcss.root({ nodes: parseStyles(components) })
@ -86,7 +138,9 @@ export default function(plugins, config) {
}
})
pluginComponents.push(...styles.nodes)
pluginComponents.push(
wrapWithLayer(wrapWithVariants(styles.nodes, options.variants), 'components')
)
},
addBase: baseStyles => {
pluginBaseStyles.push(...parseStyles(baseStyles))

View file

@ -0,0 +1,39 @@
import createColor from 'color'
function hasAlpha(color) {
return (
color.startsWith('rgba(') ||
color.startsWith('hsla(') ||
(color.startsWith('#') && color.length === 9) ||
(color.startsWith('#') && color.length === 5)
)
}
function toRgba(color) {
const [r, g, b, a] = createColor(color)
.rgb()
.array()
return [r, g, b, a === undefined && hasAlpha(color) ? 1 : a]
}
export default function withAlphaVariable({ color, property, variable }) {
try {
const [r, g, b, a] = toRgba(color)
if (a !== undefined) {
return {
[property]: color,
}
}
return {
[variable]: '1',
[property]: [color, `rgba(${r}, ${g}, ${b}, var(${variable}))`],
}
} catch (error) {
return {
[property]: color,
}
}
}

View file

@ -2,6 +2,16 @@ import postcss from 'postcss'
import cloneNodes from './cloneNodes'
export default function wrapWithVariants(rules, variants) {
let foundVariantAtRule = false
postcss.root({ nodes: rules }).walkAtRules('variants', () => {
foundVariantAtRule = true
})
if (foundVariantAtRule) {
return cloneNodes(rules)
}
return postcss
.atRule({
name: 'variants',

View file

@ -1,4 +1,6 @@
module.exports = {
purge: [],
target: 'relaxed',
prefix: '',
important: false,
separator: ':',
@ -149,6 +151,7 @@ module.exports = {
'64': '16rem',
},
backgroundColor: theme => theme('colors'),
backgroundOpacity: theme => theme('opacity'),
backgroundPosition: {
bottom: 'bottom',
center: 'center',
@ -169,6 +172,7 @@ module.exports = {
...theme('colors'),
default: theme('colors.gray.300', 'currentColor'),
}),
borderOpacity: theme => theme('opacity'),
borderRadius: {
none: '0',
sm: '0.125rem',
@ -206,6 +210,9 @@ module.exports = {
move: 'move',
'not-allowed': 'not-allowed',
},
divideColor: theme => theme('borderColor'),
divideOpacity: theme => theme('borderOpacity'),
divideWidth: theme => theme('borderWidth'),
fill: {
current: 'currentColor',
},
@ -374,6 +381,11 @@ module.exports = {
},
padding: theme => theme('spacing'),
placeholderColor: theme => theme('colors'),
placeholderOpacity: theme => theme('opacity'),
space: (theme, { negative }) => ({
...theme('spacing'),
...negative(theme('spacing')),
}),
stroke: {
current: 'currentColor',
},
@ -383,6 +395,7 @@ module.exports = {
'2': '2',
},
textColor: theme => theme('colors'),
textOpacity: theme => theme('opacity'),
width: theme => ({
auto: 'auto',
...theme('spacing'),
@ -609,6 +622,37 @@ module.exports = {
'700': '700ms',
'1000': '1000ms',
},
animation: {
none: 'none',
spin: 'spin 1s linear infinite',
ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
bounce: 'bounce 1s infinite',
},
keyframes: {
spin: {
from: { transform: 'rotate(0deg)' },
to: { transform: 'rotate(360deg)' },
},
ping: {
'0%': { transform: 'scale(1)', opacity: '1' },
'75%, 100%': { transform: 'scale(2)', opacity: '0' },
},
pulse: {
'0%, 100%': { opacity: '1' },
'50%': { opacity: '.5' },
},
bounce: {
'0%, 100%': {
transform: 'translateY(-25%)',
animationTimingFunction: 'cubic-bezier(0.8,0,1,1)',
},
'50%': {
transform: 'translateY(0)',
animationTimingFunction: 'cubic-bezier(0,0,0.2,1)',
},
},
},
},
variants: {
accessibility: ['responsive', 'focus'],
@ -618,18 +662,24 @@ module.exports = {
appearance: ['responsive'],
backgroundAttachment: ['responsive'],
backgroundColor: ['responsive', 'hover', 'focus'],
backgroundOpacity: ['responsive', 'hover', 'focus'],
backgroundPosition: ['responsive'],
backgroundRepeat: ['responsive'],
backgroundSize: ['responsive'],
borderCollapse: ['responsive'],
borderColor: ['responsive', 'hover', 'focus'],
borderOpacity: ['responsive', 'hover', 'focus'],
borderRadius: ['responsive'],
borderStyle: ['responsive'],
borderWidth: ['responsive'],
boxShadow: ['responsive', 'hover', 'focus'],
boxSizing: ['responsive'],
container: ['responsive'],
cursor: ['responsive'],
display: ['responsive'],
divideColor: ['responsive'],
divideOpacity: ['responsive'],
divideWidth: ['responsive'],
fill: ['responsive'],
flex: ['responsive'],
flexDirection: ['responsive'],
@ -661,16 +711,20 @@ module.exports = {
order: ['responsive'],
outline: ['responsive', 'focus'],
overflow: ['responsive'],
overscrollBehavior: ['responsive'],
padding: ['responsive'],
placeholderColor: ['responsive', 'focus'],
placeholderOpacity: ['responsive', 'focus'],
pointerEvents: ['responsive'],
position: ['responsive'],
resize: ['responsive'],
space: ['responsive'],
stroke: ['responsive'],
strokeWidth: ['responsive'],
tableLayout: ['responsive'],
textAlign: ['responsive'],
textColor: ['responsive', 'hover', 'focus'],
textOpacity: ['responsive', 'hover', 'focus'],
textDecoration: ['responsive', 'hover', 'focus'],
textTransform: ['responsive'],
userSelect: ['responsive'],
@ -700,6 +754,7 @@ module.exports = {
transitionTimingFunction: ['responsive'],
transitionDuration: ['responsive'],
transitionDelay: ['responsive'],
animation: ['responsive'],
},
corePlugins: {},
plugins: [],

View file

@ -1,4 +1,5 @@
module.exports = {
purge: [],
theme: {
extend: {},
},

2300
yarn.lock

File diff suppressed because it is too large Load diff