├── .gitignore ├── .release-it.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── index.js ├── package-lock.json ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "tagName": "v${version}", 4 | "requireCleanWorkingDir": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [4.0.0] - 2020-08-04 9 | 10 | ### Changed 11 | - Got rid of the internal hack we used in order to support variants, thanks to Tailwind’s `addComponents()` accepting a `variants` option since v1.5 12 | 13 | ## [3.0.0] - 2020-02-05 14 | 15 | ### Changed 16 | - Changed to use Tailwind 1.2’s new plugin definition syntax 17 | 18 | ## [2.1.0] - 2019-07-08 19 | 20 | ### Added 21 | - Added support for variants 22 | 23 | ## [2.0.0] - 2019-05-13 24 | 25 | No change since 2.0.0-beta.1 26 | 27 | ## [2.0.0-beta.1] - 2019-04-07 28 | 29 | ### Added 30 | - Tailwind 1.0.0 compatibility 31 | 32 | ### Changed 33 | - Moved the `triangles` config option to the `theme` object in your Tailwind config 34 | - Flattened `defaultOptions` and split it into the `defaultSize` and `defaultColor` options 35 | - Removed the default `direction`, it has to be set explicitly for each triangle now 36 | - The default `height` cannot be customized anymore; a triangle’s height is now always half its `size` by default, or it can be customized per triangle 37 | - Renamed the `prefix` option to `componentPrefix` 38 | 39 | ## [1.0.0] - 2019-02-17 40 | 41 | Initial release 42 | 43 | [Unreleased]: https://github.com/benface/tailwindcss-triangles/compare/v4.0.0...HEAD 44 | [4.0.0]: https://github.com/benface/tailwindcss-triangles/compare/v3.0.0...v4.0.0 45 | [3.0.0]: https://github.com/benface/tailwindcss-triangles/compare/v2.1.0...v3.0.0 46 | [2.1.0]: https://github.com/benface/tailwindcss-triangles/compare/v2.0.0...v2.1.0 47 | [2.0.0]: https://github.com/benface/tailwindcss-triangles/compare/v2.0.0-beta.1...v2.0.0 48 | [2.0.0-beta.1]: https://github.com/benface/tailwindcss-triangles/compare/v1.0.0...v2.0.0-beta.1 49 | [1.0.0]: https://github.com/benface/tailwindcss-triangles/releases/tag/v1.0.0 50 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # ISC License 2 | 3 | Copyright (c) Benoît Rouleau 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [BLACK LIVES MATTER](https://blacklivesmatters.carrd.co) 2 | 3 | ### Be aware. Be angry. Do better. Demand change. Show your support any way you can. Click on the link above to find protests, petitions, and other ways to help. DO NOT LET IT GO SILENT. 4 | 5 | # Triangles Plugin for Tailwind CSS 6 | 7 | ## Requirements 8 | 9 | This plugin requires Tailwind CSS 1.5 or later. If your project uses an older version of Tailwind, you should install the latest 2.x version of this plugin (`npm install tailwindcss-triangles@2.x`). 10 | 11 | ## Installation 12 | 13 | ```bash 14 | npm install tailwindcss-triangles 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```js 20 | // tailwind.config.js 21 | module.exports = { 22 | theme: { 23 | triangles: { // defaults to {} 24 | 'left': { 25 | direction: 'left', // one of 'left', 'right', 'up', 'down', 'left-up', 'left-down', 'right-up', and 'right-down' 26 | size: '1em', // defaults to defaultSize 27 | height: '0.5em', // defaults to half the size; has no effect on the diagonal directions (e.g. 'left-up') 28 | color: 'currentColor', // defaults to defaultColor 29 | }, 30 | }, 31 | }, 32 | variants: { 33 | triangles: ['responsive'], // defaults to [] 34 | }, 35 | plugins: [ 36 | require('tailwindcss-triangles')({ 37 | componentPrefix: 'c-', // defaults to 'c-' 38 | defaultSize: '1em', // defaults to '1em' 39 | defaultColor: 'currentColor', // defaults to 'currentColor' 40 | }), 41 | ], 42 | }; 43 | ``` 44 | 45 | The above configuration would generate the following CSS: 46 | 47 | ```css 48 | .c-triangle-left { 49 | width: 0; 50 | height: 0; 51 | border: 0; 52 | border-right: .5em solid currentColor; 53 | border-top: .5em solid transparent; 54 | border-bottom: .5em solid transparent; 55 | } 56 | 57 | @media (min-width: 640px) { 58 | .sm\\:c-triangle-left { 59 | width: 0; 60 | height: 0; 61 | border: 0; 62 | border-right: .5em solid currentColor; 63 | border-top: .5em solid transparent; 64 | border-bottom: .5em solid transparent; 65 | } 66 | } 67 | ``` 68 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin'); 2 | const _ = require('lodash'); 3 | const valueParser = require('postcss-value-parser'); 4 | 5 | const defaultOptions = { 6 | componentPrefix: 'c-', 7 | defaultSize: '1em', 8 | defaultColor: 'currentColor', 9 | }; 10 | 11 | module.exports = plugin.withOptions(function(options = {}) { 12 | return function({ theme, variants, e, addComponents }) { 13 | options = _.defaults({}, options, defaultOptions); 14 | 15 | const trianglesVariants = variants('triangles'); 16 | 17 | const trianglesComponents = _.fromPairs( 18 | _.map(theme('triangles'), function(value, modifier) { 19 | const triangle = _.defaults({}, value, { 20 | size: options.defaultSize, 21 | color: options.defaultColor, 22 | }); 23 | triangle.name = `triangle-${modifier}`; 24 | triangle.parsedSize = valueParser.unit(triangle.size); 25 | triangle.styles = { 26 | width: '0', 27 | height: '0', 28 | border: '0', 29 | }; 30 | 31 | if (_.includes(['left', 'right', 'up', 'down'], triangle.direction)) { 32 | if (triangle.height === undefined) { 33 | triangle.height = `${triangle.parsedSize.number / 2}${triangle.parsedSize.unit}`; 34 | } 35 | const solidBorderValue = `${triangle.height} solid ${triangle.color}`; 36 | const transparentBordersValue = `${triangle.parsedSize.number / 2}${triangle.parsedSize.unit} solid transparent`; 37 | switch (triangle.direction) { 38 | case 'left': triangle.styles.borderRight = solidBorderValue; break; 39 | case 'right': triangle.styles.borderLeft = solidBorderValue; break; 40 | case 'up': triangle.styles.borderBottom = solidBorderValue; break; 41 | case 'down': triangle.styles.borderTop = solidBorderValue; break; 42 | } 43 | if (triangle.direction === 'left' || triangle.direction === 'right') { 44 | triangle.styles.borderTop = transparentBordersValue; 45 | triangle.styles.borderBottom = transparentBordersValue; 46 | } 47 | if (triangle.direction === 'up' || triangle.direction === 'down') { 48 | triangle.styles.borderLeft = transparentBordersValue; 49 | triangle.styles.borderRight = transparentBordersValue; 50 | } 51 | } 52 | else { 53 | const borderWidth = `${triangle.parsedSize.number / Math.sqrt(2)}${triangle.parsedSize.unit}`; 54 | const solidBorderValue = `${borderWidth} solid ${triangle.color}`; 55 | const transparentBorderValue = `${borderWidth} solid transparent`; 56 | switch (triangle.direction) { 57 | case 'left-up': 58 | case 'up-left': 59 | triangle.styles.borderTop = solidBorderValue; 60 | triangle.styles.borderRight = transparentBorderValue; 61 | break; 62 | case 'left-down': 63 | case 'down-left': 64 | triangle.styles.borderBottom = solidBorderValue; 65 | triangle.styles.borderRight = transparentBorderValue; 66 | break; 67 | case 'right-up': 68 | case 'up-right': 69 | triangle.styles.borderTop = solidBorderValue; 70 | triangle.styles.borderLeft = transparentBorderValue; 71 | break; 72 | case 'right-down': 73 | case 'down-right': 74 | triangle.styles.borderBottom = solidBorderValue; 75 | triangle.styles.borderLeft = transparentBorderValue; 76 | break; 77 | } 78 | } 79 | 80 | return [ 81 | `.${e(`${options.componentPrefix}${triangle.name}`)}`, 82 | triangle.styles, 83 | ]; 84 | }) 85 | ); 86 | 87 | addComponents(trianglesComponents, trianglesVariants); 88 | }; 89 | }, function() { 90 | return { 91 | theme: { 92 | triangles: {}, 93 | }, 94 | variants: { 95 | triangles: [], 96 | }, 97 | }; 98 | }); 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailwindcss-triangles", 3 | "version": "4.0.0", 4 | "description": "Tailwind CSS plugin to generate custom triangle components", 5 | "author": "Benoît Rouleau ", 6 | "license": "ISC", 7 | "repository": "https://github.com/benface/tailwindcss-triangles.git", 8 | "bugs": "https://github.com/benface/tailwindcss-triangles/issues", 9 | "homepage": "https://github.com/benface/tailwindcss-triangles", 10 | "scripts": { 11 | "test": "jest", 12 | "release": "f(){ release-it $1 && github-release-from-changelog ;};f" 13 | }, 14 | "dependencies": { 15 | "lodash": "^4.17.19", 16 | "postcss-value-parser": "^4.1.0" 17 | }, 18 | "devDependencies": { 19 | "github-release-from-changelog": "^2.1.1", 20 | "jest": "^26.2.2", 21 | "jest-matcher-css": "^1.1.0", 22 | "postcss": "^7.0.32", 23 | "release-it": "^13.6.6", 24 | "tailwindcss": "^1.6.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const cssMatcher = require('jest-matcher-css'); 3 | const postcss = require('postcss'); 4 | const tailwindcss = require('tailwindcss'); 5 | const trianglesPlugin = require('./index.js'); 6 | 7 | const generatePluginCss = (config, pluginOptions = {}) => { 8 | return postcss( 9 | tailwindcss( 10 | _.merge({ 11 | theme: { 12 | screens: { 13 | 'sm': '640px', 14 | }, 15 | }, 16 | corePlugins: false, 17 | plugins: [ 18 | trianglesPlugin(pluginOptions), 19 | ], 20 | }, config) 21 | ) 22 | ) 23 | .process('@tailwind components', { 24 | from: undefined, 25 | }) 26 | .then(result => { 27 | return result.css; 28 | }); 29 | }; 30 | 31 | expect.extend({ 32 | toMatchCss: cssMatcher, 33 | }); 34 | 35 | test('there is no output by default', () => { 36 | return generatePluginCss().then(css => { 37 | expect(css).toMatchCss(``); 38 | }); 39 | }); 40 | 41 | test('only a direction is required to generate a triangle component', () => { 42 | return generatePluginCss({ 43 | theme: { 44 | triangles: { 45 | 'right': { 46 | direction: 'right', 47 | }, 48 | }, 49 | }, 50 | }).then(css => { 51 | expect(css).toMatchCss(` 52 | .c-triangle-right { 53 | width: 0; 54 | height: 0; 55 | border: 0; 56 | border-left: 0.5em solid currentColor; 57 | border-top: 0.5em solid transparent; 58 | border-bottom: 0.5em solid transparent; 59 | } 60 | `); 61 | }); 62 | }); 63 | 64 | test('the component prefix can be customized', () => { 65 | return generatePluginCss({ 66 | theme: { 67 | triangles: { 68 | 'right': { 69 | direction: 'right', 70 | }, 71 | }, 72 | }, 73 | }, { 74 | componentPrefix: '', 75 | }).then(css => { 76 | expect(css).toMatchCss(` 77 | .triangle-right { 78 | width: 0; 79 | height: 0; 80 | border: 0; 81 | border-left: 0.5em solid currentColor; 82 | border-top: 0.5em solid transparent; 83 | border-bottom: 0.5em solid transparent; 84 | } 85 | `); 86 | }); 87 | }); 88 | 89 | test('directions, sizes, heights, and colors can be customized', () => { 90 | return generatePluginCss({ 91 | theme: { 92 | triangles: { 93 | 'down': { 94 | direction: 'down', 95 | size: '24px', 96 | height: '8px', 97 | color: 'yellow', 98 | }, 99 | }, 100 | }, 101 | }).then(css => { 102 | expect(css).toMatchCss(` 103 | .c-triangle-down { 104 | width: 0; 105 | height: 0; 106 | border: 0; 107 | border-top: 8px solid yellow; 108 | border-left: 12px solid transparent; 109 | border-right: 12px solid transparent; 110 | } 111 | `); 112 | }); 113 | }); 114 | 115 | test('there are 8 possible directions', () => { 116 | return generatePluginCss({ 117 | theme: { 118 | triangles: { 119 | 'left': { 120 | direction: 'left', 121 | }, 122 | 'right': { 123 | direction: 'right', 124 | }, 125 | 'up': { 126 | direction: 'up', 127 | }, 128 | 'down': { 129 | direction: 'down', 130 | }, 131 | 'left-up': { 132 | direction: 'left-up', 133 | }, 134 | 'left-down': { 135 | direction: 'left-down', 136 | }, 137 | 'right-up': { 138 | direction: 'right-up', 139 | }, 140 | 'right-down': { 141 | direction: 'right-down', 142 | }, 143 | }, 144 | }, 145 | }).then(css => { 146 | expect(css).toMatchCss(` 147 | .c-triangle-left { 148 | width: 0; 149 | height: 0; 150 | border: 0; 151 | border-right: 0.5em solid currentColor; 152 | border-top: 0.5em solid transparent; 153 | border-bottom: 0.5em solid transparent; 154 | } 155 | .c-triangle-right { 156 | width: 0; 157 | height: 0; 158 | border: 0; 159 | border-left: 0.5em solid currentColor; 160 | border-top: 0.5em solid transparent; 161 | border-bottom: 0.5em solid transparent; 162 | } 163 | .c-triangle-up { 164 | width: 0; 165 | height: 0; 166 | border: 0; 167 | border-bottom: 0.5em solid currentColor; 168 | border-left: 0.5em solid transparent; 169 | border-right: 0.5em solid transparent; 170 | } 171 | .c-triangle-down { 172 | width: 0; 173 | height: 0; 174 | border: 0; 175 | border-top: 0.5em solid currentColor; 176 | border-left: 0.5em solid transparent; 177 | border-right: 0.5em solid transparent; 178 | } 179 | .c-triangle-left-up { 180 | width: 0; 181 | height: 0; 182 | border: 0; 183 | border-top: 0.7071067811865475em solid currentColor; 184 | border-right: 0.7071067811865475em solid transparent; 185 | } 186 | .c-triangle-left-down { 187 | width: 0; 188 | height: 0; 189 | border: 0; 190 | border-bottom: 0.7071067811865475em solid currentColor; 191 | border-right: 0.7071067811865475em solid transparent; 192 | } 193 | .c-triangle-right-up { 194 | width: 0; 195 | height: 0; 196 | border: 0; 197 | border-top: 0.7071067811865475em solid currentColor; 198 | border-left: 0.7071067811865475em solid transparent; 199 | } 200 | .c-triangle-right-down { 201 | width: 0; 202 | height: 0; 203 | border: 0; 204 | border-bottom: 0.7071067811865475em solid currentColor; 205 | border-left: 0.7071067811865475em solid transparent; 206 | } 207 | `); 208 | }); 209 | }); 210 | 211 | test('when the height of a triangle is not set, it defaults to half its size', () => { 212 | return generatePluginCss({ 213 | theme: { 214 | triangles: { 215 | 'default': { 216 | direction: 'right', 217 | size: '24px', 218 | }, 219 | }, 220 | }, 221 | }).then(css => { 222 | expect(css).toMatchCss(` 223 | .c-triangle-default { 224 | width: 0; 225 | height: 0; 226 | border: 0; 227 | border-left: 12px solid currentColor; 228 | border-top: 12px solid transparent; 229 | border-bottom: 12px solid transparent; 230 | } 231 | `); 232 | }); 233 | }); 234 | 235 | test('the default size and color are customizable and overridable', () => { 236 | return generatePluginCss({ 237 | theme: { 238 | triangles: { 239 | 'default': { 240 | direction: 'up', 241 | height: '2em', 242 | }, 243 | 'special-left': { 244 | direction: 'left', 245 | size: '10vw', 246 | height: '5vw', 247 | color: 'blue', 248 | }, 249 | }, 250 | }, 251 | }, { 252 | defaultSize: '2em', 253 | defaultColor: 'red', 254 | }).then(css => { 255 | expect(css).toMatchCss(` 256 | .c-triangle-default { 257 | width: 0; 258 | height: 0; 259 | border: 0; 260 | border-bottom: 2em solid red; 261 | border-left: 1em solid transparent; 262 | border-right: 1em solid transparent; 263 | } 264 | .c-triangle-special-left { 265 | width: 0; 266 | height: 0; 267 | border: 0; 268 | border-right: 5vw solid blue; 269 | border-top: 5vw solid transparent; 270 | border-bottom: 5vw solid transparent; 271 | } 272 | `); 273 | }); 274 | }); 275 | 276 | test('variants are supported', () => { 277 | return generatePluginCss({ 278 | theme: { 279 | triangles: { 280 | 'right': { 281 | direction: 'right', 282 | }, 283 | }, 284 | }, 285 | variants: { 286 | triangles: ['responsive', 'hover'], 287 | }, 288 | }).then(css => { 289 | expect(css).toMatchCss(` 290 | .c-triangle-right { 291 | width: 0; 292 | height: 0; 293 | border: 0; 294 | border-left: 0.5em solid currentColor; 295 | border-top: 0.5em solid transparent; 296 | border-bottom: 0.5em solid transparent; 297 | } 298 | .hover\\:c-triangle-right:hover { 299 | width: 0; 300 | height: 0; 301 | border: 0; 302 | border-left: 0.5em solid currentColor; 303 | border-top: 0.5em solid transparent; 304 | border-bottom: 0.5em solid transparent; 305 | } 306 | @media (min-width: 640px) { 307 | .sm\\:c-triangle-right { 308 | width: 0; 309 | height: 0; 310 | border: 0; 311 | border-left: 0.5em solid currentColor; 312 | border-top: 0.5em solid transparent; 313 | border-bottom: 0.5em solid transparent; 314 | } 315 | .sm\\:hover\\:c-triangle-right:hover { 316 | width: 0; 317 | height: 0; 318 | border: 0; 319 | border-left: 0.5em solid currentColor; 320 | border-top: 0.5em solid transparent; 321 | border-bottom: 0.5em solid transparent; 322 | } 323 | } 324 | `); 325 | }); 326 | }); 327 | --------------------------------------------------------------------------------