├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jest └── customMatchers.js ├── jestconfig.coverage.json ├── package-lock.json ├── package.json ├── prettier.config.js ├── src ├── plugin.js └── util │ ├── extractGridAreaNames.js │ └── lodash-fns.js └── test ├── arbitrary-values.test.js ├── css.test.js ├── plugin.test.js └── util ├── extractGridAreaNames.test.js ├── run.js └── strings.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "parserOptions": { 6 | "ecmaVersion": 2020, 7 | "sourceType": "module" 8 | }, 9 | "extends": ["prettier"], 10 | "plugins": ["prettier"], 11 | "rules": { 12 | "camelcase": ["error", { "allow": ["^unstable_"] }], 13 | "no-unused-vars": [2, { "args": "all", "argsIgnorePattern": "^_" }], 14 | "no-warning-comments": 0, 15 | "prettier/prettier": "error" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.operating-system }} 12 | 13 | strategy: 14 | matrix: 15 | operating-system: [ubuntu-latest, windows-latest, macos-latest] 16 | node-version: [ 16, 18, 20 ] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - run: npm install 26 | - run: npm test 27 | env: 28 | CI: true 29 | 30 | coverage: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v3 34 | 35 | - name: Generate coverage report 36 | uses: actions/setup-node@v3 37 | with: 38 | node-version: 20.x 39 | - run: npm install 40 | - run: npm run test:coverage 41 | - run: bash <(curl -s https://codecov.io/bash) 42 | env: 43 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | coverage -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | coverage/ 3 | jest/ 4 | test/ 5 | .*.json 6 | *.config.js 7 | jestconfig.coverage.json 8 | .idea -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.1.0 - 2022-04-21 4 | * Allow arbitrary values for `grid-areas-[]` and `grid-in-[]` 5 | 6 | ## 2.0.1 - 2022-02-04 7 | * Fixed misspelling of Tailwind CSS in documentation 8 | 9 | ## 2.0.0 - 2021-12-13 10 | * Tested against Tailwind CSS v3 11 | * Updated minimum node requirements (v12) 12 | * Updated documentation 13 | 14 | ## 1.3.2 - 2021-02-24 15 | * Fixed link on npm button 16 | 17 | ## 1.3.1 - 2021-02-23 18 | * Removed .idea from npm package 19 | 20 | ## 1.3.0 - 2021-02-23 21 | * Fixed dependency for lodash in package.json. 22 | * Minor formatting improvements in README. 23 | 24 | ## 1.2.0 - 2020-12-11 25 | * Added .npmignore to lighten the installation size. 26 | 27 | ## 1.1.0 - 2020-11-27 28 | * Updated to work with Tailwind CSS version 2. 29 | * Added CHANGELOG. 30 | 31 | ## 1.0.1 - 2020-10-18 32 | * Initial release. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tailwind CSS Grid Areas 2 | 3 | [![Latest Version on NPM](https://img.shields.io/npm/v/@savvywombat/tailwindcss-grid-areas)](https://www.npmjs.com/package/@savvywombat/tailwindcss-grid-areas) 4 | [![Tailwind CSS](https://img.shields.io/badge/tailwind%20css-3-blue)](https://tailwindcss.com/) 5 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/SavvyWombat/tailwindcss-grid-areas/blob/main/LICENSE) 6 | [![Build](https://img.shields.io/github/actions/workflow/status/SavvyWombat/tailwindcss-grid-areas/test.yml?branch=main)](https://github.com/SavvyWombat/tailwindcss-grid-areas/actions) 7 | [![Code Coverage](https://codecov.io/gh/SavvyWombat/tailwindcss-grid-areas/branch/main/graph/badge.svg)](https://codecov.io/gh/SavvyWombat/tailwindcss-grid-areas) 8 | 9 | A plugin to provide Tailwind CSS utilities for grid areas. 10 | 11 | ## Installation 12 | 13 | ``` 14 | # npm 15 | npm install --save-dev @savvywombat/tailwindcss-grid-areas 16 | 17 | # yarn 18 | yarn add --dev @savvywombat/tailwindcss-grid-areas 19 | ``` 20 | 21 | ## Usage 22 | 23 | See the documentation at https://savvywombat.com.au/tailwind-css/grid-areas/ 24 | 25 | ## Related packages 26 | 27 | ### [Tailwind CSS Grid Named Lines](https://github.com/SavvyWombat/tailwindcss-grid-named-lines) 28 | 29 | A plugin to provide Tailwind CSS utilities for named grid lines. 30 | 31 | ## Licence 32 | 33 | [MIT](https://github.com/SavvyWombat/tailwindcss-grid-areas/blob/main/LICENSE) 34 | -------------------------------------------------------------------------------- /jest/customMatchers.js: -------------------------------------------------------------------------------- 1 | const prettier = require('prettier') 2 | const { diff } = require('jest-diff') 3 | 4 | function format(input) { 5 | return prettier.format(input, { 6 | parser: 'css', 7 | printWidth: 100, 8 | }) 9 | } 10 | 11 | expect.extend({ 12 | // Compare two CSS strings with all whitespace removed 13 | // This is probably naive but it's fast and works well enough. 14 | toMatchCss(received, argument) { 15 | function stripped(str) { 16 | return str.replace(/\s/g, '').replace(/;/g, '') 17 | } 18 | 19 | const options = { 20 | comment: 'stripped(received) === stripped(argument)', 21 | isNot: this.isNot, 22 | promise: this.promise, 23 | } 24 | 25 | const pass = stripped(received) === stripped(argument) 26 | 27 | const message = pass 28 | ? () => { 29 | return ( 30 | this.utils.matcherHint('toMatchCss', undefined, undefined, options) + 31 | '\n\n' + 32 | `Expected: not ${this.utils.printExpected(format(received))}\n` + 33 | `Received: ${this.utils.printReceived(format(argument))}` 34 | ) 35 | } 36 | : () => { 37 | const actual = format(received) 38 | const expected = format(argument) 39 | 40 | const diffString = diff(expected, actual, { 41 | expand: this.expand, 42 | }) 43 | 44 | return ( 45 | this.utils.matcherHint('toMatchCss', undefined, undefined, options) + 46 | '\n\n' + 47 | (diffString && diffString.includes('- Expect') 48 | ? `Difference:\n\n${diffString}` 49 | : `Expected: ${this.utils.printExpected(expected)}\n` + 50 | `Received: ${this.utils.printReceived(actual)}`) 51 | ) 52 | } 53 | 54 | return { actual: received, message, pass } 55 | }, 56 | toIncludeCss(received, argument) { 57 | function stripped(str) { 58 | return str.replace(/\s/g, '').replace(/;/g, '') 59 | } 60 | 61 | const options = { 62 | comment: 'stripped(received).includes(stripped(argument))', 63 | isNot: this.isNot, 64 | promise: this.promise, 65 | } 66 | 67 | const pass = stripped(received).includes(stripped(argument)) 68 | 69 | const message = pass 70 | ? () => { 71 | return ( 72 | this.utils.matcherHint('toIncludeCss', undefined, undefined, options) + 73 | '\n\n' + 74 | `Expected: not ${this.utils.printExpected(format(received))}\n` + 75 | `Received: ${this.utils.printReceived(format(argument))}` 76 | ) 77 | } 78 | : () => { 79 | const actual = format(received) 80 | const expected = format(argument) 81 | 82 | const diffString = diff(expected, actual, { 83 | expand: this.expand, 84 | }) 85 | 86 | return ( 87 | this.utils.matcherHint('toIncludeCss', undefined, undefined, options) + 88 | '\n\n' + 89 | (diffString && diffString.includes('- Expect') 90 | ? `Difference:\n\n${diffString}` 91 | : `Expected: ${this.utils.printExpected(expected)}\n` + 92 | `Received: ${this.utils.printReceived(actual)}`) 93 | ) 94 | } 95 | 96 | return { actual: received, message, pass } 97 | }, 98 | // Compare two CSS strings with all whitespace removed 99 | // This is probably naive but it's fast and works well enough. 100 | toMatchFormattedCss(received, argument) { 101 | function format(input) { 102 | return prettier.format(input.replace(/\n/g, ''), { 103 | parser: 'css', 104 | printWidth: 100, 105 | }) 106 | } 107 | const options = { 108 | comment: 'stripped(received) === stripped(argument)', 109 | isNot: this.isNot, 110 | promise: this.promise, 111 | } 112 | 113 | let formattedReceived = format(received) 114 | let formattedArgument = format(argument) 115 | 116 | const pass = formattedReceived.includes(formattedArgument) 117 | 118 | const message = pass 119 | ? () => { 120 | return ( 121 | this.utils.matcherHint('toMatchCss', undefined, undefined, options) + 122 | '\n\n' + 123 | `Expected: not ${this.utils.printExpected(formattedReceived)}\n` + 124 | `Received: ${this.utils.printReceived(formattedArgument)}` 125 | ) 126 | } 127 | : () => { 128 | const actual = formattedReceived 129 | const expected = formattedArgument 130 | 131 | const diffString = diff(expected, actual, { 132 | expand: this.expand, 133 | }) 134 | 135 | return ( 136 | this.utils.matcherHint('toMatchCss', undefined, undefined, options) + 137 | '\n\n' + 138 | (diffString && diffString.includes('- Expect') 139 | ? `Difference:\n\n${diffString}` 140 | : `Expected: ${this.utils.printExpected(expected)}\n` + 141 | `Received: ${this.utils.printReceived(actual)}`) 142 | ) 143 | } 144 | 145 | return { actual: received, message, pass } 146 | }, 147 | }) 148 | -------------------------------------------------------------------------------- /jestconfig.coverage.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectCoverage": true, 3 | "setupFilesAfterEnv": [ 4 | "/jest/customMatchers.js" 5 | ], 6 | "testPathIgnorePatterns": [ 7 | "/node_modules/" 8 | ] 9 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@savvywombat/tailwindcss-grid-areas", 3 | "version": "4.0.0", 4 | "description": "A plugin to provide Tailwind CSS utilities for grid areas.", 5 | "keywords": [ 6 | "tailwind", 7 | "tailwindcss", 8 | "css", 9 | "css grid", 10 | "css grid area", 11 | "grid-template-areas" 12 | ], 13 | "license": "MIT", 14 | "author": "Stuart Jones ", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/savvywombat/tailwindcss-grid-areas.git" 18 | }, 19 | "bugs": "https://github.com/savvywombat/tailwindcss-grid-areas/issues", 20 | "homepage": "https://github.com/savvywombat/tailwindcss-grid-areas#readme", 21 | "main": "src/plugin.js", 22 | "scripts": { 23 | "style": "eslint .", 24 | "test": "jest && eslint .", 25 | "test:coverage": "jest --config jestconfig.coverage.json" 26 | }, 27 | "peerDependencies": { 28 | "tailwindcss": "^3.0.1" 29 | }, 30 | "devDependencies": { 31 | "@babel/cli": "^7.22.5", 32 | "@babel/core": "^7.22.5", 33 | "@babel/node": "^7.22.5", 34 | "@babel/preset-env": "^7.22.5", 35 | "babel-jest": "^29.5.0", 36 | "clean-css": "^5.3.2", 37 | "eslint": "^8.43.0", 38 | "eslint-config-prettier": "^8.8.0", 39 | "eslint-plugin-prettier": "^4.2.1", 40 | "jest": "^29.5.0", 41 | "jest-matcher-css": "^1.1.0", 42 | "postcss": "^8.4.24", 43 | "prettier": "^2.8.8", 44 | "prettier-plugin-tailwindcss": "^0.3.0", 45 | "tailwindcss": "^3.3.2" 46 | }, 47 | "babel": { 48 | "presets": [ 49 | [ 50 | "@babel/preset-env", 51 | { 52 | "targets": { 53 | "node": "16.20.2" 54 | } 55 | } 56 | ] 57 | ] 58 | }, 59 | "engines": { 60 | "node": ">=16.20.2" 61 | }, 62 | "jest": { 63 | "testTimeout": 30000, 64 | "setupFilesAfterEnv": [ 65 | "/jest/customMatchers.js" 66 | ], 67 | "testPathIgnorePatterns": [ 68 | "/node_modules/" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // These settings are duplicated in .editorconfig: 3 | tabWidth: 2, // indent_size = 2 4 | useTabs: false, // indent_style = space 5 | endOfLine: 'lf', // end_of_line = lf 6 | semi: false, // default: true 7 | singleQuote: true, // default: false 8 | printWidth: 100, // default: 80 9 | trailingComma: 'es5', 10 | bracketSpacing: true, 11 | overrides: [ 12 | { 13 | files: '*.js', 14 | options: { 15 | parser: 'flow', 16 | }, 17 | }, 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | const { reduce } = require('./util/lodash-fns') 2 | const extractGridAreaNames = require('./util/extractGridAreaNames') 3 | 4 | module.exports = function ({ addUtilities, matchUtilities, theme, variants }) { 5 | const gridAreaNames = extractGridAreaNames(theme('gridTemplateAreas')) 6 | 7 | const templateAreas = reduce( 8 | theme('gridTemplateAreas'), 9 | (templates, area, name) => { 10 | return { 11 | ...templates, 12 | [`.grid-areas-${name}`]: { 13 | 'grid-template-areas': area 14 | .map((row) => { 15 | return `"${row}"` 16 | }) 17 | .join('\n'), 18 | }, 19 | } 20 | }, 21 | {} 22 | ) 23 | 24 | const specialTemplateAreas = { 25 | '.grid-areas-none': { 'grid-template-areas': 'none' }, 26 | '.grid-areas-inherit': { 'grid-template-areas': 'inherit' }, 27 | '.grid-areas-initial': { 'grid-template-areas': 'initial' }, 28 | '.grid-areas-revert': { 'grid-template-areas': 'revert' }, 29 | '.grid-areas-revert-layer': { 'grid-template-areas': 'revert-layer' }, 30 | '.grid-areas-unset': { 'grid-template-areas': 'unset' }, 31 | } 32 | 33 | addUtilities([templateAreas, specialTemplateAreas], variants('gridTemplateAreas')) 34 | 35 | const namedAreas = gridAreaNames.reduce((areas, name) => { 36 | return { 37 | ...areas, 38 | [`.grid-in-${name}`]: { 39 | 'grid-area': name, 40 | }, 41 | } 42 | }, {}) 43 | 44 | const specialNamedAreas = { 45 | '.grid-in-auto': { 'grid-area': 'auto' }, 46 | '.grid-in-inherit': { 'grid-area': 'inherit' }, 47 | '.grid-in-initial': { 'grid-area': 'initial' }, 48 | '.grid-in-revert': { 'grid-area': 'revert' }, 49 | '.grid-in-revert-layer': { 'grid-area': 'revert-layer' }, 50 | '.grid-in-unset': { 'grid-area': 'unset' }, 51 | } 52 | 53 | addUtilities([namedAreas, specialNamedAreas], []) 54 | 55 | const namedLines = gridAreaNames.reduce((lines, name) => { 56 | return { 57 | ...lines, 58 | [`.row-start-${name}`]: { 59 | 'grid-row-start': `${name}-start`, 60 | }, 61 | [`.row-end-${name}`]: { 62 | 'grid-row-end': `${name}-end`, 63 | }, 64 | [`.col-start-${name}`]: { 65 | 'grid-column-start': `${name}-start`, 66 | }, 67 | [`.col-end-${name}`]: { 68 | 'grid-column-end': `${name}-end`, 69 | }, 70 | } 71 | }, {}) 72 | 73 | addUtilities([namedLines], []) 74 | 75 | // allow arbitrary values 76 | matchUtilities({ 77 | 'grid-areas': (value) => { 78 | value = value 79 | .split(/ *, */) 80 | .map((row) => (row.match(/var\(.*\)/g) ? row : `"${row}"`)) 81 | .join(' ') 82 | return { 83 | 'grid-template-areas': `${value}`, 84 | } 85 | }, 86 | 'grid-in': (value) => { 87 | return { 88 | 'grid-area': `${value}`, 89 | } 90 | }, 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /src/util/extractGridAreaNames.js: -------------------------------------------------------------------------------- 1 | const { uniq, flatMap } = require('./lodash-fns') 2 | 3 | module.exports = function (gridTemplateAreas) { 4 | return uniq( 5 | flatMap(gridTemplateAreas, (row) => { 6 | return flatMap(row, (area) => { 7 | // extract grid area names from the gridTemplate 8 | return flatMap(area.match(/[^\s]+/g), (match) => { 9 | if (match !== '.') { 10 | return match 11 | } 12 | return [] 13 | }) 14 | }) 15 | }) 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/util/lodash-fns.js: -------------------------------------------------------------------------------- 1 | function reduce(collection, item, initialVal) { 2 | if (!collection) return [] 3 | return Object.keys(collection).reduce( 4 | (carry, current, index, array) => 5 | item( 6 | carry, 7 | !Array.isArray(collection) ? collection[current] : current, 8 | !Array.isArray(collection) ? current : array, 9 | index 10 | ), 11 | initialVal 12 | ) 13 | } 14 | 15 | function baseFlatten(array, depth) { 16 | const result = [] 17 | if (!array) { 18 | return result 19 | } 20 | 21 | for (const value of array) { 22 | if (depth && Array.isArray(value)) { 23 | if (depth > 1) { 24 | // Recursively flatten arrays (susceptible to call stack limits). 25 | baseFlatten(value, depth - 1) 26 | } else { 27 | result.push(...value) 28 | } 29 | } else { 30 | result[result.length] = value 31 | } 32 | } 33 | return result 34 | } 35 | 36 | function flatMap(arr, mapper) { 37 | if (!arr) return [] 38 | return baseFlatten( 39 | Object.keys(arr).map((value, index) => mapper(arr[value], !Array.isArray(arr) ? value : index)), 40 | 1 41 | ) 42 | } 43 | 44 | function uniq(arr) { 45 | return [...new Set(arr)] 46 | } 47 | 48 | module.exports = { reduce, flatMap, uniq } 49 | -------------------------------------------------------------------------------- /test/arbitrary-values.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import postcss from 'postcss' 3 | import tailwind from 'tailwindcss' 4 | import gridAreasPlugin from '../src/plugin' 5 | 6 | let css = String.raw 7 | let html = String.raw 8 | 9 | function run(input, config) { 10 | let { currentTestName } = expect.getState() 11 | 12 | config = { 13 | ...{ plugins: [gridAreasPlugin], corePlugins: { preflight: false } }, 14 | ...config, 15 | } 16 | 17 | return postcss(tailwind(config)).process(input, { 18 | from: `${path.resolve(__filename)}?test=${currentTestName}`, 19 | }) 20 | } 21 | 22 | it('should generate grid-template-areas', () => { 23 | let config = { 24 | content: [ 25 | { 26 | raw: html`
`, 27 | }, 28 | ], 29 | } 30 | 31 | return run('@tailwind utilities', config).then((result) => { 32 | return expect(result.css).toMatchFormattedCss(css` 33 | .grid-areas-\[left\] { 34 | grid-template-areas: 'left'; 35 | } 36 | `) 37 | }) 38 | }) 39 | 40 | it('should generate grid-template-areas with multiple columns', () => { 41 | let config = { 42 | content: [ 43 | { 44 | raw: html`
`, 45 | }, 46 | ], 47 | } 48 | 49 | return run('@tailwind utilities', config).then((result) => { 50 | return expect(result.css).toMatchFormattedCss(css` 51 | .grid-areas-\[left_right\] { 52 | grid-template-areas: 'left right'; 53 | } 54 | `) 55 | }) 56 | }) 57 | 58 | it('should generate grid-template-areas with multiple rows', () => { 59 | let config = { 60 | content: [ 61 | { 62 | raw: html`
`, 63 | }, 64 | ], 65 | } 66 | 67 | return run('@tailwind utilities', config).then((result) => { 68 | return expect(result.css).toMatchFormattedCss(css` 69 | .grid-areas-\[left_right\2c left_right\] { 70 | grid-template-areas: 'left right' 'left right'; 71 | } 72 | `) 73 | }) 74 | }) 75 | 76 | it('ignores underscores/spaces around the comma', () => { 77 | let config = { 78 | content: [ 79 | { 80 | raw: html` 81 |
82 |
83 |
84 | `, 85 | }, 86 | ], 87 | } 88 | 89 | return run('@tailwind utilities', config).then((result) => { 90 | return expect(result.css).toMatchFormattedCss(css` 91 | .grid-areas-\[left_right\2c _left_right\] { 92 | grid-template-areas: 'left right' 'left right'; 93 | } 94 | 95 | .grid-areas-\[left_right_\2c _\._right\] { 96 | grid-template-areas: 'left right' '. right'; 97 | } 98 | 99 | .grid-areas-\[left_right_\2c left_right\] { 100 | grid-template-areas: 'left right' 'left right'; 101 | } 102 | `) 103 | }) 104 | }) 105 | 106 | it('generates grid-area', () => { 107 | let config = { 108 | content: [ 109 | { 110 | raw: html`
`, 111 | }, 112 | ], 113 | } 114 | 115 | return run('@tailwind utilities', config).then((result) => { 116 | return expect(result.css).toMatchFormattedCss(css` 117 | .grid-in-\[left\] { 118 | grid-area: left; 119 | } 120 | `) 121 | }) 122 | }) 123 | 124 | it('handles custom properties in arbitrary grid-areas', () => { 125 | let config = { 126 | content: [ 127 | { 128 | raw: html`
`, 129 | }, 130 | ], 131 | } 132 | 133 | return run('@tailwind utilities', config).then((result) => { 134 | return expect(result.css).toMatchFormattedCss(css` 135 | .grid-areas-\[var\(--grid-areas\)\] { 136 | grid-template-areas: var(--grid-areas); 137 | } 138 | `) 139 | }) 140 | }) 141 | 142 | it('handles custom properties mixed with regular properties in arbitrary grid-areas', () => { 143 | let config = { 144 | content: [ 145 | { 146 | raw: html`
`, 147 | }, 148 | ], 149 | } 150 | 151 | return run('@tailwind utilities', config).then((result) => { 152 | return expect(result.css).toMatchFormattedCss(css` 153 | .grid-areas-\[top_top\2c var\(--grid-areas\)\] { 154 | grid-template-areas: 'top top' var(--grid-areas); 155 | } 156 | `) 157 | }) 158 | }) 159 | -------------------------------------------------------------------------------- /test/css.test.js: -------------------------------------------------------------------------------- 1 | import { run } from './util/run' 2 | 3 | test('original utilities are still available', () => { 4 | let config = { 5 | content: [ 6 | { 7 | raw: ` 8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | `, 19 | }, 20 | ], 21 | theme: { 22 | extend: { 23 | gridTemplateAreas: { 24 | 'mobile-order-list': ['left right', 'left2 right2', 'notes notes'], 25 | }, 26 | gridTemplateColumns: { 27 | base: '240px 1fr', 28 | }, 29 | }, 30 | }, 31 | corePlugins: { preflight: false }, 32 | plugins: [require('../src/plugin')], 33 | } 34 | 35 | let input = ` 36 | @tailwind utilities; 37 | ` 38 | 39 | return run(input, config).then((result) => { 40 | expect(result.css).toMatchFormattedCss(` 41 | .grid { 42 | display: grid; 43 | } 44 | 45 | .grid-cols-2 { 46 | grid-template-columns: repeat(2, minmax(0, 1fr)); 47 | } 48 | 49 | .grid-cols-3 { 50 | grid-template-columns: repeat(3, minmax(0, 1fr)); 51 | } 52 | 53 | .grid-cols-\\[120px_120px_120px\\] { 54 | grid-template-columns: 120px 120px 120px; 55 | } 56 | 57 | .grid-cols-\\[120px_1fr\\] { 58 | grid-template-columns: 120px 1fr; 59 | } 60 | 61 | .grid-cols-\\[1fr_120px\\] { 62 | grid-template-columns: 1fr 120px; 63 | } 64 | 65 | .grid-cols-\\[minmax\\(100px\\2c 120px\\)_1fr\\] { 66 | grid-template-columns: minmax(100px,120px) 1fr; 67 | } 68 | 69 | .grid-cols-base { 70 | grid-template-columns: 240px 1fr; 71 | } 72 | 73 | .grid-areas-mobile-order-list { 74 | grid-template-areas: 'left right' 'left2 right2' 'notes notes'; 75 | } 76 | 77 | .grid-in-left { 78 | grid-area: left; 79 | } 80 | `) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /test/plugin.test.js: -------------------------------------------------------------------------------- 1 | import escapeClassName from 'tailwindcss/lib/util/escapeClassName' 2 | import plugin from '../src/plugin' 3 | import { expect, test } from '@jest/globals' 4 | 5 | function get(object, path, defaultValue) { 6 | let localePath = path 7 | if (typeof path === 'string') { 8 | localePath = path.split('.').map((key) => { 9 | const numKey = Number(key) 10 | return Number.isNaN(numKey) ? key : numKey 11 | }) 12 | } 13 | 14 | let result = object 15 | 16 | for (const key of localePath) { 17 | if (!result) { 18 | return defaultValue 19 | } 20 | 21 | result = result[key] 22 | } 23 | 24 | return result ?? defaultValue 25 | } 26 | 27 | test('returns default utilities', () => { 28 | const addedUtilities = [] 29 | 30 | const config = {} 31 | 32 | const getConfigValue = (path, defaultValue) => get(config, path, defaultValue) 33 | const pluginApi = { 34 | config: getConfigValue, 35 | e: escapeClassName, 36 | theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue), 37 | variants: (path, defaultValue) => { 38 | if (Array.isArray(config.variants)) { 39 | return config.variants 40 | } 41 | 42 | return getConfigValue(`variants.${path}`, defaultValue) 43 | }, 44 | addUtilities(utilities, variants) { 45 | addedUtilities.push({ 46 | utilities, 47 | variants, 48 | }) 49 | }, 50 | matchUtilities() {}, 51 | } 52 | 53 | plugin(pluginApi) 54 | 55 | expect(addedUtilities).toEqual( 56 | expect.arrayContaining([ 57 | expect.objectContaining({ 58 | utilities: expect.arrayContaining([ 59 | expect.objectContaining({ 60 | '.grid-areas-none': { 61 | 'grid-template-areas': 'none', 62 | }, 63 | '.grid-areas-inherit': { 64 | 'grid-template-areas': 'inherit', 65 | }, 66 | '.grid-areas-initial': { 67 | 'grid-template-areas': 'initial', 68 | }, 69 | '.grid-areas-revert': { 70 | 'grid-template-areas': 'revert', 71 | }, 72 | '.grid-areas-revert-layer': { 73 | 'grid-template-areas': 'revert-layer', 74 | }, 75 | '.grid-areas-unset': { 76 | 'grid-template-areas': 'unset', 77 | }, 78 | }), 79 | ]), 80 | }), 81 | 82 | expect.objectContaining({ 83 | utilities: expect.arrayContaining([ 84 | expect.objectContaining({ 85 | '.grid-in-auto': { 86 | 'grid-area': 'auto', 87 | }, 88 | '.grid-in-inherit': { 89 | 'grid-area': 'inherit', 90 | }, 91 | '.grid-in-initial': { 92 | 'grid-area': 'initial', 93 | }, 94 | '.grid-in-revert': { 95 | 'grid-area': 'revert', 96 | }, 97 | '.grid-in-revert-layer': { 98 | 'grid-area': 'revert-layer', 99 | }, 100 | '.grid-in-unset': { 101 | 'grid-area': 'unset', 102 | }, 103 | }), 104 | ]), 105 | }), 106 | ]) 107 | ) 108 | }) 109 | 110 | test('returns all utilities for grid areas', () => { 111 | const addedUtilities = [] 112 | 113 | const config = { 114 | theme: { 115 | gridTemplateAreas: { 116 | layout: ['first .', 'second second'], 117 | }, 118 | }, 119 | variants: { 120 | gridTemplateAreas: ['responsive'], 121 | }, 122 | } 123 | 124 | const getConfigValue = (path, defaultValue) => get(config, path, defaultValue) 125 | const pluginApi = { 126 | config: getConfigValue, 127 | e: escapeClassName, 128 | theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue), 129 | variants: (path, defaultValue) => { 130 | if (Array.isArray(config.variants)) { 131 | return config.variants 132 | } 133 | 134 | return getConfigValue(`variants.${path}`, defaultValue) 135 | }, 136 | addUtilities(utilities, variants) { 137 | addedUtilities.push({ 138 | utilities, 139 | variants, 140 | }) 141 | }, 142 | matchUtilities() {}, 143 | } 144 | 145 | plugin(pluginApi) 146 | 147 | expect(addedUtilities).toEqual( 148 | expect.arrayContaining([ 149 | expect.objectContaining({ 150 | utilities: expect.arrayContaining([ 151 | expect.objectContaining({ 152 | '.grid-areas-layout': { 153 | 'grid-template-areas': '"first ."\n"second second"', 154 | }, 155 | }), 156 | ]), 157 | variants: ['responsive'], 158 | }), 159 | 160 | expect.objectContaining({ 161 | utilities: expect.arrayContaining([ 162 | expect.objectContaining({ 163 | '.grid-in-first': { 164 | 'grid-area': 'first', 165 | }, 166 | '.grid-in-second': { 167 | 'grid-area': 'second', 168 | }, 169 | }), 170 | ]), 171 | variants: [], 172 | }), 173 | 174 | expect.objectContaining({ 175 | utilities: expect.arrayContaining([ 176 | expect.objectContaining({ 177 | '.row-start-first': { 178 | 'grid-row-start': 'first-start', 179 | }, 180 | '.row-end-first': { 181 | 'grid-row-end': 'first-end', 182 | }, 183 | '.col-start-first': { 184 | 'grid-column-start': 'first-start', 185 | }, 186 | '.col-end-first': { 187 | 'grid-column-end': 'first-end', 188 | }, 189 | '.row-start-second': { 190 | 'grid-row-start': 'second-start', 191 | }, 192 | '.row-end-second': { 193 | 'grid-row-end': 'second-end', 194 | }, 195 | '.col-start-second': { 196 | 'grid-column-start': 'second-start', 197 | }, 198 | '.col-end-second': { 199 | 'grid-column-end': 'second-end', 200 | }, 201 | }), 202 | ]), 203 | variants: [], 204 | }), 205 | ]) 206 | ) 207 | }) 208 | 209 | test('works for multiple grid templates', () => { 210 | const addedUtilities = [] 211 | 212 | const config = { 213 | theme: { 214 | gridTemplateAreas: { 215 | default: ['first .', 'second second'], 216 | slim: ['first', 'second'], 217 | }, 218 | }, 219 | variants: { 220 | gridTemplateAreas: ['responsive'], 221 | }, 222 | } 223 | 224 | const getConfigValue = (path, defaultValue) => get(config, path, defaultValue) 225 | const pluginApi = { 226 | config: getConfigValue, 227 | e: escapeClassName, 228 | theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue), 229 | variants: (path, defaultValue) => { 230 | if (Array.isArray(config.variants)) { 231 | return config.variants 232 | } 233 | 234 | return getConfigValue(`variants.${path}`, defaultValue) 235 | }, 236 | addUtilities(utilities, variants) { 237 | addedUtilities.push({ 238 | utilities, 239 | variants, 240 | }) 241 | }, 242 | matchUtilities() {}, 243 | } 244 | 245 | plugin(pluginApi) 246 | 247 | expect(addedUtilities).toEqual( 248 | expect.arrayContaining([ 249 | expect.objectContaining({ 250 | utilities: expect.arrayContaining([ 251 | expect.objectContaining({ 252 | '.grid-areas-default': { 253 | 'grid-template-areas': '"first ."\n"second second"', 254 | }, 255 | '.grid-areas-slim': { 256 | 'grid-template-areas': '"first"\n"second"', 257 | }, 258 | }), 259 | ]), 260 | variants: ['responsive'], 261 | }), 262 | 263 | expect.objectContaining({ 264 | utilities: expect.arrayContaining([ 265 | expect.objectContaining({ 266 | '.grid-in-first': { 267 | 'grid-area': 'first', 268 | }, 269 | '.grid-in-second': { 270 | 'grid-area': 'second', 271 | }, 272 | }), 273 | ]), 274 | variants: [], 275 | }), 276 | 277 | expect.objectContaining({ 278 | utilities: expect.arrayContaining([ 279 | expect.objectContaining({ 280 | '.row-start-first': { 281 | 'grid-row-start': 'first-start', 282 | }, 283 | '.row-end-first': { 284 | 'grid-row-end': 'first-end', 285 | }, 286 | '.col-start-first': { 287 | 'grid-column-start': 'first-start', 288 | }, 289 | '.col-end-first': { 290 | 'grid-column-end': 'first-end', 291 | }, 292 | '.row-start-second': { 293 | 'grid-row-start': 'second-start', 294 | }, 295 | '.row-end-second': { 296 | 'grid-row-end': 'second-end', 297 | }, 298 | '.col-start-second': { 299 | 'grid-column-start': 'second-start', 300 | }, 301 | '.col-end-second': { 302 | 'grid-column-end': 'second-end', 303 | }, 304 | }), 305 | ]), 306 | variants: [], 307 | }), 308 | ]) 309 | ) 310 | }) 311 | 312 | test('works for more than two rows', () => { 313 | const addedUtilities = [] 314 | 315 | const config = { 316 | theme: { 317 | gridTemplateAreas: { 318 | layout: ['first .', 'second second', 'third third'], 319 | }, 320 | }, 321 | variants: { 322 | gridTemplateAreas: ['responsive'], 323 | }, 324 | } 325 | 326 | const getConfigValue = (path, defaultValue) => get(config, path, defaultValue) 327 | const pluginApi = { 328 | config: getConfigValue, 329 | e: escapeClassName, 330 | theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue), 331 | variants: (path, defaultValue) => { 332 | if (Array.isArray(config.variants)) { 333 | return config.variants 334 | } 335 | 336 | return getConfigValue(`variants.${path}`, defaultValue) 337 | }, 338 | addUtilities(utilities, variants) { 339 | addedUtilities.push({ 340 | utilities, 341 | variants, 342 | }) 343 | }, 344 | matchUtilities() {}, 345 | } 346 | 347 | plugin(pluginApi) 348 | 349 | expect(addedUtilities).toEqual( 350 | expect.arrayContaining([ 351 | expect.objectContaining({ 352 | utilities: expect.arrayContaining([ 353 | expect.objectContaining({ 354 | '.grid-areas-layout': { 355 | 'grid-template-areas': '"first ."\n"second second"\n"third third"', 356 | }, 357 | }), 358 | ]), 359 | variants: ['responsive'], 360 | }), 361 | 362 | expect.objectContaining({ 363 | utilities: expect.arrayContaining([ 364 | expect.objectContaining({ 365 | '.grid-in-first': { 366 | 'grid-area': 'first', 367 | }, 368 | '.grid-in-second': { 369 | 'grid-area': 'second', 370 | }, 371 | '.grid-in-third': { 372 | 'grid-area': 'third', 373 | }, 374 | }), 375 | ]), 376 | variants: [], 377 | }), 378 | 379 | expect.objectContaining({ 380 | utilities: expect.arrayContaining([ 381 | expect.objectContaining({ 382 | '.row-start-first': { 383 | 'grid-row-start': 'first-start', 384 | }, 385 | '.row-end-first': { 386 | 'grid-row-end': 'first-end', 387 | }, 388 | '.col-start-first': { 389 | 'grid-column-start': 'first-start', 390 | }, 391 | '.col-end-first': { 392 | 'grid-column-end': 'first-end', 393 | }, 394 | '.row-start-second': { 395 | 'grid-row-start': 'second-start', 396 | }, 397 | '.row-end-second': { 398 | 'grid-row-end': 'second-end', 399 | }, 400 | '.col-start-second': { 401 | 'grid-column-start': 'second-start', 402 | }, 403 | '.col-end-second': { 404 | 'grid-column-end': 'second-end', 405 | }, 406 | '.row-start-third': { 407 | 'grid-row-start': 'third-start', 408 | }, 409 | '.row-end-third': { 410 | 'grid-row-end': 'third-end', 411 | }, 412 | '.col-start-third': { 413 | 'grid-column-start': 'third-start', 414 | }, 415 | '.col-end-third': { 416 | 'grid-column-end': 'third-end', 417 | }, 418 | }), 419 | ]), 420 | variants: [], 421 | }), 422 | ]) 423 | ) 424 | }) 425 | -------------------------------------------------------------------------------- /test/util/extractGridAreaNames.test.js: -------------------------------------------------------------------------------- 1 | import extractGridAreaNames from '../../src/util/extractGridAreaNames' 2 | 3 | test('passing nothing gives you an empty list', () => { 4 | expect(extractGridAreaNames()).toEqual([]) 5 | }) 6 | 7 | test('passing an empty object gives you an empty list', () => { 8 | expect(extractGridAreaNames({})).toEqual([]) 9 | }) 10 | 11 | test('passing an empty definition gives you an empty list', () => { 12 | const gridTemplateAreas = { 13 | layout: [], 14 | } 15 | 16 | expect(extractGridAreaNames(gridTemplateAreas)).toEqual([]) 17 | }) 18 | 19 | test('single grid area', () => { 20 | const gridTemplateAreas = { 21 | layout: ['abc'], 22 | } 23 | 24 | expect(extractGridAreaNames(gridTemplateAreas)).toEqual(['abc']) 25 | }) 26 | 27 | test('deduplicates names', () => { 28 | const gridTemplateAreas = { 29 | layout: ['abc abc'], 30 | } 31 | 32 | expect(extractGridAreaNames(gridTemplateAreas)).toEqual(['abc']) 33 | }) 34 | 35 | test('multiple areas', () => { 36 | const gridTemplateAreas = { 37 | layout: ['abc def'], 38 | } 39 | 40 | expect(extractGridAreaNames(gridTemplateAreas)).toEqual(['abc', 'def']) 41 | }) 42 | 43 | test('multiple rows', () => { 44 | const gridTemplateAreas = { 45 | layout: ['abc', 'def'], 46 | } 47 | 48 | expect(extractGridAreaNames(gridTemplateAreas)).toEqual(['abc', 'def']) 49 | }) 50 | 51 | test('excludes . (empty cell)', () => { 52 | const gridTemplateAreas = { 53 | layout: ['abc abc', 'def .'], 54 | } 55 | 56 | expect(extractGridAreaNames(gridTemplateAreas)).toEqual(['abc', 'def']) 57 | }) 58 | 59 | test('multiple layouts', () => { 60 | const gridTemplateAreas = { 61 | default: ['abc abc', 'def .'], 62 | large: ['abc abc abc', 'def hij hij'], 63 | } 64 | 65 | expect(extractGridAreaNames(gridTemplateAreas)).toEqual(['abc', 'def', 'hij']) 66 | }) 67 | -------------------------------------------------------------------------------- /test/util/run.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss' 2 | import tailwind from 'tailwindcss' 3 | 4 | export let map = JSON.stringify({ 5 | version: 3, 6 | file: null, 7 | sources: [], 8 | names: [], 9 | mappings: '', 10 | }) 11 | 12 | export function run(input, config, plugin = tailwind) { 13 | let { currentTestName, testPath } = expect.getState() 14 | let path = `${testPath}?test=${Buffer.from(currentTestName).toString('base64')}` 15 | 16 | return postcss(plugin(config)).process(input, { 17 | from: path, 18 | to: path, 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /test/util/strings.js: -------------------------------------------------------------------------------- 1 | export let css = String.raw 2 | export let html = String.raw 3 | export let javascript = String.raw 4 | --------------------------------------------------------------------------------